From 3b6f5d85a44cfd673762f30bf339d5dac93cbc64 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Fri, 19 Jun 2026 00:02:32 +0000 Subject: [PATCH] =?UTF-8?q?dgb(#82):=20won-block=20reconstructor=20body=20?= =?UTF-8?q?=E2=80=94=20compose=20resolve+assemble+frame=20as=5Fblock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tie the three landed sub-slices into the single composition the dispatch handler injects as its WonBlockReconstructor: resolve_other_tx_hashes (#174) -> assemble_other_txs (#176) -> assemble_won_block (#168 framing) Faithful port of p2pool data.py Share.as_block: produces {bytes, hex} for broadcast_won_block dual path. Gentx enters as a deserialized MutableTransaction + txid (the SSOT gentx-bytes -> MutableTransaction unpack, inverse of #173 exposure, is the next slice). Injected tracker/known_txs seams keep it unit-testable; out_of_range propagation from each step preserved (never emit a partial/wrong block). dgb_reconstruct_won_block_test 5/5 PASS: end-to-end == manual resolve+assemble+frame (byte-identical), [gentx]++others in ref order, empty-refs gentx-only, ref-past-chain + unknown-known-tx throw. Wired into test/CMakeLists.txt + both build.yml --target allowlists (#143 trap). Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: none. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/reconstruct_won_block.hpp | 127 +++++++++ src/impl/dgb/test/CMakeLists.txt | 15 + .../dgb/test/reconstruct_won_block_test.cpp | 257 ++++++++++++++++++ 4 files changed, 401 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/reconstruct_won_block.hpp create mode 100644 src/impl/dgb/test/reconstruct_won_block_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2f1c2f03..d84340770 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ v37_test \ -j$(nproc) @@ -201,7 +201,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test dgb_block_assembly_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_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/reconstruct_won_block.hpp b/src/impl/dgb/coin/reconstruct_won_block.hpp new file mode 100644 index 000000000..26324577f --- /dev/null +++ b/src/impl/dgb/coin/reconstruct_won_block.hpp @@ -0,0 +1,127 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::reconstruct_won_block -- the won-block reconstructor's (#82) BODY: +// the single composition that the dispatch handler (won_block_dispatch.hpp, +// make_on_block_found) injects as its WonBlockReconstructor. It ties the three +// previously-landed sub-slices into one faithful port of p2pool data.py +// Share.as_block(tracker, known_txs): +// +// gentx = self.check(tracker, known_txs) # coinbase tx +// other_txs = [known_txs[h] +// for h in self.get_other_tx_hashes(tracker)] # ref-walk +// return dict(header=self.header, txs=[gentx]+other_txs) +// +// Composition (each step is its own unit-tested SSOT slice): +// 1. resolve_other_tx_hashes (other_tx_resolver.hpp, sub-slice 1 / #174) +// share.transaction_hash_refs --ancestry walk--> ordered other_tx hashes +// 2. assemble_other_txs (other_tx_assembler.hpp, sub-slice 2 / #176) +// ordered hashes --known_txs lookup--> deserialized MutableTransactions +// 3. assemble_won_block (block_assembly.hpp, as_block FRAMING / #168) +// small_header + gentx + merkle_link + other_txs --> {bytes, hex} +// (merkle_root recomputed from gentx_hash up the share merkle_link; +// txs = [gentx] ++ other_txs; BlockType codec = live submitblock path) +// +// The gentx enters as an already-deserialized MutableTransaction + its txid +// (gentx_hash), exactly as block_assembly_test models it. In the run-loop this +// binds to the SSOT gentx that generate_share_transaction exposes via its +// out_gentx parameter (#173): GentxCoinbase{bytes, txid}. Turning those SSOT +// non-witness bytes back into a MutableTransaction is the inverse of #173's +// exposure -- a distinct param-stream codec concern with its own round-trip KAT +// (the explicitly-next slice) -- so it is kept OUT of this pure-composition body +// to keep the as_block ordering + merkle math build-verifiable and KAT-able NOW. +// +// All three tracker/mempool operations are INJECTED as callables (the same +// seam-first decomposition the sub-slices already use), so the whole +// reconstruction is unit-testable against a synthetic ancestry + known-tx set +// without standing up a live ShareTracker / mempool. In the run-loop they bind +// to (cf. won_block_dispatch.hpp / the sub-slice run-loop notes): +// nth_parent_fn = chain.get_nth_parent_via_skip(h, n) +// new_tx_hashes_fn = chain.get_share(h)->m_tx_info.m_new_transaction_hashes +// known_txs_fn = node known-tx store (mempool + peer-relayed tx cache) +// +// Failure posture inherited from the sub-slices: a ref that walks past the +// known sharechain, a tx_count out of range, or an unknown other_tx hash each +// throw std::out_of_range -- a partial/wrong reconstruction would hash to the +// wrong merkle root and be rejected by the daemon, so failing loudly here is +// strictly safer than broadcasting a malformed block. +// +// Per-coin isolation: src/impl/dgb/ only. p2pool-merged-v36 surface: NONE -- +// pure composition of already-validated share_info + already-relayed txs + the +// proven BlockType serializer; no share format, PoW, 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_assembly.hpp" // assemble_won_block, SmallBlockHeaderType, BlockType +#include "other_tx_resolver.hpp" // resolve_other_tx_hashes +#include "other_tx_assembler.hpp" // assemble_other_txs +#include "transaction.hpp" // MutableTransaction +#include "../share_types.hpp" // TxHashRefs, MerkleLink, uint256 + +namespace dgb +{ +namespace coin +{ + +// The reconstructed parent block, ready for broadcast_won_block's dual path: +// bytes : the blob the embedded P2P relay sends +// hex : the same block for the external submitblock (RPC) fallback +struct ReconstructedWonBlock +{ + std::vector bytes; + std::string hex; +}; + +// Reconstruct the full serialized parent block for a won share. +// +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// merkle_link : share.m_merkle_link (gentx -> merkle root branch) +// gentx : the share's coinbase tx, already deserialized (block tx 0) +// gentx_hash : its txid (double-SHA256) == p2pool gentx_hash, for the +// merkle_root walk +// won_share_hash : hash of the share that found the block (ref-walk origin) +// refs : share.m_tx_info.m_transaction_hash_refs, in order +// nth_parent_fn : (start, n) -> hash of the n-th parent (n==0 -> start); +// IsNull() if the walk runs off the known sharechain +// new_tx_hashes_fn : share_hash -> that share's new_transaction_hashes +// known_txs_fn : hash -> pointer to the known MutableTransaction, or +// nullptr if absent +// +// Returns {bytes, hex} with hex == HexStr(bytes). Throws std::out_of_range on +// any malformed-share / incomplete-known-txs condition (see the per-step slices). +inline ReconstructedWonBlock +reconstruct_won_block( + const SmallBlockHeaderType& small_header, + const ::dgb::MerkleLink& merkle_link, + const MutableTransaction& gentx, + const uint256& gentx_hash, + const uint256& won_share_hash, + const std::vector& refs, + const std::function& nth_parent_fn, + const std::function&(const uint256&)>& new_tx_hashes_fn, + const std::function& known_txs_fn) +{ + // 1. share.transaction_hash_refs -> ordered other_tx hashes (ancestry walk). + const std::vector other_tx_hashes = + resolve_other_tx_hashes(won_share_hash, refs, nth_parent_fn, new_tx_hashes_fn); + + // 2. ordered hashes -> deserialized MutableTransactions (known_txs lookup). + const std::vector other_txs = + assemble_other_txs(other_tx_hashes, known_txs_fn); + + // 3. small_header + gentx + merkle_link + other_txs -> {bytes, hex}. + // (merkle_root recomputed from gentx_hash up merkle_link; [gentx]++others.) + auto framed = assemble_won_block(small_header, gentx, gentx_hash, merkle_link, other_txs); + + return ReconstructedWonBlock{std::move(framed.first), std::move(framed.second)}; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 82ce1a55a..bc5e506a3 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -32,6 +32,21 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_block_assembly_test "" AUTO) + # --- #82: won-block reconstructor BODY (as_block composition) ---------- + # Pins coin/reconstruct_won_block.hpp: the single composition the dispatch + # handler injects as its WonBlockReconstructor (resolve_other_tx_hashes -> + # assemble_other_txs -> assemble_won_block). Links the dgb OBJECT lib like + # dgb_block_assembly_test because it pulls block_assembly.hpp (BlockType / + # MutableTransaction / check_merkle_link). MUST appear in BOTH this + # registration AND the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_reconstruct_won_block_test reconstruct_won_block_test.cpp) + target_link_libraries(dgb_reconstruct_won_block_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_reconstruct_won_block_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/reconstruct_won_block_test.cpp b/src/impl/dgb/test/reconstruct_won_block_test.cpp new file mode 100644 index 000000000..e4a4418d1 --- /dev/null +++ b/src/impl/dgb/test/reconstruct_won_block_test.cpp @@ -0,0 +1,257 @@ +// --------------------------------------------------------------------------- +// dgb_reconstruct_won_block_test -- pins coin/reconstruct_won_block.hpp, the +// won-block reconstructor's (#82) BODY: the single composition the dispatch +// handler injects as its WonBlockReconstructor. Proves the three landed +// sub-slices compose into a faithful p2pool Share.as_block, end to end, against +// a SYNTHETIC ancestry + known-tx set (no live ShareTracker / mempool): +// * refs --resolve--> ordered hashes --assemble--> txs --frame--> {bytes,hex} +// * the composed bytes are IDENTICAL to assemble_won_block fed the manually +// resolved other_txs (no step reorders / drops / duplicates), hex==HexStr; +// * block tx layout is [gentx] ++ other_txs in transaction_hash_refs order; +// * empty refs => block carries the gentx only; +// * a malformed ref (walk past chain end) and an unknown other_tx hash each +// PROPAGATE as std::out_of_range -- the reconstructor never emits a partial +// or wrong block. +// +// Links the dgb OBJECT lib like dgb_block_assembly_test (block_assembly.hpp pulls +// BlockType / MutableTransaction / check_merkle_link). MUST also appear in the +// build.yml --target allowlist (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../coin/reconstruct_won_block.hpp" + +using dgb::coin::reconstruct_won_block; +using dgb::coin::assemble_won_block; +using dgb::coin::resolve_other_tx_hashes; +using dgb::coin::assemble_other_txs; +using dgb::coin::ReconstructedWonBlock; +using dgb::coin::BlockType; +using dgb::coin::SmallBlockHeaderType; +using dgb::coin::MutableTransaction; +using dgb::TxHashRefs; + +namespace { + +uint256 H(const char* two) // 0x"two" repeated to 64 hex chars +{ + std::string s; + for (int i = 0; i < 32; ++i) s += two; + uint256 h; h.SetHex(s); + return h; +} + +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; +} + +MutableTransaction make_gentx() +{ + MutableTransaction tx; + tx.version = 1; + tx.locktime = 0; + dgb::coin::TxIn in; + in.prevout.hash.SetNull(); + in.prevout.index = 0xffffffff; // coinbase + in.sequence = 0xffffffff; + tx.vin.push_back(in); + dgb::coin::TxOut out; + out.value = 5000000000LL; + tx.vout.push_back(out); + return tx; +} + +SmallBlockHeaderType make_small_header() +{ + SmallBlockHeaderType h; + h.m_version = 0x20000000; + h.m_previous_block.SetHex("00000000000000000000000000000000000000000000000000000000deadbeef"); + h.m_timestamp = 1718700000; + h.m_bits = 0x1a0fffff; + h.m_nonce = 0x12345678; + return h; +} + +uint256 fixed_gentx_hash() +{ + uint256 h; + h.SetHex("1111111111111111111111111111111111111111111111111111111111111111"); + return h; +} + +// Synthetic ancestry won <- p1 <- p2, each share carrying new_transaction_hashes, +// plus a known_txs store mapping those hashes to distinct MutableTransactions. +struct Fixture +{ + uint256 won = H("a0"), p1 = H("a1"), p2 = H("a2"); + std::map parent; + std::map> nths; + std::map known; + + Fixture() + { + parent[won] = p1; + parent[p1] = p2; + nths[won] = { H("c0"), H("c1") }; + nths[p1] = { H("d0") }; + nths[p2] = { H("e0"), H("e1") }; + // distinct values so order is observable downstream + known[H("c1")] = make_tx(11); + known[H("d0")] = make_tx(22); + known[H("c0")] = make_tx(33); + } + + std::function nth_parent() const + { + auto p = parent; + return [p](const uint256& start, uint64_t n) { + uint256 cur = start; + for (uint64_t i = 0; i < n; ++i) { + auto it = p.find(cur); + if (it == p.end()) return uint256(); + cur = it->second; + } + return cur; + }; + } + std::function&(const uint256&)> new_tx() const + { + return [this](const uint256& h) -> const std::vector& { return nths.at(h); }; + } + std::function known_fn() const + { + return [this](const uint256& h) -> const MutableTransaction* { + auto it = known.find(h); + return it == known.end() ? nullptr : &it->second; + }; + } +}; + +// --- Test 1: end-to-end composition == manual resolve+assemble+frame ---------- +TEST(DgbReconstructWonBlock, ComposesToIdenticalBlock) +{ + Fixture f; + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; // empty branch => root == gentx_hash + // refs: (0,1) -> won.nths[1]=c1 ; (1,0) -> p1.nths[0]=d0 + std::vector refs = { TxHashRefs(0, 1), TxHashRefs(1, 0) }; + + auto got = reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()); + + // Independently drive the three SSOT steps and frame directly. + auto hashes = resolve_other_tx_hashes(f.won, refs, f.nth_parent(), f.new_tx()); + auto others = assemble_other_txs(hashes, f.known_fn()); + auto direct = assemble_won_block(sh, gentx, gh, link, others); + + EXPECT_EQ(got.bytes, direct.first); + EXPECT_EQ(got.hex, direct.second); + auto sp = std::span( + reinterpret_cast(got.bytes.data()), got.bytes.size()); + EXPECT_EQ(got.hex, HexStr(sp)); +} + +// --- Test 2: tx layout is [gentx] ++ other_txs in ref order ------------------- +TEST(DgbReconstructWonBlock, CoinbaseFirstThenRefOrder) +{ + Fixture f; + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(0, 1), TxHashRefs(1, 0) }; + + auto got = reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()); + + PackStream ps(got.bytes); + BlockType blk; + ps >> blk; + + EXPECT_EQ(blk.m_merkle_root, gh); // empty link => root == gentx_hash + ASSERT_EQ(blk.m_txs.size(), 3u); // gentx + 2 others + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); // coinbase at 0 + EXPECT_TRUE(blk.m_txs[0].vin[0].prevout.hash.IsNull()); + EXPECT_EQ(blk.m_txs[1].vout[0].value, 11); // c1 -> make_tx(11), ref order + EXPECT_EQ(blk.m_txs[2].vout[0].value, 22); // d0 -> make_tx(22) +} + +// --- Test 3: empty refs => block carries only the gentx ----------------------- +TEST(DgbReconstructWonBlock, EmptyRefsGentxOnly) +{ + Fixture f; + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector refs; // none + + auto got = reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()); + + PackStream ps(got.bytes); + BlockType blk; + ps >> blk; + ASSERT_EQ(blk.m_txs.size(), 1u); + EXPECT_EQ(blk.m_txs[0].vin[0].prevout.index, 0xffffffffu); +} + +// --- Test 4: malformed ref (walk past chain end) propagates as throw ---------- +TEST(DgbReconstructWonBlock, RefPastChainThrows) +{ + Fixture f; + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + // share_count 9 walks far past p2 (the chain end) -> null ancestor. + std::vector refs = { TxHashRefs(9, 0) }; + + EXPECT_THROW( + reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()), + std::out_of_range); +} + +// --- Test 5: unknown other_tx hash propagates as throw ------------------------ +TEST(DgbReconstructWonBlock, UnknownKnownTxThrows) +{ + Fixture f; + f.known.erase(H("c1")); // resolves but is absent from known_txs + auto sh = make_small_header(); + auto gentx = make_gentx(); + auto gh = fixed_gentx_hash(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(0, 1) }; // -> c1, now missing + + EXPECT_THROW( + reconstruct_won_block(sh, link, gentx, gh, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()), + std::out_of_range); +} + +} // namespace