Skip to content

Commit 4d5c3f2

Browse files
committed
dgb(#82): dual-path won-block broadcaster + dispatch contract test
Add coin/block_broadcast.hpp -- broadcast_won_block(), the DGB dual-path won-block dispatcher the run-loop wires tracker().m_on_block_found to. Fires BOTH sinks per the broadcaster-gate rule: embedded P2P relay (PRIMARY, supplied as a sink fn until NodeP2P/EmbeddedDaemon lands) AND external digibyted submitblock via the CoinNode seam (FALLBACK, fired ALWAYS, ignore_failure=true so a duplicate after a P2P accept never masks the win). landed_first records the race winner; with neither sink live the call is a safe no-op (any()=false, no throw, null seam never dereferenced). 1:1 contract mirror of bch broadcast_won_block (90a3553); decoupled via std::function P2P sink + ICoinNode* so it is build-verified now. DGB-Scrypt standalone parent -- no merged-coinbase leg in the default build. Complements the #163 RPC-fallback seam half. dgb_block_broadcast_test: 5-case dispatch contract (no-sink no-op, p2p-only, rpc-only, dual-path always-fires-fallback, fallback-no-ack-does-not-mask-p2p) over a fake ICoinNode -- 5/5 PASS, build EXIT=0. Registered in test/CMakeLists.txt AND both build.yml --target allowlists (#143 NOT_BUILT trap avoided). p2pool-merged-v36 surface: NONE (block dispatch only). Per-coin isolation: src/impl/dgb/coin/ only.
1 parent 1d3cf93 commit 4d5c3f2

4 files changed

Lines changed: 244 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ jobs:
7171
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
7272
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
7373
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
74-
dgb_coin_node_seam_test \
74+
dgb_coin_node_seam_test dgb_block_broadcast_test \
7575
v37_test \
7676
-j$(nproc)
7777
@@ -204,7 +204,7 @@ jobs:
204204
dgb_gentx_coinbase_test nmc_auxpow_merkle_test dgb_gentx_share_path_test dgb_other_tx_resolver_test \
205205
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_gentx_unpack_test \
206206
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
207-
dgb_coin_node_seam_test \
207+
dgb_coin_node_seam_test dgb_block_broadcast_test \
208208
test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
209209
v37_test \
210210
-j$(nproc)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// dgb::coin::broadcast_won_block -- the DGB dual-path won-block dispatcher.
4+
//
5+
// 1:1 contract mirror of bch::coin::EmbeddedDaemon::broadcast_won_block
6+
// (src/impl/bch/coin/embedded_daemon.hpp @90a35536): on a won block it fires
7+
// BOTH broadcast sinks --
8+
// PRIMARY : embedded P2P relay (fastest propagation), supplied by the
9+
// run-loop as a sink fn (DGB has no submit_block_p2p_raw on
10+
// coin::Node yet -- the EmbeddedDaemon/NodeP2P port binds this).
11+
// FALLBACK : external digibyted submitblock via the CoinNode seam
12+
// (core::coin::ICoinNode::submit_block_hex) -- fired ALWAYS, per
13+
// the broadcaster-gate dual-path rule, NOT a P2P-only path with
14+
// RPC as a catch. A duplicate on the RPC leg after a P2P accept
15+
// still proves both paths reached the node; landed_first records
16+
// which won the race. ignore_failure=true so an already-have on
17+
// the fallback never masks the P2P win.
18+
//
19+
// This is the sink the DGB pool node wires tracker().m_on_block_found to so an
20+
// in-operation win emits immediately (the remaining #82 run-loop slice; the
21+
// pool-node wiring mirrors bch @9fed4955). Decoupled from the (not-yet-ported)
22+
// DGB EmbeddedDaemon by taking the P2P sink as a std::function and the seam as
23+
// an ICoinNode*, so it is build-verifiable now and the run-loop binds the live
24+
// objects when NodeP2P lands.
25+
//
26+
// DGB-Scrypt is a STANDALONE parent in the V36 default build -- no merged-
27+
// coinbase leg (DOGE aux is -DAUX_DOGE=ON stretch, a separate parent path).
28+
// p2pool-merged-v36 surface: NONE -- block dispatch only, no PoW hash, share
29+
// format, coinbase commitment, or PPLNS math is touched. Per-coin isolation:
30+
// src/impl/dgb/coin/ only.
31+
// ---------------------------------------------------------------------------
32+
33+
#include <cstdint>
34+
#include <functional>
35+
#include <string>
36+
#include <vector>
37+
38+
#include <core/coin/node_iface.hpp>
39+
#include <core/log.hpp>
40+
41+
namespace dgb
42+
{
43+
namespace coin
44+
{
45+
46+
// Embedded P2P relay sink: the run-loop binds this to NodeP2P::submit_block_p2p_raw
47+
// once the embedded daemon is stood up. Empty == no embedded P2P sink present.
48+
using P2pRelaySink = std::function<void(const std::vector<unsigned char>&)>;
49+
50+
// Outcome of a won-block broadcast: which of the two sinks fired and whether
51+
// the external node acked. P2P is primary; the submitblock RPC fallback fires
52+
// ALWAYS (dual-path rule). landed_first records which path won the race.
53+
struct BlockBroadcast
54+
{
55+
bool p2p_sent = false; // embedded P2P relay issued (sink present)
56+
bool rpc_ok = false; // submitblock returned ok OR duplicate
57+
const char* landed_first = "none"; // "p2p" | "rpc" | "none"
58+
bool any() const { return p2p_sent || rpc_ok; }
59+
};
60+
61+
// Fire a won block down BOTH broadcast paths. `block_bytes` is the pre-
62+
// serialized block blob the embedded P2P relay sends; `block_hex` is the same
63+
// block hex for the external submitblock fallback. `p2p_relay` may be empty
64+
// (no embedded sink yet); `seam` may be null or RPC-less -- each leg is guarded
65+
// so the dispatcher never throws or dereferences a null sink.
66+
inline BlockBroadcast broadcast_won_block(const P2pRelaySink& p2p_relay,
67+
core::coin::ICoinNode* seam,
68+
const std::vector<unsigned char>& block_bytes,
69+
const std::string& block_hex)
70+
{
71+
BlockBroadcast r;
72+
73+
// PRIMARY: embedded P2P relay (fastest propagation).
74+
if (p2p_relay) {
75+
p2p_relay(block_bytes);
76+
r.p2p_sent = true;
77+
r.landed_first = "p2p";
78+
LOG_INFO << "[EMB-DGB] won-block P2P relay issued (" << block_bytes.size()
79+
<< " bytes) -- primary path.";
80+
} else {
81+
LOG_WARNING << "[EMB-DGB] won-block: no embedded P2P sink; relying on RPC fallback.";
82+
}
83+
84+
// FALLBACK (always fired): external digibyted submitblock. A duplicate here
85+
// after a P2P accept is success, not failure -- ignore_failure=true so a
86+
// duplicate/already-have does not mask the P2P win.
87+
if (seam && seam->has_rpc()) {
88+
r.rpc_ok = seam->submit_block_hex(block_hex, /*ignore_failure=*/true);
89+
if (r.rpc_ok && !r.p2p_sent) r.landed_first = "rpc";
90+
LOG_INFO << "[EMB-DGB] won-block submitblock RPC fallback "
91+
<< (r.rpc_ok ? "ok/duplicate" : "no-ack") << ".";
92+
} else {
93+
LOG_WARNING << "[EMB-DGB] won-block: no external digibyted-RPC fallback sink wired.";
94+
}
95+
96+
if (!r.any())
97+
LOG_ERROR << "[EMB-DGB] won-block had NEITHER broadcast sink -- block NOT relayed!";
98+
else
99+
LOG_INFO << "[EMB-DGB] won-block broadcast: p2p=" << (r.p2p_sent ? "sent" : "off")
100+
<< " rpc=" << (r.rpc_ok ? "ok" : "off")
101+
<< " landed_first=" << r.landed_first << ".";
102+
return r;
103+
}
104+
105+
} // namespace coin
106+
} // namespace dgb

src/impl/dgb/test/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,16 @@ if (BUILD_TESTING AND GTest_FOUND)
148148
dgb_coin pool sharechain)
149149
gtest_add_tests(dgb_coin_node_seam_test "" AUTO)
150150

