From aa917e0386d7d1b64bb2de389807a1058bf6d746 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 21 Jun 2026 00:54:22 +0000 Subject: [PATCH] dgb(#82): forced-won-share dual-path binding KAT Drive a forced won share through the REAL run-loop reconstructor (make_reconstruct_closure_from_template) wired into make_on_block_found, and assert the one won block fans out down BOTH broadcast arms carrying the byte-identical faithfully-reconstructed block: ARM A embedded P2P relay (primary) -> block_bytes ARM B external digibyted submitblock fallback -> block_hex cross-arm identity: HexStr(arm A bytes) == arm B hex == oracle This is the deterministic gate-closer for the broadcaster dual-path: the existing dgb_won_block_dispatch_test pins the same fan-out with a CANNED stub reconstructor; this KAT proves it with the real reconstructor, so the bytes both arms carry are the genuine won block, not synthesized constants. Also pins coinbase-only-template (empty mempool, todays shape), RPC-only deployment, and fail-closed (unknown forced share fires NEITHER arm). Fenced src/impl/dgb test-only + build.yml allowlist (both sites). No p2pool-merged-v36 surface: block dispatch + framing only. --- .github/workflows/build.yml | 4 +- src/impl/dgb/test/CMakeLists.txt | 15 + .../test/forced_won_share_dualpath_test.cpp | 258 ++++++++++++++++++ 3 files changed, 275 insertions(+), 2 deletions(-) create mode 100644 src/impl/dgb/test/forced_won_share_dualpath_test.cpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 042ac7336..db1c1eeb5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_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 dgb_coinbase_value_parity_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 \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \ v37_test \ -j$(nproc) @@ -217,7 +217,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test dgb_gentx_share_path_test dgb_other_tx_resolver_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 dgb_coinbase_value_parity_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 \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 62e34b083..55a64efa1 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -108,6 +108,21 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_reconstruct_closure_test "" AUTO) + # --- #82 broadcaster-gate BINDING KAT: forced won share -> dual path ------ + # Drives a forced won share through the REAL from-template reconstruct + # closure wired into make_on_block_found, asserting BOTH broadcast arms + # (P2P relay + submitblock fallback) carry the byte-identical faithful block. + # Links the dgb OBJECT lib like dgb_reconstruct_closure_test (pulls + # reconstruct_won_block / block_assembly via the closure). MUST also appear + # in the build.yml --target allowlist (#143 NOT_BUILT trap). + add_executable(dgb_forced_won_share_dualpath_test forced_won_share_dualpath_test.cpp) + target_link_libraries(dgb_forced_won_share_dualpath_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_forced_won_share_dualpath_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/forced_won_share_dualpath_test.cpp b/src/impl/dgb/test/forced_won_share_dualpath_test.cpp new file mode 100644 index 000000000..d8996d7bc --- /dev/null +++ b/src/impl/dgb/test/forced_won_share_dualpath_test.cpp @@ -0,0 +1,258 @@ +// --------------------------------------------------------------------------- +// dgb_forced_won_share_dualpath_test -- the #82 broadcaster-gate BINDING KAT. +// +// This is the deterministic gate-closer for DGB's won-block broadcaster +// dual-path (integrator 2026-06-21): it drives a FORCED won share through the +// REAL run-loop reconstructor (coin::make_reconstruct_closure_from_template, +// the #271/#279/#280 closure main_dgb.cpp installs) wired into the REAL +// dispatch handler (coin::make_on_block_found, tracker.m_on_block_found), and +// asserts the one won block fans out down BOTH broadcast sinks -- +// ARM A : embedded P2P relay (primary) -> receives block_bytes +// ARM B : external digibyted submitblock (fallback, always-fire) -> block_hex +// -- carrying the BYTE-IDENTICAL faithfully-reconstructed block (HexStr(bytes) +// of arm A == hex of arm B == the oracle reconstruct_won_block_from_template +// output). The existing dgb_won_block_dispatch_test pins the same fan-out with +// a CANNED stub reconstructor; this KAT closes the gap integrator named: prove +// the dual-path with the REAL reconstructor so the bytes both arms carry are +// the genuine won block, not synthesized constants. +// +// The live A/B soak (block lands on peer node B + submitblock accepts on a +// self-provisioned regtest digibyted) is the CORROBORATING evidence; THIS KAT +// is the binding proof on the record. SYNTHETIC seams only -- no live +// ShareTracker / mempool / network. Fail-closed end-to-end is also pinned: a +// forced won share the chain does not know fires NEITHER arm. +// +// DGB-Scrypt is a STANDALONE parent in the V36 default build (no merged- +// coinbase leg). p2pool-merged-v36 surface: NONE -- block dispatch + framing +// only; no PoW hash, share format, coinbase commitment, or PPLNS math touched. +// Per-coin isolation: src/impl/dgb/ only. MUST appear in BOTH test/CMakeLists +// AND the build.yml --target allowlist (#143 NOT_BUILT sentinel trap). +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "../coin/reconstruct_closure.hpp" // make_reconstruct_closure_from_template, reconstruct_won_block_from_template, unpack_gentx_coinbase, WonShareInputs, types +#include "../coin/won_block_dispatch.hpp" // make_on_block_found + +using dgb::coin::make_reconstruct_closure_from_template; +using dgb::coin::reconstruct_won_block_from_template; +using dgb::coin::unpack_gentx_coinbase; +using dgb::coin::make_on_block_found; +using dgb::coin::SmallBlockHeaderType; +using dgb::coin::MutableTransaction; +using dgb::coin::WonShareInputs; + +namespace { + +// ---- synthetic share inputs (mirror reconstruct_closure_test.cpp) ----------- + +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 gentx serialization (the run-loop gentx_bytes_fn 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()); +} + +// won_share_fields_fn that knows ONLY the won share; throws for any other hash +// (the run-loop's chain.get_share miss => fail-closed). +std::function +won_fields(const uint256& won, SmallBlockHeaderType sh, ::dgb::MerkleLink link) +{ + return [won, sh, link](const uint256& h) -> WonShareInputs { + if (h != won) throw std::out_of_range("won_share_fields: unknown share"); + return WonShareInputs{sh, link}; + }; +} + +// Independent hex encoder so the cross-arm identity (arm A bytes hex-encode to +// the exact hex arm B submitted) is asserted self-containedly, not assumed. +std::string to_hex(const std::vector& b) +{ + static const char* d = "0123456789abcdef"; + std::string s; s.reserve(b.size() * 2); + for (unsigned char c : b) { s += d[c >> 4]; s += d[c & 0x0f]; } + return s; +} + +// ---- controllable submitblock seam (mirror won_block_dispatch_test.cpp) ------ + +class FakeSeam : public core::coin::ICoinNode { +public: + bool rpc_present = true; + bool submit_ack = true; + int submit_calls = 0; + std::string last_hex; + + core::coin::WorkView get_work_view() override { return {}; } + bool submit_block_hex(const std::string& hex, bool) override { + ++submit_calls; last_hex = hex; return submit_ack; + } + bool is_embedded() const override { return false; } + bool has_rpc() const override { return rpc_present; } +}; + +// Build the REAL from-template reconstruct closure over a won share with the +// given non-coinbase template set, and the oracle block it must produce. +struct Rig { + uint256 won = H("a0"); + SmallBlockHeaderType sh = make_small_header(); + ::dgb::MerkleLink link; + std::vector gentx_bytes = noseg_bytes(make_gentx()); + dgb::coin::WonBlockReconstructor reconstruct; + std::vector oracle_bytes; + std::string oracle_hex; + + explicit Rig(std::vector other) + { + auto ug = unpack_gentx_coinbase(gentx_bytes); + auto oracle = reconstruct_won_block_from_template(sh, link, ug.tx, ug.txid, other); + oracle_bytes = oracle.bytes; + oracle_hex = oracle.hex; + auto gb = gentx_bytes; + reconstruct = make_reconstruct_closure_from_template( + won_fields(won, sh, link), + [gb](const uint256&) { return gb; }, + [other](const uint256&) { return other; }); + } +}; + +} // namespace + +// 1) THE GATE: one forced won share -> REAL reconstruct -> BOTH arms carry the +// byte-identical faithful block (P2P primary bytes, submitblock fallback hex). +TEST(DgbForcedWonShareDualPath, BothArmsCarryIdenticalFaithfulBlock) +{ + Rig rig({ make_tx(11), make_tx(22) }); // a non-trivial template tx set + + std::vector relayed; + bool did_relay = false; + auto p2p = [&](const std::vector& b) { did_relay = true; relayed = b; }; + FakeSeam seam; + + auto handler = make_on_block_found(rig.reconstruct, p2p, &seam); + handler(rig.won); // FORCE the won share + + // ARM A -- embedded P2P relay fired with the faithful block bytes. + ASSERT_TRUE(did_relay); + EXPECT_FALSE(relayed.empty()); + EXPECT_EQ(relayed, rig.oracle_bytes); + + // ARM B -- submitblock fallback ALWAYS fired, with the faithful block hex. + ASSERT_EQ(seam.submit_calls, 1); + EXPECT_EQ(seam.last_hex, rig.oracle_hex); + + // CROSS-ARM IDENTITY: both arms carry the SAME block bytes (arm-A bytes + // hex-encode to exactly arm-B's submitted hex). This is the dual-path proof. + EXPECT_EQ(to_hex(relayed), seam.last_hex); +} + +// 2) Today's realistic shape: empty template (mempool tx-selection pending) -> +// a valid coinbase-only block still dual-paths byte-identically (NOT a stub, +// NOT fail-closed -- correct-and-empty). +TEST(DgbForcedWonShareDualPath, CoinbaseOnlyTemplateStillDualPaths) +{ + Rig rig(std::vector{}); + + std::vector relayed; + auto p2p = [&](const std::vector& b) { relayed = b; }; + FakeSeam seam; + + auto handler = make_on_block_found(rig.reconstruct, p2p, &seam); + handler(rig.won); + + EXPECT_EQ(relayed, rig.oracle_bytes); + EXPECT_EQ(seam.submit_calls, 1); + EXPECT_EQ(seam.last_hex, rig.oracle_hex); + EXPECT_EQ(to_hex(relayed), seam.last_hex); +} + +// 3) RPC-only deployment (no embedded P2P sink wired yet): the same forced won +// block still reaches the network via the submitblock fallback alone, +// carrying the identical faithful hex. +TEST(DgbForcedWonShareDualPath, RpcOnlyDeploymentStillReachesNetwork) +{ + Rig rig({ make_tx(7) }); + FakeSeam seam; + + auto handler = make_on_block_found(rig.reconstruct, /*p2p_relay=*/{}, &seam); + handler(rig.won); + + EXPECT_EQ(seam.submit_calls, 1); + EXPECT_EQ(seam.last_hex, rig.oracle_hex); +} + +// 4) FAIL-CLOSED end-to-end: a forced won share the chain cannot reconstruct +// (won_share_fields miss) fires NEITHER arm -- no fabricated/partial block +// ever reaches either broadcast path. +TEST(DgbForcedWonShareDualPath, UnreconstructableForcedShareFiresNeitherArm) +{ + Rig rig({ make_tx(11) }); + + bool did_relay = false; + auto p2p = [&](const std::vector&) { did_relay = true; }; + FakeSeam seam; + + auto handler = make_on_block_found(rig.reconstruct, p2p, &seam); + handler(H("ff")); // NOT the won share the rig knows + + EXPECT_FALSE(did_relay); // P2P arm silent + EXPECT_EQ(seam.submit_calls, 0); // submitblock fallback silent +}