diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7a121ae16..f1bc0296a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -67,7 +67,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test \ + test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -197,7 +197,7 @@ jobs: test_phase4_embedded \ test_mweb_builder \ test_address_resolution test_compute_share_target \ - test_utxo test_dgb_subsidy dgb_share_test \ + test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ diff --git a/src/impl/dgb/coin/block_assembly.hpp b/src/impl/dgb/coin/block_assembly.hpp new file mode 100644 index 000000000..bd84f7585 --- /dev/null +++ b/src/impl/dgb/coin/block_assembly.hpp @@ -0,0 +1,118 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::assemble_won_block -- the share->block "as_block" reassembly that +// the won-block reconstructor (#82) feeds to broadcast_won_block. +// +// This is the faithful C++ port of the FRAMING half of p2pool data.py +// Share.as_block(tracker, known_txs): +// +// gentx = self.check(tracker, known_txs) # the coinbase tx +// other_txs = [known_txs[h] for h in transaction_hashes] +// return dict(header=self.header, txs=[gentx]+other_txs) +// +// Two consensus-relevant facts this captures: +// 1. p2pool stores only a SmallBlockHeader on the share (version|prev|time| +// bits|nonce -- NO merkle_root). The full block header's merkle_root is +// RECONSTRUCTED as check_merkle_link(gentx_hash, share.m_merkle_link). +// as_block/get_pow both recompute it this way; we must too, or the block +// hashes wrong and the daemon rejects it. +// 2. Block tx order is [gentx] ++ other_txs, with other_txs in the share's +// transaction_hashes order. gentx is always index 0 (the coinbase). +// +// The gentx bytes and the other_txs are INJECTED (already deserialized +// MutableTransaction objects): the gentx-byte build (mirroring +// generate_share_transaction's coinbase assembly -- the part that will hit the +// coinbase-byte adjudication, cf. the BCH lane) and the known_txs lookup are +// the explicitly-next reconstructor slice. Keeping them as inputs makes this +// framing build-verifiable and KAT-testable NOW (same seam-first decomposition +// as won_block_dispatch.hpp), and lets the proven BlockType serializer (the +// live submitblock path, rpc.cpp NodeRPC::submit_block) do the wire encoding so +// the reconstructed block is byte-identical to a daemon-built one. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- +// block framing reuses BlockType + check_merkle_link verbatim; no share format, +// PoW hash, coinbase commitment, or PPLNS math is touched. DGB-Scrypt is a +// STANDALONE parent in the V36 default build (no merged-coinbase leg). +// --------------------------------------------------------------------------- + +#include +#include +#include + +#include +#include + +#include "block.hpp" +#include "../share_check.hpp" // dgb::check_merkle_link (SSOT merkle-branch walk) +#include "../share_types.hpp" // dgb::MerkleLink + +namespace dgb +{ +namespace coin +{ + +// Reconstruct the full block header from the share's stored SmallBlockHeader +// plus the gentx hash + merkle link. Mirrors the merkle_root recomputation in +// p2pool data.py get_pow_hash / as_block (SmallBlockHeader omits merkle_root). +inline BlockHeaderType +reconstruct_block_header(const SmallBlockHeaderType& small_header, + const uint256& gentx_hash, + const ::dgb::MerkleLink& merkle_link) +{ + BlockHeaderType header; + header.m_version = small_header.m_version; + header.m_previous_block = small_header.m_previous_block; + header.m_timestamp = small_header.m_timestamp; + header.m_bits = small_header.m_bits; + header.m_nonce = small_header.m_nonce; + // SmallBlockHeader has no merkle_root -- recompute it from the coinbase + // (gentx) hash walked up the share's merkle branch, exactly as the share's + // PoW hash was computed at verification time. + header.m_merkle_root = ::dgb::check_merkle_link(gentx_hash, merkle_link); + return header; +} + +// Assemble the full serialized parent block for a won share. +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// gentx : the reconstructed coinbase transaction (block tx 0) +// gentx_hash : its txid (double-SHA256), for the merkle_root walk +// merkle_link : share.m_merkle_link (gentx -> merkle root branch) +// other_txs : the share's transaction_hashes resolved to txs, in order +// Returns {block_bytes, block_hex}: block_bytes is the blob the embedded P2P +// relay sends; block_hex is the same block for the external submitblock +// fallback. Wire encoding is BlockType::Serialize -> TX_WITH_WITNESS(m_txs): +// the standard Bitcoin-Core CONDITIONAL serializer -- it emits the per-tx +// witness marker/flag iff some tx HasWitness(), legacy otherwise. The block's +// witness shape is thus governed by whether the gentx carries a witness (i.e. +// is_segwit_activated(share_version) at gentx-build time: DGB v36 segwit-active +// => witness block; a BCH-style is_segwit_activated()==false coin => legacy), +// NOT an unconditional witness branch here. This is the identical path +// NodeRPC::submit_block uses, so the result round-trips and the daemon accepts. +inline std::pair, std::string> +assemble_won_block(const SmallBlockHeaderType& small_header, + const MutableTransaction& gentx, + const uint256& gentx_hash, + const ::dgb::MerkleLink& merkle_link, + const std::vector& other_txs) +{ + BlockType block; + static_cast(block) = + reconstruct_block_header(small_header, gentx_hash, merkle_link); + + // txs = [gentx] ++ other_txs (coinbase first; data.py as_block ordering). + block.m_txs.reserve(1 + other_txs.size()); + block.m_txs.push_back(gentx); + for (const auto& tx : other_txs) + block.m_txs.push_back(tx); + + PackStream packed = pack(block); + auto sp = packed.get_span(); + std::vector bytes( + reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()) + sp.size()); + std::string hex = HexStr(sp); + return {std::move(bytes), std::move(hex)}; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 4446c52b3..683aeb525 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -17,6 +17,21 @@ if (BUILD_TESTING AND GTest_FOUND) include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) gtest_add_tests(dgb_share_test "" AUTO) + # --- #82 broadcaster-gate: faithful as_block FRAMING ----------------------- + # Pins coin/block_assembly.hpp (share->block reassembly: merkle_root + # reconstruction from the gentx hash + the share`s merkle_link, then + # [gentx]++other_txs framing through the live BlockType submitblock codec). + # Links the dgb OBJECT lib like dgb_share_test because block_assembly.hpp + # reuses dgb::check_merkle_link (share_check.hpp) as the merkle SSOT. MUST + # appear in BOTH this registration AND the build.yml --target allowlist. + add_executable(dgb_block_assembly_test block_assembly_test.cpp) + target_link_libraries(dgb_block_assembly_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_block_assembly_test "" AUTO) + # --- M3 RPC-transport standalone regression guards --------------------- # Header-only guards over the external-daemon RPC coin-layer SSOTs # (rpc_request.hpp request-shape + version floor + genesis identity, and diff --git a/src/impl/dgb/test/block_assembly_test.cpp b/src/impl/dgb/test/block_assembly_test.cpp new file mode 100644 index 000000000..28ac0d0fd --- /dev/null +++ b/src/impl/dgb/test/block_assembly_test.cpp @@ -0,0 +1,298 @@ +// --------------------------------------------------------------------------- +// dgb::coin::assemble_won_block / reconstruct_block_header test (#82 +// broadcaster-gate, faithful as_block FRAMING half). +// +// Locks the share->block reassembly contract that the won-block reconstructor +// feeds to broadcast_won_block, mirroring p2pool data.py Share.as_block: +// * the full header's merkle_root is RECONSTRUCTED from the gentx hash walked +// up the share's merkle_link (SmallBlockHeader stores no merkle_root) -- +// empty branch => root == gentx_hash; index bit selects branch side; +// * block txs are [gentx] ++ other_txs with the coinbase at index 0; +// * the assembled block round-trips through BlockType (the live submitblock +// wire path) and block_hex == HexStr(block_bytes). +// +// The gentx bytes + known_txs lookup are injected (the gentx-byte build that +// hits coinbase-byte adjudication is the next reconstructor slice); this pins +// the framing + merkle_root math NOW. KATs are self-derived via the same Hash() +// the merkle walk uses, so they are independent of any fixture file. +// +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist +// or it becomes a #143-style NOT_BUILT sentinel that reds master. +// --------------------------------------------------------------------------- + +#include + +#include +#include + +#include +#include +#include +#include + +#include "../coin/block_assembly.hpp" + +namespace { + +using dgb::coin::BlockType; +using dgb::coin::SmallBlockHeaderType; +using dgb::coin::MutableTransaction; +using dgb::coin::assemble_won_block; +using dgb::coin::reconstruct_block_header; + +// A minimal coinbase-shaped gentx: one input spending the null outpoint, one +// output. Exact bytes are irrelevant to the framing math -- it just needs to +// serialize and round-trip. +MutableTransaction make_gentx() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + dgb::coin::TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffff; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + dgb::coin::TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + return tx; +} + +MutableTransaction make_tx(int64_t value) +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + dgb::coin::TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0; + in.sequence = 0xffffffff; + tx.vin.push_back(in); + dgb::coin::TxOut out; + out.value = value; + tx.vout.push_back(out); + return tx; +} + +SmallBlockHeaderType make_small_header() +{ + SmallBlockHeaderType h; + h.m_version = 0x20000000; // a non-trivial version (algo bits live here) + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); + h.m_timestamp = 1718700000; + h.m_bits = 0x1a0fffff; + h.m_nonce = 0x12345678; + return h; +} + +// Replicate one merkle-branch combine step the way dgb::check_merkle_link does: +// if index bit is set, branch is on the LEFT, else cur is on the LEFT; then +// double-SHA256 the 64-byte concat. +uint256 combine(const uint256& cur, const uint256& branch, bool branch_left) +{ + PackStream ps; + if (branch_left) { ps << branch; ps << cur; } + else { ps << cur; ps << branch; } + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + return Hash(sp); +} + +uint256 fixed_gentx_hash() +{ + uint256 h; + h.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + return h; +} + +// --- Test 1: empty merkle branch => merkle_root == gentx_hash, header copied -- +TEST(DgbBlockAssembly, EmptyBranchRootIsGentxHash) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; // empty branch, index 0 + + auto header = reconstruct_block_header(sh, gtx_hash, link); + + EXPECT_EQ(header.m_merkle_root, gtx_hash); + EXPECT_EQ(header.m_version, sh.m_version); + EXPECT_EQ(header.m_previous_block, sh.m_previous_block); + EXPECT_EQ(header.m_timestamp, sh.m_timestamp); + EXPECT_EQ(header.m_bits, sh.m_bits); + EXPECT_EQ(header.m_nonce, sh.m_nonce); +} + +// --- Test 2: one-branch link, index bit 0 => cur on left (KAT) ---------------- +TEST(DgbBlockAssembly, SingleBranchIndexZero) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; + uint256 b0; b0.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + link.m_branch.push_back(b0); + link.m_index = 0; + + auto header = reconstruct_block_header(sh, gtx_hash, link); + EXPECT_EQ(header.m_merkle_root, combine(gtx_hash, b0, /*branch_left=*/false)); +} + +// --- Test 3: index bit set => branch on left (order matters) ------------------- +TEST(DgbBlockAssembly, SingleBranchIndexOne) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; + uint256 b0; b0.SetHex("2222222222222222222222222222222222222222222222222222222222222222"); + link.m_branch.push_back(b0); + link.m_index = 1; + + auto header = reconstruct_block_header(sh, gtx_hash, link); + EXPECT_EQ(header.m_merkle_root, combine(gtx_hash, b0, /*branch_left=*/true)); + // index discriminates side: the two orderings must differ. + EXPECT_NE(combine(gtx_hash, b0, true), combine(gtx_hash, b0, false)); +} + +// --- Test 4: full assemble round-trips, coinbase first, hex==HexStr(bytes) ----- +TEST(DgbBlockAssembly, AssembleRoundTripsCoinbaseFirst) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; // empty branch => root == gtx_hash + std::vector other = { make_tx(10), make_tx(20) }; + + auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, other); + + ASSERT_FALSE(bytes.empty()); + // hex is the lowercase serialization of the same bytes. + auto sp = std::span( + reinterpret_cast(bytes.data()), bytes.size()); + EXPECT_EQ(hex, HexStr(sp)); + + // Round-trip through the live BlockType wire codec. + PackStream ps(bytes); + BlockType blk; + ps >> blk; + + EXPECT_EQ(blk.m_version, sh.m_version); + EXPECT_EQ(blk.m_previous_block, sh.m_previous_block); + EXPECT_EQ(blk.m_timestamp, sh.m_timestamp); + EXPECT_EQ(blk.m_bits, sh.m_bits); + EXPECT_EQ(blk.m_nonce, sh.m_nonce); + EXPECT_EQ(blk.m_merkle_root, gtx_hash); + + // txs = [gentx] ++ other_txs : coinbase at index 0, total = 1 + 2. + ASSERT_EQ(blk.m_txs.size(), 3u); + // tx 0 must be the coinbase (gentx): structural identity (spends the null + // outpoint at the 0xffffffff index) -- proves coinbase-first ordering. + EXPECT_EQ(blk.m_txs[0].version, gentx.version); + ASSERT_EQ(blk.m_txs[0].vin.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); + EXPECT_TRUE(blk.m_txs[0].vin[0].prevout.hash.IsNull()); + ASSERT_EQ(blk.m_txs[0].vout.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vout[0].value, gentx.vout[0].value); + // the other_txs follow, in transaction_hashes order. + ASSERT_EQ(blk.m_txs[1].vout.size(), 1u); + ASSERT_EQ(blk.m_txs[2].vout.size(), 1u); + EXPECT_EQ(blk.m_txs[1].vout[0].value, 10); + EXPECT_EQ(blk.m_txs[2].vout[0].value, 20); +} + +// --- Test 5: no other_txs => single-tx block (coinbase only) ------------------- +TEST(DgbBlockAssembly, CoinbaseOnlyBlock) +{ + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector none; + + auto [bytes, hex] = assemble_won_block(sh, gentx, gtx_hash, link, none); + + PackStream ps(bytes); + BlockType blk; + ps >> blk; + EXPECT_EQ(blk.m_txs.size(), 1u); + EXPECT_EQ(blk.m_merkle_root, gtx_hash); +} + + +// === Witness predicate KATs (#82 DGB<->BCH coinbase adjudication) ============= +// The block wire codec is BlockType::Serialize -> TX_WITH_WITNESS(m_txs), the +// standard Bitcoin-Core CONDITIONAL serializer: it emits the per-tx witness +// marker/flag (and the witness stacks) iff some tx HasWitness(), and a legacy +// blob otherwise. So the won-block's witness shape is governed by whether the +// gentx carries a witness -- i.e. by is_segwit_activated(share_version) at +// gentx-build time -- NOT by an unconditional witness branch in the framer. +// DGB: SEGWIT_ACTIVATION_VERSION=35 => v36 gentx carries the BIP141 coinbase +// witness reserved value => won block is TX_WITH_WITNESS (asserted here). +// BCH: SEGWIT_ACTIVATION_VERSION=0 sentinel => is_segwit_activated() false for +// every version => gentx has no witness => the identical codec emits a +// LEGACY block (the companion bch test asserts the absence). +// These pin the predicate so emission == verification == oracle, per the paired +// adjudication; we do not assume it. + +// The BIP141 coinbase witness reserved value: a single 32-byte zero stack item. +MutableTransaction make_segwit_gentx() +{ + auto tx = make_gentx(); + tx.vin[0].scriptWitness.stack.assign(1, std::vector(32, 0x00)); + return tx; +} + +// --- Test 6: DGB segwit-active gentx => TX_WITH_WITNESS block (predicate true) - +TEST(DgbBlockAssembly, SegwitGentxEmitsWitnessBlock) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector none; + + auto seg = make_segwit_gentx(); + ASSERT_TRUE(seg.HasWitness()); // gentx carries the coinbase witness + auto [wbytes, whex] = assemble_won_block(sh, seg, gtx_hash, link, none); + + // The conditional codec must have emitted the witness: the witnessful block + // is strictly larger than the legacy block over the same logical txs, and + // the round-tripped coinbase preserves the reserved value. + auto [lbytes, lhex] = assemble_won_block(sh, make_gentx(), gtx_hash, link, none); + EXPECT_GT(wbytes.size(), lbytes.size()); + + PackStream ps(wbytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_TRUE(blk.m_txs[0].HasWitness()); + ASSERT_EQ(blk.m_txs[0].vin.size(), 1u); + ASSERT_EQ(blk.m_txs[0].vin[0].scriptWitness.stack.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].scriptWitness.stack[0], + std::vector(32, 0x00)); + EXPECT_EQ(whex, HexStr(std::span( + reinterpret_cast(wbytes.data()), wbytes.size()))); +} + +// --- Test 7: no-witness gentx => LEGACY block (predicate false, BCH-shape) ----- +TEST(DgbBlockAssembly, LegacyGentxEmitsLegacyBlock) +{ + auto sh = make_small_header(); + auto gtx_hash = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector none; + + auto plain = make_gentx(); + ASSERT_FALSE(plain.HasWitness()); + auto [bytes, hex] = assemble_won_block(sh, plain, gtx_hash, link, none); + + PackStream ps(bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_FALSE(blk.m_txs[0].HasWitness()); // legacy: no marker/flag emitted + + // Legacy block re-serializes byte-identically (no witness round-trip drift). + auto [bytes2, hex2] = assemble_won_block(sh, blk.m_txs[0], gtx_hash, link, none); + EXPECT_EQ(hex, hex2); +} + +} // namespace