Skip to content

Commit 35693c5

Browse files
authored
Merge pull request #465 from frstrtr/dash-broadcaster-keystone-clean
dash(s8): broadcaster_full won-block keystone — dual-arm relay (#83) [fresh base, supersedes #456]
2 parents c67e7b9 + 1334f73 commit 35693c5

4 files changed

Lines changed: 502 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
test_threading test_weights \
7373
test_header_chain test_mempool test_template_builder \
7474
test_doge_chain test_compact_blocks test_dash_x11_kat \
75-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
75+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
7676
test_multiaddress_pplns test_pplns_stress \
7777
test_hash_link test_decay_pplns \
7878
test_pplns_consensus \
@@ -205,7 +205,7 @@ jobs:
205205
test_threading test_weights \
206206
test_header_chain test_mempool test_template_builder \
207207
test_doge_chain test_compact_blocks test_dash_x11_kat \
208-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
208+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer \
209209
test_hash_link test_decay_pplns \
210210
test_pplns_consensus \
211211
test_v36_script_sorting test_v36_cross_impl_refhash \

src/impl/dash/broadcaster_full.hpp

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#pragma once
2+
3+
// Phase S8 — DashBroadcasterFull: the won-block KEYSTONE (dual-path #83).
4+
//
5+
// One rung above the DashBroadcaster peer-pool LEAF (broadcaster.hpp, #405):
6+
//
7+
// p2p_connection -> p2p_node -> broadcaster -> [broadcaster_full]
8+
//
9+
// The leaf is a PURE peer-pool + discovery + fan-out *scaffold* whose
10+
// submit_block_raw_all() only iterates the LIVE slots invoking an injectable
11+
// no-op hook. This keystone closes the loop: it routes a WON block to the
12+
// network over BOTH arms of the dual-path gate every other coin already passes:
13+
//
14+
// ARM A — EMBEDDED P2P FAN-OUT
15+
// on_block_found() drives the leaf's submit_block_raw_all(), whose fan-out
16+
// hook is wired here to the per-slot submit seam (m_peer_submit). The block
17+
// is pushed in parallel to every LIVE dashd P2P slot for low-latency
18+
// propagation below what a single submitblock can reach.
19+
//
20+
// ARM B — submitblock RPC FALLBACK (dashd authoritative; NEVER removed)
21+
// on_block_found() also hands the packed block to the injected submitblock
22+
// RPC callback (m_rpc_submit), so the block reaches the network through the
23+
// local dashd even when the embedded peer-pool is empty/cold. This arm is
24+
// the standing safety net per the dashd-RPC-fallback invariant: it is
25+
// attempted whenever configured, independent of the embedded arm.
26+
//
27+
// reached_network() is TRUE iff at least one arm placed the block on the wire
28+
// (>=1 embedded peer reached OR the RPC submit succeeded). A won block with
29+
// zero live peers AND no RPC arm returns reached_network()==false — the caller
30+
// MUST treat that as "block not relayed" rather than silently dropping it.
31+
//
32+
// 3-WAY RECONCILE NOTE (honest): the dash-spv-embedded reference
33+
// broadcaster_full.hpp ports c2pool-ltc's CoinBroadcaster verbatim and depends
34+
// on c2pool/merged/coin_peer_manager.hpp, coin/node_interface.hpp,
35+
// coin/transaction.hpp, coin/block.hpp and NodeP2P::submit_block — NONE of
36+
// which are on master. A wholesale copy would (1) fail to compile, (2) drag in
37+
// the shared merged/ tree, breaking the single-coin DASH isolation fence, and
38+
// (3) regress the newer master broadcaster.hpp(#405)/p2p_node/config. So this
39+
// keystone is reconciled against what master ACTUALLY carries: it composes the
40+
// #405 DashBroadcaster leaf's FanOutHook/submit_block_raw_all seam (embedded
41+
// arm) and an injectable submitblock-RPC callback (dashd arm). The concrete
42+
// per-slot NodeP2P::submit_block_raw and the live getpeerinfo RPC tick land
43+
// when those node methods exist on master; the seams here are exactly where
44+
// they bolt in. Header-only, single dash tree, NO src/core / NO merged/ adds.
45+
46+
#include "broadcaster.hpp"
47+
#include "config.hpp"
48+
49+
#include <cstdint>
50+
#include <exception>
51+
#include <functional>
52+
#include <span>
53+
#include <string>
54+
55+
#include <core/log.hpp>
56+
57+
namespace dash
58+
{
59+
60+
// Won-block keystone: wraps the DashBroadcaster peer-pool and routes a found
61+
// block to the network over the embedded-P2P and submitblock-RPC arms.
62+
class DashBroadcasterFull
63+
{
64+
public:
65+
// Per-slot block submit (embedded arm). Pushes the packed block to ONE live
66+
// dashd P2P slot. Defaults to a no-op: the concrete NodeP2P::submit_block_raw
67+
// lands on master separately; this seam is where it bolts in. Tests inject a
68+
// capturing fn to prove each live peer received the exact bytes.
69+
using PeerSubmitFn =
70+
std::function<void(dash::coin::p2p::NodeP2P& /*slot*/,
71+
std::span<const unsigned char> /*block_bytes*/)>;
72+
73+
// submitblock RPC fallback (dashd arm). Returns true iff dashd accepted the
74+
// block. Defaults unset; when unset the RPC arm is simply not attempted (the
75+
// dashd-RPC fallback is opt-in by wiring, never removed). main_dash wires
76+
// this to the NodeRPC submitblock call.
77+
using RpcSubmitFn = std::function<bool(const std::string& /*block_hex*/)>;
78+
79+
// Outcome of a won-block relay attempt across both arms.
80+
struct Outcome
81+
{
82+
size_t peers_reached{0}; // live embedded P2P slots the block fanned to
83+
bool rpc_attempted{false};
84+
bool rpc_submitted{false}; // dashd accepted via submitblock
85+
86+
// The gate predicate: did the won block actually reach the network on
87+
// AT LEAST ONE arm?
88+
bool reached_network() const { return peers_reached > 0 || rpc_submitted; }
89+
};
90+
91+
explicit DashBroadcasterFull(DashBroadcaster* pool)
92+
: m_pool(pool)
93+
, m_peer_submit(
94+
[](dash::coin::p2p::NodeP2P&, std::span<const unsigned char>) {})
95+
{
96+
// Wire the leaf's fan-out hook to OUR per-slot submit seam so that
97+
// submit_block_raw_all() actually pushes bytes to live peers instead of
98+
// running the leaf's default no-op.
99+
if (m_pool)
100+
m_pool->set_fan_out_hook(
101+
[this](dash::coin::p2p::NodeP2P& slot,
102+
std::span<const unsigned char> bytes) {
103+
m_peer_submit(slot, bytes);
104+
});
105+
}
106+
107+
// ── injection seams (main_dash / tests) ──────────────────────────────
108+
109+
void set_peer_submit(PeerSubmitFn f)
110+
{
111+
m_peer_submit = std::move(f);
112+
// Re-bind the leaf hook so it forwards to the updated submit fn.
113+
if (m_pool)
114+
m_pool->set_fan_out_hook(
115+
[this](dash::coin::p2p::NodeP2P& slot,
116+
std::span<const unsigned char> bytes) {
117+
m_peer_submit(slot, bytes);
118+
});
119+
}
120+
121+
void set_rpc_submit(RpcSubmitFn f) { m_rpc_submit = std::move(f); }
122+
123+
bool has_rpc_arm() const { return static_cast<bool>(m_rpc_submit); }
124+
125+
// ── the won-block path ────────────────────────────────────────────────
126+
127+
// Relay a WON block (already packed to wire bytes by submit_validator) to
128+
// the network over BOTH arms:
129+
// ARM A: fan out to every live embedded P2P peer (parallel propagation).
130+
// ARM B: hand to dashd via submitblock RPC (authoritative; if wired).
131+
// Both arms are attempted independently — the RPC fallback fires whenever it
132+
// is configured, NOT only when the peer-pool is empty, so a cold pool can
133+
// never silence the authoritative dashd path. Returns the per-arm Outcome.
134+
Outcome on_block_found(std::span<const unsigned char> block_bytes)
135+
{
136+
Outcome out;
137+
138+
// ARM A — embedded P2P fan-out via the leaf scaffold. GUARDED: a
139+
// throwing per-slot relay must NOT prevent the ARM B submitblock RPC
140+
// fallback below — otherwise a P2P-leg fault silently drops a won block
141+
// AND skips its safety net. peers_reached stays 0 unless the fan-out
142+
// returns cleanly. (Mirror of NMC #468 / DGB #469 / BCH #471.)
143+
if (m_pool) {
144+
try {
145+
out.peers_reached = m_pool->submit_block_raw_all(block_bytes);
146+
} catch (const std::exception& e) {
147+
LOG_ERROR << "[DashBroadcastFull] ARM A (embedded P2P fan-out) threw ("
148+
<< e.what() << ") — falling through to submitblock RPC fallback.";
149+
} catch (...) {
150+
LOG_ERROR << "[DashBroadcastFull] ARM A (embedded P2P fan-out) threw "
151+
"(non-std) — falling through to submitblock RPC fallback.";
152+
}
153+
}
154+
155+
// ARM B — submitblock RPC fallback (dashd authoritative). GUARDED: a
156+
// throwing RPC sink must not propagate and must not mask an ARM A win
157+
// already recorded — rpc_submitted stays false, peers_reached stands.
158+
if (m_rpc_submit) {
159+
out.rpc_attempted = true;
160+
try {
161+
out.rpc_submitted = m_rpc_submit(to_hex(block_bytes));
162+
} catch (const std::exception& e) {
163+
LOG_ERROR << "[DashBroadcastFull] ARM B (submitblock RPC) threw ("
164+
<< e.what() << ") — treated as no-ack, ARM A win (if any) preserved.";
165+
} catch (...) {
166+
LOG_ERROR << "[DashBroadcastFull] ARM B (submitblock RPC) threw "
167+
"(non-std) — treated as no-ack.";
168+
}
169+
}
170+
171+
if (out.reached_network())
172+
LOG_INFO << "[DashBroadcastFull] won block relayed: peers="
173+
<< out.peers_reached
174+
<< " rpc=" << (out.rpc_submitted ? "ok" : "no");
175+
else
176+
LOG_WARNING << "[DashBroadcastFull] won block NOT relayed — "
177+
"no live peers and no RPC arm! block bytes="
178+
<< block_bytes.size();
179+
return out;
180+
}
181+
182+
// Lowercase hex of the packed block, as dashd submitblock expects.
183+
static std::string to_hex(std::span<const unsigned char> bytes)
184+
{
185+
static const char* k = "0123456789abcdef";
186+
std::string s;
187+
s.reserve(bytes.size() * 2);
188+
for (unsigned char b : bytes) {
189+
s.push_back(k[b >> 4]);
190+
s.push_back(k[b & 0x0f]);
191+
}
192+
return s;
193+
}
194+
195+
DashBroadcaster* pool() const { return m_pool; }
196+
197+
private:
198+
DashBroadcaster* m_pool{};
199+
PeerSubmitFn m_peer_submit;
200+
RpcSubmitFn m_rpc_submit;
201+
};
202+
203+
} // namespace dash

test/CMakeLists.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,16 @@ if (BUILD_TESTING AND GTest_FOUND)
478478
nlohmann_json::nlohmann_json)
479479
gtest_add_tests(test_dash_rpc_request "" AUTO)
480480

481+
add_executable(test_dash_broadcaster_full test_dash_broadcaster_full.cpp)
482+
target_link_libraries(test_dash_broadcaster_full PRIVATE
483+
GTest::gtest_main GTest::gtest
484+
dash_x11 core
485+
nlohmann_json::nlohmann_json
486+
${Boost_LIBRARIES}
487+
)
488+
target_link_libraries(test_dash_broadcaster_full PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39)
489+
gtest_add_tests(test_dash_broadcaster_full "" AUTO)
490+
481491
add_executable(test_dash_block_relay test_dash_block_relay.cpp)
482492
target_link_libraries(test_dash_block_relay PRIVATE
483493
GTest::gtest_main GTest::gtest

0 commit comments

Comments
 (0)