diff --git a/src/impl/nmc/coin/header_chain.hpp b/src/impl/nmc/coin/header_chain.hpp index 2175400da..72936629a 100644 --- a/src/impl/nmc/coin/header_chain.hpp +++ b/src/impl/nmc/coin/header_chain.hpp @@ -311,6 +311,45 @@ struct AuxPow { bool IsNull() const { return parent_header.IsNull(); } + /// Canonical Namecoin CAuxPow wire layout, fenced to the NMC tree (only + /// core/* + nmc::coin types -- no btc/ or bitcoin_family/ include). A + /// byte-faithful port of Namecoin CAuxPow / classic Bitcoin auxpow: the + /// CMerkleTx coinbase leg, then the chain-merkle leg, then the parent + /// header, in daemon field order: + /// 1. parent_coinbase -- parent (BTC) coinbase CTransaction, + /// serialized WITNESS-STRIPPED (TX_NO_WITNESS) so the bytes match the + /// txid the parent tx-merkle tree commits (see parent_coinbase_txid) + /// and the daemon auxpow encoding -- NOT the wtxid form; + /// 2. parent_block_hash -- CMerkleTx::hashBlock; + /// 3. parent_coinbase_branch -- CMerkleTx::vMerkleBranch; + /// 4. parent_coinbase_index -- CMerkleTx::nIndex (int32 LE); + /// 5. chain_merkle_branch -- CAuxPow::vChainMerkleBranch; + /// 6. chain_merkle_index -- CAuxPow::nChainIndex (int32 LE); + /// 7. parent_header -- CAuxPow::parentBlock (80-byte header). + /// Hand-written (not C2POOL_SERIALIZE_METHODS) because the parent coinbase + /// needs the explicit TX_NO_WITNESS param wrapper. Backs the P1f on-disk + /// auxpow blob and is reused by PD dual-target wire path. + template + void Serialize(Stream& s) const { + ::Serialize(s, TX_NO_WITNESS(parent_coinbase)); + ::Serialize(s, parent_block_hash); + ::Serialize(s, parent_coinbase_branch); + ::Serialize(s, parent_coinbase_index); + ::Serialize(s, chain_merkle_branch); + ::Serialize(s, chain_merkle_index); + ::Serialize(s, parent_header); + } + template + void Unserialize(Stream& s) { + ::Unserialize(s, TX_NO_WITNESS(parent_coinbase)); + ::Unserialize(s, parent_block_hash); + ::Unserialize(s, parent_coinbase_branch); + ::Unserialize(s, parent_coinbase_index); + ::Unserialize(s, chain_merkle_branch); + ::Unserialize(s, chain_merkle_index); + ::Unserialize(s, parent_header); + } + /// Result of a (future) AuxPow verification. enum class CheckResult { NOT_IMPLEMENTED_P0, //!< P0 leaf — verification path not built yet @@ -561,20 +600,20 @@ struct IndexEntry { /// status (as uint32_t), then the AuxPow presence flag (uint8_t: 1 if the entry /// carried a merge-mining proof, else 0). /// -/// P1f-DEFER: auxpow blob persistence -- next sub-leg. nmc::coin::AuxPow itself -/// has NO ::Serialize/::Unserialize / C2POOL_SERIALIZE_METHODS, so per the -/// fence we do NOT hand-roll its wire layout here. This leg persists ONLY the -/// presence flag; on load the AuxPow is reconstructed as std::nullopt, but the -/// entry's status (HEADER_VALID_CHAIN for a merge-mined header) is preserved -/// faithfully so the restored chain's validation state is not lost. The actual -/// proof blob lands in the next sub-leg behind this same flag. +/// P1f(a): the AuxPow proof blob is persisted behind has_auxpow using the +/// canonical Namecoin CAuxPow wire layout (AuxPow::Serialize, fenced to +/// src/impl/nmc/). When has_auxpow == 1 the blob trails the flag and is +/// restored verbatim on load; when 0, nothing follows. The entry's status +/// (HEADER_VALID_CHAIN for a merge-mined header) is preserved either way, so +/// the restored chain's validation state is never lost. struct IndexEntryDiskV1 { 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}; - uint8_t has_auxpow{0}; // presence flag (blob deferred -- see above) + uint8_t has_auxpow{0}; // presence flag for the trailing blob + std::optional auxpow; // P1f(a): proof blob, present iff has_auxpow template void Serialize(Stream& s) const { @@ -584,9 +623,9 @@ struct IndexEntryDiskV1 { ::Serialize(s, chain_work); ::Serialize(s, static_cast(status)); ::Serialize(s, has_auxpow); - // P1f-DEFER: auxpow blob persistence -- next sub-leg. Nothing follows the - // flag yet; when AuxPow gains serialization the blob is written here iff - // has_auxpow == 1. + // P1f(a): the canonical CAuxPow blob trails the flag iff present. + if (has_auxpow) + ::Serialize(s, *auxpow); } template void Unserialize(Stream& s) { @@ -598,8 +637,14 @@ struct IndexEntryDiskV1 { ::Unserialize(s, st); status = static_cast(st); ::Unserialize(s, has_auxpow); - // P1f-DEFER: auxpow blob persistence -- next sub-leg. The blob is not on - // disk yet, so there is nothing further to read behind the flag. + // P1f(a): read the canonical CAuxPow blob iff the flag says it is there. + if (has_auxpow) { + AuxPow ap; + ::Unserialize(s, ap); + auxpow = std::move(ap); + } else { + auxpow = std::nullopt; + } } /// Materialize the slim in-memory form. auxpow is reconstructed as nullopt @@ -611,7 +656,7 @@ struct IndexEntryDiskV1 { e.height = height; e.chain_work = chain_work; e.status = status; - e.auxpow = std::nullopt; // P1f-DEFER: blob not persisted yet + e.auxpow = auxpow; // P1f(a): blob restored behind has_auxpow return e; } @@ -624,6 +669,7 @@ struct IndexEntryDiskV1 { d.chain_work = e.chain_work; d.status = e.status; d.has_auxpow = e.auxpow.has_value() ? 1 : 0; + d.auxpow = e.auxpow; // P1f(a): carry the proof blob too return d; } }; diff --git a/src/impl/nmc/test/auxpow_merkle_test.cpp b/src/impl/nmc/test/auxpow_merkle_test.cpp index a288fee38..1f026482f 100644 --- a/src/impl/nmc/test/auxpow_merkle_test.cpp +++ b/src/impl/nmc/test/auxpow_merkle_test.cpp @@ -1193,7 +1193,10 @@ TEST(NmcP1fPersist, DiskRoundTripPreservesMergeMinedStatusAndAuxFlag) IndexEntry r = back.to_entry(); EXPECT_EQ(r.status, nmc::coin::HEADER_VALID_CHAIN); // status survives reload - EXPECT_FALSE(r.auxpow.has_value()); // P1f-DEFER: blob not on disk + ASSERT_TRUE(r.auxpow.has_value()); // P1f(a): blob now restored + EXPECT_EQ(r.auxpow->chain_merkle_index, e.auxpow->chain_merkle_index); + EXPECT_EQ(parent_coinbase_txid(r.auxpow->parent_coinbase), + parent_coinbase_txid(e.auxpow->parent_coinbase)); } TEST(NmcP1fPersist, ReopenRestoresEmptyChainAsEmpty) @@ -1285,4 +1288,94 @@ TEST(NmcP1fPersist, InMemoryModeWithoutDbPathPersistsNothing) EXPECT_FALSE(fresh.has_header(block_hash(g))); } +// --------------------------------------------------------------------------- +// P1f(a): nmc::coin::AuxPow canonical CAuxPow serialization round-trip. +// +// Pins the on-disk / wire codec for the merge-mining proof. A fully-populated +// AuxPow (the same complete_proof fixture steps 1-4 validate) is packed and +// unpacked; every field must survive, AND the restored proof must still verify +// VALID through check_proof -- so a field-order or witness-flag regression in +// the codec is caught against the consensus verifier, not merely a self-compare. +// Ground-truth wire-vector assertion against the Namecoin daemon's own bytes is +// the immediate follow-up (needs a captured testnet auxpow blob). +// Per-coin isolation: src/impl/nmc/ only; core/pack + nmc::coin types. +// --------------------------------------------------------------------------- +TEST(NmcAuxPowCodec, FullProofRoundTripsAndStillVerifies) +{ + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu); + uint32_t aux_bits = 0x207fffffu; + ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits))); + ASSERT_EQ(ap.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID); + + PackStream ps; + ps << ap; + AuxPow back; + ps >> back; + + // Every field survives the round-trip. + EXPECT_EQ(back.parent_block_hash, ap.parent_block_hash); + EXPECT_EQ(back.parent_coinbase_branch, ap.parent_coinbase_branch); + EXPECT_EQ(back.parent_coinbase_index, ap.parent_coinbase_index); + EXPECT_EQ(back.chain_merkle_branch, ap.chain_merkle_branch); + EXPECT_EQ(back.chain_merkle_index, ap.chain_merkle_index); + EXPECT_EQ(block_hash(back.parent_header), block_hash(ap.parent_header)); + EXPECT_EQ(parent_coinbase_txid(back.parent_coinbase), + parent_coinbase_txid(ap.parent_coinbase)); + + // The restored proof is consensus-equivalent: still VALID. + EXPECT_EQ(back.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID); +} + +// --------------------------------------------------------------------------- +// P1f(a): IndexEntryDiskV1 persists the AuxPow blob behind has_auxpow. +// +// The persistence leg (#201) stored only the presence flag; this leg writes the +// canonical CAuxPow blob after it iff has_auxpow == 1, and restores it on load. +// Asserts: a merge-mined entry round-trips its proof through from_entry -> +// PackStream -> to_entry; and a proofless entry trails no blob and restores to +// std::nullopt (so the flag/blob coupling cannot desync the disk format). +// --------------------------------------------------------------------------- +TEST(NmcAuxPowPersist, IndexEntryDiskV1RoundTripsAuxPowBlob) +{ + using nmc::coin::IndexEntry; + using nmc::coin::IndexEntryDiskV1; + + uint256 aux = leaf_of(0x01); + AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu); + + IndexEntry e; + e.header = ap.parent_header; // any 80-byte header + e.block_hash = leaf_of(0xAB); + e.height = 42; + e.chain_work = leaf_of(0xCD); + e.status = nmc::coin::HEADER_VALID_CHAIN; + e.auxpow = ap; + + IndexEntryDiskV1 disk = IndexEntryDiskV1::from_entry(e); + ASSERT_EQ(disk.has_auxpow, 1); + + PackStream ps; ps << disk; + IndexEntryDiskV1 back; ps >> back; + ASSERT_EQ(back.has_auxpow, 1); + ASSERT_TRUE(back.auxpow.has_value()); + + IndexEntry r = back.to_entry(); + EXPECT_EQ(r.height, e.height); + EXPECT_EQ(r.status, e.status); + ASSERT_TRUE(r.auxpow.has_value()); + EXPECT_EQ(r.auxpow->chain_merkle_index, e.auxpow->chain_merkle_index); + EXPECT_EQ(r.auxpow->parent_coinbase_branch, e.auxpow->parent_coinbase_branch); + EXPECT_EQ(parent_coinbase_txid(r.auxpow->parent_coinbase), + parent_coinbase_txid(e.auxpow->parent_coinbase)); + + // A proofless entry trails no blob and restores to nullopt. + IndexEntry plain = e; plain.auxpow = std::nullopt; + IndexEntryDiskV1 pdisk = IndexEntryDiskV1::from_entry(plain); + ASSERT_EQ(pdisk.has_auxpow, 0); + PackStream ps2; ps2 << pdisk; + IndexEntryDiskV1 pback; ps2 >> pback; + EXPECT_FALSE(pback.to_entry().auxpow.has_value()); +} + } // namespace