151+
# --- #82 dual-path won-block broadcaster contract ----------------------
152+
# Header-only contract test for coin/block_broadcast.hpp (the dispatcher the
153+
# run-loop wires tracker().m_on_block_found to). Links core for the ICoinNode
154+
# seam + LOG_* symbols; needs no dgb OBJECT lib (uses a fake ICoinNode).
155+
add_executable(dgb_block_broadcast_test block_broadcast_test.cpp)
156+
target_link_libraries(dgb_block_broadcast_test PRIVATE
157+
GTest::gtest_main GTest::gtest
158+
core dgb
159+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage
160+
dgb_coin pool sharechain)
161+
gtest_add_tests(dgb_block_broadcast_test "" AUTO)
162+
151163
endif()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
// ---------------------------------------------------------------------------
2+
// dgb::coin::broadcast_won_block dual-path dispatcher test (#82 broadcaster-
3+
// gate, dispatcher half).
4+
//
5+
// Offline contract slice mirroring src/impl/bch/test/embedded_block_broadcast_test.cpp
6+
// (@90a35536). Locks the dual-path rule the DGB run-loop depends on when it
7+
// wires tracker().m_on_block_found to this dispatcher: BOTH the embedded P2P
8+
// relay (PRIMARY) and the external digibyted submitblock (FALLBACK, fired
9+
// ALWAYS) are attempted, each leg is guarded, landed_first records the race
10+
// winner, and with NEITHER sink live the call is a safe no-op (any()=false,
11+
// no throw). Uses a fake ICoinNode so the dispatch contract is verified without
12+
// the real NodeRPC transport (deferred). p2pool-merged-v36 surface: NONE.
13+
//
14+
// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist
15+
// or it becomes a #143-style NOT_BUILT sentinel that reds master.
16+
// ---------------------------------------------------------------------------
17+
18+
#include <gtest/gtest.h>
19+
20+
#include <string>
21+
#include <vector>
22+
23+
#include "../coin/block_broadcast.hpp"
24+
25+
namespace {
26+
27+
// Controllable external-RPC seam: lets each case set has_rpc() and the
28+
// submit_block_hex() ack independently, and records the call so the "always
29+
// fired" rule is observable.
30+
class FakeSeam : public core::coin::ICoinNode {
31+
public:
32+
bool rpc_present = true;
33+
bool submit_ack = true;
34+
int submit_calls = 0;
35+
bool last_ignore_failure = false;
36+
37+
core::coin::WorkView get_work_view() override { return {}; }
38+
39+
bool submit_block_hex(const std::string&, bool ignore_failure) override {
40+
++submit_calls;
41+
last_ignore_failure = ignore_failure;
42+
return submit_ack;
43+
}
44+
45+
bool is_embedded() const override { return false; }
46+
bool has_rpc() const override { return rpc_present; }
47+
};
48+
49+
const std::vector<unsigned char> kBytes(120, 0x42);
50+
const std::string kHex = "00112233";
51+
52+
} // namespace
53+
54+
// 1) NEITHER sink live -> safe no-op: any()=false, landed_first="none", no throw,
55+
// and a null seam is never dereferenced.
56+
TEST(DgbBlockBroadcast, NoSinkLiveIsSafeNoOp) {
57+
auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, /*seam=*/nullptr,
58+
kBytes, kHex);
59+
EXPECT_FALSE(r.p2p_sent);
60+
EXPECT_FALSE(r.rpc_ok);
61+
EXPECT_FALSE(r.any());
62+
EXPECT_STREQ(r.landed_first, "none");
63+
}
64+
65+
// 2) P2P-only: relay sink present, no RPC -> p2p wins, fallback skipped cleanly.
66+
TEST(DgbBlockBroadcast, P2pOnly) {
67+
bool relayed = false;
68+
std::vector<unsigned char> seen;
69+
auto relay = [&](const std::vector<unsigned char>& b) { relayed = true; seen = b; };
70+
71+
FakeSeam seam; seam.rpc_present = false;
72+
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);
73+
74+
EXPECT_TRUE(relayed);
75+
EXPECT_EQ(seen, kBytes);
76+
EXPECT_TRUE(r.p2p_sent);
77+
EXPECT_FALSE(r.rpc_ok);
78+
EXPECT_TRUE(r.any());
79+
EXPECT_STREQ(r.landed_first, "p2p");
80+
EXPECT_EQ(seam.submit_calls, 0); // no RPC sink -> fallback not attempted
81+
}
82+
83+
// 3) RPC-only: no embedded P2P sink, RPC acks -> fallback wins the race.
84+
TEST(DgbBlockBroadcast, RpcOnly) {
85+
FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true;
86+
auto r = dgb::coin::broadcast_won_block(/*p2p_relay=*/{}, &seam, kBytes, kHex);
87+
88+
EXPECT_FALSE(r.p2p_sent);
89+
EXPECT_TRUE(r.rpc_ok);
90+
EXPECT_TRUE(r.any());
91+
EXPECT_STREQ(r.landed_first, "rpc");
92+
EXPECT_EQ(seam.submit_calls, 1);
93+
EXPECT_TRUE(seam.last_ignore_failure); // duplicate must not mask a P2P win
94+
}
95+
96+
// 4) DUAL-PATH: both sinks live -> p2p wins landed_first, but the RPC fallback
97+
// STILL fires (the always-fire rule), and a duplicate ack there is success.
98+
TEST(DgbBlockBroadcast, DualPathAlwaysFiresFallback) {
99+
bool relayed = false;
100+
auto relay = [&](const std::vector<unsigned char>&) { relayed = true; };
101+
102+
FakeSeam seam; seam.rpc_present = true; seam.submit_ack = true;
103+
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);
104+
105+
EXPECT_TRUE(relayed);
106+
EXPECT_TRUE(r.p2p_sent);
107+
EXPECT_TRUE(r.rpc_ok); // fallback fired even though P2P won
108+
EXPECT_EQ(seam.submit_calls, 1); // ALWAYS-fired, not P2P-only catch
109+
EXPECT_STREQ(r.landed_first, "p2p"); // primary won the race
110+
}
111+
112+
// 5) Both sinks present but RPC no-acks (rejected): p2p still carried the block,
113+
// rpc_ok=false, any()=true (P2P leg succeeded).
114+
TEST(DgbBlockBroadcast, FallbackNoAckDoesNotMaskP2p) {
115+
auto relay = [&](const std::vector<unsigned char>&) {};
116+
FakeSeam seam; seam.rpc_present = true; seam.submit_ack = false;
117+
auto r = dgb::coin::broadcast_won_block(relay, &seam, kBytes, kHex);
118+
119+
EXPECT_TRUE(r.p2p_sent);
120+
EXPECT_FALSE(r.rpc_ok);
121+
EXPECT_TRUE(r.any());
122+
EXPECT_STREQ(r.landed_first, "p2p");
123+
EXPECT_EQ(seam.submit_calls, 1);
124+
}

0 commit comments

Comments
 (0)