diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index 629a33bb7..dc534519d 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -242,6 +242,43 @@ inline MMScan scan_mm_commitment(const std::vector& script, return MMScan::MATCH; } +/// Build side -- the inverse of scan_mm_commitment(). Emit the canonical +/// merged-mining commitment payload a parent (BTC) coinbase scriptSig must carry +/// so this aux chain's work is provable. The byte layout is exactly the sequence +/// scan_mm_commitment() walks: +/// MM_HEADER_MAGIC(4) | chain_merkle_root REVERSED(32) | size_le32 | nonce_le32 +/// where size = 2^chain_height is the chain-merkle tree size and the aux chain +/// occupies slot aux_expected_index(nonce, chain_id, chain_height). The root is +/// stored reversed (big-endian display order) to match auxpow.cpp. This emits +/// ONLY the MM marker payload; the caller (PD dual-target build path) prepends +/// any coinbase scriptSig framing (height push, extranonce). Kept NMC-LOCAL per +/// the coin fence (only std + core types). chain_merkle_root is taken by value +/// because base_uint::begin() is non-const (same as scan_mm_commitment). +inline std::vector build_mm_commitment(uint256 chain_merkle_root, + unsigned chain_height, + uint32_t nonce) { + std::vector out; + out.reserve(4 + uint256::BYTES + 8); + out.insert(out.end(), MM_HEADER_MAGIC, MM_HEADER_MAGIC + 4); + + // Chain-merkle root 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()); + out.insert(out.end(), root_be.begin(), root_be.end()); + + auto put_le32 = [&out](uint32_t x) { + out.push_back(static_cast( x & 0xff)); + out.push_back(static_cast((x >> 8) & 0xff)); + out.push_back(static_cast((x >> 16) & 0xff)); + out.push_back(static_cast((x >> 24) & 0xff)); + }; + put_le32(1u << chain_height); // tree size = 2^chain_height + put_le32(nonce); + return out; +} + // ─── AuxPow (merge-mining proof) ───────────────────────────────────────────── /// One aux-chain slot inside a parent coinbase's merged-mining merkle tree. diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index c71c498a2..b851e201b 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -299,6 +299,7 @@ TEST(NmcAuxCheckProof, ParentCoinbaseIndexTooWideForDepthIsInvalid) using nmc::coin::scan_mm_commitment; using nmc::coin::aux_expected_index; using nmc::coin::MMScan; +using nmc::coin::build_mm_commitment; static const unsigned char MM_MAGIC[4] = {0xfa, 0xbe, 'm', 'm'}; @@ -450,6 +451,63 @@ TEST(NmcAuxStep2, NoMarkerLeavesProofIncomplete) EXPECT_EQ(ap.check_proof(aux, 1), AuxPow::CheckResult::INCOMPLETE); } +// --------------------------------------------------------------------------- +// build_mm_commitment -- the BUILD-side inverse of scan_mm_commitment (PC-prep +// auxpow commitment scaffolding, overlapping PD's dual-target parent coinbase). +// The commitment a c2pool-built parent coinbase embeds must round-trip through +// the SAME scanner the verifier runs: emit then scan the marker for the slot +// aux_expected_index(nonce, chain_id, height) and it MUST return MATCH; any +// tamper (committed root / slot / tree size) MUST NOT. The byte layout is pinned +// explicitly so a build-side layout drift is caught, not mirrored. +// Per-coin isolation: src/impl/nmc/ only; btc tree consumed READ-ONLY. +// --------------------------------------------------------------------------- + +TEST(NmcBuildMMCommitment, RoundTripsThroughScannerAtExpectedSlot) +{ + uint256 aux = leaf_of(0x01), sib = leaf_of(0x55); + uint256 root = combine(aux, sib); + const unsigned h = 1; const int32_t cid = 1; const uint32_t nonce = 1; + + auto payload = build_mm_commitment(root, h, nonce); + const uint32_t slot = aux_expected_index(nonce, cid, h); + EXPECT_EQ(scan_mm_commitment(payload, root, h, cid, slot), MMScan::MATCH); +} + +TEST(NmcBuildMMCommitment, ByteLayoutIsMagicReversedRootSizeNonce) +{ + uint256 root = leaf_of(0x01); + const unsigned h = 3; const uint32_t nonce = 0x04030201u; + + auto got = build_mm_commitment(root, h, nonce); + + std::vector want; + want.insert(want.end(), MM_MAGIC, MM_MAGIC + 4); + auto rr = root_reversed(root); + want.insert(want.end(), rr.begin(), rr.end()); + put_le32(want, 1u << h); // tree size = 2^h = 8 + put_le32(want, nonce); + + EXPECT_EQ(got, want); + EXPECT_EQ(got.size(), static_cast(4 + 32 + 4 + 4)); +} + +TEST(NmcBuildMMCommitment, TamperedRootOrSlotFailsScan) +{ + uint256 aux = leaf_of(0x02), sib = leaf_of(0x77); + uint256 root = combine(aux, sib); + const unsigned h = 2; const int32_t cid = 1; const uint32_t nonce = 7; + + auto payload = build_mm_commitment(root, h, nonce); + const uint32_t slot = aux_expected_index(nonce, cid, h); + + EXPECT_EQ(scan_mm_commitment(payload, root, h, cid, slot), MMScan::MATCH); + // Wrong committed root. + EXPECT_EQ(scan_mm_commitment(payload, leaf_of(0x99), h, cid, slot), MMScan::MISMATCH); + // Right marker read at the wrong deterministic slot. + EXPECT_EQ(scan_mm_commitment(payload, root, h, cid, (slot + 1u) & ((1u << h) - 1u)), + MMScan::MISMATCH); +} + // --------------------------------------------------------------------------- // P1c-step4: AuxPow::check_proof parent proof-of-work leg (step 4). //