Skip to content

Commit 02a9571

Browse files
authored
Merge pull request #162 from frstrtr/btc/broadcaster-gate-fallback
btc: broadcaster-gate FILL — submitblock RPC FALLBACK + never-silent-drop
2 parents 7364408 + 1810621 commit 02a9571

7 files changed

Lines changed: 198 additions & 16 deletions

File tree

src/c2pool/main_btc.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -747,14 +747,14 @@ int main(int argc, char* argv[])
747747
// captures by reference so it reuses the existing B5 infrastructure
748748
// instead of duplicating it.
749749
auto stratum_submit_fn = [&coin_node, pending_mu, pending_submits]
750-
(const std::vector<unsigned char>& block_bytes, uint32_t height)
750+
(const std::vector<unsigned char>& block_bytes, uint32_t height) -> bool
751751
{
752752
// Compute block_hash for pending_submits tracking. BTC block_hash =
753753
// SHA256d of the first 80 bytes (the header).
754754
if (block_bytes.size() < 80) {
755755
LOG_WARNING << "[BTC-STRATUM-BLOCK] block bytes too short ("
756756
<< block_bytes.size() << " < 80) — not submitting";
757-
return;
757+
return false;
758758
}
759759
uint256 block_hash = Hash(std::span<const unsigned char>(block_bytes.data(), 80));
760760
{
@@ -765,7 +765,8 @@ int main(int argc, char* argv[])
765765
}
766766
LOG_INFO << "[BTC-SUBMIT] sending block " << block_hash.GetHex().substr(0, 16)
767767
<< " height=" << height << " (via stratum)";
768-
coin_node.submit_block_p2p_raw(block_bytes);
768+
// FALLBACK: P2P relay primary, submitblock RPC if P2P unavailable/failed.
769+
return coin_node.submit_block_with_fallback(block_bytes);
769770
};
770771

771772
// Construct the work source. Holds non-owning refs to chain + mempool;
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#pragma once
2+
3+
#include <functional>
4+
5+
namespace btc
6+
{
7+
namespace coin
8+
{
9+
10+
// FALLBACK broadcast orchestration for a WON block.
11+
//
12+
// P2P relay is PRIMARY; the submitblock RPC is the FALLBACK and fires ONLY
13+
// when P2P is unavailable or the relay did not succeed. This is deliberately
14+
// NOT always-both / double-broadcast: bitcoind dedupes a duplicate submitblock
15+
// harmlessly, but the fallback gives us robustness against a silent
16+
// P2P-unavailable / relay-failed hole without an unconditional double submit.
17+
//
18+
// `relay_p2p` -> returns true on a successful P2P relay.
19+
// `submit_rpc` -> returns true when bitcoind accepts the block.
20+
//
21+
// Returns true iff the block reached AT LEAST ONE sink. A false return means
22+
// the block reached NEITHER sink: the caller MUST treat that as a lost
23+
// subsidy and scream (never silent-drop a won block).
24+
inline bool broadcast_block_with_fallback(
25+
const std::function<bool()>& relay_p2p,
26+
const std::function<bool()>& submit_rpc)
27+
{
28+
if (relay_p2p && relay_p2p())
29+
return true; // primary succeeded -> no double-broadcast
30+
if (submit_rpc)
31+
return submit_rpc(); // fallback path
32+
return false; // reached neither sink
33+
}
34+
35+
} // namespace coin
36+
} // namespace btc

src/impl/btc/coin/node.hpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44

55
#include <boost/asio.hpp>
66

7+
#include <btclibs/util/strencodings.h> // HexStr
8+
79
#include "rpc.hpp"
810
#include "p2p_node.hpp"
911
#include "node_interface.hpp"
12+
#include "block_broadcast.hpp"
1013

