Skip to content

Commit ab249df

Browse files
authored
Merge pull request #694 from frstrtr/dash/s8-reception-wire-mnlist-leg
dash(S8): reception-wire leg 4 -- mn_list_update -> on_mn_list_update (closes the 4-leg wire)
2 parents a20c558 + 8a4a768 commit ab249df

3 files changed

Lines changed: 186 additions & 1 deletion

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ===========================================================================
4+
// dash reception wire (leg 4 of 4) -- interfaces::Node::mn_list_update ->
5+
// maintainer.
6+
//
7+
// Legs 1-3 wired the mempool-relay (new_tx), tip-advance (new_tip) and block-
8+
// connect (block_connected) events into CoinStateMaintainer. This CLOSES the
9+
// wire with the mnlistdiff RESYNC leg: every full masternode-set snapshot
10+
// dashd delivers is routed to CoinStateMaintainer::on_mn_list_update(), which
11+
// REPLACES the DMN set the embedded coinbase pays wholesale (the authoritative
12+
// bulk feed), as opposed to leg 3s on_block_connected() which folds per-block
13+
// special-tx deltas INCREMENTALLY between snapshots. Together the two keep the
14+
// masternode set both correct at each snapshot and current between them.
15+
//
16+
// PAYLOAD: on_mn_list_update() takes the full projected set as a
17+
// vector<pair<uint256, MNState>>; leg 1s scope note called out that this leg
18+
// "has no source event on interfaces::Node at all". That source event
19+
// (mn_list_update, carrying MnListUpdate) is the interface-shape addition this
20+
// leg makes -- purely additive, dash interface only (single-coin), mirroring
21+
// leg 2s TipAdvance and leg 3s BlockConnected. An EMPTY snapshot is forwarded
22+
// verbatim: on_mn_list_update() treats it as a set-gap and demotes the bundle
23+
// to the retained dashd fallback rather than backing a template with no payee.
24+
//
25+
// LIFETIME: the handler captures maint by reference, so maint (and the
26+
// NodeCoinState it drives) MUST outlive node. The returned EventDisposable lets
27+
// a caller tear the subscription down explicitly; it does NOT auto-dispose on
28+
// drop -- identical contract to wire_mempool_ingest / wire_tip_ingest /
29+
// wire_block_connect_ingest.
30+
//
31+
// SCC: pulls node_interface.hpp (which now carries MnListUpdate -> MNState via
32+
// mn_state_machine.hpp) and coin_state_maintainer.hpp, so include this header
33+
// ONLY from a TU that already links the full dash codec (main_dash + this leg
34+
// KAT), never a guard-weight TU.
35+
// ===========================================================================
36+
#include <memory>
37+
#include <utility>
38+
39+
#include <core/events.hpp>
40+
41+
#include "node_interface.hpp" // dash::interfaces::Node (mn_list_update feed) + MnListUpdate
42+
#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_mn_list_update
43+
44+
namespace c2pool::dash
45+
{
46+
47+
// Subscribe maint to node.mn_list_update: every mnlistdiff snapshot RESYNCS the
48+
// DMN set via on_mn_list_update(mnstates), which republishes when the set is
49+
// non-empty (MN-readiness met) or demotes the node-held bundle to the dashd
50+
// fallback when the snapshot is empty (set-gap). Returns the subscription
51+
// handle so the caller controls teardown.
52+
inline std::shared_ptr<EventDisposable>
53+
wire_mn_list_ingest(::dash::interfaces::Node& node,
54+
::dash::coin::CoinStateMaintainer& maint)
55+
{
56+
return node.mn_list_update.subscribe(
57+
[&maint](const ::dash::interfaces::MnListUpdate& u)
58+
{
59+
maint.on_mn_list_update(u.mnstates);
60+
});
61+
}
62+
63+
} // namespace c2pool::dash

src/impl/dash/coin/node_interface.hpp

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

44
#include "block.hpp"
55
#include "transaction.hpp"
6+
#include "mn_state_machine.hpp"
67

78
#include <core/uint256.hpp>
89
#include <core/events.hpp>
910

1011
#include <cstdint>
1112
#include <map>
13+
#include <utility>
1214
#include <vector>
1315

