From 4216200c48f289f5073015c7235b8400f16e8a4c Mon Sep 17 00:00:00 2001 From: frstrtr Date: Tue, 21 Jul 2026 20:01:42 +0000 Subject: [PATCH] btc(#744): wire sharechain won-block dispatch (make_on_block_found) ShareTracker::m_on_block_found fires when a verified sharechain share crosses the BTC network target (share_tracker.hpp L385/L540), but main_btc.cpp never assigned it -- a peer-relayed won share was detected then silently dropped, the full subsidy lost. This adds btc::coin::make_on_block_found: reconstruct the won block (injected reconstructor) then dispatch connect-authoritative dual-path via broadcast_block_for_connect (P2P relay best-effort + submitblock RPC always). Reconstructor injected as std::function so the dispatch is build+run tested now; the faithful share->block reassembly (data.py Share.as_block) is the next slice. Adds won_share_dualpath_test.cpp (3 KATs: both-arms byte-identity, unassemblable -> broadcast-nothing, P2P-fail-still-connects) riding the allowlisted btc_share_test. Per-coin isolation: src/impl/btc/ only. --- src/impl/btc/coin/won_block_dispatch.hpp | 115 ++++++++++++++++++ src/impl/btc/test/CMakeLists.txt | 2 +- src/impl/btc/test/won_share_dualpath_test.cpp | 110 +++++++++++++++++ 3 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 src/impl/btc/coin/won_block_dispatch.hpp create mode 100644 src/impl/btc/test/won_share_dualpath_test.cpp diff --git a/src/impl/btc/coin/won_block_dispatch.hpp b/src/impl/btc/coin/won_block_dispatch.hpp new file mode 100644 index 000000000..463f63977 --- /dev/null +++ b/src/impl/btc/coin/won_block_dispatch.hpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// --------------------------------------------------------------------------- +// btc::coin::make_on_block_found -- the won-block DISPATCH handler the run-loop +// installs as btc::ShareTracker::m_on_block_found (gate #744). +// +// BTC exposes ShareTracker::m_on_block_found and fires it (share_tracker.hpp +// L385 scan path, L540 live-verify path) the moment a verified sharechain share +// crosses the BTC network target -- i.e. a block was WON by ANY peer's share, +// not just a local stratum submit. But main_btc.cpp never ASSIGNED that member, +// so a peer-relayed won share was detected and then SILENTLY DROPPED: the full +// subsidy lost. (The stratum submit path already dual-broadcasts via +// coin_node.submit_block_for_connect; this closes the OTHER seam -- the +// sharechain-detected won block.) +// +// This handler turns the winning share hash into a fully serialized parent +// block (via an injected WonBlockReconstructor -- the share->block "as_block" +// step) and dispatches it down the CONNECT-AUTHORITATIVE dual-path via +// broadcast_block_for_connect (P2P relay best-effort + submitblock RPC ALWAYS, +// because a cmpctblock announce alone does not ConnectBlock the tip -- the same +// policy the stratum won-block path already uses, block_broadcast.hpp). +// +// The reconstruct step is injected as a std::function rather than called inline +// so this handler is build-verifiable and run-tested NOW, before the faithful +// share->block reassembly (gentx + ref/merkle link + other-tx lookup, mirroring +// p2pool data.py Share.as_block, DGB #174/#176) is ported for BTC. The run-loop +// binds the real reconstructor + the live P2P relay sink + the submitblock RPC +// when they land; until then an injected reconstructor already carries a won +// block onto the network. Per-coin isolation: src/impl/btc/coin/ only. +// +// Contract: +// * reconstruct(share_hash) -> nullopt => UNKNOWN/unassemblable share: log a +// warning and broadcast NOTHING (never fabricate or relay a partial block). +// * reconstruct(share_hash) -> {bytes,hex} => fire broadcast_block_for_connect, +// whose own dual-path guards decide which sink(s) carry it; a both-legs-fail +// return is logged as a LOST SUBSIDY (never a silent success). +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include +#include + +#include "block_broadcast.hpp" // btc::coin::broadcast_block_for_connect + +namespace btc +{ +namespace coin +{ + +// Reconstructs the full serialized parent block for a won share. +// share_hash -> {block_bytes, block_hex}, or nullopt if the share is unknown +// or cannot be assembled (e.g. a referenced tx is missing). block_bytes is the +// pre-serialized blob the P2P relay sends; block_hex is the same block for the +// submitblock RPC. Mirrors p2pool data.py Share.as_block; the faithful body +// lands in a follow-up slice. +using WonBlockReconstructor = + std::function, std::string>>(const uint256&)>; + +// Embedded P2P relay sink: sends the pre-serialized block bytes. Returns true +// iff the relay accepted/sent. May be empty (no embedded sink wired yet). +using P2pRelaySink = std::function&)>; + +// submitblock RPC sink: connect-authoritative full-block submission. Returns +// true iff the daemon accepted. May be empty (RPC-less). +using SubmitRpcSink = std::function; + +// Build the m_on_block_found handler. The run-loop assigns the returned closure +// to tracker.m_on_block_found; `relay_p2p` may be empty (no embedded sink yet) +// and `submit_rpc` may be empty -- broadcast_block_for_connect guards each leg. +inline std::function +make_on_block_found(WonBlockReconstructor reconstruct, + P2pRelaySink relay_p2p, + SubmitRpcSink submit_rpc) +{ + return [reconstruct = std::move(reconstruct), + relay_p2p = std::move(relay_p2p), + submit_rpc = std::move(submit_rpc)](const uint256& share_hash) + { + if (!reconstruct) { + LOG_ERROR << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16) + << " -- no reconstructor wired; cannot broadcast."; + return; + } + + auto blk = reconstruct(share_hash); + if (!blk) { + LOG_WARNING << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16) + << " could not be reconstructed -- NOT broadcast."; + return; + } + + LOG_INFO << "[EMB-BTC] GOT BLOCK! share=" << share_hash.GetHex().substr(0, 16) + << " reconstructed " << blk->first.size() + << " bytes -- dispatching connect-authoritative dual-path."; + + const std::vector& bytes = blk->first; + const std::string& hex = blk->second; + bool reached = broadcast_block_for_connect( + [&]() -> bool { return relay_p2p ? relay_p2p(bytes) : false; }, + [&]() -> bool { return submit_rpc ? submit_rpc(hex) : false; }); + + if (!reached) { + LOG_ERROR << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16) + << " reached NEITHER sink -- SUBSIDY LOST."; + } + }; +} + +} // namespace coin +} // namespace btc diff --git a/src/impl/btc/test/CMakeLists.txt b/src/impl/btc/test/CMakeLists.txt index eaf0f47a8..e675af715 100644 --- a/src/impl/btc/test/CMakeLists.txt +++ b/src/impl/btc/test/CMakeLists.txt @@ -1,7 +1,7 @@ if (BUILD_TESTING AND GTest_FOUND) # btc twin of ltc share_test — uniquely named to avoid the CMP0002 target # collision with src/impl/ltc/test (both subdirs build in the same tree). - add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp) + add_executable(btc_share_test share_test.cpp f11_donation_invariance_test.cpp f10b_version_punish_removal_test.cpp g01_share_format_parity_test.cpp auto_ratchet_sim_test.cpp block_broadcast_guard_test.cpp block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp) target_link_libraries(btc_share_test PRIVATE GTest::gtest_main GTest::gtest core btc diff --git a/src/impl/btc/test/won_share_dualpath_test.cpp b/src/impl/btc/test/won_share_dualpath_test.cpp new file mode 100644 index 000000000..aa9b71ad2 --- /dev/null +++ b/src/impl/btc/test/won_share_dualpath_test.cpp @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +// --------------------------------------------------------------------------- +// Gate #744 BINDING KAT: forced won share -> make_on_block_found -> BOTH arms. +// +// BTC ShareTracker::m_on_block_found fires the instant a verified sharechain +// share crosses the BTC network target, but main_btc.cpp never assigned it, so +// a peer-relayed won share was silently dropped (subsidy lost). This drives a +// forced won share through the connect-authoritative dispatch handler and +// asserts BOTH broadcast arms fire: P2P relay (best-effort bytes) + submitblock +// RPC (connect-authoritative hex), carrying the byte-identical block. +// +// The reconstructor is INJECTED as a stub here -- the faithful share->block +// reassembly (mirroring p2pool data.py Share.as_block / DGB #174/#176) is the +// next sub-slice; this KAT pins the DISPATCH contract independent of it, exactly +// as the header's "build-verifiable and run-tested NOW" contract intends. +// +// Rides the already-allowlisted btc_share_test executable, so no build.yml +// --target allowlist change is needed (no #137-style NOT_BUILT sentinel risk). +// p2pool-merged-v36 surface: NONE. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +#include + +#include "../coin/won_block_dispatch.hpp" + +using btc::coin::make_on_block_found; + +namespace { + +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.push_back(d[c >> 4]); s.push_back(d[c & 0xf]); } + return s; +} + +using Recon = std::optional, std::string>>; + +} // namespace + +// 1) THE GATE: one forced won share -> reconstruct -> BOTH arms carry the +// byte-identical block (P2P relay bytes, submitblock RPC hex). +TEST(BtcForcedWonShareDualPath, BothArmsCarryIdenticalBlock) { + const std::vector block_bytes = {0x01, 0x02, 0x03, 0xde, 0xad, 0xbe, 0xef}; + const std::string block_hex = to_hex(block_bytes); + + std::vector relayed; + bool did_relay = false; + std::string submitted; + int submit_calls = 0; + + auto reconstruct = [&](const uint256&) -> Recon { return std::make_pair(block_bytes, block_hex); }; + auto relay = [&](const std::vector& b) { did_relay = true; relayed = b; return true; }; + auto submit = [&](const std::string& h) { ++submit_calls; submitted = h; return true; }; + + auto handler = make_on_block_found(reconstruct, relay, submit); + handler(uint256::ZERO); // FORCE the won share + + // ARM A -- embedded P2P relay fired with the block bytes. + ASSERT_TRUE(did_relay); + EXPECT_EQ(relayed, block_bytes); + + // ARM B -- submitblock RPC ALWAYS fired (connect-authoritative), with the hex. + ASSERT_EQ(submit_calls, 1); + EXPECT_EQ(submitted, block_hex); + + // CROSS-ARM IDENTITY: arm-A bytes hex-encode to exactly arm-B's hex. + EXPECT_EQ(to_hex(relayed), submitted); +} + +// 2) Unassemblable share -> reconstruct returns nullopt -> NEITHER arm fires +// (never fabricate or relay a partial block). +TEST(BtcForcedWonShareDualPath, UnassemblableShareBroadcastsNothing) { + bool did_relay = false; + int submit_calls = 0; + + auto reconstruct = [&](const uint256&) -> Recon { return std::nullopt; }; + auto relay = [&](const std::vector&) { did_relay = true; return true; }; + auto submit = [&](const std::string&) { ++submit_calls; return true; }; + + auto handler = make_on_block_found(reconstruct, relay, submit); + handler(uint256::ZERO); + + EXPECT_FALSE(did_relay); + EXPECT_EQ(submit_calls, 0); +} + +// 3) Connect-authoritative: even when the P2P relay FAILS, the submitblock RPC +// still fires and the block reaches a sink (no silent loss). +TEST(BtcForcedWonShareDualPath, P2pFailureStillConnectsViaRpc) { + const std::vector block_bytes = {0xaa, 0xbb}; + + int submit_calls = 0; + auto reconstruct = [&](const uint256&) -> Recon { return std::make_pair(block_bytes, std::string("aabb")); }; + auto relay = [&](const std::vector&) { return false; }; // P2P down + auto submit = [&](const std::string&) { ++submit_calls; return true; }; + + auto handler = make_on_block_found(reconstruct, relay, submit); + handler(uint256::ZERO); + + EXPECT_EQ(submit_calls, 1); // RPC fired despite P2P failure +}