|
| 1 | +// SPDX-License-Identifier: AGPL-3.0-or-later |
| 2 | +/// Phase C-TEMPLATE step 9 -- reception-wire KAT (leg 1: mempool relay). |
| 3 | +/// |
| 4 | +/// #672..#685 landed the node-held embedded coin-state bundle + its async |
| 5 | +/// CoinStateMaintainer, and test_dash_coin_state_maintainer / _node_embedded_wire |
| 6 | +/// proved that DRIVING the maintainer's on_*() methods flips select_work() to |
| 7 | +/// the embedded arm. But every one of those suites poked the maintainer DIRECTLY |
| 8 | +/// -- nothing subscribed a live interfaces::Node's reception events to it, so in |
| 9 | +/// a running node the arm could never flip on its own. wire_mempool_ingest() |
| 10 | +/// (src/impl/dash/coin/mempool_ingest.hpp) closes the FIRST of the four legs: |
| 11 | +/// interfaces::Node::new_tx -> CoinStateMaintainer::on_mempool_tx. This suite |
| 12 | +/// proves that subscription end-to-end, off the real Event, with no direct poke: |
| 13 | +/// |
| 14 | +/// * a new_tx relay fired on the interface FOLDS into the maintainer's mempool |
| 15 | +/// (size 0 -> 1) -- the reception path, not a test poke, drives the state; |
| 16 | +/// * disposing the returned handle tears the subscription down: a later relay |
| 17 | +/// is NOT folded (size stays put) -- teardown is honoured; |
| 18 | +/// * a relayed tx reaches the assembled embedded template once MN+tip arm the |
| 19 | +/// bundle (select_work -> WorkSource::Embedded, tx present in m_tx_hashes), |
| 20 | +/// and an on_invalidate() reorg demotes back to the retained dashd fallback. |
| 21 | +/// |
| 22 | +/// Seeding mirrors test_dash_coin_state_maintainer.cpp exactly so the suites pin |
| 23 | +/// the SAME projection. Scope-honest: only the new_tx leg is wired here; the |
| 24 | +/// on_block_connected / on_new_tip / on_mn_list_update legs need payload the |
| 25 | +/// interface does not yet carry (block height / tip params / a mnlistdiff event) |
| 26 | +/// and land in their own slices -- their maintainer methods are still exercised |
| 27 | +/// directly here only to arm the bundle for the template-reach assertion. |
| 28 | + |
| 29 | +#include <gtest/gtest.h> |
| 30 | + |
| 31 | +#include <impl/dash/coin/node_interface.hpp> // dash::interfaces::Node |
| 32 | +#include <impl/dash/coin/mempool_ingest.hpp> // c2pool::dash::wire_mempool_ingest |
| 33 | +#include <impl/dash/coin/tip_ingest.hpp> // c2pool::dash::wire_tip_ingest |
| 34 | +#include <impl/dash/coin/coin_state_maintainer.hpp> |
| 35 | +#include <impl/dash/coin/node_coin_state.hpp> |
| 36 | +#include <impl/dash/coin/embedded_gbt.hpp> |
| 37 | +#include <impl/dash/coin/mn_state_machine.hpp> |
| 38 | +#include <impl/dash/coin/mempool.hpp> |
| 39 | +#include <impl/dash/coin/utxo_adapter.hpp> |
| 40 | +#include <impl/dash/coin/rpc_data.hpp> |
| 41 | +#include <impl/dash/coin/transaction.hpp> |
| 42 | + |
| 43 | +#include <core/uint256.hpp> |
| 44 | +#include <core/events.hpp> |
| 45 | + |
| 46 | +#include <array> |
| 47 | +#include <cstdint> |
| 48 | +#include <cstring> |
| 49 | +#include <memory> |
| 50 | +#include <utility> |
| 51 | +#include <vector> |
| 52 | + |
| 53 | +using c2pool::dash::wire_mempool_ingest; |
| 54 | +using c2pool::dash::wire_tip_ingest; |
| 55 | +using dash::coin::CoinStateMaintainer; |
| 56 | +using dash::coin::NodeCoinState; |
| 57 | +using dash::coin::WorkSource; |
| 58 | +using dash::coin::WorkSelection; |
| 59 | +using dash::coin::MNState; |
| 60 | +using dash::coin::MutableTransaction; |
| 61 | +using dash::coin::Transaction; |
| 62 | +using ::core::coin::UTXOViewCache; |
| 63 | +using ::core::coin::Outpoint; |
| 64 | +using ::core::coin::Coin; |
| 65 | +using ::bitcoin_family::coin::TxIn; |
| 66 | +using ::bitcoin_family::coin::TxOut; |
| 67 | + |
| 68 | +static constexpr uint8_t DASH_PUBKEY_VER = 76; |
| 69 | +static constexpr uint8_t DASH_P2SH_VER = 16; |
| 70 | +static constexpr uint32_t H = 2'400'000; // past MN_RR: platform burn active |
| 71 | + |
| 72 | +static uint256 raw256(uint8_t base) { |
| 73 | + uint256 h; |
| 74 | + std::array<uint8_t, 32> p{}; |
| 75 | + for (size_t i = 0; i < 32; ++i) p[i] = static_cast<uint8_t>(base + i); |
| 76 | + std::memcpy(h.data(), p.data(), 32); |
| 77 | + return h; |
| 78 | +} |
| 79 | + |
| 80 | +static std::vector<unsigned char> p2pkh_script(uint8_t hashseed) { |
| 81 | + std::vector<unsigned char> s{0x76, 0xa9, 0x14}; |
| 82 | + for (int i = 0; i < 20; ++i) s.push_back(static_cast<unsigned char>(hashseed + i)); |
| 83 | + s.push_back(0x88); s.push_back(0xac); |
| 84 | + return s; |
| 85 | +} |
| 86 | + |
| 87 | +static MutableTransaction make_spend(const uint256& prev, uint32_t idx, |
| 88 | + int64_t out_value, uint32_t salt) { |
| 89 | + MutableTransaction tx; |
| 90 | + tx.version = 1; tx.type = 0; tx.locktime = salt; |
| 91 | + TxIn in; in.prevout.hash = prev; in.prevout.index = idx; |
| 92 | + in.sequence = 0xffffffffu; |
| 93 | + tx.vin.push_back(in); |
| 94 | + TxOut o; o.value = out_value; |
| 95 | + tx.vout.push_back(o); |
| 96 | + return tx; |
| 97 | +} |
| 98 | + |
| 99 | +static std::vector<std::pair<uint256, MNState>> single_mn(const std::vector<unsigned char>& payout) { |
| 100 | + MNState s; |
| 101 | + s.isValid = true; |
| 102 | + s.nRegisteredHeight = 2'300'000; |
| 103 | + s.nLastPaidHeight = 0; |
| 104 | + s.scriptPayout.m_data = payout; |
| 105 | + return std::vector<std::pair<uint256, MNState>>{{raw256(0x01), s}}; |
| 106 | +} |
| 107 | + |
| 108 | +static const uint256 PREV_HASH = raw256(0xAB); |
| 109 | +static const uint32_t BITS = 0x1b104be3u; |
| 110 | +static const uint32_t MTP = 1'700'000'000u; |
| 111 | +static const uint32_t CURTIME = 1'700'000'123u; |
| 112 | +static const uint32_t VERSION = 0x20000000u; |
| 113 | + |
| 114 | +// ════════════════════════════════════════════════════════════════════════ |
| 115 | +// Leg 1: a new_tx relay fired on the interface folds through the maintainer. |
| 116 | +// No direct on_mempool_tx() poke -- the Event drives it. |
| 117 | +// ════════════════════════════════════════════════════════════════════════ |
| 118 | +TEST(DashReceptionWire, NewTxRelayFoldsThroughMaintainer) { |
| 119 | + UTXOViewCache utxo(nullptr); |
| 120 | + const uint256 prev = raw256(0x77); |
| 121 | + utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, 1, false)); |
| 122 | + |
| 123 | + dash::interfaces::Node node; |
| 124 | + NodeCoinState st; |
| 125 | + st.mempool().set_utxo(&utxo); |
| 126 | + CoinStateMaintainer m(st); |
| 127 | + |
| 128 | + auto sub = wire_mempool_ingest(node, m); |
| 129 | + ASSERT_TRUE(sub) << "wire must return a live subscription handle"; |
| 130 | + ASSERT_EQ(st.mempool().size(), 0u); |
| 131 | + |
| 132 | + // Fire the reception event (NOT a direct maintainer poke). |
| 133 | + node.new_tx.happened(Transaction(make_spend(prev, 0, 90'000, /*salt=*/1))); |
| 134 | + |
| 135 | + EXPECT_EQ(st.mempool().size(), 1u) << "new_tx relay must fold into the mempool"; |
| 136 | +} |
| 137 | + |
| 138 | +// ════════════════════════════════════════════════════════════════════════ |
| 139 | +// Disposing the handle tears the subscription down: later relays are dropped. |
| 140 | +// ════════════════════════════════════════════════════════════════════════ |
| 141 | +TEST(DashReceptionWire, DisposeStopsIngest) { |
| 142 | + UTXOViewCache utxo(nullptr); |
| 143 | + const uint256 a = raw256(0x77); |
| 144 | + const uint256 b = raw256(0x66); |
| 145 | + utxo.add_coin(Outpoint(a, 0), Coin(100'000, {}, 1, false)); |
| 146 | + utxo.add_coin(Outpoint(b, 0), Coin(100'000, {}, 1, false)); |
| 147 | + |
| 148 | + dash::interfaces::Node node; |
| 149 | + NodeCoinState st; |
| 150 | + st.mempool().set_utxo(&utxo); |
| 151 | + CoinStateMaintainer m(st); |
| 152 | + |
| 153 | + auto sub = wire_mempool_ingest(node, m); |
| 154 | + node.new_tx.happened(Transaction(make_spend(a, 0, 90'000, /*salt=*/1))); |
| 155 | + ASSERT_EQ(st.mempool().size(), 1u); |
| 156 | + |
| 157 | + sub->dispose(); |
| 158 | + node.new_tx.happened(Transaction(make_spend(b, 0, 90'000, /*salt=*/2))); |
| 159 | + EXPECT_EQ(st.mempool().size(), 1u) << "after dispose, no further relay may fold"; |
| 160 | +} |
| 161 | + |
| 162 | +// ════════════════════════════════════════════════════════════════════════ |
| 163 | +// A relayed tx reaches the assembled embedded template once MN+tip arm the |
| 164 | +// bundle; an on_invalidate() reorg demotes back to the retained dashd fallback. |
| 165 | +// (MN/tip legs are still poked directly -- only new_tx is wired in this slice.) |
| 166 | +// ════════════════════════════════════════════════════════════════════════ |
| 167 | +TEST(DashReceptionWire, RelayedTxReachesEmbeddedTemplateThenInvalidateDemotes) { |
| 168 | + UTXOViewCache utxo(nullptr); |
| 169 | + const uint256 prev = raw256(0x77); |
| 170 | + utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, 1, false)); |
| 171 | + |
| 172 | + dash::interfaces::Node node; |
| 173 | + NodeCoinState st; |
| 174 | + st.mempool().set_utxo(&utxo); |
| 175 | + CoinStateMaintainer m(st); |
| 176 | + auto sub = wire_mempool_ingest(node, m); |
| 177 | + |
| 178 | + // Reception leg feeds the mempool; MN + tip arm the bundle. |
| 179 | + node.new_tx.happened(Transaction(make_spend(prev, 0, 90'000, /*salt=*/1))); // fee 10'000 |
| 180 | + m.on_mn_list_update(single_mn(p2pkh_script(0x30))); |
| 181 | + m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION); |
| 182 | + ASSERT_TRUE(m.live()); |
| 183 | + |
| 184 | + bool fb = false; |
| 185 | + WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; }); |
| 186 | + EXPECT_EQ(sel.source, WorkSource::Embedded); |
| 187 | + EXPECT_FALSE(fb) << "embedded arm must not invoke the dashd fallback"; |
| 188 | + EXPECT_EQ(sel.work.m_height, H); |
| 189 | + ASSERT_EQ(st.mempool().size(), 1u); |
| 190 | + EXPECT_EQ(sel.work.m_tx_hashes.size(), 1u) |
| 191 | + << "the RELAYED tx must reach the assembled embedded template"; |
| 192 | + |
| 193 | + // Reorg: on_invalidate drops the tip -> next get_work falls back to dashd. |
| 194 | + m.on_invalidate(); |
| 195 | + EXPECT_FALSE(m.live()); |
| 196 | + bool fb2 = false; |
| 197 | + WorkSelection sel2 = st.select_work([&]() { fb2 = true; return dash::coin::DashWorkData{}; }); |
| 198 | + EXPECT_EQ(sel2.source, WorkSource::DashdFallback); |
| 199 | + EXPECT_TRUE(fb2) << "after invalidate, the retained dashd fallback must run"; |
| 200 | +} |
| 201 | + |
| 202 | +// ════════════════════════════════════════════════════════════════════════ |
| 203 | +// Leg 2: a new_tip advance fired on the interface arms the maintainer tip- |
| 204 | +// readiness THROUGH THE WIRE -- no direct on_new_tip() poke. A tip arriving |
| 205 | +// before the first mnlistdiff must NOT go live (MN list absent); once the MN |
| 206 | +// list seeds the other prerequisite the bundle arms and select_work flips to |
| 207 | +// the embedded arm, and the WIRED tip params reach the assembled template. |
| 208 | +// ════════════════════════════════════════════════════════════════════════ |
| 209 | +TEST(DashReceptionWire, NewTipRelayArmsBundleOnceMnSeeded) { |
| 210 | + UTXOViewCache utxo(nullptr); |
| 211 | + dash::interfaces::Node node; |
| 212 | + NodeCoinState st; |
| 213 | + st.mempool().set_utxo(&utxo); |
| 214 | + CoinStateMaintainer m(st); |
| 215 | + |
| 216 | + auto sub = wire_tip_ingest(node, m); |
| 217 | + ASSERT_TRUE(sub) << "wire must return a live subscription handle"; |
| 218 | + |
| 219 | + // Tip arrives first (reception is async): tip-readiness is set via the |
| 220 | + // wire, but the MN list is still empty, so the bundle stays on dashd. |
| 221 | + dash::interfaces::TipAdvance t; |
| 222 | + t.prev_height = H - 1; t.prev_hash = PREV_HASH; t.bits_for_next = BITS; |
| 223 | + t.mtp_at_tip = MTP; t.address_version = DASH_PUBKEY_VER; |
| 224 | + t.address_p2sh_version = DASH_P2SH_VER; t.curtime = CURTIME; t.version = VERSION; |
| 225 | + node.new_tip.happened(t); |
| 226 | + EXPECT_FALSE(m.live()) << "tip alone must not arm the bundle -- MN list absent"; |
| 227 | + |
| 228 | + // MN list seeds the second prerequisite -> bundle arms, embedded arm wins. |
| 229 | + m.on_mn_list_update(single_mn(p2pkh_script(0x30))); |
| 230 | + ASSERT_TRUE(m.live()) << "tip (via wire) + MN must arm the embedded bundle"; |
| 231 | + |
| 232 | + bool fb = false; |
| 233 | + WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; }); |
| 234 | + EXPECT_EQ(sel.source, WorkSource::Embedded); |
| 235 | + EXPECT_FALSE(fb) << "embedded arm must not invoke the dashd fallback"; |
| 236 | + EXPECT_EQ(sel.work.m_height, H) << "the WIRED tip params must reach the template"; |
| 237 | +} |
| 238 | + |
| 239 | +// ════════════════════════════════════════════════════════════════════════ |
| 240 | +// Disposing the tip handle tears the subscription down: a later tip advance is |
| 241 | +// not applied, so a post-reorg bundle cannot silently re-arm off a stale feed. |
| 242 | +// ════════════════════════════════════════════════════════════════════════ |
| 243 | +TEST(DashReceptionWire, DisposeStopsTipIngest) { |
| 244 | + UTXOViewCache utxo(nullptr); |
| 245 | + dash::interfaces::Node node; |
| 246 | + NodeCoinState st; |
| 247 | + st.mempool().set_utxo(&utxo); |
| 248 | + CoinStateMaintainer m(st); |
| 249 | + |
| 250 | + // MN present up front; the tip is the gating event under test. |
| 251 | + m.on_mn_list_update(single_mn(p2pkh_script(0x30))); |
| 252 | + auto sub = wire_tip_ingest(node, m); |
| 253 | + sub->dispose(); |
| 254 | + |
| 255 | + dash::interfaces::TipAdvance t; |
| 256 | + t.prev_height = H - 1; t.prev_hash = PREV_HASH; t.bits_for_next = BITS; |
| 257 | + t.mtp_at_tip = MTP; t.address_version = DASH_PUBKEY_VER; |
| 258 | + t.address_p2sh_version = DASH_P2SH_VER; t.curtime = CURTIME; t.version = VERSION; |
| 259 | + node.new_tip.happened(t); |
| 260 | + EXPECT_FALSE(m.live()) << "after dispose, a tip advance must not arm the bundle"; |
| 261 | +} |
0 commit comments