diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index 17b918076..bf06ccb93 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -40,7 +40,9 @@ #include #include #include +#include // chain::bits_to_target (parent-PoW, step 4) +#include #include #include #include @@ -117,6 +119,122 @@ inline uint256 aux_merkle_root(const uint256& leaf, return cur; } +// ─── Parent-coinbase txid (P1c: AuxPow step-3 leaf identity) ─────── + +/// Witness-stripped transaction id of the parent (BTC) coinbase. +/// +/// AuxPow step 3 proves the parent coinbase is committed in the parent +/// block's transaction merkle root. That tree commits *txids*, NOT wtxids — +/// so the coinbase id MUST be hashed over the LEGACY (no-witness, no segwit +/// marker/flag) serialization even though a real BTC coinbase carries a +/// witness (the BIP141 segwit-commitment reserved value). Byte-for-byte +/// identical to the btc tree's compute_txid() (mempool.hpp) but kept +/// NMC-LOCAL per the coin fence: consumes only the nmc::coin transaction +/// serializer (TX_NO_WITNESS) + core::Hash, no btc include. +inline uint256 parent_coinbase_txid(const MutableTransaction& tx) { + auto packed = pack(TX_NO_WITNESS(tx)); + return Hash(packed.get_span()); +} + +// ─── Merged-mining commitment scan (P1c-step2: AuxPow step-2 binding) ── + +/// pchMergedMiningHeader — the 4-byte magic tag that precedes the merged-mining +/// commitment inside the parent coinbase scriptSig. Byte-faithful with +/// Namecoin/Bitcoin auxpow.cpp (the same SSOT btc/doge consume); kept NMC-LOCAL +/// per the coin fence (only std + core types, no btc include). +inline constexpr unsigned char MM_HEADER_MAGIC[4] = {0xfa, 0xbe, 'm', 'm'}; + +/// Read a little-endian uint32 from 4 bytes WITHOUT a host-endian memcpy: the +/// on-wire MM size/nonce fields are always little-endian. +inline uint32_t read_le32(const unsigned char* p) { + return static_cast(p[0]) + | (static_cast(p[1]) << 8) + | (static_cast(p[2]) << 16) + | (static_cast(p[3]) << 24); +} + +/// Deterministic chain-merkle slot for a (nonce, chain_id, height) triple. +/// Byte-faithful port of Namecoin/Bitcoin CAuxPow::getExpectedIndex: pins each +/// aux chain to a pseudo-random-but-fixed slot so the same parent work cannot be +/// replayed for two chains and inter-chain slot clashes are minimised. The +/// arithmetic is intentionally wrap-around uint32 (LCG constants 1103515245 / +/// 12345) to match the reference bit-for-bit. +inline uint32_t aux_expected_index(uint32_t nonce, int32_t chain_id, unsigned height) { + uint32_t rnd = nonce; + rnd = rnd * 1103515245u + 12345u; + rnd += static_cast(chain_id); + rnd = rnd * 1103515245u + 12345u; + return rnd % (1u << height); +} + +/// Result of scanning a parent coinbase scriptSig for the merged-mining +/// commitment of a given chain-merkle root. +enum class MMScan { + ABSENT, //!< no MM magic in the scriptSig — nothing to assert (staged leg) + MATCH, //!< magic found and root/size/nonce/slot all bind correctly + MISMATCH, //!< magic found but the commitment is malformed or inconsistent +}; + +/// Step 2 of AuxPow verification: confirm the reconstructed chain-merkle root is +/// the one committed inside the parent coinbase's merged-mining marker, and that +/// this chain occupies the slot the (nonce, chain_id) binding demands. +/// +/// Byte-faithful port of the coinbase scan in Namecoin/Bitcoin auxpow.cpp +/// (CAuxPow::check), kept NMC-LOCAL per the coin fence (only std + core types): +/// * the 4-byte MM magic must appear exactly once; +/// * the chain-merkle root must follow the magic immediately, stored in the +/// REVERSED (big-endian display) byte order auxpow.cpp uses; +/// * the next 4 LE bytes are the tree size and MUST equal 2^height; +/// * the following 4 LE bytes are the nonce, and chain_index MUST equal +/// aux_expected_index(nonce, chain_id, height). +/// Returns ABSENT when the magic is absent entirely — the staged-leg posture +/// (mirrors the null-parent-header gate in step 3): step 2 cannot be asserted off +/// a fixture carrying no marker, and the proof never upgrades to VALID regardless. +/// NOTE (constant-pinning): the legacy no-magic "root in the first 20 bytes" +/// back-compat branch of the reference is intentionally NOT honoured — post- +/// activation Namecoin merge-mining always carries the magic; the final +/// endianness/layout cross-check happens against a live namecoind at pinning. +/// chain_merkle_root is taken by value (base_uint::begin() is non-const). +inline MMScan scan_mm_commitment(const std::vector& script, + uint256 chain_merkle_root, + unsigned chain_height, + int32_t expected_chain_id, + uint32_t chain_index) { + const unsigned char* magic = MM_HEADER_MAGIC; + auto magic_it = std::search(script.begin(), script.end(), magic, magic + 4); + if (magic_it == script.end()) + return MMScan::ABSENT; // staged: no commitment to bind against yet + + // Replay guard: exactly one MM header may appear (the reference rejects a + // second so one parent block cannot smuggle two commitments for one chain). + if (std::search(magic_it + 4, script.end(), magic, magic + 4) != script.end()) + return MMScan::MISMATCH; + + // The chain-merkle root is committed reversed (big-endian display order). + const unsigned char* rb = + reinterpret_cast(chain_merkle_root.begin()); + std::vector root_be(rb, rb + uint256::BYTES); + std::reverse(root_be.begin(), root_be.end()); + + auto root_pos = magic_it + 4; + if (static_cast(script.end() - root_pos) < root_be.size()) + return MMScan::MISMATCH; // truncated before the root + if (!std::equal(root_be.begin(), root_be.end(), root_pos)) + return MMScan::MISMATCH; // magic present but commits a different root + + auto pc = root_pos + static_cast(root_be.size()); + if (script.end() - pc < 8) + return MMScan::MISMATCH; // missing the 4-byte size + 4-byte nonce + uint32_t n_size = read_le32(&*pc); + uint32_t n_nonce = read_le32(&*(pc + 4)); + if (n_size != (1u << chain_height)) + return MMScan::MISMATCH; // tree size disagrees with the branch depth + if (chain_index != aux_expected_index(n_nonce, expected_chain_id, chain_height)) + return MMScan::MISMATCH; // chain sits in the wrong deterministic slot + + return MMScan::MATCH; +} + // ─── AuxPow (merge-mining proof) ───────────────────────────────────────────── /// One aux-chain slot inside a parent coinbase's merged-mining merkle tree. @@ -193,6 +311,7 @@ struct AuxPow { /// Result of a (future) AuxPow verification. enum class CheckResult { NOT_IMPLEMENTED_P0, //!< P0 leaf — verification path not built yet + INCOMPLETE, //!< P1b — merkle leg(s) wired; steps 2/3/4 pending VALID, INVALID, }; @@ -200,7 +319,8 @@ struct AuxPow { /// 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 + /// P1b: step 1 (chain-merkle leg) is wired below via aux_merkle_root(). + /// The FULL 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 @@ -215,11 +335,110 @@ struct AuxPow { /// 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; + int32_t expected_chain_id, + uint32_t aux_bits = 0) const { + // P1b+P1c — three merkle legs wired. Step 1 (chain-merkle) walks the + // aux (NMC) block hash through chain_merkle_branch/index to reconstruct + // the merged-mining root; step 2 (MM-marker) confirms that root is the + // one committed inside the parent coinbase's merged-mining marker, with + // the (nonce, chain_id) slot binding; step 3 (parent-coinbase tx-merkle) + // walks the witness-stripped parent coinbase txid through + // parent_coinbase_branch/index and, when a parent header is present, + // requires it to reproduce that header's tx-merkle-root; step 4 (parent + // PoW) verifies the parent block's SHA256d hash clears the AUX block's + // target. A structurally-complete proof — all four legs, a matched MM + // marker, a real parent header, and an aux_bits that the parent PoW + // satisfies — is VALID. Absent any of those (a leg-only fixture: null + // parent, no marker, or aux_bits unset) the proof stays INCOMPLETE, so + // NMC never block-validates off a partial proof. Any malformed leg (negative slot index, an + // index wider than its branch depth, a parent-coinbase leg that does not + // reconstruct the parent header's tx-merkle-root, or an MM marker that + // commits a different root / wrong tree-size / wrong slot) is INVALID. + + // ── step 1 (P1b): chain-merkle leg ── + if (chain_merkle_index < 0) + return CheckResult::INVALID; // negative slot index is malformed + uint256 reconstructed_mm_root; + try { + // Reconstruct the merged-mining merkle root; step 2 binds it to the + // parent coinbase commitment. + reconstructed_mm_root = aux_merkle_root( + aux_block_hash, chain_merkle_branch, + static_cast(chain_merkle_index)); + } catch (const std::invalid_argument&) { + return CheckResult::INVALID; // branch index out of range for depth + } + + // ── step 2 (P1c-step2): MM-marker commitment + chain_id/slot binding ── + // Confirm the reconstructed MM root is the one committed inside the + // parent coinbase's merged-mining marker, with the (nonce, chain_id) + // slot binding. When the coinbase carries no marker (a leg-only + // structural fixture, or an empty parent coinbase) there is nothing to + // assert yet — staged posture, mirrors the null-parent-header gate in + // step 3. A marker present but committing a different root / wrong size / + // wrong slot is INVALID. + bool mm_marker_matched = false; + if (!parent_coinbase.vin.empty()) { + MMScan scan = scan_mm_commitment( + parent_coinbase.vin[0].scriptSig.m_data, + reconstructed_mm_root, + static_cast(chain_merkle_branch.size()), + expected_chain_id, + static_cast(chain_merkle_index)); + if (scan == MMScan::MISMATCH) + return CheckResult::INVALID; + mm_marker_matched = (scan == MMScan::MATCH); + } + + // ── step 3 (P1c): parent-coinbase tx-merkle leg ── + if (parent_coinbase_index < 0) + return CheckResult::INVALID; // negative slot index is malformed + uint256 reconstructed_parent_merkle; + try { + // The parent tx-merkle tree commits the WITNESS-STRIPPED txid. + reconstructed_parent_merkle = aux_merkle_root( + parent_coinbase_txid(parent_coinbase), parent_coinbase_branch, + static_cast(parent_coinbase_index)); + } catch (const std::invalid_argument&) { + return CheckResult::INVALID; // branch index out of range for depth + } + // The equality gate only fires once a parent header is present: a real + // proof always carries one, but a leg-only structural fixture leaves it + // null and has nothing to match against yet (step 2 may also pend). + if (!parent_header.IsNull() && + reconstructed_parent_merkle != parent_header.m_merkle_root) + return CheckResult::INVALID; + + // ── step 4 (P1c-step4): parent proof-of-work vs the AUX target ── + // The parent (BTC) block's SHA256d PoW hash is the work backing this + // Namecoin block. Per Namecoin's CheckAuxPowProofOfWork the parent PoW + // hash must satisfy the AUX (NMC) block's difficulty target — aux_bits, + // the NMC header's nBits — NOT the parent header's own nBits. Target is + // expanded with the shared core helper chain::bits_to_target(); the hash + // is the nmc-local pow_hash(parent_header). No btc-tree dependency: both + // helpers live outside src/impl/btc (core + nmc-local). + // + // aux_bits == 0 is the leg-only-fixture sentinel (caller has not supplied + // the aux header's difficulty): step 4 is skipped and the proof stays + // INCOMPLETE, mirroring the null-parent / no-marker gates above. + bool parent_pow_ok = false; + if (!parent_header.IsNull() && aux_bits != 0) { + uint256 parent_powhash = pow_hash(parent_header); + uint256 aux_target = chain::bits_to_target(aux_bits); + // Valid PoW means hash <= target, i.e. NOT (hash > target) — same + // ordering convention as the btc work_source block/share gate. + if (parent_powhash > aux_target) + return CheckResult::INVALID; // parent has insufficient work + parent_pow_ok = true; + } + + // Only a structurally-complete proof is VALID: all four legs satisfied + // against a real parent header, a matched MM-marker commitment, and a + // parent PoW that clears the aux target. Anything short stays INCOMPLETE. + if (parent_pow_ok && mm_marker_matched && !parent_header.IsNull()) + return CheckResult::VALID; + + return CheckResult::INCOMPLETE; } }; @@ -414,16 +633,36 @@ class HeaderChain { return false; } + /// Verify the AuxPow consensus GATE for an incoming merge-mined header. + /// P1d: factored out so the gate is unit-testable independently of the + /// (still-deferred) header-storage path. Runs the full four-leg + /// AuxPow::check_proof against the AUX block hash (block_hash(header)), this + /// chain's claimed aux_chain_id, and the header's own nBits as the aux PoW + /// target (Namecoin checks the parent PoW against the AUX bits, not the + /// parent's own). A header is admissible ONLY when this returns VALID. + AuxPow::CheckResult verify_auxpow_header(const BlockHeaderType& header, + const AuxPow& auxpow) const { + return auxpow.check_proof(block_hash(header), m_params.aux_chain_id, + header.m_bits); + } + /// 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. + /// P1d: the AuxPow VERIFICATION GATE is now wired - check_proof() must + /// return VALID or the header is rejected, so the (future) accept path can + /// never admit an unproven merge-mined header. The header-storage / + /// chain-connection path, and the height-derived is_auxpow_active() + /// activation gate (which needs a connected parent), remain P0-DEFER - see + /// add_header(). A header that passes the gate is therefore NOT persisted + /// yet; P1d's contract is the rejection half: nothing unproven gets in. 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. + if (verify_auxpow_header(header, auxpow) != AuxPow::CheckResult::VALID) { + LOG_WARNING << "[EMB-NMC] reject auxpow header " + << block_hash(header).ToString() + << ": AuxPow check_proof != VALID"; + return false; + } + // P0-DEFER: proof verified, but header storage/connection + the + // is_auxpow_active(height) activation gate are not built yet. return false; } diff --git a/src/impl/nmc/test/CMakeLists.txt b/src/impl/nmc/test/CMakeLists.txt index 619f076dd..c00731fb1 100644 --- a/src/impl/nmc/test/CMakeLists.txt +++ b/src/impl/nmc/test/CMakeLists.txt @@ -19,6 +19,10 @@ if (BUILD_TESTING AND GTest_FOUND) # 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) + # P1b: the check_proof KATs construct AuxPow, whose parent_coinbase member + # is an nmc::coin::MutableTransaction with an out-of-line ctor -> link the + # nmc_coin lib (transaction.cpp). Still no node/pool/share libs (none exist). + target_link_libraries(nmc_auxpow_merkle_test PRIVATE nmc_coin) include(GoogleTest) include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index 379223133..5ebc00f5f 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -23,18 +23,22 @@ #include +#include #include #include #include #include #include +#include #include "../coin/header_chain.hpp" namespace { using nmc::coin::aux_merkle_root; +using nmc::coin::AuxPow; +using nmc::coin::pow_hash; // Build a uint256 whose first byte is `b` (rest zero) — distinct, legible leaves. static uint256 leaf_of(unsigned char b) @@ -100,4 +104,535 @@ TEST(NmcAuxMerkle, IndexTooLargeForBranchThrows) EXPECT_THROW(aux_merkle_root(leaf, {sib}, 2u), std::invalid_argument); } + +// --------------------------------------------------------------------------- +// P1b: AuxPow::check_proof chain-merkle leg (step 1) wiring. +// +// check_proof() now reconstructs the merged-mining merkle root from the aux +// block hash through chain_merkle_branch/index via aux_merkle_root(). Steps 2 +// (MM-marker commitment), 3 (parent-coinbase tx-merkle leg) and 4 (parent PoW) +// are NOT built, so a structurally-consistent walk returns INCOMPLETE — never +// VALID. A malformed slot index (negative, or wider than the branch depth) +// returns INVALID. These KATs pin exactly that boundary so the leg can never +// silently regress into asserting VALID before the rest of the proof exists. +// --------------------------------------------------------------------------- + +TEST(NmcAuxCheckProof, ChainLegStructurallyValidIsIncomplete) +{ + AuxPow ap; + ap.chain_merkle_branch = {leaf_of(0xbe)}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(leaf_of(0x01), /*expected_chain_id=*/1), + AuxPow::CheckResult::INCOMPLETE); +} + +TEST(NmcAuxCheckProof, EmptyChainBranchIsIncompleteNeverValid) +{ + AuxPow ap; // empty branch, index 0 => identity walk, still not provable + EXPECT_EQ(ap.check_proof(leaf_of(0x07), 1), + AuxPow::CheckResult::INCOMPLETE); + EXPECT_NE(ap.check_proof(leaf_of(0x07), 1), + AuxPow::CheckResult::VALID); +} + +TEST(NmcAuxCheckProof, NegativeChainIndexIsInvalid) +{ + AuxPow ap; + ap.chain_merkle_branch = {leaf_of(0x0a)}; + ap.chain_merkle_index = -1; + EXPECT_EQ(ap.check_proof(leaf_of(0x02), 1), + AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxCheckProof, ChainIndexTooWideForDepthIsInvalid) +{ + AuxPow ap; + ap.chain_merkle_branch = {leaf_of(0x0a)}; // depth 1 => max index 1 + ap.chain_merkle_index = 2; + EXPECT_EQ(ap.check_proof(leaf_of(0x02), 1), + AuxPow::CheckResult::INVALID); +} + +// --------------------------------------------------------------------------- +// P1c: AuxPow::check_proof parent-coinbase tx-merkle leg (step 3) wiring. +// +// step 3 reconstructs the parent block's transaction merkle root from the +// WITNESS-STRIPPED parent coinbase txid through parent_coinbase_branch/index +// via aux_merkle_root(), and (once a parent header is present) requires it to +// equal parent_header.m_merkle_root. The load-bearing test asserts the exact +// byte serialization of the txid: it is hashed over the LEGACY (no-witness) +// layout -- txid, NOT wtxid -- so a coinbase carrying the BIP141 segwit +// reserved value still hashes witness-stripped. The legacy preimage is +// re-derived here field-by-field WITHOUT calling SerializeTransaction's +// witness/marker path, so a stray marker/flag byte is caught, not mirrored. +// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY. +// --------------------------------------------------------------------------- + +using nmc::coin::MutableTransaction; +using nmc::coin::TxIn; +using nmc::coin::TxOut; +using nmc::coin::parent_coinbase_txid; + +// A minimal parent (BTC) coinbase carrying the BIP141 witness reserved value +// (single 32-byte zero stack item) -- exactly what a real segwit coinbase +// carries, so the txid path is forced to strip a present witness. +static MutableTransaction make_parent_coinbase() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffffu; + static const unsigned char sig[] = {0x03, 0x4e, 0x4d, 0x43}; // arbitrary scriptSig + in.scriptSig = OPScript(sig, sig + sizeof(sig)); + in.sequence = 0xffffffffu; + tx.vin.push_back(in); + TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + tx.vin[0].scriptWitness.stack.assign(1, std::vector(32, 0x00)); + return tx; +} + +// Independent legacy (no-witness) txid: lay out version|vin|vout|locktime via +// PackStream directly (the TxIn serializer writes prevout+scriptSig+sequence, +// NO witness) and double-SHA256 it -- never touching parent_coinbase_txid's +// SerializeTransaction path. +static uint256 legacy_txid(const MutableTransaction& tx) +{ + PackStream ref; + ref << tx.version; + ref << tx.vin; + ref << tx.vout; + ref << tx.locktime; + auto sp = std::span( + reinterpret_cast(ref.data()), ref.size()); + return Hash(sp); +} + +TEST(NmcAuxParentCoinbase, TxidIsWitnessStrippedLegacyHashNotWtxid) +{ + auto cb = make_parent_coinbase(); + ASSERT_TRUE(cb.HasWitness()); // a real BTC coinbase carries the reserved value + + // (a) txid is hashed over the independently-laid-out legacy serialization. + EXPECT_EQ(parent_coinbase_txid(cb), legacy_txid(cb)); + + // (b) txid != wtxid: the with-witness hash differs precisely because the + // witness is present and the txid path must strip it. + auto wpacked = pack(nmc::coin::TX_WITH_WITNESS(cb)); + uint256 wtxid = Hash(wpacked.get_span()); + EXPECT_NE(parent_coinbase_txid(cb), wtxid); +} + +TEST(NmcAuxCheckProof, ParentLegReconstructingHeaderMerkleIsIncomplete) +{ + AuxPow ap; + ap.parent_coinbase = make_parent_coinbase(); + uint256 cbid = parent_coinbase_txid(ap.parent_coinbase); + uint256 sib = leaf_of(0x55); + ap.parent_coinbase_branch = {sib}; + ap.parent_coinbase_index = 0; // bit0=0 => coinbase LEFT + // parent header commits the reconstructed tx-merkle-root; mark it non-null. + ap.parent_header.m_merkle_root = combine(cbid, sib); + ap.parent_header.m_bits = 1; // non-null so the equality gate fires + // chain leg left at defaults (empty branch, index 0) => passes step 1. + EXPECT_EQ(ap.check_proof(leaf_of(0x01), 1), + AuxPow::CheckResult::INCOMPLETE); +} + +TEST(NmcAuxCheckProof, ParentLegNotReconstructingHeaderMerkleIsInvalid) +{ + AuxPow ap; + ap.parent_coinbase = make_parent_coinbase(); + ap.parent_coinbase_branch = {leaf_of(0x55)}; + ap.parent_coinbase_index = 0; + ap.parent_header.m_merkle_root = leaf_of(0x99); // deliberately wrong root + ap.parent_header.m_bits = 1; // non-null => equality gate fires + EXPECT_EQ(ap.check_proof(leaf_of(0x01), 1), + AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxCheckProof, NegativeParentCoinbaseIndexIsInvalid) +{ + AuxPow ap; + ap.parent_coinbase = make_parent_coinbase(); + ap.parent_coinbase_branch = {leaf_of(0x55)}; + ap.parent_coinbase_index = -1; + EXPECT_EQ(ap.check_proof(leaf_of(0x01), 1), + AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxCheckProof, ParentCoinbaseIndexTooWideForDepthIsInvalid) +{ + AuxPow ap; + ap.parent_coinbase = make_parent_coinbase(); + ap.parent_coinbase_branch = {leaf_of(0x55)}; // depth 1 => max index 1 + ap.parent_coinbase_index = 2; + EXPECT_EQ(ap.check_proof(leaf_of(0x01), 1), + AuxPow::CheckResult::INVALID); +} + + +// --------------------------------------------------------------------------- +// P1c-step2: AuxPow::check_proof MM-marker commitment + chain_id/slot binding. +// +// step 2 confirms the chain-merkle root reconstructed in step 1 is the one +// committed inside the parent coinbase's merged-mining marker (pchMergedMiningHeader +// = fa be 6d 6d), that the marker's tree size == 2^height, and that the chain +// occupies the slot aux_expected_index(nonce, chain_id, height) demands. A +// coinbase with NO marker leaves the proof INCOMPLETE (staged: nothing to +// assert, mirrors the null-parent-header gate). A marker present but committing +// a different root / wrong size / wrong slot, or a duplicated marker, is INVALID. +// The proof never reaches VALID — step 4 (parent PoW) is still unbuilt. +// +// aux_expected_index is pinned against values computed OFFLINE (not by calling +// the production helper), so an LCG-constant/typo in the port is caught. The +// committed root is laid out here by hand (magic|reversed-root|LE size|LE nonce) +// without touching scan_mm_commitment, so a scan/parse bug is caught not mirrored. +// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY. +// --------------------------------------------------------------------------- + +using nmc::coin::scan_mm_commitment; +using nmc::coin::aux_expected_index; +using nmc::coin::MMScan; + +static const unsigned char MM_MAGIC[4] = {0xfa, 0xbe, 'm', 'm'}; + +static void put_le32(std::vector& v, uint32_t x) +{ + v.push_back(static_cast( x & 0xff)); + v.push_back(static_cast((x >> 8) & 0xff)); + v.push_back(static_cast((x >> 16) & 0xff)); + v.push_back(static_cast((x >> 24) & 0xff)); +} + +// The chain-merkle root as committed: reversed (big-endian display) byte order, +// derived here without touching scan_mm_commitment. +static std::vector root_reversed(uint256 r) +{ + const unsigned char* p = reinterpret_cast(r.begin()); + std::vector v(p, p + uint256::BYTES); + std::reverse(v.begin(), v.end()); + return v; +} + +// [dummy coinbase-height prefix][magic][reversed root][LE size][LE nonce]. +static std::vector mm_script(const std::vector& reversed_root, + uint32_t size, uint32_t nonce) +{ + std::vector s = {0x03, 0x11, 0x22, 0x33}; // arbitrary prefix + s.insert(s.end(), MM_MAGIC, MM_MAGIC + 4); + s.insert(s.end(), reversed_root.begin(), reversed_root.end()); + put_le32(s, size); + put_le32(s, nonce); + return s; +} + +static MutableTransaction coinbase_with_script(const std::vector& script) +{ + MutableTransaction tx; + tx.version = 1; tx.locktime = 0; + TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffffu; + in.scriptSig = OPScript(script.data(), script.data() + script.size()); + in.sequence = 0xffffffffu; + tx.vin.push_back(in); + TxOut out; out.value = 5000000000LL; tx.vout.push_back(out); + tx.vin[0].scriptWitness.stack.assign(1, std::vector(32, 0x00)); + return tx; +} + +TEST(NmcAuxExpectedIndex, MatchesPinnedOfflineReference) +{ + // values computed offline via the LCG (1103515245 / 12345), chain_id = 1. + EXPECT_EQ(aux_expected_index(0, 1, 3), 3u); + EXPECT_EQ(aux_expected_index(1, 1, 3), 4u); + EXPECT_EQ(aux_expected_index(7, 1, 3), 2u); + EXPECT_EQ(aux_expected_index(0, 1, 1), 1u); + EXPECT_EQ(aux_expected_index(1, 1, 1), 0u); + EXPECT_EQ(aux_expected_index(12345, 1, 4), 12u); +} + +TEST(NmcAuxStep2, ScanReturnsMatchAbsentMismatch) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); // index bit0=0 => leaf left + unsigned h = 1; int32_t cid = 1; uint32_t nonce = 1; uint32_t index = 0; // slot 0 + + auto good = mm_script(root_reversed(root), 1u << h, nonce); + EXPECT_EQ(scan_mm_commitment(good, root, h, cid, index), MMScan::MATCH); + + std::vector nomagic = {0x03, 0x11, 0x22, 0x33}; + EXPECT_EQ(scan_mm_commitment(nomagic, root, h, cid, index), MMScan::ABSENT); + + auto badroot = mm_script(root_reversed(leaf_of(0x99)), 1u << h, nonce); + EXPECT_EQ(scan_mm_commitment(badroot, root, h, cid, index), MMScan::MISMATCH); +} + +TEST(NmcAuxStep2, ValidCommitmentIsIncompleteNeverValid) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); + auto script = mm_script(root_reversed(root), /*size=*/2, /*nonce=*/1); // slot 0 + + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + // parent_header null => step 3 equality skipped; step 2 MATCH; step 4 unbuilt. + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INCOMPLETE); + EXPECT_NE(ap.check_proof(aux, 1), AuxPow::CheckResult::VALID); +} + +TEST(NmcAuxStep2, WrongCommittedRootIsInvalid) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + auto script = mm_script(root_reversed(leaf_of(0x99)), 2, 1); // commits wrong root + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxStep2, WrongTreeSizeIsInvalid) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); + auto script = mm_script(root_reversed(root), /*size=*/4, /*nonce=*/1); // h=1 wants 2 + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxStep2, WrongDeterministicSlotIsInvalid) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); // correct root for index 0 + // nonce 0 => expected slot 1 (pinned) != chain_merkle_index 0 => binding fails; + // root + size are correct so only the slot check rejects this. + auto script = mm_script(root_reversed(root), 2, /*nonce=*/0); + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxStep2, DuplicateMergedMiningHeaderIsInvalid) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); + auto script = mm_script(root_reversed(root), 2, 1); + script.insert(script.end(), MM_MAGIC, MM_MAGIC + 4); // a second, illegal header + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxStep2, NoMarkerLeavesProofIncomplete) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + std::vector nomagic = {0x03, 0x4e, 0x4d, 0x43}; // no MM magic + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(nomagic); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INCOMPLETE); +} + +// --------------------------------------------------------------------------- +// P1c-step4: AuxPow::check_proof parent proof-of-work leg (step 4). +// +// The parent (BTC) block's SHA256d PoW hash must clear the AUX (NMC) block's +// difficulty target (Namecoin CheckAuxPowProofOfWork: the parent PoW is checked +// against the aux bits, NOT the parent's own bits). With steps 1-3 satisfied, +// a sufficient parent PoW finally yields VALID; an insufficient one is INVALID; +// and a caller that supplies no aux_bits (default 0) keeps the otherwise- +// complete proof INCOMPLETE so NMC never block-validates off a partial proof. +// Per-coin isolation: src/impl/nmc/ only; chain::bits_to_target is core, the +// PoW hash is nmc-local pow_hash() -- the btc tree is not touched. +// --------------------------------------------------------------------------- + +// A structurally-complete AuxPow whose steps 1-3 all pass: a parent coinbase +// carrying the MM marker committing the chain-merkle root (slot 0: nonce=1, +// chain_id=1, height=1), itself linked into the parent header tx-merkle-root. +static AuxPow complete_proof(uint256 aux, uint32_t parent_own_bits) +{ + uint256 sib = leaf_of(0x55); + uint256 root = combine(aux, sib); // chain-merkle root + auto script = mm_script(root_reversed(root), /*size=*/2, /*nonce=*/1); + + AuxPow ap; + ap.parent_coinbase = coinbase_with_script(script); + ap.chain_merkle_branch = {sib}; + ap.chain_merkle_index = 0; // expected slot 0 + + uint256 cbid = parent_coinbase_txid(ap.parent_coinbase); + uint256 sib2 = leaf_of(0x77); + ap.parent_coinbase_branch = {sib2}; + ap.parent_coinbase_index = 0; // coinbase LEFT + ap.parent_header.m_merkle_root = combine(cbid, sib2); + ap.parent_header.m_bits = parent_own_bits; // non-null header + return ap; +} + +// "Mine" a parent header against an easy target: bump m_nonce until the +// SHA256d PoW clears `target`. Touches ONLY m_nonce, so the merkle legs +// (steps 1-3) are unaffected. At regtest-style difficulty this lands in a +// couple of iterations; the bound makes it deterministic-or-loud. +static bool mine_parent(AuxPow& ap, const uint256& target) +{ + for (uint32_t n = 0; n < 100000u; ++n) { + ap.parent_header.m_nonce = n; + if (!(pow_hash(ap.parent_header) > target)) return true; + } + return false; +} + +TEST(NmcAuxStep4, CompleteProofWithSufficientParentWorkIsValid) +{ + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu); + uint32_t aux_bits = 0x207fffffu; // regtest-style easy target + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + EXPECT_EQ(ap.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID); +} + +TEST(NmcAuxStep4, CompleteProofWithInsufficientParentWorkIsInvalid) +{ + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + // Maximally-hard aux target (compact for target == 1). A real double-SHA256 + // parent hash exceeds it (guarded), so the parent fails the aux PoW. + uint32_t hard_bits = 0x03000001u; // bits_to_target => 1 + uint256 pow = pow_hash(ap.parent_header); + ASSERT_TRUE(pow > chain::bits_to_target(hard_bits)); // precondition + EXPECT_EQ(ap.check_proof(aux, 1, hard_bits), AuxPow::CheckResult::INVALID); +} + +TEST(NmcAuxStep4, CompleteProofWithoutAuxBitsStaysIncomplete) +{ + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + // Default aux_bits (0): the parent-PoW gate is skipped and the otherwise- + // complete proof stays INCOMPLETE -- never VALID. + EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INCOMPLETE); + EXPECT_NE(ap.check_proof(aux, 1), AuxPow::CheckResult::VALID); +} + +TEST(NmcAuxStep4, ParentOwnBitsAreNotTheAuxGate) +{ + // Parent header's OWN nBits set maximally hard (target == 1); if check_proof + // wrongly used the parent's own bits the proof would be INVALID. The AUX + // target (easy) governs instead -- pinning the Namecoin consensus rule that + // the parent PoW is checked against the aux bits, not the parent's own. + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x03000001u); + uint32_t aux_bits = 0x207fffffu; // easy aux target + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + EXPECT_EQ(ap.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID); +} + +// --------------------------------------------------------------------------- +// P1d - HeaderChain::add_auxpow_header / verify_auxpow_header consensus GATE. +// +// The gate wires AuxPow::check_proof into the header-accept path: an incoming +// merge-mined header is admissible ONLY when its proof fully verifies against +// THIS chain's params (aux_chain_id) and the header's own nBits as the aux PoW +// target. These KATs pin the rejection contract - nothing unproven gets in - +// reusing the step-4 complete_proof/mine_parent fixtures but binding the aux +// block hash to a REAL header via block_hash(header), the production seam. +// Storage stays P0-DEFER, so a verified header is not persisted yet (add +// returns false on both arms); the assertion is on the verify verdict. +// --------------------------------------------------------------------------- + +using nmc::coin::HeaderChain; +using nmc::coin::NMCChainParams; +using nmc::coin::BlockHeaderType; +using nmc::coin::block_hash; + +// Params with a pinned aux_chain_id == 1 so the fixture's MM-marker slot +// (chain_id=1) binds. Activation height stays unpinned: the P1d gate under test +// is the cryptographic check_proof, not the (deferred) height-activation path. +static NMCChainParams params_chain_id_1() +{ + NMCChainParams p = NMCChainParams::mainnet(); + p.aux_chain_id = 1; + return p; +} + +// A header whose block_hash() becomes the AUX hash the proof commits to; m_bits +// carries the aux PoW target the parent hash must clear (Namecoin aux-bits rule). +static BlockHeaderType aux_header_for(uint32_t aux_bits) +{ + BlockHeaderType h{}; + h.m_version = 1; + h.m_bits = aux_bits; + return h; +} + +TEST(NmcP1dGate, FullyVerifiedAuxPowPassesTheGate) +{ + uint32_t aux_bits = 0x207fffffu; // regtest-style easy target + BlockHeaderType h = aux_header_for(aux_bits); + uint256 aux = block_hash(h); // production seam + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu); + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + + HeaderChain chain_(params_chain_id_1()); + EXPECT_EQ(chain_.verify_auxpow_header(h, ap), AuxPow::CheckResult::VALID); + // Storage is still P0-DEFER, so even a verified header is not persisted yet. + EXPECT_FALSE(chain_.add_auxpow_header(h, ap)); + EXPECT_FALSE(chain_.has_header(aux)); +} + +TEST(NmcP1dGate, WrongChainIdIsNotValidAndIsRejected) +{ + uint32_t aux_bits = 0x207fffffu; + BlockHeaderType h = aux_header_for(aux_bits); + uint256 aux = block_hash(h); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + + NMCChainParams p = NMCChainParams::mainnet(); + p.aux_chain_id = 2; // != fixture's chain_id 1 + HeaderChain chain_(p); + EXPECT_NE(chain_.verify_auxpow_header(h, ap), AuxPow::CheckResult::VALID); + EXPECT_FALSE(chain_.add_auxpow_header(h, ap)); +} + +TEST(NmcP1dGate, InsufficientParentWorkIsRejected) +{ + BlockHeaderType h = aux_header_for(/*aux_bits=*/0x03000001u); // target == 1 + uint256 aux = block_hash(h); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + // Unmined parent: a real double-SHA256 hash exceeds target 1 => INVALID. + ASSERT_TRUE(pow_hash(ap.parent_header) > chain::bits_to_target(h.m_bits)); + + HeaderChain chain_(params_chain_id_1()); + EXPECT_EQ(chain_.verify_auxpow_header(h, ap), AuxPow::CheckResult::INVALID); + EXPECT_FALSE(chain_.add_auxpow_header(h, ap)); +} + +TEST(NmcP1dGate, MissingAuxBitsKeepsProofIncompleteAndRejected) +{ + BlockHeaderType h = aux_header_for(/*aux_bits=*/0u); // 0 == leg-only sentinel + uint256 aux = block_hash(h); + AuxPow ap = complete_proof(aux, 0x1d00ffffu); + + HeaderChain chain_(params_chain_id_1()); + // header.m_bits == 0 feeds check_proof's leg-only sentinel: step 4 skipped, + // the proof stays INCOMPLETE, and the gate rejects. + EXPECT_EQ(chain_.verify_auxpow_header(h, ap), AuxPow::CheckResult::INCOMPLETE); + EXPECT_FALSE(chain_.add_auxpow_header(h, ap)); +} + } // namespace