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
29 changes: 27 additions & 2 deletions src/impl/dash/coin/node_interface.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <core/uint256.hpp>
#include <core/events.hpp>

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

Expand All @@ -15,6 +16,24 @@ namespace dash
namespace interfaces
{

/// Header/think path payload: the active chain tip advanced. Carries the exact
/// inputs build_embedded_workdata() needs that a bare best_block_hash cannot --
/// the height/hash to build the next block ON, the next-block work target
/// (bits), the tip median-time-past for time bounds, and the coin address
/// versions for coinbase-payee encoding. curtime/version default to 0 so the
/// shaper applies its own SAFE-ADDITIVE defaults.
struct TipAdvance
{
uint32_t prev_height{0};
uint256 prev_hash;
uint32_t bits_for_next{0};
uint32_t mtp_at_tip{0};
uint8_t address_version{0};
uint8_t address_p2sh_version{0};
uint32_t curtime{0};
uint32_t version{0};
};

struct Node
{
Variable<uint256> best_block_hash;
Expand All @@ -23,16 +42,22 @@ struct Node
Event<std::vector<coin::BlockHeaderType>> new_headers;
Event<coin::BlockType> full_block;

// Header/think path: fires when the active chain tip advances, carrying the
// embedded-template params (see TipAdvance). The reception wire subscribes
// CoinStateMaintainer::on_new_tip to this so the node-held bundle arms its
// tip-readiness prerequisite without a direct poke.
Event<TipAdvance> new_tip;

// 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
// m_chainlocked_blocks to know whether a found block is now
// irreversible.
Event<std::pair<uint256, int32_t>> new_chainlock;
std::map<uint256, int32_t> chainlocked_blocks; // block_hash height
std::map<uint256, int32_t> chainlocked_blocks; // block_hash -> height

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

} // namespace interfaces
} // namespace dash
} // namespace dash
62 changes: 62 additions & 0 deletions src/impl/dash/coin/tip_ingest.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
#pragma once
// ===========================================================================
// dash reception wire (leg 2 of 4) -- interfaces::Node::new_tip -> maintainer.
//
// #672..#685 landed the node-held embedded coin-state bundle + its async
// CoinStateMaintainer; wire_mempool_ingest (leg 1) subscribed the mempool-relay
// event. This leg subscribes the header/think TIP-ADVANCE event: every tip
// advance is routed to CoinStateMaintainer::on_new_tip(), which stashes the tip
// params and marks tip-readiness -- one of the two prerequisites (the MN list
// is the other) that must BOTH be present before the node-held bundle publishes
// and select_work() flips off the retained dashd getblocktemplate fallback.
//
// PAYLOAD: unlike a bare best_block_hash, interfaces::Node::new_tip carries a
// TipAdvance -- the height/hash to build ON, next-block bits, tip MTP, and the
// coin address versions -- exactly build_embedded_workdata() inputs. That
// struct is the interface-shape addition this leg makes; the leg 1 note flagged
// that on_new_tip needs params best_block_hash does not carry, and this is that
// payload, kept to the dash interface only (single-coin, purely additive).
//
// STILL UNWIRED (own slices): on_mn_list_update has no source event on
// interfaces::Node yet; on_block_connected needs the connected block height,
// which full_block does not carry.
//
// 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.
//
// SCC: pulls node_interface.hpp (transaction/block codec via TipAdvance
// siblings), 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 <core/events.hpp>

#include "node_interface.hpp" // dash::interfaces::Node (new_tip feed) + TipAdvance
#include "coin_state_maintainer.hpp" // dash::coin::CoinStateMaintainer::on_new_tip

