Skip to content

Commit b0c4ce6

Browse files
authored
nmc(pe): dual-path won-aux-block broadcaster + contract KAT (#426)
PE item 3, dispatcher leg. Adds nmc::coin::broadcast_won_aux_block -- the NMC dual-path won-block dispatcher mirroring dgb::coin::broadcast_won_block: embedded P2P relay PRIMARY + external namecoind submitauxblock RPC FALLBACK (fired ALWAYS per the broadcaster-gate dual-path rule), each leg guarded, landed_first records the race, NEITHER sink => ERROR scream, never silent-drops a won block. Both legs are std::function sinks so the contract is build-verifiable now; the run-loop binds the live .140 testnet namecoind submitauxblock client in the follow-up slice (item 4 soak gated on it). 5 contract KATs (no-sink no-op / p2p-only / rpc-only / dual-path always-fires-fallback / fallback-no-ack-does-not-mask-p2p). Registered in test/CMakeLists.txt AND both build.yml --target allowlist lines (no #137 NOT_BUILT sentinel). Header-only, fenced src/impl/nmc/, consumes core/* only; no consensus-value change, no p2pool-merged-v36 surface touched. Co-authored-by: frstrtr <frstrtr@users.noreply.github.com>
1 parent 8bd1407 commit b0c4ce6

4 files changed

Lines changed: 244 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_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 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 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 \
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 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_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 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 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 \
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 test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#pragma once
2+
// ---------------------------------------------------------------------------
3+
// nmc::coin::broadcast_won_aux_block -- the NMC (Namecoin) dual-path won-block
4+
// dispatcher for a merge-mined aux block under a BTC SHA256d parent.
5+
//
6+
// 1:1 contract mirror of dgb::coin::broadcast_won_block (block dispatch half)
7+
// and bch::coin::EmbeddedDaemon::broadcast_won_block. When a found share's
8+
// parent BTC PoW also satisfies the NMC aux-network target, a REAL Namecoin
9+
// block (parent header + AuxPow proof) is formed and MUST reach the Namecoin
10+
// network. Per the broadcaster-gate dual-path rule it is fired down BOTH sinks:
11+
// PRIMARY : embedded P2P relay to namecoin peers (fastest propagation),
12+
// supplied by the run-loop as a sink fn -- the same authoritative
13+
// embedded route AuxChainEmbedded::submit_block() drives today.
14+
// FALLBACK : external namecoind `submitauxblock` RPC (to the .140 testnet
15+
// namecoind), fired ALWAYS, NOT a P2P-only path with RPC as a
16+
// catch. submitauxblock of an already-accepted aux block returns a
17+
// harmless rejection at the daemon, so a duplicate after a P2P
18+
// accept still proves the RPC path reached the node; ignore-failure
19+
// semantics mean that duplicate must never mask the P2P win.
20+
//
21+
// This is the sink the NMC run-loop wires the won-aux-block trigger to so an
22+
// in-operation win emits immediately. Decoupled from the (live) namecoind RPC
23+
// client by taking BOTH legs as std::function sinks: the dispatch CONTRACT is
24+
// build-verifiable and KAT-locked now, and the run-loop binds the live P2P
25+
// relay + the .140 submitauxblock client when they are stood up (PE item 3
26+
// live-client slice; the soak gate, item 4, follows once the RPC leg is live).
27+
//
28+
// Standardized v36-native shape: identical dual-path contract to dgb/bch so the
29+
// v37 multichain migration is a clean lift, not a reconciliation of divergent
30+
// dialects. Per-coin isolation: src/impl/nmc/ only; consumes core/* only, pulls
31+
// no btc/ or dgb/ symbol. p2pool-merged-v36 surface: NONE -- block dispatch
32+
// only; no PoW hash, share format, aux commitment, or PPLNS math is touched.
33+
// ---------------------------------------------------------------------------
34+
35+
#include <cstdint>
36+
#include <functional>
37+
#include <string>
38+
#include <vector>
39+
40+
#include <core/log.hpp>
41+
42+
namespace nmc
43+
{
44+
namespace coin
45+
{
46+
47+
// Embedded P2P relay sink: the run-loop binds this to the namecoin P2P relay
48+
// (the embedded submit_block route). Empty == no embedded P2P sink present.
49+
using P2pRelaySink = std::function<void(const std::vector<unsigned char>&)>;
50+
51+
// External submitauxblock RPC sink: the run-loop binds this to the live .140
52+
// testnet namecoind submitauxblock client. Takes the NMC block hash hex and the
53+
// serialized AuxPow hex; returns true on daemon accept OR harmless duplicate.
54+
// Empty == no RPC fallback sink wired.
55+
using AuxRpcSink =
56+
std::function<bool(const std::string& block_hash_hex, const std::string& auxpow_hex)>;
57+
58+
// Outcome of a won-aux-block broadcast: which sinks fired and whether the
59+
// external namecoind acked. P2P is primary; the submitauxblock RPC fallback is
60+
// fired ALWAYS (dual-path rule). landed_first records which path won the race.
61+
struct AuxBlockBroadcast
62+
{
63+
bool p2p_sent = false; // embedded P2P relay issued (sink present)
64+
bool rpc_ok = false; // submitauxblock accepted OR duplicate
65+
const char* landed_first = "none"; // "p2p" | "rpc" | "none"
66+
bool any() const { return p2p_sent || rpc_ok; }
67+
};
68+
69+
// Fire a won aux block down BOTH broadcast paths. `block_bytes` is the pre-
70+
// serialized NMC block blob the embedded P2P relay sends; `block_hash_hex` and
71+
// `auxpow_hex` are passed to the external submitauxblock fallback. `p2p_relay`
72+
// may be empty (no embedded sink yet); `aux_rpc` may be empty (no live RPC
73+
// client yet) -- each leg is guarded so the dispatcher never throws or
74+
// dereferences an unset sink. Never silent-drops a won block: with NEITHER sink
75+
// present it logs an ERROR (lost-subsidy scream) and returns any()==false.
76+
inline AuxBlockBroadcast broadcast_won_aux_block(const P2pRelaySink& p2p_relay,
77+
const AuxRpcSink& aux_rpc,
78+
const std::vector<unsigned char>& block_bytes,
79+
const std::string& block_hash_hex,
80+
const std::string& auxpow_hex)
81+
{
82+
AuxBlockBroadcast r;
83+
84+
// PRIMARY: embedded P2P relay (fastest propagation).
85+
if (p2p_relay) {
86+
p2p_relay(block_bytes);
87+
r.p2p_sent = true;
88+
r.landed_first = "p2p";
89+
LOG_INFO << "[EMB-NMC] won-aux-block P2P relay issued (" << block_bytes.size()
90+
<< " bytes) -- primary path.";
91+
} else {
92+
LOG_WARNING << "[EMB-NMC] won-aux-block: no embedded P2P sink; relying on submitauxblock RPC fallback.";
93+
}
94+
95+
// FALLBACK (always fired): external namecoind submitauxblock. A duplicate
96+
// after a P2P accept is success, not failure -- the sink uses ignore-failure
97+
// semantics so a duplicate/already-have never masks the P2P win.
98+
if (aux_rpc) {
99+
r.rpc_ok = aux_rpc(block_hash_hex, auxpow_hex);
100+
if (r.rpc_ok && !r.p2p_sent) r.landed_first = "rpc";
101+
LOG_INFO << "[EMB-NMC] won-aux-block submitauxblock RPC fallback "
102+
<< (r.rpc_ok ? "accept/duplicate" : "no-ack") << ".";
103+
} else {
104+
LOG_WARNING << "[EMB-NMC] won-aux-block: no external namecoind submitauxblock fallback sink wired.";
105+
}
106+
107+
if (!r.any())
108+
LOG_ERROR << "[EMB-NMC] won-aux-block had NEITHER broadcast sink -- block NOT relayed!";
109+
else
110+
LOG_INFO << "[EMB-NMC] won-aux-block broadcast: p2p=" << (r.p2p_sent ? "sent" : "off")
111+
<< " rpc=" << (r.rpc_ok ? "ok" : "off")
112+
<< " landed_first=" << r.landed_first << ".";
113+
return r;
114+
}
115+
116+
} // namespace coin
117+
} // namespace nmc

src/impl/nmc/test/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,23 @@ if (BUILD_TESTING AND GTest_FOUND)
6969
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
7070
target_link_libraries(nmc_mempool_name_test PRIVATE nmc_coin)
7171

72+
# PE item 3: dual-path won-aux-block broadcaster contract KAT. Header-only
73+
# dispatcher (coin/block_broadcast.hpp) -- pulls only core/log.hpp, so it
74+
# links the same SCC the sibling tests do (core resolves stratum_server refs
75+
# via the c2pool_* object libs). No nmc_coin needed (no transaction ctor).
76+
add_executable(nmc_block_broadcast_test nmc_block_broadcast_test.cpp)
77+
target_link_libraries(nmc_block_broadcast_test PRIVATE
78+
GTest::gtest_main GTest::gtest
79+
core nlohmann_json::nlohmann_json)
80+
target_link_libraries(nmc_block_broadcast_test PRIVATE
81+
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
82+
7283
include(GoogleTest)
7384
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
7485
gtest_add_tests(nmc_auxpow_merkle_test "" AUTO)
7586
gtest_add_tests(nmc_template_builder_test "" AUTO)
7687
gtest_add_tests(nmc_auxpow_wire_test "" AUTO)
7788
gtest_add_tests(nmc_reconstruct_won_block_test "" AUTO)
7889
gtest_add_tests(nmc_mempool_name_test "" AUTO)
90+
gtest_add_tests(nmc_block_broadcast_test "" AUTO)
7991
endif()
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// ---------------------------------------------------------------------------
2+
// nmc::coin::broadcast_won_aux_block dual-path dispatcher test (PE item 3,
3+
// broadcaster-gate dispatcher half).
4+
//
5+
// Offline contract slice mirroring src/impl/dgb/test/block_broadcast_test.cpp.
6+
// Locks the dual-path rule the NMC run-loop depends on when it wires the
7+
// won-aux-block trigger to this dispatcher: BOTH the embedded P2P relay
8+
// (PRIMARY) and the external namecoind submitauxblock (FALLBACK, fired ALWAYS)
9+
// are attempted, each leg is guarded, landed_first records the race winner, and
10+
// with NEITHER sink live the call is a safe no-op (any()=false, no throw, no
11+
// silent-drop of a won block). Uses std::function fakes so the dispatch
12+
// contract is verified without the live .140 namecoind submitauxblock client
13+
// (the next PE item-3 slice). p2pool-merged-v36 surface: NONE.
14+
//
15+
// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist
16+
// or it becomes a #137-style NOT_BUILT sentinel that reds master.
17+
// ---------------------------------------------------------------------------
18+
19+
#include <gtest/gtest.h>
20+
21+
#include <string>
22+
#include <vector>
23+
24+
#include "../coin/block_broadcast.hpp"
25+
26+
namespace {
27+
28+
const std::vector<unsigned char> kBytes(120, 0x42);
29+
const std::string kHashHex = "47589169f94e3e77bf4da8067e76b4417b021f0eb10760995671856f21b8d4b4";
30+
const std::string kAuxpowHex = "0011223344556677";
31+
32+
} // namespace
33+
34+
// 1) NEITHER sink live -> safe no-op: any()=false, landed_first="none", no throw,
35+
// an unset sink is never invoked (no silent-drop, the ERROR scream fires).
36+
TEST(NmcAuxBlockBroadcast, NoSinkLiveIsSafeNoOp) {
37+
auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, /*aux_rpc=*/{},
38+
kBytes, kHashHex, kAuxpowHex);
39+
EXPECT_FALSE(r.p2p_sent);
40+
EXPECT_FALSE(r.rpc_ok);
41+
EXPECT_FALSE(r.any());
42+
EXPECT_STREQ(r.landed_first, "none");
43+
}
44+
45+
// 2) P2P-only: relay sink present, no RPC -> p2p wins, fallback skipped cleanly.
46+
TEST(NmcAuxBlockBroadcast, P2pOnly) {
47+
bool relayed = false;
48+
std::vector<unsigned char> seen;
49+
auto relay = [&](const std::vector<unsigned char>& b) { relayed = true; seen = b; };
50+
51+
auto r = nmc::coin::broadcast_won_aux_block(relay, /*aux_rpc=*/{},
52+
kBytes, kHashHex, kAuxpowHex);
53+
EXPECT_TRUE(relayed);
54+
EXPECT_EQ(seen, kBytes);
55+
EXPECT_TRUE(r.p2p_sent);
56+
EXPECT_FALSE(r.rpc_ok);
57+
EXPECT_TRUE(r.any());
58+
EXPECT_STREQ(r.landed_first, "p2p");
59+
}
60+
61+
// 3) RPC-only: no embedded P2P sink, submitauxblock accepts -> fallback wins the
62+
// race, and it is handed the exact (hash, auxpow) payload.
63+
TEST(NmcAuxBlockBroadcast, RpcOnly) {
64+
int calls = 0;
65+
std::string saw_hash, saw_auxpow;
66+
auto aux_rpc = [&](const std::string& h, const std::string& a) {
67+
++calls; saw_hash = h; saw_auxpow = a; return true;
68+
};
69+
70+
auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, aux_rpc,
71+
kBytes, kHashHex, kAuxpowHex);
72+
EXPECT_FALSE(r.p2p_sent);
73+
EXPECT_TRUE(r.rpc_ok);
74+
EXPECT_TRUE(r.any());
75+
EXPECT_STREQ(r.landed_first, "rpc");
76+
EXPECT_EQ(calls, 1);
77+
EXPECT_EQ(saw_hash, kHashHex);
78+
EXPECT_EQ(saw_auxpow, kAuxpowHex);
79+
}
80+
81+
// 4) DUAL-PATH: both sinks live -> p2p wins landed_first, but the submitauxblock
82+
// RPC fallback STILL fires (the always-fire rule), and a duplicate accept
83+
// there is success, not a masked P2P win.
84+
TEST(NmcAuxBlockBroadcast, DualPathAlwaysFiresFallback) {
85+
bool relayed = false;
86+
auto relay = [&](const std::vector<unsigned char>&) { relayed = true; };
87+
int calls = 0;
88+
auto aux_rpc = [&](const std::string&, const std::string&) { ++calls; return true; };
89+
90+
auto r = nmc::coin::broadcast_won_aux_block(relay, aux_rpc,
91+
kBytes, kHashHex, kAuxpowHex);
92+
EXPECT_TRUE(relayed);
93+
EXPECT_TRUE(r.p2p_sent);
94+
EXPECT_TRUE(r.rpc_ok); // fallback fired even though P2P won
95+
EXPECT_EQ(calls, 1); // ALWAYS-fired, not a P2P-only catch
96+
EXPECT_STREQ(r.landed_first, "p2p"); // primary won the race
97+
}
98+
99+
// 5) Both sinks present but submitauxblock no-acks (rejected): p2p still carried
100+
// the block, rpc_ok=false, any()=true (the P2P leg succeeded).
101+
TEST(NmcAuxBlockBroadcast, FallbackNoAckDoesNotMaskP2p) {
102+
auto relay = [&](const std::vector<unsigned char>&) {};
103+
int calls = 0;
104+
auto aux_rpc = [&](const std::string&, const std::string&) { ++calls; return false; };
105+
106+
auto r = nmc::coin::broadcast_won_aux_block(relay, aux_rpc,
107+
kBytes, kHashHex, kAuxpowHex);
108+
EXPECT_TRUE(r.p2p_sent);
109+
EXPECT_FALSE(r.rpc_ok);
110+
EXPECT_TRUE(r.any());
111+
EXPECT_STREQ(r.landed_first, "p2p");
112+
EXPECT_EQ(calls, 1);
113+
}

0 commit comments

Comments
 (0)