Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
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 \
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 \
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 \
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 \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
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_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test v37_test \
Expand Down Expand Up @@ -213,7 +213,7 @@ jobs:
test_mweb_builder \
test_address_resolution test_compute_share_target \
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 \
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 \
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 \
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 \
rpc_request_test softfork_check_test genesis_check_test algo_select_test digishield_walk_test header_chain_test \
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_efficiency_test dgb_think_p5_best_share_punish_test dgb_auto_ratchet_tail_guard_test test_coin_broadcaster test_multiaddress_pplns test_pplns_stress \
Expand Down
117 changes: 117 additions & 0 deletions src/impl/nmc/coin/block_broadcast.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#pragma once
// ---------------------------------------------------------------------------
// nmc::coin::broadcast_won_aux_block -- the NMC (Namecoin) dual-path won-block
// dispatcher for a merge-mined aux block under a BTC SHA256d parent.
//
// 1:1 contract mirror of dgb::coin::broadcast_won_block (block dispatch half)
// and bch::coin::EmbeddedDaemon::broadcast_won_block. When a found share's
// parent BTC PoW also satisfies the NMC aux-network target, a REAL Namecoin
// block (parent header + AuxPow proof) is formed and MUST reach the Namecoin
// network. Per the broadcaster-gate dual-path rule it is fired down BOTH sinks:
// PRIMARY : embedded P2P relay to namecoin peers (fastest propagation),
// supplied by the run-loop as a sink fn -- the same authoritative
// embedded route AuxChainEmbedded::submit_block() drives today.
// FALLBACK : external namecoind `submitauxblock` RPC (to the .140 testnet
// namecoind), fired ALWAYS, NOT a P2P-only path with RPC as a
// catch. submitauxblock of an already-accepted aux block returns a
// harmless rejection at the daemon, so a duplicate after a P2P
// accept still proves the RPC path reached the node; ignore-failure
// semantics mean that duplicate must never mask the P2P win.
//
// This is the sink the NMC run-loop wires the won-aux-block trigger to so an
// in-operation win emits immediately. Decoupled from the (live) namecoind RPC
// client by taking BOTH legs as std::function sinks: the dispatch CONTRACT is
// build-verifiable and KAT-locked now, and the run-loop binds the live P2P
// relay + the .140 submitauxblock client when they are stood up (PE item 3
// live-client slice; the soak gate, item 4, follows once the RPC leg is live).
//
// Standardized v36-native shape: identical dual-path contract to dgb/bch so the
// v37 multichain migration is a clean lift, not a reconciliation of divergent
// dialects. Per-coin isolation: src/impl/nmc/ only; consumes core/* only, pulls
// no btc/ or dgb/ symbol. p2pool-merged-v36 surface: NONE -- block dispatch
// only; no PoW hash, share format, aux commitment, or PPLNS math is touched.
// ---------------------------------------------------------------------------

#include <cstdint>
#include <functional>
#include <string>
#include <vector>

#include <core/log.hpp>

