Skip to content

Commit 069fdaf

Browse files
authored
Merge pull request #451 from frstrtr/nmc/pe-item3-host-dualpath-lock
nmc(pe-item3): host-level dual-path won-aux broadcaster regression lock
2 parents d64d0d2 + 15915c0 commit 069fdaf

3 files changed

Lines changed: 257 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 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 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 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 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
@@ -80,6 +80,19 @@ if (BUILD_TESTING AND GTest_FOUND)
8080
target_link_libraries(nmc_block_broadcast_test PRIVATE
8181
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
8282

83+
# PE item 3 (host half): host-level dual-path won-aux broadcaster regression
84+
# lock. Reconstructs the run-loop binding (set_block_relay -> submit_block_raw
85+
# + set_fallback_backend -> AuxChainRPC::submit_aux_block) with fakes and
86+
# locks DELIVERY through both legs -- net-new vs the presence-only helper KAT
87+
# above. Same header-only dispatcher, so the same SCC (core + c2pool_* object
88+
# libs); no nmc_coin (no transaction ctor).
89+
add_executable(nmc_host_dualpath_test nmc_host_dualpath_test.cpp)
90+
target_link_libraries(nmc_host_dualpath_test PRIVATE
91+
GTest::gtest_main GTest::gtest
92+
core nlohmann_json::nlohmann_json)
93+
target_link_libraries(nmc_host_dualpath_test PRIVATE
94+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
95+
8396
include(GoogleTest)
8497
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
8598
gtest_add_tests(nmc_auxpow_merkle_test "" AUTO)
@@ -88,4 +101,5 @@ if (BUILD_TESTING AND GTest_FOUND)
88101
gtest_add_tests(nmc_reconstruct_won_block_test "" AUTO)
89102
gtest_add_tests(nmc_mempool_name_test "" AUTO)
90103
gtest_add_tests(nmc_block_broadcast_test "" AUTO)
104+
gtest_add_tests(nmc_host_dualpath_test "" AUTO)
91105
endif()
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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

Comments
 (0)