diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf8e76190..4bca032c1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ v37_test \ -j$(nproc) @@ -200,7 +200,7 @@ jobs: test_address_resolution test_compute_share_target \ test_utxo test_dgb_subsidy dgb_share_test \ rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \ - dgb_coin_node_seam_test dgb_block_broadcast_test \ + dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/won_block_dispatch.hpp b/src/impl/dgb/coin/won_block_dispatch.hpp new file mode 100644 index 000000000..6443ea026 --- /dev/null +++ b/src/impl/dgb/coin/won_block_dispatch.hpp @@ -0,0 +1,94 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::make_on_block_found -- the won-block DISPATCH handler the run-loop +// installs as dgb::ShareTracker::m_on_block_found. +// +// This is the wiring half between the dispatcher (#166, broadcast_won_block) +// and the live run-loop: on a won block the tracker fires m_on_block_found with +// the winning share hash. This handler turns that share hash into a fully +// serialized parent block (via an injected WonBlockReconstructor -- the +// share->block "as_block" step) and then dispatches it down BOTH broadcast +// paths via broadcast_won_block (P2P primary + external digibyted submitblock +// fallback, always-fire dual-path rule). +// +// 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_before_refhash + ref/merkle link + tx lookup, +// mirroring p2pool data.py Share.as_block) is ported. The run-loop binds the +// real reconstructor + the live P2P relay sink + the CoinNode seam when the +// embedded NodeP2P lands; until then a stub reconstructor + the external-RPC +// fallback already carry a won block onto the network. +// +// 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_won_block, whose +// own dual-path guards decide which sink(s) carry it. +// +// DGB-Scrypt is a STANDALONE parent in the V36 default build -- no merged- +// coinbase leg (DOGE aux is -DAUX_DOGE=ON stretch, a separate parent path). +// p2pool-merged-v36 surface: NONE -- block dispatch only; no PoW hash, share +// format, coinbase commitment, or PPLNS math is touched. Per-coin isolation: +// src/impl/dgb/coin/ only. +// --------------------------------------------------------------------------- + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "block_broadcast.hpp" + +namespace dgb +{ +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 embedded P2P relay sends; block_hex +// is the same block for the external submitblock fallback. Mirrors p2pool +// data.py Share.as_block; the faithful body lands in a follow-up slice. +using WonBlockReconstructor = + std::function, std::string>>(const uint256&)>; + +// Build the m_on_block_found handler. The run-loop assigns the returned closure +// to tracker.m_on_block_found; `p2p_relay` may be empty (no embedded sink yet) +// and `seam` may be null / RPC-less -- broadcast_won_block guards each leg. +inline std::function +make_on_block_found(WonBlockReconstructor reconstruct, + P2pRelaySink p2p_relay, + core::coin::ICoinNode* seam) +{ + return [reconstruct = std::move(reconstruct), + p2p_relay = std::move(p2p_relay), + seam](const uint256& share_hash) + { + if (!reconstruct) { + LOG_ERROR << "[EMB-DGB] won-block " << share_hash.GetHex().substr(0, 16) + << " -- no reconstructor wired; cannot broadcast."; + return; + } + + auto blk = reconstruct(share_hash); + if (!blk) { + LOG_WARNING << "[EMB-DGB] won-block " << share_hash.GetHex().substr(0, 16) + << " could not be reconstructed -- NOT broadcast."; + return; + } + + LOG_INFO << "[EMB-DGB] GOT BLOCK! share=" << share_hash.GetHex().substr(0, 16) + << " reconstructed " << blk->first.size() + << " bytes -- dispatching dual-path."; + broadcast_won_block(p2p_relay, seam, blk->first, blk->second); + }; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index 046fe7479..517c12a73 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -42,6 +42,19 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_block_broadcast_test "" AUTO) + # --- #82 broadcaster-gate: won-block DISPATCH handler -------------------- + # Header-only contract test for coin/won_block_dispatch.hpp (the handler the + # run-loop wires tracker().m_on_block_found to: reconstruct share->block, + # then fire broadcast_won_block dual-path). Links core for the ICoinNode seam + # + LOG_* symbols; uses a fake ICoinNode + stub reconstructor, no OBJECT lib. + add_executable(dgb_won_block_dispatch_test won_block_dispatch_test.cpp) + target_link_libraries(dgb_won_block_dispatch_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_won_block_dispatch_test "" AUTO) + # --- M3 RPC-transport standalone regression guards --------------------- # Header-only guards over the external-daemon RPC coin-layer SSOTs # (rpc_request.hpp request-shape + version floor + genesis identity, and diff --git a/src/impl/dgb/test/won_block_dispatch_test.cpp b/src/impl/dgb/test/won_block_dispatch_test.cpp new file mode 100644 index 000000000..cdbf8a7d4 --- /dev/null +++ b/src/impl/dgb/test/won_block_dispatch_test.cpp @@ -0,0 +1,140 @@ +// --------------------------------------------------------------------------- +// dgb::coin::make_on_block_found won-block dispatch wiring test (#82 +// broadcaster-gate, dispatch half). +// +// Drives the handler the run-loop installs as ShareTracker::m_on_block_found +// END-TO-END: invoke the closure with a winning share hash and assert it (a) +// calls the injected reconstructor for that hash, (b) on a reconstructed block +// fires broadcast_won_block down BOTH paths (P2P primary + RPC fallback), and +// (c) on an unknown/unassemblable share broadcasts NOTHING. This is the seam +// that, once the live NodeP2P relay + faithful as_block reconstructor are +// bound, makes a won block reach the network -- so it locks the dispatch +// contract now while those pieces are still being ported. +// +// Uses a fake ICoinNode (the external-digibyted submitblock fallback is the +// live leg today) + a stub reconstructor, so no embedded daemon / network is +// needed. p2pool-merged-v36 surface: NONE. +// +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist +// or it becomes a #143-style NOT_BUILT sentinel that reds master. +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include +#include + +#include "../coin/won_block_dispatch.hpp" + +namespace { + +// Same controllable seam shape as block_broadcast_test: records the submitblock +// call so the "always-fire fallback" rule is observable through the handler. +class FakeSeam : public core::coin::ICoinNode { +public: + bool rpc_present = true; + bool submit_ack = true; + int submit_calls = 0; + std::string last_hex; + + core::coin::WorkView get_work_view() override { return {}; } + bool submit_block_hex(const std::string& hex, bool) override { + ++submit_calls; last_hex = hex; return submit_ack; + } + bool is_embedded() const override { return false; } + bool has_rpc() const override { return rpc_present; } +}; + +const std::vector kBytes(160, 0x37); +const std::string kHex = "deadbeef"; + +// Reconstructor that returns a canned block for a single "known" share and +// nullopt for anything else -- and records every hash it was asked about. +struct StubReconstructor { + uint256 known; + std::vector seen; + std::optional, std::string>> + operator()(const uint256& h) { + seen.push_back(h); + if (h == known) + return std::make_pair(kBytes, kHex); + return std::nullopt; + } +}; + +uint256 hash_a() { return uint256S("00000000000000000000000000000000000000000000000000000000000000aa"); } +uint256 hash_b() { return uint256S("00000000000000000000000000000000000000000000000000000000000000bb"); } + +} // namespace + +// 1) Known won share: reconstructor consulted for THAT hash, then both paths +// fire (P2P primary relays the bytes, RPC fallback always fires the hex). +TEST(DgbWonBlockDispatch, KnownShareFiresBothPaths) { + std::vector relayed; + bool did_relay = false; + auto relay = [&](const std::vector& b) { did_relay = true; relayed = b; }; + + auto recon = std::make_shared(); + recon->known = hash_a(); + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true; + + auto handler = dgb::coin::make_on_block_found( + [recon](const uint256& h) { return (*recon)(h); }, relay, &seam); + + handler(hash_a()); + + ASSERT_EQ(recon->seen.size(), 1u); + EXPECT_EQ(recon->seen[0], hash_a()); + EXPECT_TRUE(did_relay); + EXPECT_EQ(relayed, kBytes); // P2P primary carried the reconstructed bytes + EXPECT_EQ(seam.submit_calls, 1); // RPC fallback ALWAYS fired + EXPECT_EQ(seam.last_hex, kHex); // ...with the reconstructed block hex +} + +// 2) Unknown share: reconstructor returns nullopt -> NOTHING is broadcast on +// either path (no fabricated / partial block ever reaches the network). +TEST(DgbWonBlockDispatch, UnknownShareBroadcastsNothing) { + bool did_relay = false; + auto relay = [&](const std::vector&) { did_relay = true; }; + + auto recon = std::make_shared(); + recon->known = hash_a(); + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true; + + auto handler = dgb::coin::make_on_block_found( + [recon](const uint256& h) { return (*recon)(h); }, relay, &seam); + + handler(hash_b()); // not the known share + + ASSERT_EQ(recon->seen.size(), 1u); + EXPECT_EQ(recon->seen[0], hash_b()); + EXPECT_FALSE(did_relay); // P2P primary not touched + EXPECT_EQ(seam.submit_calls, 0); // RPC fallback not touched +} + +// 3) RPC-only deployment (no embedded P2P sink yet): the handler still carries +// a known won block to the network via the submitblock fallback alone. +TEST(DgbWonBlockDispatch, RpcOnlyStillReachesNetwork) { + auto recon = std::make_shared(); + recon->known = hash_a(); + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true; + + auto handler = dgb::coin::make_on_block_found( + [recon](const uint256& h) { return (*recon)(h); }, + /*p2p_relay=*/{}, &seam); + + handler(hash_a()); + + EXPECT_EQ(seam.submit_calls, 1); // block reached the network via RPC fallback + EXPECT_EQ(seam.last_hex, kHex); +} + +// 4) No reconstructor wired at all: handler is a safe no-op, never throws, +// never dereferences the (here null) seam. +TEST(DgbWonBlockDispatch, MissingReconstructorIsSafeNoOp) { + auto handler = dgb::coin::make_on_block_found( + /*reconstruct=*/{}, /*p2p_relay=*/{}, /*seam=*/nullptr); + EXPECT_NO_THROW(handler(hash_a())); +}