|
| 1 | +// --------------------------------------------------------------------------- |
| 2 | +// nmc PF fallback-path conformance lock (Phase PF, external-daemon-fallback half). |
| 3 | +// |
| 4 | +// V36 per-coin invariant (v36-master-plan external_fallback): "an EXTERNAL-DAEMON |
| 5 | +// FALLBACK (RPC to a real coind / MM-Adapter) MUST PERSIST in every coin's c2pool |
| 6 | +// -- embedded is default, external is the RETAINED fallback path. Do NOT remove |
| 7 | +// external-daemon code paths." For NMC the retained fallback is the .140 testnet |
| 8 | +// namecoind `submitauxblock` RPC, bound by the run-loop as the AuxRpcSink. |
| 9 | +// |
| 10 | +// NET-NEW vs nmc_block_broadcast_test (dispatcher contract) and |
| 11 | +// nmc_host_dualpath_test (host wiring DELIVERY): both of those bind the fallback |
| 12 | +// leg to a fake that ALWAYS ACKS, so neither distinguishes WHICH backend the |
| 13 | +// fallback is. This file locks the one conformance fact PF owns: the fallback |
| 14 | +// MUST be the EXTERNAL namecoind RPC (a real daemon that can ack), and MUST NOT |
| 15 | +// be the embedded backend's own submit_aux_block -- which is structurally |
| 16 | +// daemon-less and returns false unconditionally (aux_chain_embedded.hpp:153, |
| 17 | +// "no daemon to submit to; embedded RPC leg cannot broadcast, returning false"). |
| 18 | +// |
| 19 | +// The trap: a refactor that "simplifies" set_fallback_backend to point at the |
| 20 | +// embedded AuxChainEmbedded::submit_aux_block leaves the wiring LOOKING present |
| 21 | +// (a sink is bound, nmc_host_dualpath stays green on its always-ack fake) but |
| 22 | +// turns the retained fallback into a dead path: when the embedded P2P relay is |
| 23 | +// down, a won aux block is then SILENTLY LOST. This file makes that regression |
| 24 | +// RED. Test-only, std::function fakes, zero consensus surface (no PoW hash, |
| 25 | +// share format, aux commitment, template, or PPLNS math). p2pool-merged-v36 |
| 26 | +// surface: NONE. Per-coin isolation: src/impl/nmc/ only; consumes |
| 27 | +// coin/block_broadcast.hpp (core/log.hpp only) -- pulls no btc/ or dgb/ symbol. |
| 28 | +// |
| 29 | +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist |
| 30 | +// or it becomes a #137/#143-style NOT_BUILT sentinel that reds master. |
| 31 | +// --------------------------------------------------------------------------- |
| 32 | + |
| 33 | +#include <gtest/gtest.h> |
| 34 | + |
| 35 | +#include <string> |
| 36 | +#include <vector> |
| 37 | + |
| 38 | +#include "../coin/block_broadcast.hpp" |
| 39 | + |
| 40 | +namespace { |
| 41 | + |
| 42 | +const std::vector<unsigned char> kBytes(120, 0x42); |
| 43 | +const std::string kHashHex = "47589169f94e3e77bf4da8067e76b4417b021f0eb10760995671856f21b8d4b4"; |
| 44 | +const std::string kAuxpowHex = "0011223344556677"; |
| 45 | + |
| 46 | +// Models c2pool::merged::AuxChainRPC -- the EXTERNAL .140 namecoind submitauxblock |
| 47 | +// client the run-loop binds via set_fallback_backend. A real daemon ACKS (true) |
| 48 | +// on accept OR harmless duplicate (merged_mining.cpp:454). This is the RETAINED |
| 49 | +// external fallback the V36 invariant requires. |
| 50 | +struct ExternalNamecoindRpc { |
| 51 | + int calls = 0; |
| 52 | + bool submit_aux_block(const std::string&, const std::string&) { ++calls; return true; } |
| 53 | +}; |
| 54 | + |
| 55 | +// Models AuxChainEmbedded::submit_aux_block (aux_chain_embedded.hpp:153): the |
| 56 | +// EMBEDDED backend has no daemon to RPC, so its RPC leg cannot broadcast and |
| 57 | +// returns false UNCONDITIONALLY. Wiring THIS as the fallback is the conformance |
| 58 | +// violation -- it is not the external daemon, it is the embedded self-leg. |
| 59 | +struct EmbeddedSelfRpcLeg { |
| 60 | + int calls = 0; |
| 61 | + bool submit_aux_block(const std::string&, const std::string&) { ++calls; return false; } |
| 62 | +}; |
| 63 | + |
| 64 | +} // namespace |
| 65 | + |
| 66 | +// 1) RETAINED EXTERNAL FALLBACK CARRIES when the embedded P2P relay is down. |
| 67 | +// The external namecoind submitauxblock saves the won aux block: rpc_ok=true, |
| 68 | +// any()=true, it won the race. This is the path the V36 external_fallback |
| 69 | +// invariant says must PERSIST. |
| 70 | +TEST(NmcPfFallbackPath, ExternalDaemonFallbackCarriesWhenEmbeddedP2pDown) { |
| 71 | + ExternalNamecoindRpc ext; |
| 72 | + nmc::coin::AuxRpcSink rpc = [&](const std::string& h, const std::string& a) { |
| 73 | + return ext.submit_aux_block(h, a); |
| 74 | + }; |
| 75 | + |
| 76 | + // P2P relay sink empty == embedded relay down / no peers route. |
| 77 | + auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, rpc, |
| 78 | + kBytes, kHashHex, kAuxpowHex); |
| 79 | + EXPECT_FALSE(r.p2p_sent); |
| 80 | + EXPECT_TRUE(r.rpc_ok); // external daemon acked |
| 81 | + EXPECT_TRUE(r.any()); // won block NOT lost |
| 82 | + EXPECT_STREQ(r.landed_first, "rpc"); |
| 83 | + EXPECT_EQ(ext.calls, 1); |
| 84 | +} |
| 85 | + |
| 86 | +// 2) CONFORMANCE LOCK: the embedded backend's own submit_aux_block CANNOT serve |
| 87 | +// as the fallback. It is daemon-less and returns false unconditionally, so if |
| 88 | +// a refactor mis-wires set_fallback_backend to it, then with the embedded P2P |
| 89 | +// relay also down the won aux block is SILENTLY LOST (any()=false). This is |
| 90 | +// exactly the regression the external_fallback invariant forbids; making it |
| 91 | +// RED is the whole point of PF's fallback-path lock. |
| 92 | +TEST(NmcPfFallbackPath, EmbeddedSelfLegCannotServeAsFallback_BlockLost) { |
| 93 | + EmbeddedSelfRpcLeg self; |
| 94 | + nmc::coin::AuxRpcSink mis_wired = [&](const std::string& h, const std::string& a) { |
| 95 | + return self.submit_aux_block(h, a); |
| 96 | + }; |
| 97 | + |
| 98 | + auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, mis_wired, |
| 99 | + kBytes, kHashHex, kAuxpowHex); |
| 100 | + EXPECT_EQ(self.calls, 1); // the leg was consulted... |
| 101 | + EXPECT_FALSE(r.rpc_ok); // ...but structurally cannot ack |
| 102 | + EXPECT_FALSE(r.p2p_sent); |
| 103 | + EXPECT_FALSE(r.any()); // WON BLOCK LOST -- the forbidden state |
| 104 | + EXPECT_STREQ(r.landed_first, "none"); |
| 105 | +} |
| 106 | + |
| 107 | +// 3) PERSISTENCE: the external fallback must stay wired ALONGSIDE a working |
| 108 | +// embedded P2P relay, not be optimized away "because embedded works". With |
| 109 | +// both present the external submitauxblock STILL fires (always-fire rule), |
| 110 | +// so deleting it to "save an RPC" flips calls 1->0 and removes the retained |
| 111 | +// safety net -- the regression this asserts against. |
| 112 | +TEST(NmcPfFallbackPath, ExternalFallbackPersistsAlongsideWorkingEmbeddedP2p) { |
| 113 | + bool relayed = false; |
| 114 | + nmc::coin::P2pRelaySink p2p = [&](const std::vector<unsigned char>&) { relayed = true; }; |
| 115 | + ExternalNamecoindRpc ext; |
| 116 | + nmc::coin::AuxRpcSink rpc = [&](const std::string& h, const std::string& a) { |
| 117 | + return ext.submit_aux_block(h, a); |
| 118 | + }; |
| 119 | + |
| 120 | + auto r = nmc::coin::broadcast_won_aux_block(p2p, rpc, kBytes, kHashHex, kAuxpowHex); |
| 121 | + EXPECT_TRUE(relayed); |
| 122 | + EXPECT_TRUE(r.p2p_sent); |
| 123 | + EXPECT_TRUE(r.rpc_ok); // retained fallback fired anyway |
| 124 | + EXPECT_EQ(ext.calls, 1); // ALWAYS-fired, not skipped because P2P won |
| 125 | + EXPECT_STREQ(r.landed_first, "p2p"); // primary still won the race |
| 126 | +} |
0 commit comments