Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
name: ci
name: CI

on:
workflow_dispatch:
push:
branches: [ main, feat/**, fix/**, chore/** ]
pull_request:
branches: [ main, feat/**, fix/**, chore/** ]

jobs:
build-and-test:
if: ${{ false }}
runs-on: ubuntu-22.04
name: Build and Test (macOS)
runs-on: macos-latest

steps:
- run: echo "CI temporarily disabled"
- name: Checkout
uses: actions/checkout@v4

- name: Install dependencies (macOS)
run: |
brew update
brew install cmake ninja openssl@3
echo "OPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" >> $GITHUB_ENV
echo "OPENSSL_INCLUDE_DIR=$(brew --prefix openssl@3)/include" >> $GITHUB_ENV
echo "OPENSSL_LIB_DIR=$(brew --prefix openssl@3)/lib" >> $GITHUB_ENV

- name: Configure (CMake)
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DOPENSSL_ROOT_DIR="${OPENSSL_ROOT_DIR}"

- name: Build
run: cmake --build build -j

- name: Run tests
run: ctest --test-dir build --output-on-failure
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/core/block.cpp)
list(APPEND ASTRO_CORE_SOURCES src/core/block.cpp)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/core/merkle.cpp)
list(APPEND ASTRO_CORE_SOURCES src/core/merkle.cpp)
endif()
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/src/net/p2p.cpp)
list(APPEND ASTRO_CORE_SOURCES src/net/p2p.cpp)
endif()
Expand Down
3 changes: 3 additions & 0 deletions include/astro/core/hash.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ namespace astro::core {
return hash160(std::span<const uint8_t>(
reinterpret_cast<const uint8_t*>(data.data()), data.size()));
}
// inline auto hash_concat(const Hash256& left, const Hash256& right) -> Hash256 {
// return hash_concat(std::span<const uint8_t>(left.data(), left.size()), std::span<const uint8_t>(right.data(), right.size()));
// }

auto toHex(std::span<const uint8_t> data) -> std::string;
inline std::string to_hex(std::span<const uint8_t> data) { return toHex(data); }
Expand Down
27 changes: 27 additions & 0 deletions include/astro/core/merkle.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#pragma once
#include <vector>
#include <span>
#include <cstdint>
#include <astro/core/hash.hpp>

namespace astro::core {

struct ProofStep {
Hash256 sibling;
bool sibling_on_left;
};

struct MerkleProof {
std::vector<ProofStep> steps;
};

Hash256 root(const std::vector<Hash256>& leaves);

MerkleProof build_proof(const std::vector<Hash256>& leaves, size_t index);

bool verify_proof(std::span<const uint8_t> leaf_hash, const MerkleProof& proof, const Hash256& expected_root);

inline bool verify_proof(const Hash256& leaf_hash, const MerkleProof& proof, const Hash256& root) {
return verify_proof(std::span<const uint8_t>(leaf_hash.data(), leaf_hash.size()), proof, root);
}
}
4 changes: 4 additions & 0 deletions include/astro/core/serializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ namespace astro::core {
void write_u32(uint32_t value) { write_le_value(value); }
void write_u64(uint64_t value) { write_le_value(value); }

void write_raw(std::span<const uint8_t> bytes) {
buffer_.insert(buffer_.end(), bytes.begin(), bytes.end());
}

void write_bytes(std::span<const uint8_t> bytes) {
write_u32(static_cast<uint32_t>(bytes.size()));
buffer_.insert(buffer_.end(), bytes.begin(), bytes.end());
Expand Down
38 changes: 9 additions & 29 deletions src/core/block.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#include "astro/core/block.hpp"
#include "astro/core/serializer.hpp"
#include "astro/core/hash.hpp"
#include "astro/core/merkle.hpp"

#include <chrono>
#include <cstring>

namespace astro::core {
Expand All @@ -11,11 +11,9 @@ namespace astro::core {
ByteWriter writer;
writer.write_u32(version);

writer.write_u32(32);
writer.write_bytes(std::span<const uint8_t>(prev_hash.data(), prev_hash.size()));
writer.write_raw(std::span<const uint8_t>(prev_hash.data(), prev_hash.size()));

writer.write_u32(32);
writer.write_bytes(std::span<const uint8_t>(merkle_root.data(), merkle_root.size()));
writer.write_raw(std::span<const uint8_t>(merkle_root.data(), merkle_root.size()));

writer.write_u64(timestamp);
writer.write_u64(nonce);
Expand All @@ -31,14 +29,13 @@ namespace astro::core {
ByteWriter writer;

auto header_bytes = header.serialize();
writer.write_u32(static_cast<uint32_t>(header_bytes.size()));
writer.write_bytes(std::span<const uint8_t>(header_bytes.data(), header_bytes.size()));
writer.write_raw(std::span<const uint8_t>(header_bytes.data(), header_bytes.size()));

writer.write_u32(static_cast<uint32_t>(transactions.size()));
for (const auto& tx : transactions) {
auto tx_bytes = tx.serialize(false);
writer.write_u32(static_cast<uint32_t>(tx_bytes.size()));
writer.write_bytes(std::span<const uint8_t>(tx_bytes.data(), tx_bytes.size()));
writer.write_raw(std::span<const uint8_t>(tx_bytes.data(), tx_bytes.size()));
}
return writer.take();
}
Expand All @@ -49,27 +46,10 @@ namespace astro::core {
}

Hash256 compute_merkle_root(const std::vector<Transaction>& transactions) {
if (transactions.empty()) return empty_merkle_root();

std::vector<Hash256> level_hashes;
level_hashes.reserve(transactions.size());
for (const auto& tx : transactions) level_hashes.push_back(tx.tx_hash());

while (level_hashes.size() > 1) {
std::vector<Hash256> next_level;
next_level.reserve(level_hashes.size() + 1 / 2);
for (size_t i = 0; i < level_hashes.size(); i += 2) {
const Hash256& left = level_hashes[i];
const Hash256& right = (i + 1 < level_hashes.size()) ? level_hashes[i + 1] : level_hashes[i];
Hash256 pair_hash = hash_concat(
std::span<const uint8_t>(left.data(), left.size()),
std::span<const uint8_t>(right.data(), right.size())
);
next_level.push_back(pair_hash);
}
level_hashes.swap(next_level);
}
return level_hashes.front();
std::vector<Hash256> leaves;
leaves.reserve(transactions.size());
for (const auto& tx : transactions) leaves.push_back(tx.tx_hash());
return root(leaves);
}

Block make_genesis_block(std::string genesis_note, uint64_t unix_time) {
Expand Down
112 changes: 112 additions & 0 deletions src/core/merkle.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#include "astro/core/merkle.hpp"
#include "astro/core/hash.hpp"
#include <cassert>
#include <cstring>

namespace astro::core {

static Hash256 hash_pair(std::span<const uint8_t> left, std::span<const uint8_t> right) {
return hash_concat(left, right);
}

static Hash256 empty_root() {
const uint8_t* empty_hash = nullptr;
return sha256(std::span<const uint8_t>(empty_hash, static_cast<size_t>(0)));
}

Hash256 root(const std::vector<Hash256>& leaves) {
if (leaves.empty()) return empty_root();
if (leaves.size() == 1) {
const Hash256& only = leaves.front();
return hash_pair(
std::span<const uint8_t>(only.data(), only.size()),
std::span<const uint8_t>(only.data(), only.size())
);
}
std::vector<Hash256> level_hashes = leaves;

while (level_hashes.size() > 1) {
std::vector<Hash256> next_level;
next_level.reserve((level_hashes.size() + 1) / 2);
for (size_t i = 0; i < level_hashes.size(); i += 2) {
const Hash256& left = level_hashes[i];
const Hash256& right = (i + 1 < level_hashes.size()) ? level_hashes[i + 1] : level_hashes[i];
next_level.push_back(hash_pair(
std::span<const uint8_t>(left.data(), left.size()),
std::span<const uint8_t>(right.data(), right.size())
));
}
level_hashes.swap(next_level);
}
return level_hashes.front();
}

MerkleProof build_proof(const std::vector<Hash256>& leaves, size_t index) {
MerkleProof proof{};
if (leaves.empty()) return proof;
assert(index < leaves.size());

std::vector<Hash256> level_hashes = leaves;
size_t proof_index = index;

while (level_hashes.size() > 1) {
size_t last_index = level_hashes.size() - 1;
size_t sibling_index = (proof_index % 2 == 0) ? (proof_index + 1 <= last_index ? proof_index + 1 : proof_index) :
(proof_index - 1);
bool sibling_on_left = (proof_index % 2 == 1);

proof.steps.push_back({
level_hashes[sibling_index],
sibling_on_left
});

proof_index = proof_index / 2;

std::vector<Hash256> next_level;
next_level.reserve((level_hashes.size() + 1) / 2);
for (size_t i = 0; i < level_hashes.size(); i += 2) {
const Hash256& left = level_hashes[i];
const Hash256& right = (i + 1 < level_hashes.size()) ? level_hashes[i + 1] : level_hashes[i];
next_level.push_back(hash_pair(
std::span<const uint8_t>(left.data(), left.size()),
std::span<const uint8_t>(right.data(), right.size())
));
}
level_hashes.swap(next_level);
}
return proof;
}

bool verify_proof(std::span<const uint8_t> leaf_hash, const MerkleProof& proof, const Hash256& expected_root) {
Hash256 current_hash{};
if (leaf_hash.size() == current_hash.size()) {
std::memcpy(current_hash.data(), leaf_hash.data(), current_hash.size());
} else {
current_hash = sha256(leaf_hash);
}

if (proof.steps.empty()) {
// Single-leaf tree: root is H(leaf || leaf)
Hash256 dup = hash_pair(
std::span<const uint8_t>(current_hash.data(), current_hash.size()),
std::span<const uint8_t>(current_hash.data(), current_hash.size())
);
return std::equal(dup.begin(), dup.end(), expected_root.begin(), expected_root.end());
}

for (const auto& step : proof.steps) {
if (step.sibling_on_left) {
current_hash = hash_pair(
std::span<const uint8_t>(step.sibling.data(), step.sibling.size()),
std::span<const uint8_t>(current_hash.data(), current_hash.size())
);
} else {
current_hash = hash_pair(
std::span<const uint8_t>(current_hash.data(), current_hash.size()),
std::span<const uint8_t>(step.sibling.data(), step.sibling.size())
);
}
}
return std::equal(current_hash.begin(), current_hash.end(), expected_root.begin(), expected_root.end());
}
}
60 changes: 59 additions & 1 deletion src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <astro/core/transaction.hpp>
#include <astro/core/serializer.hpp>
#include <astro/core/block.hpp>
#include <astro/core/merkle.hpp>
#include <chrono>

using namespace astro::core;
Expand All @@ -19,13 +20,16 @@ static void print_usage() {
"Usage:\n"
" astro-node demo-keys [--curve CURVE] [--message MESSAGE]\n"
" astro-node demo-tx [--amount N] [--nonce N] [--to LABEL]\n"
" astro-node demo-genesis\n\n"
" astro-node demo-genesis\n"
" astro-node demo-merkle [--leaves CSV] [--index N]\n\n"
"Options:\n"
" --curve EC curve name (default: secp256k1)\n"
" --message Message to sign (default: 'astro demo')\n"
" --amount Transaction amount (default: 123)\n"
" --nonce Transaction nonce (default: 1)\n"
" --to Recipient label (default: 'demo-recipient')\n"
" --leaves CSV of leaf strings (default: a,b,c,d,e)\n"
" --index Leaf index for proof (default: 0)\n"
);
}

Expand Down Expand Up @@ -160,6 +164,60 @@ int main(int argc, char** argv) {
return 0;
}

if (command == "demo-merkle") {
std::string csv = "a,b,c,d,e";
size_t index = 0;
for (int i = 2; i < argc; ++i) {
std::string arg = argv[i];
if (arg.rfind("--leaves=", 0) == 0) {
csv = arg.substr(9);
} else if (arg == "--leaves" && i + 1 < argc) {
csv = argv[++i];
} else if (arg.rfind("--index=", 0) == 0) {
index = static_cast<size_t>(std::stoull(arg.substr(8)));
} else if (arg == "--index" && i + 1 < argc) {
index = static_cast<size_t>(std::stoull(argv[++i]));
} else if (arg == "-h" || arg == "--help") {
print_usage();
return 0;
} else {
std::fprintf(stderr, "Unknown option: %s\n", arg.c_str());
print_usage();
return 1;
}
}
std::vector<std::string> parts;
{
std::string cur;
for (char c : csv) {
if (c == ',') { parts.push_back(cur); cur.clear(); }
else { cur.push_back(c); }
}
parts.push_back(cur);
}
if (parts.empty()) {
std::fprintf(stderr, "No leaves provided\n");
return 1;
}
std::vector<Hash256> leaves;
leaves.reserve(parts.size());
for (const auto& s : parts) leaves.push_back(sha256(s));
if (index >= leaves.size()) index = leaves.size() - 1;
auto R = root(leaves);
std::cout << "leaves: " << leaves.size() << "\n";
std::cout << "root: " << to_hex(std::span<const uint8_t>(R.data(), R.size())) << "\n";
auto proof = build_proof(leaves, index);
std::cout << "proof.steps: " << proof.steps.size() << "\n";
bool ok = verify_proof(leaves[index], proof, R);
std::cout << "verify[" << index << "]: " << (ok ? "OK" : "FAIL") << "\n";
if (!parts[index].empty()) {
auto wrong = sha256(std::string(parts[index].size(), parts[index][0] == 'A' ? 'B' : 'A'));
bool bad = verify_proof(wrong, proof, R);
std::cout << "verify tampered: " << (bad ? "UNEXPECTED_OK" : "EXPECTED_FAIL") << "\n";
}
return 0;
}

print_usage();
return 0;
}
Loading
Loading