Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 60 additions & 14 deletions src/impl/nmc/coin/header_chain.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename Stream>
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<typename Stream>
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
Expand Down Expand Up @@ -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> auxpow; // P1f(a): proof blob, present iff has_auxpow

template<typename Stream>
void Serialize(Stream& s) const {
Expand All @@ -584,9 +623,9 @@ struct IndexEntryDiskV1 {
::Serialize(s, chain_work);
::Serialize(s, static_cast<uint32_t>(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<typename Stream>
void Unserialize(Stream& s) {
Expand All @@ -598,8 +637,14 @@ struct IndexEntryDiskV1 {
::Unserialize(s, st);
status = static_cast<HeaderStatus>(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
Expand All @@ -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;
}

Expand All @@ -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;
}
};
Expand Down
95 changes: 94 additions & 1 deletion src/impl/nmc/test/auxpow_merkle_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Loading