namespace c2pool::dash
{

// Subscribe maint to node.new_tip: every tip advance stashes the embedded
// template params and marks tip-readiness via on_new_tip(); the maintainer
// republishes the node-held bundle only once the MN list is ALSO seeded, so a
// tip arriving before the first mnlistdiff leaves the bundle on the dashd
// fallback. Returns the subscription handle so the caller controls teardown.
inline std::shared_ptr<EventDisposable>
wire_tip_ingest(::dash::interfaces::Node& node,
::dash::coin::CoinStateMaintainer& maint)
{
return node.new_tip.subscribe(
[&maint](const ::dash::interfaces::TipAdvance& t)
{
maint.on_new_tip(t.prev_height, t.prev_hash, t.bits_for_next,
t.mtp_at_tip, t.address_version, t.address_p2sh_version,
t.curtime, t.version);
});
}

} // namespace c2pool::dash
261 changes: 261 additions & 0 deletions test/test_dash_node_reception_wire.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
/// Phase C-TEMPLATE step 9 -- reception-wire KAT (leg 1: mempool relay).
///
/// #672..#685 landed the node-held embedded coin-state bundle + its async
/// CoinStateMaintainer, and test_dash_coin_state_maintainer / _node_embedded_wire
/// proved that DRIVING the maintainer's on_*() methods flips select_work() to
/// the embedded arm. But every one of those suites poked the maintainer DIRECTLY
/// -- nothing subscribed a live interfaces::Node's reception events to it, so in
/// a running node the arm could never flip on its own. wire_mempool_ingest()
/// (src/impl/dash/coin/mempool_ingest.hpp) closes the FIRST of the four legs:
/// interfaces::Node::new_tx -> CoinStateMaintainer::on_mempool_tx. This suite
/// proves that subscription end-to-end, off the real Event, with no direct poke:
///
/// * a new_tx relay fired on the interface FOLDS into the maintainer's mempool
/// (size 0 -> 1) -- the reception path, not a test poke, drives the state;
/// * disposing the returned handle tears the subscription down: a later relay
/// is NOT folded (size stays put) -- teardown is honoured;
/// * a relayed tx reaches the assembled embedded template once MN+tip arm the
/// bundle (select_work -> WorkSource::Embedded, tx present in m_tx_hashes),
/// and an on_invalidate() reorg demotes back to the retained dashd fallback.
///
/// Seeding mirrors test_dash_coin_state_maintainer.cpp exactly so the suites pin
/// the SAME projection. Scope-honest: only the new_tx leg is wired here; the
/// on_block_connected / on_new_tip / on_mn_list_update legs need payload the
/// interface does not yet carry (block height / tip params / a mnlistdiff event)
/// and land in their own slices -- their maintainer methods are still exercised
/// directly here only to arm the bundle for the template-reach assertion.

#include <gtest/gtest.h>

#include <impl/dash/coin/node_interface.hpp> // dash::interfaces::Node
#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/coin_state_maintainer.hpp>
#include <impl/dash/coin/node_coin_state.hpp>
#include <impl/dash/coin/embedded_gbt.hpp>
#include <impl/dash/coin/mn_state_machine.hpp>
#include <impl/dash/coin/mempool.hpp>
#include <impl/dash/coin/utxo_adapter.hpp>
#include <impl/dash/coin/rpc_data.hpp>
#include <impl/dash/coin/transaction.hpp>

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

#include <array>
#include <cstdint>
#include <cstring>
#include <memory>
#include <utility>
#include <vector>

using c2pool::dash::wire_mempool_ingest;
using c2pool::dash::wire_tip_ingest;
using dash::coin::CoinStateMaintainer;
using dash::coin::NodeCoinState;
using dash::coin::WorkSource;
using dash::coin::WorkSelection;
using dash::coin::MNState;
using dash::coin::MutableTransaction;
using dash::coin::Transaction;
using ::core::coin::UTXOViewCache;
using ::core::coin::Outpoint;
using ::core::coin::Coin;
using ::bitcoin_family::coin::TxIn;
using ::bitcoin_family::coin::TxOut;

static constexpr uint8_t DASH_PUBKEY_VER = 76;
static constexpr uint8_t DASH_P2SH_VER = 16;
static constexpr uint32_t H = 2'400'000; // past MN_RR: platform burn active

static uint256 raw256(uint8_t base) {
uint256 h;
std::array<uint8_t, 32> p{};
for (size_t i = 0; i < 32; ++i) p[i] = static_cast<uint8_t>(base + i);
std::memcpy(h.data(), p.data(), 32);
return h;
}