namespace nmc
{
namespace coin
{

// Embedded P2P relay sink: the run-loop binds this to the namecoin P2P relay
// (the embedded submit_block route). Empty == no embedded P2P sink present.
using P2pRelaySink = std::function<void(const std::vector<unsigned char>&)>;

// External submitauxblock RPC sink: the run-loop binds this to the live .140
// testnet namecoind submitauxblock client. Takes the NMC block hash hex and the
// serialized AuxPow hex; returns true on daemon accept OR harmless duplicate.
// Empty == no RPC fallback sink wired.
using AuxRpcSink =
std::function<bool(const std::string& block_hash_hex, const std::string& auxpow_hex)>;

// Outcome of a won-aux-block broadcast: which sinks fired and whether the
// external namecoind acked. P2P is primary; the submitauxblock RPC fallback is
// fired ALWAYS (dual-path rule). landed_first records which path won the race.
struct AuxBlockBroadcast
{
bool p2p_sent = false; // embedded P2P relay issued (sink present)
bool rpc_ok = false; // submitauxblock accepted OR duplicate
const char* landed_first = "none"; // "p2p" | "rpc" | "none"
bool any() const { return p2p_sent || rpc_ok; }
};

// Fire a won aux block down BOTH broadcast paths. `block_bytes` is the pre-
// serialized NMC block blob the embedded P2P relay sends; `block_hash_hex` and
// `auxpow_hex` are passed to the external submitauxblock fallback. `p2p_relay`
// may be empty (no embedded sink yet); `aux_rpc` may be empty (no live RPC
// client yet) -- each leg is guarded so the dispatcher never throws or
// dereferences an unset sink. Never silent-drops a won block: with NEITHER sink
// present it logs an ERROR (lost-subsidy scream) and returns any()==false.
inline AuxBlockBroadcast broadcast_won_aux_block(const P2pRelaySink& p2p_relay,
const AuxRpcSink& aux_rpc,
const std::vector<unsigned char>& block_bytes,
const std::string& block_hash_hex,
const std::string& auxpow_hex)
{
AuxBlockBroadcast r;

// PRIMARY: embedded P2P relay (fastest propagation).
if (p2p_relay) {
p2p_relay(block_bytes);
r.p2p_sent = true;
r.landed_first = "p2p";
LOG_INFO << "[EMB-NMC] won-aux-block P2P relay issued (" << block_bytes.size()
<< " bytes) -- primary path.";
} else {
LOG_WARNING << "[EMB-NMC] won-aux-block: no embedded P2P sink; relying on submitauxblock RPC fallback.";
}

// FALLBACK (always fired): external namecoind submitauxblock. A duplicate
// after a P2P accept is success, not failure -- the sink uses ignore-failure
// semantics so a duplicate/already-have never masks the P2P win.
if (aux_rpc) {
r.rpc_ok = aux_rpc(block_hash_hex, auxpow_hex);
if (r.rpc_ok && !r.p2p_sent) r.landed_first = "rpc";
LOG_INFO << "[EMB-NMC] won-aux-block submitauxblock RPC fallback "
<< (r.rpc_ok ? "accept/duplicate" : "no-ack") << ".";
} else {
LOG_WARNING << "[EMB-NMC] won-aux-block: no external namecoind submitauxblock fallback sink wired.";
}

if (!r.any())
LOG_ERROR << "[EMB-NMC] won-aux-block had NEITHER broadcast sink -- block NOT relayed!";
else
LOG_INFO << "[EMB-NMC] won-aux-block broadcast: p2p=" << (r.p2p_sent ? "sent" : "off")
<< " rpc=" << (r.rpc_ok ? "ok" : "off")
<< " landed_first=" << r.landed_first << ".";
return r;
}

} // namespace coin
} // namespace nmc
12 changes: 12 additions & 0 deletions src/impl/nmc/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,23 @@ if (BUILD_TESTING AND GTest_FOUND)
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)
target_link_libraries(nmc_mempool_name_test PRIVATE nmc_coin)

# PE item 3: dual-path won-aux-block broadcaster contract KAT. Header-only
# dispatcher (coin/block_broadcast.hpp) -- pulls only core/log.hpp, so it
# links the same SCC the sibling tests do (core resolves stratum_server refs
# via the c2pool_* object libs). No nmc_coin needed (no transaction ctor).
add_executable(nmc_block_broadcast_test nmc_block_broadcast_test.cpp)
target_link_libraries(nmc_block_broadcast_test PRIVATE
GTest::gtest_main GTest::gtest
core nlohmann_json::nlohmann_json)
target_link_libraries(nmc_block_broadcast_test PRIVATE
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)

include(GoogleTest)
include_directories(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR})
gtest_add_tests(nmc_auxpow_merkle_test "" AUTO)
gtest_add_tests(nmc_template_builder_test "" AUTO)
gtest_add_tests(nmc_auxpow_wire_test "" AUTO)
gtest_add_tests(nmc_reconstruct_won_block_test "" AUTO)
gtest_add_tests(nmc_mempool_name_test "" AUTO)
gtest_add_tests(nmc_block_broadcast_test "" AUTO)
endif()
113 changes: 113 additions & 0 deletions src/impl/nmc/test/nmc_block_broadcast_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// ---------------------------------------------------------------------------
// nmc::coin::broadcast_won_aux_block dual-path dispatcher test (PE item 3,
// broadcaster-gate dispatcher half).
//
// Offline contract slice mirroring src/impl/dgb/test/block_broadcast_test.cpp.
// Locks the dual-path rule the NMC run-loop depends on when it wires the
// won-aux-block trigger to this dispatcher: BOTH the embedded P2P relay
// (PRIMARY) and the external namecoind submitauxblock (FALLBACK, fired ALWAYS)
// are attempted, each leg is guarded, landed_first records the race winner, and
// with NEITHER sink live the call is a safe no-op (any()=false, no throw, no
// silent-drop of a won block). Uses std::function fakes so the dispatch
// contract is verified without the live .140 namecoind submitauxblock client
// (the next PE item-3 slice). p2pool-merged-v36 surface: NONE.
//
// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist
// or it becomes a #137-style NOT_BUILT sentinel that reds master.
// ---------------------------------------------------------------------------

