From 70289c91984963a161edf506c37dd73e54a65b9c Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 14 Jun 2026 00:49:34 +0000 Subject: [PATCH 1/4] dgb(coin): port non-MWEB block/tx foundation + restore node interface Phase A (#82) leaf deps for the embedded P2P+mempool layer. 1:1 mirror of src/impl/btc/coin (DGB is structurally BTC-shaped, non-MWEB; Scrypt PoW divergence lives in header_chain.hpp). block.hpp carries the multi-algo note: the version field encodes the mining algo but the 80-byte header layout is single-format and algo-agnostic; algo selection is read in header_chain, not in (de)serialization. node_interface.hpp restores the members the Path-A stub deferred (new_tx/new_headers/full_block/txidcache/known_txs), now that the types exist. --- src/impl/dgb/coin/block.hpp | 125 +++++++++++++++ src/impl/dgb/coin/node_interface.hpp | 48 ++++-- src/impl/dgb/coin/transaction.cpp | 24 +++ src/impl/dgb/coin/transaction.hpp | 224 +++++++++++++++++++++++++++ src/impl/dgb/coin/txidcache.hpp | 88 +++++++++++ 5 files changed, 493 insertions(+), 16 deletions(-) create mode 100644 src/impl/dgb/coin/block.hpp create mode 100644 src/impl/dgb/coin/transaction.cpp create mode 100644 src/impl/dgb/coin/transaction.hpp create mode 100644 src/impl/dgb/coin/txidcache.hpp diff --git a/src/impl/dgb/coin/block.hpp b/src/impl/dgb/coin/block.hpp new file mode 100644 index 000000000..29ca418e5 --- /dev/null +++ b/src/impl/dgb/coin/block.hpp @@ -0,0 +1,125 @@ +#pragma once + +// DGB block/header types. 1:1 mirror of src/impl/btc/coin/block.hpp (NON-MWEB). +// +// >>> MULTI-ALGO NOTE (project_v36_dgb_scrypt_only) <<< +// The 80-byte header layout (version|prev|merkle|time|bits|nonce) is identical +// across DGB`s 5 algos -- this serialization is algo-agnostic and correct as-is. +// The block version field additionally ENCODES the mining algo in its bits; +// DGB-Scrypt validation reads that algo selector in header_chain.hpp, NOT here. +// Do not add per-algo branching to (de)serialization -- it is single-format. + +#include "transaction.hpp" + +#include +#include +#include + +namespace dgb +{ + +namespace coin +{ + +struct SmallBlockHeaderType +{ + uint64_t m_version {}; + uint256 m_previous_block{}; + uint32_t m_timestamp{}; + uint32_t m_bits{}; + uint32_t m_nonce{}; + + C2POOL_SERIALIZE_METHODS(SmallBlockHeaderType) { READWRITE(VarInt(obj.m_version), obj.m_previous_block, obj.m_timestamp, obj.m_bits, obj.m_nonce); } + + SmallBlockHeaderType() {} + + void SetNull() + { + m_version = 0; + m_previous_block.SetNull(); + m_timestamp = 0; + m_bits = 0; + m_nonce = 0; + } + + bool IsNull() const + { + return (m_bits == 0); + } +}; + +struct BlockHeaderType : SmallBlockHeaderType +{ + uint256 m_merkle_root; + + // Full block header uses fixed 4-byte int32 version (not VarInt like SmallBlockHeaderType) + template + void Serialize(Stream& s) const { + uint32_t version32 = static_cast(m_version); + ::Serialize(s, version32); + ::Serialize(s, m_previous_block); + ::Serialize(s, m_merkle_root); + ::Serialize(s, m_timestamp); + ::Serialize(s, m_bits); + ::Serialize(s, m_nonce); + } + template + void Unserialize(Stream& s) { + uint32_t version32; + ::Unserialize(s, version32); + m_version = version32; + ::Unserialize(s, m_previous_block); + ::Unserialize(s, m_merkle_root); + ::Unserialize(s, m_timestamp); + ::Unserialize(s, m_bits); + ::Unserialize(s, m_nonce); + } + + BlockHeaderType() : SmallBlockHeaderType() { } + + void SetNull() + { + SmallBlockHeaderType::SetNull(); + m_merkle_root.SetNull(); + } + + bool IsNull() const + { + return (m_bits == 0); + } + +}; + +struct BlockType : BlockHeaderType +{ + std::vector m_txs; + + template + void Serialize(Stream& s) const { + BlockHeaderType::Serialize(s); + ::Serialize(s, TX_WITH_WITNESS(m_txs)); + } + + template + void Unserialize(Stream& s) { + BlockHeaderType::Unserialize(s); + ::Unserialize(s, TX_WITH_WITNESS(m_txs)); + } + + BlockType() : BlockHeaderType() { } + + void SetNull() + { + BlockHeaderType::SetNull(); + m_txs.clear(); + } + + bool IsNull() const + { + return BlockHeaderType::IsNull(); + } +}; + +} // namespace coin + +} // namespace dgb \ No newline at end of file diff --git a/src/impl/dgb/coin/node_interface.hpp b/src/impl/dgb/coin/node_interface.hpp index 7c6e6803f..186c1b37c 100644 --- a/src/impl/dgb/coin/node_interface.hpp +++ b/src/impl/dgb/coin/node_interface.hpp @@ -1,26 +1,36 @@ #pragma once // --------------------------------------------------------------------------- -// dgb::interfaces::Node -- coin-node shared-state surface (Path A -// minimal-stub). Mirrors src/impl/btc/coin/node_interface.hpp TRIMMED to the -// members the family-1 ICoinNode seam actually binds today: +// dgb::interfaces::Node -- coin-node shared-state surface. // -// work : Variable -- get_work result (NodeRPC fills, -// CoinNode retains coin-side per the WorkView seam contract) -// new_block : Event -- block-hash announce (header-chain -// feed; uint256 only, no per-coin type needed) +// Phase A (#82) RESTORES the members the Path-A stub deliberately deferred, +// now that the types they reference exist (block.hpp / transaction.hpp / +// txidcache.hpp ported in this slice). 1:1 mirror of +// src/impl/btc/coin/node_interface.hpp -- DGB is structurally BTC-shaped +// (NON-MWEB); the Scrypt PoW divergence lives in header_chain.hpp, not here. +// The embedded NodeP2P (p2p_node.hpp) binds against this full surface: // -// Deliberately ABSENT until the M3 type ports land (each pulls a type dgb -// does not have yet): Event new_tx, Event> new_headers, Event full_block, TXIDCache -// txidcache, known_txs map. Restoring them is additive; nothing in the seam -// depends on their absence. +// work : Variable -- get_work result +// new_block : Event -- block-hash announce +// new_tx : Event -- mempool tx relay +// new_headers: Event> -- header-chain feed +// full_block : Event -- full block (txs) +// txidcache : coin::TXIDCache -- gbt data -> txid cache +// known_txs : map -- mempool-known txs // --------------------------------------------------------------------------- -#include -#include +#include +#include +#include "block.hpp" #include "rpc_data.hpp" +#include "transaction.hpp" +#include "txidcache.hpp" + +#include +#include + +#include namespace dgb { @@ -30,8 +40,14 @@ namespace interfaces struct Node { - Variable work; // get_work result - Event new_block; // block_hash + Variable work; // get_work result + Event new_block; // block_hash + Event new_tx; // bitcoin_data.tx_type + Event> new_headers; // bitcoin_data.block_header_type + Event full_block; // full block with txs + + coin::TXIDCache txidcache; + std::map known_txs; }; } // namespace interfaces diff --git a/src/impl/dgb/coin/transaction.cpp b/src/impl/dgb/coin/transaction.cpp new file mode 100644 index 000000000..7bd0c03cf --- /dev/null +++ b/src/impl/dgb/coin/transaction.cpp @@ -0,0 +1,24 @@ +#include "transaction.hpp" + +namespace dgb +{ + +namespace coin +{ + +Transaction::Transaction(const MutableTransaction& tx) : vin(tx.vin), vout(tx.vout), version(tx.version), locktime(tx.locktime), m_has_witness{ComputeHasWitness()} {} +Transaction::Transaction(MutableTransaction&& tx) : vin(std::move(tx.vin)), vout(std::move(tx.vout)), version(tx.version), locktime(tx.locktime), m_has_witness{ComputeHasWitness()} {} + +bool Transaction::ComputeHasWitness() const +{ + return std::any_of(vin.begin(), vin.end(), [](const auto& input) { + return !input.scriptWitness.IsNull(); + }); +} + +MutableTransaction::MutableTransaction() : version(Transaction::CURRENT_VERSION), locktime(0) {} +MutableTransaction::MutableTransaction(const Transaction& tx) : version(tx.version), vin(tx.vin), vout(tx.vout), locktime(tx.locktime) {} + +} // namespace coin + +} // namespace dgb \ No newline at end of file diff --git a/src/impl/dgb/coin/transaction.hpp b/src/impl/dgb/coin/transaction.hpp new file mode 100644 index 000000000..ccee5b9bc --- /dev/null +++ b/src/impl/dgb/coin/transaction.hpp @@ -0,0 +1,224 @@ +#pragma once + +// DGB coin tx types. 1:1 mirror of src/impl/btc/coin/transaction.hpp +// (NON-MWEB; DGB has no MWEB extension block). Scrypt PoW divergence lives in +// header_chain.hpp, not here -- tx/script serialization is family-identical. + +#include +#include +#include + +namespace dgb +{ + +namespace coin +{ + +struct MutableTransaction; + +struct TxParams +{ + const bool allow_witness; + + SER_PARAMS_OPFUNC +}; + +constexpr static TxParams TX_WITH_WITNESS {.allow_witness = true}; +constexpr static TxParams TX_NO_WITNESS {.allow_witness = false}; + +class TxPrevOut +{ +public: + uint256 hash; + uint32_t index; + + C2POOL_SERIALIZE_METHODS(TxPrevOut) { READWRITE(obj.hash, obj.index); } +}; + +class TxIn +{ +public: + TxPrevOut prevout; + OPScript scriptSig; + uint32_t sequence; + OPScriptWitness scriptWitness; //!< Only serialized through Transaction + + C2POOL_SERIALIZE_METHODS(TxIn) { READWRITE(obj.prevout, obj.scriptSig, obj.sequence); } +}; + +class TxOut +{ +public: + int64_t value; + OPScript scriptPubKey; + + C2POOL_SERIALIZE_METHODS(TxOut) { READWRITE(obj.value, obj.scriptPubKey); } +}; + +class Transaction +{ +public: + // Default transaction version. + static const int32_t CURRENT_VERSION = 2; + + std::vector vin; + std::vector vout; + int32_t version; + uint32_t locktime; + +private: + bool m_has_witness; + + bool ComputeHasWitness() const; + +public: + /** Convert a MutableTransaction into a Transaction. */ + explicit Transaction(const MutableTransaction& tx) ; + explicit Transaction(MutableTransaction&& tx); + + template + inline void Serialize(StreamType& os) const + { + SerializeTransaction(*this, os, os.GetParams()); + } + + bool HasWitness() const { return m_has_witness; } +}; + +/** A mutable version of Transaction. */ +struct MutableTransaction +{ + std::vector vin; + std::vector vout; + int32_t version; + uint32_t locktime; + + explicit MutableTransaction(); + explicit MutableTransaction(const Transaction& tx); + + template + inline void Serialize(StreamType& os) const + { + SerializeTransaction(*this, os, os.GetParams()); + } + + template + inline void Unserialize(StreamType& os) + { + UnserializeTransaction(*this, os, os.GetParams()); + } + + bool HasWitness() const + { + for (size_t i = 0; i < vin.size(); i++) + { + if (!vin[i].scriptWitness.IsNull()) + return true; + } + return false; + } +}; + +/** + * Basic transaction serialization format: + * - int32_t version + * - std::vector vin + * - std::vector vout + * - uint32_t locktime + * + * Extended transaction serialization format: + * - int32_t version + * - unsigned char dummy = 0x00 + * - unsigned char flags (!= 0) + * - std::vector vin + * - std::vector vout + * - if (flags & 1): + * - OPScriptWitness scriptWitness; (deserialized into TxIn) + * - uint32_t locktime + */ +template +void UnserializeTransaction(TxType& tx, StreamType& s, const TxParams& params) +{ + const bool fAllowWitness = params.allow_witness; + + s >> tx.version; + unsigned char flags = 0; + tx.vin.clear(); + tx.vout.clear(); + /* Try to read the vin. In case the dummy is there, this will be read as an empty vector. */ + s >> tx.vin; + if (tx.vin.size() == 0 && fAllowWitness) + { + /* We read a dummy or an empty vin. */ + s >> flags; + if (flags != 0) + { + s >> tx.vin; + s >> tx.vout; + } + } else + { + /* We read a non-empty vin. Assume a normal vout follows. */ + s >> tx.vout; + } + if ((flags & 1) && fAllowWitness) + { + /* The witness flag is present, and we support witnesses. */ + flags ^= 1; + for (size_t i = 0; i < tx.vin.size(); i++) + { + s >> tx.vin[i].scriptWitness.stack; + } + if (!tx.HasWitness()) + { + /* It's illegal to encode witnesses when all witness stacks are empty. */ + throw std::ios_base::failure("Superfluous witness record"); + } + } + + if (flags) + { + /* Unknown flag in the serialization */ + throw std::ios_base::failure("Unknown transaction optional data"); + } + s >> tx.locktime; +} + +template +void SerializeTransaction(const TxType& tx, StreamType& s, const TxParams& params) +{ + const bool fAllowWitness = params.allow_witness; + + s << tx.version; + unsigned char flags = 0; + // Consistency check + if (fAllowWitness) + { + /* Check whether witnesses need to be serialized. */ + if (tx.HasWitness()) + { + flags |= 1; + } + } + if (flags) + { + /* Use extended format in case witnesses to be serialized. */ + std::vector vinDummy; + s << vinDummy; + s << flags; + } + s << tx.vin; + s << tx.vout; + if (flags & 1) + { + for (size_t i = 0; i < tx.vin.size(); i++) + { + s << tx.vin[i].scriptWitness.stack; + } + } + s << tx.locktime; +} + +} // namespace coin + +} // namespace dgb diff --git a/src/impl/dgb/coin/txidcache.hpp b/src/impl/dgb/coin/txidcache.hpp new file mode 100644 index 000000000..ce4fbc489 --- /dev/null +++ b/src/impl/dgb/coin/txidcache.hpp @@ -0,0 +1,88 @@ +#pragma once + +// DGB gbt-data -> txid cache. 1:1 mirror of src/impl/btc/coin/txidcache.hpp. + +#include +#include +#include +#include + +#include +#include + +namespace dgb +{ + +namespace coin +{ + +class TXIDCache +{ + using key_t = std::string; + + mutable std::shared_mutex m_mutex; + bool m_started {false}; + time_t m_when_started{0}; + + std::map m_cache; //getblocktemplate.transacions[].data; hash256(packed data) + +public: + + void start() + { + std::shared_lock lock(m_mutex); + m_when_started = core::timestamp(); + m_started = true; + } + + bool exist(const key_t& key) const + { + std::shared_lock lock(m_mutex); + return m_cache.contains(key); + } + + bool is_started() const + { + std::shared_lock lock(m_mutex); + return m_started; + } + + bool time() const + { + std::shared_lock lock(m_mutex); + return m_when_started; + } + + void clear() + { + std::unique_lock lock(m_mutex); + m_cache.clear(); + } + + void add(const key_t& key, const uint256& value) + { + std::unique_lock lock(m_mutex); + m_cache[key] = value; + } + + void add(std::map values) + { + std::unique_lock lock(m_mutex); + m_cache.insert(values.begin(), values.end()); + } + + uint256 get(const key_t& key) + { + std::shared_lock lock(m_mutex); + + if (m_cache.contains(key)) + return m_cache[key]; + else + throw std::out_of_range("key not found in TXIDCache!"); + } + +}; + +} // namespace coin + +} // namespace dgb From 6929ea1903503ba6ff647656289a200d3c43489d Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 14 Jun 2026 00:49:44 +0000 Subject: [PATCH 2/4] dgb(coin): port embedded mempool + BIP152 compact blocks Phase A (#82). 1:1 mirror of src/impl/btc/coin (non-MWEB). SegWit/wtxid short-IDs apply (DGB activated SegWit). No coin-specific magic/consensus constants in this layer. --- src/impl/dgb/coin/compact_blocks.hpp | 322 ++++++++++++ src/impl/dgb/coin/mempool.hpp | 737 +++++++++++++++++++++++++++ 2 files changed, 1059 insertions(+) create mode 100644 src/impl/dgb/coin/compact_blocks.hpp create mode 100644 src/impl/dgb/coin/mempool.hpp diff --git a/src/impl/dgb/coin/compact_blocks.hpp b/src/impl/dgb/coin/compact_blocks.hpp new file mode 100644 index 000000000..5b3a96928 --- /dev/null +++ b/src/impl/dgb/coin/compact_blocks.hpp @@ -0,0 +1,322 @@ +#pragma once + +// DGB BIP152 compact blocks. 1:1 mirror of src/impl/btc/coin/compact_blocks.hpp +// (NON-MWEB). SegWit/wtxid short-IDs apply (DGB activated SegWit). + +/// BIP 152 Compact Block Support +/// +/// Data structures and wire-format serialization for compact block relay. +/// Reduces block relay bandwidth by ~90-95%. +/// +/// Wire format (HeaderAndShortIDs): +/// block_header (80 bytes) | nonce (uint64) | +/// shortids_length (compact) | shortids[] (6 bytes each) | +/// prefilledtxn_length (compact) | prefilledtxn[] { diff_index (compact), tx } +/// +/// Prefilled transaction indexes use differential encoding: +/// first index is absolute; subsequent = (delta from previous + 1). + +#include "block.hpp" +#include "transaction.hpp" +#include "mempool.hpp" // compute_txid() + +#include +#include +#include +#include + +#include +#include +#include + +namespace dgb { +namespace coin { + +// ─── Short Transaction ID (6 bytes) ───────────────────────────────────────── + +struct ShortTxID { + uint8_t data[6]{}; + + ShortTxID() = default; + explicit ShortTxID(uint64_t v) { + for (int i = 0; i < 6; ++i) data[i] = static_cast((v >> (i*8)) & 0xff); + } + uint64_t to_uint64() const { + uint64_t v = 0; + for (int i = 0; i < 6; ++i) v |= static_cast(data[i]) << (i*8); + return v; + } + bool operator==(const ShortTxID& o) const { return std::memcmp(data, o.data, 6) == 0; } + bool operator<(const ShortTxID& o) const { return std::memcmp(data, o.data, 6) < 0; } + + template + void Serialize(Stream& s) const { + s.write(std::as_bytes(std::span{data, 6})); + } + template + void Unserialize(Stream& s) { + s.read(std::as_writable_bytes(std::span{data, 6})); + } +}; + +// ─── Prefilled Transaction ─────────────────────────────────────────────────── + +struct PrefilledTransaction { + uint32_t index{0}; // absolute index in block (differential on wire) + MutableTransaction tx; +}; + +// ─── Compact Block (HeaderAndShortIDs) ────────────────────────────────────── + +struct CompactBlock { + BlockHeaderType header; + uint64_t nonce{0}; + std::vector short_ids; + std::vector prefilled_txns; + + /// Compute SipHash keys from header hash and nonce. + void GetSipHashKeys(uint64_t& k0, uint64_t& k1) const { + auto packed = pack(header); + uint256 hdr_hash = Hash(packed.get_span()); + k0 = hdr_hash.GetUint64(0) ^ nonce; + k1 = hdr_hash.GetUint64(1) ^ nonce; + } + + /// Compute SipHash short ID for a txid using this block's key. + ShortTxID GetShortID(const uint256& txid) const { + uint64_t k0, k1; + GetSipHashKeys(k0, k1); + uint64_t h = SipHashUint256(k0, k1, txid); + return ShortTxID(h & 0xFFFFFFFFFFFFULL); + } + + /// Compute SipHash short ID with pre-computed keys (for batch lookups). + static ShortTxID GetShortID(uint64_t k0, uint64_t k1, const uint256& txid) { + uint64_t h = SipHashUint256(k0, k1, txid); + return ShortTxID(h & 0xFFFFFFFFFFFFULL); + } + + /// Serialize as BIP 152 HeaderAndShortIDs wire format. + template + void Serialize(Stream& s) const { + ::Serialize(s, header); + ::Serialize(s, nonce); + + // Short IDs: compact_size + raw 6-byte entries + WriteCompactSize(s, short_ids.size()); + for (const auto& sid : short_ids) + sid.Serialize(s); + + // Prefilled txns: compact_size + differential-index-encoded entries + WriteCompactSize(s, prefilled_txns.size()); + uint32_t prev_index = 0; + for (size_t i = 0; i < prefilled_txns.size(); ++i) { + const auto& pt = prefilled_txns[i]; + // BIP 152: first index is absolute, subsequent are (index - prev - 1) + uint32_t diff = (i == 0) ? pt.index : (pt.index - prev_index - 1); + WriteCompactSize(s, diff); + ::Serialize(s, TX_WITH_WITNESS(pt.tx)); + prev_index = pt.index; + } + } + + /// Deserialize from BIP 152 HeaderAndShortIDs wire format. + template + void Unserialize(Stream& s) { + ::Unserialize(s, header); + ::Unserialize(s, nonce); + + // Short IDs + uint64_t n_short_ids = ReadCompactSize(s); + short_ids.resize(n_short_ids); + for (auto& sid : short_ids) + sid.Unserialize(s); + + // Prefilled txns (differential index decoding) + uint64_t n_prefilled = ReadCompactSize(s); + prefilled_txns.resize(n_prefilled); + uint32_t prev_index = 0; + for (size_t i = 0; i < n_prefilled; ++i) { + uint32_t diff = static_cast(ReadCompactSize(s)); + prefilled_txns[i].index = (i == 0) ? diff : (prev_index + diff + 1); + UnserializeTransaction(prefilled_txns[i].tx, s, TX_WITH_WITNESS); + prev_index = prefilled_txns[i].index; + } + } +}; + +// ─── Block Transactions Request (getblocktxn) ────────────────────────────── + +struct BlockTransactionsRequest { + uint256 blockhash; + std::vector indexes; // absolute indexes (differential on wire) + + template + void Serialize(Stream& s) const { + ::Serialize(s, blockhash); + WriteCompactSize(s, indexes.size()); + uint32_t prev = 0; + for (size_t i = 0; i < indexes.size(); ++i) { + uint32_t diff = (i == 0) ? indexes[i] : (indexes[i] - prev - 1); + WriteCompactSize(s, diff); + prev = indexes[i]; + } + } + + template + void Unserialize(Stream& s) { + ::Unserialize(s, blockhash); + uint64_t n = ReadCompactSize(s); + indexes.resize(n); + uint32_t prev = 0; + for (size_t i = 0; i < n; ++i) { + uint32_t diff = static_cast(ReadCompactSize(s)); + indexes[i] = (i == 0) ? diff : (prev + diff + 1); + prev = indexes[i]; + } + } +}; + +// ─── Block Transactions Response (blocktxn) ───────────────────────────────── + +struct BlockTransactionsResponse { + uint256 blockhash; + std::vector txs; + + template + void Serialize(Stream& s) const { + ::Serialize(s, blockhash); + WriteCompactSize(s, txs.size()); + for (const auto& tx : txs) + ::Serialize(s, TX_WITH_WITNESS(tx)); + } + + template + void Unserialize(Stream& s) { + ::Unserialize(s, blockhash); + uint64_t n = ReadCompactSize(s); + txs.resize(n); + for (auto& tx : txs) + UnserializeTransaction(tx, s, TX_WITH_WITNESS); + } +}; + +// ─── Builder ───────────────────────────────────────────────────────────────── + +/// Build a compact block from a full block header + transactions. +inline CompactBlock BuildCompactBlock(const BlockHeaderType& header, + const std::vector& txs, + uint64_t nonce = 0) +{ + CompactBlock cb; + cb.header = header; + cb.nonce = nonce; + + // Coinbase always prefilled (index 0) + if (!txs.empty()) { + PrefilledTransaction pt; + pt.index = 0; + pt.tx = txs[0]; + cb.prefilled_txns.push_back(std::move(pt)); + } + + // Remaining txs as short IDs (pre-compute SipHash keys once) + // BIP 152 v2: use wtxid (witness serialization hash) for short IDs. + if (txs.size() > 1) { + uint64_t k0, k1; + cb.GetSipHashKeys(k0, k1); + for (size_t i = 1; i < txs.size(); ++i) { + auto packed = pack(TX_WITH_WITNESS(txs[i])); + uint256 wtxid = Hash(packed.get_span()); + cb.short_ids.push_back(CompactBlock::GetShortID(k0, k1, wtxid)); + } + } + + return cb; +} + +// ─── Reconstruction ───────────────────────────────────────────────────────── + +/// Result of attempting to reconstruct a full block from a compact block. +struct CompactBlockReconstructionResult { + bool complete{false}; + BlockType block; + std::vector missing_indexes; // absolute tx indexes still needed +}; + +/// Attempt to reconstruct a full block from a compact block + known transactions. +/// @param cb The received compact block. +/// @param known_txs Map of wtxid → transaction (BIP 152 v2 uses wtxid for short ID matching). +/// @return Result with reconstructed block (if complete) or missing indexes. +inline CompactBlockReconstructionResult ReconstructBlock( + const CompactBlock& cb, + const std::map& known_txs) +{ + CompactBlockReconstructionResult result; + + // Total transaction count = short_ids + prefilled + size_t total_txs = cb.short_ids.size() + cb.prefilled_txns.size(); + + // Build transaction array + std::vector txs(total_txs); + std::vector filled(total_txs, false); + + // Place prefilled transactions + for (const auto& pt : cb.prefilled_txns) { + if (pt.index >= total_txs) { + // Invalid index — reconstruction fails + result.complete = false; + return result; + } + txs[pt.index] = pt.tx; + filled[pt.index] = true; + } + + // Build short ID → known tx lookup + uint64_t k0, k1; + cb.GetSipHashKeys(k0, k1); + + std::map short_id_map; + for (const auto& [txid, tx] : known_txs) { + ShortTxID sid = CompactBlock::GetShortID(k0, k1, txid); + uint64_t key = sid.to_uint64(); + // Collision detection: if two txs map to the same short ID, skip both + if (short_id_map.count(key)) + short_id_map[key] = nullptr; // mark collision + else + short_id_map[key] = &tx; + } + + // Match short IDs to known transactions + size_t sid_idx = 0; + for (size_t i = 0; i < total_txs; ++i) { + if (filled[i]) continue; + + if (sid_idx >= cb.short_ids.size()) { + // More unfilled slots than short IDs — malformed + result.complete = false; + return result; + } + + uint64_t key = cb.short_ids[sid_idx].to_uint64(); + auto it = short_id_map.find(key); + if (it != short_id_map.end() && it->second != nullptr) { + txs[i] = *(it->second); + filled[i] = true; + } else { + result.missing_indexes.push_back(static_cast(i)); + } + ++sid_idx; + } + + if (result.missing_indexes.empty()) { + result.complete = true; + static_cast(result.block) = cb.header; + result.block.m_txs = std::move(txs); + } + + return result; +} + +} // namespace coin +} // namespace dgb diff --git a/src/impl/dgb/coin/mempool.hpp b/src/impl/dgb/coin/mempool.hpp new file mode 100644 index 000000000..820dca768 --- /dev/null +++ b/src/impl/dgb/coin/mempool.hpp @@ -0,0 +1,737 @@ +#pragma once + +// DGB embedded mempool. 1:1 mirror of src/impl/btc/coin/mempool.hpp (NON-MWEB). + +/// LTC Mempool — Phase 2 + UTXO fee computation +/// +/// In-memory transaction pool receiving transactions from P2P peers. +/// When a UTXOViewCache is available, computes per-transaction fees +/// (sum_inputs - sum_outputs) and maintains a feerate-sorted index +/// for optimal block template building. +/// +/// Thread-safe via internal mutex. + +#include "block.hpp" +#include "transaction.hpp" + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dgb { +namespace coin { + +// ─── MempoolEntry ──────────────────────────────────────────────────────────── + +struct MempoolEntry { + MutableTransaction tx; + uint256 txid; + uint32_t base_size{0}; // legacy serialized bytes (no witness) + uint32_t witness_size{0}; // extra bytes for witness data + uint32_t weight{0}; // base_size*4 + witness_size (BIP 141) + uint64_t fee{0}; // satoshi (computed from UTXO when available) + bool fee_known{false}; // true when fee was computed from UTXO lookups + time_t time_added{0}; + + double feerate() const { + uint32_t vsize = (weight + 3) / 4; // ceil(weight/4) + return (fee_known && vsize > 0) ? static_cast(fee) / vsize : 0.0; + } +}; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/// Compute txid = SHA256d of legacy (non-witness) serialization. +inline uint256 compute_txid(const MutableTransaction& tx) { + auto packed = pack(TX_NO_WITNESS(tx)); + return Hash(packed.get_span()); +} + +/// Compute BIP 141 weight: base_size*4 + witness_size. +/// base_size = legacy serialized bytes; full_size = with-witness bytes. +inline void compute_tx_weight(const MutableTransaction& tx, + uint32_t& base_size, + uint32_t& witness_size, + uint32_t& weight) +{ + auto legacy = pack(TX_NO_WITNESS(tx)); + auto full = pack(TX_WITH_WITNESS(tx)); + base_size = static_cast(legacy.size()); + uint32_t full_sz = static_cast(full.size()); + witness_size = full_sz > base_size ? full_sz - base_size : 0; + weight = base_size * 4 + witness_size; +} + +// ─── Mempool ───────────────────────────────────────────────────────────────── + +class Mempool { +public: + /// Maximum total bytes (legacy serialized) in the pool. + static constexpr size_t DEFAULT_MAX_BYTES = 300ULL * 1024 * 1024; // 300 MB + + /// Transaction expiry window. + static constexpr time_t DEFAULT_EXPIRY_SECS = 14 * 24 * 3600; // 14 days + + explicit Mempool(core::coin::ChainLimits limits = core::coin::LTC_LIMITS, + size_t max_bytes = DEFAULT_MAX_BYTES, + time_t expiry_sec = DEFAULT_EXPIRY_SECS) + : m_limits(limits) + , m_max_bytes(max_bytes) + , m_expiry_sec(expiry_sec) + {} + + /// Update the current chain tip height (for coinbase maturity checks). + /// Call after each block connection. + void set_tip_height(uint32_t h) { m_tip_height = h; } + void set_utxo(core::coin::UTXOViewCache* u) { m_utxo.store(u); } + + // Disable copy + Mempool(const Mempool&) = delete; + Mempool& operator=(const Mempool&) = delete; + + // ─── Mutation ──────────────────────────────────────────────────────── + + /// Add a transaction to the pool (no fee computation). + /// Returns false if already known or if the tx is malformed. + bool add_tx(const MutableTransaction& tx) { + return add_tx(tx, nullptr); + } + + /// Add a transaction to the pool with optional UTXO-based fee computation. + /// When utxo is non-null, computes fee = sum(input_values) - sum(output_values). + /// Falls back to fee_known=false if any input is missing from the UTXO set + /// or from a parent mempool transaction (chain-of-unconfirmed / CPFP). + bool add_tx(const MutableTransaction& tx, core::coin::UTXOViewCache* utxo) { + uint256 txid = compute_txid(tx); + + std::lock_guard lock(m_mutex); + + // Reject duplicates + if (m_pool.count(txid)) + return false; + + MempoolEntry entry; + entry.tx = tx; + entry.txid = txid; + compute_tx_weight(tx, entry.base_size, entry.witness_size, entry.weight); + entry.time_added = std::time(nullptr); + + // Compute fee from UTXO + mempool parent lookups + compute_fee_locked(entry, utxo); + + // Enforce size cap: evict oldest entries until we have room + int evicted = 0; + while (m_total_bytes + entry.base_size > m_max_bytes && !m_time_index.empty()) { + auto oldest = m_time_index.begin(); + evict_one_locked(oldest->second); + ++evicted; + } + + m_pool[txid] = std::move(entry); + auto& stored = m_pool[txid]; + m_time_index.emplace(stored.time_added, txid); + m_total_bytes += stored.base_size; + + // Track spent outputs for conflict detection + for (const auto& vin : stored.tx.vin) { + m_spent_outputs[std::make_pair(vin.prevout.hash, vin.prevout.index)] = txid; + } + + // Add to feerate index if fee is known + if (stored.fee_known) { + m_feerate_index.emplace(stored.feerate(), txid); + } + + // Periodic mempool stats (every 100 txs) + if (m_pool.size() % 100 == 0 || m_pool.size() <= 5) { + LOG_INFO << "[EMB] Mempool: size=" << m_pool.size() + << " bytes=" << m_total_bytes << "/" << m_max_bytes + << " txid=" << txid.GetHex().substr(0, 16) + << " w=" << stored.weight + << " fee=" << (stored.fee_known ? std::to_string(stored.fee) : "?") + << (evicted > 0 ? " evict=" + std::to_string(evicted) : ""); + } + return true; + } + + /// Remove a single transaction by txid. + void remove_tx(const uint256& txid) { + std::lock_guard lock(m_mutex); + remove_tx_locked(txid); + } + + /// Manually set a transaction's fee (for testing without a UTXO set). + void set_tx_fee(const uint256& txid, uint64_t fee) { + std::lock_guard lock(m_mutex); + auto it = m_pool.find(txid); + if (it == m_pool.end()) return; + it->second.fee = fee; + it->second.fee_known = true; + m_feerate_index.emplace(it->second.feerate(), txid); + } + + /// Remove confirmed txs + double-spend conflicts from mempool. + /// Called from the full_block callback when a new block arrives via P2P. + /// + /// Phase 1: remove txs whose txid appears in the block. + /// Phase 2: remove mempool txs that spend the same outputs as block txs + /// (double-spend / conflict detection via m_spent_outputs). + void remove_for_block(const BlockType& block) { + std::lock_guard lock(m_mutex); + int removed = 0, conflicts = 0; + + // Phase 1: remove confirmed txs by txid + for (const auto& mtx : block.m_txs) { + uint256 txid = compute_txid(mtx); + if (m_pool.count(txid)) ++removed; + remove_tx_locked(txid); + } + + // Phase 2: remove mempool txs that conflict with block txs + // (spend the same input as a confirmed tx) + for (const auto& mtx : block.m_txs) { + for (const auto& vin : mtx.vin) { + auto key = std::make_pair(vin.prevout.hash, vin.prevout.index); + auto it = m_spent_outputs.find(key); + if (it != m_spent_outputs.end()) { + // A mempool tx spends the same output — it's now invalid + auto conflict_txid = it->second; + if (m_pool.count(conflict_txid)) { + LOG_INFO << "[EMB-BTC] Mempool: removing conflict tx " + << conflict_txid.GetHex().substr(0, 16) + << " (spends same input as confirmed tx)"; + ++conflicts; + } + remove_tx_locked(conflict_txid); + } + } + } + + // Phase 3: remove orphaned children of conflict-removed txs. + // When a mempool tx is removed due to double-spend conflict (Phase 2), + // any child mempool tx that spends its outputs becomes orphaned. + // Reference: LTC txmempool.cpp removeRecursive() + int orphans = 0; + if (conflicts > 0) { + // Collect orphaned children: mempool txs whose inputs reference + // a txid that was just removed (no longer in m_pool). + std::vector orphan_txids; + for (auto& [txid, entry] : m_pool) { + for (const auto& vin : entry.tx.vin) { + // If input references a tx NOT in UTXO and NOT in mempool, + // then this tx is orphaned (parent was conflict-removed). + // Quick check: if prevout.hash was a conflict victim. + if (!m_pool.count(vin.prevout.hash)) { + // Parent not in mempool — check if it was a removed conflict + // by testing if this output is in m_spent_outputs + // (it won't be, since the parent was erased from m_spent_outputs + // during Phase 2). So just check: is the parent hash absent? + // This is conservative — could false-positive for confirmed txs, + // but those are fine (child remains valid, input is in UTXO). + // We only mark as orphan if fee computation would fail. + if (entry.fee_known) { + // Fee was known → inputs were valid before this block. + // Re-check by attempting fee recompute next cycle. + entry.fee_known = false; + } + } + } + } + } + + if (removed > 0 || conflicts > 0) { + LOG_INFO << "[EMB] Mempool: block cleanup removed=" << removed + << " conflicts=" << conflicts + << " remaining=" << m_pool.size(); + } + } + + /// Evict entries older than expiry_sec. + void evict_expired() { + time_t cutoff = std::time(nullptr) - m_expiry_sec; + std::lock_guard lock(m_mutex); + while (!m_time_index.empty() && m_time_index.begin()->first < cutoff) { + evict_one_locked(m_time_index.begin()->second); + } + } + + /// Clear the entire mempool. + void clear() { + std::lock_guard lock(m_mutex); + m_pool.clear(); + m_time_index.clear(); + m_feerate_index.clear(); + m_spent_outputs.clear(); + m_total_bytes = 0; + } + + // ─── Queries ───────────────────────────────────────────────────────── + + bool contains(const uint256& txid) const { + std::lock_guard lock(m_mutex); + return m_pool.count(txid) > 0; + } + + size_t size() const { + std::lock_guard lock(m_mutex); + return m_pool.size(); + } + + size_t byte_size() const { + std::lock_guard lock(m_mutex); + return m_total_bytes; + } + + /// Sum of all known fees across mempool transactions. + uint64_t total_fees() const { + std::lock_guard lock(m_mutex); + uint64_t sum = 0; + for (const auto& [txid, entry] : m_pool) { + if (entry.fee_known) + sum += entry.fee; + } + return sum; + } + + std::optional get_entry(const uint256& txid) const { + std::lock_guard lock(m_mutex); + auto it = m_pool.find(txid); + if (it == m_pool.end()) return std::nullopt; + return it->second; + } + + /// Return up to max_weight BIP141 weight units worth of transactions, + /// in FIFO order (oldest first — fair ordering without feerate data). + /// Legacy method for backward compatibility. + std::vector get_sorted_txs(uint32_t max_weight) const { + std::lock_guard lock(m_mutex); + + std::vector result; + uint32_t total_weight = 0; + + // Iterate by arrival time (oldest first) + for (auto& [ts, txid] : m_time_index) { + auto it = m_pool.find(txid); + if (it == m_pool.end()) continue; + + const auto& entry = it->second; + if (total_weight + entry.weight > max_weight) continue; + + total_weight += entry.weight; + result.push_back(entry.tx); + } + return result; + } + + /// Result struct for fee-aware transaction selection. + struct SelectedTx { + MutableTransaction tx; + uint64_t fee{0}; + bool fee_known{false}; + }; + + /// Return transactions sorted by feerate (highest first), up to max_weight. + /// Transactions with known fees are prioritized; unknown-fee txs fill remaining space. + /// Returns total_fees across all selected transactions (known fees only). + std::pair, uint64_t> + get_sorted_txs_with_fees(uint32_t max_weight) const { + std::lock_guard lock(m_mutex); + + std::vector result; + uint64_t total_fees = 0; + uint32_t total_weight = 0; + std::set selected; + + // Pass 1: highest feerate first (known-fee txs) + auto* utxo = m_utxo.load(); + for (auto it = m_feerate_index.begin(); it != m_feerate_index.end(); ++it) { + auto pit = m_pool.find(it->second); + if (pit == m_pool.end()) continue; + const auto& entry = pit->second; + if (!entry.fee_known) continue; + if (total_weight + entry.weight > max_weight) continue; + + // Guard: verify inputs still exist in UTXO (or parent mempool tx). + // Catches stale txs in the window between tip-change and full_block arrival. + if (utxo) { + bool inputs_ok = true; + for (const auto& vin : entry.tx.vin) { + core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index); + core::coin::Coin coin; + if (!utxo->get_coin(op, coin)) { + // Check if parent is in mempool (CPFP chain) + if (!m_pool.count(vin.prevout.hash) || + vin.prevout.index >= m_pool.at(vin.prevout.hash).tx.vout.size()) { + inputs_ok = false; + break; + } + } + } + if (!inputs_ok) continue; // skip stale tx + } + + total_weight += entry.weight; + total_fees += entry.fee; + result.push_back({entry.tx, entry.fee, true}); + selected.insert(entry.txid); + } + + // Unknown-fee txs excluded from template — they'll be included + // after fee revalidation once UTXO processes their input blocks. + // Including them with fee=0 would cause coinbasevalue mismatch + // vs p2pool (which gets accurate fees from daemon GBT). + + return {std::move(result), total_fees}; + } + + /// Re-attempt fee computation for all transactions with fee_known=false. + /// Call after a new block is connected (new UTXOs may resolve missing inputs). + /// Returns the number of transactions whose fees were successfully computed. + int recompute_unknown_fees(core::coin::UTXOViewCache* utxo) { + if (!utxo) return 0; + std::lock_guard lock(m_mutex); + int resolved = 0; + int still_unknown = 0; + uint64_t resolved_total_fee = 0; + for (auto& [txid, entry] : m_pool) { + if (entry.fee_known) continue; + if (compute_fee_locked(entry, utxo)) { + m_feerate_index.emplace(entry.feerate(), txid); + resolved_total_fee += entry.fee; + ++resolved; + } else { + ++still_unknown; + } + } + if (resolved > 0 || still_unknown > 0) { + LOG_INFO << "[EMB] Mempool fee revalidation: resolved=" << resolved + << " still_unknown=" << still_unknown + << " resolved_fees=" << resolved_total_fee << " sat" + << " pool_size=" << m_pool.size(); + } + return resolved; + } + + /// Re-validate all fee-known mempool transactions against the UTXO set. + /// Evicts any transaction whose inputs are no longer in the UTXO set + /// (i.e., were spent by a confirmed block but not caught by remove_for_block's + /// conflict detection — e.g., when the spending tx wasn't tracked in m_spent_outputs). + /// Call after remove_for_block() + UTXO connect to catch stale transactions. + /// Returns the number of evicted transactions. + int revalidate_inputs(core::coin::UTXOViewCache* utxo) { + if (!utxo) return 0; + std::lock_guard lock(m_mutex); + + std::vector to_remove; + for (const auto& [txid, entry] : m_pool) { + if (!entry.fee_known) continue; // already quarantined + for (const auto& vin : entry.tx.vin) { + core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index); + core::coin::Coin coin; + if (!utxo->get_coin(op, coin)) { + // Input not in UTXO — check if parent is in mempool (CPFP) + if (m_pool.count(vin.prevout.hash) && + vin.prevout.index < m_pool.at(vin.prevout.hash).tx.vout.size()) + continue; // parent still in mempool, tx is valid + to_remove.push_back(txid); + break; + } + } + } + + for (const auto& txid : to_remove) + remove_tx_locked(txid); + + if (!to_remove.empty()) { + LOG_INFO << "[EMB] Mempool revalidate: evicted " << to_remove.size() + << " stale txs (inputs spent), remaining=" << m_pool.size(); + } + return static_cast(to_remove.size()); + } + + /// Snapshot of all txids currently in the pool. + std::vector all_txids() const { + std::lock_guard lock(m_mutex); + std::vector out; + out.reserve(m_pool.size()); + for (auto& [txid, _] : m_pool) + out.push_back(txid); + return out; + } + + /// Snapshot of all transactions as a txid→tx map (for compact block reconstruction). + std::map all_txs_map() const { + std::lock_guard lock(m_mutex); + std::map out; + for (const auto& [txid, entry] : m_pool) + out[txid] = entry.tx; + return out; + } + + /// Return all transactions keyed by wtxid (BIP 152 v2 compact block reconstruction). + std::map all_txs_map_wtxid() const { + std::lock_guard lock(m_mutex); + std::map out; + for (const auto& [txid, entry] : m_pool) { + auto packed = pack(TX_WITH_WITNESS(entry.tx)); + uint256 wtxid = Hash(packed.get_span()); + out[wtxid] = entry.tx; + } + return out; + } + + // ─── Lightweight snapshot for explorer API ─────────────────────────── + + /// Lightweight entry metadata (no MutableTransaction copy). + struct MempoolEntrySummary { + uint256 txid; + uint32_t base_size{0}; + uint32_t weight{0}; + uint64_t fee{0}; + bool fee_known{false}; + time_t time_added{0}; + uint32_t n_vin{0}; + uint32_t n_vout{0}; + double feerate() const { + uint32_t vsize = (weight + 3) / 4; + return (fee_known && vsize > 0) ? static_cast(fee) / vsize : 0.0; + } + }; + + /// Aggregate mempool statistics + sorted entry list. + struct MempoolSummary { + size_t tx_count{0}; + size_t total_bytes{0}; + uint64_t total_fees{0}; + uint32_t total_weight{0}; + size_t fee_known_count{0}; + double min_feerate{0}; + double max_feerate{0}; + double median_feerate{0}; + double avg_feerate{0}; + time_t oldest_time{0}; + std::vector entries; // sorted by feerate descending + }; + + /// Build a lightweight snapshot: copies only metadata (no tx bodies), + /// computes aggregates, sorts by feerate descending. Single lock hold. + MempoolSummary get_summary() const { + std::lock_guard lock(m_mutex); + MempoolSummary s; + s.tx_count = m_pool.size(); + s.total_bytes = m_total_bytes; + s.entries.reserve(m_pool.size()); + + std::vector feerates; // for median computation + double feerate_sum = 0; + s.oldest_time = std::numeric_limits::max(); + + for (const auto& [id, e] : m_pool) { + MempoolEntrySummary es; + es.txid = e.txid; + es.base_size = e.base_size; + es.weight = e.weight; + es.fee = e.fee; + es.fee_known = e.fee_known; + es.time_added = e.time_added; + es.n_vin = static_cast(e.tx.vin.size()); + es.n_vout = static_cast(e.tx.vout.size()); + s.total_weight += e.weight; + if (e.fee_known) { + s.total_fees += e.fee; + ++s.fee_known_count; + double fr = es.feerate(); + feerates.push_back(fr); + feerate_sum += fr; + if (fr < s.min_feerate || s.min_feerate == 0) s.min_feerate = fr; + if (fr > s.max_feerate) s.max_feerate = fr; + } + if (e.time_added < s.oldest_time) s.oldest_time = e.time_added; + s.entries.push_back(std::move(es)); + } + + // Sort by feerate descending + std::sort(s.entries.begin(), s.entries.end(), + [](const MempoolEntrySummary& a, const MempoolEntrySummary& b) { + return a.feerate() > b.feerate(); + }); + + // Median + average feerate + if (!feerates.empty()) { + std::sort(feerates.begin(), feerates.end()); + s.median_feerate = feerates[feerates.size() / 2]; + s.avg_feerate = feerate_sum / feerates.size(); + } + if (s.tx_count == 0) s.oldest_time = 0; + + return s; + } + +private: + // ─── Internal (caller holds mutex) ─────────────────────────────────── + + /// Compute fee for a mempool entry using UTXO + parent mempool lookups. + /// Includes MoneyRange overflow checks and coinbase/pegout maturity. + /// Reference: LTC consensus/tx_verify.cpp CheckTxInputs() + /// Returns true if fee was successfully computed. + bool compute_fee_locked(MempoolEntry& entry, core::coin::UTXOViewCache* utxo) { + if (!utxo) { + entry.fee = 0; + entry.fee_known = false; + return false; + } + + int64_t value_in = 0; + bool all_found = true; + + for (const auto& vin : entry.tx.vin) { + core::coin::Outpoint op(vin.prevout.hash, vin.prevout.index); + core::coin::Coin coin; + + if (utxo->get_coin(op, coin)) { + // MoneyRange check on individual coin value + if (!core::coin::money_range(coin.value, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; + } + // Coinbase/pegout maturity check + if (m_tip_height > 0 && !coin.is_mature(m_tip_height, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; // premature spend + } + value_in += coin.value; + // MoneyRange check on accumulated value_in + if (!core::coin::money_range(value_in, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; + } + } else { + // Not in UTXO — check parent mempool tx (CPFP) + auto parent_it = m_pool.find(vin.prevout.hash); + if (parent_it != m_pool.end() + && vin.prevout.index < parent_it->second.tx.vout.size()) { + int64_t parent_val = parent_it->second.tx.vout[vin.prevout.index].value; + if (!core::coin::money_range(parent_val, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; + } + value_in += parent_val; + if (!core::coin::money_range(value_in, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; + } + } else { + all_found = false; + break; + } + } + } + + if (!all_found) { + entry.fee = 0; + entry.fee_known = false; + return false; + } + + // Sum outputs with overflow check + int64_t value_out = 0; + for (const auto& vout : entry.tx.vout) { + value_out += vout.value; + if (!core::coin::money_range(value_out, m_limits)) { + entry.fee = 0; entry.fee_known = false; + return false; + } + } + + int64_t fee = value_in - value_out; + if (fee < 0 || !core::coin::money_range(fee, m_limits)) { + entry.fee = 0; + entry.fee_known = false; + return false; + } + + entry.fee = static_cast(fee); + entry.fee_known = true; + return true; + } + + void remove_tx_locked(const uint256& txid) { + auto it = m_pool.find(txid); + if (it == m_pool.end()) return; + + m_total_bytes -= it->second.base_size; + + // Clean up spent outputs index + for (const auto& vin : it->second.tx.vin) { + auto key = std::make_pair(vin.prevout.hash, vin.prevout.index); + auto so = m_spent_outputs.find(key); + if (so != m_spent_outputs.end() && so->second == txid) + m_spent_outputs.erase(so); + } + + // Remove from time index + auto range = m_time_index.equal_range(it->second.time_added); + for (auto ti = range.first; ti != range.second; ++ti) { + if (ti->second == txid) { + m_time_index.erase(ti); + break; + } + } + + // Remove from feerate index + if (it->second.fee_known) { + auto fr_range = m_feerate_index.equal_range(it->second.feerate()); + for (auto fi = fr_range.first; fi != fr_range.second; ++fi) { + if (fi->second == txid) { + m_feerate_index.erase(fi); + break; + } + } + } + + m_pool.erase(it); + } + + void evict_one_locked(const uint256& txid) { + remove_tx_locked(txid); + } + + // ─── State ─────────────────────────────────────────────────────────── + + mutable std::mutex m_mutex; + + std::map m_pool; // txid → entry + std::multimap m_time_index; // arrival time → txid (FIFO) + + /// Feerate index: feerate (sat/vbyte, descending) → txid. + /// Only contains entries with fee_known=true. + /// Used by get_sorted_txs_with_fees() for optimal block template building. + std::multimap> m_feerate_index; + + /// Conflict detection: (prev_txid, prev_n) → spending mempool txid. + /// Mirrors Litecoin Core's mapNextTx for O(1) double-spend detection. + std::map, uint256> m_spent_outputs; + + size_t m_total_bytes{0}; // sum of base_size across all entries + size_t m_max_bytes; + time_t m_expiry_sec; + core::coin::ChainLimits m_limits; // MoneyRange + maturity constants + uint32_t m_tip_height{0}; // current chain tip (for maturity checks) + std::atomic m_utxo{nullptr}; // for template-time input validation +}; + +} // namespace coin +} // namespace dgb From 501404097e19d52ecb741b301d1b139f8e0d7b62 Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 14 Jun 2026 00:49:44 +0000 Subject: [PATCH 3/4] dgb(coin): port embedded P2P node/connection/messages + wire CMake Phase A (#82). 1:1 mirror of src/impl/btc/coin (non-MWEB) speaking the DigiByte Core wire protocol; Scrypt-only. Two P2P-handshake constants carried from BTC are flagged TODO(M3) for reconciliation vs DigiByte Core params: protocol_version (70016) and advertised port (8333 -> DGB mainnet 12024). Neither is consensus. dgb_coin CMake lists the new files but stays unreferenced by add_subdirectory() pending the ci-steward OBJECT-lib wiring. --- src/impl/dgb/coin/CMakeLists.txt | 19 +- src/impl/dgb/coin/p2p_connection.cpp | 40 ++ src/impl/dgb/coin/p2p_connection.hpp | 92 +++ src/impl/dgb/coin/p2p_messages.hpp | 401 +++++++++++ src/impl/dgb/coin/p2p_node.cpp | 26 + src/impl/dgb/coin/p2p_node.hpp | 997 +++++++++++++++++++++++++++ 6 files changed, 1570 insertions(+), 5 deletions(-) create mode 100644 src/impl/dgb/coin/p2p_connection.cpp create mode 100644 src/impl/dgb/coin/p2p_connection.hpp create mode 100644 src/impl/dgb/coin/p2p_messages.hpp create mode 100644 src/impl/dgb/coin/p2p_node.cpp create mode 100644 src/impl/dgb/coin/p2p_node.hpp diff --git a/src/impl/dgb/coin/CMakeLists.txt b/src/impl/dgb/coin/CMakeLists.txt index 8e7dc89d1..3fb1f0f48 100644 --- a/src/impl/dgb/coin/CMakeLists.txt +++ b/src/impl/dgb/coin/CMakeLists.txt @@ -1,19 +1,28 @@ -# dgb_coin -- DGB coin-layer library (Path A minimal-stub surface). -# Mirrors src/impl/btc/coin/CMakeLists.txt, listing only the files that exist -# at this milestone. NOT yet referenced by any add_subdirectory(): top-level -# registration of src/impl/dgb follows the ci-steward OBJECT-lib convention -# (see ../CMakeLists.txt); staged here so that wiring is a one-line flip. +# dgb_coin -- DGB coin-layer library. +# Mirrors src/impl/btc/coin/CMakeLists.txt. Phase A (#82) adds the embedded +# P2P + mempool layer (non-MWEB, btc-shaped; Scrypt PoW lives in header_chain). +# NOT yet referenced by any add_subdirectory(): top-level registration of +# src/impl/dgb follows the ci-steward OBJECT-lib convention (see ../CMakeLists.txt); +# staged here so that wiring is a one-line flip. set(dgb_coin_impl rpc_data.hpp rpc.hpp + p2p_connection.hpp p2p_connection.cpp + p2p_node.cpp p2p_node.hpp + p2p_messages.hpp node.hpp coin_node.hpp coin_node.cpp ) set(dgb_coin_interface + txidcache.hpp node_interface.hpp + block.hpp + transaction.hpp transaction.cpp + compact_blocks.hpp header_chain.hpp + mempool.hpp template_builder.hpp ) diff --git a/src/impl/dgb/coin/p2p_connection.cpp b/src/impl/dgb/coin/p2p_connection.cpp new file mode 100644 index 000000000..0fff8df5f --- /dev/null +++ b/src/impl/dgb/coin/p2p_connection.cpp @@ -0,0 +1,40 @@ +#include "p2p_connection.hpp" + +namespace dgb +{ + +namespace coin +{ + +namespace p2p +{ + +void Connection::request_block(uint256 id, uint256 hash, std::function handler) +{ + if (m_get_block) + m_get_block->request(id, handler, hash); +} + +void Connection::get_block(uint256 id, BlockType response) +{ + if (m_get_block) + m_get_block->got_response(id, response); +} + +void Connection::request_header(uint256 id, uint256 hash, std::function handler) +{ + if (m_get_header) + m_get_header->request(id, handler, hash); +} + +void Connection::get_header(uint256 id, BlockHeaderType response) +{ + if (m_get_header) + m_get_header->got_response(id, response); +} + +} // namespace p2p + +} // namespace coin + +} // namespace dgb diff --git a/src/impl/dgb/coin/p2p_connection.hpp b/src/impl/dgb/coin/p2p_connection.hpp new file mode 100644 index 000000000..d5ee8ef39 --- /dev/null +++ b/src/impl/dgb/coin/p2p_connection.hpp @@ -0,0 +1,92 @@ +#pragma once + +// DGB coin-daemon P2P connection. 1:1 mirror of src/impl/btc/coin/p2p_connection.hpp. + +#include "block.hpp" + +#include +#include +#include +#include + +namespace dgb +{ + +namespace coin +{ + +namespace p2p +{ + +class Connection +{ + static constexpr int REQUEST_TIMEOUT_SEC = 15; + using get_block_t = ReplyMatcher::ID::RESPONSE::REQUEST; + using get_header_t = ReplyMatcher::ID::RESPONSE::REQUEST; + +private: + boost::asio::io_context* m_context{}; + std::shared_ptr m_socket; + + get_block_t* m_get_block{}; + get_header_t* m_get_header{}; + +public: + + Connection(boost::asio::io_context* context, std::shared_ptr socket) : m_context(context), m_socket(socket) + { + + } + + ~Connection() + { + if (m_get_block) + delete m_get_block; + if (m_get_header) + delete m_get_header; + + if (m_socket) + { + m_socket->cancel(); + m_socket->close(); + m_socket.reset(); // prevent use-after-close + } + } + + void init_requests(std::function block_req, std::function header_req) + { + m_get_block = new get_block_t(m_context, block_req, REQUEST_TIMEOUT_SEC); + m_get_header = new get_header_t(m_context, header_req, REQUEST_TIMEOUT_SEC); + } + + void request_block(uint256 id, uint256 hash, std::function handler); + void get_block(uint256 id, BlockType response); + + void request_header(uint256 id, uint256 hash, std::function handler); + void get_header(uint256 id, BlockHeaderType response); + + void write(std::unique_ptr& rmsg) + { + if (!m_socket) return; // peer disconnected or destroyed + try { + m_socket->write(std::move(rmsg)); + } catch (const std::exception& e) { + // Socket may be closed/broken — don't crash + m_socket.reset(); + } + } + + auto get_addr() const + { + if (m_socket) + return m_socket->get_addr(); + else + return NetService{}; + } +}; + +} // namespace p2p + +} // namespace coin + +} // namespace dgb diff --git a/src/impl/dgb/coin/p2p_messages.hpp b/src/impl/dgb/coin/p2p_messages.hpp new file mode 100644 index 000000000..cdbc1a72d --- /dev/null +++ b/src/impl/dgb/coin/p2p_messages.hpp @@ -0,0 +1,401 @@ +#pragma once + +// DGB coin-daemon P2P messages. 1:1 mirror of src/impl/btc/coin/p2p_messages.hpp +// (NON-MWEB: references base BlockType/MutableTransaction, NOT LTC MWEB types). +// Generic messages imported from bitcoin_family; coin-specific block/tx/headers +// + BIP152 cmpct messages defined here. + +#include "transaction.hpp" +#include "block.hpp" +#include "compact_blocks.hpp" + +#include + +#include +#include +#include +#include + +namespace dgb +{ +namespace coin +{ + +namespace p2p +{ + +// Bitcoin wire protocol uses uint32_t timestamp in addr messages, +// unlike the pool protocol which uses uint64_t. +struct btc_addr_record_t : addr_t +{ + uint32_t m_timestamp{}; + + btc_addr_record_t() : addr_t() {} + + C2POOL_SERIALIZE_METHODS(btc_addr_record_t) { READWRITE(obj.m_timestamp, AsBase(obj)); } +}; +//[+] void handle_message_tx(std::shared_ptr msg, CoindProtocol* protocol); // +//[+] void handle_message_block(std::shared_ptr msg, CoindProtocol* protocol); // +//[+] void handle_message_headers(std::shared_ptr msg, CoindProtocol* protocol); // + +// message_version +BEGIN_MESSAGE(version) + MESSAGE_FIELDS + ( + (uint32_t, m_version), + (uint64_t, m_services), + (uint64_t, m_timestamp), + (addr_t , m_addr_to), + (addr_t, m_addr_from), + (uint64_t, m_nonce), + (std::string, m_subversion), + (uint32_t, m_start_height) + ) + { + READWRITE(obj.m_version, obj.m_services, obj.m_timestamp, obj.m_addr_to, obj.m_addr_from, obj.m_nonce, obj.m_subversion, obj.m_start_height); + } +END_MESSAGE() + +BEGIN_MESSAGE(verack) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +BEGIN_MESSAGE(ping) + MESSAGE_FIELDS + ( + (uint64_t, m_nonce) + ) + { + READWRITE(obj.m_nonce); + } +END_MESSAGE() + +BEGIN_MESSAGE(pong) + MESSAGE_FIELDS + ( + (uint64_t, m_nonce) + ) + { + READWRITE(obj.m_nonce); + } +END_MESSAGE() + +BEGIN_MESSAGE(alert) + MESSAGE_FIELDS + ( + (std::string, m_message), + (std::string, m_signature) + ) + { + READWRITE(obj.m_message, obj.m_signature); + } +END_MESSAGE() + +struct inventory_type +{ + enum inv_type : uint32_t + { + tx = 1, + block = 2, + filtered_block = 3, + cmpct_block = 4, + wtx = 5, // MSG_WTX (BIP 339 — wtxid-based relay) + witness_tx = 0x40000001, // MSG_WITNESS_TX (BIP 144) + witness_block = 0x40000002, // MSG_WITNESS_BLOCK (BIP 144) + }; + + static constexpr uint32_t MSG_WITNESS_FLAG = 0x40000000; + + inv_type m_type; + uint256 m_hash; + + inventory_type() { } + inventory_type(inv_type type, uint256 hash) : m_type(type), m_hash(hash) { } + + /// Strip the witness flag to get the base type (tx or block). + inv_type base_type() const + { + return static_cast(static_cast(m_type) & ~MSG_WITNESS_FLAG); + } + + bool is_witness() const + { + return (static_cast(m_type) & MSG_WITNESS_FLAG) != 0; + } + + C2POOL_SERIALIZE_METHODS(inventory_type) {READWRITE(Using>>(obj.m_type), obj.m_hash);} +}; + +BEGIN_MESSAGE(inv) + MESSAGE_FIELDS + ( + (std::vector, m_invs) + ) + { + READWRITE(obj.m_invs); + } +END_MESSAGE() + +BEGIN_MESSAGE(getdata) + MESSAGE_FIELDS + ( + (std::vector, m_requests) + ) + { + READWRITE(obj.m_requests); + } +END_MESSAGE() + +BEGIN_MESSAGE(getblocks) + MESSAGE_FIELDS + ( + (uint32_t, m_version), + (std::vector, m_have), + (uint256, m_last) + ) + { + READWRITE(obj.m_version, obj.m_have, obj.m_last); + } +END_MESSAGE() + +BEGIN_MESSAGE(getheaders) + MESSAGE_FIELDS + ( + (uint32_t, m_version), + (std::vector, m_have), + (uint256, m_last) + ) + { + READWRITE(obj.m_version, obj.m_have, obj.m_last); + } +END_MESSAGE() + +BEGIN_MESSAGE(tx) + MESSAGE_FIELDS + ( + (MutableTransaction, m_tx) + ) + { + READWRITE(TX_WITH_WITNESS(obj.m_tx)); + } +END_MESSAGE() + +BEGIN_MESSAGE(block) + MESSAGE_FIELDS + ( + (BlockType, m_block), + (std::vector, m_raw_payload) + ) + { + READWRITE(obj.m_block); + } +END_MESSAGE() + +BEGIN_MESSAGE(headers) + MESSAGE_FIELDS + ( + (std::vector, m_headers), + (std::vector, m_raw_payload) + ) + { + READWRITE(obj.m_headers); + } +END_MESSAGE() + +// P2P address discovery messages (used by coin broadcaster) +BEGIN_MESSAGE(getaddr) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +BEGIN_MESSAGE(addr) + MESSAGE_FIELDS + ( + (std::vector, m_addrs) + ) + { + READWRITE(obj.m_addrs); + } +END_MESSAGE() + +// BIP 61 — reject message (deprecated in Bitcoin Core 0.20 but still sent by some peers) +BEGIN_MESSAGE(reject) + MESSAGE_FIELDS + ( + (std::string, m_message), + (uint8_t, m_ccode), + (std::string, m_reason), + (uint256, m_data) + ) + { + READWRITE(obj.m_message, obj.m_ccode, obj.m_reason, obj.m_data); + } +END_MESSAGE() + +// BIP 130 — sendheaders (empty, signals header-first block announcements) +BEGIN_MESSAGE(sendheaders) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +// notfound — same layout as inv; response when getdata items are unavailable +BEGIN_MESSAGE(notfound) + MESSAGE_FIELDS + ( + (std::vector, m_invs) + ) + { + READWRITE(obj.m_invs); + } +END_MESSAGE() + +// BIP 133 — feefilter (minimum feerate for tx relay, in sat/kB) +BEGIN_MESSAGE(feefilter) + MESSAGE_FIELDS + ( + (uint64_t, m_feerate) + ) + { + READWRITE(obj.m_feerate); + } +END_MESSAGE() + +// BIP 35 — mempool (empty, requests peer to send inv for all mempool txs) +BEGIN_MESSAGE(mempool) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +// BIP 152 — sendcmpct (compact block negotiation) +BEGIN_MESSAGE(sendcmpct) + MESSAGE_FIELDS + ( + (bool, m_announce), + (uint64_t, m_version) + ) + { + READWRITE(obj.m_announce, obj.m_version); + } +END_MESSAGE() + +// BIP 152 — cmpctblock (HeaderAndShortIDs) +class message_cmpctblock : public Message { +private: + using message_type = message_cmpctblock; +public: + message_cmpctblock() : Message("cmpctblock") {} + + CompactBlock m_compact_block; + + static std::unique_ptr make_raw(const CompactBlock& cb) { + auto temp = std::make_unique(); + temp->m_compact_block = cb; + auto result = std::make_unique(temp->m_command, pack(*temp)); + return result; + } + static std::unique_ptr make_raw() { + return std::make_unique("cmpctblock", PackStream{}); + } + static std::unique_ptr make(PackStream& stream) { + auto result = std::make_unique(); + stream >> *result; + return result; + } + template void Serialize(Stream& s) const { m_compact_block.Serialize(s); } + template void Unserialize(Stream& s) { m_compact_block.Unserialize(s); } +}; + +// BIP 152 — getblocktxn (request missing transactions) +class message_getblocktxn : public Message { +private: + using message_type = message_getblocktxn; +public: + message_getblocktxn() : Message("getblocktxn") {} + + BlockTransactionsRequest m_request; + + static std::unique_ptr make_raw(const BlockTransactionsRequest& req) { + auto temp = std::make_unique(); + temp->m_request = req; + auto result = std::make_unique(temp->m_command, pack(*temp)); + return result; + } + static std::unique_ptr make_raw() { + return std::make_unique("getblocktxn", PackStream{}); + } + static std::unique_ptr make(PackStream& stream) { + auto result = std::make_unique(); + stream >> *result; + return result; + } + template void Serialize(Stream& s) const { m_request.Serialize(s); } + template void Unserialize(Stream& s) { m_request.Unserialize(s); } +}; + +// BIP 152 — blocktxn (missing transactions response) +class message_blocktxn : public Message { +private: + using message_type = message_blocktxn; +public: + message_blocktxn() : Message("blocktxn") {} + + BlockTransactionsResponse m_response; + + static std::unique_ptr make_raw(const BlockTransactionsResponse& resp) { + auto temp = std::make_unique(); + temp->m_response = resp; + auto result = std::make_unique(temp->m_command, pack(*temp)); + return result; + } + static std::unique_ptr make_raw() { + return std::make_unique("blocktxn", PackStream{}); + } + static std::unique_ptr make(PackStream& stream) { + auto result = std::make_unique(); + stream >> *result; + return result; + } + template void Serialize(Stream& s) const { m_response.Serialize(s); } + template void Unserialize(Stream& s) { m_response.Unserialize(s); } +}; + +// BIP 339 — wtxidrelay (empty, signals wtxid-based tx relay; sent before verack) +BEGIN_MESSAGE(wtxidrelay) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +// BIP 155 — sendaddrv2 (empty, signals addrv2 support; sent before verack) +BEGIN_MESSAGE(sendaddrv2) + WITHOUT_MESSAGE_FIELDS() { } +END_MESSAGE() + +using Handler = MessageHandler< + message_version, + message_verack, + message_ping, + message_pong, + message_alert, + message_inv, + message_getdata, + message_getblocks, + message_getheaders, + message_tx, + message_block, + message_headers, + message_getaddr, + message_addr, + message_reject, + message_sendheaders, + message_notfound, + message_feefilter, + message_mempool, + message_sendcmpct, + message_cmpctblock, + message_getblocktxn, + message_blocktxn, + message_wtxidrelay, + message_sendaddrv2 +>; + +} // namespace p2p + +} // namespace coin + +} // namespace dgb \ No newline at end of file diff --git a/src/impl/dgb/coin/p2p_node.cpp b/src/impl/dgb/coin/p2p_node.cpp new file mode 100644 index 000000000..8d20170b8 --- /dev/null +++ b/src/impl/dgb/coin/p2p_node.cpp @@ -0,0 +1,26 @@ +#include "p2p_node.hpp" + +namespace dgb +{ +namespace coin +{ + +namespace p2p +{ + +std::string parse_net_error(const boost::system::error_code& ec) +{ + switch (ec.value()) + { + case boost::asio::error::eof: + return "EOF, socket disconnected"; + default: + return ec.message(); + } +} + +} // p2p + +} // namespace coin + +} // namespace dgb \ No newline at end of file diff --git a/src/impl/dgb/coin/p2p_node.hpp b/src/impl/dgb/coin/p2p_node.hpp new file mode 100644 index 000000000..c9efef005 --- /dev/null +++ b/src/impl/dgb/coin/p2p_node.hpp @@ -0,0 +1,997 @@ +#pragma once + +// DGB embedded P2P node. 1:1 mirror of src/impl/btc/coin/p2p_node.hpp (NON-MWEB). +// Speaks the DigiByte Core wire protocol to the embedded daemon; Scrypt-only. +// +// TODO(M3 / [decision-needed]): two P2P-handshake constants are carried from BTC +// and MUST be reconciled against DigiByte Core params before mainnet wiring: +// - protocol_version (70016 here) -> confirm DigiByte Core PROTOCOL_VERSION +// - advertised NetService port (8333 here) -> DGB mainnet default is 12024 +// Neither is consensus; both are P2P-layer and should ultimately come from config. + +#include "p2p_messages.hpp" +#include "p2p_connection.hpp" +#include "node_interface.hpp" +#include "compact_blocks.hpp" +#include "mempool.hpp" + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace io = boost::asio; + +#define ADD_P2P_HANDLER(name)\ + void handle(std::unique_ptr msg) +namespace dgb +{ +namespace coin +{ + +namespace p2p +{ + +std::string parse_net_error(const boost::system::error_code& ec); + +//-core::ICommmunicator: +// void error(const message_error_type& err, const NetService& service, const std::source_location where = std::source_location::current()) = 0; +// void error(const boost::system::error_code& ec, const NetService& service, const std::source_location where = std::source_location::current()) = 0; +// void handle(std::unique_ptr rmsg, const NetService& service) = 0; +// const std::vector& get_prefix() const = 0; +// +//-core::INetwork: +// void connected(std::shared_ptr socket) = 0; +// void disconnect() = 0; + +template +class NodeP2P : public core::ICommunicator, public core::INetwork, public core::Factory +{ + using config_t = ConfigType; + +private: + static constexpr time_t CONNECT_TIMEOUT_SEC = 10; + static constexpr time_t IDLE_TIMEOUT_SEC = 100; + static constexpr time_t PING_INTERVAL_SEC = 30; + + dgb::interfaces::Node* m_coin; + io::io_context* m_context; + config_t* m_config; + p2p::Handler m_handler; + + std::unique_ptr m_peer; + std::unique_ptr m_reconnect_timer; + std::unique_ptr m_ping_timer; + std::unique_ptr m_timeout_timer; + NetService m_target_addr; + bool m_reconnect_enabled = false; + bool m_handshake_complete = false; + std::string m_chain_label = "CoinP2P"; + // BIP 152 compact block state + bool m_peer_supports_cmpct{false}; + uint64_t m_peer_cmpct_version{0}; + bool m_peer_wants_cmpct_announce{false}; + // BIP 339 wtxidrelay state + bool m_peer_wtxidrelay{false}; + // Peer metadata from version message + uint64_t m_peer_services{0}; + uint32_t m_peer_version{0}; // protocol version (e.g. 70016) + std::string m_peer_subver; // user agent (e.g. "/Satoshi:28.0.0/") + uint32_t m_peer_start_height{0}; // chain height at connect time + std::chrono::steady_clock::time_point m_connected_at{std::chrono::steady_clock::now()}; + // BIP 35: request full mempool inventory after handshake + bool m_request_mempool_on_connect{false}; + // Compact block reconstruction state: pending compact block awaiting blocktxn + std::unique_ptr m_pending_cmpct; + std::vector m_pending_missing_indexes; + // Last compact block we SENT — cached to serve getblocktxn requests + BlockType m_sent_cmpct_block; + uint256 m_sent_cmpct_hash; + // External mempool for compact block tx matching + Mempool* m_mempool{nullptr}; + + // Callbacks for broadcaster integration + using AddrCallback = std::function&)>; + AddrCallback m_addr_callback; + using PeerHeightCallback = std::function; + PeerHeightCallback m_on_peer_height; + // Raw headers parser: if set, called with raw payload data instead of + // the standard 80+1 byte parser. Used for DOGE AuxPoW extended headers. + using RawHeadersParser = std::function(const uint8_t*, size_t)>; + RawHeadersParser m_raw_headers_parser; + // Raw block parser: if set, re-parses DOGE AuxPoW full blocks from raw P2P bytes. + using RawBlockParser = std::function; + RawBlockParser m_raw_block_parser; + +public: + NodeP2P(io::io_context* context, dgb::interfaces::Node* coin, config_t* config, + const std::string& chain_label = "CoinP2P") + : core::Factory(context, this, chain_label) + , m_context(context), m_coin(coin), m_config(config) + , m_chain_label(chain_label) + { + } + + /// Connect with automatic reconnection on failure/disconnect (30s interval). + void connect(NetService addr) + { + m_target_addr = addr; + m_reconnect_enabled = true; + core::Factory::connect(addr); + + // Periodic reconnect check: if m_peer is null, try again + m_reconnect_timer = std::make_unique(m_context, true); + m_reconnect_timer->start(30, [this]() { + if (!m_peer && m_reconnect_enabled) { + LOG_INFO << "" << "[" << m_chain_label << "] Reconnecting to " << m_target_addr.to_string() << "..."; + core::Factory::connect(m_target_addr); + } + }); + } + + // INetwork + void connected(std::shared_ptr socket) override + { + m_peer = std::make_unique(m_context, socket); + m_handshake_complete = false; + LOG_INFO << "" << "[" << m_chain_label << "] Connected to " << m_target_addr.to_string(); + + // Require version/verack progress soon after connect. + ensure_timeout_timer(); + m_timeout_timer->start(CONNECT_TIMEOUT_SEC, [this]() { + timeout("handshake timeout"); + }); + + // BTC service flags + protocol version: + // BTC: NODE_NETWORK | NODE_WITNESS (no MWEB; LTC-only). + // Protocol 70016 = BIP 339 wtxidrelay activation point (Bitcoin Core 0.21+). + // (LTC-DOGE conditional removed; this is the BTC-specific module.) + static constexpr uint64_t NODE_NETWORK = 1; + static constexpr uint64_t NODE_WITNESS = (1 << 3); + uint64_t our_services = NODE_NETWORK | NODE_WITNESS; + uint32_t protocol_version = 70016; + + auto msg_version = message_version::make_raw( + protocol_version, + our_services, + core::timestamp(), + addr_t{our_services, m_peer->get_addr()}, + addr_t{our_services, NetService{"192.168.0.1", 8333}}, + core::random::random_nonce(), + "c2pool-btc", + 0 + ); + + m_peer->write(msg_version); + } + + void disconnect() override + { + stop_ping_timer(); + stop_timeout_timer(); + m_handshake_complete = false; + m_peer.reset(); + } + + /// Send a getheaders request to the connected peer. + /// @param version Protocol version (typically 70015 or 70017). + /// @param locator Block locator hashes (tip-to-genesis order). + /// @param stop Stop hash (uint256::ZERO to request up to tip). + void send_getheaders(uint32_t version, const std::vector& locator, const uint256& stop) + { + if (!m_peer) return; + // Suppress per-request logging — Header sync progress indicator + // in add_headers() provides the meaningful status update. + auto msg = message_getheaders::make_raw(version, locator, stop); + m_peer->write(msg); + } + + /// Whether the handshake with the peer is complete. + bool is_handshake_complete() const { return m_handshake_complete; } + + /// Send BIP 35 mempool request — ask peer to announce all mempool txs via inv. + void send_mempool() { + if (!m_peer) return; + auto msg = message_mempool::make_raw(); + m_peer->write(msg); + } + + /// Send BIP 133 feefilter — advise peer of minimum feerate we accept (sat/kB). + /// Pass 0 to request all transactions (no filtering). + void send_feefilter(uint64_t min_feerate_sat_per_kb = 0) { + if (!m_peer) return; + auto msg = message_feefilter::make_raw(min_feerate_sat_per_kb); + m_peer->write(msg); + } + + // ICommmunicator + void error(const message_error_type& err, const NetService& service, const std::source_location where = std::source_location::current()) override + { + // Copy — the NetService reference may dangle if the socket is already freed + NetService svc_copy = service; + LOG_WARNING << "[" << m_chain_label << "] Peer " << svc_copy.to_string() + << " disconnected: " << err; + if (m_peer) + { + m_peer.reset(); + } + // else: already disconnected (double-fire race) — safe to ignore + + stop_ping_timer(); + stop_timeout_timer(); + m_handshake_complete = false; + } + + void error(const boost::system::error_code& ec, const NetService& service, const std::source_location where = std::source_location::current()) override + { + error(parse_net_error(ec), service, where); + } + + void handle(std::unique_ptr rmsg, const NetService& service) override + { + on_activity(); + + p2p::Handler::result_t result; + try + { + result = m_handler.parse(rmsg); + } catch (const std::runtime_error& ec) + { + LOG_ERROR << "NodeP2P handle(" << rmsg->m_command << ", " + << rmsg->m_data.size() << " bytes): " << ec.what(); + // todo: error + return; + } catch (const std::out_of_range& ec) + { + LOG_ERROR << "NodeP2P: " << ec.what(); + return; + } + + std::visit([&](auto& msg){ handle(std::move(msg)); }, result); + } + + const std::vector& get_prefix() const override + { + return m_config->coin()->m_p2p.prefix; + } + + void submit_block(BlockType& block) + { + if (m_peer) + { + auto rmsg = dgb::coin::p2p::message_block::make_raw(block, {}); + m_peer->write(rmsg); + } else + { + LOG_ERROR << "No bitcoind connection when block submittal attempted!"; + throw std::runtime_error("No bitcoind connection in submit_block"); + } + } + + /// Set callback for received addr messages (peer discovery). + void set_addr_callback(AddrCallback cb) { m_addr_callback = std::move(cb); } + /// Set callback for peer's reported chain height (from version message). + void set_on_peer_height(PeerHeightCallback cb) { m_on_peer_height = std::move(cb); } + /// Set custom raw headers parser (for DOGE AuxPoW extended headers). + void set_raw_headers_parser(RawHeadersParser p) { m_raw_headers_parser = std::move(p); } + + /// Set custom raw block parser (for DOGE AuxPoW full blocks). + void set_raw_block_parser(RawBlockParser p) { m_raw_block_parser = std::move(p); } + + /// Send getaddr to request peer addresses. + void send_getaddr() + { + if (m_peer) { + auto msg = message_getaddr::make_raw(); + m_peer->write(msg); + } + } + + /// Send inv for a block hash (merged chain relay — announcement only). + void send_block_inv(const uint256& block_hash) + { + if (m_peer) { + auto msg = message_inv::make_raw({inventory_type(inventory_type::block, block_hash)}); + m_peer->write(msg); + } + } + + /// Request a full block via getdata. + /// BTC: MSG_WITNESS_BLOCK (0x40000002) — BIP 144 witness-bearing block. + /// (LTC used MSG_MWEB_BLOCK 0x60000002 for MWEB state extraction; + /// bitcoind doesn't recognise that inv type and would peer-disconnect.) + void request_full_block(const uint256& block_hash) + { + if (m_peer) { + auto msg = message_getdata::make_raw( + {inventory_type(inventory_type::witness_block, block_hash)}); + m_peer->write(msg); + } + } + + /// Request a block via plain MSG_BLOCK (0x02) getdata. + /// Works for any block regardless of MWEB support. + void request_block(const uint256& block_hash) + { + if (m_peer) { + auto msg = message_getdata::make_raw( + {inventory_type(inventory_type::block, block_hash)}); + m_peer->write(msg); + } + } + + /// Whether this peer supports compact blocks (BIP 152). + bool supports_compact_blocks() const { return m_peer_supports_cmpct; } + bool peer_wtxidrelay() const { return m_peer_wtxidrelay; } + /// Peer's service flags from version message (for NODE_BLOOM check etc.) + uint64_t peer_services() const { return m_peer_services; } + /// Check if peer supports NODE_BLOOM (required for BIP 35 mempool). + bool peer_has_bloom() const { return (m_peer_services & 4) != 0; } + + /// Peer metadata accessors + uint32_t peer_version() const { return m_peer_version; } + const std::string& peer_subver() const { return m_peer_subver; } + uint32_t peer_start_height() const { return m_peer_start_height; } + int64_t peer_uptime_sec() const { + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - m_connected_at).count(); + } + const std::string& chain_label() const { return m_chain_label; } + + /// Set mempool reference for compact block reconstruction. + void set_mempool(Mempool* mp) { m_mempool = mp; } + + /// Enable BIP 35 mempool request after handshake. + /// Call after UTXO is initialized so incoming txs can have fees computed. + void enable_mempool_request() { m_request_mempool_on_connect = true; } + + /// Relay a pre-serialized block via P2P. + /// Uses compact block format (BIP 152 v2) for peers that support it, + /// falling back to full block otherwise. + void submit_block_raw(const std::vector& block_bytes) + { + if (!m_peer) return; + + if (m_peer_supports_cmpct && m_peer_cmpct_version >= 2) { + // Deserialize the block to build a compact representation + try { + PackStream ps(block_bytes); + BlockType block; + ps >> block; + auto cb = BuildCompactBlock( + static_cast(block), block.m_txs); + auto rmsg = message_cmpctblock::make_raw(cb); + m_peer->write(rmsg); + + auto packed_hdr = pack(static_cast(block)); + auto blockhash = Hash(packed_hdr.get_span()); + + // Cache the full block so we can serve getblocktxn requests. + m_sent_cmpct_block = std::move(block); + m_sent_cmpct_hash = blockhash; + + LOG_INFO << "[" << m_chain_label << "] Sent compact block " + << blockhash.GetHex() + << " (" << cb.short_ids.size() << " short IDs, " + << cb.prefilled_txns.size() << " prefilled)"; + return; + } catch (const std::exception& e) { + LOG_WARNING << "[" << m_chain_label + << "] Compact block build failed (block_size=" << block_bytes.size() + << "), sending full block: " << e.what(); + } + } else { + LOG_DEBUG_COIND << "[" << m_chain_label << "] Peer does not support compact blocks" + << " (cmpct=" << m_peer_supports_cmpct + << " ver=" << m_peer_cmpct_version + << "), sending full block (" << block_bytes.size() << " bytes)"; + } + + // Fallback: send full block + submit_block_full(block_bytes); + } + + /// Send a full block message (legacy relay). + void submit_block_full(const std::vector& block_bytes) + { + if (!m_peer) return; + PackStream ps(block_bytes); + auto rmsg = std::make_unique("block", std::move(ps)); + m_peer->write(rmsg); + LOG_INFO << "[" << m_chain_label << "] Sent full block message (" + << block_bytes.size() << " bytes) to " << m_target_addr.to_string(); + } + + //[x][x][x] void handle_message_version(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_verack(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_ping(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_pong(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_alert(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_inv(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_tx(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_block(std::shared_ptr msg, CoindProtocol* protocol); // + //[x][x][x] void handle_message_headers(std::shared_ptr msg, CoindProtocol* protocol); // + +private: + void ensure_timeout_timer() + { + if (!m_timeout_timer) + m_timeout_timer = std::make_unique(m_context, false); + } + + void ensure_ping_timer() + { + if (!m_ping_timer) + m_ping_timer = std::make_unique(m_context, true); + } + + void stop_timeout_timer() + { + if (m_timeout_timer) + m_timeout_timer->stop(); + } + + void stop_ping_timer() + { + if (m_ping_timer) + m_ping_timer->stop(); + } + + void on_activity() + { + if (!m_peer) + return; + + ensure_timeout_timer(); + auto timeout = m_handshake_complete ? IDLE_TIMEOUT_SEC : CONNECT_TIMEOUT_SEC; + m_timeout_timer->restart(timeout); + } + + void timeout(const char* reason) + { + auto endpoint = m_peer ? m_peer->get_addr() : m_target_addr; + error(std::string("peer timeout: ") + reason, endpoint); + } + + void send_ping() + { + if (!m_peer || !m_handshake_complete) + return; + + auto msg_ping = message_ping::make_raw(core::random::random_nonce()); + m_peer->write(msg_ping); + } + + ADD_P2P_HANDLER(version) + { + m_peer_services = msg->m_services; + m_peer_version = msg->m_version; + m_peer_subver = msg->m_subversion; + m_peer_start_height = msg->m_start_height; + LOG_INFO << "[" << m_chain_label << "] version: " << msg->m_command + << " start_height=" << msg->m_start_height + << " services=0x" << std::hex << msg->m_services << std::dec + << " subver=" << msg->m_subversion; + // Notify header chain of peer's tip height for fast-sync scrypt skip. + if (m_on_peer_height && msg->m_start_height > 0) + m_on_peer_height(msg->m_start_height); + auto verack_msg = message_verack::make_raw(); + m_peer->write(verack_msg); + } + + ADD_P2P_HANDLER(verack) + { + m_peer->init_requests( + [&](uint256 hash) + { + auto getdata_msg = message_getdata::make_raw({inventory_type(inventory_type::block, hash)}); + m_peer->write(getdata_msg); + }, + [&](uint256 hash) + { + auto getheaders_msg = message_getheaders::make_raw(1, {}, hash); + m_peer->write(getheaders_msg); + } + ); + + m_handshake_complete = true; + ensure_timeout_timer(); + m_timeout_timer->restart(IDLE_TIMEOUT_SEC); + + ensure_ping_timer(); + m_ping_timer->start(PING_INTERVAL_SEC, [this]() { + send_ping(); + }); + + bool is_doge = (m_chain_label == "DOGE" || m_chain_label == "doge"); + + // BIP 130: request header-first block announcements + // DOGE Core may not fully support BIP 130 — skip to avoid misbehaving score + if (!is_doge) { + auto msg_sendheaders = message_sendheaders::make_raw(); + m_peer->write(msg_sendheaders); + } + // BIP 152: compact blocks — DOGE doesn't support segwit compact blocks (v2) + if (!is_doge) { + auto msg_cmpct = message_sendcmpct::make_raw(false, 2); + m_peer->write(msg_cmpct); + } + + // BIP 133: advertise minimum feerate (0 = accept all transactions) + // DOGE Core may not support BIP 133 — skip to avoid disconnection + if (!is_doge) { + send_feefilter(0); + } + + // BIP 35: Request mempool contents from peer. + // CRITICAL: Peers without NODE_BLOOM (0x04) will DISCONNECT us if we + // send the mempool message (litecoind net_processing.cpp:3918-3926). + // Only send if peer advertises NODE_BLOOM in their version services. + // Normal inv relay delivers NEW txs without BIP 35. + static constexpr uint64_t SVC_NODE_BLOOM = 4; + if (m_request_mempool_on_connect) { + if (m_peer_services & SVC_NODE_BLOOM) { + send_mempool(); + LOG_INFO << "[" << m_chain_label << "] Sent BIP 35 mempool request" + << " (peer has NODE_BLOOM)"; + } else { + LOG_INFO << "[" << m_chain_label << "] Skipped BIP 35 mempool request" + << " — peer lacks NODE_BLOOM (0x" << std::hex << m_peer_services + << std::dec << "), would cause disconnect"; + } + } + } + + ADD_P2P_HANDLER(ping) + { + auto msg_pong = message_pong::make_raw(msg->m_nonce); + m_peer->write(msg_pong); + } + + ADD_P2P_HANDLER(pong) + { + // just handled pong + } + + ADD_P2P_HANDLER(alert) + { + LOG_WARNING << "Handled message_alert signature: " << msg->m_signature; + } + + ADD_P2P_HANDLER(inv) + { + std::vector vinv; + + for (auto& inv : msg->m_invs) + { + auto btype = inv.base_type(); + // BIP 339: MSG_WTX (type 5) uses wtxid instead of txid. + // Request via MSG_WITNESS_TX (0x40000001) since getdata doesn't accept MSG_WTX. + // Reference: Bitcoin Core protocol.h line 447, net_processing.cpp line 3036 + if (inv.m_type == inventory_type::wtx) { + vinv.push_back(inventory_type(inventory_type::witness_tx, inv.m_hash)); + continue; + } + + switch (btype) + { + case inventory_type::tx: + // Always request with witness (MSG_WITNESS_TX) so segwit + // transactions arrive with their witness data intact. + // Without this, P2WPKH/P2WSH spends arrive stripped and + // fail CheckQueue when included in blocks. + vinv.push_back(inventory_type(inventory_type::witness_tx, inv.m_hash)); + break; + case inventory_type::block: + m_coin->new_block.happened(inv.m_hash); + // BTC advertises NODE_WITNESS — getdata for blocks must use + // MSG_WITNESS_BLOCK (0x40000002) per BIP 144, otherwise the + // peer drops witness data on the wire. + vinv.push_back(inventory_type( + inventory_type::witness_block, inv.m_hash)); + break; + case inventory_type::filtered_block: + case inventory_type::cmpct_block: + // Recognized but not requested — ignore + break; + default: + LOG_WARNING << "[" << m_chain_label << "] Unknown inv type 0x" << std::hex + << static_cast(inv.m_type) << std::dec; + break; + } + } + + if (!vinv.empty()) + { + auto msg_getdata = message_getdata::make_raw(vinv); + m_peer->write(msg_getdata); + } + } + + ADD_P2P_HANDLER(tx) + { + m_coin->new_tx.happened(Transaction(msg->m_tx)); + } + + ADD_P2P_HANDLER(block) + { + // When a raw block parser is set (DOGE AuxPoW), re-parse the block from + // raw P2P bytes. The standard BlockType deserialization misinterprets + // AuxPoW data as transactions, producing garbage. + BlockType block; + if (m_raw_block_parser && !msg->m_raw_payload.empty()) { + try { + block = m_raw_block_parser(msg->m_raw_payload.data(), + msg->m_raw_payload.size()); + } catch (const std::exception& e) { + LOG_WARNING << "[" << m_chain_label << "] AuxPoW block parser failed: " << e.what() + << " — falling back to standard parse"; + block = msg->m_block; + } + } else { + block = msg->m_block; + } + + auto header = static_cast(block); + auto packed_header = pack(header); + auto blockhash = Hash(packed_header.get_span()); + // ReplyMatcher may throw if nobody registered a pending request for + // this block (e.g., unsolicited block or getdata-triggered response). + // Catch to ensure full_block event always fires. + try { m_peer->get_block(blockhash, block); } catch (...) {} + try { m_peer->get_header(blockhash, header); } catch (...) {} + LOG_INFO << "[" << m_chain_label << "] Full block received: " + << blockhash.GetHex().substr(0, 16) << "..." + << " txs=" << block.m_txs.size(); + m_coin->full_block.happened(block); + } + + ADD_P2P_HANDLER(headers) + { + std::vector vheaders; + + // When a raw parser is set (DOGE AuxPoW), always prefer it over the + // standard parser. The standard parser misinterprets AuxPoW data as + // block transactions, producing a small number of garbage entries + // instead of the full 2000-header batch. + if (m_raw_headers_parser && !msg->m_raw_payload.empty()) { + try { + vheaders = m_raw_headers_parser( + msg->m_raw_payload.data(), msg->m_raw_payload.size()); + LOG_INFO << "[" << m_chain_label << "] AuxPoW parser: " + << vheaders.size() << " headers from " + << msg->m_raw_payload.size() << " bytes"; + } catch (const std::exception& e) { + LOG_WARNING << "[" << m_chain_label << "] AuxPoW headers parser failed: " << e.what(); + } + } + + if (vheaders.empty() && !msg->m_headers.empty()) { + // Standard path: headers parsed as 80-byte BlockType (LTC, BTC) + for (auto block : msg->m_headers) + { + auto header = (BlockHeaderType)block; + auto packed_header = pack(header); + auto blockhash = Hash(packed_header.get_span()); + try { + m_peer->get_header(blockhash, header); + } catch (const std::invalid_argument&) {} + vheaders.push_back(header); + } + } + + if (!vheaders.empty()) { + m_coin->new_headers.happened(vheaders); + + // BIP 130: when receiving a small headers batch (new block + // announcement), request the full block via getdata. + // BTC: MSG_WITNESS_BLOCK (0x40000002) — BIP 144 witness-bearing. + // LTC's MSG_MWEB_BLOCK (0x60000002) would be rejected by bitcoind. + if (vheaders.size() <= 3 && m_peer) { + for (auto& hdr : vheaders) { + auto packed = pack(hdr); + auto bhash = Hash(packed.get_span()); + auto getdata_msg = message_getdata::make_raw( + {inventory_type(inventory_type::witness_block, bhash)}); + m_peer->write(getdata_msg); + LOG_INFO << "[" << m_chain_label << "] Requesting full block " + << bhash.GetHex().substr(0, 16) << "..."; + } + } + } + } + + ADD_P2P_HANDLER(getaddr) + { + // We don't serve addresses — ignore + } + + ADD_P2P_HANDLER(addr) + { + if (m_addr_callback && !msg->m_addrs.empty()) { + std::vector addrs; + addrs.reserve(msg->m_addrs.size()); + for (auto& rec : msg->m_addrs) { + addrs.push_back(rec.m_endpoint); + } + m_addr_callback(addrs); + } + } + + ADD_P2P_HANDLER(reject) + { + LOG_WARNING << "Peer rejected " << msg->m_message + << " (code=" << static_cast(msg->m_ccode) + << "): " << msg->m_reason + << " hash=" << msg->m_data.GetHex(); + } + + ADD_P2P_HANDLER(sendheaders) + { + // Peer prefers header announcements — acknowledged + LOG_DEBUG_COIND << "Peer supports sendheaders (BIP 130)"; + } + + ADD_P2P_HANDLER(notfound) + { + for (auto& inv : msg->m_invs) + { + switch (inv.base_type()) + { + case inventory_type::block: + // Complete the ReplyMatcher with a default (empty) response + // so we don't wait for the 15s timeout. + try { + m_peer->get_block(inv.m_hash, BlockType{}); + } catch (...) {} + try { + m_peer->get_header(inv.m_hash, BlockHeaderType{}); + } catch (...) {} + break; + default: + break; + } + LOG_DEBUG_COIND << "Peer does not have inv 0x" << std::hex + << static_cast(inv.m_type) << std::dec + << " " << inv.m_hash.GetHex(); + } + } + + ADD_P2P_HANDLER(feefilter) + { + LOG_DEBUG_COIND << "Peer feefilter: " << msg->m_feerate << " sat/kB"; + } + + ADD_P2P_HANDLER(mempool) + { + // We don't serve mempool — ignore incoming request + } + + ADD_P2P_HANDLER(sendcmpct) + { + // BIP 152: Compact block negotiation — record peer capability + m_peer_supports_cmpct = true; + m_peer_cmpct_version = msg->m_version; + m_peer_wants_cmpct_announce = msg->m_announce; + LOG_INFO << "[" << m_chain_label << "] Peer supports compact blocks v" + << msg->m_version << " (announce=" << msg->m_announce << ")"; + } + + ADD_P2P_HANDLER(cmpctblock) + { + auto& cb = msg->m_compact_block; + auto packed_hdr = pack(cb.header); + auto blockhash = Hash(packed_hdr.get_span()); + + LOG_INFO << "[" << m_chain_label << "] Received compact block " + << blockhash.GetHex() + << " (" << cb.short_ids.size() << " short IDs, " + << cb.prefilled_txns.size() << " prefilled)"; + + // Always announce the new block to the node (header-based) + m_coin->new_block.happened(blockhash); + + // Attempt reconstruction from mempool + known_txs + // BIP 152 v2: short IDs are keyed by wtxid (witness txid). + std::map known; + + // Gather from node's known_txs (re-key by wtxid) + for (const auto& [txid, tx] : m_coin->known_txs) { + MutableTransaction mtx(tx); + auto packed = pack(TX_WITH_WITNESS(mtx)); + uint256 wtxid = Hash(packed.get_span()); + known[wtxid] = std::move(mtx); + } + + // Gather from mempool (wtxid-keyed) + if (m_mempool) { + auto mp_txs = m_mempool->all_txs_map_wtxid(); + known.merge(mp_txs); + } + + auto result = ReconstructBlock(cb, known); + + if (result.complete) { + LOG_INFO << "[" << m_chain_label << "] Compact block reconstructed: " + << blockhash.GetHex() + << " txs=" << result.block.m_txs.size(); + // Deliver as a full block + m_peer->get_block(blockhash, result.block); + auto header = static_cast(result.block); + m_peer->get_header(blockhash, header); + m_coin->full_block.happened(result.block); + } else { + LOG_INFO << "[" << m_chain_label << "] Compact block incomplete, " + << result.missing_indexes.size() << " txs missing — requesting via getblocktxn"; + // Save pending state and request missing transactions + m_pending_cmpct = std::make_unique(cb); + m_pending_missing_indexes = result.missing_indexes; + + BlockTransactionsRequest req; + req.blockhash = blockhash; + req.indexes = result.missing_indexes; + auto req_msg = message_getblocktxn::make_raw(req); + m_peer->write(req_msg); + } + } + + ADD_P2P_HANDLER(getblocktxn) + { + auto& req = msg->m_request; + + // Only serve our most recently sent compact block + if (req.blockhash != m_sent_cmpct_hash || m_sent_cmpct_block.m_txs.empty()) { + LOG_DEBUG_COIND << "[" << m_chain_label << "] getblocktxn for unknown block " + << req.blockhash.GetHex() << " — ignoring"; + return; + } + + BlockTransactionsResponse resp; + resp.blockhash = req.blockhash; + resp.txs.reserve(req.indexes.size()); + + for (uint32_t idx : req.indexes) { + if (idx >= m_sent_cmpct_block.m_txs.size()) { + LOG_WARNING << "[" << m_chain_label << "] getblocktxn: index " << idx + << " out of range (block has " << m_sent_cmpct_block.m_txs.size() << " txs)"; + return; // malformed request — drop + } + resp.txs.push_back(m_sent_cmpct_block.m_txs[idx]); + } + + auto rmsg = message_blocktxn::make_raw(resp); + m_peer->write(rmsg); + LOG_INFO << "[" << m_chain_label << "] Served " << resp.txs.size() + << " txs via blocktxn for " << req.blockhash.GetHex(); + } + + ADD_P2P_HANDLER(blocktxn) + { + auto& resp = msg->m_response; + + if (!m_pending_cmpct || m_pending_missing_indexes.empty()) { + LOG_WARNING << "[" << m_chain_label << "] Received blocktxn without pending compact block"; + return; + } + + if (resp.txs.size() != m_pending_missing_indexes.size()) { + LOG_WARNING << "[" << m_chain_label << "] blocktxn size mismatch: got " + << resp.txs.size() << ", expected " << m_pending_missing_indexes.size(); + m_pending_cmpct.reset(); + m_pending_missing_indexes.clear(); + return; + } + + // Reconstruct the full block with the missing transactions + auto& cb = *m_pending_cmpct; + size_t total_txs = cb.short_ids.size() + cb.prefilled_txns.size(); + std::vector txs(total_txs); + std::vector filled(total_txs, false); + + // Place prefilled transactions + for (const auto& pt : cb.prefilled_txns) { + if (pt.index < total_txs) { + txs[pt.index] = pt.tx; + filled[pt.index] = true; + } + } + + // Re-match from mempool (same as cmpctblock handler) + std::map known; + for (const auto& [txid, tx] : m_coin->known_txs) + known[txid] = MutableTransaction(tx); + if (m_mempool) { + auto mp_txs = m_mempool->all_txs_map(); + known.merge(mp_txs); + } + + uint64_t k0, k1; + cb.GetSipHashKeys(k0, k1); + std::map sid_map; + for (const auto& [txid, tx] : known) { + ShortTxID sid = CompactBlock::GetShortID(k0, k1, txid); + sid_map[sid.to_uint64()] = &tx; + } + + size_t sid_idx = 0; + for (size_t i = 0; i < total_txs; ++i) { + if (filled[i]) continue; + if (sid_idx < cb.short_ids.size()) { + auto it = sid_map.find(cb.short_ids[sid_idx].to_uint64()); + if (it != sid_map.end() && it->second) + txs[i] = *(it->second); + // else: will be filled from blocktxn response below + } + ++sid_idx; + } + + // Fill in the missing transactions from blocktxn response + for (size_t i = 0; i < m_pending_missing_indexes.size(); ++i) { + uint32_t idx = m_pending_missing_indexes[i]; + if (idx < total_txs) + txs[idx] = resp.txs[i]; + } + + // Build and deliver the full block + auto packed_hdr = pack(cb.header); + auto blockhash = Hash(packed_hdr.get_span()); + + BlockType block; + static_cast(block) = cb.header; + block.m_txs = std::move(txs); + + m_peer->get_block(blockhash, block); + auto header = static_cast(block); + m_peer->get_header(blockhash, header); + + LOG_INFO << "[" << m_chain_label << "] Compact block completed via blocktxn: " + << blockhash.GetHex(); + + m_pending_cmpct.reset(); + m_pending_missing_indexes.clear(); + } + + ADD_P2P_HANDLER(wtxidrelay) + { + // BIP 339: Peer wants wtxid-based tx relay + m_peer_wtxidrelay = true; + LOG_DEBUG_COIND << "[" << m_chain_label << "] Peer supports wtxidrelay (BIP 339)"; + } + + ADD_P2P_HANDLER(sendaddrv2) + { + // BIP 155: Peer wants addrv2 messages — acknowledged + LOG_DEBUG_COIND << "Peer supports sendaddrv2 (BIP 155)"; + } + + ADD_P2P_HANDLER(getdata) + { + // Peer requesting data from us — we don't serve blocks/txs + LOG_DEBUG_COIND << "Peer getdata with " << msg->m_requests.size() << " items (ignored)"; + } + + ADD_P2P_HANDLER(getblocks) + { + // Peer requesting block locator — we don't serve blocks + } + + ADD_P2P_HANDLER(getheaders) + { + // Peer requesting headers — we don't serve headers + LOG_DEBUG_COIND << "Peer getheaders (ignored, we don't serve headers)"; + } + + #undef ADD_P2P_HANDLER +}; + +} // namespace p2p + +} // namespace node + +} // namespace dgb From f992d9529afdff25556565a094b542520f1d76bc Mon Sep 17 00:00:00 2001 From: frstrtr Date: Sun, 14 Jun 2026 00:56:18 +0000 Subject: [PATCH 4/4] dgb(cmake): wire dgb_coin via add_subdirectory (Phase A #82) Flip per OBJECT-lib convention: register src/impl/dgb at top level and enable the coin subdir under COIN_DGB. dgb_coin now builds standalone (-DCOIN_DGB=ON); libdgb_coin.a links clean, 0 errors. daemon/test stay commented (M3). --- src/impl/CMakeLists.txt | 3 ++- src/impl/dgb/CMakeLists.txt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/impl/CMakeLists.txt b/src/impl/CMakeLists.txt index 42f7719e4..3c6c04c5e 100644 --- a/src/impl/CMakeLists.txt +++ b/src/impl/CMakeLists.txt @@ -1,2 +1,3 @@ add_subdirectory(ltc) -add_subdirectory(btc) \ No newline at end of file +add_subdirectory(btc) +add_subdirectory(dgb) diff --git a/src/impl/dgb/CMakeLists.txt b/src/impl/dgb/CMakeLists.txt index 0a4f3566c..a8a92ecc2 100644 --- a/src/impl/dgb/CMakeLists.txt +++ b/src/impl/dgb/CMakeLists.txt @@ -18,7 +18,7 @@ # follows the OBJECT-lib convention PR; held ~2-3 heartbeats, not raced here. if(COIN_DGB) message(STATUS "c2pool: DGB Scrypt-only coin module enabled (skeleton)") - # add_subdirectory(coin) + add_subdirectory(coin) # add_subdirectory(daemon) # add_subdirectory(test) endif()