Skip to content

Commit b22daa7

Browse files
authored
nmc(PF): lock external-daemon fallback-path conformance (#493)
Phase PF fallback-path half. The V36 external_fallback invariant requires the external-daemon (.140 namecoind submitauxblock) RPC fallback to PERSIST as the retained path alongside the embedded P2P relay. Existing dispatcher and host-wiring KATs bind the fallback leg to an always-ack fake, so neither distinguishes WHICH backend serves it. This adds nmc_fallback_path_conformance_test (3 KATs) locking the one fact PF owns: the fallback MUST be the external namecoind RPC (a daemon that can ack), NOT the embedded backend daemon-less submit_aux_block (structurally false, aux_chain_embedded.hpp:153). Mis-wiring set_fallback_backend to the embedded self-leg leaves the wiring looking present but turns the retained fallback into a dead path -- a won aux block is silently lost when P2P is down. Test 2 makes that regression RED; test 1 proves the external path carries when P2P is down; test 3 locks that the external fallback always-fires alongside a working embedded relay (not optimized away). Test-only, NMC-fenced (src/impl/nmc/ only), zero consensus surface. Registered in test/CMakeLists.txt + gtest_add_tests and the build.yml --target allowlist (both legs) to avoid a #137-style NOT_BUILT sentinel. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 9445b46 commit b22daa7

3 files changed

Lines changed: 142 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ jobs:
8181
test_mweb_builder \
8282
test_address_resolution test_compute_share_target \
8383
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_witness_commitment_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
84-
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \
84+
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test nmc_fallback_path_conformance_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \
8585
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test \
8686
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
8787
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test v37_test \
@@ -213,7 +213,7 @@ jobs:
213213
test_mweb_builder \
214214
test_address_resolution test_compute_share_target \
215215
test_utxo test_dgb_subsidy test_dgb_coinbase_value dgb_share_test dgb_redistribute_test dgb_block_assembly_test dgb_witness_commitment_test dgb_header_sample_build_test dgb_header_ingest_test dgb_mempool_ingest_test \
216-
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \
216+
dgb_gentx_coinbase_test dgb_connection_coinbase_test dgb_pplns_payout_split_test nmc_auxpow_merkle_test nmc_template_builder_test nmc_auxpow_wire_test nmc_reconstruct_won_block_test nmc_mempool_name_test nmc_block_broadcast_test nmc_host_dualpath_test nmc_fallback_path_conformance_test dgb_gentx_share_path_test dgb_conn_pplns_producer_test dgb_other_tx_resolver_test \
217217
dgb_other_tx_assembler_test dgb_reconstruct_won_block_test dgb_reconstruct_closure_test dgb_gentx_unpack_test dgb_work_source_test dgb_template_builder_test dgb_embedded_coin_node_test dgb_embedded_tx_select_test dgb_template_other_txs_test dgb_coinbase_value_parity_test dgb_submit_classify_test dgb_aux_parent_coinbase_parity_test dgb_template_capture_test dgb_aux_doge_db_commitment_bind_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test \
218218
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
219219
dgb_coin_node_seam_test dgb_block_broadcast_test dgb_won_block_dispatch_test dgb_forced_won_share_dualpath_test dgb_scrypt_pow_test dgb_nonce_grinder_test dgb_regrind_block_test dgb_won_block_finalize_test dgb_share_target_genesis_test dgb_pool_msg_wire_test dgb_get_shares_walk_test dgb_download_stops_test dgb_think_p1_walk_bounds_test dgb_think_p1_desired_emit_test dgb_think_p6_desired_cutoff_test dgb_think_p4_head_keys_test dgb_think_p3_best_head_test dgb_g1_oracle_byte_parity_test dgb_think_p2_walk_bounds_test dgb_expected_time_to_block_test dgb_tail_score_endpoints_test dgb_pool_attempts_per_second_test dgb_pool_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test dgb_binomial_conf_interval_test dgb_desired_version_tally_test dgb_get_height_and_last_endpoints_test dgb_chain_walk_window_test dgb_redistribute_delegate_ghal_test dgb_share_weight_decay_test dgb_naughty_propagation_test dgb_hash_format_parity_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \

src/impl/nmc/test/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,19 @@ if (BUILD_TESTING AND GTest_FOUND)
9393
target_link_libraries(nmc_host_dualpath_test PRIVATE
9494
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
9595

96+
# PF (fallback-path conformance): locks that the RETAINED external-daemon
97+
# fallback is the .140 namecoind submitauxblock RPC (a real daemon that can
98+
# ack), NOT the embedded backend daemon-less submit_aux_block (structurally
99+
# false). Mis-wiring the fallback to the embedded self-leg silently loses a
100+
# won block when P2P is down -- this test makes that RED. Same header-only
101+
# dispatcher SCC (core + c2pool_* object libs); no nmc_coin (no transaction ctor).
102+
add_executable(nmc_fallback_path_conformance_test nmc_fallback_path_conformance_test.cpp)
103+
target_link_libraries(nmc_fallback_path_conformance_test PRIVATE
104+
GTest::gtest_main GTest::gtest
105+
core nlohmann_json::nlohmann_json)
106+
target_link_libraries(nmc_fallback_path_conformance_test PRIVATE
107+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
108+
96109
include(GoogleTest)
97110
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
98111
gtest_add_tests(nmc_auxpow_merkle_test "" AUTO)
@@ -102,4 +115,5 @@ if (BUILD_TESTING AND GTest_FOUND)
102115
gtest_add_tests(nmc_mempool_name_test "" AUTO)
103116
gtest_add_tests(nmc_block_broadcast_test "" AUTO)
104117
gtest_add_tests(nmc_host_dualpath_test "" AUTO)
118+
gtest_add_tests(nmc_fallback_path_conformance_test "" AUTO)
105119
endif()
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
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

Comments
 (0)