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
115 changes: 115 additions & 0 deletions src/impl/btc/coin/won_block_dispatch.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once
// ---------------------------------------------------------------------------
// btc::coin::make_on_block_found -- the won-block DISPATCH handler the run-loop
// installs as btc::ShareTracker::m_on_block_found (gate #744).
//
// BTC exposes ShareTracker::m_on_block_found and fires it (share_tracker.hpp
// L385 scan path, L540 live-verify path) the moment a verified sharechain share
// crosses the BTC network target -- i.e. a block was WON by ANY peer's share,
// not just a local stratum submit. But main_btc.cpp never ASSIGNED that member,
// so a peer-relayed won share was detected and then SILENTLY DROPPED: the full
// subsidy lost. (The stratum submit path already dual-broadcasts via
// coin_node.submit_block_for_connect; this closes the OTHER seam -- the
// sharechain-detected won block.)
//
// This handler turns the winning share hash into a fully serialized parent
// block (via an injected WonBlockReconstructor -- the share->block "as_block"
// step) and dispatches it down the CONNECT-AUTHORITATIVE dual-path via
// broadcast_block_for_connect (P2P relay best-effort + submitblock RPC ALWAYS,
// because a cmpctblock announce alone does not ConnectBlock the tip -- the same
// policy the stratum won-block path already uses, block_broadcast.hpp).
//
// The reconstruct step is injected as a std::function rather than called inline
// so this handler is build-verifiable and run-tested NOW, before the faithful
// share->block reassembly (gentx + ref/merkle link + other-tx lookup, mirroring
// p2pool data.py Share.as_block, DGB #174/#176) is ported for BTC. The run-loop
// binds the real reconstructor + the live P2P relay sink + the submitblock RPC
// when they land; until then an injected reconstructor already carries a won
// block onto the network. Per-coin isolation: src/impl/btc/coin/ only.
//
// Contract:
// * reconstruct(share_hash) -> nullopt => UNKNOWN/unassemblable share: log a
// warning and broadcast NOTHING (never fabricate or relay a partial block).
// * reconstruct(share_hash) -> {bytes,hex} => fire broadcast_block_for_connect,
// whose own dual-path guards decide which sink(s) carry it; a both-legs-fail
// return is logged as a LOST SUBSIDY (never a silent success).
// ---------------------------------------------------------------------------

#include <functional>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include <core/log.hpp>
#include <core/uint256.hpp>

#include "block_broadcast.hpp" // btc::coin::broadcast_block_for_connect

