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_witness_commitment_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 nmc_block_broadcast_test nmc_host_dualpath_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 nmc_host_dualpath_test nmc_fallback_path_conformance_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 dgb_compact_blocks_bip152_parity_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_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 \
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_witness_commitment_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 nmc_block_broadcast_test nmc_host_dualpath_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 nmc_host_dualpath_test nmc_fallback_path_conformance_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 dgb_compact_blocks_bip152_parity_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_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 \
Expand Down
14 changes: 14 additions & 0 deletions src/impl/nmc/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ if (BUILD_TESTING AND GTest_FOUND)
target_link_libraries(nmc_host_dualpath_test PRIVATE
c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage)

# PF (fallback-path conformance): locks that the RETAINED external-daemon
# fallback is the .140 namecoind submitauxblock RPC (a real daemon that can
# ack), NOT the embedded backend daemon-less submit_aux_block (structurally
# false). Mis-wiring the fallback to the embedded self-leg silently loses a
# won block when P2P is down -- this test makes that RED. Same header-only
# dispatcher SCC (core + c2pool_* object libs); no nmc_coin (no transaction ctor).
add_executable(nmc_fallback_path_conformance_test nmc_fallback_path_conformance_test.cpp)
target_link_libraries(nmc_fallback_path_conformance_test PRIVATE
GTest::gtest_main GTest::gtest
core nlohmann_json::nlohmann_json)
target_link_libraries(nmc_fallback_path_conformance_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)
Expand All @@ -102,4 +115,5 @@ if (BUILD_TESTING AND GTest_FOUND)
gtest_add_tests(nmc_mempool_name_test "" AUTO)
gtest_add_tests(nmc_block_broadcast_test "" AUTO)
gtest_add_tests(nmc_host_dualpath_test "" AUTO)
gtest_add_tests(nmc_fallback_path_conformance_test "" AUTO)
endif()
126 changes: 126 additions & 0 deletions src/impl/nmc/test/nmc_fallback_path_conformance_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
// ---------------------------------------------------------------------------
// nmc PF fallback-path conformance lock (Phase PF, external-daemon-fallback half).
//
// V36 per-coin invariant (v36-master-plan external_fallback): "an EXTERNAL-DAEMON
// FALLBACK (RPC to a real coind / MM-Adapter) MUST PERSIST in every coin's c2pool
// -- embedded is default, external is the RETAINED fallback path. Do NOT remove
// external-daemon code paths." For NMC the retained fallback is the .140 testnet
// namecoind `submitauxblock` RPC, bound by the run-loop as the AuxRpcSink.
//
// NET-NEW vs nmc_block_broadcast_test (dispatcher contract) and
// nmc_host_dualpath_test (host wiring DELIVERY): both of those bind the fallback
// leg to a fake that ALWAYS ACKS, so neither distinguishes WHICH backend the
// fallback is. This file locks the one conformance fact PF owns: the fallback
// MUST be the EXTERNAL namecoind RPC (a real daemon that can ack), and MUST NOT
// be the embedded backend's own submit_aux_block -- which is structurally
// daemon-less and returns false unconditionally (aux_chain_embedded.hpp:153,
// "no daemon to submit to; embedded RPC leg cannot broadcast, returning false").
//
// The trap: a refactor that "simplifies" set_fallback_backend to point at the
// embedded AuxChainEmbedded::submit_aux_block leaves the wiring LOOKING present
// (a sink is bound, nmc_host_dualpath stays green on its always-ack fake) but
// turns the retained fallback into a dead path: when the embedded P2P relay is
// down, a won aux block is then SILENTLY LOST. This file makes that regression
// RED. Test-only, std::function fakes, zero consensus surface (no PoW hash,
// share format, aux commitment, template, or PPLNS math). p2pool-merged-v36
// surface: NONE. Per-coin isolation: src/impl/nmc/ only; consumes
// coin/block_broadcast.hpp (core/log.hpp only) -- pulls no btc/ or dgb/ symbol.
//
// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist
// or it becomes a #137/#143-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";

