Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/impl/dash/coin/mn_list_ingest.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once
// ===========================================================================
// dash reception wire (leg 4 of 4) -- interfaces::Node::mn_list_update ->
// maintainer.
//
// Legs 1-3 wired the mempool-relay (new_tx), tip-advance (new_tip) and block-
// connect (block_connected) events into CoinStateMaintainer. This CLOSES the
// wire with the mnlistdiff RESYNC leg: every full masternode-set snapshot
// dashd delivers is routed to CoinStateMaintainer::on_mn_list_update(), which
// REPLACES the DMN set the embedded coinbase pays wholesale (the authoritative
// bulk feed), as opposed to leg 3s on_block_connected() which folds per-block
// special-tx deltas INCREMENTALLY between snapshots. Together the two keep the
// masternode set both correct at each snapshot and current between them.
//
// PAYLOAD: on_mn_list_update() takes the full projected set as a
// vector<pair<uint256, MNState>>; leg 1s scope note called out that this leg
// "has no source event on interfaces::Node at all". That source event
// (mn_list_update, carrying MnListUpdate) is the interface-shape addition this
// leg makes -- purely additive, dash interface only (single-coin), mirroring
// leg 2s TipAdvance and leg 3s BlockConnected. An EMPTY snapshot is forwarded
// verbatim: on_mn_list_update() treats it as a set-gap and demotes the bundle
// to the retained dashd fallback rather than backing a template with no payee.
//
// 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 /
// wire_block_connect_ingest.
//
// SCC: pulls node_interface.hpp (which now carries MnListUpdate -> MNState via
// mn_state_machine.hpp) and coin_state_maintainer.hpp, 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 <memory>
#include <utility>

#include <core/events.hpp>

#include "node_interface.hpp" // dash::interfaces::Node (mn_list_update feed) + MnListUpdate
#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_mn_list_update

namespace c2pool::dash
{

// Subscribe maint to node.mn_list_update: every mnlistdiff snapshot RESYNCS the
// DMN set via on_mn_list_update(mnstates), which republishes when the set is
// non-empty (MN-readiness met) or demotes the node-held bundle to the dashd
// fallback when the snapshot is empty (set-gap). Returns the subscription
// handle so the caller controls teardown.
inline std::shared_ptr<EventDisposable>
wire_mn_list_ingest(::dash::interfaces::Node& node,
::dash::coin::CoinStateMaintainer& maint)
{
return node.mn_list_update.subscribe(
[&maint](const ::dash::interfaces::MnListUpdate& u)
{
maint.on_mn_list_update(u.mnstates);
});
}

} // namespace c2pool::dash
29 changes: 28 additions & 1 deletion src/impl/dash/coin/node_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

#include "block.hpp"
#include "transaction.hpp"
#include "mn_state_machine.hpp"

#include <core/uint256.hpp>
#include <core/events.hpp>

#include <cstdint>
#include <map>
#include <utility>
#include <vector>

namespace dash
Expand Down Expand Up @@ -40,14 +42,31 @@ struct TipAdvance
/// 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
/// dash interface only (leg 2s new_tip added TipAdvance for the same reason a
/// bare best_block_hash was insufficient).
struct BlockConnected
{
coin::BlockType block;
uint32_t height{0};
};

/// Reception path payload (mnlistdiff): the AUTHORITATIVE masternode-set
/// snapshot the embedded coinbase pays. dashd (protx diff / the qdata
/// mnlistdiff message) yields the full projected DMN set as
/// (proTxHash -> MNState) pairs; CoinStateMaintainer::on_mn_list_update()
/// takes exactly that vector. This is the bulk RESYNC feed -- distinct from
/// leg 3s block_connected, which folds per-block special txs INCREMENTALLY
/// between snapshots. An empty vector is a set-gap signal (see on_mn_list_
/// update): it cannot back a payee, so it demotes the bundle to the dashd
/// fallback rather than publishing a template with a phantom payment. Purely
/// additive, dash interface only -- carries dash::coin::MNState, so this
/// header now pulls mn_state_machine.hpp (dash-coin scoped, no cross-coin
/// reach), matching the codec weight it already takes from block.hpp.
struct MnListUpdate
{
std::vector<std::pair<uint256, coin::MNState>> mnstates;
};

