|
| 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