// Models c2pool::merged::AuxChainRPC -- the EXTERNAL .140 namecoind submitauxblock
// client the run-loop binds via set_fallback_backend. A real daemon ACKS (true)
// on accept OR harmless duplicate (merged_mining.cpp:454). This is the RETAINED
// external fallback the V36 invariant requires.
struct ExternalNamecoindRpc {
int calls = 0;
bool submit_aux_block(const std::string&, const std::string&) { ++calls; return true; }
};

// Models AuxChainEmbedded::submit_aux_block (aux_chain_embedded.hpp:153): the
// EMBEDDED backend has no daemon to RPC, so its RPC leg cannot broadcast and
// returns false UNCONDITIONALLY. Wiring THIS as the fallback is the conformance
// violation -- it is not the external daemon, it is the embedded self-leg.
struct EmbeddedSelfRpcLeg {
int calls = 0;
bool submit_aux_block(const std::string&, const std::string&) { ++calls; return false; }
};

} // namespace

// 1) RETAINED EXTERNAL FALLBACK CARRIES when the embedded P2P relay is down.
// The external namecoind submitauxblock saves the won aux block: rpc_ok=true,
// any()=true, it won the race. This is the path the V36 external_fallback
// invariant says must PERSIST.
TEST(NmcPfFallbackPath, ExternalDaemonFallbackCarriesWhenEmbeddedP2pDown) {
ExternalNamecoindRpc ext;
nmc::coin::AuxRpcSink rpc = [&](const std::string& h, const std::string& a) {
return ext.submit_aux_block(h, a);
};

// P2P relay sink empty == embedded relay down / no peers route.
auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, rpc,
kBytes, kHashHex, kAuxpowHex);
EXPECT_FALSE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok); // external daemon acked
EXPECT_TRUE(r.any()); // won block NOT lost
EXPECT_STREQ(r.landed_first, "rpc");
EXPECT_EQ(ext.calls, 1);
}

// 2) CONFORMANCE LOCK: the embedded backend's own submit_aux_block CANNOT serve
// as the fallback. It is daemon-less and returns false unconditionally, so if
// a refactor mis-wires set_fallback_backend to it, then with the embedded P2P
// relay also down the won aux block is SILENTLY LOST (any()=false). This is
// exactly the regression the external_fallback invariant forbids; making it
// RED is the whole point of PF's fallback-path lock.
TEST(NmcPfFallbackPath, EmbeddedSelfLegCannotServeAsFallback_BlockLost) {
EmbeddedSelfRpcLeg self;
nmc::coin::AuxRpcSink mis_wired = [&](const std::string& h, const std::string& a) {
return self.submit_aux_block(h, a);
};

auto r = nmc::coin::broadcast_won_aux_block(/*p2p_relay=*/{}, mis_wired,
kBytes, kHashHex, kAuxpowHex);
EXPECT_EQ(self.calls, 1); // the leg was consulted...
EXPECT_FALSE(r.rpc_ok); // ...but structurally cannot ack
EXPECT_FALSE(r.p2p_sent);
EXPECT_FALSE(r.any()); // WON BLOCK LOST -- the forbidden state
EXPECT_STREQ(r.landed_first, "none");
}

// 3) PERSISTENCE: the external fallback must stay wired ALONGSIDE a working
// embedded P2P relay, not be optimized away "because embedded works". With
// both present the external submitauxblock STILL fires (always-fire rule),
// so deleting it to "save an RPC" flips calls 1->0 and removes the retained
// safety net -- the regression this asserts against.
TEST(NmcPfFallbackPath, ExternalFallbackPersistsAlongsideWorkingEmbeddedP2p) {
bool relayed = false;
nmc::coin::P2pRelaySink p2p = [&](const std::vector<unsigned char>&) { relayed = true; };
ExternalNamecoindRpc ext;
nmc::coin::AuxRpcSink rpc = [&](const std::string& h, const std::string& a) {
return ext.submit_aux_block(h, a);
};

auto r = nmc::coin::broadcast_won_aux_block(p2p, rpc, kBytes, kHashHex, kAuxpowHex);
EXPECT_TRUE(relayed);
EXPECT_TRUE(r.p2p_sent);
EXPECT_TRUE(r.rpc_ok); // retained fallback fired anyway
EXPECT_EQ(ext.calls, 1); // ALWAYS-fired, not skipped because P2P won
EXPECT_STREQ(r.landed_first, "p2p"); // primary still won the race
}
Loading