Skip to content

Commit f76a948

Browse files
committed
dash(S8): reception-wire leg 3 -- interfaces::Node::block_connected -> CoinStateMaintainer::on_block_connected
Legs 1 (mempool) + 2 (tip) subscribed the mempool-relay and header/think TIP- ADVANCE events to the node-held embedded coin-state bundle. Nothing yet wired the BLOCK-CONNECT event, so in a live node the DMN set the embedded coinbase pays could only be seeded by a full mnlistdiff snapshot (on_mn_list_update) and never auto-maintained forward between snapshots. This leg closes that gap: - interfaces::Node gains an additive BlockConnected payload + Event<BlockConnected> block_connected. A bare full_block carries the block body but NOT the height on_block_connected -> MnStateMachine::apply_block needs (DIP3 special-tx chain position); block_connected pairs (block, height), exactly as leg 2 added TipAdvance for params best_block_hash could not carry. Purely additive, dash interface only (single-coin). - block_connect_ingest.hpp::wire_block_connect_ingest subscribes on_block_connected to block_connected, mirroring wire_mempool_ingest / wire_tip_ingest contract (by-ref capture, explicit-teardown disposable; dashd GBT arm retained as the fallback when the post-apply set can no longer back a payee). - test_dash_node_reception_wire gains leg-3 KATs off the real Event (no direct poke): a connected block that spends the sole MN collateral empties the DMN set and demotes the armed bundle to the dashd fallback; a no-special-tx block preserves readiness; dispose() stops ingest. Also carries the leg-1 recovery (mempool_ingest.hpp + test registration) that was authored in-worktree but never reached master via #686 -- without it the shared reception-wire KAT does not build. Additive, src/impl/dash/ only, dashd GBT fallback retained.
1 parent a1db112 commit f76a948

4 files changed

