diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 71130d3f9..226201d29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -82,7 +82,7 @@ jobs: 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 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_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_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_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_test dgb_aux_broadcast_path_election_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 \ -j$(nproc) @@ -214,7 +214,7 @@ jobs: 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 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_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_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_mm_commitment_test dgb_aux_doge_dc_proof_test dgb_aux_doge_bind_parsers_test dgb_compact_blocks_bip152_parity_test dgb_aux_dual_target_select_test dgb_aux_broadcast_path_election_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 \ v37_test \ diff --git a/src/impl/dgb/coin/aux_broadcast_path_election.hpp b/src/impl/dgb/coin/aux_broadcast_path_election.hpp new file mode 100644 index 000000000..3dc8d164b --- /dev/null +++ b/src/impl/dgb/coin/aux_broadcast_path_election.hpp @@ -0,0 +1,112 @@ +// --------------------------------------------------------------------------- +// DGB+DOGE (phase DC) — broadcast-path ELECTION + single-fire ledger. +// +// Fenced / test-only contract. Pins the HARD precondition the integrator set +// for any DC live-wire (UID2478): the submitauxblock RPC fallback MUST NOT +// double-fire the same DOGE block hash alongside the embedded submit path. +// Both paths must be mutually exclusive PER WON BLOCK. +// +// This complements coin/aux_dual_target_select.hpp (#490), which decides WHICH +// chain a scrypt pow_hash hits. This header decides, once a DOGE-aux win +// exists, WHICH broadcast path carries it to the network — and proves exactly +// one ever does. +// +// Two orthogonal guards compose into single-fire: +// +// ELECTION (mutual exclusion AT one decision point): the embedded +// submit_block path (frozen fully-assembled block -> P2P relay) is PRIMARY, +// mirroring aux_chain_embedded.hpp where submit_block is the primary method +// and submit_aux_block is the "no daemon to submit to" fallback. RPC +// submitauxblock is elected ONLY when no embedded relay is available. A win +// must be present for any path to fire. The function is total and returns +// exactly one AuxBroadcastPath — never "both". +// +// LEDGER (mutual exclusion ACROSS TIME / retries): even with a correct +// election, a retry or a both-paths-attempted race could re-broadcast the +// same DOGE block hash. AuxBroadcastLedger::try_fire records a hash on its +// first authorization and suppresses every later attempt for that hash, +// regardless of which path attempts it. This is the idempotent guard the +// integrator named ("idempotent guard or path-election before broadcast"). +// +// Pure helpers; links only core (uint256) + . Consumes nothing in +// src/impl/doge and touches no node seam (the seam stays parked on the live +// DC slice). Non-circular: the primary-embedded / fallback-RPC posture is +// restated by value here, mirroring aux_chain_embedded.hpp's documented method +// roles without including it. +// --------------------------------------------------------------------------- +#pragma once + +#include +#include + +namespace dgb +{ +namespace coin +{ + +// The broadcast carrier for a won DOGE-aux block. Exactly one is ever elected. +enum class AuxBroadcastPath +{ + None, // no win, or no carrier available + EmbeddedSubmitBlock, // PRIMARY: frozen block via embedded P2P relay + RpcSubmitAuxBlock, // FALLBACK: external daemon submitauxblock RPC +}; + +// Path-election. Total function: returns exactly one path. Embedded is primary; +// RPC fallback only when embedded relay is unavailable; None when there is no +// win or no carrier at all. By construction the two fire-paths are mutually +// exclusive — there is no input for which both EmbeddedSubmitBlock and +// RpcSubmitAuxBlock are "selected", because the result is a single enum value. +inline AuxBroadcastPath elect_aux_broadcast_path(bool have_doge_aux_win, + bool embedded_relay_available, + bool rpc_submit_available) +{ + if (!have_doge_aux_win) return AuxBroadcastPath::None; + if (embedded_relay_available) return AuxBroadcastPath::EmbeddedSubmitBlock; + if (rpc_submit_available) return AuxBroadcastPath::RpcSubmitAuxBlock; + return AuxBroadcastPath::None; +} + +// Idempotent single-fire ledger keyed on the DOGE block hash. try_fire returns +// true EXACTLY ONCE per distinct hash (first call records + authorizes); every +// later call for the same hash returns false (double-fire suppressed), +// independent of which path attempts it. Distinct hashes are independent. +class AuxBroadcastLedger +{ +public: + // Authorize-and-record. true => fresh, caller should broadcast. + // false => this hash was already broadcast; suppress. + bool try_fire(const uint256& doge_block_hash) + { + return m_fired.insert(doge_block_hash).second; + } + + bool already_fired(const uint256& doge_block_hash) const + { + return m_fired.find(doge_block_hash) != m_fired.end(); + } + +private: + std::set m_fired; +}; + +// Convenience composition: elect a path AND consult the ledger in one call. +// Returns the path that should actually broadcast — None if no win, no carrier, +// OR the hash was already fired. On a non-None return the hash is recorded, so +// a subsequent call with the same hash yields None (single-fire guaranteed even +// if BOTH carrier conditions are true on the retry). +inline AuxBroadcastPath elect_and_claim(AuxBroadcastLedger& ledger, + const uint256& doge_block_hash, + bool have_doge_aux_win, + bool embedded_relay_available, + bool rpc_submit_available) +{ + const AuxBroadcastPath path = elect_aux_broadcast_path( + have_doge_aux_win, embedded_relay_available, rpc_submit_available); + if (path == AuxBroadcastPath::None) return AuxBroadcastPath::None; + if (!ledger.try_fire(doge_block_hash)) return AuxBroadcastPath::None; + return path; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/test/CMakeLists.txt b/src/impl/dgb/test/CMakeLists.txt index c7e987a09..dbdcd5137 100644 --- a/src/impl/dgb/test/CMakeLists.txt +++ b/src/impl/dgb/test/CMakeLists.txt @@ -635,6 +635,23 @@ if (BUILD_TESTING AND GTest_FOUND) dgb_coin pool sharechain) gtest_add_tests(dgb_aux_dual_target_select_test "" AUTO) + # --- DGB+DOGE (phase DC) -- broadcast-path single-fire PROOF KAT ---------- + # Proves the integrator HARD precondition (UID2478): the submitauxblock RPC + # fallback cannot double-fire the same DOGE block hash alongside the embedded + # submit path. Pins coin/aux_broadcast_path_election.hpp -- election returns + # exactly one carrier per won block (embedded primary / RPC fallback) and the + # idempotent ledger suppresses any second broadcast of an already-fired hash. + # Pure helpers, consumes nothing in src/impl/doge, touches no node seam. MUST + # appear in BOTH this registration AND the build.yml --target allowlist + # (#143 NOT_BUILT trap). + add_executable(dgb_aux_broadcast_path_election_test aux_broadcast_path_election_test.cpp) + target_link_libraries(dgb_aux_broadcast_path_election_test PRIVATE + GTest::gtest_main GTest::gtest + core dgb + c2pool_payout c2pool_merged_mining c2pool_hashrate c2pool_storage + dgb_coin pool sharechain) + gtest_add_tests(dgb_aux_broadcast_path_election_test "" AUTO) + # dgb_share_target_genesis_test: FENCED KAT pinning ShareTracker::compute_ # share_target genesis/unknown-prev clamp to [MAX_TARGET/30, MAX_TARGET] vs # the DGB oracle data.py generate_transaction. Links the same proven set as diff --git a/src/impl/dgb/test/aux_broadcast_path_election_test.cpp b/src/impl/dgb/test/aux_broadcast_path_election_test.cpp new file mode 100644 index 000000000..56cf52fe3 --- /dev/null +++ b/src/impl/dgb/test/aux_broadcast_path_election_test.cpp @@ -0,0 +1,138 @@ +// --------------------------------------------------------------------------- +// DGB+DOGE (phase DC) — broadcast-path single-fire proof KAT. +// +// Fenced / test-only. Positively PROVES the integrator's HARD precondition for +// any DC live-wire (UID2478): the submitauxblock RPC fallback cannot double- +// fire the same DOGE block hash alongside the embedded submit path. Proven two +// ways that together cover the race: +// (A) ELECTION returns exactly one carrier per won block (never both), with +// embedded primary / RPC fallback ordering (mirrors aux_chain_embedded +// .hpp submit_block-primary / submit_aux_block-fallback roles). +// (B) the idempotent LEDGER suppresses any second broadcast of an already- +// fired hash, regardless of path — so a retry or both-paths-attempted +// race yields exactly ONE network broadcast. +// +// Non-circular: every golden is restated by value here. Pure helpers; links +// only core (uint256). Consumes nothing in src/impl/doge, touches no node seam. +// MUST appear in BOTH test/CMakeLists.txt AND the build.yml --target allowlist +// (#143 NOT_BUILT sentinel trap). +// --------------------------------------------------------------------------- + +#include + +#include +#include + +using dgb::coin::AuxBroadcastPath; +using dgb::coin::AuxBroadcastLedger; +using dgb::coin::elect_aux_broadcast_path; +using dgb::coin::elect_and_claim; + +namespace { +uint256 H(uint64_t n) { uint256 h; h.SetNull(); *h.begin() = static_cast(n & 0xff); *(h.begin() + 1) = static_cast((n >> 8) & 0xff); return h; } +} + +// ---- (A) ELECTION truth table: exactly one carrier per won block ----------- + +// No win -> no carrier on EITHER path, whatever the carrier availability. +TEST(DGB_AuxBroadcastElection, NoWinNeverFires) { + for (int e = 0; e < 2; ++e) + for (int r = 0; r < 2; ++r) + EXPECT_EQ(elect_aux_broadcast_path(/*win*/false, e, r), + AuxBroadcastPath::None); +} + +// Win + embedded relay available -> ALWAYS embedded (primary), even if RPC is +// also available. This is the core mutual-exclusion: RPC is never co-selected. +TEST(DGB_AuxBroadcastElection, EmbeddedIsPrimaryWhenAvailable) { + EXPECT_EQ(elect_aux_broadcast_path(true, /*emb*/true, /*rpc*/true), + AuxBroadcastPath::EmbeddedSubmitBlock); + EXPECT_EQ(elect_aux_broadcast_path(true, /*emb*/true, /*rpc*/false), + AuxBroadcastPath::EmbeddedSubmitBlock); +} + +// Win + no embedded relay + RPC available -> RPC fallback (and ONLY then). +TEST(DGB_AuxBroadcastElection, RpcOnlyAsFallback) { + EXPECT_EQ(elect_aux_broadcast_path(true, /*emb*/false, /*rpc*/true), + AuxBroadcastPath::RpcSubmitAuxBlock); +} + +// Win + no carrier at all -> None (cannot fabricate a broadcast). +TEST(DGB_AuxBroadcastElection, NoCarrierNoFire) { + EXPECT_EQ(elect_aux_broadcast_path(true, /*emb*/false, /*rpc*/false), + AuxBroadcastPath::None); +} + +// Exhaustive: across the full 2x2x2 input cube the result is a single enum +// value — there is structurally no input that yields "both paths". +TEST(DGB_AuxBroadcastElection, ResultIsAlwaysSinglePath) { + for (int w = 0; w < 2; ++w) + for (int e = 0; e < 2; ++e) + for (int r = 0; r < 2; ++r) { + AuxBroadcastPath p = elect_aux_broadcast_path(w, e, r); + const bool is_embedded = (p == AuxBroadcastPath::EmbeddedSubmitBlock); + const bool is_rpc = (p == AuxBroadcastPath::RpcSubmitAuxBlock); + EXPECT_FALSE(is_embedded && is_rpc); // never both + if (!w) EXPECT_FALSE(is_embedded || is_rpc); + } +} + +// ---- (B) idempotent LEDGER: one broadcast per distinct hash ---------------- + +TEST(DGB_AuxBroadcastLedger, FirstFireFreshSecondSuppressed) { + AuxBroadcastLedger ledger; + const uint256 h = H(0xABCD); + EXPECT_FALSE(ledger.already_fired(h)); + EXPECT_TRUE (ledger.try_fire(h)); // first authorization + EXPECT_TRUE (ledger.already_fired(h)); + EXPECT_FALSE(ledger.try_fire(h)); // double-fire suppressed + EXPECT_FALSE(ledger.try_fire(h)); // and stays suppressed +} + +TEST(DGB_AuxBroadcastLedger, DistinctHashesAreIndependent) { + AuxBroadcastLedger ledger; + EXPECT_TRUE(ledger.try_fire(H(1))); + EXPECT_TRUE(ledger.try_fire(H(2))); + EXPECT_FALSE(ledger.try_fire(H(1))); // h1 already fired + EXPECT_TRUE(ledger.try_fire(H(3))); +} + +// ---- (A)+(B) end-to-end single-fire: both paths race the SAME hash --------- + +// The integrator's exact scenario: the same won DOGE block is attempted via the +// embedded path AND the RPC fallback. elect_and_claim authorizes exactly ONE; +// the second attempt (different carrier availability, same hash) yields None. +TEST(DGB_AuxBroadcastSingleFire, EmbeddedThenRpcSameHashFiresOnce) { + AuxBroadcastLedger ledger; + const uint256 won = H(0x5151); + + // Embedded path wins the election and claims the hash. + EXPECT_EQ(elect_and_claim(ledger, won, /*win*/true, /*emb*/true, /*rpc*/true), + AuxBroadcastPath::EmbeddedSubmitBlock); + + // RPC fallback later attempts the SAME hash (e.g. daemon retry) — suppressed. + EXPECT_EQ(elect_and_claim(ledger, won, /*win*/true, /*emb*/false, /*rpc*/true), + AuxBroadcastPath::None); +} + +// Symmetric: RPC fires first, embedded retry suppressed — order-independent. +TEST(DGB_AuxBroadcastSingleFire, RpcThenEmbeddedSameHashFiresOnce) { + AuxBroadcastLedger ledger; + const uint256 won = H(0x6262); + + EXPECT_EQ(elect_and_claim(ledger, won, /*win*/true, /*emb*/false, /*rpc*/true), + AuxBroadcastPath::RpcSubmitAuxBlock); + EXPECT_EQ(elect_and_claim(ledger, won, /*win*/true, /*emb*/true, /*rpc*/true), + AuxBroadcastPath::None); +} + +// A no-win claim never consumes the ledger, so a later genuine win still fires. +TEST(DGB_AuxBroadcastSingleFire, NoWinDoesNotBurnTheHash) { + AuxBroadcastLedger ledger; + const uint256 h = H(0x7777); + EXPECT_EQ(elect_and_claim(ledger, h, /*win*/false, true, true), + AuxBroadcastPath::None); + EXPECT_FALSE(ledger.already_fired(h)); // not recorded + EXPECT_EQ(elect_and_claim(ledger, h, /*win*/true, true, true), + AuxBroadcastPath::EmbeddedSubmitBlock); // genuine win still fires +}