From 18106213f2b41a15a399ea6f6bb17159ddb0cf64 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Thu, 18 Jun 2026 16:02:21 +0000 Subject: [PATCH] =?UTF-8?q?btc:=20broadcaster-gate=20FILL=20=E2=80=94=20su?= =?UTF-8?q?bmitblock=20RPC=20FALLBACK=20+=20never-silent-drop=20won=20bloc?= =?UTF-8?q?k?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the won-block broadcaster gate for BTC. Prior path was single-sink: the stratum won-block lambda called submit_block_p2p_raw only, and node.hpp had a bare `if (m_p2p)` with no else — a silent-drop hole on a found block. FALLBACK semantics (P2P relay primary; submitblock RPC fires only when P2P is unavailable or relay failed — NOT always-both / no double-broadcast): - coin/block_broadcast.hpp (new): broadcast_block_with_fallback() seam — pure, testable decision: try P2P; on null/fail fall back to RPC; returns true iff at least one sink accepted. - coin/node.hpp: submit_block_p2p_raw now returns bool; new submit_block_hex (RPC sink via NodeRPC::submit_block_hex) and submit_block_with_fallback. - coin/p2p_node.hpp: submit_block_raw / submit_block_full return bool (false when no peer or relay failed) so failure — not just null — triggers the fallback. - stratum/work_source.{hpp,cpp}: SubmitBlockFn returns bool; the won-block path logs a loud ERROR when a found block reaches NEITHER sink (or no fn is wired). A won block can never be lost quietly. - c2pool/main_btc.cpp: stratum_submit_fn returns the fallback outcome. - test/block_broadcast_test.cpp (new): standalone gate harness, 13/13 PASS — happy (P2P only, RPC does NOT fire), degraded/null-p2p (RPC fallback fires), both-down (reaches neither -> won-block path screams). BTC-only; build c2pool-btc GREEN. --- src/c2pool/main_btc.cpp | 7 +- src/impl/btc/coin/block_broadcast.hpp | 36 ++++++++ src/impl/btc/coin/node.hpp | 32 ++++++- src/impl/btc/coin/p2p_node.hpp | 16 ++-- src/impl/btc/stratum/work_source.cpp | 16 +++- src/impl/btc/stratum/work_source.hpp | 5 +- src/impl/btc/test/block_broadcast_test.cpp | 102 +++++++++++++++++++++ 7 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 src/impl/btc/coin/block_broadcast.hpp create mode 100644 src/impl/btc/test/block_broadcast_test.cpp diff --git a/src/c2pool/main_btc.cpp b/src/c2pool/main_btc.cpp index e24b3fff5..13046c35c 100644 --- a/src/c2pool/main_btc.cpp +++ b/src/c2pool/main_btc.cpp @@ -747,14 +747,14 @@ int main(int argc, char* argv[]) // captures by reference so it reuses the existing B5 infrastructure // instead of duplicating it. auto stratum_submit_fn = [&coin_node, pending_mu, pending_submits] - (const std::vector& block_bytes, uint32_t height) + (const std::vector& block_bytes, uint32_t height) -> bool { // Compute block_hash for pending_submits tracking. BTC block_hash = // SHA256d of the first 80 bytes (the header). if (block_bytes.size() < 80) { LOG_WARNING << "[BTC-STRATUM-BLOCK] block bytes too short (" << block_bytes.size() << " < 80) — not submitting"; - return; + return false; } uint256 block_hash = Hash(std::span(block_bytes.data(), 80)); { @@ -765,7 +765,8 @@ int main(int argc, char* argv[]) } LOG_INFO << "[BTC-SUBMIT] sending block " << block_hash.GetHex().substr(0, 16) << " height=" << height << " (via stratum)"; - coin_node.submit_block_p2p_raw(block_bytes); + // FALLBACK: P2P relay primary, submitblock RPC if P2P unavailable/failed. + return coin_node.submit_block_with_fallback(block_bytes); }; // Construct the work source. Holds non-owning refs to chain + mempool; diff --git a/src/impl/btc/coin/block_broadcast.hpp b/src/impl/btc/coin/block_broadcast.hpp new file mode 100644 index 000000000..1c5dc973a --- /dev/null +++ b/src/impl/btc/coin/block_broadcast.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +namespace btc +{ +namespace coin +{ + +// FALLBACK broadcast orchestration for a WON block. +// +// P2P relay is PRIMARY; the submitblock RPC is the FALLBACK and fires ONLY +// when P2P is unavailable or the relay did not succeed. This is deliberately +// NOT always-both / double-broadcast: bitcoind dedupes a duplicate submitblock +// harmlessly, but the fallback gives us robustness against a silent +// P2P-unavailable / relay-failed hole without an unconditional double submit. +// +// `relay_p2p` -> returns true on a successful P2P relay. +// `submit_rpc` -> returns true when bitcoind accepts the block. +// +// Returns true iff the block reached AT LEAST ONE sink. A false return means +// the block reached NEITHER sink: the caller MUST treat that as a lost +// subsidy and scream (never silent-drop a won block). +inline bool broadcast_block_with_fallback( + const std::function& relay_p2p, + const std::function& submit_rpc) +{ + if (relay_p2p && relay_p2p()) + return true; // primary succeeded -> no double-broadcast + if (submit_rpc) + return submit_rpc(); // fallback path + return false; // reached neither sink +} + +} // namespace coin +} // namespace btc diff --git a/src/impl/btc/coin/node.hpp b/src/impl/btc/coin/node.hpp index 48842ca0e..5e5f540ff 100644 --- a/src/impl/btc/coin/node.hpp +++ b/src/impl/btc/coin/node.hpp @@ -4,9 +4,12 @@ #include +#include // HexStr + #include "rpc.hpp" #include "p2p_node.hpp" #include "node_interface.hpp" +#include "block_broadcast.hpp" namespace btc { @@ -91,10 +94,35 @@ class Node : public btc::interfaces::Node /// source which already has the full block bytes assembled from /// (header || tx_count || coinbase || tx_data) and doesn't need to /// round-trip through BlockType deserialization. - void submit_block_p2p_raw(const std::vector& block_bytes) + /// Returns true iff the block was relayed to a connected peer. + bool submit_block_p2p_raw(const std::vector& block_bytes) { if (m_p2p) - m_p2p->submit_block_raw(block_bytes); + return m_p2p->submit_block_raw(block_bytes); + return false; + } + + /// Submit a pre-serialized block to bitcoind via the submitblock RPC. + /// This is the FALLBACK sink (P2P relay is primary). Returns true iff + /// the daemon accepted the block. ignore_failure is set so a rejection + /// is logged but does not throw out of the won-block path. + bool submit_block_hex(const std::vector& block_bytes) + { + if (!m_rpc) + return false; + return m_rpc->submit_block_hex(HexStr(block_bytes), /*ignore_failure=*/true); + } + + /// Broadcast a WON block with FALLBACK semantics: P2P relay is primary, + /// the submitblock RPC fires only if P2P is unavailable or the relay did + /// not succeed (NOT always-both). Returns true iff the block reached at + /// least one sink; a false return means it reached NEITHER and the caller + /// MUST treat it as a lost subsidy. + bool submit_block_with_fallback(const std::vector& block_bytes) + { + return broadcast_block_with_fallback( + [&]{ return submit_block_p2p_raw(block_bytes); }, + [&]{ return submit_block_hex(block_bytes); }); } bool has_p2p() const { return m_p2p != nullptr; } diff --git a/src/impl/btc/coin/p2p_node.hpp b/src/impl/btc/coin/p2p_node.hpp index 87bad1ba6..2edb31a1e 100644 --- a/src/impl/btc/coin/p2p_node.hpp +++ b/src/impl/btc/coin/p2p_node.hpp @@ -345,9 +345,9 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core:: /// Relay a pre-serialized block via P2P. /// Uses compact block format (BIP 152 v2) for peers that support it, /// falling back to full block otherwise. - void submit_block_raw(const std::vector& block_bytes) + bool submit_block_raw(const std::vector& block_bytes) { - if (!m_peer) return; + if (!m_peer) return false; if (m_peer_supports_cmpct && m_peer_cmpct_version >= 2) { // Deserialize the block to build a compact representation @@ -371,7 +371,7 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core:: << blockhash.GetHex() << " (" << cb.short_ids.size() << " short IDs, " << cb.prefilled_txns.size() << " prefilled)"; - return; + return true; } catch (const std::exception& e) { LOG_WARNING << "[" << m_chain_label << "] Compact block build failed (block_size=" << block_bytes.size() @@ -385,18 +385,20 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core:: } // Fallback: send full block - submit_block_full(block_bytes); + return submit_block_full(block_bytes); } - /// Send a full block message (legacy relay). - void submit_block_full(const std::vector& block_bytes) + /// Send a full block message (legacy relay). Returns true iff a peer + /// was connected and the block bytes were written to it. + bool submit_block_full(const std::vector& block_bytes) { - if (!m_peer) return; + if (!m_peer) return false; PackStream ps(block_bytes); auto rmsg = std::make_unique("block", std::move(ps)); m_peer->write(rmsg); LOG_INFO << "[" << m_chain_label << "] Sent full block message (" << block_bytes.size() << " bytes) to " << m_target_addr.to_string(); + return true; } //[x][x][x] void handle_message_version(std::shared_ptr msg, CoindProtocol* protocol); // diff --git a/src/impl/btc/stratum/work_source.cpp b/src/impl/btc/stratum/work_source.cpp index 778b9694e..0e9b10b68 100644 --- a/src/impl/btc/stratum/work_source.cpp +++ b/src/impl/btc/stratum/work_source.cpp @@ -808,14 +808,24 @@ nlohmann::json BTCWorkSource::mining_submit( block_bytes.insert(block_bytes.end(), tx_bytes.begin(), tx_bytes.end()); } + // A won block is a full subsidy: it must NEVER be silently dropped. + // submit_block_fn_ relays via P2P (primary) and falls back to the + // submitblock RPC; it returns true iff the block reached at least one + // sink. If it reaches neither (false / throw / no fn wired) we scream. + bool reached_network = false; if (submit_block_fn_) { try { - submit_block_fn_(block_bytes, height); + reached_network = submit_block_fn_(block_bytes, height); } catch (const std::exception& e) { - LOG_WARNING << "[BTC-STRATUM-BLOCK] submit_block_fn threw: " << e.what(); + LOG_ERROR << "[BTC-STRATUM-BLOCK] submit_block_fn threw: " << e.what(); + } + if (!reached_network) { + LOG_ERROR << "[BTC-STRATUM-BLOCK] WON BLOCK height=" << height + << " reached NEITHER P2P nor RPC — lost subsidy!"; } } else { - LOG_WARNING << "[BTC-STRATUM-BLOCK] no submit_block_fn wired — block not broadcast"; + LOG_ERROR << "[BTC-STRATUM-BLOCK] no submit_block_fn wired — WON BLOCK height=" + << height << " not broadcast — lost subsidy!"; } // Update worker stats (block-find counts as accepted) diff --git a/src/impl/btc/stratum/work_source.hpp b/src/impl/btc/stratum/work_source.hpp index ee55503d0..5920f4305 100644 --- a/src/impl/btc/stratum/work_source.hpp +++ b/src/impl/btc/stratum/work_source.hpp @@ -60,7 +60,10 @@ class BTCWorkSource : public core::stratum::IWorkSource /// to a lambda that calls `coin_node.get_p2p()->submit_block_raw(bytes)` /// + adds to the B5 pending_submits map. Raw-bytes form keeps /// BTCWorkSource decoupled from the BlockType serialization details. - using SubmitBlockFn = std::function& block_bytes, + /// Returns true iff the won block reached at least one network sink + /// (P2P relay or submitblock RPC fallback). A false return means it + /// reached NEITHER and the won-block path logs a loud error. + using SubmitBlockFn = std::function& block_bytes, uint32_t height)>; /// PPLNS payout query: walks back N shares from prev_share_hash and diff --git a/src/impl/btc/test/block_broadcast_test.cpp b/src/impl/btc/test/block_broadcast_test.cpp new file mode 100644 index 000000000..1b615f3c8 --- /dev/null +++ b/src/impl/btc/test/block_broadcast_test.cpp @@ -0,0 +1,102 @@ +// Broadcaster-gate verify test (path 3): proves the WON-BLOCK broadcast obeys +// FALLBACK semantics and NEVER silent-drops. Standalone harness in the style +// of tx_data_memo_test.cpp -- no btc/test CMake plumbing required. +// +// Build (from repo root): +// g++ -std=gnu++20 -I src -I src/btclibs \ +// src/impl/btc/test/block_broadcast_test.cpp -o /tmp/block_broadcast_test +// Run: +// /tmp/block_broadcast_test +// +// Maps to the integrator gate spec: +// relay_p2p sink <-> btc::coin::Node::submit_block_p2p_raw -> NodeP2P:: +// submit_block_raw -> message_block::make_raw (p2p_node.hpp) +// submit_rpc sink <-> btc::coin::Node::submit_block_hex -> NodeRPC:: +// submit_block_hex -> submitblock CallMethod (rpc.cpp:393) +// +// The seam (block_broadcast.hpp) is the new wiring under test; make_raw / +// CallMethod themselves are already-exercised primitives. The fakes record +// invocation so we can assert exactly WHICH sink fired in each scenario. +// +// Invariants proven: +// (1) happy path (P2P up) -> P2P fires, RPC does NOT (fallback, not +// always-both / no double-broadcast) +// (2) degraded (P2P null/fail) -> RPC fires (fallback engages) +// (3) both down -> reaches NEITHER -> won-block path screams +// (4) the won-block decision (work_source.cpp) flips "reached_network" off +// exactly when both sinks fail, so a found block can never be lost quietly + +#include + +#include + +using btc::coin::broadcast_block_with_fallback; + +static int g_fails = 0; +static void check(bool ok, const char* label) { + std::printf(" %s %s\n", ok ? "PASS" : "FAIL", label); + if (!ok) ++g_fails; +} + +int main() { + std::printf("== broadcaster gate: FALLBACK + never-silent-drop ==\n"); + + // (1) Happy path: P2P relay succeeds. RPC must NOT fire. + { + bool p2p_called = false, rpc_called = false; + bool reached = broadcast_block_with_fallback( + [&]{ p2p_called = true; return true; }, + [&]{ rpc_called = true; return true; }); + check(reached, "happy: block reached network"); + check(p2p_called, "happy: P2P (primary) fired"); + check(!rpc_called, "happy: RPC did NOT fire (no double-broadcast)"); + } + + // (2) Degraded: P2P unavailable/failed -> RPC fallback engages. + { + bool p2p_called = false, rpc_called = false; + bool reached = broadcast_block_with_fallback( + [&]{ p2p_called = true; return false; }, // relay failed / no peer + [&]{ rpc_called = true; return true; }); // bitcoind accepts + check(reached, "degraded: block reached network"); + check(p2p_called, "degraded: P2P attempted first"); + check(rpc_called, "degraded: RPC fallback fired"); + } + + // (2b) Degraded by null P2P sink (m_p2p == null -> empty std::function). + { + bool rpc_called = false; + std::function no_p2p; // empty: models m_p2p == nullptr + bool reached = broadcast_block_with_fallback( + no_p2p, + [&]{ rpc_called = true; return true; }); + check(reached, "null-p2p: block reached network"); + check(rpc_called, "null-p2p: RPC fallback fired"); + } + + // (3) Both sinks down -> reaches NEITHER -> caller MUST scream. + { + bool p2p_called = false, rpc_called = false; + bool reached = broadcast_block_with_fallback( + [&]{ p2p_called = true; return false; }, + [&]{ rpc_called = true; return false; }); + check(!reached, "both-down: reached NEITHER sink"); + check(p2p_called, "both-down: P2P attempted"); + check(rpc_called, "both-down: RPC fallback attempted"); + + // Mirror the won-block decision in work_source.cpp: a found block with + // reached_network==false triggers the loud error (never silent-drop). + bool would_scream = !reached; + check(would_scream, "both-down: won-block path screams (no silent drop)"); + } + + // (3b) No fn wired at all is handled at the work_source layer (empty both) + { + std::function none; + bool reached = broadcast_block_with_fallback(none, none); + check(!reached, "no-sinks: reached NEITHER (work_source screams)"); + } + + std::printf(g_fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", g_fails); + return g_fails ? 1 : 0; +}