Lines changed: 203 additions & 2 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ jobs:
102102
core_test sharechain_test share_test btc_share_test \
103103
test_threading test_weights \
104104
test_header_chain test_mempool test_template_builder \
105-
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire \
105+
test_doge_chain test_compact_blocks test_dash_x11_kat test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire \
106106
test_multiaddress_pplns test_pplns_stress \
107107
test_hash_link test_decay_pplns \
108108
test_pplns_consensus \
@@ -252,7 +252,7 @@ jobs:
252252
test_threading test_weights \
253253
test_header_chain test_mempool test_template_builder \
254254
test_doge_chain test_compact_blocks test_dash_x11_kat \
255-
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire \
255+
test_dash_header_chain test_dash_block_replay test_dash_conformance test_dash_subsidy test_dash_mempool test_dash_simplifiedmns test_dash_quorum test_dash_quorum_root test_dash_mn_state test_dash_embedded_gbt test_dash_smldiff test_dash_p2p_messages test_dash_p2p_connection test_dash_p2p_node test_dash_node_interface test_dash_config test_dash_broadcaster test_dash_broadcaster_full test_dash_share_hash_link test_dash_block_relay test_dash_rpc_request test_dash_rpc_conf test_dash_block_producer test_dash_embedded_relay_e2e test_dash_block_relay_plan test_dash_version_activation_latch test_dash_block_relay_binding test_dash_block_relay_dual_arm test_dash_coinbase_parity test_dash_donation_combined test_dash_g3_assembled test_dash_work_target test_dash_work_job_targets test_dash_stratum_binding test_dash_job_notify_roundtrip test_dash_cb_payee test_dash_stratum_notify_roundtrip test_dash_poolnode_messages test_dash_peer test_dash_share_tracker test_dash_node test_dash_share_messages test_dash_stratum_extranonce_split test_dash_stratum_submit_reassembly test_dash_difficulty_parity test_dash_auto_ratchet test_dash_min_protocol_gate test_dash_work_source test_dash_node_coin_state test_dash_coin_state_maintainer test_dash_node_embedded_wire test_dash_node_reception_wire \
256256
test_hash_link test_decay_pplns \
257257
test_pplns_consensus \
258258
test_v36_script_sorting test_v36_cross_impl_refhash \
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-License-Identifier: AGPL-3.0-or-later
2+
#pragma once
3+
// ===========================================================================
4+
// dash reception wire (leg 3 of 4) -- interfaces::Node::block_connected ->
5+
// maintainer.
6+
//
7+
// #672..#685 landed the node-held embedded coin-state bundle + its async
8+
// CoinStateMaintainer; leg 1 (wire_mempool_ingest) subscribed the mempool-relay
9+
// event and leg 2 (wire_tip_ingest) the header/think TIP-ADVANCE event. This
10+
// leg subscribes the BLOCK-CONNECT event: every connected block is routed to
11+
// CoinStateMaintainer::on_block_connected(), which folds the block's DIP3
12+
// special txs into the DMN set incrementally (MnStateMachine::apply_block),
13+
// keeping the masternode set the embedded coinbase pays current BETWEEN full
14+
// mnlistdiff snapshots. A block that empties the set (all collateral spent)
15+
// clears MN-readiness and demotes the bundle to the retained dashd getblock-
16+
// template fallback rather than backing a template with a phantom payee.
17+
//
18+
// PAYLOAD: a bare full_block carries the block body but NOT its height, and
19+
// apply_block needs the connected height (DIP3 special-tx chain position). So
20+
// this leg subscribes block_connected, which pairs (block, height) -- the
21+
// interface-shape addition this leg makes, mirroring leg 2's TipAdvance, kept to
22+
// the dash interface only (single-coin, purely additive). on_mn_list_update
23+
// remains the authoritative bulk resync (leg 4, mnlistdiff source event).
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+
//
30+
// SCC: pulls node_interface.hpp (transaction/block codec via BlockConnected),
31+
// so include this header ONLY from a TU that already links the full dash codec
32+
// (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 (block_connected feed) + BlockConnected
39+
#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_block_connected
40+
41+
namespace c2pool::dash
42+
{
43+
44+
// Subscribe maint to node.block_connected: every connected block folds its DIP3
45+
// special txs into the DMN set via on_block_connected(block, height). The
46+
// maintainer refreshes MN-readiness from the post-apply set size -- a block that
47+
// empties the set demotes the node-held bundle to the dashd fallback; otherwise
48+
// it republishes with the freshened MN set. Returns the subscription handle so
49+
// the caller controls teardown.
50+
inline std::shared_ptr<EventDisposable>
51+
wire_block_connect_ingest(::dash::interfaces::Node& node,
52+
::dash::coin::CoinStateMaintainer& maint)
53+
{
54+
return node.block_connected.subscribe(
55+
[&maint](const ::dash::interfaces::BlockConnected& bc)
56+
{
57+
maint.on_block_connected(bc.block, bc.height);
58+
});
59+
}
60+
61+
} // namespace c2pool::dash

src/impl/dash/coin/node_interface.hpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ struct TipAdvance
3434
uint32_t version{0};
3535
};
3636

