diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 887264d1d..decda14e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,6 +68,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -198,6 +199,7 @@ jobs: test_mweb_builder \ test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ + dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ @@ -302,6 +304,11 @@ jobs: qt-webengine: name: Qt WebEngine leak gate runs-on: ubuntu-24.04 + # Hard ceiling: orphaned QtWebEngine/Chromium helper children survive + # the inner `timeout 180`, holding the xvfb pipe open and hanging the + # job to the 360-min runner default. Cap it so a hung leak gate is + # force-killed (with its whole process tree) in minutes, not hours. + timeout-minutes: 20 steps: - uses: actions/checkout@v6 diff --git a/src/impl/CMakeLists.txt b/src/impl/CMakeLists.txt index cb7b6a370..ae12a7c2f 100644 --- a/src/impl/CMakeLists.txt +++ b/src/impl/CMakeLists.txt @@ -2,3 +2,4 @@ add_subdirectory(ltc) add_subdirectory(btc) add_subdirectory(dgb) add_subdirectory(dash) +add_subdirectory(nmc) diff --git a/src/impl/dgb/coin/gentx_coinbase.hpp b/src/impl/dgb/coin/gentx_coinbase.hpp new file mode 100644 index 000000000..20a15d466 --- /dev/null +++ b/src/impl/dgb/coin/gentx_coinbase.hpp @@ -0,0 +1,131 @@ +#pragma once +// ============================================================================ +// gentx_coinbase.hpp — SSOT non-witness coinbase (gentx) assembler. +// +// Single source of the p2pool coinbase wire layout. Consumed by: +// - share_check.hpp generate_share_transaction() (the verification SSOT) +// - share_check.hpp create_local_share() (the "same format as" smell) +// - won-block reconstruction (as_block framing) +// so that emission and verification can never diverge on a byte. +// +// Produces the NON-WITNESS serialization and its double-SHA256 txid +// (== p2pool gentx_hash). Byte layout mirrors +// frstrtr/p2pool-merged-v36 data.py generate_transaction(): +// +// version(4 LE = 1) +// vin_count(varint = 1) +// vin[0] = prev_hash(32 zero) | prev_idx(0xffffffff) | script(VarStr) | seq(0xffffffff) +// vout_count(varint) +// vouts: [segwit_commitment?] ++ payout_outputs ++ donation ++ op_return_commitment +// each vout = value(8 LE) | script(VarStr) +// lock_time(4 = 0) +// +// Pure: takes already-built script/amount inputs (no tracker, no share template +// dependency) so it is directly KAT-able against a canonical oracle vector. +// ============================================================================ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace dgb::coin +{ + +struct GentxCoinbase +{ + std::vector bytes; // non-witness serialization + uint256 txid; // double-SHA256(bytes) == gentx_hash +}; + +// payout_outputs: (scriptPubKey, value) pairs in final consensus order. +// segwit_commitment_script / segwit absent -> no witness-commitment vout. +inline GentxCoinbase assemble_gentx_coinbase( + const std::vector& coinbase_script, + const std::optional>& segwit_commitment_script, + const std::vector, uint64_t>>& payout_outputs, + uint64_t donation_amount, + const std::vector& donation_script, + const std::vector& op_return_script) +{ + PackStream tx; + + // tx version = 1 + uint32_t tx_version = 1; + tx.write(std::span(reinterpret_cast(&tx_version), 4)); + + // vin count = 1 + { + unsigned char one = 1; + tx.write(std::span(reinterpret_cast(&one), 1)); + } + + // vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0xffffffff + { + uint256 zero_hash; + tx << zero_hash; + uint32_t prev_idx = 0xffffffff; + tx.write(std::span(reinterpret_cast(&prev_idx), 4)); + BaseScript cb; cb.m_data = coinbase_script; + tx << cb; + uint32_t seq = 0xffffffff; + tx.write(std::span(reinterpret_cast(&seq), 4)); + } + + // vout count + size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN */ + + (segwit_commitment_script.has_value() ? 1 : 0); + if (n_outs < 253) + { + uint8_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 1)); + } + else + { + uint8_t marker = 0xfd; + tx.write(std::span(reinterpret_cast(&marker), 1)); + uint16_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 2)); + } + + auto write_txout = [&](uint64_t value, const std::vector& script) { + tx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; bs.m_data = script; + tx << bs; + }; + + // segwit witness-commitment vout (value 0) + if (segwit_commitment_script.has_value()) + write_txout(0, segwit_commitment_script.value()); + + // PPLNS payout outputs (caller supplies final sorted order) + for (auto& [script, amount] : payout_outputs) + write_txout(amount, script); + + // donation output + write_txout(donation_amount, donation_script); + + // OP_RETURN ref commitment (value 0) + write_txout(0, op_return_script); + + // lock_time = 0 + { + uint32_t locktime = 0; + tx.write(std::span(reinterpret_cast(&locktime), 4)); + } + + GentxCoinbase out; + out.bytes.assign(reinterpret_cast(tx.data()), + reinterpret_cast(tx.data()) + tx.size()); + auto sp = std::span(out.bytes.data(), out.bytes.size()); + out.txid = Hash(sp); + return out; +} + +} // namespace dgb::coin diff --git a/src/impl/dgb/share_check.hpp b/src/impl/dgb/share_check.hpp index 622f3b51f..467a727da 100644 --- a/src/impl/dgb/share_check.hpp +++ b/src/impl/dgb/share_check.hpp @@ -8,6 +8,8 @@ #include "share_messages.hpp" #include "share_types.hpp" +#include + #include #include #include @@ -1133,81 +1135,22 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS); // --- 4. Serialise the coinbase transaction --- - // Non-witness serialization (for txid computation): - // version(4) + vin_count(varint) + vin + vout_count(varint) + vouts + locktime(4) - PackStream tx; - - // tx version = 1 - uint32_t tx_version = 1; - tx.write(std::span(reinterpret_cast(&tx_version), 4)); - - // vin count = 1 - { - unsigned char one = 1; - tx.write(std::span(reinterpret_cast(&one), 1)); - } - - // vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0 - { - // prev_hash (32 zero bytes) - uint256 zero_hash; - tx << zero_hash; - // prev_index (0xffffffff) - uint32_t prev_idx = 0xffffffff; - tx.write(std::span(reinterpret_cast(&prev_idx), 4)); - // script (VarStr) - tx << share.m_coinbase; - // sequence (0xffffffff — standard coinbase sequence, matches Python) - uint32_t seq = 0xffffffff; - tx.write(std::span(reinterpret_cast(&seq), 4)); - } - - // Count total outputs - size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN commitment */; - // Segwit commitment output (if applicable) - bool has_segwit = false; + // Wire layout (version|vin|vouts|locktime) and txid are produced by the + // SSOT assembler dgb::coin::assemble_gentx_coinbase() so that emission and + // verification can never diverge on a byte. This function only builds the + // per-share inputs (coinbase script, optional segwit commitment, payout + // outputs, donation, OP_RETURN ref commitment). + + // Segwit witness-commitment output (value=0) — present iff this share + // carries segwit data on a segwit-active version. The script is + // 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || P2POOL_WITNESS_NONCE). + std::optional> segwit_commitment_script; if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) { if constexpr (requires { share.m_segwit_data; }) { if (share.m_segwit_data.has_value()) { - has_segwit = true; - n_outs += 1; - } - } - } - - // vout count (varint — for < 253 outputs, it's a single byte) - if (n_outs < 253) - { - uint8_t cnt = static_cast(n_outs); - tx.write(std::span(reinterpret_cast(&cnt), 1)); - } - else - { - uint8_t marker = 0xfd; - tx.write(std::span(reinterpret_cast(&marker), 1)); - uint16_t cnt = static_cast(n_outs); - tx.write(std::span(reinterpret_cast(&cnt), 2)); - } - - // Helper to write a single tx_out: value(8LE) + script(VarStr) - auto write_txout = [&](uint64_t value, const std::vector& script) { - tx.write(std::span(reinterpret_cast(&value), 8)); - BaseScript bs; - bs.m_data = script; - tx << bs; - }; - - // Segwit commitment output (value=0, script=OP_RETURN + witness_commitment) - if (has_segwit) - { - if constexpr (requires { share.m_segwit_data; }) - { - if (share.m_segwit_data.has_value()) - { - // witness commitment: 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || '[P2Pool]'*4) std::vector wscript; wscript.push_back(0x6a); // OP_RETURN wscript.push_back(0x24); // PUSH 36 @@ -1219,7 +1162,7 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); auto commitment_bytes = commitment.GetChars(); wscript.insert(wscript.end(), commitment_bytes.begin(), commitment_bytes.end()); - write_txout(0, wscript); + segwit_commitment_script = std::move(wscript); if (dump_diag) { LOG_INFO << "[WC-GST] wtxid_root=" << sd.m_wtxid_merkle_root.GetHex() << " commitment=" << commitment.GetHex(); @@ -1228,17 +1171,13 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const } } - // PPLNS payout outputs - for (auto& [script, amount] : payout_outputs) - write_txout(amount, script); - // Donation output — V35 shares always use P2PK DONATION_SCRIPT, // V36 shares always use P2SH COMBINED_DONATION_SCRIPT. // Each share was created with the donation script matching its version. auto donation_script = params.donation_script_func(ver); - write_txout(donation_amount, donation_script); // OP_RETURN commitment: value=0, script = 0x6a28 + ref_hash(32) + last_txout_nonce(8) + std::vector op_return_script; { // We need the ref_hash — recompute it from the share the same way share_init_verify does PackStream ref_stream; @@ -1350,7 +1289,6 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); // Build OP_RETURN script: 0x6a 0x28 + ref_hash(32) + last_txout_nonce(8) - std::vector op_return_script; op_return_script.push_back(0x6a); // OP_RETURN op_return_script.push_back(0x28); // PUSH 40 bytes op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); @@ -1359,19 +1297,18 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const auto* p = reinterpret_cast(&nonce); op_return_script.insert(op_return_script.end(), p, p + 8); } - write_txout(0, op_return_script); - } - - // lock_time = 0 - { - uint32_t locktime = 0; - tx.write(std::span(reinterpret_cast(&locktime), 4)); } - // --- 5. Compute txid (double-SHA256 of non-witness serialization) --- - auto tx_span = std::span( - reinterpret_cast(tx.data()), tx.size()); - auto txid = Hash(tx_span); + // --- 5. Assemble + compute txid via the SSOT (double-SHA256 of + // non-witness serialization). Reconstruct the PackStream tx so the + // downstream V36 hash_link diagnostics keep working unchanged. + auto _gc = dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_commitment_script, payout_outputs, + donation_amount, donation_script, op_return_script); + PackStream tx; + tx.write(std::span( + reinterpret_cast(_gc.bytes.data()), _gc.bytes.size())); + auto txid = _gc.txid; // V36 hash_link cross-check: compute prefix hash_link from our coinbase // and compare with the share's stored hash_link. If states differ, @@ -1452,8 +1389,8 @@ uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, const LOG_WARNING << "[GENTX-DIAG] coinbase_hex=" << to_hex(cp, tx.size()); LOG_WARNING << "[GENTX-DIAG] pplns_outputs=" << payout_outputs.size() << " donation_amount=" << donation_amount - << " n_outs=" << n_outs - << " has_segwit=" << has_segwit; + << " n_outs=" << (payout_outputs.size() + 2 + (segwit_commitment_script.has_value() ? 1u : 0u)) + << " has_segwit=" << segwit_commitment_script.has_value(); LOG_WARNING << "[GENTX-DIAG] total_weight=" << total_weight.GetHex() << " total_don_weight=" << total_donation_weight.GetHex(); for (size_t i = 0; i < payout_outputs.size(); ++i) { @@ -2729,7 +2666,8 @@ uint256 create_local_share( share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ (static_cast(min_header.m_nonce) << 32); - // --- Build the p2pool coinbase in the same format as generate_share_transaction --- + // --- Build the p2pool coinbase via the SSOT assemble_gentx_coinbase() --- + // (identical wire layout to generate_share_transaction; one byte path, not a twin) // This is needed to compute hash_link and to verify the share locally. // The coinbase format is: version(4) + vin(1 input) + vout(outputs...) + locktime(4) // @@ -2942,41 +2880,29 @@ uint256 create_local_share( if (payout_outputs.size() > 4000) payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); - PackStream gentx; - { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } - { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } - { uint256 z; gentx << z; } - { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } - gentx << share.m_coinbase; - { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } - size_t n_outs = payout_outputs.size() + 1 + 1; - bool has_segwit_fb = share.m_segwit_data.has_value(); - if (has_segwit_fb) n_outs += 1; - if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } - else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } - auto write_txout = [&](uint64_t value, const std::vector& script) { - gentx.write(std::span(reinterpret_cast(&value), 8)); - BaseScript bs; bs.m_data = script; gentx << bs; - }; - if (has_segwit_fb) { + // Build the p2pool coinbase via the SSOT assembler + // dgb::coin::assemble_gentx_coinbase() — identical wire layout to + // generate_share_transaction(). This is no longer a hand-rolled twin: + // both emission and verification route through the one byte path. + std::optional> segwit_opt; + if (share.m_segwit_data.has_value()) { auto& sd = share.m_segwit_data.value(); std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); auto cb = commitment.GetChars(); wscript.insert(wscript.end(), cb.begin(), cb.end()); - write_txout(0, wscript); - } - for (auto& [script, amount] : payout_outputs) write_txout(amount, script); - write_txout(donation_amount, params.donation_script_func(int64_t(36))); - { std::vector op; op.push_back(0x6a); op.push_back(0x28); - op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); - uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); - op.insert(op.end(), p, p + 8); write_txout(0, op); } - { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } - - coinbase_bytes_for_hashlink.assign( - reinterpret_cast(gentx.data()), - reinterpret_cast(gentx.data()) + gentx.size()); + segwit_opt = std::move(wscript); + } + auto donation_script = params.donation_script_func(int64_t(36)); + std::vector op_return_vec; + op_return_vec.push_back(0x6a); op_return_vec.push_back(0x28); + op_return_vec.insert(op_return_vec.end(), ref_hash.data(), ref_hash.data() + 32); + { uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op_return_vec.insert(op_return_vec.end(), p, p + 8); } + auto _gc = dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_opt, payout_outputs, + donation_amount, donation_script, op_return_vec); + coinbase_bytes_for_hashlink = _gc.bytes; } // The split point: everything before ref_hash + last_txout_nonce + locktime diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 683aeb525..63a082a16 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -40,6 +40,34 @@ if (BUILD_TESTING AND GTest_FOUND) # transport rpc.cpp wiring is deferred (Option-B scope). Each MUST appear # in BOTH this ctest registration AND the build.yml --target allowlist, # or it becomes a #143-style NOT_BUILT sentinel that reds master. + + # --- #82 SSOT gentx coinbase assembler KAT ---------------------------- + # coin/gentx_coinbase.hpp uses core primitives (PackStream/BaseScript/Hash/ + # uint256), so it links the same proven set as dgb_share_test rather than + # the header-only guard foreach above. Oracle ground-truth vectors. MUST + # also be in the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_gentx_coinbase_test gentx_coinbase_test.cpp) + target_link_libraries(dgb_gentx_coinbase_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_gentx_coinbase_test "" AUTO) + + # --- #172 share-path gentx KAT (collapse consumption proof) ------------ + # Proves generate_share_transaction (the verification SSOT call site #172 + # rewired) EMITS assemble_gentx_coinbase framing for a real v36 share, via + # a round-trip equivalence (the op_return ref_hash is share-derived, so it + # cannot be pinned to a fixed oracle vector). Links the dgb OBJECT lib like + # dgb_share_test. MUST also be in the build.yml --target allowlist (#143). + add_executable(dgb_gentx_share_path_test gentx_share_path_test.cpp) + target_link_libraries(dgb_gentx_share_path_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_gentx_share_path_test "" AUTO) + foreach(dgb_guard rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test) add_executable(${dgb_guard} ${dgb_guard}.cpp) target_link_libraries(${dgb_guard} PRIVATE diff --git a/src/impl/dgb/test/gentx_coinbase_test.cpp b/src/impl/dgb/test/gentx_coinbase_test.cpp new file mode 100644 index 000000000..e96b47caf --- /dev/null +++ b/src/impl/dgb/test/gentx_coinbase_test.cpp @@ -0,0 +1,104 @@ +// DGB #82 — SSOT non-witness coinbase (gentx) assembler KAT. +// +// Locks dgb::coin::assemble_gentx_coinbase() (coin/gentx_coinbase.hpp) — the +// single coinbase wire-layout extracted from generate_share_transaction() +// (share_check.hpp:933, the verification SSOT) — against GROUND-TRUTH vectors +// derived from the canonical oracle frstrtr/p2pool-dgb-scrypt +// (util/pack.py byte logic + bitcoin/data.py tx_id_type; donation script +// 4104ffd0...ac). The vectors are NOT self-generated from this builder: they +// are the oracle serializer's output, so a PASS proves emission==oracle, not +// merely self-consistency. +// +// Asserts, for both the no-segwit and witness-commitment-first layouts: +// build -> non-witness serialize == oracle BYTES +// double-SHA256(bytes) (GetHex display) == oracle TXID +// Per-coin isolation: the only DGB<->BCH divergence is the segwit predicate at +// serialize time (witness vout present-or-absent), exercised by both cases. + +#include +#include + +#include +#include +#include +#include + +namespace { + +std::vector unhex(const std::string& h) { + std::vector v; v.reserve(h.size() / 2); + auto nyb = [](char c) -> int { return (c <= '9') ? c - '0' : (c | 0x20) - 'a' + 10; }; + for (size_t i = 0; i + 1 < h.size(); i += 2) + v.push_back(static_cast((nyb(h[i]) << 4) | nyb(h[i + 1]))); + return v; +} +std::string tohex(const std::vector& v) { + static const char* H = "0123456789abcdef"; + std::string s; s.reserve(v.size() * 2); + for (unsigned char b : v) { s.push_back(H[b >> 4]); s.push_back(H[b & 0xf]); } + return s; +} + +// --- fixed inputs shared verbatim with the oracle generator ---------------- +const std::vector CB = unhex("03a1b2c3041122334455667788"); +const std::vector P1 = unhex(std::string("76a914") + std::string(40, '1') + "88ac"); +const std::vector P2 = unhex(std::string("76a914") + std::string(40, '2') + "88ac"); +const std::vector DON = unhex("4104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac"); + +const std::vector, uint64_t>> PAYOUTS = { + {P1, 5000000000ull}, + {P2, 2500000000ull}, +}; + +// Ground truth from the oracle serializer (see file header). +const std::string NOSEG_BYTES = + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff0400f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000"; +const std::string NOSEG_TXID = + "c5734775c1521b216e0e1bca506e4d15755cf55125caf56b7e0728a6d54a9b59"; +const std::string SEG_BYTES = + "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0d03a1b2c3041122334455667788ffffffff050000000000000000266a24aa21a9edcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd00f2052a010000001976a914111111111111111111111111111111111111111188ac00f90295000000001976a914222222222222222222222222222222222222222288ac0100000000000000434104ffd03de44a6e11b9917f3a29f9443283d9871c9d743ef30d5eddcd37094b64d1b3d8090496b53256786bf5c82932ec23c3b74d9f05a6f95a8b5529352656664bac00000000000000002a6a28abababababababababababababababababababababababababababababababab080706050403020100000000"; +const std::string SEG_TXID = + "4b66aabff52ca1336b52688815cece123075856880aaf6e77cb44f0d477b6162"; + +} // namespace + +// op_return / wc literals: build them explicitly to avoid std::string surprises. +static std::vector make_opret() { + auto v = unhex("6a28"); + for (int i = 0; i < 32; ++i) v.push_back(0xab); + const unsigned char nonce[8] = {0x08, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01}; + for (unsigned char b : nonce) v.push_back(b); + return v; +} +static std::vector make_wc() { + auto v = unhex("6a24aa21a9ed"); + for (int i = 0; i < 32; ++i) v.push_back(0xcd); + return v; +} + +TEST(DGB_gentx_coinbase, NoSegwitOracleVector) { + auto opret = make_opret(); + auto g = dgb::coin::assemble_gentx_coinbase( + CB, std::nullopt, PAYOUTS, /*donation_amount=*/1, DON, opret); + EXPECT_EQ(tohex(g.bytes), NOSEG_BYTES); + EXPECT_EQ(g.txid.GetHex(), NOSEG_TXID); +} + +TEST(DGB_gentx_coinbase, SegwitCommitmentFirstOracleVector) { + auto opret = make_opret(); + auto wc = make_wc(); + auto g = dgb::coin::assemble_gentx_coinbase( + CB, std::optional>(wc), PAYOUTS, + /*donation_amount=*/1, DON, opret); + EXPECT_EQ(tohex(g.bytes), SEG_BYTES); + EXPECT_EQ(g.txid.GetHex(), SEG_TXID); +} + +// Guard the per-coin isolation invariant: toggling only the segwit predicate +// changes vout-count + prepends exactly one 0-value commitment vout, nothing +// else — the divergence is gated, not a forked framer. +TEST(DGB_gentx_coinbase, SegwitPredicateIsTheOnlyDivergence) { + EXPECT_NE(NOSEG_BYTES, SEG_BYTES); + // both share the identical coinbase-script vin prefix and donation/op_return tail + EXPECT_EQ(NOSEG_BYTES.substr(0, 90), SEG_BYTES.substr(0, 90)); +} diff --git a/src/impl/dgb/test/gentx_share_path_test.cpp b/src/impl/dgb/test/gentx_share_path_test.cpp new file mode 100644 index 000000000..e1a3e4824 --- /dev/null +++ b/src/impl/dgb/test/gentx_share_path_test.cpp @@ -0,0 +1,264 @@ +// DGB #82 / #172 — share-path gentx KAT. +// +// The helper-level KAT (gentx_coinbase_test) pins dgb::coin::assemble_gentx_coinbase +// directly against oracle vectors. This test proves the *consumption* side of +// the #172 collapse: that generate_share_transaction() — the verification SSOT +// call site that #172 rewired onto assemble_gentx_coinbase — actually EMITS the +// SSOT framing for a real share, not merely that the pure helper does. +// +// The OP_RETURN ref commitment is share-derived: +// ref_hash = check_merkle_link(Hash(ref_serialization), m_ref_merkle_link) +// so the share-path gentx cannot be pinned to a fixed 0xab..×32 oracle byte +// vector (criterion 1: no oracle vector regeneration). Instead we assert a +// round-trip equivalence: generate_share_transaction(share) == a verbatim +// re-derivation of the same per-share inputs handed to the SAME SSOT assembler. +// If the share path ever stops routing through the SSOT, or builds the coinbase +// inputs differently, the two txids diverge and this fails — which is exactly +// "the collapse is what's tested." +// +// A v36 MergedMiningShare with a NULL parent is used so the PPLNS walk is +// skipped entirely (no tracker chain entries are touched): payout_outputs is +// empty and donation_amount == subsidy. That isolates the coinbase framing + +// op_return derivation, which is the SSOT contract under test. +// +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist +// or it becomes a #143-style NOT_BUILT sentinel that reds master. + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace { + +// A v36 share with a null parent => PPLNS skipped (empty payouts, donation == +// subsidy). Coinbase script / nonce / timestamp / last_txout_nonce are set to +// non-trivial values so the op_return ref derivation is exercised. +dgb::MergedMiningShare make_v36_share() +{ + dgb::MergedMiningShare s; + s.m_prev_hash.SetNull(); // null parent => PPLNS walk skipped + s.m_coinbase.m_data = { 0x03, 0xa1, 0xb2, 0xc3, 0x04, 0x11, 0x22, 0x33, 0x44 }; + s.m_nonce = 0x12345678; + s.m_pubkey_hash.SetNull(); + s.m_pubkey_type = 0; + s.m_subsidy = 500000000ull; + s.m_donation = 0; + s.m_stale_info = dgb::none; + s.m_desired_version = 36; + s.m_segwit_data = std::nullopt; + s.m_far_share_hash.SetNull(); + s.m_max_bits = 0x1e0fffff; + s.m_bits = 0x1e0fffff; + s.m_timestamp = 1718700000; + s.m_absheight = 1000; + // m_abswork default-constructs to zero (uint128) + s.m_merged_payout_hash.SetNull(); + s.m_last_txout_nonce = 0x0001020304050607ull; + // m_ref_merkle_link left empty => check_merkle_link returns Hash(ref) directly + return s; +} + +// Verbatim re-derivation of generate_share_transaction's per-share coinbase +// inputs (v36 path), ending in the SAME SSOT assembler. Mirrors share_check.hpp +// section 4/5 byte-for-byte via the identical pack primitives. +template +dgb::coin::GentxCoinbase rederive_via_ssot( + const ShareT& share, const core::CoinParams& params) +{ + constexpr int64_t ver = ShareT::version; + + // null parent => empty payouts, donation_amount == subsidy + std::vector, uint64_t>> payout_outputs; + uint64_t donation_amount = share.m_subsidy; + + // no segwit_data => no witness-commitment vout + std::optional> segwit_commitment_script; + + auto donation_script = params.donation_script_func(ver); + + std::vector op_return_script; + { + PackStream ref_stream; + { + auto hex = params.active_identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (core::version_gate::is_v36_active(ver)) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= dgb::SEGWIT_ACTIVATION_VERSION) + { + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + if constexpr (core::version_gate::is_v36_active(ver)) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + uint256 ref_hash = dgb::check_merkle_link(hash_ref, share.m_ref_merkle_link); + + op_return_script.push_back(0x6a); // OP_RETURN + op_return_script.push_back(0x28); // PUSH 40 + op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); + { + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + op_return_script.insert(op_return_script.end(), p, p + 8); + } + } + + return dgb::coin::assemble_gentx_coinbase( + share.m_coinbase.m_data, segwit_commitment_script, payout_outputs, + donation_amount, donation_script, op_return_script); +} + +// --- Test 1: share path emits SSOT framing (round-trip equivalence) ----------- +TEST(DgbGentxSharePath, GenerateShareTransactionEmitsSsotFraming) +{ + auto params = dgb::make_coin_params(/*testnet=*/false); + dgb::ShareTracker tracker; // empty chain + auto share = make_v36_share(); + + // Route A: the verification SSOT call site under test. + uint256 actual = dgb::generate_share_transaction( + share, tracker, params, /*dump_diag=*/false, /*v36_active=*/true); + + // Route B: verbatim re-derivation through the SAME assembler. + auto expected = rederive_via_ssot(share, params); + + EXPECT_EQ(actual, expected.txid) + << "generate_share_transaction no longer emits assemble_gentx_coinbase framing"; +} + +// --- Test 2: determinism (the SSOT path is a pure function of the share) ------- +TEST(DgbGentxSharePath, Deterministic) +{ + auto params = dgb::make_coin_params(false); + dgb::ShareTracker tracker; + auto share = make_v36_share(); + + uint256 a = dgb::generate_share_transaction(share, tracker, params, false, true); + uint256 b = dgb::generate_share_transaction(share, tracker, params, false, true); + EXPECT_EQ(a, b); +} + +// --- Test 3: coinbase-affecting fields flow into the SSOT gentx ---------------- +// last_txout_nonce feeds the op_return; coinbase script feeds vin[0]. Changing +// either must change the gentx txid (proves the share data reaches the SSOT, +// not a cached/stubbed value). +TEST(DgbGentxSharePath, CoinbaseInputsAreLoadBearing) +{ + auto params = dgb::make_coin_params(false); + dgb::ShareTracker tracker; + + auto base = make_v36_share(); + uint256 h0 = dgb::generate_share_transaction(base, tracker, params, false, true); + + auto bumped_nonce = base; + bumped_nonce.m_last_txout_nonce ^= 0xffull; + uint256 h1 = dgb::generate_share_transaction(bumped_nonce, tracker, params, false, true); + EXPECT_NE(h0, h1) << "last_txout_nonce did not reach the op_return commitment"; + + auto bumped_cb = base; + bumped_cb.m_coinbase.m_data.push_back(0x99); + uint256 h2 = dgb::generate_share_transaction(bumped_cb, tracker, params, false, true); + EXPECT_NE(h0, h2) << "coinbase script did not reach the gentx vin"; +} + +} // namespace diff --git a/src/impl/dgb/test/share_test.cpp b/src/impl/dgb/test/share_test.cpp index 1c141cba9..3263ce4bc 100644 --- a/src/impl/dgb/test/share_test.cpp +++ b/src/impl/dgb/test/share_test.cpp @@ -36,6 +36,27 @@ TEST(DGB_share_test, OracleConsensusParams) EXPECT_EQ(dgb::PoolConfig::CHAIN_LENGTH, 2880u); } +// Pool/share layer timing + bound params — audited CONFORM to the DGB oracle +// this slice (networks/digibyte.py + bitcoin/networks/digibyte.py). These drive +// PPLNS weighting (SPREAD), retarget lookbehind, chain pruning (REAL_CHAIN_LENGTH), +// block-template caps, and the tracker score() denominator (PARENT.BLOCK_PERIOD). +TEST(DGB_share_test, OraclePoolShareLayer) +{ + EXPECT_EQ(dgb::PoolConfig::SPREAD, 24u); // networks/digibyte.py SPREAD + EXPECT_EQ(dgb::PoolConfig::TARGET_LOOKBEHIND, 100u); // TARGET_LOOKBEHIND + EXPECT_EQ(dgb::PoolConfig::REAL_CHAIN_LENGTH, 2880u); // REAL_CHAIN_LENGTH = 12*60*60//15 + EXPECT_EQ(dgb::PoolConfig::BLOCK_MAX_SIZE, 32000000u); // BLOCK_MAX_SIZE + EXPECT_EQ(dgb::PoolConfig::BLOCK_MAX_WEIGHT, 128000000u); // BLOCK_MAX_WEIGHT + + // tracker score() denominator: work / ((1 - block_rel_height) * BLOCK_PERIOD). + // PARENT.BLOCK_PERIOD = 75 (Scrypt-algo period), bitcoin/networks/digibyte.py. + EXPECT_EQ(dgb::CoinParams::BLOCK_PERIOD, 75u); + + // MAX_TARGET = 2**256//2**20 - 1 = 2^236 - 1 (top 20 bits zero, rest ones). + EXPECT_EQ(dgb::PoolConfig::max_target().GetHex(), + "00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); +} + // v35 donation = forrestv P2PK (4104ffd0…ac, 65-byte pubkey push + CHECKSIG). // v36 donation = combined P2SH (23-byte). get_donation_script() switches on // share_version at the SEGWIT/merged boundary. diff --git a/src/impl/nmc/CMakeLists.txt b/src/impl/nmc/CMakeLists.txt new file mode 100644 index 000000000..da7056244 --- /dev/null +++ b/src/impl/nmc/CMakeLists.txt @@ -0,0 +1,8 @@ +# NMC (embedded merge-mined Namecoin) — P0 structural leaf. +# Only the coin tree exists at P0. node/stratum/share layers come later. +add_subdirectory(coin) + +# P1: NMC module tests (AuxPow merkle-walk KAT). Gated on BUILD_TESTING. +if (BUILD_TESTING) + add_subdirectory(test) +endif() diff --git a/src/impl/nmc/coin/CMakeLists.txt b/src/impl/nmc/coin/CMakeLists.txt new file mode 100644 index 000000000..85e446b50 --- /dev/null +++ b/src/impl/nmc/coin/CMakeLists.txt @@ -0,0 +1,15 @@ +# NMC coin tree — P0 structural leaf. +# Mirror of src/impl/btc/coin/CMakeLists.txt (header-only interface files + +# a translation unit that forces them to compile). STRUCTURE-only: no node / +# rpc / p2p layers yet. + +set(nmc_coin_interface + transaction.hpp transaction.cpp + block.hpp + header_chain.hpp + coin_smoke.cpp +) + +add_library(nmc_coin ${nmc_coin_interface}) + +target_link_libraries(nmc_coin core nlohmann_json::nlohmann_json) diff --git a/src/impl/nmc/coin/block.hpp b/src/impl/nmc/coin/block.hpp new file mode 100644 index 000000000..e6cdf9b45 --- /dev/null +++ b/src/impl/nmc/coin/block.hpp @@ -0,0 +1,127 @@ +#pragma once + +// NMC (Namecoin) block / header primitives. +// +// Mirror of src/impl/btc/coin/block.hpp. Namecoin is a Bitcoin (SHA256d) fork; +// its plain block header layout is byte-identical to Bitcoin's. Re-homed into +// namespace nmc::coin. No btc header is modified. +// +// P0-DEFER: Namecoin blocks are AUX-POW capable. The merge-mined parent-chain +// proof (CAuxPow: parent coinbase tx + coinbase merkle branch + parent block +// header) is NOT modelled here. The plain header below is the foundation leaf; +// the AuxPow attachment is a later leaf — see header_chain.hpp AuxPowStub. + +#include "transaction.hpp" + +#include +#include +#include + +namespace nmc +{ + +namespace coin +{ + +struct SmallBlockHeaderType +{ + uint64_t m_version {}; + uint256 m_previous_block{}; + uint32_t m_timestamp{}; + uint32_t m_bits{}; + uint32_t m_nonce{}; + + C2POOL_SERIALIZE_METHODS(SmallBlockHeaderType) { READWRITE(VarInt(obj.m_version), obj.m_previous_block, obj.m_timestamp, obj.m_bits, obj.m_nonce); } + + SmallBlockHeaderType() {} + + void SetNull() + { + m_version = 0; + m_previous_block.SetNull(); + m_timestamp = 0; + m_bits = 0; + m_nonce = 0; + } + + bool IsNull() const + { + return (m_bits == 0); + } +}; + +struct BlockHeaderType : SmallBlockHeaderType +{ + uint256 m_merkle_root; + + // Full block header uses fixed 4-byte int32 version (not VarInt like SmallBlockHeaderType) + template + void Serialize(Stream& s) const { + uint32_t version32 = static_cast(m_version); + ::Serialize(s, version32); + ::Serialize(s, m_previous_block); + ::Serialize(s, m_merkle_root); + ::Serialize(s, m_timestamp); + ::Serialize(s, m_bits); + ::Serialize(s, m_nonce); + } + template + void Unserialize(Stream& s) { + uint32_t version32; + ::Unserialize(s, version32); + m_version = version32; + ::Unserialize(s, m_previous_block); + ::Unserialize(s, m_merkle_root); + ::Unserialize(s, m_timestamp); + ::Unserialize(s, m_bits); + ::Unserialize(s, m_nonce); + } + + BlockHeaderType() : SmallBlockHeaderType() { } + + void SetNull() + { + SmallBlockHeaderType::SetNull(); + m_merkle_root.SetNull(); + } + + bool IsNull() const + { + return (m_bits == 0); + } + +}; + +struct BlockType : BlockHeaderType +{ + std::vector m_txs; + + template + void Serialize(Stream& s) const { + BlockHeaderType::Serialize(s); + ::Serialize(s, TX_WITH_WITNESS(m_txs)); + } + + template + void Unserialize(Stream& s) { + BlockHeaderType::Unserialize(s); + ::Unserialize(s, TX_WITH_WITNESS(m_txs)); + } + + BlockType() : BlockHeaderType() { } + + void SetNull() + { + BlockHeaderType::SetNull(); + m_txs.clear(); + } + + bool IsNull() const + { + return BlockHeaderType::IsNull(); + } +}; + +} // namespace coin + +} // namespace nmc diff --git a/src/impl/nmc/coin/coin_smoke.cpp b/src/impl/nmc/coin/coin_smoke.cpp new file mode 100644 index 000000000..f69d727cf --- /dev/null +++ b/src/impl/nmc/coin/coin_smoke.cpp @@ -0,0 +1,41 @@ +// NMC coin P0 structural-leaf smoke translation unit. +// +// Header-only NMC coin types (block.hpp, header_chain.hpp) are otherwise never +// instantiated in a .cpp, so they would not be compile-checked by the build. +// This TU forces instantiation of the P0 structural leaf so CMake's nmc_coin +// target actually compiles header_chain.hpp (and its AuxPow / HeaderChain +// skeleton). It does NOT perform any validation — consistent with the P0 fence. + +#include "header_chain.hpp" +#include "block.hpp" + +namespace nmc { +namespace coin { + +// Force template/inline instantiation of the structural leaf. +void nmc_coin_p0_smoke() +{ + NMCChainParams params = NMCChainParams::mainnet(); + HeaderChain chain(params); + (void)chain.init(); + (void)chain.height(); + (void)chain.size(); + + BlockHeaderType header; + header.SetNull(); + (void)block_hash(header); + (void)pow_hash(header); + + AuxPow auxpow; + auxpow.SetNull(); + // P0-DEFER stub must report NOT_IMPLEMENTED_P0. + (void)auxpow.check_proof(uint256{}, params.aux_chain_id); + + AuxChain aux_slot; + std::vector aux_chains; // nmc-local list (fence #4) + aux_chains.push_back(aux_slot); + (void)aux_chains; +} + +} // namespace coin +} // namespace nmc diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp new file mode 100644 index 000000000..17b918076 --- /dev/null +++ b/src/impl/nmc/coin/header_chain.hpp @@ -0,0 +1,455 @@ +#pragma once + +/// NMC (Namecoin) Header Chain — P0 STRUCTURAL LEAF +/// +/// Validated header-only chain skeleton for embedded merge-mined Namecoin. +/// +/// Namecoin is a SHA256d Bitcoin fork. Its *plain* block header is byte- +/// identical to Bitcoin's (see block.hpp) and its difficulty retarget is the +/// classic Bitcoin 2016-block window. What makes Namecoin different from a +/// straight Bitcoin clone is MERGE-MINING: from the auxpow-activation height +/// onward, a Namecoin block may carry an AuxPow proof — a parent (BTC) +/// coinbase transaction plus the merkle branches that bind it to the parent +/// block header — instead of doing its own proof-of-work. The Namecoin block +/// hash still has to be < target, but the proof-of-work is *demonstrated on +/// the parent chain* via the AuxPow. +/// +/// Mirror of src/impl/btc/coin/header_chain.hpp (class shape, namespace +/// conventions, include style). Re-homed into namespace nmc::coin so the NMC +/// coin tree is self-contained. Only core/* PUBLIC headers and the NMC-local +/// block.hpp are consumed. The btc tree is consumed READ-ONLY and is NOT +/// modified. +/// +/// ============================ P0 FENCE ==================================== +/// This is a STRUCTURE-ONLY leaf: correct types + class skeleton, NOT a +/// working consensus validator. In particular: +/// * AuxPow::check_proof() is a P0-DEFER STUB (see below). It does NOT walk +/// the coinbase merkle branch, does NOT verify the chain-merkle-root, and +/// does NOT verify the parent block's proof-of-work. NMC MUST NOT be used +/// to block-validate off this P0 leaf. +/// * Consensus constants (auxpow activation height, chain id, parent powhash +/// path) are NOT baked as compiled constants. They are left as +/// // TO-CONFIRM: placeholders with sentinel values, pending pinning +/// against Namecoin chainparams + namecoind. +/// ========================================================================== + +#include "block.hpp" +#include "transaction.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace nmc { +namespace coin { + +// uint256 hasher for unordered containers (low-64 bits is already a uniform +// distribution over a cryptographic hash, no further mixing needed). +struct Uint256Hasher { + size_t operator()(const uint256& h) const { return h.GetLow64(); } +}; + +// ─── PoW / identity hashes ─────────────────────────────────────────────────── + +/// Compute SHA256d hash of the plain NMC block header (block identification). +/// Namecoin uses SHA256d for both identity and (own-chain) proof-of-work, so +/// — as on BTC — the identity hash and the own-PoW hash are the same value. +inline uint256 block_hash(const BlockHeaderType& header) { + auto packed = pack(header); + return Hash(packed.get_span()); +} + +/// Own-chain proof-of-work hash. Identical to block_hash() on a SHA256d chain; +/// kept as a distinct function for call-site symmetry across coins (LTC splits +/// these: scrypt for PoW, SHA256d for identity). +inline uint256 pow_hash(const BlockHeaderType& header) { + auto packed = pack(header); + return Hash(packed.get_span()); +} + +// ─── Merkle-branch walk (P1: AuxPow merkle-proof primitive) ──────────────── + +/// Walk a merkle BRANCH up to its root, starting from `leaf`. +/// +/// The core primitive of AuxPow verification: BOTH legs of the proof are +/// merkle-branch walks — the chain-merkle leg (aux block hash → merged-mining +/// root, AuxPow::check_proof step 1) and the parent-coinbase leg (coinbase +/// txid → parent block tx-merkle-root, step 3). At level i, bit i of `index` +/// selects the side: if set, branch[i] is the LEFT sibling (running hash on the +/// right); else branch[i] is on the right. Each pair is SHA256d'd (the +/// Bitcoin/Namecoin merkle convention). +/// +/// Byte-faithful port of legacy libcoind/data.cpp check_merkle_link() — the +/// same SSOT btc/ltc use — kept NMC-LOCAL per the coin fence (no btc/ltc +/// include; only core/* primitives: Hash, PackStream). +inline uint256 aux_merkle_root(const uint256& leaf, + const std::vector& branch, + uint32_t index) { + if (!branch.empty() && index >= (1u << branch.size())) + throw std::invalid_argument("aux_merkle_root: index too large for branch depth"); + + uint256 cur = leaf; + for (size_t i = 0; i < branch.size(); ++i) { + PackStream ps; + if ((index >> i) & 1u) { + ps << branch[i]; // sibling on the left + ps << cur; + } else { + ps << cur; + ps << branch[i]; // sibling on the right + } + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + cur = Hash(sp); // SHA256d of the 64-byte concatenation + } + return cur; +} + +// ─── AuxPow (merge-mining proof) ───────────────────────────────────────────── + +/// One aux-chain slot inside a parent coinbase's merged-mining merkle tree. +/// +/// Kept NMC-LOCAL on purpose (P0 fence #4): do NOT lift this into +/// bitcoin_family/ or src/core/. A parent block can merge-mine several aux +/// chains at once; each occupies a deterministic slot in the chain-merkle-tree +/// keyed by (chain_id, merkle size, nonce). For NMC under a BTC parent the +/// list is normally length 1, but the type models the general case. +struct AuxChain { + // TO-CONFIRM: Namecoin mainnet chain id. Namecoin historically uses + // chain_id = 1 for mainnet merge-mining, but this MUST be pinned against + // Namecoin chainparams (CChainParams::nAuxpowChainId) + namecoind before + // it is treated as consensus. Sentinel below is a placeholder, NOT a + // committed constant. + int32_t chain_id{-1}; // TO-CONFIRM: sentinel (-1 = unpinned) + uint256 aux_block_hash; // the aux (NMC) block hash being committed + uint32_t merkle_index{0}; // slot index within the chain merkle tree +}; + +/// AuxPow — the merge-mining proof attached to a Namecoin header once +/// merge-mining is active. +/// +/// Field layout mirrors Namecoin's CAuxPow / Bitcoin's classic auxpow: +/// * the PARENT (BTC) coinbase transaction, which embeds the merged-mining +/// commitment in its scriptSig / an output script; +/// * the coinbase merkle BRANCH proving that coinbase is in the parent +/// block (parent_coinbase_branch + index); +/// * the CHAIN merkle BRANCH proving this aux chain's hash is in the +/// merged-mining merkle tree committed by the parent coinbase +/// (chain_merkle_branch + chain_index); +/// * the PARENT block header, whose proof-of-work is what actually backs +/// this Namecoin block. +/// +/// The parent coinbase is modelled with the NMC-local MutableTransaction type +/// (Bitcoin-format tx; Namecoin's parent is BTC, same serialization). We do +/// NOT pull btc::coin::MutableTransaction here — the nmc tree stays +/// self-contained per the fence. +struct AuxPow { + /// Parent (BTC) coinbase transaction carrying the MM commitment. + MutableTransaction parent_coinbase; + + /// Hash of the parent block (redundant with parent_header, kept for the + /// classic auxpow wire layout / quick checks). + uint256 parent_block_hash; + + /// Merkle branch + index linking parent_coinbase into the parent block's + /// transaction merkle root. + std::vector parent_coinbase_branch; + int32_t parent_coinbase_index{0}; + + /// Merkle branch + index linking this aux chain's blockhash into the + /// merged-mining merkle root committed inside the parent coinbase. + std::vector chain_merkle_branch; + int32_t chain_merkle_index{0}; + + /// The parent (BTC) block header. Its SHA256d PoW is the work that backs + /// the Namecoin block. We reuse the NMC-local BlockHeaderType because a + /// BTC parent header is byte-identical in layout. + BlockHeaderType parent_header; + + void SetNull() { + parent_coinbase = MutableTransaction{}; + parent_block_hash.SetNull(); + parent_coinbase_branch.clear(); + parent_coinbase_index = 0; + chain_merkle_branch.clear(); + chain_merkle_index = 0; + parent_header.SetNull(); + } + + bool IsNull() const { return parent_header.IsNull(); } + + /// Result of a (future) AuxPow verification. + enum class CheckResult { + NOT_IMPLEMENTED_P0, //!< P0 leaf — verification path not built yet + VALID, + INVALID, + }; + + /// Verify that this AuxPow actually proves work for `aux_block_hash` + /// against the given expected aux chain id. + /// + /// P0-DEFER: NOT IMPLEMENTED in P0. The real implementation must, against + /// a properly-pinned Namecoin chainparams: + /// 1. recompute the merged-mining merkle root from `chain_merkle_branch` + /// + `chain_merkle_index` starting at `aux_block_hash`, and confirm + /// the slot derivation matches (chain_id, nonce) — i.e. consume the + /// btc tree's parent merkle-branch helper READ-ONLY; + /// 2. confirm that root is committed in `parent_coinbase`'s MM marker; + /// 3. recompute the parent tx-merkle-root from `parent_coinbase_branch` + /// + `parent_coinbase_index` and confirm it == parent_header merkle + /// root; + /// 4. verify the PARENT block's proof-of-work (SHA256d(parent_header) < + /// target) — this is the parent-powhash verification path, consumed + /// from the btc tree READ-ONLY (e.g. btc::coin pow check). + /// Until all four steps exist, NMC MUST NOT block-validate off this leaf. + CheckResult check_proof(const uint256& aux_block_hash, + int32_t expected_chain_id) const { + // P0-DEFER: structural stub. Deliberately performs NO verification. + (void)aux_block_hash; + (void)expected_chain_id; + return CheckResult::NOT_IMPLEMENTED_P0; + } +}; + +// ─── Chain parameters ──────────────────────────────────────────────────────── + +/// NMC chain parameters. Mirror of btc::coin::BTCChainParams shape; Namecoin +/// shares Bitcoin's retarget window/spacing (SHA256d, 2016-block window, +/// 10-minute target). +struct NMCChainParams { + // Namecoin shares Bitcoin's retarget window/spacing. + static constexpr int64_t MAINNET_TARGET_TIMESPAN = 1209600; // 2 weeks + static constexpr int64_t MAINNET_TARGET_SPACING = 600; // 10 minutes + static constexpr bool MAINNET_ALLOW_MIN_DIFF = false; + + static constexpr int64_t TESTNET_TARGET_TIMESPAN = 1209600; // 2 weeks + static constexpr int64_t TESTNET_TARGET_SPACING = 600; // 10 minutes + static constexpr bool TESTNET_ALLOW_MIN_DIFF = true; + + static constexpr int64_t difficulty_adjustment_interval(int64_t timespan, int64_t spacing) { + return timespan / spacing; + } + + int64_t target_timespan{MAINNET_TARGET_TIMESPAN}; + int64_t target_spacing{MAINNET_TARGET_SPACING}; + bool allow_min_difficulty{false}; + bool no_retargeting{false}; + uint256 pow_limit; + uint256 genesis_hash; + + // ── Merge-mining consensus parameters ── + // + // TO-CONFIRM: NONE of the following are committed constants. They are + // placeholder sentinels pending pinning against Namecoin chainparams + // (CChainParams) + a live namecoind. Do NOT promote to consensus until + // verified. + // + // auxpow_activation_height: the height at/after which a Namecoin block MAY + // (and on mainnet, MUST) carry an AuxPow. Historically cited as 19200 on + // Namecoin mainnet — but this is left UNBAKED on purpose; -1 = unpinned. + int32_t auxpow_activation_height{-1}; // TO-CONFIRM: mainnet ~19200 (unpinned sentinel) + int32_t testnet_auxpow_activation_height{-1}; // TO-CONFIRM: testnet analog (unpinned sentinel) + // aux_chain_id: the chain id this NMC instance claims in the parent's + // merged-mining tree. -1 = unpinned sentinel. + int32_t aux_chain_id{-1}; // TO-CONFIRM: Namecoin mainnet chain id (unpinned) + + int64_t difficulty_adjustment_interval() const { + return target_timespan / target_spacing; + } + + /// Whether merge-mining is required at `height`. Returns false while the + /// activation height is unpinned (sentinel -1) so the P0 leaf never claims + /// merge-mining is active off a placeholder. + bool is_auxpow_active(int32_t height) const { + if (auxpow_activation_height < 0) return false; // TO-CONFIRM unpinned + return height >= auxpow_activation_height; + } + + /// NMC mainnet params skeleton. + /// TO-CONFIRM: pow_limit + genesis_hash below are placeholders copied from + /// the Bitcoin-family default; they MUST be replaced with Namecoin's actual + /// chainparams values before any validation use. + static NMCChainParams mainnet() { + NMCChainParams p; + p.target_timespan = MAINNET_TARGET_TIMESPAN; + p.target_spacing = MAINNET_TARGET_SPACING; + p.allow_min_difficulty = MAINNET_ALLOW_MIN_DIFF; + p.no_retargeting = false; + // TO-CONFIRM: Namecoin mainnet powLimit (placeholder = BTC default). + p.pow_limit.SetHex("00000000ffff0000000000000000000000000000000000000000000000000000"); + // TO-CONFIRM: Namecoin mainnet genesis hash (placeholder = null). + p.genesis_hash.SetNull(); + // TO-CONFIRM: activation height + chain id stay unpinned (-1). + return p; + } + + /// NMC testnet params skeleton (same TO-CONFIRM caveats as mainnet()). + static NMCChainParams testnet() { + NMCChainParams p; + p.target_timespan = TESTNET_TARGET_TIMESPAN; + p.target_spacing = TESTNET_TARGET_SPACING; + p.allow_min_difficulty = TESTNET_ALLOW_MIN_DIFF; + p.no_retargeting = false; + // TO-CONFIRM: Namecoin testnet powLimit (placeholder = BTC default). + p.pow_limit.SetHex("00000000ffff0000000000000000000000000000000000000000000000000000"); + // TO-CONFIRM: Namecoin testnet genesis hash (placeholder = null). + p.genesis_hash.SetNull(); + return p; + } +}; + +// ─── Index Entry ───────────────────────────────────────────────────────────── + +/// Status flags for header validation state (mirror of btc::coin shape). +enum HeaderStatus : uint32_t { + HEADER_VALID_UNKNOWN = 0, + HEADER_VALID_HEADER = 1, // Header parsed and (own) PoW valid + HEADER_VALID_TREE = 2, // Connected to genesis via prev_hash chain + HEADER_VALID_CHAIN = 3, // Difficulty validated +}; + +/// A validated header with chain metadata (in-memory representation). +/// +/// Mirror of btc::coin::IndexEntry, with an OPTIONAL AuxPow attachment: once +/// merge-mining is active a Namecoin header carries its parent-chain proof. +/// In P0 the AuxPow is stored structurally but NOT verified. +struct IndexEntry { + BlockHeaderType header; + uint256 block_hash; // SHA256d(header) + uint32_t height{0}; + uint256 chain_work; // cumulative work up to this header + HeaderStatus status{HEADER_VALID_UNKNOWN}; + std::optional auxpow; // merge-mining proof (P0: stored, unverified) +}; + +// ─── HeaderChain ───────────────────────────────────────────────────────────── + +/// Header-only chain skeleton for embedded NMC. Mirrors the public surface of +/// btc::coin::HeaderChain. P0: the validation internals (difficulty checks, +/// AuxPow checks, LevelDB persistence) are intentionally NOT implemented — the +/// methods below are structural skeletons so call sites compile. +class HeaderChain { +public: + explicit HeaderChain(const NMCChainParams& params, const std::string& db_path = "") + : m_params(params) + , m_db_path(db_path) + { + } + + ~HeaderChain() = default; + + // Disable copy (mirror of btc). + HeaderChain(const HeaderChain&) = delete; + HeaderChain& operator=(const HeaderChain&) = delete; + + /// Initialize the chain. + /// P0-DEFER: no LevelDB open, no persisted-state load, no checkpoint seed. + /// Returns true (structural success) so wiring smoke-tests pass. + bool init() { + LOG_INFO << "[EMB-NMC] HeaderChain::init() (P0 structural stub) db_path=" + << (m_db_path.empty() ? "(in-memory)" : m_db_path); + // P0-DEFER: persistence + checkpoint-seed not implemented. + return true; + } + + /// Current chain tip (best header). P0: empty until add path is built. + std::optional tip() const { + std::lock_guard lock(m_mutex); + if (m_tip.IsNull()) return std::nullopt; + auto it = m_index.find(m_tip); + if (it == m_index.end()) return std::nullopt; + return it->second; + } + + /// Current chain tip height (0 if empty). + uint32_t height() const { + std::lock_guard lock(m_mutex); + return m_tip_height; + } + + /// Cumulative work at the tip. + uint256 cumulative_work() const { + std::lock_guard lock(m_mutex); + return m_best_work; + } + + /// Number of headers tracked. + size_t size() const { + std::lock_guard lock(m_mutex); + return m_index.size(); + } + + /// Whether a header with `block_hash` is known. + bool has_header(const uint256& block_hash) const { + std::lock_guard lock(m_mutex); + return m_index.find(block_hash) != m_index.end(); + } + + /// Look up a header by its block hash. + std::optional get_header(const uint256& block_hash) const { + std::lock_guard lock(m_mutex); + auto it = m_index.find(block_hash); + if (it == m_index.end()) return std::nullopt; + return it->second; + } + + /// Add a single plain header. + /// P0-DEFER: NO difficulty validation, NO PoW check, NO connection check, + /// NO persistence. Structural skeleton only — returns false. + bool add_header(const BlockHeaderType& header) { + (void)header; + // P0-DEFER: header add/validate path not implemented. + return false; + } + + /// Add a header that carries a merge-mining AuxPow proof. + /// P0-DEFER: stores nothing and verifies nothing. The real path must call + /// AuxPow::check_proof() (itself a P0 stub) before accepting. + bool add_auxpow_header(const BlockHeaderType& header, const AuxPow& auxpow) { + (void)header; + (void)auxpow; + // P0-DEFER: auxpow header add/validate path not implemented. + // NOTE: even when built, must reject unless params.is_auxpow_active(h) + // and auxpow.check_proof(block_hash(header), params.aux_chain_id) + // == AuxPow::CheckResult::VALID. + return false; + } + + /// Add a batch of plain headers. P0-DEFER: returns 0 (none accepted). + int add_headers(const std::vector& headers) { + (void)headers; + // P0-DEFER: batch header path not implemented. + return 0; + } + + const NMCChainParams& params() const { return m_params; } + +private: + NMCChainParams m_params; + std::string m_db_path; + + mutable std::mutex m_mutex; + std::unordered_map m_index; + uint256 m_tip; // null until first accepted header + uint32_t m_tip_height{0}; + uint256 m_best_work; + + // P0-DEFER: difficulty validation, AuxPow verification, LevelDB + // persistence, height-index rebuild, and locator generation are all + // deliberately absent in this structural leaf. +}; + +} // namespace coin +} // namespace nmc diff --git a/src/impl/nmc/coin/transaction.cpp b/src/impl/nmc/coin/transaction.cpp new file mode 100644 index 000000000..22179e68b --- /dev/null +++ b/src/impl/nmc/coin/transaction.cpp @@ -0,0 +1,24 @@ +#include "transaction.hpp" + +namespace nmc +{ + +namespace coin +{ + +Transaction::Transaction(const MutableTransaction& tx) : vin(tx.vin), vout(tx.vout), version(tx.version), locktime(tx.locktime), m_has_witness{ComputeHasWitness()} {} +Transaction::Transaction(MutableTransaction&& tx) : vin(std::move(tx.vin)), vout(std::move(tx.vout)), version(tx.version), locktime(tx.locktime), m_has_witness{ComputeHasWitness()} {} + +bool Transaction::ComputeHasWitness() const +{ + return std::any_of(vin.begin(), vin.end(), [](const auto& input) { + return !input.scriptWitness.IsNull(); + }); +} + +MutableTransaction::MutableTransaction() : version(Transaction::CURRENT_VERSION), locktime(0) {} +MutableTransaction::MutableTransaction(const Transaction& tx) : version(tx.version), vin(tx.vin), vout(tx.vout), locktime(tx.locktime) {} + +} // namespace coin + +} // namespace nmc diff --git a/src/impl/nmc/coin/transaction.hpp b/src/impl/nmc/coin/transaction.hpp new file mode 100644 index 000000000..c61f9849b --- /dev/null +++ b/src/impl/nmc/coin/transaction.hpp @@ -0,0 +1,228 @@ +#pragma once + +// NMC (Namecoin) transaction primitives. +// +// Mirror of src/impl/btc/coin/transaction.hpp — Namecoin is a Bitcoin fork and +// shares Bitcoin's transaction serialization format (witness-aware, BIP144). +// Re-homed into namespace nmc::coin so the NMC coin tree is self-contained and +// does not pull btc::coin symbols. Only core/* PUBLIC headers are used; no btc +// header is modified. + +#include +#include +#include + +namespace nmc +{ + +namespace coin +{ + +struct MutableTransaction; + +struct TxParams +{ + const bool allow_witness; + + SER_PARAMS_OPFUNC +}; + +constexpr static TxParams TX_WITH_WITNESS {.allow_witness = true}; +constexpr static TxParams TX_NO_WITNESS {.allow_witness = false}; + +class TxPrevOut +{ +public: + uint256 hash; + uint32_t index; + + C2POOL_SERIALIZE_METHODS(TxPrevOut) { READWRITE(obj.hash, obj.index); } +}; + +class TxIn +{ +public: + TxPrevOut prevout; + OPScript scriptSig; + uint32_t sequence; + OPScriptWitness scriptWitness; //!< Only serialized through Transaction + + C2POOL_SERIALIZE_METHODS(TxIn) { READWRITE(obj.prevout, obj.scriptSig, obj.sequence); } +}; + +class TxOut +{ +public: + int64_t value; + OPScript scriptPubKey; + + C2POOL_SERIALIZE_METHODS(TxOut) { READWRITE(obj.value, obj.scriptPubKey); } +}; + +class Transaction +{ +public: + // Default transaction version. + static const int32_t CURRENT_VERSION = 2; + + std::vector vin; + std::vector vout; + int32_t version; + uint32_t locktime; + +private: + bool m_has_witness; + + bool ComputeHasWitness() const; + +public: + /** Convert a MutableTransaction into a Transaction. */ + explicit Transaction(const MutableTransaction& tx) ; + explicit Transaction(MutableTransaction&& tx); + + template + inline void Serialize(StreamType& os) const + { + SerializeTransaction(*this, os, os.GetParams()); + } + + bool HasWitness() const { return m_has_witness; } +}; + +/** A mutable version of Transaction. */ +struct MutableTransaction +{ + std::vector vin; + std::vector vout; + int32_t version; + uint32_t locktime; + + explicit MutableTransaction(); + explicit MutableTransaction(const Transaction& tx); + + template + inline void Serialize(StreamType& os) const + { + SerializeTransaction(*this, os, os.GetParams()); + } + + template + inline void Unserialize(StreamType& os) + { + UnserializeTransaction(*this, os, os.GetParams()); + } + + bool HasWitness() const + { + for (size_t i = 0; i < vin.size(); i++) + { + if (!vin[i].scriptWitness.IsNull()) + return true; + } + return false; + } +}; + +/** + * Basic transaction serialization format: + * - int32_t version + * - std::vector vin + * - std::vector vout + * - uint32_t locktime + * + * Extended transaction serialization format: + * - int32_t version + * - unsigned char dummy = 0x00 + * - unsigned char flags (!= 0) + * - std::vector vin + * - std::vector vout + * - if (flags & 1): + * - OPScriptWitness scriptWitness; (deserialized into TxIn) + * - uint32_t locktime + */ +template +void UnserializeTransaction(TxType& tx, StreamType& s, const TxParams& params) +{ + const bool fAllowWitness = params.allow_witness; + + s >> tx.version; + unsigned char flags = 0; + tx.vin.clear(); + tx.vout.clear(); + /* Try to read the vin. In case the dummy is there, this will be read as an empty vector. */ + s >> tx.vin; + if (tx.vin.size() == 0 && fAllowWitness) + { + /* We read a dummy or an empty vin. */ + s >> flags; + if (flags != 0) + { + s >> tx.vin; + s >> tx.vout; + } + } else + { + /* We read a non-empty vin. Assume a normal vout follows. */ + s >> tx.vout; + } + if ((flags & 1) && fAllowWitness) + { + /* The witness flag is present, and we support witnesses. */ + flags ^= 1; + for (size_t i = 0; i < tx.vin.size(); i++) + { + s >> tx.vin[i].scriptWitness.stack; + } + if (!tx.HasWitness()) + { + /* It's illegal to encode witnesses when all witness stacks are empty. */ + throw std::ios_base::failure("Superfluous witness record"); + } + } + + if (flags) + { + /* Unknown flag in the serialization */ + throw std::ios_base::failure("Unknown transaction optional data"); + } + s >> tx.locktime; +} + +template +void SerializeTransaction(const TxType& tx, StreamType& s, const TxParams& params) +{ + const bool fAllowWitness = params.allow_witness; + + s << tx.version; + unsigned char flags = 0; + // Consistency check + if (fAllowWitness) + { + /* Check whether witnesses need to be serialized. */ + if (tx.HasWitness()) + { + flags |= 1; + } + } + if (flags) + { + /* Use extended format in case witnesses to be serialized. */ + std::vector vinDummy; + s << vinDummy; + s << flags; + } + s << tx.vin; + s << tx.vout; + if (flags & 1) + { + for (size_t i = 0; i < tx.vin.size(); i++) + { + s << tx.vin[i].scriptWitness.stack; + } + } + s << tx.locktime; +} + +} // namespace coin + +} // namespace nmc diff --git a/src/impl/nmc/test/CMakeLists.txt b/src/impl/nmc/test/CMakeLists.txt new file mode 100644 index 000000000..619f076dd --- /dev/null +++ b/src/impl/nmc/test/CMakeLists.txt @@ -0,0 +1,26 @@ +# NMC module tests (P1). Mirrors the dgb header-only guard registration in +# src/impl/dgb/test/CMakeLists.txt: gated on BUILD_TESTING + GTest, registered +# via gtest_add_tests for the CI --target allowlist. +# +# nmc_auxpow_merkle_test pins the aux_merkle_root walk in coin/header_chain.hpp +# (the AuxPow merge-mining merkle-branch walk). It links core because the walk +# folds nodes with core::Hash / PackStream; it does NOT link any node/pool/ +# share OBJECT lib (none exist for NMC yet -- P0/P1 is coin-tree only). +# +# Each target MUST appear in BOTH this registration AND the build.yml +# --target allowlist, or it becomes a NOT_BUILT sentinel that reds master. +if (BUILD_TESTING AND GTest_FOUND) + add_executable(nmc_auxpow_merkle_test auxpow_merkle_test.cpp) + target_link_libraries(nmc_auxpow_merkle_test PRIVATE + GTest::gtest_main GTest::gtest + core nlohmann_json::nlohmann_json) + # core pulls stratum_server.cpp (hashrate refs); link the same OBJECT-lib + # SCC the sibling dgb merkle test links so core resolves. No node/pool/ + # share/coin libs are needed -- this KAT only folds with core::Hash. + target_link_libraries(nmc_auxpow_merkle_test PRIVATE + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage) + + include(GoogleTest) + include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) + gtest_add_tests(nmc_auxpow_merkle_test "" AUTO) +endif() diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp new file mode 100644 index 000000000..379223133 --- /dev/null +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -0,0 +1,103 @@ +// --------------------------------------------------------------------------- +// nmc::coin::aux_merkle_root KAT (P1 — AuxPow merkle-proof walk). +// +// Pins the merge-mining merkle-branch walk that AuxPow::check_proof() (still a +// P-DEFER stub) will consume for step-1 (chain-merkle-root) and step-3 (parent +// tx-merkle-root). The walk is a byte-faithful port of legacy +// libcoind/data.cpp check_merkle_link() — the same SSOT btc uses — so these +// KATs lock the consensus-relevant ordering: +// * empty branch => root == leaf (identity); +// * index bit i selects whether branch[i] is the LEFT or RIGHT sibling at +// depth i, folded with double-SHA256 of the 64-byte concatenation; +// * an index that does not fit in branch.size() bits is rejected. +// +// Expected roots are derived INDEPENDENTLY here via core::Hash on the explicit +// concatenation (not by calling aux_merkle_root), so a side-swap or fold bug in +// the walk is caught rather than mirrored. Self-derived => no fixture file. +// +// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY (this is +// an independent NMC-local re-derivation, fence #4). MUST appear in BOTH +// test/CMakeLists.txt AND the build.yml --target allowlist or it becomes a +// NOT_BUILT sentinel that reds master. +// --------------------------------------------------------------------------- + +#include + +#include +#include + +#include +#include +#include + +#include "../coin/header_chain.hpp" + +namespace { + +using nmc::coin::aux_merkle_root; + +// Build a uint256 whose first byte is `b` (rest zero) — distinct, legible leaves. +static uint256 leaf_of(unsigned char b) +{ + uint256 u; + u.SetNull(); + *(u.begin()) = b; + return u; +} + +// Independent reference combine: double-SHA256 of l||r, mirroring the legacy +// merkle node hash but computed here WITHOUT touching aux_merkle_root. +static uint256 combine(const uint256& l, const uint256& r) +{ + PackStream ps; + ps << l; + ps << r; + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + return Hash(sp); +} + +TEST(NmcAuxMerkle, EmptyBranchIsIdentity) +{ + uint256 leaf = leaf_of(0x11); + EXPECT_EQ(aux_merkle_root(leaf, {}, 0), leaf); + // index is ignored when there is no branch to walk. + EXPECT_EQ(aux_merkle_root(leaf, {}, 12345u), leaf); +} + +TEST(NmcAuxMerkle, SingleStepIndexZeroLeafOnLeft) +{ + uint256 leaf = leaf_of(0x11); + uint256 sib = leaf_of(0x22); + // index bit 0 == 0 => leaf is LEFT, sibling RIGHT. + EXPECT_EQ(aux_merkle_root(leaf, {sib}, 0), combine(leaf, sib)); +} + +TEST(NmcAuxMerkle, SingleStepIndexOneLeafOnRight) +{ + uint256 leaf = leaf_of(0x11); + uint256 sib = leaf_of(0x22); + // index bit 0 == 1 => sibling LEFT, leaf RIGHT. + EXPECT_EQ(aux_merkle_root(leaf, {sib}, 1), combine(sib, leaf)); +} + +TEST(NmcAuxMerkle, TwoStepFoldRespectsPerLevelIndexBits) +{ + uint256 leaf = leaf_of(0x11); + uint256 b0 = leaf_of(0x22); + uint256 b1 = leaf_of(0x33); + // index = 0b10: level0 bit=0 (leaf left), level1 bit=1 (accum right). + uint256 lvl0 = combine(leaf, b0); + uint256 want = combine(b1, lvl0); + EXPECT_EQ(aux_merkle_root(leaf, {b0, b1}, 0b10u), want); +} + +TEST(NmcAuxMerkle, IndexTooLargeForBranchThrows) +{ + uint256 leaf = leaf_of(0x11); + uint256 sib = leaf_of(0x22); + // one-element branch admits indices 0..1; 2 overflows the implied tree. + EXPECT_THROW(aux_merkle_root(leaf, {sib}, 2u), std::invalid_argument); +} + +} // namespace