1114
namespace btc
1215
{
@@ -91,10 +94,35 @@ class Node : public btc::interfaces::Node
9194
/// source which already has the full block bytes assembled from
9295
/// (header || tx_count || coinbase || tx_data) and doesn't need to
9396
/// round-trip through BlockType deserialization.
94-
void submit_block_p2p_raw(const std::vector<unsigned char>& block_bytes)
97+
/// Returns true iff the block was relayed to a connected peer.
98+
bool submit_block_p2p_raw(const std::vector<unsigned char>& block_bytes)
9599
{
96100
if (m_p2p)
97-
m_p2p->submit_block_raw(block_bytes);
101+
return m_p2p->submit_block_raw(block_bytes);
102+
return false;
103+
}
104+
105+
/// Submit a pre-serialized block to bitcoind via the submitblock RPC.
106+
/// This is the FALLBACK sink (P2P relay is primary). Returns true iff
107+
/// the daemon accepted the block. ignore_failure is set so a rejection
108+
/// is logged but does not throw out of the won-block path.
109+
bool submit_block_hex(const std::vector<unsigned char>& block_bytes)
110+
{
111+
if (!m_rpc)
112+
return false;
113+
return m_rpc->submit_block_hex(HexStr(block_bytes), /*ignore_failure=*/true);
114+
}
115+
116+
/// Broadcast a WON block with FALLBACK semantics: P2P relay is primary,
117+
/// the submitblock RPC fires only if P2P is unavailable or the relay did
118+
/// not succeed (NOT always-both). Returns true iff the block reached at
119+
/// least one sink; a false return means it reached NEITHER and the caller
120+
/// MUST treat it as a lost subsidy.
121+
bool submit_block_with_fallback(const std::vector<unsigned char>& block_bytes)
122+
{
123+
return broadcast_block_with_fallback(
124+
[&]{ return submit_block_p2p_raw(block_bytes); },
125+
[&]{ return submit_block_hex(block_bytes); });
98126
}
99127

100128
bool has_p2p() const { return m_p2p != nullptr; }

src/impl/btc/coin/p2p_node.hpp

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -345,9 +345,9 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
345345
/// Relay a pre-serialized block via P2P.
346346
/// Uses compact block format (BIP 152 v2) for peers that support it,
347347
/// falling back to full block otherwise.
348-
void submit_block_raw(const std::vector<unsigned char>& block_bytes)
348+
bool submit_block_raw(const std::vector<unsigned char>& block_bytes)
349349
{
350-
if (!m_peer) return;
350+
if (!m_peer) return false;
351351

352352
if (m_peer_supports_cmpct && m_peer_cmpct_version >= 2) {
353353
// Deserialize the block to build a compact representation
@@ -371,7 +371,7 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
371371
<< blockhash.GetHex()
372372
<< " (" << cb.short_ids.size() << " short IDs, "
373373
<< cb.prefilled_txns.size() << " prefilled)";
374-
return;
374+
return true;
375375
} catch (const std::exception& e) {
376376
LOG_WARNING << "[" << m_chain_label
377377
<< "] Compact block build failed (block_size=" << block_bytes.size()
@@ -385,18 +385,20 @@ class NodeP2P : public core::ICommunicator, public core::INetwork, public core::
385385
}
386386

387387
// Fallback: send full block
388-
submit_block_full(block_bytes);
388+
return submit_block_full(block_bytes);
389389
}
390390

391-
/// Send a full block message (legacy relay).
392-
void submit_block_full(const std::vector<unsigned char>& block_bytes)
391+
/// Send a full block message (legacy relay). Returns true iff a peer
392+
/// was connected and the block bytes were written to it.
393+
bool submit_block_full(const std::vector<unsigned char>& block_bytes)
393394
{
394-
if (!m_peer) return;
395+
if (!m_peer) return false;
395396
PackStream ps(block_bytes);
396397
auto rmsg = std::make_unique<RawMessage>("block", std::move(ps));
397398
m_peer->write(rmsg);
398399
LOG_INFO << "[" << m_chain_label << "] Sent full block message ("
399400
<< block_bytes.size() << " bytes) to " << m_target_addr.to_string();
401+
return true;
400402
}
401403

402404
//[x][x][x] void handle_message_version(std::shared_ptr<coind::messages::message_version> msg, CoindProtocol* protocol); //

src/impl/btc/stratum/work_source.cpp

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -808,14 +808,24 @@ nlohmann::json BTCWorkSource::mining_submit(
808808
block_bytes.insert(block_bytes.end(), tx_bytes.begin(), tx_bytes.end());
809809
}
810810

811+
// A won block is a full subsidy: it must NEVER be silently dropped.
812+
// submit_block_fn_ relays via P2P (primary) and falls back to the
813+
// submitblock RPC; it returns true iff the block reached at least one
814+
// sink. If it reaches neither (false / throw / no fn wired) we scream.
815+
bool reached_network = false;
811816
if (submit_block_fn_) {
812817
try {
813-
submit_block_fn_(block_bytes, height);
818+
reached_network = submit_block_fn_(block_bytes, height);
814819
} catch (const std::exception& e) {
815-
LOG_WARNING << "[BTC-STRATUM-BLOCK] submit_block_fn threw: " << e.what();
820+
LOG_ERROR << "[BTC-STRATUM-BLOCK] submit_block_fn threw: " << e.what();
821+
}
822+
if (!reached_network) {
823+
LOG_ERROR << "[BTC-STRATUM-BLOCK] WON BLOCK height=" << height
824+
<< " reached NEITHER P2P nor RPC — lost subsidy!";
816825
}
817826
} else {
818-
LOG_WARNING << "[BTC-STRATUM-BLOCK] no submit_block_fn wired — block not broadcast";
827+
LOG_ERROR << "[BTC-STRATUM-BLOCK] no submit_block_fn wired — WON BLOCK height="
828+
<< height << " not broadcast — lost subsidy!";
819829
}
820830

821831
// Update worker stats (block-find counts as accepted)

src/impl/btc/stratum/work_source.hpp

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,10 @@ class BTCWorkSource : public core::stratum::IWorkSource
6060
/// to a lambda that calls `coin_node.get_p2p()->submit_block_raw(bytes)`
6161
/// + adds to the B5 pending_submits map. Raw-bytes form keeps
6262
/// BTCWorkSource decoupled from the BlockType serialization details.
63-
using SubmitBlockFn = std::function<void(const std::vector<unsigned char>& block_bytes,
63+
/// Returns true iff the won block reached at least one network sink
64+
/// (P2P relay or submitblock RPC fallback). A false return means it
65+
/// reached NEITHER and the won-block path logs a loud error.
66+
using SubmitBlockFn = std::function<bool(const std::vector<unsigned char>& block_bytes,
6467
uint32_t height)>;
6568

6669
/// PPLNS payout query: walks back N shares from prev_share_hash and
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Broadcaster-gate verify test (path 3): proves the WON-BLOCK broadcast obeys
2+
// FALLBACK semantics and NEVER silent-drops. Standalone harness in the style
3+
// of tx_data_memo_test.cpp -- no btc/test CMake plumbing required.
4+
//
5+
// Build (from repo root):
6+
// g++ -std=gnu++20 -I src -I src/btclibs \
7+
// src/impl/btc/test/block_broadcast_test.cpp -o /tmp/block_broadcast_test
8+
// Run:
9+
// /tmp/block_broadcast_test
10+
//
11+
// Maps to the integrator gate spec:
12+
// relay_p2p sink <-> btc::coin::Node::submit_block_p2p_raw -> NodeP2P::
13+
// submit_block_raw -> message_block::make_raw (p2p_node.hpp)
14+
// submit_rpc sink <-> btc::coin::Node::submit_block_hex -> NodeRPC::
15+
// submit_block_hex -> submitblock CallMethod (rpc.cpp:393)
16+
//
17+
// The seam (block_broadcast.hpp) is the new wiring under test; make_raw /
18+
// CallMethod themselves are already-exercised primitives. The fakes record
19+
// invocation so we can assert exactly WHICH sink fired in each scenario.
20+
//
21+
// Invariants proven:
22+
// (1) happy path (P2P up) -> P2P fires, RPC does NOT (fallback, not
23+
// always-both / no double-broadcast)
24+
// (2) degraded (P2P null/fail) -> RPC fires (fallback engages)
25+
// (3) both down -> reaches NEITHER -> won-block path screams
26+
// (4) the won-block decision (work_source.cpp) flips "reached_network" off
27+
// exactly when both sinks fail, so a found block can never be lost quietly
28+
29+
#include <impl/btc/coin/block_broadcast.hpp>
30+
31+
#include <cstdio>
32+
33+
using btc::coin::broadcast_block_with_fallback;
34+
35+
static int g_fails = 0;
36+
static void check(bool ok, const char* label) {
37+
std::printf(" %s %s\n", ok ? "PASS" : "FAIL", label);
38+
if (!ok) ++g_fails;
39+
}
40+
41+
int main() {
42+
std::printf("== broadcaster gate: FALLBACK + never-silent-drop ==\n");
43+
44+
// (1) Happy path: P2P relay succeeds. RPC must NOT fire.
45+
{
46+
bool p2p_called = false, rpc_called = false;
47+
bool reached = broadcast_block_with_fallback(
48+
[&]{ p2p_called = true; return true; },
49+
[&]{ rpc_called = true; return true; });
50+
check(reached, "happy: block reached network");
51+
check(p2p_called, "happy: P2P (primary) fired");
52+
check(!rpc_called, "happy: RPC did NOT fire (no double-broadcast)");
53+
}
54+
55+
// (2) Degraded: P2P unavailable/failed -> RPC fallback engages.
56+
{
57+
bool p2p_called = false, rpc_called = false;
58+
bool reached = broadcast_block_with_fallback(
59+
[&]{ p2p_called = true; return false; }, // relay failed / no peer
60+
[&]{ rpc_called = true; return true; }); // bitcoind accepts
61+
check(reached, "degraded: block reached network");
62+
check(p2p_called, "degraded: P2P attempted first");
63+
check(rpc_called, "degraded: RPC fallback fired");
64+
}
65+
66+
// (2b) Degraded by null P2P sink (m_p2p == null -> empty std::function).
67+
{
68+
bool rpc_called = false;
69+
std::function<bool()> no_p2p; // empty: models m_p2p == nullptr
70+
bool reached = broadcast_block_with_fallback(
71+
no_p2p,
72+
[&]{ rpc_called = true; return true; });
73+
check(reached, "null-p2p: block reached network");
74+
check(rpc_called, "null-p2p: RPC fallback fired");
75+
}
76+
77+
// (3) Both sinks down -> reaches NEITHER -> caller MUST scream.
78+
{
79+
bool p2p_called = false, rpc_called = false;
80+
bool reached = broadcast_block_with_fallback(
81+
[&]{ p2p_called = true; return false; },
82+
[&]{ rpc_called = true; return false; });
83+
check(!reached, "both-down: reached NEITHER sink");
84+
check(p2p_called, "both-down: P2P attempted");
85+
check(rpc_called, "both-down: RPC fallback attempted");
86+
87+
// Mirror the won-block decision in work_source.cpp: a found block with
88+
// reached_network==false triggers the loud error (never silent-drop).
89+
bool would_scream = !reached;
90+
check(would_scream, "both-down: won-block path screams (no silent drop)");
91+
}
92+
93+
// (3b) No fn wired at all is handled at the work_source layer (empty both)
94+
{
95+
std::function<bool()> none;
96+
bool reached = broadcast_block_with_fallback(none, none);
97+
check(!reached, "no-sinks: reached NEITHER (work_source screams)");
98+
}
99+
100+
std::printf(g_fails ? "\nFAILED (%d)\n" : "\nALL PASS\n", g_fails);
101+
return g_fails ? 1 : 0;
102+
}

0 commit comments

Comments
 (0)