37+
/// Header/think path payload: a block was connected to the active chain. The
38+
/// bare full_block carries the block body but NOT the height CoinStateMaintainer
39+
/// ::on_block_connected needs to drive MnStateMachine::apply_block (the DIP3
40+
/// special-tx height is a chain-position input apply_block cannot recover from
41+
/// the block alone). block_connected pairs the two so the reception wire feeds
42+
/// 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
44+
/// bare best_block_hash was insufficient).
45+
struct BlockConnected
46+
{
47+
coin::BlockType block;
48+
uint32_t height{0};
49+
};
50+
3751
struct Node
3852
{
3953
Variable<uint256> best_block_hash;
@@ -48,6 +62,14 @@ struct Node
4862
// tip-readiness prerequisite without a direct poke.
4963
Event<TipAdvance> new_tip;
5064

65+
// Header/think path: fires when a block is connected to the active chain,
66+
// carrying (block, height) (see BlockConnected). The reception wire
67+
// subscribes CoinStateMaintainer::on_block_connected to this so the DMN set
68+
// the embedded coinbase pays auto-maintains incrementally between full
69+
// mnlistdiff snapshots -- a block that empties the set demotes to the dashd
70+
// fallback rather than backing a template with a phantom payee.
71+
Event<BlockConnected> block_connected;
72+
5173
// SPV A1 (parity audit): fires when dashd announces a ChainLock has
5274
// been aggregated for a block. Carries {block_hash, height}.
5375
// Consumers (e.g. block-find submit handler) can consult

test/test_dash_node_reception_wire.cpp

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@
3131
#include <impl/dash/coin/node_interface.hpp> // dash::interfaces::Node
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
34+
#include <impl/dash/coin/block_connect_ingest.hpp> // c2pool::dash::wire_block_connect_ingest
3435
#include <impl/dash/coin/coin_state_maintainer.hpp>
3536
#include <impl/dash/coin/node_coin_state.hpp>
3637
#include <impl/dash/coin/embedded_gbt.hpp>
3738
#include <impl/dash/coin/mn_state_machine.hpp>
39+
#include <impl/dash/coin/block.hpp>
3840
#include <impl/dash/coin/mempool.hpp>
3941
#include <impl/dash/coin/utxo_adapter.hpp>
4042
#include <impl/dash/coin/rpc_data.hpp>
@@ -52,11 +54,13 @@
5254

5355
using c2pool::dash::wire_mempool_ingest;
5456
using c2pool::dash::wire_tip_ingest;
57+
using c2pool::dash::wire_block_connect_ingest;
5558
using dash::coin::CoinStateMaintainer;
5659
using dash::coin::NodeCoinState;
5760
using dash::coin::WorkSource;
5861
using dash::coin::WorkSelection;
5962
using dash::coin::MNState;
63+
using dash::coin::BlockType;
6064
using dash::coin::MutableTransaction;
6165
using dash::coin::Transaction;
6266
using ::core::coin::UTXOViewCache;
@@ -105,6 +109,21 @@ static std::vector<std::pair<uint256, MNState>> single_mn(const std::vector<unsi
105109
return std::vector<std::pair<uint256, MNState>>{{raw256(0x01), s}};
106110
}
107111

112+
// Same as single_mn but records the MN's collateral outpoint, so a block that
113+
// spends it via apply_block (pass 2) removes the record (leg-3 demote KAT).
114+
static std::vector<std::pair<uint256, MNState>>
115+
single_mn_coll(const std::vector<unsigned char>& payout,
116+
const uint256& coll_hash, uint32_t coll_idx) {
117+
MNState s;
118+
s.isValid = true;
119+
s.nRegisteredHeight = 2'300'000;
120+
s.nLastPaidHeight = 0;
121+
s.scriptPayout.m_data = payout;
122+
s.collateralOutpoint.hash = coll_hash;
123+
s.collateralOutpoint.index = coll_idx;
124+
return std::vector<std::pair<uint256, MNState>>{{raw256(0x01), s}};
125+
}
126+
108127
static const uint256 PREV_HASH = raw256(0xAB);
109128
static const uint32_t BITS = 0x1b104be3u;
110129
static const uint32_t MTP = 1'700'000'000u;
@@ -259,3 +278,102 @@ TEST(DashReceptionWire, DisposeStopsTipIngest) {
259278
node.new_tip.happened(t);
260279
EXPECT_FALSE(m.live()) << "after dispose, a tip advance must not arm the bundle";
261280
}
281+
282+
// ════════════════════════════════════════════════════════════════════════
283+
// Leg 3: a block_connected relay fired on the interface drives the maintainer's
284+
// incremental MnStateMachine::apply_block THROUGH THE WIRE -- no direct
285+
// on_block_connected() poke. A connected block whose non-coinbase tx spends the
286+
// sole MN's collateral empties the DMN set; the now-unbacked payee demotes the
287+
// live bundle to the retained dashd fallback, and select_work routes get_work
288+
// there. Proves the block-connect leg mutates coin-state off the real Event.
289+
// ════════════════════════════════════════════════════════════════════════
290+
TEST(DashReceptionWire, BlockConnectRelayDrivesApplyBlockThenDemotes) {
291+
UTXOViewCache utxo(nullptr);
292+
dash::interfaces::Node node;
293+
NodeCoinState st;
294+
st.mempool().set_utxo(&utxo);
295+
CoinStateMaintainer m(st);
296+
297+
auto sub = wire_block_connect_ingest(node, m);
298+
ASSERT_TRUE(sub) << "wire must return a live subscription handle";
299+
300+
// Arm the bundle: MN (with a known collateral) + tip both present.
301+
const uint256 coll = raw256(0x55);
302+
m.on_mn_list_update(single_mn_coll(p2pkh_script(0x30), coll, 3));
303+
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
304+
ASSERT_TRUE(m.live());
305+
ASSERT_EQ(st.mnstates().size(), 1u);
306+
307+
// A connected block spends the sole MN's collateral -- fired on the wire,
308+
// NOT a direct maintainer poke.
309+
dash::interfaces::BlockConnected bc;
310+
bc.height = H;
311+
bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped)
312+
bc.block.m_txs.push_back(make_spend(coll, 3, 400'000'000, 2)); // spends MN collateral
313+
node.block_connected.happened(bc);
314+
315+
EXPECT_EQ(st.mnstates().size(), 0u) << "apply_block (via wire) must remove the MN";
316+
EXPECT_FALSE(m.live()) << "emptied DMN set must drop the live bundle";
317+
318+
bool fb = false;
319+
WorkSelection sel = st.select_work([&]() { fb = true; return dash::coin::DashWorkData{}; });
320+
EXPECT_EQ(sel.source, WorkSource::DashdFallback);
321+
EXPECT_TRUE(fb) << "after the collateral-spend block, get_work must fall back to dashd";
322+
}
323+
324+
// ════════════════════════════════════════════════════════════════════════
325+
// A block with no special txs touches no DMN record: apply_block registers
326+
// nothing, the set is unchanged, and the armed bundle stays live -- the wire
327+
// routes the event but a no-op block must not disturb readiness.
328+
// ════════════════════════════════════════════════════════════════════════
329+
TEST(DashReceptionWire, BlockConnectRelayNoSpecialTxPreservesReadiness) {
330+
UTXOViewCache utxo(nullptr);
331+
dash::interfaces::Node node;
332+
NodeCoinState st;
333+
st.mempool().set_utxo(&utxo);
334+
CoinStateMaintainer m(st);
335+
336+
auto sub = wire_block_connect_ingest(node, m);
337+
m.on_mn_list_update(single_mn(p2pkh_script(0x30)));
338+
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
339+
ASSERT_TRUE(m.live());
340+
341+
dash::interfaces::BlockConnected bc;
342+
bc.height = H;
343+
bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped)
344+
bc.block.m_txs.push_back(make_spend(raw256(0x91), 0, 400'000'000, 2)); // plain spend, no collateral
345+
node.block_connected.happened(bc);
346+
347+
EXPECT_EQ(st.mnstates().size(), 1u) << "no-special-tx block must not touch the DMN set";
348+
EXPECT_TRUE(m.live()) << "no-op block via wire must not drop the live bundle";
349+
}
350+
351+
// ════════════════════════════════════════════════════════════════════════
352+
// Disposing the block-connect handle tears the subscription down: a later
353+
// connected block is not applied, so a collateral spend cannot silently demote
354+
// an armed bundle off a stale feed.
355+
// ════════════════════════════════════════════════════════════════════════
356+
TEST(DashReceptionWire, DisposeStopsBlockConnectIngest) {
357+
UTXOViewCache utxo(nullptr);
358+
dash::interfaces::Node node;
359+
NodeCoinState st;
360+
st.mempool().set_utxo(&utxo);
361+
CoinStateMaintainer m(st);
362+
363+
const uint256 coll = raw256(0x55);
364+
m.on_mn_list_update(single_mn_coll(p2pkh_script(0x30), coll, 3));
365+
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
366+
ASSERT_TRUE(m.live());
367+
368+
auto sub = wire_block_connect_ingest(node, m);
369+
sub->dispose();
370+
371+
dash::interfaces::BlockConnected bc;
372+
bc.height = H;
373+
bc.block.m_txs.push_back(make_spend(raw256(0x90), 0, 500'000'000, 1)); // cb (idx 0, skipped)
374+
bc.block.m_txs.push_back(make_spend(coll, 3, 400'000'000, 2)); // would spend MN collateral
375+
node.block_connected.happened(bc);
376+
377+
EXPECT_EQ(st.mnstates().size(), 1u) << "after dispose, a connected block must not be applied";
378+
EXPECT_TRUE(m.live()) << "after dispose, the armed bundle must stay live";
379+
}

0 commit comments

Comments
 (0)