|
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// nmc host-level dual-path won-aux broadcaster regression-lock (PE item 3, |
| 3 | +// HOST-wiring half). NET-NEW vs the helper-level nmc_block_broadcast_test: |
| 4 | +// that test hands raw std::function sinks straight to broadcast_won_aux_block |
| 5 | +// and only checks sink PRESENCE. This one reconstructs the actual NMC run-loop |
| 6 | +// binding -- exactly as src/c2pool/c2pool_refactored.cpp does -- and locks |
| 7 | +// DELIVERY through both legs: |
| 8 | +// PRIMARY : backend->set_block_relay([merged_broadcasters, chain_id]{...}) |
| 9 | +// -> merged_broadcasters[chain_id]->submit_block_raw(bytes) |
| 10 | +// (host site c2pool_refactored.cpp:5276, returns relayed peers) |
| 11 | +// FALLBACK : mm_manager->set_fallback_backend(chain_id, AuxChainRPC) |
| 12 | +// -> AuxChainRPC::submit_aux_block(hash_hex, auxpow_hex) |
| 13 | +// (host site c2pool_refactored.cpp:5290 / merged_mining.cpp:454) |
| 14 | +// |
| 15 | +// Why a separate lock: a future refactor CAN silently drop the RPC fallback |
| 16 | +// (delete set_fallback_backend), drop the P2P relay (delete set_block_relay), |
| 17 | +// or MIS-KEY merged_broadcasters by chain_id -- and the helper test would stay |
| 18 | +// green because the dispatcher's p2p_sent flag is presence-based, not delivery- |
| 19 | +// based. host_dual_path_delivered() below asserts both networks were ACTUALLY |
| 20 | +// reached, so each of those regressions turns this RED. That is the whole point |
| 21 | +// of item 3's host-level lock vs the dispatcher contract slice. |
| 22 | +// |
| 23 | +// Test-only, fake sinks, zero consensus surface: no PoW hash, share format, aux |
| 24 | +// commitment, template, or PPLNS math is touched. p2pool-merged-v36 surface: |
| 25 | +// NONE. Per-coin isolation: src/impl/nmc/ only; consumes coin/block_broadcast.hpp |
| 26 | +// (core/log.hpp only) -- pulls no btc/ or dgb/ symbol. |
| 27 | +// |
| 28 | +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist |
| 29 | +// or it becomes a #137/#143-style NOT_BUILT sentinel that reds master. |
| 30 | +// --------------------------------------------------------------------------- |
| 31 | + |
| 32 | +#include <gtest/gtest.h> |
| 33 | + |
| 34 | +#include <cstdint> |
| 35 | +#include <map> |
| 36 | +#include <string> |
| 37 | +#include <vector> |
| 38 | + |
| 39 | +#include "../coin/block_broadcast.hpp" |
| 40 | + |
| 41 | +namespace { |
| 42 | + |
| 43 | +// Canonical won-aux payload (shared across cases). |
| 44 | +const std::vector<unsigned char> kBytes(120, 0x42); |
| 45 | +const std::string kHashHex = "47589169f94e3e77bf4da8067e76b4417b021f0eb10760995671856f21b8d4b4"; |
| 46 | +const std::string kAuxpowHex = "0011223344556677"; |
| 47 | + |
| 48 | +// NMC aux chain_id SSOT (NMC_AUXPOW_CHAIN_ID=0x0001) vs DOGE's 0x0062 -- the |
| 49 | +// keys the run-loop registers merged_broadcasters under. Used to exercise the |
| 50 | +// host map-keying guard (a mis-key relays to nobody). |
| 51 | +constexpr uint32_t kNmcChainId = 0x0001; |
| 52 | +constexpr uint32_t kDogeChainId = 0x0062; |
| 53 | + |
| 54 | +// Fake of the embedded multi-peer P2P broadcaster registered in |
| 55 | +// merged_broadcasters. The host set_block_relay closure calls submit_block_raw, |
| 56 | +// which returns the relayed peer count (host site c2pool_refactored.cpp:5281). |
| 57 | +struct FakeP2pBroadcaster { |
| 58 | + std::size_t peers = 4; |
| 59 | + int calls = 0; |
| 60 | + std::vector<unsigned char> last_block; |
| 61 | + std::size_t submit_block_raw(const std::vector<unsigned char>& b) { |
| 62 | + ++calls; |
| 63 | + last_block = b; |
| 64 | + return peers; |
| 65 | + } |
| 66 | +}; |
| 67 | + |
| 68 | +// Fake of c2pool::merged::AuxChainRPC, the fallback backend the host binds via |
| 69 | +// set_fallback_backend. submit_aux_block fires submitauxblock and returns true |
| 70 | +// on daemon accept OR harmless duplicate (merged_mining.cpp:454). |
| 71 | +struct FakeAuxChainRpc { |
| 72 | + bool accept = true; |
| 73 | + int calls = 0; |
| 74 | + std::string saw_hash, saw_auxpow; |
| 75 | + bool submit_aux_block(const std::string& h, const std::string& a) { |
| 76 | + ++calls; |
| 77 | + saw_hash = h; |
| 78 | + saw_auxpow = a; |
| 79 | + return accept; |
| 80 | + } |
| 81 | +}; |
| 82 | + |
| 83 | +// Reconstruct the NMC host dual-path wiring exactly as the run-loop does, then |
| 84 | +// fire a won-aux block through nmc::coin::broadcast_won_aux_block. |
| 85 | +// bind_p2p_relay == false models a refactor dropping backend->set_block_relay |
| 86 | +// fallback == nullptr models dropping mm_manager->set_fallback_backend |
| 87 | +// The P2P closure mirrors the host's lazy merged_broadcasters[chain_id] lookup |
| 88 | +// AND its not-found guard (return 0 / relay to nobody) verbatim. |
| 89 | +nmc::coin::AuxBlockBroadcast host_dispatch_won_aux( |
| 90 | + std::map<uint32_t, FakeP2pBroadcaster*>& merged_broadcasters, |
| 91 | + std::uint32_t chain_id, |
| 92 | + bool bind_p2p_relay, |
| 93 | + FakeAuxChainRpc* fallback, |
| 94 | + const std::vector<unsigned char>& bytes, |
| 95 | + const std::string& hash_hex, |
| 96 | + const std::string& auxpow_hex) |
| 97 | +{ |
| 98 | + nmc::coin::P2pRelaySink p2p; // empty unless host bound set_block_relay |
| 99 | + if (bind_p2p_relay) { |
| 100 | + p2p = [&merged_broadcasters, chain_id](const std::vector<unsigned char>& b) { |
| 101 | + auto it = merged_broadcasters.find(chain_id); |
| 102 | + if (it == merged_broadcasters.end()) return; // host guard: no peer reached |
| 103 | + it->second->submit_block_raw(b); |
| 104 | + }; |
| 105 | + } |
| 106 | + nmc::coin::AuxRpcSink rpc; // empty unless host bound set_fallback_backend |
| 107 | + if (fallback) { |
| 108 | + rpc = [fallback](const std::string& h, const std::string& a) { |
| 109 | + return fallback->submit_aux_block(h, a); |
| 110 | + }; |
| 111 | + } |
| 112 | + return nmc::coin::broadcast_won_aux_block(p2p, rpc, bytes, hash_hex, auxpow_hex); |
| 113 | +} |
| 114 | + |
| 115 | +// The host dual-path invariant: a correctly-wired won-aux dispatch reaches BOTH |
| 116 | +// networks. Presence of a sink is NOT enough -- a mis-keyed merged_broadcasters |
| 117 | +// lookup leaves bc.p2p_sent==true yet relays to nobody, so we require the |
| 118 | +// embedded broadcaster to have been ACTUALLY invoked (>=1 relay call) AND the |
| 119 | +// submitauxblock fallback to have been invoked and acked. Dropping either |
| 120 | +// binding (or mis-keying the map) flips this false -- that is the lock. |
| 121 | +bool host_dual_path_delivered(const nmc::coin::AuxBlockBroadcast& bc, |
| 122 | + int p2p_relay_calls, |
| 123 | + int rpc_calls) { |
| 124 | + return bc.p2p_sent && p2p_relay_calls >= 1 && bc.rpc_ok && rpc_calls >= 1; |
| 125 | +} |
| 126 | + |
| 127 | +} // namespace |
| 128 | + |
| 129 | +// 1) CANONICAL HOST WIRING -- both legs bound and correctly chain_id-keyed. |
| 130 | +// This is the positive lock: it goes RED if production drops a binding or |
| 131 | +// mis-keys the broadcaster map. Both networks are actually reached and each |
| 132 | +// leg is handed the exact payload. |
| 133 | +TEST(NmcHostDualPath, BindsBothLegsAndDeliversToBothNetworks) { |
| 134 | + FakeP2pBroadcaster p2p; |
| 135 | + FakeAuxChainRpc rpc; |
| 136 | + std::map<std::uint32_t, FakeP2pBroadcaster*> merged_broadcasters{{kNmcChainId, &p2p}}; |
| 137 | + |
| 138 | + auto bc = host_dispatch_won_aux(merged_broadcasters, kNmcChainId, |
| 139 | + /*bind_p2p_relay=*/true, &rpc, |
| 140 | + kBytes, kHashHex, kAuxpowHex); |
| 141 | + |
| 142 | + // Both legs DELIVERED -- the host-level invariant. |
| 143 | + EXPECT_TRUE(host_dual_path_delivered(bc, p2p.calls, rpc.calls)); |
| 144 | + |
| 145 | + // Primary P2P leg actually relayed the exact block to the peer broadcaster. |
| 146 | + EXPECT_EQ(p2p.calls, 1); |
| 147 | + EXPECT_EQ(p2p.last_block, kBytes); |
| 148 | + EXPECT_TRUE(bc.p2p_sent); |
| 149 | + |
| 150 | + // Fallback RPC leg fired ALWAYS with the exact (hash, auxpow) payload. |
| 151 | + EXPECT_EQ(rpc.calls, 1); |
| 152 | + EXPECT_EQ(rpc.saw_hash, kHashHex); |
| 153 | + EXPECT_EQ(rpc.saw_auxpow, kAuxpowHex); |
| 154 | + EXPECT_TRUE(bc.rpc_ok); |
| 155 | + |
| 156 | + EXPECT_TRUE(bc.any()); |
| 157 | + EXPECT_STREQ(bc.landed_first, "p2p"); // primary won the race |
| 158 | +} |
| 159 | + |
| 160 | +// 2) REGRESSION: a refactor deletes backend->set_block_relay. The P2P sink is |
| 161 | +// never bound; only the submitauxblock fallback carries the block. The host |
| 162 | +// invariant must catch the missing primary leg. |
| 163 | +TEST(NmcHostDualPath, DropP2pRelayBindingIsCaught) { |
| 164 | + FakeP2pBroadcaster p2p; |
| 165 | + FakeAuxChainRpc rpc; |
| 166 | + std::map<std::uint32_t, FakeP2pBroadcaster*> merged_broadcasters{{kNmcChainId, &p2p}}; |
| 167 | + |
| 168 | + auto bc = host_dispatch_won_aux(merged_broadcasters, kNmcChainId, |
| 169 | + /*bind_p2p_relay=*/false, &rpc, |
| 170 | + kBytes, kHashHex, kAuxpowHex); |
| 171 | + |
| 172 | + EXPECT_FALSE(host_dual_path_delivered(bc, p2p.calls, rpc.calls)); // LOCK fires |
| 173 | + EXPECT_FALSE(bc.p2p_sent); |
| 174 | + EXPECT_EQ(p2p.calls, 0); // embedded peer never reached |
| 175 | + EXPECT_TRUE(bc.rpc_ok); // fallback alone saved the won block |
| 176 | + EXPECT_EQ(rpc.calls, 1); |
| 177 | + EXPECT_TRUE(bc.any()); |
| 178 | + EXPECT_STREQ(bc.landed_first, "rpc"); |
| 179 | +} |
| 180 | + |
| 181 | +// 3) REGRESSION: a refactor deletes mm_manager->set_fallback_backend. The RPC |
| 182 | +// fallback is never bound; only the embedded P2P relay carries. The host |
| 183 | +// invariant must catch the missing fallback leg. |
| 184 | +TEST(NmcHostDualPath, DropFallbackBindingIsCaught) { |
| 185 | + FakeP2pBroadcaster p2p; |
| 186 | + std::map<std::uint32_t, FakeP2pBroadcaster*> merged_broadcasters{{kNmcChainId, &p2p}}; |
| 187 | + |
| 188 | + auto bc = host_dispatch_won_aux(merged_broadcasters, kNmcChainId, |
| 189 | + /*bind_p2p_relay=*/true, /*fallback=*/nullptr, |
| 190 | + kBytes, kHashHex, kAuxpowHex); |
| 191 | + |
| 192 | + EXPECT_FALSE(host_dual_path_delivered(bc, p2p.calls, /*rpc_calls=*/0)); // LOCK fires |
| 193 | + EXPECT_FALSE(bc.rpc_ok); |
| 194 | + EXPECT_TRUE(bc.p2p_sent); // primary alone carried |
| 195 | + EXPECT_EQ(p2p.calls, 1); |
| 196 | + EXPECT_EQ(p2p.last_block, kBytes); |
| 197 | + EXPECT_TRUE(bc.any()); |
| 198 | + EXPECT_STREQ(bc.landed_first, "p2p"); |
| 199 | +} |
| 200 | + |
| 201 | +// 4) REGRESSION (host map-keying): both legs are bound, but the run-loop |
| 202 | +// registers the embedded broadcaster under the wrong chain_id (here DOGE's |
| 203 | +// 0x0062 while dispatching the NMC 0x0001 win). The dispatcher's p2p_sent |
| 204 | +// stays TRUE -- the sink is present -- yet submit_block_raw is NEVER reached, |
| 205 | +// so the won block silently fails to relay over P2P. Only a DELIVERY-level |
| 206 | +// invariant catches this; the helper-level presence check cannot. The |
| 207 | +// fallback still saves the block, which is exactly why this is insidious. |
| 208 | +TEST(NmcHostDualPath, MiskeyedChainIdSilentlyDropsP2pButLockCatchesIt) { |
| 209 | + FakeP2pBroadcaster p2p; |
| 210 | + FakeAuxChainRpc rpc; |
| 211 | + std::map<std::uint32_t, FakeP2pBroadcaster*> merged_broadcasters{{kDogeChainId, &p2p}}; |
| 212 | + |
| 213 | + auto bc = host_dispatch_won_aux(merged_broadcasters, kNmcChainId, |
| 214 | + /*bind_p2p_relay=*/true, &rpc, |
| 215 | + kBytes, kHashHex, kAuxpowHex); |
| 216 | + |
| 217 | + EXPECT_TRUE(bc.p2p_sent); // sink PRESENT -- helper test would pass here |
| 218 | + EXPECT_EQ(p2p.calls, 0); // ...but the peer broadcaster was never hit |
| 219 | + EXPECT_FALSE(host_dual_path_delivered(bc, p2p.calls, rpc.calls)); // LOCK fires |
| 220 | + EXPECT_TRUE(bc.rpc_ok); // fallback masked the silent P2P drop |
| 221 | + EXPECT_EQ(rpc.calls, 1); |
| 222 | +} |
| 223 | + |
| 224 | +// 5) REGRESSION (both bindings dropped): neither leg bound -> never silent-drop. |
| 225 | +// The dispatcher screams (lost-subsidy ERROR) and reports any()==false; no |
| 226 | +// sink is invoked. |
| 227 | +TEST(NmcHostDualPath, NeitherLegBoundNeverSilentDrops) { |
| 228 | + FakeP2pBroadcaster p2p; |
| 229 | + std::map<std::uint32_t, FakeP2pBroadcaster*> merged_broadcasters{{kNmcChainId, &p2p}}; |
| 230 | + |
| 231 | + auto bc = host_dispatch_won_aux(merged_broadcasters, kNmcChainId, |
| 232 | + /*bind_p2p_relay=*/false, /*fallback=*/nullptr, |
| 233 | + kBytes, kHashHex, kAuxpowHex); |
| 234 | + |
| 235 | + EXPECT_FALSE(host_dual_path_delivered(bc, p2p.calls, /*rpc_calls=*/0)); |
| 236 | + EXPECT_FALSE(bc.any()); |
| 237 | + EXPECT_FALSE(bc.p2p_sent); |
| 238 | + EXPECT_FALSE(bc.rpc_ok); |
| 239 | + EXPECT_EQ(p2p.calls, 0); |
| 240 | + EXPECT_STREQ(bc.landed_first, "none"); |
| 241 | +} |
0 commit comments