Skip to content

Commit a6be3db

Browse files
committed
btc: guard both broadcast_block_with_fallback legs vs throwing sink
A throwing relay_p2p() sink unwound past broadcast_block_with_fallback, bypassing the submitblock RPC fallback entirely and silently dropping a won block -- the exact hole the fallback exists to close, and a violation of the doc-comment contract that a won block is never silent-dropped. Guard both legs: a throwing primary relay is treated as relay-failed and falls through to the submitblock fallback; a throwing last-resort submit_rpc collapses to a definite false so the caller scream path fires instead of an exception escaping the won-block handler. Mirror of the NMC broadcast leg-guard fix (#468). 6 KATs ride the already-allowlisted btc_share_test exe (no build.yml allowlist change, no #137 NOT_BUILT risk).
1 parent add8751 commit a6be3db

3 files changed

Lines changed: 116 additions & 4 deletions

File tree

src/impl/btc/coin/block_broadcast.hpp

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,35 @@ namespace coin
2121
// Returns true iff the block reached AT LEAST ONE sink. A false return means
2222
// the block reached NEITHER sink: the caller MUST treat that as a lost
2323
// subsidy and scream (never silent-drop a won block).
24+
//
25+
// BOTH legs are guarded so a throwing sink can never bypass the fallback or
26+
// escape this function: a THROWING relay_p2p() is a relay-FAILED mode, not a
27+
// reason to skip the fallback -- without the guard the exception would unwind
28+
// PAST this function, bypassing the submitblock fallback and silently dropping
29+
// the won block (the exact hole the fallback exists to close). A throwing
30+
// submit_rpc() (last-resort sink) collapses to a definite false return so the
31+
// caller's documented "reached neither sink, scream" path fires instead of an
32+
// exception escaping the won-block handler.
2433
inline bool broadcast_block_with_fallback(
2534
const std::function<bool()>& relay_p2p,
2635
const std::function<bool()>& submit_rpc)
2736
{
28-
if (relay_p2p && relay_p2p())
37+
bool p2p_ok = false;
38+
try {
39+
p2p_ok = (relay_p2p && relay_p2p());
40+
} catch (...) {
41+
p2p_ok = false; // relay threw -> treat as relay-failed
42+
}
43+
if (p2p_ok)
2944
return true; // primary succeeded -> no double-broadcast
30-
if (submit_rpc)
31-
return submit_rpc(); // fallback path
45+
46+
if (submit_rpc) {
47+
try {
48+
return submit_rpc(); // fallback path
49+
} catch (...) {
50+
return false; // last-resort sink threw -> caller screams
51+
}
52+
}
3253
return false; // reached neither sink
3354
}
3455

src/impl/btc/test/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
if (BUILD_TESTING AND GTest_FOUND)
22
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
33
# collision with src/impl/ltc/test (both subdirs build in the same tree).
4-
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)
4+
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)
55
target_link_libraries(btc_share_test PRIVATE
66
GTest::gtest_main GTest::gtest
77
core btc
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// ---------------------------------------------------------------------------
2+
// btc::coin::broadcast_block_with_fallback dual-path guard KATs.
3+
//
4+
// Offline contract slice locking the won-block fallback orchestration the BTC
5+
// run-loop depends on: P2P relay is PRIMARY, submitblock RPC is the FALLBACK
6+
// (fired ONLY when P2P is unavailable / relay-failed -- deliberately NOT
7+
// always-both). The function's doc-comment promises a false return is the
8+
// ONLY "reached neither sink" signal and that a won block is NEVER
9+
// silent-dropped. A THROWING relay_p2p() sink violated that promise: the
10+
// exception unwound PAST the function, bypassing the submitblock fallback
11+
// entirely and silently dropping the won block -- the exact hole the fallback
12+
// exists to close. These KATs lock BOTH legs guarded (mirror of the NMC #468
13+
// leg-guard fix in nmc_block_broadcast_test.cpp).
14+
//
15+
// Rides the already-allowlisted btc_share_test executable, so no build.yml
16+
// --target allowlist change is needed and there is no #137-style NOT_BUILT
17+
// sentinel risk. p2pool-merged-v36 surface: NONE.
18+
// ---------------------------------------------------------------------------
19+
20+
#include <gtest/gtest.h>
21+
22+
#include <stdexcept>
23+
24+
#include "../coin/block_broadcast.hpp"
25+
26+
// 1) Primary succeeds -> true, and the submitblock fallback is NOT invoked
27+
// (the deliberate no-double-broadcast rule).
28+
TEST(BtcBlockBroadcastGuard, P2pSuccessSkipsFallback) {
29+
bool rpc_called = false;
30+
auto relay = [] { return true; };
31+
auto submit = [&] { rpc_called = true; return true; };
32+
33+
EXPECT_TRUE(btc::coin::broadcast_block_with_fallback(relay, submit));
34+
EXPECT_FALSE(rpc_called); // primary won -> no double submit
35+
}
36+
37+
// 2) Primary relay-fails (returns false) -> submitblock fallback fires.
38+
TEST(BtcBlockBroadcastGuard, P2pFalseFiresFallback) {
39+
int rpc_calls = 0;
40+
auto relay = [] { return false; };
41+
auto submit = [&] { ++rpc_calls; return true; };
42+
43+
EXPECT_TRUE(btc::coin::broadcast_block_with_fallback(relay, submit));
44+
EXPECT_EQ(rpc_calls, 1);
45+
}
46+
47+
// 3) PRIMARY-LEG GUARD (the fix): a THROWING relay sink is a relay-failed mode,
48+
// not a reason to skip the fallback. The submitblock RPC STILL fires, the
49+
// won block reaches the node, and the dispatcher does NOT propagate the
50+
// throw. Before the guard this exception unwound past the function and the
51+
// block was silently dropped.
52+
TEST(BtcBlockBroadcastGuard, ThrowingP2pStillFiresFallback) {
53+
int rpc_calls = 0;
54+
auto relay = []() -> bool { throw std::runtime_error("p2p down mid-relay"); };
55+
auto submit = [&] { ++rpc_calls; return true; };
56+
57+
bool ok = false;
58+
EXPECT_NO_THROW(ok = btc::coin::broadcast_block_with_fallback(relay, submit));
59+
EXPECT_TRUE(ok); // fallback carried the won block
60+
EXPECT_EQ(rpc_calls, 1); // fallback fired despite the throw
61+
}
62+
63+
// 4) Throwing primary with NO fallback sink -> definite false (reached neither
64+
// sink, the caller's documented "scream / lost-subsidy" path), never a throw.
65+
TEST(BtcBlockBroadcastGuard, ThrowingP2pNoFallbackReturnsFalse) {
66+
auto relay = []() -> bool { throw std::runtime_error("p2p down"); };
67+
68+
bool ok = true;
69+
EXPECT_NO_THROW(ok = btc::coin::broadcast_block_with_fallback(relay, /*submit_rpc=*/{}));
70+
EXPECT_FALSE(ok);
71+
}
72+
73+
// 5) FALLBACK-LEG GUARD: when submitblock itself throws there is no further
74+
// sink, so the throw collapses to a definite false return -> the caller's
75+
// "reached neither sink, scream" contract fires instead of an exception
76+
// escaping the won-block handler.
77+
TEST(BtcBlockBroadcastGuard, ThrowingFallbackReturnsFalse) {
78+
auto relay = [] { return false; };
79+
auto submit = []() -> bool { throw std::runtime_error("submitblock rpc threw"); };
80+
81+
bool ok = true;
82+
EXPECT_NO_THROW(ok = btc::coin::broadcast_block_with_fallback(relay, submit));
83+
EXPECT_FALSE(ok);
84+
}
85+
86+
// 6) Neither sink live -> safe false no-op (no throw, no silent-drop).
87+
TEST(BtcBlockBroadcastGuard, NoSinkIsSafeFalse) {
88+
bool ok = true;
89+
EXPECT_NO_THROW(ok = btc::coin::broadcast_block_with_fallback({}, {}));
90+
EXPECT_FALSE(ok);
91+
}

0 commit comments

Comments
 (0)