diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e9993d14e..9038c5333 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -71,6 +71,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_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 \ v37_test \ -j$(nproc) @@ -203,6 +204,7 @@ jobs: dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \ dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_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 \ test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \ v37_test \ -j$(nproc) diff --git a/src/impl/dgb/coin/block_broadcast.hpp b/src/impl/dgb/coin/block_broadcast.hpp new file mode 100644 index 000000000..988d3f686 --- /dev/null +++ b/src/impl/dgb/coin/block_broadcast.hpp @@ -0,0 +1,106 @@ +#pragma once +// --------------------------------------------------------------------------- +// dgb::coin::broadcast_won_block -- the DGB dual-path won-block dispatcher. +// +// 1:1 contract mirror of bch::coin::EmbeddedDaemon::broadcast_won_block +// (src/impl/bch/coin/embedded_daemon.hpp @90a35536): on a won block it fires +// BOTH broadcast sinks -- +// PRIMARY : embedded P2P relay (fastest propagation), supplied by the +// run-loop as a sink fn (DGB has no submit_block_p2p_raw on +// coin::Node yet -- the EmbeddedDaemon/NodeP2P port binds this). +// FALLBACK : external digibyted submitblock via the CoinNode seam +// (core::coin::ICoinNode::submit_block_hex) -- fired ALWAYS, per +// the broadcaster-gate dual-path rule, NOT a P2P-only path with +// RPC as a catch. A duplicate on the RPC leg after a P2P accept +// still proves both paths reached the node; landed_first records +// which won the race. ignore_failure=true so an already-have on +// the fallback never masks the P2P win. +// +// This is the sink the DGB pool node wires tracker().m_on_block_found to so an +// in-operation win emits immediately (the remaining #82 run-loop slice; the +// pool-node wiring mirrors bch @9fed4955). Decoupled from the (not-yet-ported) +// DGB EmbeddedDaemon by taking the P2P sink as a std::function and the seam as +// an ICoinNode*, so it is build-verifiable now and the run-loop binds the live +// objects when NodeP2P lands. +// +// 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 + +namespace dgb +{ +namespace coin +{ + +// Embedded P2P relay sink: the run-loop binds this to NodeP2P::submit_block_p2p_raw +// once the embedded daemon is stood up. Empty == no embedded P2P sink present. +using P2pRelaySink = std::function&)>; + +// Outcome of a won-block broadcast: which of the two sinks fired and whether +// the external node acked. P2P is primary; the submitblock RPC fallback fires +// ALWAYS (dual-path rule). landed_first records which path won the race. +struct BlockBroadcast +{ + bool p2p_sent = false; // embedded P2P relay issued (sink present) + bool rpc_ok = false; // submitblock returned ok OR duplicate + const char* landed_first = "none"; // "p2p" | "rpc" | "none" + bool any() const { return p2p_sent || rpc_ok; } +}; + +// Fire a won block down BOTH broadcast paths. `block_bytes` is the pre- +// serialized block blob the embedded P2P relay sends; `block_hex` is the same +// block hex for the external submitblock fallback. `p2p_relay` may be empty +// (no embedded sink yet); `seam` may be null or RPC-less -- each leg is guarded +// so the dispatcher never throws or dereferences a null sink. +inline BlockBroadcast broadcast_won_block(const P2pRelaySink& p2p_relay, + core::coin::ICoinNode* seam, + const std::vector& block_bytes, + const std::string& block_hex) +{ + BlockBroadcast r; + + // PRIMARY: embedded P2P relay (fastest propagation). + if (p2p_relay) { + p2p_relay(block_bytes); + r.p2p_sent = true; + r.landed_first = "p2p"; + LOG_INFO << "[EMB-DGB] won-block P2P relay issued (" << block_bytes.size() + << " bytes) -- primary path."; + } else { + LOG_WARNING << "[EMB-DGB] won-block: no embedded P2P sink; relying on RPC fallback."; + } + + // FALLBACK (always fired): external digibyted submitblock. A duplicate here + // after a P2P accept is success, not failure -- ignore_failure=true so a + // duplicate/already-have does not mask the P2P win. + if (seam && seam->has_rpc()) { + r.rpc_ok = seam->submit_block_hex(block_hex, /*ignore_failure=*/true); + if (r.rpc_ok && !r.p2p_sent) r.landed_first = "rpc"; + LOG_INFO << "[EMB-DGB] won-block submitblock RPC fallback " + << (r.rpc_ok ? "ok/duplicate" : "no-ack") << "."; + } else { + LOG_WARNING << "[EMB-DGB] won-block: no external digibyted-RPC fallback sink wired."; + } + + if (!r.any()) + LOG_ERROR << "[EMB-DGB] won-block had NEITHER broadcast sink -- block NOT relayed!"; + else + LOG_INFO << "[EMB-DGB] won-block broadcast: p2p=" << (r.p2p_sent ? "sent" : "off") + << " rpc=" << (r.rpc_ok ? "ok" : "off") + << " landed_first=" << r.landed_first << "."; + return r; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index cd711f609..8af21bd4a 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -135,4 +135,29 @@ if (BUILD_TESTING AND GTest_FOUND) GTest::gtest_main GTest::gtest nlohmann_json::nlohmann_json) gtest_add_tests(${dgb_guard} "" AUTO) endforeach() + # --- #82 broadcaster-gate: CoinNode submit/seam contract ----------------- + # Links the dgb OBJECT lib (coin_node.cpp) like dgb_share_test so the live + # CoinNode::submit_block_hex !m_rpc guard + embedded WorkView slice are + # build- and run-verified. Pins the external-digibyted RPC fallback half of + # the won-block dispatch (web_server -> ICoinNode::submit_block_hex). + add_executable(dgb_coin_node_seam_test coin_node_seam_test.cpp) + target_link_libraries(dgb_coin_node_seam_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_coin_node_seam_test "" AUTO) + + # --- #82 dual-path won-block broadcaster contract ---------------------- + # Header-only contract test for coin/block_broadcast.hpp (the dispatcher the + # run-loop wires tracker().m_on_block_found to). Links core for the ICoinNode + # seam + LOG_* symbols; needs no dgb OBJECT lib (uses a fake ICoinNode). + add_executable(dgb_block_broadcast_test block_broadcast_test.cpp) + target_link_libraries(dgb_block_broadcast_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_block_broadcast_test "" AUTO) + endif() diff --git a/src/impl/dgb/test/block_broadcast_test.cpp b/src/impl/dgb/test/block_broadcast_test.cpp new file mode 100644 index 000000000..8727f297f --- /dev/null +++ b/src/impl/dgb/test/block_broadcast_test.cpp @@ -0,0 +1,124 @@ +// --------------------------------------------------------------------------- +// dgb::coin::broadcast_won_block dual-path dispatcher test (#82 broadcaster- +// gate, dispatcher half). +// +// Offline contract slice mirroring src/impl/bch/test/embedded_block_broadcast_test.cpp +// (@90a35536). Locks the dual-path rule the DGB run-loop depends on when it +// wires tracker().m_on_block_found to this dispatcher: BOTH the embedded P2P +// relay (PRIMARY) and the external digibyted submitblock (FALLBACK, fired +// ALWAYS) are attempted, each leg is guarded, landed_first records the race +// winner, and with NEITHER sink live the call is a safe no-op (any()=false, +// no throw). Uses a fake ICoinNode so the dispatch contract is verified without +// the real NodeRPC transport (deferred). 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 "../coin/block_broadcast.hpp" + +namespace { + +// Controllable external-RPC seam: lets each case set has_rpc() and the +// submit_block_hex() ack independently, and records the call so the "always +// fired" rule is observable. +class FakeSeam : public core::coin::ICoinNode { +public: + bool rpc_present = true; + bool submit_ack = true; + int submit_calls = 0; + bool last_ignore_failure = false; + + core::coin::WorkView get_work_view() override { return {}; } + + bool submit_block_hex(const std::string&, bool ignore_failure) override { + ++submit_calls; + last_ignore_failure = ignore_failure; + return submit_ack; + } + + bool is_embedded() const override { return false; } + bool has_rpc() const override { return rpc_present; } +}; + +const std::vector kBytes(120, 0x42); +const std::string kHex = "00112233"; + +} // namespace + +// 1) NEITHER sink live -> safe no-op: any()=false, landed_first="none", no throw, +// and a null seam is never dereferenced. +TEST(DgbBlockBroadcast, NoSinkLiveIsSafeNoOp) { + auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, /*seam=*/nullptr, + kBytes, kHex); + EXPECT_FALSE(r.p2p_sent); + EXPECT_FALSE(r.rpc_ok); + EXPECT_FALSE(r.any()); + EXPECT_STREQ(r.landed_first, "none"); +} + +// 2) P2P-only: relay sink present, no RPC -> p2p wins, fallback skipped cleanly. +TEST(DgbBlockBroadcast, P2pOnly) { + bool relayed = false; + std::vector seen; + auto relay = [&](const std::vector& b) { relayed = true; seen = b; }; + + FakeSeam seam; seam.rpc_present = false; + auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex); + + EXPECT_TRUE(relayed); + EXPECT_EQ(seen, kBytes); + EXPECT_TRUE(r.p2p_sent); + EXPECT_FALSE(r.rpc_ok); + EXPECT_TRUE(r.any()); + EXPECT_STREQ(r.landed_first, "p2p"); + EXPECT_EQ(seam.submit_calls, 0); // no RPC sink -> fallback not attempted +} + +// 3) RPC-only: no embedded P2P sink, RPC acks -> fallback wins the race. +TEST(DgbBlockBroadcast, RpcOnly) { + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true; + auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, &seam, kBytes, kHex); + + EXPECT_FALSE(r.p2p_sent); + EXPECT_TRUE(r.rpc_ok); + EXPECT_TRUE(r.any()); + EXPECT_STREQ(r.landed_first, "rpc"); + EXPECT_EQ(seam.submit_calls, 1); + EXPECT_TRUE(seam.last_ignore_failure); // duplicate must not mask a P2P win +} + +// 4) DUAL-PATH: both sinks live -> p2p wins landed_first, but the RPC fallback +// STILL fires (the always-fire rule), and a duplicate ack there is success. +TEST(DgbBlockBroadcast, DualPathAlwaysFiresFallback) { + bool relayed = false; + auto relay = [&](const std::vector&) { relayed = true; }; + + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true; + auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex); + + EXPECT_TRUE(relayed); + EXPECT_TRUE(r.p2p_sent); + EXPECT_TRUE(r.rpc_ok); // fallback fired even though P2P won + EXPECT_EQ(seam.submit_calls, 1); // ALWAYS-fired, not P2P-only catch + EXPECT_STREQ(r.landed_first, "p2p"); // primary won the race +} + +// 5) Both sinks present but RPC no-acks (rejected): p2p still carried the block, +// rpc_ok=false, any()=true (P2P leg succeeded). +TEST(DgbBlockBroadcast, FallbackNoAckDoesNotMaskP2p) { + auto relay = [&](const std::vector&) {}; + FakeSeam seam; seam.rpc_present = true; seam.submit_ack = false; + auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex); + + EXPECT_TRUE(r.p2p_sent); + EXPECT_FALSE(r.rpc_ok); + EXPECT_TRUE(r.any()); + EXPECT_STREQ(r.landed_first, "p2p"); + EXPECT_EQ(seam.submit_calls, 1); +} diff --git a/src/impl/dgb/test/coin_node_seam_test.cpp b/src/impl/dgb/test/coin_node_seam_test.cpp new file mode 100644 index 000000000..9bd4b342b --- /dev/null +++ b/src/impl/dgb/test/coin_node_seam_test.cpp @@ -0,0 +1,94 @@ +// --------------------------------------------------------------------------- +// dgb::coin::CoinNode seam-contract test (#82 broadcaster-gate, RPC-fallback +// half). +// +// Locks the embedded-primary / external-RPC-fallback contract of the DGB +// CoinNode seam (core::coin::ICoinNode) that the won-block dispatch depends on. +// web_server.cpp submits a solved block through exactly this seam: +// m_coin_node->submit_block_hex(hex, false) (core/web_server.cpp) +// which forwards to NodeRPC::submitblock (the external-digibyted fallback that +// MUST persist alongside the embedded path). This test pins the three +// invariants that path relies on, so a regression here is caught before it can +// silently break block submission: +// 1. no work source at all -> empty WorkView, no throw +// 2. embedded present, no RPC -> WorkView sourced from embedded; +// is_embedded()=true, has_rpc()=false +// 3. submit_block_hex w/ no RPC sink -> false (the !m_rpc guard, NOT a throw +// and NOT a crash dereferencing null) +// +// SCOPE: the no-RPC guard + embedded WorkView slice only. The LIVE submitblock +// RPC call and the m_on_block_found tracker callback are wired by the DGB +// run-loop standup (NodeBridge + web_server; main_dgb.cpp is selftest-only +// today) -- that is the remaining #82 slice and is NOT exercised here (rpc= +// nullptr in every case). p2pool-merged-v36 surface: NONE (pure local seam). +// +// Mirrors src/impl/bch/test/coin_node_seam_test.cpp; written gtest-style to +// match the sibling dgb tests (gtest_add_tests AUTO). MUST appear in BOTH the +// test/CMakeLists.txt registration AND the build.yml --target allowlist or it +// becomes a #143-style NOT_BUILT sentinel that reds master. +// --------------------------------------------------------------------------- + +#include + +#include + +#include "../coin/coin_node.hpp" + +namespace { + +// Minimal embedded work source: returns a recognisable WorkData so the test can +// prove the seam moved THIS object's agnostic slice into the WorkView. +class FakeEmbedded : public dgb::coin::CoinNodeInterface { +public: + int calls = 0; + + dgb::coin::rpc::WorkData getwork() override { + ++calls; + dgb::coin::rpc::WorkData wd; + wd.m_data = nlohmann::json{{"marker", "embedded-dgb"}}; + wd.m_hashes.push_back(uint256(static_cast(0xab))); + wd.m_latency = 42; + return wd; + } + + nlohmann::json getblockchaininfo() override { return nlohmann::json::object(); } + bool is_synced() const override { return true; } +}; + +} // namespace + +// 1) No work source configured -> empty view, no throw (1:1 btc/bch/ltc ref). +TEST(DgbCoinNodeSeam, NoSourceEmptyView) { + dgb::coin::CoinNode n(nullptr, nullptr); + EXPECT_FALSE(n.is_embedded()); + EXPECT_FALSE(n.has_rpc()); + core::coin::WorkView v = n.get_work_view(); + EXPECT_TRUE(v.m_data.is_null() || v.m_data.empty()); + EXPECT_TRUE(v.m_hashes.empty()); + EXPECT_EQ(v.m_latency, 0); +} + +// 2) Embedded preferred, no RPC fallback wired -> view comes from embedded. +TEST(DgbCoinNodeSeam, EmbeddedSourcedView) { + FakeEmbedded emb; + dgb::coin::CoinNode n(&emb, /*rpc=*/nullptr); + EXPECT_TRUE(n.is_embedded()); + EXPECT_FALSE(n.has_rpc()); + core::coin::WorkView v = n.get_work_view(); + EXPECT_EQ(emb.calls, 1); // embedded WAS the source + ASSERT_TRUE(v.m_data.contains("marker")); + EXPECT_EQ(v.m_data["marker"], "embedded-dgb"); + EXPECT_EQ(v.m_hashes.size(), 1u); + EXPECT_EQ(v.m_latency, 42); +} + +// 3) No RPC sink -> submit_block_hex returns false (the !m_rpc guard), not a +// throw and not a null-deref crash. This is the won-block fallback contract. +TEST(DgbCoinNodeSeam, SubmitNoRpcReturnsFalse) { + FakeEmbedded emb; + dgb::coin::CoinNode n(&emb, /*rpc=*/nullptr); + EXPECT_FALSE(n.submit_block_hex("00", /*ignore_failure=*/true)); + + dgb::coin::CoinNode bare(nullptr, nullptr); + EXPECT_FALSE(bare.submit_block_hex("00", /*ignore_failure=*/false)); +}