From 15606382f6989f462b5fee1e581724059a74d5dc Mon Sep 17 00:00:00 2001 From: Emmanuel Atawodi Date: Sun, 9 Nov 2025 14:33:00 +0100 Subject: [PATCH 1/2] feat(core/merkle/serialize): raw-concat Merkle + non-prefixed 32B hashes --- CMakeLists.txt | 3 + include/astro/core/hash.hpp | 3 + include/astro/core/merkle.hpp | 27 +++++++ include/astro/core/serializer.hpp | 4 ++ src/core/block.cpp | 38 +++------- src/core/merkle.cpp | 112 ++++++++++++++++++++++++++++++ src/main.cpp | 60 +++++++++++++++- tests/test_block_serialize.cpp | 104 +++++++++++++++++++++++++++ tests/test_hash_concat.cpp | 63 +++++++++++++++++ tests/test_merkle.cpp | 61 ++++++++++++++++ tests/test_serializer.cpp | 24 +++++++ tests/test_transaction.cpp | 3 - 12 files changed, 469 insertions(+), 33 deletions(-) create mode 100644 include/astro/core/merkle.hpp create mode 100644 src/core/merkle.cpp create mode 100644 tests/test_block_serialize.cpp create mode 100644 tests/test_hash_concat.cpp create mode 100644 tests/test_merkle.cpp create mode 100644 tests/test_serializer.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5b126a5..c8b2234 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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() diff --git a/include/astro/core/hash.hpp b/include/astro/core/hash.hpp index c026365..9f263b1 100644 --- a/include/astro/core/hash.hpp +++ b/include/astro/core/hash.hpp @@ -19,6 +19,9 @@ namespace astro::core { return hash160(std::span( reinterpret_cast(data.data()), data.size())); } + // inline auto hash_concat(const Hash256& left, const Hash256& right) -> Hash256 { + // return hash_concat(std::span(left.data(), left.size()), std::span(right.data(), right.size())); + // } auto toHex(std::span data) -> std::string; inline std::string to_hex(std::span data) { return toHex(data); } diff --git a/include/astro/core/merkle.hpp b/include/astro/core/merkle.hpp new file mode 100644 index 0000000..4e95667 --- /dev/null +++ b/include/astro/core/merkle.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include +#include +#include + +namespace astro::core { + + struct ProofStep { + Hash256 sibling; + bool sibling_on_left; + }; + + struct MerkleProof { + std::vector steps; + }; + + Hash256 root(const std::vector& leaves); + + MerkleProof build_proof(const std::vector& leaves, size_t index); + + bool verify_proof(std::span 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(leaf_hash.data(), leaf_hash.size()), proof, root); + } +} \ No newline at end of file diff --git a/include/astro/core/serializer.hpp b/include/astro/core/serializer.hpp index 5d056b7..2f9578b 100644 --- a/include/astro/core/serializer.hpp +++ b/include/astro/core/serializer.hpp @@ -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 bytes) { + buffer_.insert(buffer_.end(), bytes.begin(), bytes.end()); + } + void write_bytes(std::span bytes) { write_u32(static_cast(bytes.size())); buffer_.insert(buffer_.end(), bytes.begin(), bytes.end()); diff --git a/src/core/block.cpp b/src/core/block.cpp index 3be8a4d..b8bf9b2 100644 --- a/src/core/block.cpp +++ b/src/core/block.cpp @@ -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 #include namespace astro::core { @@ -11,11 +11,9 @@ namespace astro::core { ByteWriter writer; writer.write_u32(version); - writer.write_u32(32); - writer.write_bytes(std::span(prev_hash.data(), prev_hash.size())); + writer.write_raw(std::span(prev_hash.data(), prev_hash.size())); - writer.write_u32(32); - writer.write_bytes(std::span(merkle_root.data(), merkle_root.size())); + writer.write_raw(std::span(merkle_root.data(), merkle_root.size())); writer.write_u64(timestamp); writer.write_u64(nonce); @@ -31,14 +29,13 @@ namespace astro::core { ByteWriter writer; auto header_bytes = header.serialize(); - writer.write_u32(static_cast(header_bytes.size())); - writer.write_bytes(std::span(header_bytes.data(), header_bytes.size())); + writer.write_raw(std::span(header_bytes.data(), header_bytes.size())); writer.write_u32(static_cast(transactions.size())); for (const auto& tx : transactions) { auto tx_bytes = tx.serialize(false); writer.write_u32(static_cast(tx_bytes.size())); - writer.write_bytes(std::span(tx_bytes.data(), tx_bytes.size())); + writer.write_raw(std::span(tx_bytes.data(), tx_bytes.size())); } return writer.take(); } @@ -49,27 +46,10 @@ namespace astro::core { } Hash256 compute_merkle_root(const std::vector& transactions) { - if (transactions.empty()) return empty_merkle_root(); - - std::vector 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 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(left.data(), left.size()), - std::span(right.data(), right.size()) - ); - next_level.push_back(pair_hash); - } - level_hashes.swap(next_level); - } - return level_hashes.front(); + std::vector 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) { diff --git a/src/core/merkle.cpp b/src/core/merkle.cpp new file mode 100644 index 0000000..8f765a9 --- /dev/null +++ b/src/core/merkle.cpp @@ -0,0 +1,112 @@ +#include "astro/core/merkle.hpp" +#include "astro/core/hash.hpp" +#include +#include + +namespace astro::core { + + static Hash256 hash_pair(std::span left, std::span right) { + return hash_concat(left, right); + } + + static Hash256 empty_root() { + const uint8_t* empty_hash = nullptr; + return sha256(std::span(empty_hash, static_cast(0))); + } + + Hash256 root(const std::vector& leaves) { + if (leaves.empty()) return empty_root(); + if (leaves.size() == 1) { + const Hash256& only = leaves.front(); + return hash_pair( + std::span(only.data(), only.size()), + std::span(only.data(), only.size()) + ); + } + std::vector level_hashes = leaves; + + while (level_hashes.size() > 1) { + std::vector 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(left.data(), left.size()), + std::span(right.data(), right.size()) + )); + } + level_hashes.swap(next_level); + } + return level_hashes.front(); + } + + MerkleProof build_proof(const std::vector& leaves, size_t index) { + MerkleProof proof{}; + if (leaves.empty()) return proof; + assert(index < leaves.size()); + + std::vector 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 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(left.data(), left.size()), + std::span(right.data(), right.size()) + )); + } + level_hashes.swap(next_level); + } + return proof; + } + + bool verify_proof(std::span 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(current_hash.data(), current_hash.size()), + std::span(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(step.sibling.data(), step.sibling.size()), + std::span(current_hash.data(), current_hash.size()) + ); + } else { + current_hash = hash_pair( + std::span(current_hash.data(), current_hash.size()), + std::span(step.sibling.data(), step.sibling.size()) + ); + } + } + return std::equal(current_hash.begin(), current_hash.end(), expected_root.begin(), expected_root.end()); + } +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 7453f01..a5eebaf 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include using namespace astro::core; @@ -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" ); } @@ -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(std::stoull(arg.substr(8))); + } else if (arg == "--index" && i + 1 < argc) { + index = static_cast(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 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 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(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; } \ No newline at end of file diff --git a/tests/test_block_serialize.cpp b/tests/test_block_serialize.cpp new file mode 100644 index 0000000..206c0d3 --- /dev/null +++ b/tests/test_block_serialize.cpp @@ -0,0 +1,104 @@ +#include +#include "astro/core/block.hpp" +#include "astro/core/hash.hpp" +#include +# +using namespace astro::core; +# +static uint32_t read_le_u32(const std::vector& buf, size_t offset) { + return static_cast(buf[offset]) | + (static_cast(buf[offset+1]) << 8) | + (static_cast(buf[offset+2]) << 16) | + (static_cast(buf[offset+3]) << 24); +} +# +static uint64_t read_le_u64(const std::vector& buf, size_t offset) { + uint64_t v = 0; + for (int i = 0; i < 8; ++i) v |= (static_cast(buf[offset + i]) << (8 * i)); + return v; +} +# +TEST(BlockHeaderSerialize, UsesRaw32ByteHashes_NoLengthPrefix) { + BlockHeader header; + header.version = 0x01020304u; + for (size_t i = 0; i < header.prev_hash.size(); ++i) header.prev_hash[i] = static_cast(i); + for (size_t i = 0; i < header.merkle_root.size(); ++i) header.merkle_root[i] = static_cast(0xFF - i); + header.timestamp = 0x0102030405060708ULL; + header.nonce = 0xA1A2A3A4A5A6A7A8ULL; +# + auto bytes = header.serialize(); + ASSERT_EQ(bytes.size(), 84u); // 4 + 32 + 32 + 8 + 8 +# + EXPECT_EQ(read_le_u32(bytes, 0), header.version); + EXPECT_TRUE(std::equal(bytes.begin() + 4, bytes.begin() + 36, header.prev_hash.begin())); + EXPECT_TRUE(std::equal(bytes.begin() + 36, bytes.begin() + 68, header.merkle_root.begin())); + EXPECT_EQ(read_le_u64(bytes, 68), header.timestamp); + EXPECT_EQ(read_le_u64(bytes, 76), header.nonce); +} +# +TEST(BlockSerialize, HeaderIsRawPrefix_AndTxsLengthPrefixed) { + // Build a header and two simple transactions (no signatures needed) + Block block; + block.header.version = 2; + block.header.prev_hash = {}; + block.header.merkle_root = {}; + block.header.timestamp = 123456789ULL; + block.header.nonce = 42ULL; +# + Transaction tx1; + tx1.version = 1; tx1.nonce = 1; tx1.amount = 10; + tx1.from_pub_pem = {}; tx1.to_label = "a"; +# + Transaction tx2; + tx2.version = 1; tx2.nonce = 2; tx2.amount = 20; + tx2.from_pub_pem = {}; tx2.to_label = "bb"; +# + block.transactions = {tx1, tx2}; +# + auto header_bytes = block.header.serialize(); + auto tx1_bytes = tx1.serialize(false); + auto tx2_bytes = tx2.serialize(false); +# + auto block_bytes = block.serialize(); +# + // Prefix must equal header bytes exactly + ASSERT_GE(block_bytes.size(), header_bytes.size() + 4u); + EXPECT_TRUE(std::equal(block_bytes.begin(), block_bytes.begin() + header_bytes.size(), header_bytes.begin())); +# + // Next u32 is number of txs + size_t offset = header_bytes.size(); + ASSERT_EQ(read_le_u32(block_bytes, offset), 2u); + offset += 4; +# + // First tx + ASSERT_EQ(read_le_u32(block_bytes, offset), tx1_bytes.size()); + offset += 4; + ASSERT_TRUE(std::equal(block_bytes.begin() + offset, block_bytes.begin() + offset + tx1_bytes.size(), tx1_bytes.begin())); + offset += tx1_bytes.size(); +# + // Second tx + ASSERT_EQ(read_le_u32(block_bytes, offset), tx2_bytes.size()); + offset += 4; + ASSERT_TRUE(std::equal(block_bytes.begin() + offset, block_bytes.begin() + offset + tx2_bytes.size(), tx2_bytes.begin())); + offset += tx2_bytes.size(); +# + // Tail consumed + EXPECT_EQ(offset, block_bytes.size()); +} +# +TEST(BlockSerialize, ZeroTransactions) { + Block block; + block.header.version = 1; + block.header.prev_hash = {}; + block.header.merkle_root = {}; + block.header.timestamp = 0; + block.header.nonce = 0; + block.transactions = {}; +# + auto header_bytes = block.header.serialize(); + auto block_bytes = block.serialize(); +# + ASSERT_EQ(block_bytes.size(), header_bytes.size() + 4u); + EXPECT_TRUE(std::equal(block_bytes.begin(), block_bytes.begin() + header_bytes.size(), header_bytes.begin())); + EXPECT_EQ(read_le_u32(block_bytes, header_bytes.size()), 0u); +} diff --git a/tests/test_hash_concat.cpp b/tests/test_hash_concat.cpp new file mode 100644 index 0000000..69fc6bc --- /dev/null +++ b/tests/test_hash_concat.cpp @@ -0,0 +1,63 @@ +#include +#include "astro/core/hash.hpp" +# +using namespace astro::core; +# +static std::vector concat_vecs(const std::vector& a, const std::vector& b) { + std::vector out; + out.reserve(a.size() + b.size()); + out.insert(out.end(), a.begin(), a.end()); + out.insert(out.end(), b.begin(), b.end()); + return out; +} +# +TEST(HashConcat, HandlesEmptyInputs) { + std::vector empty; + auto got = hash_concat(std::span(empty.data(), empty.size()), + std::span(empty.data(), empty.size())); + auto expected = sha256(std::span(empty.data(), empty.size())); + EXPECT_EQ(to_hex(std::span(got.data(), got.size())), + to_hex(std::span(expected.data(), expected.size()))); +} +# +TEST(HashConcat, LeftEmptyEqualsSha256Right) { + std::vector empty; + std::vector right{'a','b','c'}; + auto got = hash_concat(std::span(empty.data(), empty.size()), + std::span(right.data(), right.size())); + auto expected = sha256(std::span(right.data(), right.size())); + EXPECT_EQ(to_hex(std::span(got.data(), got.size())), + to_hex(std::span(expected.data(), expected.size()))); +} +# +TEST(HashConcat, RightEmptyEqualsSha256Left) { + std::vector left{'x','y'}; + std::vector empty; + auto got = hash_concat(std::span(left.data(), left.size()), + std::span(empty.data(), empty.size())); + auto expected = sha256(std::span(left.data(), left.size())); + EXPECT_EQ(to_hex(std::span(got.data(), got.size())), + to_hex(std::span(expected.data(), expected.size()))); +} +# +TEST(HashConcat, MatchesManualConcatThenSha256) { + std::vector left{'1','2','3'}; + std::vector right{'4','5'}; + auto got = hash_concat(std::span(left.data(), left.size()), + std::span(right.data(), right.size())); + auto combined = concat_vecs(left, right); + auto expected = sha256(std::span(combined.data(), combined.size())); + EXPECT_EQ(to_hex(std::span(got.data(), got.size())), + to_hex(std::span(expected.data(), expected.size()))); +} +# +TEST(HashConcat, OrderMatters) { + std::vector left{'A'}; + std::vector right{'B'}; + auto ab = hash_concat(std::span(left.data(), left.size()), + std::span(right.data(), right.size())); + auto ba = hash_concat(std::span(right.data(), right.size()), + std::span(left.data(), left.size())); + EXPECT_NE(to_hex(std::span(ab.data(), ab.size())), + to_hex(std::span(ba.data(), ba.size()))); +} diff --git a/tests/test_merkle.cpp b/tests/test_merkle.cpp new file mode 100644 index 0000000..8c759c2 --- /dev/null +++ b/tests/test_merkle.cpp @@ -0,0 +1,61 @@ +#include +#include "astro/core/merkle.hpp" +#include "astro/core/hash.hpp" + +using namespace astro::core; + +static Hash256 hash_of(const std::string& s) { + return sha256(s); +} + +TEST(Merkle, EmptyAndSingle) { + std::vector leaves; + auto root_empty = root(leaves); + // empty root should equal sha256("") + const uint8_t* p=nullptr; + auto empty = sha256(std::span(p, (size_t)0)); + EXPECT_EQ(to_hex(std::span(root_empty.data(), root_empty.size())), + to_hex(std::span(empty.data(), empty.size()))); + + leaves.push_back(hash_of("a")); + auto root_single = root(leaves); + // single leaf -> parent of (a,a) + std::vector two{ leaves[0], leaves[0] }; + auto duplicated_pair_root = root(two); + EXPECT_EQ(to_hex(std::span(root_single.data(), root_single.size())), + to_hex(std::span(duplicated_pair_root.data(), duplicated_pair_root.size()))); +} + +TEST(Merkle, SmallSetsDeterministic) { + std::vector set_two{ hash_of("a"), hash_of("b") }; + auto root_two = root(set_two); + + // reorder leaves -> different root + std::vector set_two_swapped{ hash_of("b"), hash_of("a") }; + auto root_two_swapped = root(set_two_swapped); + EXPECT_NE(to_hex(std::span(root_two.data(), root_two.size())), + to_hex(std::span(root_two_swapped.data(), root_two_swapped.size()))); + + std::vector set_three{ hash_of("a"), hash_of("b"), hash_of("c") }; + auto root_three = root(set_three); + // change any leaf -> different root + set_three[2] = hash_of("x"); + auto root_three_changed = root(set_three); + EXPECT_NE(to_hex(std::span(root_three.data(), root_three.size())), + to_hex(std::span(root_three_changed.data(), root_three_changed.size()))); +} + +TEST(Merkle, ProofBuildAndVerify) { + std::vector leaves{ hash_of("a"), hash_of("b"), hash_of("c"), hash_of("d"), hash_of("e") }; + auto expected_root = root(leaves); + + for (size_t i = 0; i < leaves.size(); ++i) { + auto proof = build_proof(leaves, i); + EXPECT_TRUE(verify_proof(leaves[i], proof, expected_root)); + } + + // Tamper the leaf + auto proof0 = build_proof(leaves, 0); + auto wrong_leaf = hash_of("A"); + EXPECT_FALSE(verify_proof(wrong_leaf, proof0, expected_root)); +} \ No newline at end of file diff --git a/tests/test_serializer.cpp b/tests/test_serializer.cpp new file mode 100644 index 0000000..5a73cc2 --- /dev/null +++ b/tests/test_serializer.cpp @@ -0,0 +1,24 @@ +#include +#include "astro/core/serializer.hpp" +# +using namespace astro::core; +# +TEST(Serializer, WriteRaw_NoLengthPrefix) { + ByteWriter writer; + writer.write_u32(0x01020304u); + std::vector raw{0xAA, 0xBB, 0xCC}; + writer.write_raw(std::span(raw.data(), raw.size())); + std::vector pref{0x10, 0x20}; + writer.write_bytes(std::span(pref.data(), pref.size())); +# + auto out = writer.buffer(); + // Expect: [04 03 02 01] + [AA BB CC] + [02 00 00 00] + [10 20] + std::vector expected{ + 0x04, 0x03, 0x02, 0x01, + 0xAA, 0xBB, 0xCC, + 0x02, 0x00, 0x00, 0x00, + 0x10, 0x20 + }; + ASSERT_EQ(out.size(), expected.size()); + EXPECT_TRUE(std::equal(out.begin(), out.end(), expected.begin(), expected.end())); +} diff --git a/tests/test_transaction.cpp b/tests/test_transaction.cpp index 01ae0d6..43e7445 100644 --- a/tests/test_transaction.cpp +++ b/tests/test_transaction.cpp @@ -17,9 +17,6 @@ TEST(Transaction, RoundTripAndVerify) { transaction.from_pub_pem = key_pair.pubkey_pem; // PEM bytes transaction.to_label = "alice"; - // hash before signing is stable - auto tx_hash_1 = transaction.tx_hash(); - // sign transaction.sign(key_pair.privkey_pem); ASSERT_FALSE(transaction.signature.empty()); From 06734b24d5f8f63be9402799e372e71dd57429c0 Mon Sep 17 00:00:00 2001 From: Emmanuel Atawodi Date: Sun, 9 Nov 2025 14:37:48 +0100 Subject: [PATCH 2/2] chore: setup CI workflow --- .github/workflows/ci.yml | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 509a67e..333a16a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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" \ No newline at end of file + - 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 \ No newline at end of file