namespace btc
{
namespace coin
{

// Reconstructs the full serialized parent block for a won share.
// share_hash -> {block_bytes, block_hex}, or nullopt if the share is unknown
// or cannot be assembled (e.g. a referenced tx is missing). block_bytes is the
// pre-serialized blob the P2P relay sends; block_hex is the same block for the
// submitblock RPC. Mirrors p2pool data.py Share.as_block; the faithful body
// lands in a follow-up slice.
using WonBlockReconstructor =
std::function<std::optional<std::pair<std::vector<unsigned char>, std::string>>(const uint256&)>;

// Embedded P2P relay sink: sends the pre-serialized block bytes. Returns true
// iff the relay accepted/sent. May be empty (no embedded sink wired yet).
using P2pRelaySink = std::function<bool(const std::vector<unsigned char>&)>;

// submitblock RPC sink: connect-authoritative full-block submission. Returns
// true iff the daemon accepted. May be empty (RPC-less).
using SubmitRpcSink = std::function<bool(const std::string&)>;

// Build the m_on_block_found handler. The run-loop assigns the returned closure
// to tracker.m_on_block_found; `relay_p2p` may be empty (no embedded sink yet)
// and `submit_rpc` may be empty -- broadcast_block_for_connect guards each leg.
inline std::function<void(const uint256&)>
make_on_block_found(WonBlockReconstructor reconstruct,
P2pRelaySink relay_p2p,
SubmitRpcSink submit_rpc)
{
return [reconstruct = std::move(reconstruct),
relay_p2p = std::move(relay_p2p),
submit_rpc = std::move(submit_rpc)](const uint256& share_hash)
{
if (!reconstruct) {
LOG_ERROR << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16)
<< " -- no reconstructor wired; cannot broadcast.";
return;
}

auto blk = reconstruct(share_hash);
if (!blk) {
LOG_WARNING << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16)
<< " could not be reconstructed -- NOT broadcast.";
return;
}

LOG_INFO << "[EMB-BTC] GOT BLOCK! share=" << share_hash.GetHex().substr(0, 16)
<< " reconstructed " << blk->first.size()
<< " bytes -- dispatching connect-authoritative dual-path.";

const std::vector<unsigned char>& bytes = blk->first;
const std::string& hex = blk->second;
bool reached = broadcast_block_for_connect(
[&]() -> bool { return relay_p2p ? relay_p2p(bytes) : false; },
[&]() -> bool { return submit_rpc ? submit_rpc(hex) : false; });

if (!reached) {
LOG_ERROR << "[EMB-BTC] won-block " << share_hash.GetHex().substr(0, 16)
<< " reached NEITHER sink -- SUBSIDY LOST.";
}
};
}

} // namespace coin
} // namespace btc
2 changes: 1 addition & 1 deletion src/impl/btc/test/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
if (BUILD_TESTING AND GTest_FOUND)
# btc twin of ltc share_test — uniquely named to avoid the CMP0002 target
# collision with src/impl/ltc/test (both subdirs build in the same tree).
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 block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp)
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 block_broadcast_connect_test.cpp regtest_sharechain_isolation_test.cpp stratum_merkle_branch_test.cpp witness_commitment_merkle_test.cpp finder_fee_integer_exact_test.cpp rpc_conf_test.cpp genesis_check_test.cpp won_share_dualpath_test.cpp)
target_link_libraries(btc_share_test PRIVATE
GTest::gtest_main GTest::gtest
core btc
Expand Down
110 changes: 110 additions & 0 deletions src/impl/btc/test/won_share_dualpath_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
// ---------------------------------------------------------------------------
// Gate #744 BINDING KAT: forced won share -> make_on_block_found -> BOTH arms.
//
// BTC ShareTracker::m_on_block_found fires the instant a verified sharechain
// share crosses the BTC network target, but main_btc.cpp never assigned it, so
// a peer-relayed won share was silently dropped (subsidy lost). This drives a
// forced won share through the connect-authoritative dispatch handler and
// asserts BOTH broadcast arms fire: P2P relay (best-effort bytes) + submitblock
// RPC (connect-authoritative hex), carrying the byte-identical block.
//
// The reconstructor is INJECTED as a stub here -- the faithful share->block
// reassembly (mirroring p2pool data.py Share.as_block / DGB #174/#176) is the
// next sub-slice; this KAT pins the DISPATCH contract independent of it, exactly
// as the header's "build-verifiable and run-tested NOW" contract intends.
//
// Rides the already-allowlisted btc_share_test executable, so no build.yml
// --target allowlist change is needed (no #137-style NOT_BUILT sentinel risk).
// p2pool-merged-v36 surface: NONE.
// ---------------------------------------------------------------------------

#include <gtest/gtest.h>

#include <optional>
#include <string>
#include <utility>
#include <vector>

#include <core/uint256.hpp>

#include "../coin/won_block_dispatch.hpp"

using btc::coin::make_on_block_found;

namespace {

std::string to_hex(const std::vector<unsigned char>& b) {
static const char* d = "0123456789abcdef";
std::string s;
s.reserve(b.size() * 2);
for (unsigned char c : b) { s.push_back(d[c >> 4]); s.push_back(d[c & 0xf]); }
return s;
}

using Recon = std::optional<std::pair<std::vector<unsigned char>, std::string>>;

} // namespace

// 1) THE GATE: one forced won share -> reconstruct -> BOTH arms carry the
// byte-identical block (P2P relay bytes, submitblock RPC hex).
TEST(BtcForcedWonShareDualPath, BothArmsCarryIdenticalBlock) {
const std::vector<unsigned char> block_bytes = {0x01, 0x02, 0x03, 0xde, 0xad, 0xbe, 0xef};
const std::string block_hex = to_hex(block_bytes);

std::vector<unsigned char> relayed;
bool did_relay = false;
std::string submitted;
int submit_calls = 0;

auto reconstruct = [&](const uint256&) -> Recon { return std::make_pair(block_bytes, block_hex); };
auto relay = [&](const std::vector<unsigned char>& b) { did_relay = true; relayed = b; return true; };
auto submit = [&](const std::string& h) { ++submit_calls; submitted = h; return true; };

auto handler = make_on_block_found(reconstruct, relay, submit);
handler(uint256::ZERO); // FORCE the won share

// ARM A -- embedded P2P relay fired with the block bytes.
ASSERT_TRUE(did_relay);
EXPECT_EQ(relayed, block_bytes);

// ARM B -- submitblock RPC ALWAYS fired (connect-authoritative), with the hex.
ASSERT_EQ(submit_calls, 1);
EXPECT_EQ(submitted, block_hex);

// CROSS-ARM IDENTITY: arm-A bytes hex-encode to exactly arm-B's hex.
EXPECT_EQ(to_hex(relayed), submitted);
}

// 2) Unassemblable share -> reconstruct returns nullopt -> NEITHER arm fires
// (never fabricate or relay a partial block).
TEST(BtcForcedWonShareDualPath, UnassemblableShareBroadcastsNothing) {
bool did_relay = false;
int submit_calls = 0;

auto reconstruct = [&](const uint256&) -> Recon { return std::nullopt; };
auto relay = [&](const std::vector<unsigned char>&) { did_relay = true; return true; };
auto submit = [&](const std::string&) { ++submit_calls; return true; };

auto handler = make_on_block_found(reconstruct, relay, submit);
handler(uint256::ZERO);

EXPECT_FALSE(did_relay);
EXPECT_EQ(submit_calls, 0);
}

// 3) Connect-authoritative: even when the P2P relay FAILS, the submitblock RPC
// still fires and the block reaches a sink (no silent loss).
TEST(BtcForcedWonShareDualPath, P2pFailureStillConnectsViaRpc) {
const std::vector<unsigned char> block_bytes = {0xaa, 0xbb};

int submit_calls = 0;
auto reconstruct = [&](const uint256&) -> Recon { return std::make_pair(block_bytes, std::string("aabb")); };
auto relay = [&](const std::vector<unsigned char>&) { return false; }; // P2P down
auto submit = [&](const std::string&) { ++submit_calls; return true; };

auto handler = make_on_block_found(reconstruct, relay, submit);
handler(uint256::ZERO);

EXPECT_EQ(submit_calls, 1); // RPC fired despite P2P failure
}
Loading