static std::vector<unsigned char> p2pkh_script(uint8_t hashseed) {
std::vector<unsigned char> s{0x76, 0xa9, 0x14};
for (int i = 0; i < 20; ++i) s.push_back(static_cast<unsigned char>(hashseed + i));
s.push_back(0x88); s.push_back(0xac);
return s;
}

static MutableTransaction make_spend(const uint256& prev, uint32_t idx,
int64_t out_value, uint32_t salt) {
MutableTransaction tx;
tx.version = 1; tx.type = 0; tx.locktime = salt;
TxIn in; in.prevout.hash = prev; in.prevout.index = idx;
in.sequence = 0xffffffffu;
tx.vin.push_back(in);
TxOut o; o.value = out_value;
tx.vout.push_back(o);
return tx;
}

static std::vector<std::pair<uint256, MNState>> single_mn(const std::vector<unsigned char>& payout) {
MNState s;
s.isValid = true;
s.nRegisteredHeight = 2'300'000;
s.nLastPaidHeight = 0;
s.scriptPayout.m_data = payout;
return std::vector<std::pair<uint256, MNState>>{{raw256(0x01), s}};
}

static const uint256 PREV_HASH = raw256(0xAB);
static const uint32_t BITS = 0x1b104be3u;
static const uint32_t MTP = 1'700'000'000u;
static const uint32_t CURTIME = 1'700'000'123u;
static const uint32_t VERSION = 0x20000000u;

// ════════════════════════════════════════════════════════════════════════
// Leg 1: a new_tx relay fired on the interface folds through the maintainer.
// No direct on_mempool_tx() poke -- the Event drives it.
// ════════════════════════════════════════════════════════════════════════
TEST(DashReceptionWire, NewTxRelayFoldsThroughMaintainer) {
UTXOViewCache utxo(nullptr);
const uint256 prev = raw256(0x77);
utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, 1, false));

dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

auto sub = wire_mempool_ingest(node, m);
ASSERT_TRUE(sub) << "wire must return a live subscription handle";
ASSERT_EQ(st.mempool().size(), 0u);

