Skip to content

Commit d8b4023

Browse files
authored
Merge pull request #202 from frstrtr/nmc/p1f-auxpow-wire
nmc(P1f-a): canonical CAuxPow Serialize + persist auxpow blob behind has_auxpow
2 parents 2a54ce9 + 3c904a9 commit d8b4023

2 files changed

Lines changed: 154 additions & 15 deletions

File tree

src/impl/nmc/coin/header_chain.hpp

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,45 @@ struct AuxPow {
311311

312312
bool IsNull() const { return parent_header.IsNull(); }
313313

314+
/// Canonical Namecoin CAuxPow wire layout, fenced to the NMC tree (only
315+
/// core/* + nmc::coin types -- no btc/ or bitcoin_family/ include). A
316+
/// byte-faithful port of Namecoin CAuxPow / classic Bitcoin auxpow: the
317+
/// CMerkleTx coinbase leg, then the chain-merkle leg, then the parent
318+
/// header, in daemon field order:
319+
/// 1. parent_coinbase -- parent (BTC) coinbase CTransaction,
320+
/// serialized WITNESS-STRIPPED (TX_NO_WITNESS) so the bytes match the
321+
/// txid the parent tx-merkle tree commits (see parent_coinbase_txid)
322+
/// and the daemon auxpow encoding -- NOT the wtxid form;
323+
/// 2. parent_block_hash -- CMerkleTx::hashBlock;
324+
/// 3. parent_coinbase_branch -- CMerkleTx::vMerkleBranch;
325+
/// 4. parent_coinbase_index -- CMerkleTx::nIndex (int32 LE);
326+
/// 5. chain_merkle_branch -- CAuxPow::vChainMerkleBranch;
327+
/// 6. chain_merkle_index -- CAuxPow::nChainIndex (int32 LE);
328+
/// 7. parent_header -- CAuxPow::parentBlock (80-byte header).
329+
/// Hand-written (not C2POOL_SERIALIZE_METHODS) because the parent coinbase
330+
/// needs the explicit TX_NO_WITNESS param wrapper. Backs the P1f on-disk
331+
/// auxpow blob and is reused by PD dual-target wire path.
332+
template<typename Stream>
333+
void Serialize(Stream& s) const {
334+
::Serialize(s, TX_NO_WITNESS(parent_coinbase));
335+
::Serialize(s, parent_block_hash);
336+
::Serialize(s, parent_coinbase_branch);
337+
::Serialize(s, parent_coinbase_index);
338+
::Serialize(s, chain_merkle_branch);
339+
::Serialize(s, chain_merkle_index);
340+
::Serialize(s, parent_header);
341+
}
342+
template<typename Stream>
343+
void Unserialize(Stream& s) {
344+
::Unserialize(s, TX_NO_WITNESS(parent_coinbase));
345+
::Unserialize(s, parent_block_hash);
346+
::Unserialize(s, parent_coinbase_branch);
347+
::Unserialize(s, parent_coinbase_index);
348+
::Unserialize(s, chain_merkle_branch);
349+
::Unserialize(s, chain_merkle_index);
350+
::Unserialize(s, parent_header);
351+
}
352+
314353
/// Result of a (future) AuxPow verification.
315354
enum class CheckResult {
316355
NOT_IMPLEMENTED_P0, //!< P0 leaf — verification path not built yet
@@ -561,20 +600,20 @@ struct IndexEntry {
561600
/// status (as uint32_t), then the AuxPow presence flag (uint8_t: 1 if the entry
562601
/// carried a merge-mining proof, else 0).
563602
///
564-
/// P1f-DEFER: auxpow blob persistence -- next sub-leg. nmc::coin::AuxPow itself
565-
/// has NO ::Serialize/::Unserialize / C2POOL_SERIALIZE_METHODS, so per the
566-
/// fence we do NOT hand-roll its wire layout here. This leg persists ONLY the
567-
/// presence flag; on load the AuxPow is reconstructed as std::nullopt, but the
568-
/// entry's status (HEADER_VALID_CHAIN for a merge-mined header) is preserved
569-
/// faithfully so the restored chain's validation state is not lost. The actual
570-
/// proof blob lands in the next sub-leg behind this same flag.
603+
/// P1f(a): the AuxPow proof blob is persisted behind has_auxpow using the
604+
/// canonical Namecoin CAuxPow wire layout (AuxPow::Serialize, fenced to
605+
/// src/impl/nmc/). When has_auxpow == 1 the blob trails the flag and is
606+
/// restored verbatim on load; when 0, nothing follows. The entry's status
607+
/// (HEADER_VALID_CHAIN for a merge-mined header) is preserved either way, so
608+
/// the restored chain's validation state is never lost.
571609
struct IndexEntryDiskV1 {
572610
BlockHeaderType header;
573611
uint256 block_hash; // SHA256d(header)
574612
uint32_t height{0};
575613
uint256 chain_work; // cumulative work up to this header
576614
HeaderStatus status{HEADER_VALID_UNKNOWN};
577-
uint8_t has_auxpow{0}; // presence flag (blob deferred -- see above)
615+
uint8_t has_auxpow{0}; // presence flag for the trailing blob
616+
std::optional<AuxPow> auxpow; // P1f(a): proof blob, present iff has_auxpow
578617

579618
template<typename Stream>
580619
void Serialize(Stream& s) const {
@@ -584,9 +623,9 @@ struct IndexEntryDiskV1 {
584623
::Serialize(s, chain_work);
585624
::Serialize(s, static_cast<uint32_t>(status));
586625
::Serialize(s, has_auxpow);
587-
// P1f-DEFER: auxpow blob persistence -- next sub-leg. Nothing follows the
588-
// flag yet; when AuxPow gains serialization the blob is written here iff
589-
// has_auxpow == 1.
626+
// P1f(a): the canonical CAuxPow blob trails the flag iff present.
627+
if (has_auxpow)
628+
::Serialize(s, *auxpow);
590629
}
591630
template<typename Stream>
592631
void Unserialize(Stream& s) {
@@ -598,8 +637,14 @@ struct IndexEntryDiskV1 {
598637
::Unserialize(s, st);
599638
status = static_cast<HeaderStatus>(st);
600639
::Unserialize(s, has_auxpow);
601-
// P1f-DEFER: auxpow blob persistence -- next sub-leg. The blob is not on
602-
// disk yet, so there is nothing further to read behind the flag.
640+
// P1f(a): read the canonical CAuxPow blob iff the flag says it is there.
641+
if (has_auxpow) {
642+
AuxPow ap;
643+
::Unserialize(s, ap);
644+
auxpow = std::move(ap);
645+
} else {
646+
auxpow = std::nullopt;
647+
}
603648
}
604649

605650
/// Materialize the slim in-memory form. auxpow is reconstructed as nullopt
@@ -611,7 +656,7 @@ struct IndexEntryDiskV1 {
611656
e.height = height;
612657
e.chain_work = chain_work;
613658
e.status = status;
614-
e.auxpow = std::nullopt; // P1f-DEFER: blob not persisted yet
659+
e.auxpow = auxpow; // P1f(a): blob restored behind has_auxpow
615660
return e;
616661
}
617662

@@ -624,6 +669,7 @@ struct IndexEntryDiskV1 {
624669
d.chain_work = e.chain_work;
625670
d.status = e.status;
626671
d.has_auxpow = e.auxpow.has_value() ? 1 : 0;
672+
d.auxpow = e.auxpow; // P1f(a): carry the proof blob too
627673
return d;
628674
}
629675
};

src/impl/nmc/test/auxpow_merkle_test.cpp

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1193,7 +1193,10 @@ TEST(NmcP1fPersist, DiskRoundTripPreservesMergeMinedStatusAndAuxFlag)
11931193

11941194
IndexEntry r = back.to_entry();
11951195
EXPECT_EQ(r.status, nmc::coin::HEADER_VALID_CHAIN); // status survives reload
1196-
EXPECT_FALSE(r.auxpow.has_value()); // P1f-DEFER: blob not on disk
1196+
ASSERT_TRUE(r.auxpow.has_value()); // P1f(a): blob now restored
1197+
EXPECT_EQ(r.auxpow->chain_merkle_index, e.auxpow->chain_merkle_index);
1198+
EXPECT_EQ(parent_coinbase_txid(r.auxpow->parent_coinbase),
1199+
parent_coinbase_txid(e.auxpow->parent_coinbase));
11971200
}
11981201

11991202
TEST(NmcP1fPersist, ReopenRestoresEmptyChainAsEmpty)
@@ -1285,4 +1288,94 @@ TEST(NmcP1fPersist, InMemoryModeWithoutDbPathPersistsNothing)
12851288
EXPECT_FALSE(fresh.has_header(block_hash(g)));
12861289
}
12871290

1291+
// ---------------------------------------------------------------------------
1292+
// P1f(a): nmc::coin::AuxPow canonical CAuxPow serialization round-trip.
1293+
//
1294+
// Pins the on-disk / wire codec for the merge-mining proof. A fully-populated
1295+
// AuxPow (the same complete_proof fixture steps 1-4 validate) is packed and
1296+
// unpacked; every field must survive, AND the restored proof must still verify
1297+
// VALID through check_proof -- so a field-order or witness-flag regression in
1298+
// the codec is caught against the consensus verifier, not merely a self-compare.
1299+
// Ground-truth wire-vector assertion against the Namecoin daemon's own bytes is
1300+
// the immediate follow-up (needs a captured testnet auxpow blob).
1301+
// Per-coin isolation: src/impl/nmc/ only; core/pack + nmc::coin types.
1302+
// ---------------------------------------------------------------------------
1303+
TEST(NmcAuxPowCodec, FullProofRoundTripsAndStillVerifies)
1304+
{
1305+
uint256 aux = leaf_of(0x01);
1306+
AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu);
1307+
uint32_t aux_bits = 0x207fffffu;
1308+
ASSERT_TRUE(mine_parent(ap, chain::bits_to_target(aux_bits)));
1309+
ASSERT_EQ(ap.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID);
1310+
1311+
PackStream ps;
1312+
ps << ap;
1313+
AuxPow back;
1314+
ps >> back;
1315+
1316+
// Every field survives the round-trip.
1317+
EXPECT_EQ(back.parent_block_hash, ap.parent_block_hash);
1318+
EXPECT_EQ(back.parent_coinbase_branch, ap.parent_coinbase_branch);
1319+
EXPECT_EQ(back.parent_coinbase_index, ap.parent_coinbase_index);
1320+
EXPECT_EQ(back.chain_merkle_branch, ap.chain_merkle_branch);
1321+
EXPECT_EQ(back.chain_merkle_index, ap.chain_merkle_index);
1322+
EXPECT_EQ(block_hash(back.parent_header), block_hash(ap.parent_header));
1323+
EXPECT_EQ(parent_coinbase_txid(back.parent_coinbase),
1324+
parent_coinbase_txid(ap.parent_coinbase));
1325+
1326+
// The restored proof is consensus-equivalent: still VALID.
1327+
EXPECT_EQ(back.check_proof(aux, 1, aux_bits), AuxPow::CheckResult::VALID);
1328+
}
1329+
1330+
// ---------------------------------------------------------------------------
1331+
// P1f(a): IndexEntryDiskV1 persists the AuxPow blob behind has_auxpow.
1332+
//
1333+
// The persistence leg (#201) stored only the presence flag; this leg writes the
1334+
// canonical CAuxPow blob after it iff has_auxpow == 1, and restores it on load.
1335+
// Asserts: a merge-mined entry round-trips its proof through from_entry ->
1336+
// PackStream -> to_entry; and a proofless entry trails no blob and restores to
1337+
// std::nullopt (so the flag/blob coupling cannot desync the disk format).
1338+
// ---------------------------------------------------------------------------
1339+
TEST(NmcAuxPowPersist, IndexEntryDiskV1RoundTripsAuxPowBlob)
1340+
{
1341+
using nmc::coin::IndexEntry;
1342+
using nmc::coin::IndexEntryDiskV1;
1343+
1344+
uint256 aux = leaf_of(0x01);
1345+
AuxPow ap = complete_proof(aux, /*parent_own_bits=*/0x1d00ffffu);
1346+
1347+
IndexEntry e;
1348+
e.header = ap.parent_header; // any 80-byte header
1349+
e.block_hash = leaf_of(0xAB);
1350+
e.height = 42;
1351+
e.chain_work = leaf_of(0xCD);
1352+
e.status = nmc::coin::HEADER_VALID_CHAIN;
1353+
e.auxpow = ap;
1354+
1355+
IndexEntryDiskV1 disk = IndexEntryDiskV1::from_entry(e);
1356+
ASSERT_EQ(disk.has_auxpow, 1);
1357+
1358+
PackStream ps; ps << disk;
1359+
IndexEntryDiskV1 back; ps >> back;
1360+
ASSERT_EQ(back.has_auxpow, 1);
1361+
ASSERT_TRUE(back.auxpow.has_value());
1362+
1363+
IndexEntry r = back.to_entry();
1364+
EXPECT_EQ(r.height, e.height);
1365+
EXPECT_EQ(r.status, e.status);
1366+
ASSERT_TRUE(r.auxpow.has_value());
1367+
EXPECT_EQ(r.auxpow->chain_merkle_index, e.auxpow->chain_merkle_index);
1368+
EXPECT_EQ(r.auxpow->parent_coinbase_branch, e.auxpow->parent_coinbase_branch);
1369+
EXPECT_EQ(parent_coinbase_txid(r.auxpow->parent_coinbase),
1370+
parent_coinbase_txid(e.auxpow->parent_coinbase));
1371+
1372+
// A proofless entry trails no blob and restores to nullopt.
1373+
IndexEntry plain = e; plain.auxpow = std::nullopt;
1374+
IndexEntryDiskV1 pdisk = IndexEntryDiskV1::from_entry(plain);
1375+
ASSERT_EQ(pdisk.has_auxpow, 0);
1376+
PackStream ps2; ps2 << pdisk;
1377+
IndexEntryDiskV1 pback; ps2 >> pback;
1378+
EXPECT_FALSE(pback.to_entry().auxpow.has_value());
1379+
}
1380+
12881381
} // namespace

0 commit comments

Comments
 (0)