1416
namespace dash
@@ -40,14 +42,31 @@ struct TipAdvance
4042
/// special-tx height is a chain-position input apply_block cannot recover from
4143
/// the block alone). block_connected pairs the two so the reception wire feeds
4244
/// apply_block the exact (block, height) apply_block expects -- purely additive,
43-
/// dash interface only (leg 2's new_tip added TipAdvance for the same reason a
45+
/// dash interface only (leg 2s new_tip added TipAdvance for the same reason a
4446
/// bare best_block_hash was insufficient).
4547
struct BlockConnected
4648
{
4749
coin::BlockType block;
4850
uint32_t height{0};
4951
};
5052

53+
/// Reception path payload (mnlistdiff): the AUTHORITATIVE masternode-set
54+
/// snapshot the embedded coinbase pays. dashd (protx diff / the qdata
55+
/// mnlistdiff message) yields the full projected DMN set as
56+
/// (proTxHash -> MNState) pairs; CoinStateMaintainer::on_mn_list_update()
57+
/// takes exactly that vector. This is the bulk RESYNC feed -- distinct from
58+
/// leg 3s block_connected, which folds per-block special txs INCREMENTALLY
59+
/// between snapshots. An empty vector is a set-gap signal (see on_mn_list_
60+
/// update): it cannot back a payee, so it demotes the bundle to the dashd
61+
/// fallback rather than publishing a template with a phantom payment. Purely
62+
/// additive, dash interface only -- carries dash::coin::MNState, so this
63+
/// header now pulls mn_state_machine.hpp (dash-coin scoped, no cross-coin
64+
/// reach), matching the codec weight it already takes from block.hpp.
65+
struct MnListUpdate
66+
{
67+
std::vector<std::pair<uint256, coin::MNState>> mnstates;
68+
};
69+
5170
struct Node
5271
{
5372
Variable<uint256> best_block_hash;
@@ -70,6 +89,14 @@ struct Node
7089
// fallback rather than backing a template with a phantom payee.
7190
Event<BlockConnected> block_connected;
7291

92+
// Reception path: fires when dashd delivers a full mnlistdiff snapshot,
93+
// carrying the authoritative projected DMN set (see MnListUpdate). The
94+
// reception wire subscribes CoinStateMaintainer::on_mn_list_update to this
95+
// so the masternode set the embedded coinbase pays is bulk-RESYNCED off the
96+
// real feed (block_connected only folds per-block deltas between snapshots);
97+
// an empty snapshot demotes the bundle to the dashd fallback.
98+
Event<MnListUpdate> mn_list_update;
99+
73100
// SPV A1 (parity audit): fires when dashd announces a ChainLock has
74101
// been aggregated for a block. Carries {block_hash, height}.
75102
// Consumers (e.g. block-find submit handler) can consult

test/test_dash_node_reception_wire.cpp

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include <impl/dash/coin/mempool_ingest.hpp> // c2pool::dash::wire_mempool_ingest
3333
#include <impl/dash/coin/tip_ingest.hpp> // c2pool::dash::wire_tip_ingest
3434
#include <impl/dash/coin/block_connect_ingest.hpp> // c2pool::dash::wire_block_connect_ingest
35+
#include <impl/dash/coin/mn_list_ingest.hpp> // c2pool::dash::wire_mn_list_ingest
3536
#include <impl/dash/coin/coin_state_maintainer.hpp>
3637
#include <impl/dash/coin/node_coin_state.hpp>
3738
#include <impl/dash/coin/embedded_gbt.hpp>
@@ -55,6 +56,7 @@
5556
using c2pool::dash::wire_mempool_ingest;
5657
using c2pool::dash::wire_tip_ingest;
5758
using c2pool::dash::wire_block_connect_ingest;
59+
using c2pool::dash::wire_mn_list_ingest;
5860
using dash::coin::CoinStateMaintainer;
5961
using dash::coin::NodeCoinState;
6062
using dash::coin::WorkSource;
@@ -377,3 +379,96 @@ TEST(DashReceptionWire, DisposeStopsBlockConnectIngest) {
377379
EXPECT_EQ(st.mnstates().size(), 1u) << "after dispose, a connected block must not be applied";
378380
EXPECT_TRUE(m.live()) << "after dispose, the armed bundle must stay live";
379381
}
382+
383+
// ========================================================================
384+
// Leg 4: an mn_list_update snapshot fired on the interface RESYNCS the DMN set
385+
// THROUGH THE WIRE -- no direct on_mn_list_update() poke. With a tip already
386+
// present, the wired mnlistdiff snapshot arms MN-readiness and select_work
387+
// flips to the embedded arm. This is the authoritative bulk feed (leg 3's
388+
// block_connected only folds per-block deltas between these snapshots).
389+
// ========================================================================
390+
TEST(DashReceptionWire, MnListUpdateRelaySeedsMnSetThroughWire) {
391+
UTXOViewCache utxo(nullptr);
392+
dash::interfaces::Node node;
393+
NodeCoinState st;
394+
st.mempool().set_utxo(&utxo);
395+
CoinStateMaintainer m(st);
396+
397+
auto sub = wire_mn_list_ingest(node, m);
398+
ASSERT_TRUE(sub) << "wire must return a live subscription handle";
399+
400+
// Tip arrives first (reception is async); the MN list is the gating event.
401+
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
402+
EXPECT_FALSE(m.live()) << "tip alone must not arm the bundle -- MN list absent";
403+
ASSERT_EQ(st.mnstates().size(), 0u);
404+
405+
// mnlistdiff snapshot fired on the interface (NOT a direct maintainer poke).
406+
dash::interfaces::MnListUpdate u;
407+
u.mnstates = single_mn(p2pkh_script(0x30));
408+
node.mn_list_update.happened(u);
409+
410+
EXPECT_EQ(st.mnstates().size(), 1u) << "mnlistdiff relay (via wire) must seed the DMN set";
411+
ASSERT_TRUE(m.live()) << "tip + MN (via wire) must arm the embedded bundle";
412+
413+
bool fb = false;
414+
WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; });
415+
EXPECT_EQ(sel.source, WorkSource::Embedded);
416+
EXPECT_FALSE(fb) << "embedded arm must not invoke the dashd fallback";
417+
}
418+
419+
// ========================================================================
420+
// An EMPTY mnlistdiff snapshot fired on the wire is a set-gap: on_mn_list_update
421+
// clears MN-readiness and demotes an armed bundle to the retained dashd
422+
// fallback rather than backing a template with no masternode payee.
423+
// ========================================================================
424+
TEST(DashReceptionWire, EmptyMnListRelayDemotesThroughWire) {
425+
UTXOViewCache utxo(nullptr);
426+
dash::interfaces::Node node;
427+
NodeCoinState st;
428+
st.mempool().set_utxo(&utxo);
429+
CoinStateMaintainer m(st);
430+
431+
auto sub = wire_mn_list_ingest(node, m);
432+
433+
// Arm the bundle: tip + a non-empty snapshot delivered on the wire.
434+
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
435+
dash::interfaces::MnListUpdate seed;
436+
seed.mnstates = single_mn(p2pkh_script(0x30));
437+
node.mn_list_update.happened(seed);
438+
ASSERT_TRUE(m.live());
439+
ASSERT_EQ(st.mnstates().size(), 1u);
440+
441+
// An empty snapshot fired on the wire -> set-gap -> demote.
442+
dash::interfaces::MnListUpdate empty; // empty.mnstates == {}
443+
node.mn_list_update.happened(empty);
444+
445+
EXPECT_EQ(st.mnstates().size(), 0u) << "empty snapshot must clear the DMN set";
446+
EXPECT_FALSE(m.live()) << "empty mnlistdiff (set-gap) must demote the live bundle";
447+
448+
bool fb = false;
449+
WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; });
450+
EXPECT_EQ(sel.source, WorkSource::DashdFallback);
451+
EXPECT_TRUE(fb) << "after an empty snapshot, get_work must fall back to dashd";
452+
}
453+
454+
// ========================================================================
455+
// Disposing the mn-list handle tears the subscription down: a later snapshot is
456+
// not applied, so a stale mnlistdiff feed cannot silently re-seed the DMN set.
457+
// ========================================================================
458+
TEST(DashReceptionWire, DisposeStopsMnListIngest) {
459+
UTXOViewCache utxo(nullptr);
460+
dash::interfaces::Node node;
461+
NodeCoinState st;
462+
st.mempool().set_utxo(&utxo);
463+
CoinStateMaintainer m(st);
464+
465+
auto sub = wire_mn_list_ingest(node, m);
466+
sub->dispose();
467+
468+
dash::interfaces::MnListUpdate u;
469+
u.mnstates = single_mn(p2pkh_script(0x30));
470+
node.mn_list_update.happened(u);
471+
472+
EXPECT_EQ(st.mnstates().size(), 0u)
473+
<< "after dispose, an mnlistdiff snapshot must not seed the set";
474+
}

0 commit comments

Comments
 (0)