struct Node
{
Variable<uint256> best_block_hash;
Expand All @@ -70,6 +89,14 @@ struct Node
// fallback rather than backing a template with a phantom payee.
Event<BlockConnected> block_connected;

// Reception path: fires when dashd delivers a full mnlistdiff snapshot,
// carrying the authoritative projected DMN set (see MnListUpdate). The
// reception wire subscribes CoinStateMaintainer::on_mn_list_update to this
// so the masternode set the embedded coinbase pays is bulk-RESYNCED off the
// real feed (block_connected only folds per-block deltas between snapshots);
// an empty snapshot demotes the bundle to the dashd fallback.
Event<MnListUpdate> mn_list_update;

// 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
Expand Down
95 changes: 95 additions & 0 deletions test/test_dash_node_reception_wire.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <impl/dash/coin/mempool_ingest.hpp> // c2pool::dash::wire_mempool_ingest
#include <impl/dash/coin/tip_ingest.hpp> // c2pool::dash::wire_tip_ingest
#include <impl/dash/coin/block_connect_ingest.hpp> // c2pool::dash::wire_block_connect_ingest
#include <impl/dash/coin/mn_list_ingest.hpp> // c2pool::dash::wire_mn_list_ingest
#include <impl/dash/coin/coin_state_maintainer.hpp>
#include <impl/dash/coin/node_coin_state.hpp>
#include <impl/dash/coin/embedded_gbt.hpp>
Expand All @@ -55,6 +56,7 @@
using c2pool::dash::wire_mempool_ingest;
using c2pool::dash::wire_tip_ingest;
using c2pool::dash::wire_block_connect_ingest;
using c2pool::dash::wire_mn_list_ingest;
using dash::coin::CoinStateMaintainer;
using dash::coin::NodeCoinState;
using dash::coin::WorkSource;
Expand Down Expand Up @@ -377,3 +379,96 @@ TEST(DashReceptionWire, DisposeStopsBlockConnectIngest) {
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";
}

// ========================================================================
// Leg 4: an mn_list_update snapshot fired on the interface RESYNCS the DMN set
// THROUGH THE WIRE -- no direct on_mn_list_update() poke. With a tip already
// present, the wired mnlistdiff snapshot arms MN-readiness and select_work
// flips to the embedded arm. This is the authoritative bulk feed (leg 3's
// block_connected only folds per-block deltas between these snapshots).
// ========================================================================
TEST(DashReceptionWire, MnListUpdateRelaySeedsMnSetThroughWire) {
UTXOViewCache utxo(nullptr);
dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

auto sub = wire_mn_list_ingest(node, m);
ASSERT_TRUE(sub) << "wire must return a live subscription handle";

// Tip arrives first (reception is async); the MN list is the gating event.
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
EXPECT_FALSE(m.live()) << "tip alone must not arm the bundle -- MN list absent";
ASSERT_EQ(st.mnstates().size(), 0u);

// mnlistdiff snapshot fired on the interface (NOT a direct maintainer poke).
dash::interfaces::MnListUpdate u;
u.mnstates = single_mn(p2pkh_script(0x30));
node.mn_list_update.happened(u);

EXPECT_EQ(st.mnstates().size(), 1u) << "mnlistdiff relay (via wire) must seed the DMN set";
ASSERT_TRUE(m.live()) << "tip + MN (via wire) must arm the embedded bundle";

bool fb = false;
WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; });
EXPECT_EQ(sel.source, WorkSource::Embedded);
EXPECT_FALSE(fb) << "embedded arm must not invoke the dashd fallback";
}

// ========================================================================
// An EMPTY mnlistdiff snapshot fired on the wire is a set-gap: on_mn_list_update
// clears MN-readiness and demotes an armed bundle to the retained dashd
// fallback rather than backing a template with no masternode payee.
// ========================================================================
TEST(DashReceptionWire, EmptyMnListRelayDemotesThroughWire) {
UTXOViewCache utxo(nullptr);
dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

auto sub = wire_mn_list_ingest(node, m);

// Arm the bundle: tip + a non-empty snapshot delivered on the wire.
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
dash::interfaces::MnListUpdate seed;
seed.mnstates = single_mn(p2pkh_script(0x30));
node.mn_list_update.happened(seed);
ASSERT_TRUE(m.live());
ASSERT_EQ(st.mnstates().size(), 1u);

// An empty snapshot fired on the wire -> set-gap -> demote.
dash::interfaces::MnListUpdate empty; // empty.mnstates == {}
node.mn_list_update.happened(empty);

EXPECT_EQ(st.mnstates().size(), 0u) << "empty snapshot must clear the DMN set";
EXPECT_FALSE(m.live()) << "empty mnlistdiff (set-gap) must demote 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 an empty snapshot, get_work must fall back to dashd";
}

// ========================================================================
// Disposing the mn-list handle tears the subscription down: a later snapshot is
// not applied, so a stale mnlistdiff feed cannot silently re-seed the DMN set.
// ========================================================================
TEST(DashReceptionWire, DisposeStopsMnListIngest) {
UTXOViewCache utxo(nullptr);
dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

auto sub = wire_mn_list_ingest(node, m);
sub->dispose();

dash::interfaces::MnListUpdate u;
u.mnstates = single_mn(p2pkh_script(0x30));
node.mn_list_update.happened(u);

EXPECT_EQ(st.mnstates().size(), 0u)
<< "after dispose, an mnlistdiff snapshot must not seed the set";
}
Loading