// Fire the reception event (NOT a direct maintainer poke).
node.new_tx.happened(Transaction(make_spend(prev, 0, 90'000, /*salt=*/1)));

EXPECT_EQ(st.mempool().size(), 1u) << "new_tx relay must fold into the mempool";
}

// ════════════════════════════════════════════════════════════════════════
// Disposing the handle tears the subscription down: later relays are dropped.
// ════════════════════════════════════════════════════════════════════════
TEST(DashReceptionWire, DisposeStopsIngest) {
UTXOViewCache utxo(nullptr);
const uint256 a = raw256(0x77);
const uint256 b = raw256(0x66);
utxo.add_coin(Outpoint(a, 0), Coin(100'000, {}, 1, false));
utxo.add_coin(Outpoint(b, 0), Coin(100'000, {}, 1, false));

dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

auto sub = wire_mempool_ingest(node, m);
node.new_tx.happened(Transaction(make_spend(a, 0, 90'000, /*salt=*/1)));
ASSERT_EQ(st.mempool().size(), 1u);

sub->dispose();
node.new_tx.happened(Transaction(make_spend(b, 0, 90'000, /*salt=*/2)));
EXPECT_EQ(st.mempool().size(), 1u) << "after dispose, no further relay may fold";
}

// ════════════════════════════════════════════════════════════════════════
// A relayed tx reaches the assembled embedded template once MN+tip arm the
// bundle; an on_invalidate() reorg demotes back to the retained dashd fallback.
// (MN/tip legs are still poked directly -- only new_tx is wired in this slice.)
// ════════════════════════════════════════════════════════════════════════
TEST(DashReceptionWire, RelayedTxReachesEmbeddedTemplateThenInvalidateDemotes) {
UTXOViewCache utxo(nullptr);
const uint256 prev = raw256(0x77);
utxo.add_coin(Outpoint(prev, 0), Coin(100'000, {}, 1, false));

dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);
auto sub = wire_mempool_ingest(node, m);

// Reception leg feeds the mempool; MN + tip arm the bundle.
node.new_tx.happened(Transaction(make_spend(prev, 0, 90'000, /*salt=*/1))); // fee 10'000
m.on_mn_list_update(single_mn(p2pkh_script(0x30)));
m.on_new_tip(H - 1, PREV_HASH, BITS, MTP, DASH_PUBKEY_VER, DASH_P2SH_VER, CURTIME, VERSION);
ASSERT_TRUE(m.live());

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";
EXPECT_EQ(sel.work.m_height, H);
ASSERT_EQ(st.mempool().size(), 1u);
EXPECT_EQ(sel.work.m_tx_hashes.size(), 1u)
<< "the RELAYED tx must reach the assembled embedded template";

// Reorg: on_invalidate drops the tip -> next get_work falls back to dashd.
m.on_invalidate();
EXPECT_FALSE(m.live());
bool fb2 = false;
WorkSelection sel2 = st.select_work([&]() { fb2 = true; return dash::coin::DashWorkData{}; });
EXPECT_EQ(sel2.source, WorkSource::DashdFallback);
EXPECT_TRUE(fb2) << "after invalidate, the retained dashd fallback must run";
}

// ════════════════════════════════════════════════════════════════════════
// Leg 2: a new_tip advance fired on the interface arms the maintainer tip-
// readiness THROUGH THE WIRE -- no direct on_new_tip() poke. A tip arriving
// before the first mnlistdiff must NOT go live (MN list absent); once the MN
// list seeds the other prerequisite the bundle arms and select_work flips to
// the embedded arm, and the WIRED tip params reach the assembled template.
// ════════════════════════════════════════════════════════════════════════
TEST(DashReceptionWire, NewTipRelayArmsBundleOnceMnSeeded) {
UTXOViewCache utxo(nullptr);
dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

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

// Tip arrives first (reception is async): tip-readiness is set via the
// wire, but the MN list is still empty, so the bundle stays on dashd.
dash::interfaces::TipAdvance t;
t.prev_height = H - 1; t.prev_hash = PREV_HASH; t.bits_for_next = BITS;
t.mtp_at_tip = MTP; t.address_version = DASH_PUBKEY_VER;
t.address_p2sh_version = DASH_P2SH_VER; t.curtime = CURTIME; t.version = VERSION;
node.new_tip.happened(t);
EXPECT_FALSE(m.live()) << "tip alone must not arm the bundle -- MN list absent";

// MN list seeds the second prerequisite -> bundle arms, embedded arm wins.
m.on_mn_list_update(single_mn(p2pkh_script(0x30)));
ASSERT_TRUE(m.live()) << "tip (via wire) + MN 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";
EXPECT_EQ(sel.work.m_height, H) << "the WIRED tip params must reach the template";
}

// ════════════════════════════════════════════════════════════════════════
// Disposing the tip handle tears the subscription down: a later tip advance is
// not applied, so a post-reorg bundle cannot silently re-arm off a stale feed.
// ════════════════════════════════════════════════════════════════════════
TEST(DashReceptionWire, DisposeStopsTipIngest) {
UTXOViewCache utxo(nullptr);
dash::interfaces::Node node;
NodeCoinState st;
st.mempool().set_utxo(&utxo);
CoinStateMaintainer m(st);

// MN present up front; the tip is the gating event under test.
m.on_mn_list_update(single_mn(p2pkh_script(0x30)));
auto sub = wire_tip_ingest(node, m);
sub->dispose();

dash::interfaces::TipAdvance t;
t.prev_height = H - 1; t.prev_hash = PREV_HASH; t.bits_for_next = BITS;
t.mtp_at_tip = MTP; t.address_version = DASH_PUBKEY_VER;
t.address_p2sh_version = DASH_P2SH_VER; t.curtime = CURTIME; t.version = VERSION;
node.new_tip.happened(t);
EXPECT_FALSE(m.live()) << "after dispose, a tip advance must not arm the bundle";
}
Loading