Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/c2pool/main_btc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char>& block_bytes, uint32_t height)
(const std::vector<unsigned char>& 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<const unsigned char>(block_bytes.data(), 80));
{
Expand All @@ -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;
Expand Down
36 changes: 36 additions & 0 deletions src/impl/btc/coin/block_broadcast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#pragma once

#include <functional>

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<bool()>& relay_p2p,
const std::function<bool()>& 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
32 changes: 30 additions & 2 deletions src/impl/btc/coin/node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@

#include <boost/asio.hpp>

#include <btclibs/util/strencodings.h> // HexStr

#include "rpc.hpp"
#include "p2p_node.hpp"
#include "node_interface.hpp"
#include "block_broadcast.hpp"

namespace btc
{
Expand Down Expand Up @@ -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<unsigned char>& block_bytes)
/// Returns true iff the block was relayed to a connected peer.
bool submit_block_p2p_raw(const std::vector<unsigned char>& 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<unsigned char>& 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<unsigned char>& 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; }
Expand Down
16 changes: 9 additions & 7 deletions src/impl/btc/coin/p2p_node.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<unsigned char>& block_bytes)
bool submit_block_raw(const std::vector<unsigned char>& 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
Expand All @@ -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()
Expand All @@ -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<unsigned char>& 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<unsigned char>& block_bytes)
{
if (!m_peer) return;
if (!m_peer) return false;
PackStream ps(block_bytes);
auto rmsg = std::make_unique<RawMessage>("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<coind::messages::message_version> msg, CoindProtocol* protocol); //
Expand Down
16 changes: 13 additions & 3 deletions src/impl/btc/stratum/work_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/impl/btc/stratum/work_source.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<void(const std::vector<unsigned char>& 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<bool(const std::vector<unsigned char>& block_bytes,
uint32_t height)>;

/// PPLNS payout query: walks back N shares from prev_share_hash and
Expand Down
102 changes: 102 additions & 0 deletions src/impl/btc/test/block_broadcast_test.cpp
Original file line number Diff line number Diff line change
@@ -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 <impl/btc/coin/block_broadcast.hpp>

#include <cstdio>

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<bool()> 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<bool()> 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;
}
Loading