Skip to content

Commit 0fbc143

Browse files
authored
Merge pull request #686 from frstrtr/dash/s8-reception-wire-tip-leg
dash(S8): reception-wire leg 2 — interfaces::Node::new_tip → CoinStateMaintainer::on_new_tip
2 parents 10626e3 + 4854d02 commit 0fbc143

3 files changed

Lines changed: 350 additions & 2 deletions

File tree

src/impl/dash/coin/node_interface.hpp

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <core/uint256.hpp>
88
#include <core/events.hpp>
99

10+
#include <cstdint>
1011
#include <map>
1112
#include <vector>
1213

@@ -15,6 +16,24 @@ namespace dash
1516
namespace interfaces
1617
{
1718

19+
/// Header/think path payload: the active chain tip advanced. Carries the exact
20+
/// inputs build_embedded_workdata() needs that a bare best_block_hash cannot --
21+
/// the height/hash to build the next block ON, the next-block work target
22+
/// (bits), the tip median-time-past for time bounds, and the coin address
23+
/// versions for coinbase-payee encoding. curtime/version default to 0 so the
24+
/// shaper applies its own SAFE-ADDITIVE defaults.
25+
struct TipAdvance
26+
{
27+
uint32_t prev_height{0};
28+
uint256 prev_hash;
29+
uint32_t bits_for_next{0};
30+
uint32_t mtp_at_tip{0};
31+
uint8_t address_version{0};
32+
uint8_t address_p2sh_version{0};
33+
uint32_t curtime{0};
34+
uint32_t version{0};
35+
};
36+
1837
struct Node
1938
{
2039
Variable<uint256> best_block_hash;
@@ -23,16 +42,22 @@ struct Node
2342
Event<std::vector<coin::BlockHeaderType>> new_headers;
2443
Event<coin::BlockType> full_block;
2544

45+
// Header/think path: fires when the active chain tip advances, carrying the
46+
// embedded-template params (see TipAdvance). The reception wire subscribes
47+
// CoinStateMaintainer::on_new_tip to this so the node-held bundle arms its
48+
// tip-readiness prerequisite without a direct poke.
49+
Event<TipAdvance> new_tip;
50+
2651
// SPV A1 (parity audit): fires when dashd announces a ChainLock has
2752
// been aggregated for a block. Carries {block_hash, height}.
2853
// Consumers (e.g. block-find submit handler) can consult
2954
// m_chainlocked_blocks to know whether a found block is now
3055
// irreversible.
3156
Event<std::pair<uint256, int32_t>> new_chainlock;
32-
std::map<uint256, int32_t> chainlocked_blocks; // block_hash height
57+
std::map<uint256, int32_t> chainlocked_blocks; // block_hash -> height
3358

3459
std::map<uint256, coin::Transaction> known_txs;
3560
};
3661

3762
} // namespace interfaces
38-
} // namespace dash
63+
} // namespace dash

src/impl/dash/coin/tip_ingest.hpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ===========================================================================
4+
// dash reception wire (leg 2 of 4) -- interfaces::Node::new_tip -> maintainer.
5+
//
6+
// #672..#685 landed the node-held embedded coin-state bundle + its async
7+
// CoinStateMaintainer; wire_mempool_ingest (leg 1) subscribed the mempool-relay
8+
// event. This leg subscribes the header/think TIP-ADVANCE event: every tip
9+
// advance is routed to CoinStateMaintainer::on_new_tip(), which stashes the tip
10+
// params and marks tip-readiness -- one of the two prerequisites (the MN list
11+
// is the other) that must BOTH be present before the node-held bundle publishes
12+
// and select_work() flips off the retained dashd getblocktemplate fallback.
13+
//
14+
// PAYLOAD: unlike a bare best_block_hash, interfaces::Node::new_tip carries a
15+
// TipAdvance -- the height/hash to build ON, next-block bits, tip MTP, and the
16+
// coin address versions -- exactly build_embedded_workdata() inputs. That
17+
// struct is the interface-shape addition this leg makes; the leg 1 note flagged
18+
// that on_new_tip needs params best_block_hash does not carry, and this is that
19+
// payload, kept to the dash interface only (single-coin, purely additive).
20+
//
21+
// STILL UNWIRED (own slices): on_mn_list_update has no source event on
22+
// interfaces::Node yet; on_block_connected needs the connected block height,
23+
// which full_block does not carry.
24+
//
25+
// LIFETIME: the handler captures maint by reference, so maint (and the
26+
// NodeCoinState it drives) MUST outlive node. The returned EventDisposable
27+
// lets a caller tear the subscription down explicitly; it does NOT auto-dispose
28+
// on drop -- identical contract to wire_mempool_ingest.
29+
//
30+
// SCC: pulls node_interface.hpp (transaction/block codec via TipAdvance
31+
// siblings), so include this header ONLY from a TU that already links the full
32+
// dash codec (main_dash + this leg KAT), never a guard-weight TU.
33+
// ===========================================================================
34+
#include <memory>
35+
36+
#include <core/events.hpp>
37+
38+
#include "node_interface.hpp" // dash::interfaces::Node (new_tip feed) + TipAdvance
39+
#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_new_tip
40+
41+
namespace c2pool::dash
42+
{
43+
44+
// Subscribe maint to node.new_tip: every tip advance stashes the embedded
45+
// template params and marks tip-readiness via on_new_tip(); the maintainer
46+
// republishes the node-held bundle only once the MN list is ALSO seeded, so a
47+
// tip arriving before the first mnlistdiff leaves the bundle on the dashd
48+
// fallback. Returns the subscription handle so the caller controls teardown.
49+
inline std::shared_ptr<EventDisposable>
50+
wire_tip_ingest(::dash::interfaces::Node& node,
51+
::dash::coin::CoinStateMaintainer& maint)
52+
{
53+
return node.new_tip.subscribe(
54+
[&maint](const ::dash::interfaces::TipAdvance& t)
55+
{
56+
maint.on_new_tip(t.prev_height, t.prev_hash, t.bits_for_next,
57+
t.mtp_at_tip, t.address_version, t.address_p2sh_version,
58+
t.curtime, t.version);
59+
});
60+
}
61+
62+
} // namespace c2pool::dash
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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

Comments
 (0)