|
| 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 |
0 commit comments