From a1db112bcd6a79431b7de7d14802b275974fa955 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Mon, 13 Jul 2026 02:53:08 +0000 Subject: [PATCH 1/2] dash(S8): include leg-1 mempool_ingest + test registration (self-contained slice) The reception-wire leg-1 header (wire_mempool_ingest) and the test_dash_node_reception_wire CMake registration were authored in the worktree but never committed to master -- this PR carries the FULL reception wire (legs 1+2), so a fresh checkout builds the KAT target and its mempool_ingest include. No file here is on master; purely additive, dash-only. --- src/impl/dash/coin/mempool_ingest.hpp | 68 +++++++++++++++++++++++++++ test/CMakeLists.txt | 15 ++++++ 2 files changed, 83 insertions(+) create mode 100644 src/impl/dash/coin/mempool_ingest.hpp diff --git a/src/impl/dash/coin/mempool_ingest.hpp b/src/impl/dash/coin/mempool_ingest.hpp new file mode 100644 index 000000000..ce44ac8bf --- /dev/null +++ b/src/impl/dash/coin/mempool_ingest.hpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// =========================================================================== +// dash reception wire (leg 1 of 4) -- interfaces::Node::new_tx -> maintainer. +// +// #672..#685 landed the node-held embedded coin-state bundle plus its async +// CoinStateMaintainer, and proved that DRIVING the maintainer's on_*() methods +// flips select_work() to the embedded arm. But nothing in a LIVE NodeImpl ever +// subscribed the coin interface's reception events to that maintainer, so in a +// running node the arm could never flip on its own -- the maintainer sat idle. +// This is the first of the four reception legs +// (on_mempool_tx / on_block_connected / on_new_tip / on_mn_list_update): the +// mempool-relay leg, wired exactly as the dgb sibling (wire_mempool_ingest) +// but routed through the maintainer's on_mempool_tx() so the readiness / +// republish bookkeeping stays in one place. +// +// PAYLOAD-COMPLETE TODAY: interfaces::Node::new_tx carries a whole +// coin::Transaction, and on_mempool_tx() never gates publication (an empty +// mempool still yields a valid coinbase-only template), so this leg needs no +// chain context. The other three legs are NOT wired here: on_block_connected +// needs the connected block's HEIGHT (full_block carries none -- see bch's +// block_connector deriving it from the chain tip), on_new_tip needs +// bits/mtp/address-version params a bare best_block_hash does not carry, and +// on_mn_list_update has no source event on interfaces::Node at all. Those are +// their own slices; opening them touches the interface shape and is a design +// decision surfaced to the integrator, not guessed here. +// +// LIFETIME: the handler captures `maint` by reference, so `maint` (and the +// NodeCoinState it drives) MUST outlive `node`. The returned EventDisposable +// lets a caller tear the subscription down explicitly; EventDisposable does +// NOT auto-dispose on destruction, so if the handle is dropped the +// subscription persists for the node's life. +// +// SCC (#143 / #22 / #39): pulls transaction.hpp (the tx serialization codec), +// so include this header ONLY from a TU that already links the full dash codec +// (main_dash + this leg's KAT), never a guard-weight TU -- identical to the +// dgb mempool_ingest / embedded_tx_select btclibs-SCC trap. +// =========================================================================== +#include + +#include + +#include "node_interface.hpp" // dash::interfaces::Node (new_tx feed) +#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_mempool_tx +#include "transaction.hpp" // dash::coin::Transaction -> MutableTransaction + +namespace c2pool::dash +{ + +// Subscribe `maint` to `node.new_tx`: every relayed tx is folded into the +// maintainer's mempool via on_mempool_tx (fee_known=false until a UTXO view +// feeds it, matching the embedded shaper's conservative default -- a P2P-fed +// mempool cannot desync coinbasevalue versus a daemon's GBT). The mempool leg +// never gates publication, so a rejected tx (bad utxo ref / already-in) is a +// no-op here, not a demotion. Returns the subscription handle so the caller +// controls teardown. +inline std::shared_ptr +wire_mempool_ingest(::dash::interfaces::Node& node, + ::dash::coin::CoinStateMaintainer& maint) +{ + return node.new_tx.subscribe( + [&maint](const ::dash::coin::Transaction& tx) + { + maint.on_mempool_tx(::dash::coin::MutableTransaction(tx)); + }); +} + +} // namespace c2pool::dash diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 1d5912d86..b50e46172 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -560,6 +560,21 @@ if (BUILD_TESTING AND GTest_FOUND) target_link_libraries(test_dash_node_embedded_wire PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining c2pool_storage pool) # OBJECT-lib SCC direct-naming (#22/#39) gtest_add_tests(test_dash_node_embedded_wire "" AUTO) + # Reception wire (legs 1-3): interfaces::Node reception Events -> + # CoinStateMaintainer. Proves the wire_*_ingest() subscriptions fold off the + # real Events (no direct poke), honour dispose(), and reach the embedded + # template. Same header-only link set as the maintainer suite (no NodeImpl + # -> no pool/storage dep). + add_executable(test_dash_node_reception_wire test_dash_node_reception_wire.cpp) + target_link_libraries(test_dash_node_reception_wire PRIVATE + GTest::gtest_main GTest::gtest + dash_x11 core + nlohmann_json::nlohmann_json + ${Boost_LIBRARIES} + ) + target_link_libraries(test_dash_node_reception_wire PRIVATE c2pool_payout c2pool_hashrate c2pool_merged_mining) # OBJECT-lib SCC direct-naming (#22/#39) + gtest_add_tests(test_dash_node_reception_wire "" 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. From f76a9486618a6e3f0bbbfb93f08f5b32dc50e164 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Mon, 13 Jul 2026 03:55:16 +0000 Subject: [PATCH 2/2] dash(S8): reception-wire leg 3 -- interfaces::Node::block_connected -> CoinStateMaintainer::on_block_connected Legs 1 (mempool) + 2 (tip) subscribed the mempool-relay and header/think TIP- ADVANCE events to the node-held embedded coin-state bundle. Nothing yet wired the BLOCK-CONNECT event, so in a live node the DMN set the embedded coinbase pays could only be seeded by a full mnlistdiff snapshot (on_mn_list_update) and never auto-maintained forward between snapshots. This leg closes that gap: - interfaces::Node gains an additive BlockConnected payload + Event block_connected. A bare full_block carries the block body but NOT the height on_block_connected -> MnStateMachine::apply_block needs (DIP3 special-tx chain position); block_connected pairs (block, height), exactly as leg 2 added TipAdvance for params best_block_hash could not carry. Purely additive, dash interface only (single-coin). - block_connect_ingest.hpp::wire_block_connect_ingest subscribes on_block_connected to block_connected, mirroring wire_mempool_ingest / wire_tip_ingest contract (by-ref capture, explicit-teardown disposable; dashd GBT arm retained as the fallback when the post-apply set can no longer back a payee). - test_dash_node_reception_wire gains leg-3 KATs off the real Event (no direct poke): a connected block that spends the sole MN collateral empties the DMN set and demotes the armed bundle to the dashd fallback; a no-special-tx block preserves readiness; dispose() stops ingest. Also carries the leg-1 recovery (mempool_ingest.hpp + test registration) that was authored in-worktree but never reached master via #686 -- without it the shared reception-wire KAT does not build. Additive, src/impl/dash/ only, dashd GBT fallback retained. --- .github/workflows/build.yml | 4 +- src/impl/dash/coin/block_connect_ingest.hpp | 61 ++++++++++ src/impl/dash/coin/node_interface.hpp | 22 ++++ test/test_dash_node_reception_wire.cpp | 118 ++++++++++++++++++++ 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 src/impl/dash/coin/block_connect_ingest.hpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a6e857778..ec919c425 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_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire \ + 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_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire \ 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_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire \ + 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_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire \ test_hash_link test_decay_pplns \ test_pplns_consensus \ test_v36_script_sorting test_v36_cross_impl_refhash \ diff --git a/src/impl/dash/coin/block_connect_ingest.hpp b/src/impl/dash/coin/block_connect_ingest.hpp new file mode 100644 index 000000000..4ad16ab32 --- /dev/null +++ b/src/impl/dash/coin/block_connect_ingest.hpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: AGPL-3.0-or-later +#pragma once +// =========================================================================== +// dash reception wire (leg 3 of 4) -- interfaces::Node::block_connected -> +// maintainer. +// +// #672..#685 landed the node-held embedded coin-state bundle + its async +// CoinStateMaintainer; leg 1 (wire_mempool_ingest) subscribed the mempool-relay +// event and leg 2 (wire_tip_ingest) the header/think TIP-ADVANCE event. This +// leg subscribes the BLOCK-CONNECT event: every connected block is routed to +// CoinStateMaintainer::on_block_connected(), which folds the block's DIP3 +// special txs into the DMN set incrementally (MnStateMachine::apply_block), +// keeping the masternode set the embedded coinbase pays current BETWEEN full +// mnlistdiff snapshots. A block that empties the set (all collateral spent) +// clears MN-readiness and demotes the bundle to the retained dashd getblock- +// template fallback rather than backing a template with a phantom payee. +// +// PAYLOAD: a bare full_block carries the block body but NOT its height, and +// apply_block needs the connected height (DIP3 special-tx chain position). So +// this leg subscribes block_connected, which pairs (block, height) -- the +// interface-shape addition this leg makes, mirroring leg 2's TipAdvance, kept to +// the dash interface only (single-coin, purely additive). on_mn_list_update +// remains the authoritative bulk resync (leg 4, mnlistdiff source event). +// +// LIFETIME: the handler captures maint by reference, so maint (and the +// NodeCoinState it drives) MUST outlive node. The returned EventDisposable lets +// a caller tear the subscription down explicitly; it does NOT auto-dispose on +// drop -- identical contract to wire_mempool_ingest / wire_tip_ingest. +// +// SCC: pulls node_interface.hpp (transaction/block codec via BlockConnected), +// so include this header ONLY from a TU that already links the full dash codec +// (main_dash + this leg KAT), never a guard-weight TU. +// =========================================================================== +#include + +#include + +#include "node_interface.hpp" // dash::interfaces::Node (block_connected feed) + BlockConnected +#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_block_connected + +namespace c2pool::dash +{ + +// Subscribe maint to node.block_connected: every connected block folds its DIP3 +// special txs into the DMN set via on_block_connected(block, height). The +// maintainer refreshes MN-readiness from the post-apply set size -- a block that +// empties the set demotes the node-held bundle to the dashd fallback; otherwise +// it republishes with the freshened MN set. Returns the subscription handle so +// the caller controls teardown. +inline std::shared_ptr +wire_block_connect_ingest(::dash::interfaces::Node& node, + ::dash::coin::CoinStateMaintainer& maint) +{ + return node.block_connected.subscribe( + [&maint](const ::dash::interfaces::BlockConnected& bc) + { + maint.on_block_connected(bc.block, bc.height); + }); +} + +} // namespace c2pool::dash diff --git a/src/impl/dash/coin/node_interface.hpp b/src/impl/dash/coin/node_interface.hpp index 719a9114b..dd530fd25 100644 --- a/src/impl/dash/coin/node_interface.hpp +++ b/src/impl/dash/coin/node_interface.hpp @@ -34,6 +34,20 @@ struct TipAdvance uint32_t version{0}; }; +/// Header/think path payload: a block was connected to the active chain. The +/// bare full_block carries the block body but NOT the height CoinStateMaintainer +/// ::on_block_connected needs to drive MnStateMachine::apply_block (the DIP3 +/// special-tx height is a chain-position input apply_block cannot recover from +/// the block alone). block_connected pairs the two so the reception wire feeds +/// apply_block the exact (block, height) apply_block expects -- purely additive, +/// dash interface only (leg 2's new_tip added TipAdvance for the same reason a +/// bare best_block_hash was insufficient). +struct BlockConnected +{ + coin::BlockType block; + uint32_t height{0}; +}; + struct Node { Variable best_block_hash; @@ -48,6 +62,14 @@ struct Node // tip-readiness prerequisite without a direct poke. Event new_tip; + // Header/think path: fires when a block is connected to the active chain, + // carrying (block, height) (see BlockConnected). The reception wire + // subscribes CoinStateMaintainer::on_block_connected to this so the DMN set + // the embedded coinbase pays auto-maintains incrementally between full + // mnlistdiff snapshots -- a block that empties the set demotes to the dashd + // fallback rather than backing a template with a phantom payee. + Event block_connected; + // SPV A1 (parity audit): fires when dashd announces a ChainLock has // been aggregated for a block. Carries {block_hash, height}. // Consumers (e.g. block-find submit handler) can consult diff --git a/test/test_dash_node_reception_wire.cpp b/test/test_dash_node_reception_wire.cpp index 498a4edb4..b66d4c208 100644 --- a/test/test_dash_node_reception_wire.cpp +++ b/test/test_dash_node_reception_wire.cpp @@ -31,10 +31,12 @@ #include // dash::interfaces::Node #include // c2pool::dash::wire_mempool_ingest #include // c2pool::dash::wire_tip_ingest +#include // c2pool::dash::wire_block_connect_ingest #include #include #include #include +#include #include #include #include @@ -52,11 +54,13 @@ using c2pool::dash::wire_mempool_ingest; using c2pool::dash::wire_tip_ingest; +using c2pool::dash::wire_block_connect_ingest; using dash::coin::CoinStateMaintainer; using dash::coin::NodeCoinState; using dash::coin::WorkSource; using dash::coin::WorkSelection; using dash::coin::MNState; +using dash::coin::BlockType; using dash::coin::MutableTransaction; using dash::coin::Transaction; using ::core::coin::UTXOViewCache; @@ -105,6 +109,21 @@ static std::vector> single_mn(const std::vector>{{raw256(0x01), s}}; } +// Same as single_mn but records the MN's collateral outpoint, so a block that +// spends it via apply_block (pass 2) removes the record (leg-3 demote KAT). +static std::vector> +single_mn_coll(const std::vector& payout, + const uint256& coll_hash, uint32_t coll_idx) { + MNState s; + s.isValid = true; + s.nRegisteredHeight = 2'300'000; + s.nLastPaidHeight = 0; + s.scriptPayout.m_data = payout; + s.collateralOutpoint.hash = coll_hash; + s.collateralOutpoint.index = coll_idx; + return std::vector>{{raw256(0x01), s}}; +} + static const uint256 PREV_HASH = raw256(0xAB); static const uint32_t BITS = 0x1b104be3u; static const uint32_t MTP = 1'700'000'000u; @@ -259,3 +278,102 @@ TEST(DashReceptionWire, DisposeStopsTipIngest) { node.new_tip.happened(t); EXPECT_FALSE(m.live()) << "after dispose, a tip advance must not arm the bundle"; } + +// ════════════════════════════════════════════════════════════════════════ +// Leg 3: a block_connected relay fired on the interface drives the maintainer's +// incremental MnStateMachine::apply_block THROUGH THE WIRE -- no direct +// on_block_connected() poke. A connected block whose non-coinbase tx spends the +// sole MN's collateral empties the DMN set; the now-unbacked payee demotes the +// live bundle to the retained dashd fallback, and select_work routes get_work +// there. Proves the block-connect leg mutates coin-state off the real Event. +// ════════════════════════════════════════════════════════════════════════ +TEST(DashReceptionWire, BlockConnectRelayDrivesApplyBlockThenDemotes) { + UTXOViewCache utxo(nullptr); + dash::interfaces::Node node; + NodeCoinState st; + st.mempool().set_utxo(&utxo); + CoinStateMaintainer m(st); + + auto sub = wire_block_connect_ingest(node, m); + ASSERT_TRUE(sub) << "wire must return a live subscription handle"; + + // Arm the bundle: MN (with a known collateral) + tip both present. + const uint256 coll = raw256(0x55); + m.on_mn_list_update(single_mn_coll(p2pkh_script(0x30), coll, 3)); + m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION); + ASSERT_TRUE(m.live()); + ASSERT_EQ(st.mnstates().size(), 1u); + + // A connected block spends the sole MN's collateral -- fired on the wire, + // NOT a direct maintainer poke. + dash::interfaces::BlockConnected bc; + bc.height = H; + bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped) + bc.block.m_txs.push_back(make_spend(coll, 3, 400'000'000, 2)); // spends MN collateral + node.block_connected.happened(bc); + + EXPECT_EQ(st.mnstates().size(), 0u) << "apply_block (via wire) must remove the MN"; + EXPECT_FALSE(m.live()) << "emptied DMN set must drop the live bundle"; + + bool fb = false; + WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; }); + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_TRUE(fb) << "after the collateral-spend block, get_work must fall back to dashd"; +} + +// ════════════════════════════════════════════════════════════════════════ +// A block with no special txs touches no DMN record: apply_block registers +// nothing, the set is unchanged, and the armed bundle stays live -- the wire +// routes the event but a no-op block must not disturb readiness. +// ════════════════════════════════════════════════════════════════════════ +TEST(DashReceptionWire, BlockConnectRelayNoSpecialTxPreservesReadiness) { + UTXOViewCache utxo(nullptr); + dash::interfaces::Node node; + NodeCoinState st; + st.mempool().set_utxo(&utxo); + CoinStateMaintainer m(st); + + auto sub = wire_block_connect_ingest(node, m); + m.on_mn_list_update(single_mn(p2pkh_script(0x30))); + m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION); + ASSERT_TRUE(m.live()); + + dash::interfaces::BlockConnected bc; + bc.height = H; + bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped) + bc.block.m_txs.push_back(make_spend(raw256(0x91), 0, 400'000'000, 2)); // plain spend, no collateral + node.block_connected.happened(bc); + + EXPECT_EQ(st.mnstates().size(), 1u) << "no-special-tx block must not touch the DMN set"; + EXPECT_TRUE(m.live()) << "no-op block via wire must not drop the live bundle"; +} + +// ════════════════════════════════════════════════════════════════════════ +// Disposing the block-connect handle tears the subscription down: a later +// connected block is not applied, so a collateral spend cannot silently demote +// an armed bundle off a stale feed. +// ════════════════════════════════════════════════════════════════════════ +TEST(DashReceptionWire, DisposeStopsBlockConnectIngest) { + UTXOViewCache utxo(nullptr); + dash::interfaces::Node node; + NodeCoinState st; + st.mempool().set_utxo(&utxo); + CoinStateMaintainer m(st); + + const uint256 coll = raw256(0x55); + m.on_mn_list_update(single_mn_coll(p2pkh_script(0x30), coll, 3)); + m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION); + ASSERT_TRUE(m.live()); + + auto sub = wire_block_connect_ingest(node, m); + sub->dispose(); + + dash::interfaces::BlockConnected bc; + bc.height = H; + bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped) + bc.block.m_txs.push_back(make_spend(coll, 3, 400'000'000, 2)); // would spend MN collateral + node.block_connected.happened(bc); + + EXPECT_EQ(st.mnstates().size(), 1u) << "after dispose, a connected block must not be applied"; + EXPECT_TRUE(m.live()) << "after dispose, the armed bundle must stay live"; +}