From 23683bd6bb8d6f1c368a70f5ffd950cd4c4f25fe Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sat, 20 Jun 2026 07:18:50 +0000 Subject: [PATCH] dgb(#82): faithful won-block reconstruct closure + fail-closed KAT make_reconstruct_closure (coin/reconstruct_closure.hpp) is the run-loop WonBlockReconstructor main_dgb.cpp binds in place of the interim nullopt stub: it composes the landed slices into p2pool Share.as_block -- share-field + gentx-regen seams -> unpack_gentx_coinbase -> reconstruct_ won_block (resolve refs -> assemble other_txs -> frame block). All sharechain/mempool access is INJECTED so the closure is unit-testable with no live tracker. Reward-path fail-closed: the callback fires on the compute thread and a broad std::exception catch yields std::nullopt (never throws, never a partial block) on ANY missing share, missing known-tx, out-of-range ref-walk, gentx-regen failure, or malformed gentx -- announce + audit, RPC submitblock fallback still attempts. dgb_reconstruct_closure_test 6/6 (success == reconstruct_won_block on same inputs; 5 distinct error paths each -> nullopt + loud log). Wired into test CMake + both build.yml allowlists. src/impl/dgb/ only; p2pool-merged-v36 surface: none. --- .github/workflows/build.yml | 4 +- src/impl/dgb/coin/reconstruct_closure.hpp | 126 ++++++++ src/impl/dgb/test/CMakeLists.txt | 14 + .../dgb/test/reconstruct_closure_test.cpp | 278 ++++++++++++++++++ 4 files changed, 420 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/coin/reconstruct_closure.hpp create mode 100644 src/impl/dgb/test/reconstruct_closure_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15378474d..4e991d71f 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 test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ v37_test \ @@ -202,7 +202,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_block_assembly_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \ dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ - dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test \ + dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ diff --git a/src/impl/dgb/coin/reconstruct_closure.hpp b/src/impl/dgb/coin/reconstruct_closure.hpp new file mode 100644 index 000000000..9a0453f05 --- /dev/null +++ b/src/impl/dgb/coin/reconstruct_closure.hpp @@ -0,0 +1,126 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::make_reconstruct_closure -- the run-loop's faithful +// WonBlockReconstructor (#82): the closure main_dgb.cpp binds into +// make_on_block_found, replacing the interim `return nullopt` stub. It is the +// open half of DGB's #82 broadcaster dual-path -- until this is non-stub a +// pool-won DGB block is announced + audited but never reconstructed, so neither +// the P2P-relay arm (#260) nor the submitblock RPC fallback ever fires on a +// real won share. +// +// It composes the already-landed, individually-KAT'd slices into one faithful +// port of p2pool data.py Share.as_block(tracker, known_txs): +// gentx = unpack_gentx_coinbase(generate_share_transaction(share).bytes) +// other_txs = [known_txs[h] for h in get_other_tx_hashes(tracker)] +// block = reconstruct_won_block(header, link, gentx, ..., refs, ...) +// +// SEAM-FIRST (mirrors every #82 sub-slice): the share lookup, the gentx +// regeneration, and the three tracker/mempool walks are INJECTED as callables, +// so the whole closure -- including its fail-closed posture -- is unit-testable +// against a synthetic share set with NO live ShareTracker / mempool. In the +// run-loop they bind to: +// share_fields_fn = chain.get_share(h) -> {m_min_header, m_merkle_link, +// m_tx_info.m_transaction_hash_refs} +// gentx_bytes_fn = generate_share_transaction(share, tracker, params, +// false, v36_active=false, &gc) -> gc.bytes (#173 SSOT) +// 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 = mempool/known-tx find_tx(hash) -> *MutableTransaction +// +// FAIL-CLOSED (integrator 2026-06-19/20, REWARD-PATH CRITICAL): this callback +// fires from the compute thread on a won share. It NEVER throws out of the +// callback and NEVER emits a partial/wrong block: ANY missing share, missing +// known-tx, out-of-range ref-walk, or gentx-regen failure is caught, logged +// LOUDLY, and yields std::nullopt -- the share is announced + audited, the RPC +// submitblock fallback still attempts independently, and no malformed block +// reaches the network. A wrong reconstruction would hash to the wrong merkle +// root and be daemon-rejected anyway, so failing closed here is strictly safer +// than emitting it. +// +// 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. DGB-Scrypt is a STANDALONE parent in the V36 +// default build (no merged-coinbase leg). +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "reconstruct_won_block.hpp" // reconstruct_won_block, ReconstructedWonBlock, SmallBlockHeaderType, MutableTransaction +#include "gentx_unpack.hpp" // unpack_gentx_coinbase, UnpackedGentx +#include "won_block_dispatch.hpp" // WonBlockReconstructor + +namespace dgb +{ +namespace coin +{ + +// The per-share inputs the closure pulls from the sharechain to drive +// reconstruct_won_block: everything that is NOT the gentx or the ancestry walk. +// small_header : share.m_min_header (version|prev|time|bits|nonce) +// merkle_link : share.m_merkle_link (gentx -> merkle root branch) +// refs : share.m_tx_info.m_transaction_hash_refs, in order +struct ShareReconstructFields +{ + SmallBlockHeaderType small_header; + ::dgb::MerkleLink merkle_link; + std::vector refs; +}; + +// Build the run-loop WonBlockReconstructor. See header note for the run-loop +// bindings of each injected seam. The returned callable is fail-closed: it +// returns std::nullopt (never throws, never a partial block) on ANY error. +inline WonBlockReconstructor +make_reconstruct_closure( + std::function share_fields_fn, + std::function(const uint256&)> gentx_bytes_fn, + std::function nth_parent_fn, + std::function&(const uint256&)> new_tx_hashes_fn, + std::function known_txs_fn) +{ + return [share_fields_fn = std::move(share_fields_fn), + gentx_bytes_fn = std::move(gentx_bytes_fn), + nth_parent_fn = std::move(nth_parent_fn), + new_tx_hashes_fn = std::move(new_tx_hashes_fn), + known_txs_fn = std::move(known_txs_fn)](const uint256& share_hash) + -> std::optional, std::string>> + { + try + { + // 1. share-level fields (header / merkle_link / tx refs). + const ShareReconstructFields f = share_fields_fn(share_hash); + + // 2. regenerate the share's SSOT gentx bytes, then unpack to the + // MutableTransaction (+ canonical txid) reconstruct injects at + // block tx index 0. unpack throws on a malformed serialization. + const UnpackedGentx ug = unpack_gentx_coinbase(gentx_bytes_fn(share_hash)); + + // 3. compose: resolve refs -> assemble other_txs -> frame the block. + ReconstructedWonBlock r = reconstruct_won_block( + f.small_header, f.merkle_link, ug.tx, ug.txid, share_hash, f.refs, + nth_parent_fn, new_tx_hashes_fn, known_txs_fn); + + return std::make_pair(std::move(r.bytes), std::move(r.hex)); + } + catch (const std::exception& e) + { + // Fail closed: announce + audit, never broadcast a partial/wrong + // block. The RPC submitblock fallback still attempts independently. + std::cout << "[DGB-POOL-BLOCK] won share " << share_hash.GetHex().substr(0, 16) + << " -- reconstruct FAILED CLOSED (" << e.what() + << "); NOT broadcast on P2P arm (RPC fallback still attempts)" + << std::endl; + return std::nullopt; + } + }; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index d9c71fbfb..7fbcede4d 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -94,6 +94,20 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_reconstruct_won_block_test "" AUTO) + # --- #82: won-block reconstruct CLOSURE (run-loop WonBlockReconstructor) - + # Pins coin/reconstruct_closure.hpp: make_reconstruct_closure composes the + # share-field + gentx-regen seams + reconstruct_won_block under a fail-closed + # catch (the closure main_dgb.cpp binds in place of the nullopt stub). Links + # the dgb OBJECT lib like dgb_reconstruct_won_block_test (pulls block_assembly + # via the closure). MUST also appear in the build.yml --target allowlist. + add_executable(dgb_reconstruct_closure_test reconstruct_closure_test.cpp) + target_link_libraries(dgb_reconstruct_closure_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_closure_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_closure_test.cpp b/src/impl/dgb/test/reconstruct_closure_test.cpp new file mode 100644 index 000000000..fee934a42 --- /dev/null +++ b/src/impl/dgb/test/reconstruct_closure_test.cpp @@ -0,0 +1,278 @@ +// --------------------------------------------------------------------------- +// dgb_reconstruct_closure_test -- pins coin/reconstruct_closure.hpp, the run- +// loop's faithful WonBlockReconstructor (#82) that main_dgb.cpp binds into +// make_on_block_found in place of the interim `return nullopt` stub. +// +// Proves the two contracts that matter for the reward path: +// * SUCCESS: a fully-resolvable won share reconstructs to EXACTLY the block +// reconstruct_won_block produces from the same unpacked gentx + ancestry +// (the closure adds the share-field + gentx-regen seams and changes nothing +// about the composed bytes / hex). +// * FAIL-CLOSED (integrator 2026-06-20, REWARD-PATH CRITICAL): the callback +// NEVER throws and NEVER emits a partial block -- an unknown share, a +// missing known-tx, an out-of-range ref-walk, a gentx-regen failure, and a +// malformed gentx serialization EACH yield std::nullopt (announce + audit, +// RPC fallback still attempts). The broad catch swallows ANY std::exception, +// not only std::out_of_range, so a regen path throwing some other type +// still fails closed rather than crashing the compute thread. +// +// SYNTHETIC seams only (no live ShareTracker / mempool), mirroring the fixture +// in reconstruct_won_block_test.cpp. Links the dgb OBJECT lib like +// dgb_reconstruct_won_block_test (pulls block_assembly.hpp via the closure). +// MUST also appear in the build.yml --target allowlist (#143 NOT_BUILT trap). +// --------------------------------------------------------------------------- +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "../coin/reconstruct_closure.hpp" + +using dgb::coin::make_reconstruct_closure; +using dgb::coin::ShareReconstructFields; +using dgb::coin::reconstruct_won_block; +using dgb::coin::unpack_gentx_coinbase; +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; +} + +// Canonical non-witness serialization of a gentx -- the exact bytes the run-loop +// gentx_bytes_fn surfaces (generate_share_transaction's GentxCoinbase.bytes). +std::vector noseg_bytes(const MutableTransaction& tx) +{ + auto packed = pack(dgb::coin::TX_NO_WITNESS(tx)); + auto sp = packed.get_span(); + return std::vector( + reinterpret_cast(sp.data()), + reinterpret_cast(sp.data()) + sp.size()); +} + +// Synthetic ancestry won <- p1 <- p2 (p2 has no parent => walks past it run off +// the known chain), each share carrying new_transaction_hashes, plus a known_txs +// store. e0/e1 (p2's new txs) are deliberately ABSENT from `known` so a ref that +// resolves to them models a missing known-tx. +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; // parent[p2] unset => walk off-chain + nths[won] = { H("c0"), H("c1") }; + nths[p1] = { H("d0") }; + nths[p2] = { H("e0"), H("e1") }; // NOT in `known` + 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; + }; + } + // share_fields_fn: knows ONLY the won share; throws std::out_of_range for any + // other hash (the run-loop's chain.get_share miss). + std::function + share_fields(SmallBlockHeaderType sh, ::dgb::MerkleLink link, + std::vector refs) const + { + uint256 w = won; + return [w, sh, link, refs](const uint256& h) -> ShareReconstructFields { + if (h != w) throw std::out_of_range("share_fields: unknown share"); + return ShareReconstructFields{sh, link, refs}; + }; + } +}; + +} // namespace + +// --- Test 1: success path == reconstruct_won_block on the same inputs --------- +TEST(DgbReconstructClosure, SuccessComposesIdenticalBlock) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(0, 1), TxHashRefs(1, 0) }; // -> c1, d0 + auto gentx_bytes = noseg_bytes(make_gentx()); + auto ug = unpack_gentx_coinbase(gentx_bytes); + + auto expected = reconstruct_won_block(sh, link, ug.tx, ug.txid, f.won, refs, + f.nth_parent(), f.new_tx(), f.known_fn()); + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, refs), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + auto got = closure(f.won); + ASSERT_TRUE(got.has_value()); + EXPECT_FALSE(got->first.empty()); + EXPECT_EQ(got->first, expected.bytes); + EXPECT_EQ(got->second, expected.hex); +} + +// --- Test 2: unknown share => nullopt (share_fields miss) --------------------- +TEST(DgbReconstructClosure, UnknownShareFailsClosed) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + auto gentx_bytes = noseg_bytes(make_gentx()); + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, {}), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + EXPECT_FALSE(closure(H("ff")).has_value()); // not the won share +} + +// --- Test 3: missing known-tx => nullopt (assemble_other_txs out_of_range) ----- +TEST(DgbReconstructClosure, MissingKnownTxFailsClosed) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(2, 0) }; // won->p2, nths[0]=e0 (absent) + auto gentx_bytes = noseg_bytes(make_gentx()); + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, refs), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + EXPECT_FALSE(closure(f.won).has_value()); +} + +// --- Test 4: ref-walk past chain end => nullopt ------------------------------- +TEST(DgbReconstructClosure, OutOfRangeRefWalkFailsClosed) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + std::vector refs = { TxHashRefs(9, 0) }; // walks off the known chain + auto gentx_bytes = noseg_bytes(make_gentx()); + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, refs), + [gentx_bytes](const uint256&) { return gentx_bytes; }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + EXPECT_FALSE(closure(f.won).has_value()); +} + +// --- Test 5: gentx regen throws (non-out_of_range) => still nullopt ----------- +TEST(DgbReconstructClosure, GentxRegenThrowFailsClosed) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, {}), + [](const uint256&) -> std::vector { + throw std::runtime_error("regen boom"); }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + EXPECT_FALSE(closure(f.won).has_value()); // broad catch, not just out_of_range +} + +// --- Test 6: malformed gentx bytes => nullopt (unpack out_of_range) ----------- +TEST(DgbReconstructClosure, MalformedGentxBytesFailsClosed) +{ + Fixture f; + auto sh = make_small_header(); + ::dgb::MerkleLink link; + auto bad = noseg_bytes(make_gentx()); + bad.push_back(0xff); // trailing byte => unpack throws + + auto closure = make_reconstruct_closure( + f.share_fields(sh, link, {}), + [bad](const uint256&) { return bad; }, + f.nth_parent(), f.new_tx(), f.known_fn()); + + EXPECT_FALSE(closure(f.won).has_value()); +}