diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 494093623..6ce39eefa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -102,7 +102,7 @@ jobs: core_test sharechain_test share_test btc_share_test \ test_threading test_weights \ test_header_chain test_mempool test_template_builder \ - test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate \ + test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state \ test_multiaddress_pplns test_pplns_stress \ test_hash_link test_decay_pplns \ test_pplns_consensus \ @@ -252,7 +252,7 @@ jobs: test_threading test_weights \ test_header_chain test_mempool test_template_builder \ test_doge_chain test_compact_blocks test_dash_x11_kat \ - test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate \ + test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state \ test_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/src/c2pool/main_dash.cpp b/src/c2pool/main_dash.cpp index f2a201407..b26980396 100644 --- a/src/c2pool/main_dash.cpp +++ b/src/c2pool/main_dash.cpp @@ -50,6 +50,7 @@ #include // NetService (dashd RPC endpoint) #include // dash::coin::NodeRPC — external-dashd submitblock arm (slice 3) +#include // dash::coin::select_dash_work -- embedded-gbt live-wire + dashd fallback (S8 capstone) #include // dash.conf creds resolution (rpcpassword off argv) #include #include // dash::Node — sharechain pool-node (NodeBridge) @@ -457,7 +458,25 @@ int run_mine_block(bool testnet, const std::string& rpc_endpoint, // 1) Pull the template. std::cout << "[mine] fetching block template (getblocktemplate)...\n"; - dash::coin::DashWorkData work = rpc.getwork(); + // Work source (S8 embedded_gbt live-wire capstone): PREFER the locally + // assembled embedded template via build_embedded_workdata, fall back to + // dashd getblocktemplate. The dashd arm is RETAINED as fallback + the + // [GBT-XCHECK] cross-check -- never removed. + // + // NOTE: NodeImpl does not yet hold the embedded coin-state (masternode + // list + mempool + header tip) that build_embedded_workdata consumes, so + // emb.has_state stays false here and the selector routes to the dashd + // fallback today. Populating that in-process state is the flagged next + // sub-slice; once it lands, set emb.has_state=true and the embedded arm + // goes live with zero change to this call site. + dash::coin::EmbeddedWorkInputs emb; // has_state=false until node-held coin-state lands + dash::coin::WorkSelection sel = + dash::coin::select_dash_work(emb, [&]{ return rpc.getwork(); }); + dash::coin::DashWorkData work = std::move(sel.work); + std::cout << "[mine] work source: " + << (sel.source == dash::coin::WorkSource::Embedded + ? "EMBEDDED (build_embedded_workdata)" + : "dashd getblocktemplate (fallback)") << "\n"; std::cout << "[mine] template: height=" << work.m_height << " bits=0x" << std::hex << work.m_bits << std::dec << " prev=" << work.m_previous_block.GetHex().substr(0, 16) << "..." diff --git a/src/impl/dash/coin/node_coin_state.hpp b/src/impl/dash/coin/node_coin_state.hpp new file mode 100644 index 000000000..6629cfa57 --- /dev/null +++ b/src/impl/dash/coin/node_coin_state.hpp @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once + +/// Phase C-TEMPLATE step 6 (S8 embedded_gbt live-wire, follow-on to #672): +/// the node-held embedded coin-state bundle that flips select_dash_work()'s +/// hot arm live. +/// +/// #672 landed select_dash_work() (work_source.hpp) as the stable branch +/// point, but every caller still presents has_state=false, so 100% of the +/// running node's get_work path routes to the retained dashd +/// getblocktemplate fallback. This holder is the in-process coin-state the +/// DASH node maintains as mnlistdiff / mempool / header-tip updates arrive -- +/// masternode list + mempool + header-tip params. make_embedded_work_inputs() +/// presents it as a viable() EmbeddedWorkInputs so select_dash_work() takes +/// the oracle-parity embedded path (build_embedded_workdata, pinned by +/// test_dash_embedded_gbt vs frstrtr/p2pool-dash getwork()). +/// +/// STRICTLY single-coin: src/impl/dash/ only, no bitcoin_family / src/core +/// reach. The external-daemon (dashd RPC) arm is NEVER removed -- populated() +/// == false always routes there, and it remains the [GBT-XCHECK] cross-check. + +#include // EmbeddedWorkInputs, select_dash_work, WorkSelection +#include // MnStateMachine +#include // Mempool +#include // DashWorkData + +#include + +#include +#include + +namespace dash { +namespace coin { + +/// In-process coin-state the running node maintains for LOCAL template +/// assembly. Non-copyable: it owns a Mempool (itself non-copyable) and is +/// node-owned, never duplicated. The maintainer mutates mnstates()/mempool() +/// in place and calls set_tip() once the tip advances; get_work reads it +/// through select_work(). +class NodeCoinState { +public: + NodeCoinState() = default; + NodeCoinState(const NodeCoinState&) = delete; + NodeCoinState& operator=(const NodeCoinState&) = delete; + + // Mutable accessors for the maintainer (the reception / think slices seed + // these from mnlistdiff + relayed mempool txs). + MnStateMachine& mnstates() { return m_mnstates; } + Mempool& mempool() { return m_mempool; } + + /// Record the header-tip parameters and mark the bundle live. Call after + /// the tip advances AND the MN list + mempool are seeded; until then the + /// selector must route to the dashd fallback. curtime/version left 0 use + /// build_embedded_workdata()'s own SAFE-ADDITIVE defaults. + void set_tip(uint32_t prev_height, const uint256& prev_hash, + uint32_t bits_for_next, uint32_t mtp_at_tip, + uint8_t address_version, uint8_t address_p2sh_version, + uint32_t curtime = 0, uint32_t version = 0) { + m_prev_height = prev_height; + m_prev_hash = prev_hash; + m_bits_for_next = bits_for_next; + m_mtp_at_tip = mtp_at_tip; + m_address_version = address_version; + m_address_p2sh_version = address_p2sh_version; + m_curtime = curtime; + m_version = version; + m_populated = true; + } + + /// Invalidate the bundle (reorg / mempool flush / MN list gap) so the next + /// get_work falls back to dashd until the state is rebuilt. + void invalidate() { m_populated = false; } + + bool populated() const { return m_populated; } + + /// Assemble the selector input. has_state is populated(); the two required + /// pointers are always non-null (members), so viable() reduces to + /// populated() -- exactly the semantics work_source.hpp documents. + EmbeddedWorkInputs make_embedded_work_inputs() const { + EmbeddedWorkInputs e; + e.has_state = m_populated; + e.prev_height = m_prev_height; + e.prev_hash = m_prev_hash; + e.mnstates = &m_mnstates; + e.mempool = &m_mempool; + e.bits_for_next = m_bits_for_next; + e.mtp_at_tip = m_mtp_at_tip; + e.address_version = m_address_version; + e.address_p2sh_version = m_address_p2sh_version; + e.curtime = m_curtime; + e.version = m_version; + return e; + } + + /// Live get_work entry point: prefer the locally-assembled embedded + /// template when this bundle is populated, else the supplied dashd + /// getblocktemplate fallback. Thin wrapper over select_dash_work() so the + /// node call site is one line. dashd_fallback is REQUIRED -- it is the + /// always-reachable safety path and the cross-check arm. + WorkSelection select_work( + const std::function& dashd_fallback) const { + return select_dash_work(make_embedded_work_inputs(), dashd_fallback); + } + +private: + MnStateMachine m_mnstates; + Mempool m_mempool; + uint32_t m_prev_height{0}; + uint256 m_prev_hash; + uint32_t m_bits_for_next{0}; + uint32_t m_mtp_at_tip{0}; + uint8_t m_address_version{0}; + uint8_t m_address_p2sh_version{0}; + uint32_t m_curtime{0}; + uint32_t m_version{0}; + bool m_populated{false}; +}; + +} // namespace coin +} // namespace dash diff --git a/src/impl/dash/coin/work_source.hpp b/src/impl/dash/coin/work_source.hpp new file mode 100644 index 000000000..8a91e9c8c --- /dev/null +++ b/src/impl/dash/coin/work_source.hpp @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once + +/// Phase C-TEMPLATE step 5 (capstone): embedded-vs-dashd work-source selector. +/// +/// build_embedded_workdata() (embedded_gbt.hpp) is source-complete and pinned +/// by test_dash_embedded_gbt (oracle: frstrtr/p2pool-dash getwork(), +/// older-than-v35 semantics). What was missing is a LIVE consumer: the running +/// node's get_work path must PREFER the locally-assembled embedded template and +/// fall back to dashd getblocktemplate only when the embedded coin-state bundle +/// is unavailable. +/// +/// select_dash_work() is that branch point. When the EmbeddedWorkInputs bundle +/// is viable (masternode list + mempool + header-tip params all present) it +/// assembles the template LOCALLY via build_embedded_workdata() — the +/// oracle-parity path. Otherwise it invokes the supplied dashd fallback. +/// +/// The external-daemon (dashd RPC) arm is NEVER removed: it is BOTH the fallback +/// AND the [GBT-XCHECK] cross-check. viable()==false must always route there. + +#include // build_embedded_workdata +#include // MnStateMachine +#include // Mempool +#include // DashWorkData + +#include + +#include +#include +#include +#include + +namespace dash { +namespace coin { + +/// The inputs build_embedded_workdata() consumes, plus a validity flag. For the +/// embedded path to be taken, has_state must be true AND both pointers non-null +/// (viable()). Until NodeImpl carries embedded coin-state in-process, callers +/// leave has_state=false and the selector routes to the dashd fallback. +struct EmbeddedWorkInputs { + bool has_state{false}; + uint32_t prev_height{0}; + uint256 prev_hash; + const MnStateMachine* mnstates{nullptr}; + const Mempool* mempool{nullptr}; + uint32_t bits_for_next{0}; + uint32_t mtp_at_tip{0}; + uint8_t address_version{0}; + uint8_t address_p2sh_version{0}; + // Optional seams. 0 => use build_embedded_workdata's own SAFE-ADDITIVE + // defaults (std::time(nullptr) / 0x20000000 BIP9 baseline). + uint32_t curtime{0}; + uint32_t version{0}; + + bool viable() const { + return has_state && mnstates != nullptr && mempool != nullptr; + } +}; + +enum class WorkSource { Embedded, DashdFallback }; + +struct WorkSelection { + DashWorkData work; + WorkSource source{WorkSource::DashdFallback}; +}; + +/// Injectable core — the routing decision, testable without a live daemon or a +/// populated MN/mempool harness. Production binds embedded_builder to the real +/// build_embedded_workdata() closure (see the 2-arg overload below). +inline WorkSelection select_dash_work( + const EmbeddedWorkInputs& emb, + const std::function& embedded_builder, + const std::function& dashd_fallback) +{ + if (emb.viable()) + return WorkSelection{embedded_builder(), WorkSource::Embedded}; + return WorkSelection{dashd_fallback(), WorkSource::DashdFallback}; +} + +/// Production entry point: builds the embedded template from `emb` when viable, +/// else calls `dashd_fallback` (the retained external-daemon getblocktemplate +/// arm). dashd_fallback is REQUIRED — it is the always-reachable safety path. +inline WorkSelection select_dash_work( + const EmbeddedWorkInputs& emb, + const std::function& dashd_fallback) +{ + return select_dash_work( + emb, + [&emb]() { + return build_embedded_workdata( + emb.prev_height, emb.prev_hash, *emb.mnstates, *emb.mempool, + emb.bits_for_next, emb.mtp_at_tip, + emb.address_version, emb.address_p2sh_version, + emb.curtime ? emb.curtime + : static_cast(std::time(nullptr)), + emb.version ? emb.version : 0x20000000u); + }, + dashd_fallback); +} + +} // namespace coin +} // namespace dash diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index d72432ed4..20702c38f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -504,6 +504,36 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(test_dash_embedded_gbt PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_embedded_gbt "" AUTO) + # DASH embedded-vs-dashd work-source selector (S8 embedded_gbt live-wire + # capstone): select_dash_work() prefers the locally-assembled embedded + # template (build_embedded_workdata) and falls back to dashd + # getblocktemplate when the coin-state bundle is not viable. Routing + + # retained-fallback KAT; harness-free (embedded builder injected as a stub, + # oracle-parity already pinned by test_dash_embedded_gbt). Same link set as + # the embedded_gbt capstone (work_source.hpp includes embedded_gbt.hpp). + add_executable(test_dash_work_source test_dash_work_source.cpp) + target_link_libraries(test_dash_work_source PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_work_source PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_work_source "" AUTO) + + # NodeCoinState: node-held embedded coin-state holder over the + # work_source selector link set (mn_state_machine + mempool header-only); + # no ltc/pool SCC dependency. Mirrors the test_dash_work_source link set. + add_executable(test_dash_node_coin_state test_dash_node_coin_state.cpp) + target_link_libraries(test_dash_node_coin_state PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_node_coin_state PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_node_coin_state "" AUTO) + # DASH CSimplifiedMNListDiff (mnlistdiff wire, S8.1 leaf): vendored diff # round-trip + dashcore field-layout pin + apply_diff semantics. Header-only # over the SimplifiedMNList leaf + coin/transaction; no ltc/pool SCC dep. diff --git a/test/test_dash_node_coin_state.cpp b/test/test_dash_node_coin_state.cpp new file mode 100644 index 000000000..1df895320 --- /dev/null +++ b/test/test_dash_node_coin_state.cpp @@ -0,0 +1,195 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/// Phase C-TEMPLATE step 6 -- NodeCoinState live-wire KAT. +/// +/// #672 proved select_dash_work()'s COLD arm (has_state=false -> dashd +/// fallback = retained safety path, verify point 3). This suite proves the +/// HOT arm the node-held coin-state slice unlocks: +/// +/// populated NodeCoinState -> select_work() routes WorkSource::Embedded and +/// returns EXACTLY the DashWorkData that a direct build_embedded_workdata() +/// over the same MN list + mempool + tip params produces -- i.e. wiring the +/// bundle in changes nothing about the oracle-parity template, it only +/// flips which arm runs. The dashd fallback closure is NOT invoked on the +/// hot path; it IS invoked (and only it) when the bundle is unpopulated or +/// has been invalidate()d. +/// +/// Construction mirrors test_dash_embedded_gbt.cpp exactly (same single_mn / +/// mempool seeding) so the two suites pin the SAME projection from the two +/// call shapes. No fabricated oracle values -- the "expected" work IS an +/// independent build_embedded_workdata() call, compared field-for-field. + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +using dash::coin::NodeCoinState; +using dash::coin::DashWorkData; +using dash::coin::WorkSource; +using dash::coin::WorkSelection; +using dash::coin::MNState; +using dash::coin::MnStateMachine; +using dash::coin::Mempool; +using dash::coin::MutableTransaction; +using dash::coin::build_embedded_workdata; +using ::core::coin::UTXOViewCache; +using ::core::coin::Outpoint; +using ::core::coin::Coin; +using ::bitcoin_family::coin::TxIn; +using ::bitcoin_family::coin::TxOut; + +static constexpr uint8_t DASH_PUBKEY_VER = 76; +static constexpr uint8_t DASH_P2SH_VER = 16; +static constexpr uint32_t H = 2'400'000; // past MN_RR: platform burn active + +static uint256 raw256(uint8_t base) { + uint256 h; + std::array p{}; + for (size_t i = 0; i < 32; ++i) p[i] = static_cast(base + i); + std::memcpy(h.data(), p.data(), 32); + return h; +} + +static std::vector p2pkh_script(uint8_t hashseed) { + std::vector s{0x76, 0xa9, 0x14}; + for (int i = 0; i < 20; ++i) s.push_back(static_cast(hashseed + i)); + s.push_back(0x88); s.push_back(0xac); + return s; +} + +static MutableTransaction make_spend(const uint256& prev, uint32_t idx, + int64_t out_value, uint32_t salt) { + MutableTransaction tx; + tx.version = 1; tx.type = 0; tx.locktime = salt; + TxIn in; in.prevout.hash = prev; in.prevout.index = idx; + in.sequence = 0xffffffffu; + tx.vin.push_back(in); + TxOut o; o.value = out_value; + tx.vout.push_back(o); + return tx; +} + +// Seed `st.mnstates()` with a single valid MN paying `payout`. +static void seed_single_mn(NodeCoinState& st, const std::vector& payout) { + MNState s; + s.isValid = true; + s.nRegisteredHeight = 2'300'000; + s.nLastPaidHeight = 0; + s.scriptPayout.m_data = payout; + st.mnstates().load(std::vector>{{raw256(0x01), s}}); +} + +static void expect_workdata_eq(const DashWorkData& a, const DashWorkData& b) { + EXPECT_EQ(a.m_version, b.m_version); + EXPECT_EQ(a.m_previous_block, b.m_previous_block); + EXPECT_EQ(a.m_height, b.m_height); + EXPECT_EQ(a.m_coinbase_value, b.m_coinbase_value); + EXPECT_EQ(a.m_bits, b.m_bits); + EXPECT_EQ(a.m_curtime, b.m_curtime); + EXPECT_EQ(a.m_mintime, b.m_mintime); + EXPECT_EQ(a.m_payment_amount, b.m_payment_amount); + EXPECT_EQ(a.m_tx_hashes, b.m_tx_hashes); + EXPECT_EQ(a.m_tx_fees, b.m_tx_fees); + EXPECT_EQ(a.m_txs.size(), b.m_txs.size()); +} + +// ════════════════════════════════════════════════════════════════════════ +// HOT arm: populated bundle -> Embedded, byte-equal to direct build. +// ════════════════════════════════════════════════════════════════════════ +TEST(DashNodeCoinState, PopulatedRoutesEmbeddedByteEqualToDirectBuild) { + UTXOViewCache utxo(nullptr); + uint256 prev = raw256(0x77); + utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, /*height=*/1, /*cb=*/false)); + + auto payout = p2pkh_script(0x30); + const uint256 prev_hash = raw256(0xAB); + const uint32_t bits = 0x1b104be3u; + const uint32_t mtp = 1'700'000'000u; + const uint32_t curtime = 1'700'000'123u; // pin the injectable seams so + const uint32_t version = 0x20000000u; // both build paths are identical + + NodeCoinState st; + seed_single_mn(st, payout); + st.mempool().set_utxo(&utxo); + ASSERT_TRUE(st.mempool().add_tx(make_spend(prev, 0, 90'000, /*salt=*/1))); // fee 10'000 + st.set_tip(H - 1, prev_hash, bits, mtp, DASH_PUBKEY_VER, DASH_P2SH_VER, curtime, version); + + ASSERT_TRUE(st.populated()); + ASSERT_TRUE(st.make_embedded_work_inputs().viable()); + + // Independent reference: the SAME projection built directly. The node + // bundle must reproduce it exactly, only choosing the Embedded arm. + DashWorkData reference = build_embedded_workdata( + H - 1, prev_hash, st.mnstates(), st.mempool(), + bits, mtp, DASH_PUBKEY_VER, DASH_P2SH_VER, curtime, version); + + bool fallback_called = false; + WorkSelection sel = st.select_work([&]() { + fallback_called = true; + return DashWorkData{}; // sentinel: must NOT be returned on hot path + }); + + EXPECT_EQ(sel.source, WorkSource::Embedded); + EXPECT_FALSE(fallback_called) << "dashd fallback must not run when embedded is viable"; + EXPECT_EQ(sel.work.m_height, H); + expect_workdata_eq(sel.work, reference); +} + +// ════════════════════════════════════════════════════════════════════════ +// COLD arm (retained fallback): unpopulated / invalidated -> DashdFallback. +// ════════════════════════════════════════════════════════════════════════ +TEST(DashNodeCoinState, UnpopulatedRoutesRetainedDashdFallback) { + NodeCoinState st; // default: not populated + ASSERT_FALSE(st.populated()); + ASSERT_FALSE(st.make_embedded_work_inputs().viable()); + + DashWorkData sentinel; + sentinel.m_height = 4'242'424u; // a value the embedded path would never emit here + + bool fallback_called = false; + WorkSelection sel = st.select_work([&]() { + fallback_called = true; + return sentinel; + }); + + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_TRUE(fallback_called) << "the always-reachable dashd arm must run when no coin-state"; + EXPECT_EQ(sel.work.m_height, sentinel.m_height); +} + +TEST(DashNodeCoinState, InvalidateRevertsToFallback) { + UTXOViewCache utxo(nullptr); + uint256 prev = raw256(0x77); + utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, 1, false)); + + NodeCoinState st; + seed_single_mn(st, p2pkh_script(0x30)); + st.mempool().set_utxo(&utxo); + ASSERT_TRUE(st.mempool().add_tx(make_spend(prev, 0, 90'000, 1))); + st.set_tip(H - 1, raw256(0xAB), 0x1b104be3u, 1'700'000'000u, + DASH_PUBKEY_VER, DASH_P2SH_VER); + ASSERT_TRUE(st.make_embedded_work_inputs().viable()); + + st.invalidate(); // reorg / mempool flush + EXPECT_FALSE(st.populated()); + EXPECT_FALSE(st.make_embedded_work_inputs().viable()); + + bool fallback_called = false; + WorkSelection sel = st.select_work([&]() { fallback_called = true; return DashWorkData{}; }); + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_TRUE(fallback_called); +} diff --git a/test/test_dash_work_source.cpp b/test/test_dash_work_source.cpp new file mode 100644 index 000000000..a4480e8a7 --- /dev/null +++ b/test/test_dash_work_source.cpp @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +/// +/// KAT for dash::coin::select_dash_work — the embedded-vs-dashd work-source +/// selector (S8 embedded_gbt live-wire capstone). Proves the routing contract +/// and the RETAINED dashd fallback, without a live daemon or a populated +/// MN/mempool harness (the embedded builder is injected as a stub; its +/// oracle-parity output is already pinned by test_dash_embedded_gbt). +/// +/// Contract under test: +/// 1. viable() bundle -> WorkSource::Embedded, embedded builder run, +/// fallback NEVER touched. +/// 2. has_state=false -> WorkSource::DashdFallback, fallback run. +/// 3. viable but null mnstates -> fallback (defensive null-guard). +/// 4. viable but null mempool -> fallback (defensive null-guard). + +#include + +#include + +using dash::coin::EmbeddedWorkInputs; +using dash::coin::WorkSource; +using dash::coin::WorkSelection; +using dash::coin::select_dash_work; +using dash::coin::DashWorkData; +using dash::coin::MnStateMachine; +using dash::coin::Mempool; + +namespace { + +// Distinguishable sentinels so we can prove WHICH closure produced the result. +constexpr uint32_t EMB_SENTINEL_HEIGHT = 0xE3BEDDEDu & 0xffffffu; // "embedded" +constexpr uint32_t DASHD_SENTINEL_HEIGHT = 999'999u; + +DashWorkData embedded_stub(bool& ran) { + ran = true; + DashWorkData w; + w.m_height = EMB_SENTINEL_HEIGHT; + return w; +} + +DashWorkData dashd_stub(bool& ran) { + ran = true; + DashWorkData w; + w.m_height = DASHD_SENTINEL_HEIGHT; + return w; +} + +} // namespace + +// 1) Viable bundle routes to the EMBEDDED builder; fallback is not invoked. +TEST(DashWorkSource, ViableRoutesEmbedded) +{ + MnStateMachine mn; + Mempool mp; + EmbeddedWorkInputs emb; + emb.has_state = true; + emb.mnstates = &mn; + emb.mempool = ∓ + ASSERT_TRUE(emb.viable()); + + bool emb_ran = false, fb_ran = false; + WorkSelection sel = select_dash_work( + emb, + [&] { return embedded_stub(emb_ran); }, + [&] { return dashd_stub(fb_ran); }); + + EXPECT_EQ(sel.source, WorkSource::Embedded); + EXPECT_TRUE(emb_ran); + EXPECT_FALSE(fb_ran); + EXPECT_EQ(sel.work.m_height, EMB_SENTINEL_HEIGHT); +} + +// 2) No embedded state -> the RETAINED dashd getblocktemplate fallback runs. +TEST(DashWorkSource, NoStateRoutesDashdFallback) +{ + EmbeddedWorkInputs emb; // has_state defaults false + ASSERT_FALSE(emb.viable()); + + bool emb_ran = false, fb_ran = false; + WorkSelection sel = select_dash_work( + emb, + [&] { return embedded_stub(emb_ran); }, + [&] { return dashd_stub(fb_ran); }); + + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_FALSE(emb_ran); + EXPECT_TRUE(fb_ran); + EXPECT_EQ(sel.work.m_height, DASHD_SENTINEL_HEIGHT); +} + +// 3) has_state true but mnstates null -> not viable -> fallback (null-guard). +TEST(DashWorkSource, NullMnStatesRoutesFallback) +{ + Mempool mp; + EmbeddedWorkInputs emb; + emb.has_state = true; + emb.mnstates = nullptr; + emb.mempool = ∓ + EXPECT_FALSE(emb.viable()); + + bool emb_ran = false, fb_ran = false; + WorkSelection sel = select_dash_work( + emb, + [&] { return embedded_stub(emb_ran); }, + [&] { return dashd_stub(fb_ran); }); + + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_FALSE(emb_ran); + EXPECT_TRUE(fb_ran); +} + +// 4) has_state true but mempool null -> not viable -> fallback (null-guard). +TEST(DashWorkSource, NullMempoolRoutesFallback) +{ + MnStateMachine mn; + EmbeddedWorkInputs emb; + emb.has_state = true; + emb.mnstates = &mn; + emb.mempool = nullptr; + EXPECT_FALSE(emb.viable()); + + bool emb_ran = false, fb_ran = false; + WorkSelection sel = select_dash_work( + emb, + [&] { return embedded_stub(emb_ran); }, + [&] { return dashd_stub(fb_ran); }); + + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_FALSE(emb_ran); + EXPECT_TRUE(fb_ran); +}