#include <gtest/gtest.h>

#include <string>
#include <vector>

#include "../coin/block_broadcast.hpp"

namespace {

const std::vector<unsigned char> kBytes(120, 0x42);
const std::string kHashHex = "47589169f94e3e77bf4da8067e76b4417b021f0eb10760995671856f21b8d4b4";
const std::string kAuxpowHex = "0011223344556677";

} // namespace

// 1) NEITHER sink live -> safe no-op: any()=false, landed_first="none", no throw,
// an unset sink is never invoked (no silent-drop, the ERROR scream fires).
TEST(NmcAuxBlockBroadcast, NoSinkLiveIsSafeNoOp) {
auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, /*aux_rpc=*/{},
kBytes, kHashHex, kAuxpowHex);
EXPECT_FALSE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_FALSE(r.any());
EXPECT_STREQ(r.landed_first, "none");
}

// 2) P2P-only: relay sink present, no RPC -> p2p wins, fallback skipped cleanly.
TEST(NmcAuxBlockBroadcast, P2pOnly) {
bool relayed = false;
std::vector<unsigned char> seen;
auto relay = [&](const std::vector<unsigned char>& b) { relayed = true; seen = b; };

auto r = nmc::coin::broadcast_won_aux_block(relay, /*aux_rpc=*/{},
kBytes, kHashHex, kAuxpowHex);
EXPECT_TRUE(relayed);
EXPECT_EQ(seen, kBytes);
EXPECT_TRUE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "p2p");
}

// 3) RPC-only: no embedded P2P sink, submitauxblock accepts -> fallback wins the
// race, and it is handed the exact (hash, auxpow) payload.
TEST(NmcAuxBlockBroadcast, RpcOnly) {
int calls = 0;
std::string saw_hash, saw_auxpow;
auto aux_rpc = [&](const std::string& h, const std::string& a) {
++calls; saw_hash = h; saw_auxpow = a; return true;
};

auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, aux_rpc,
kBytes, kHashHex, kAuxpowHex);
EXPECT_FALSE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "rpc");
EXPECT_EQ(calls, 1);
EXPECT_EQ(saw_hash, kHashHex);
EXPECT_EQ(saw_auxpow, kAuxpowHex);
}

// 4) DUAL-PATH: both sinks live -> p2p wins landed_first, but the submitauxblock
// RPC fallback STILL fires (the always-fire rule), and a duplicate accept
// there is success, not a masked P2P win.
TEST(NmcAuxBlockBroadcast, DualPathAlwaysFiresFallback) {
bool relayed = false;
auto relay = [&](const std::vector<unsigned char>&) { relayed = true; };
int calls = 0;
auto aux_rpc = [&](const std::string&, const std::string&) { ++calls; return true; };

auto r = nmc::coin::broadcast_won_aux_block(relay, aux_rpc,
kBytes, kHashHex, kAuxpowHex);
EXPECT_TRUE(relayed);
EXPECT_TRUE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok); // fallback fired even though P2P won
EXPECT_EQ(calls, 1); // ALWAYS-fired, not a P2P-only catch
EXPECT_STREQ(r.landed_first, "p2p"); // primary won the race
}

// 5) Both sinks present but submitauxblock no-acks (rejected): p2p still carried
// the block, rpc_ok=false, any()=true (the P2P leg succeeded).
TEST(NmcAuxBlockBroadcast, FallbackNoAckDoesNotMaskP2p) {
auto relay = [&](const std::vector<unsigned char>&) {};
int calls = 0;
auto aux_rpc = [&](const std::string&, const std::string&) { ++calls; return false; };

auto r = nmc::coin::broadcast_won_aux_block(relay, aux_rpc,
kBytes, kHashHex, kAuxpowHex);
EXPECT_TRUE(r.p2p_sent);
EXPECT_FALSE(r.rpc_ok);
EXPECT_TRUE(r.any());
EXPECT_STREQ(r.landed_first, "p2p");
EXPECT_EQ(calls, 1);
}
Loading