diff --git a/src/impl/dash/coin/coin_state_maintainer.hpp b/src/impl/dash/coin/coin_state_maintainer.hpp index c91645e3d..2acf09a73 100644 --- a/src/impl/dash/coin/coin_state_maintainer.hpp +++ b/src/impl/dash/coin/coin_state_maintainer.hpp @@ -29,6 +29,7 @@ #include // NodeCoinState #include // MNState +#include // BlockType #include // MutableTransaction #include @@ -93,6 +94,26 @@ class CoinStateMaintainer { republish(); } + /// Header / think path (block connect): fold a newly-connected block's + /// special txs into the DMN list incrementally, mirroring dashcore's + /// RebuildListFromBlock (MnStateMachine::apply_block). This is the LIVE + /// driver that keeps the masternode set the embedded coinbase pays current + /// BETWEEN full mnlistdiff snapshots -- on_mn_list_update() stays the + /// authoritative resync and is UNCHANGED. MN-readiness is refreshed from + /// the post-apply list size: a block that empties the set (all collateral + /// spent) drops the bundle to the dashd fallback rather than backing a + /// template with a phantom payee. Returns apply_block's ApplyResult. + MnStateMachine::ApplyResult + on_block_connected(const dash::coin::BlockType& block, uint32_t height) { + auto r = m_state.mnstates().apply_block(block, height); + m_have_mn = m_state.mnstates().size() != 0; + if (!m_have_mn) + demote(); + else + republish(); + return r; + } + /// Reorg / MN-list gap / mempool flush: invalidate the live bundle so the /// next get_work falls back to dashd until a fresh tip rebuilds it. The /// stashed tip params are dropped -- a reorg means the old prev_hash is no diff --git a/test/test_dash_coin_state_maintainer.cpp b/test/test_dash_coin_state_maintainer.cpp index 2bdcc4e14..7ef267b99 100644 --- a/test/test_dash_coin_state_maintainer.cpp +++ b/test/test_dash_coin_state_maintainer.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -45,6 +46,7 @@ using dash::coin::WorkSelection; using dash::coin::MNState; using dash::coin::Mempool; using dash::coin::MutableTransaction; +using dash::coin::BlockType; using dash::coin::build_embedded_workdata; using ::core::coin::UTXOViewCache; using ::core::coin::Outpoint; @@ -223,3 +225,68 @@ TEST(DashCoinStateMaintainer, InvalidateReorgRequiresFreshTipToReArm) { EXPECT_FALSE(fb); EXPECT_EQ(sel.work.m_height, H + 1); } + +// ======================================================================== +// on_block_connected() -- incremental MnStateMachine::apply_block live-wire. +// #674 populated the DMN set only via the bulk mnlistdiff snapshot +// (on_mn_list_update). This slice drives apply_block per connected block so +// the set the embedded coinbase pays auto-maintains between snapshots, while +// the dashd RPC arm stays the fallback when the set can no longer back a payee. +// ======================================================================== +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 = 2300000; + s.nLastPaidHeight = 0; + s.scriptPayout.m_data = payout; + s.collateralOutpoint.hash = coll_hash; + s.collateralOutpoint.index = coll_idx; + return std::vector>{{raw256(0x01), s}}; +} + +// A block with no special txs touches no DMN records: apply_block registers +// nothing, the set is unchanged, and the embedded bundle stays live. +TEST(DashCoinStateMaintainer, BlockConnectNoSpecialTxPreservesReadiness) { + NodeCoinState st; + CoinStateMaintainer m(st); + 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()); + + BlockType blk; + blk.m_txs.push_back(make_spend(raw256(0x90), 0, 500000000, 1)); // cb (idx 0, skipped) + blk.m_txs.push_back(make_spend(raw256(0x91), 0, 400000000, 2)); // plain spend, no collateral match + auto r = m.on_block_connected(blk, H); + + EXPECT_EQ(r.registered, 0u); + EXPECT_EQ(st.mnstates().size(), 1u); + EXPECT_TRUE(m.live()) << "no-op block must not drop the live bundle"; +} + +// A block whose non-coinbase tx spends the sole MN's collateral removes it +// (apply_block pass 2). The now-empty set cannot back a masternode payee, so +// the maintainer drops the embedded bundle and get_work falls back to dashd. +TEST(DashCoinStateMaintainer, BlockConnectCollateralSpendDropsToFallback) { + NodeCoinState st; + 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()); + ASSERT_EQ(st.mnstates().size(), 1u); + + BlockType blk; + blk.m_txs.push_back(make_spend(raw256(0x90), 0, 500000000, 1)); // cb (idx 0, skipped) + blk.m_txs.push_back(make_spend(coll, 3, 400000000, 2)); // spends the MN collateral + m.on_block_connected(blk, H); + + EXPECT_EQ(st.mnstates().size(), 0u); + EXPECT_FALSE(m.live()) << "collateral spend emptied the DMN set; bundle must drop to fallback"; + + bool fb = false; + WorkSelection sel = st.select_work([&]() { fb = true; return DashWorkData{}; }); + EXPECT_EQ(sel.source, WorkSource::DashdFallback); + EXPECT_TRUE(fb) << "empty DMN set must route get_work to the dashd RPC fallback"; +}