diff --git a/src/impl/bch/coin/block.hpp b/src/impl/bch/coin/block.hpp new file mode 100644 index 000000000..c7bfea761 --- /dev/null +++ b/src/impl/bch/coin/block.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include "transaction.hpp" + +#include +#include +#include + +// --------------------------------------------------------------------------- +// bch::coin block -- ported from src/impl/btc/coin/block.hpp. +// +// >>> BCH DIVERGENCES <<< +// * NO SegWit (M1 §4.1): the block tx vector is serialized with the plain +// legacy encoding -- the BTC source wrapped it in TX_WITH_WITNESS(m_txs); +// BCH has no witness data, so m_txs serializes directly via the legacy +// Transaction format (see transaction.hpp banner). +// * CTOR (Canonical Transaction Ordering, Nov 2018 fork): non-coinbase txs +// in a block MUST be ordered by ascending txid. CTOR is a consensus rule +// enforced by the template builder / validation path, NOT by this struct -- +// BlockType stores txs in whatever order it is given. The ordering insertion +// point lives in coin/template_builder.hpp (a later slice). +// +// The 80-byte block header (version|prev|merkle|time|bits|nonce) is structurally +// identical to BTC. The ASERT DAA (Nov 2020) changes how m_bits is *computed*, +// not the header layout, so BlockHeaderType is unchanged here. +// --------------------------------------------------------------------------- + +namespace bch +{ + +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; + + // BCH: legacy (no-witness) tx-vector serialization -- see file banner. + template + void Serialize(Stream& s) const { + BlockHeaderType::Serialize(s); + ::Serialize(s, m_txs); + } + + template + void Unserialize(Stream& s) { + BlockHeaderType::Unserialize(s); + ::Unserialize(s, m_txs); + } + + BlockType() : BlockHeaderType() { } + + void SetNull() + { + BlockHeaderType::SetNull(); + m_txs.clear(); + } + + bool IsNull() const + { + return BlockHeaderType::IsNull(); + } +}; + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/chain_seeds.hpp b/src/impl/bch/coin/chain_seeds.hpp new file mode 100644 index 000000000..42f992e99 --- /dev/null +++ b/src/impl/bch/coin/chain_seeds.hpp @@ -0,0 +1,110 @@ +#pragma once + +/// DNS seed hostnames and hardcoded fallback IPs for Bitcoin Cash P2P networks. +/// Source: ref bitcoin-cash-node/src/chainparams.cpp (vSeeds.emplace_back + +/// nDefaultPort per CChainParams subclass). +/// +/// BCH shares BTC default ports on mainnet (8333) and testnet3 (18333) by fork +/// inheritance, but the DNS seed hostnames are entirely BCH-operated and +/// disjoint from Bitcoin Core's. testnet4 (28333) and the ports above are the +/// BCHN-specific values, NOT the BTC testnet4 (48333) set in btc/coin/. +/// +/// Trailing-dot convention from btc/coin/chain_seeds.hpp is intentionally NOT +/// applied here: BCHN chainparams stores the hostnames bare, and the BCH DNS +/// seeders are matched against bare names by the reference daemon. + +#include +#include +#include + +namespace bch { +namespace coin { + +/// DNS seeds for Bitcoin Cash mainnet (port 8333). +/// Source: bitcoin-cash-node/src/chainparams.cpp lines 210-229. +inline std::vector bch_mainnet_dns_seeds() +{ + return { + {"seed.flowee.cash", 8333}, + {"seed-bch.bitcoinforks.org", 8333}, + {"btccash-seeder.bitcoinunlimited.info", 8333}, + {"seed.bchd.cash", 8333}, + {"seed.bch.loping.net", 8333}, + {"dnsseed.electroncash.de", 8333}, + {"bchseed.c3-soft.com", 8333}, + {"bch.bitjson.com", 8333}, + }; +} + +/// DNS seeds for Bitcoin Cash testnet3 (port 18333). +/// Source: bitcoin-cash-node/src/chainparams.cpp lines 467-471. +inline std::vector bch_testnet_dns_seeds() +{ + return { + {"testnet-seed.bchd.cash", 18333}, + {"seed.tbch.loping.net", 18333}, + {"testnet-seed.bitcoinunlimited.info", 18333}, + }; +} + +/// DNS seeds for Bitcoin Cash testnet4 (port 28333). +/// Source: bitcoin-cash-node/src/chainparams.cpp lines 682-688. +/// testnet4 is the preferred c2pool-bch M3 integration target — small, fresh. +inline std::vector bch_testnet4_dns_seeds() +{ + return { + {"testnet4-seed-bch.toom.im", 28333}, + {"seed.tbch4.loping.net", 28333}, + {"testnet4-seed.flowee.cash", 28333}, + {"testnet4.bitjson.com", 28333}, + }; +} + +/// Hardcoded fallback peers for Bitcoin Cash mainnet (port 8333). +/// Used if DNS seeds fail after 60 seconds. Sample of the BCHN-operated DNS +/// seed hosts themselves (long-running, dual DNS+node) — replace with current +/// top-uptime peers if these become stale. +inline std::vector bch_mainnet_fixed_seeds() +{ + return { + {"seed.flowee.cash", 8333}, + {"seed.bch.loping.net", 8333}, + {"seed.bchd.cash", 8333}, + {"bch.bitjson.com", 8333}, + }; +} + +/// Hardcoded fallback peers for Bitcoin Cash testnet3. +inline std::vector bch_testnet_fixed_seeds() +{ + return { + {"seed.tbch.loping.net", 18333}, + {"testnet-seed.bchd.cash", 18333}, + }; +} + +/// Hardcoded fallback peers for Bitcoin Cash testnet4. +inline std::vector bch_testnet4_fixed_seeds() +{ + return { + {"seed.tbch4.loping.net", 28333}, + {"testnet4.bitjson.com", 28333}, + }; +} + +/// Get DNS seeds for the appropriate BCH network. +/// (testnet currently routes to testnet3 — switch to bch_testnet4_dns_seeds() +/// in the M3 live-validation step if testnet4 is the integration target.) +inline std::vector bch_dns_seeds(bool testnet) +{ + return testnet ? bch_testnet_dns_seeds() : bch_mainnet_dns_seeds(); +} + +/// Get fixed fallback seeds for the appropriate BCH network. +inline std::vector bch_fixed_seeds(bool testnet) +{ + return testnet ? bch_testnet_fixed_seeds() : bch_mainnet_fixed_seeds(); +} + +} // namespace coin +} // namespace bch diff --git a/src/impl/bch/coin/coin_node.cpp b/src/impl/bch/coin/coin_node.cpp new file mode 100644 index 000000000..ece6bbcf7 --- /dev/null +++ b/src/impl/bch/coin/coin_node.cpp @@ -0,0 +1,50 @@ +#include "coin_node.hpp" + +#include +#include + +namespace bch +{ + +namespace coin +{ + +core::coin::WorkView CoinNode::get_work_view() +{ + // No work source configured at all -> empty view (1:1 with the btc/ltc reference). + if (!m_embedded && !m_rpc) + return {}; + + // Embedded preferred, external RPC fallback. getwork() throws + // std::runtime_error when no template can be produced -- propagated to the + // caller (web_server), matching the ICoinNode contract. + rpc::WorkData wd = m_embedded ? m_embedded->getwork() : m_rpc->getwork(); + + // Retain the FULL WorkData (incl. m_txs) coin-side. Variable::set takes its + // argument BY VALUE, so this copies wd; sequenced BEFORE the std::move()s + // below so the copy completes first -- never move out of wd beforehand. + work.set(wd); + + core::coin::WorkView v; + v.m_data = std::move(wd.m_data); + v.m_hashes = std::move(wd.m_hashes); + v.m_latency = wd.m_latency; + return v; +} + +bool CoinNode::submit_block_hex(const std::string& block_hex, bool ignore_failure) +{ + // Guard sits ahead of the submit: in embedded-preferred mode m_rpc can be + // null. Returning false is the correct "no RPC sink" result. + if (!m_rpc) + return false; + + // BCH's NodeRPC::submit_block_hex is the agnostic 2-arg/no-mweb form (MWEB + // is LTC-specific and absent here), so we forward directly -- no 3-arg + // mweb="" coin-side overload exists or is needed for BCH. + return m_rpc->submit_block_hex(block_hex, ignore_failure); +} + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/coin_node.hpp b/src/impl/bch/coin/coin_node.hpp new file mode 100644 index 000000000..b35db9fe9 --- /dev/null +++ b/src/impl/bch/coin/coin_node.hpp @@ -0,0 +1,62 @@ +#pragma once + +// --------------------------------------------------------------------------- +// bch::coin::CoinNode -- concrete core::coin::ICoinNode for BCH (family-1 P2 +// WorkView seam). 1:1 mirror of the btc::coin::CoinNode reference, member-mapped +// onto BCH's node types (M3 slices 1-4): +// m_embedded : CoinNodeInterface* -- embedded in-process work source +// m_rpc : NodeRPC* -- external coin-RPC client +// work : Variable -- full per-coin WorkData kept coin-side +// +// Build-INERT: defines the type but is NOT yet wired into web_server (a separate +// member-wiring cluster). impl_bch stays unregistered in CMake (bch = skip-green) +// so CI is untouched; the seam gate flips only after wiring + a clean nm census. +// +// BCH carries NO MWEB: get_work_view() hands core only the agnostic slice and +// submit_block_hex() forwards the 2-arg/no-mweb form (mweb is LTC-specific). +// --------------------------------------------------------------------------- + +#include + +#include +#include +#include + +#include "rpc.hpp" +#include "rpc_data.hpp" +#include "template_builder.hpp" + +namespace bch +{ + +namespace coin +{ + +class CoinNode : public core::coin::ICoinNode +{ + // Embedded in-process template source. May be null when the node runs + // RPC-only. is_embedded() reports its presence. + CoinNodeInterface* m_embedded = nullptr; + + // External coin-RPC client. May be null in embedded-preferred mode; it is + // the sole sink for submit_block_hex(). has_rpc() reports its presence. + NodeRPC* m_rpc = nullptr; + + // Full per-coin WorkData (incl. m_txs) retained coin-side via work.set(wd); + // only the agnostic WorkView slice crosses the seam into core/web_server. + Variable work; + +public: + CoinNode(CoinNodeInterface* embedded, NodeRPC* rpc) + : m_embedded(embedded), m_rpc(rpc) {} + + core::coin::WorkView get_work_view() override; + bool submit_block_hex(const std::string& block_hex, bool ignore_failure) override; + + bool is_embedded() const override { return m_embedded != nullptr; } + bool has_rpc() const override { return m_rpc != nullptr; } +}; + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/compact_blocks.hpp b/src/impl/bch/coin/compact_blocks.hpp new file mode 100644 index 000000000..79a969e4d --- /dev/null +++ b/src/impl/bch/coin/compact_blocks.hpp @@ -0,0 +1,332 @@ +#pragma once + +/// BIP 152 Compact Block Support -- ported from src/impl/btc/coin/compact_blocks.hpp. +/// +/// 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). +/// +/// >>> BCH DIVERGENCES <<< +/// * NO SegWit (M1 4.1): BCH has no witness data. The BTC source wrapped every +/// transaction body in TX_WITH_WITNESS(...); here txs serialize directly via +/// the legacy Transaction format (transaction.hpp banner). UnserializeTransaction +/// is the 2-arg (tx, stream) form -- there is no TxParams/witness flag. +/// * Short IDs over txid (BIP152): BTC v2 keys short IDs on wtxid. BCH has no +/// wtxid distinction (wtxid == txid), so BuildCompactBlock computes short IDs +/// via compute_txid(). The reconstruction lookup is keyed by the caller's +/// mempool.all_txs_map_wtxid() -- which on BCH aliases all_txs_map() (txid). +/// That alias is the seam; it stays at the call site and is not threaded here. +/// * CTOR (Nov 2018) is a template-builder / validation ordering rule, NOT +/// enforced by these carriers. Reconstruction preserves the index order it is +/// given; it does not re-sort. + +#include "block.hpp" +#include "transaction.hpp" +#include "mempool.hpp" // compute_txid() + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace bch { +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, pt.tx); // BCH: legacy (no-witness) tx body + 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); // BCH: 2-arg, no-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); // BCH: legacy (no-witness) tx body + } + + template + void Unserialize(Stream& s) { + ::Unserialize(s, blockhash); + uint64_t n = ReadCompactSize(s); + txs.resize(n); + for (auto& tx : txs) + UnserializeTransaction(tx, s); // BCH: 2-arg, no-witness + } +}; + +// --- Builder ----------------------------------------------------------------- + +/// Build a compact block from a full block header + transactions. +/// BCH: short IDs are computed over txid (compute_txid) -- wtxid == txid. +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). + // BCH: short IDs keyed on txid (no wtxid distinction). + if (txs.size() > 1) { + uint64_t k0, k1; + cb.GetSipHashKeys(k0, k1); + for (size_t i = 1; i < txs.size(); ++i) { + uint256 txid = compute_txid(txs[i]); + cb.short_ids.push_back(CompactBlock::GetShortID(k0, k1, txid)); + } + } + + 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 txid -> transaction. BCH has no wtxid distinction, so +/// the caller supplies mempool.all_txs_map_wtxid() (== all_txs_map). +/// @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 bch diff --git a/src/impl/bch/coin/mempool.hpp b/src/impl/bch/coin/mempool.hpp new file mode 100644 index 000000000..365cabe8b --- /dev/null +++ b/src/impl/bch/coin/mempool.hpp @@ -0,0 +1,722 @@ +#pragma once + +/// BCH 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. +/// +/// BCH divergences from the BTC/LTC source (see transaction.hpp banner): +/// - No SegWit: transactions have no witness data. There is no BIP141 +/// weight and no vsize. Block/template budgeting is in raw BYTES, and +/// feerate is sat/byte (not sat/vbyte). MempoolEntry tracks `byte_size` +/// only — `weight`/`witness_size` are removed, not zeroed. +/// - txid = SHA256d of the (only) legacy serialization. wtxid == txid; +/// BCH has no separate witness txid. all_txs_map_wtxid() is retained +/// purely for the compact-block call site and is identical to +/// all_txs_map() — BCH BIP152 short IDs are computed over the txid. +/// +/// 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 bch { +namespace coin { + +/// BCH money/maturity limits. +/// 21M * 1e8 sat max value, 100-block coinbase maturity, no pegout (no MWEB). +/// Defined coin-local (not in core/coin/utxo.hpp) to keep the BCH module off +/// the shared-base path — BCH is standalone parent in V36. +/// Reference: bitcoin-cash-node/src/consensus/amount.h MAX_MONEY, +/// bitcoin-cash-node/src/consensus/consensus.h COINBASE_MATURITY +static constexpr core::coin::ChainLimits BCH_LIMITS = {2'100'000'000'000'000LL, 100, 0}; + +// ─── MempoolEntry ──────────────────────────────────────────────────────────── + +struct MempoolEntry { + MutableTransaction tx; + uint256 txid; + uint32_t byte_size{0}; // serialized bytes (BCH has a single, witness-free encoding) + 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}; + + /// Feerate in sat/byte (BCH has no vsize — block budget is raw bytes). + double feerate() const { + return (fee_known && byte_size > 0) ? static_cast(fee) / byte_size : 0.0; + } +}; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/// Compute txid = SHA256d of the BCH (witness-free) serialization. +/// wtxid == txid in BCH; there is no separate witness serialization. +inline uint256 compute_txid(const MutableTransaction& tx) { + auto packed = pack(tx); + return Hash(packed.get_span()); +} + +/// Compute the serialized byte size of a transaction (BCH block budget unit). +inline uint32_t compute_tx_byte_size(const MutableTransaction& tx) { + return static_cast(pack(tx).size()); +} + +// ─── Mempool ───────────────────────────────────────────────────────────────── + +class Mempool { +public: + /// Maximum total bytes (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 = BCH_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; + entry.byte_size = compute_tx_byte_size(tx); + 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.byte_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.byte_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-BCH] Mempool: size=" << m_pool.size() + << " bytes=" << m_total_bytes << "/" << m_max_bytes + << " txid=" << txid.GetHex().substr(0, 16) + << " sz=" << stored.byte_size + << " 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-BCH] Mempool: removing conflict tx " + << conflict_txid.GetHex().substr(0, 16) + << " (spends same input as confirmed tx)"; + ++conflicts; + } + remove_tx_locked(conflict_txid); + } + } + } + + // Phase 3: quarantine 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: bitcoin-cash-node txmempool.cpp removeRecursive() + if (conflicts > 0) { + for (auto& [txid, entry] : m_pool) { + for (const auto& vin : entry.tx.vin) { + // If input references a tx no longer in mempool, the parent + // may have been conflict-removed. Conservatively re-mark the + // fee unknown so it is revalidated against the UTXO next cycle. + if (!m_pool.count(vin.prevout.hash) && entry.fee_known) { + entry.fee_known = false; + } + } + } + } + + if (removed > 0 || conflicts > 0) { + LOG_INFO << "[EMB-BCH] 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_bytes serialized bytes worth of transactions, + /// in FIFO order (oldest first — fair ordering without feerate data). + /// Legacy method for backward compatibility. + /// + /// Note: the BCH template builder is responsible for re-ordering the + /// selected set into canonical (CTOR / lexical-txid) order; the mempool + /// returns a selection, not a block-ready ordering. + std::vector get_sorted_txs(uint32_t max_bytes) const { + std::lock_guard lock(m_mutex); + + std::vector result; + uint32_t total_bytes = 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_bytes + entry.byte_size > max_bytes) continue; + + total_bytes += entry.byte_size; + 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_bytes. + /// 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_bytes) const { + std::lock_guard lock(m_mutex); + + std::vector result; + uint64_t total_fees = 0; + uint32_t total_bytes = 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_bytes + entry.byte_size > max_bytes) 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_bytes += entry.byte_size; + 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-BCH] 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-BCH] 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 for compact-block reconstruction. + /// BCH has no SegWit: wtxid == txid, so this is identical to all_txs_map(). + /// Retained under this name only to satisfy the compact-block call site; + /// BCH BIP152 short IDs are derived from the txid. + std::map all_txs_map_wtxid() const { + return all_txs_map(); + } + + // ─── Lightweight snapshot for explorer API ─────────────────────────── + + /// Lightweight entry metadata (no MutableTransaction copy). + struct MempoolEntrySummary { + uint256 txid; + uint32_t byte_size{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 { + return (fee_known && byte_size > 0) ? static_cast(fee) / byte_size : 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}; + 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.byte_size = e.byte_size; + 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()); + 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 maturity. + /// Reference: bitcoin-cash-node 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 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.byte_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/byte, 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 Bitcoin Cash Node's mapNextTx for O(1) double-spend detection. + std::map, uint256> m_spent_outputs; + + size_t m_total_bytes{0}; // sum of byte_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 bch diff --git a/src/impl/bch/coin/node.hpp b/src/impl/bch/coin/node.hpp new file mode 100644 index 000000000..835b81e9f --- /dev/null +++ b/src/impl/bch/coin/node.hpp @@ -0,0 +1,130 @@ +#pragma once + +// --------------------------------------------------------------------------- +// bch::coin::Node -- embedded coin-node front-end, ported from +// src/impl/btc/coin/node.hpp (M3 slice 13, last P2P-layer file). +// +// This is the site where bch's config.hpp FIRST resolves concretely: the +// class is template and config binds only at the Node +// instantiation (binary entrypoint), exactly as the btc reference defers it. +// Until then nothing in the P2P layer is config-gated. +// +// Aggregates the embedded daemon front-ends landed in slices 1-12: +// m_rpc : NodeRPC -- external coin-RPC client (work source) +// m_p2p : p2p::NodeP2P -- embedded BCHN peer driver (fast relay) +// and derives from bch::interfaces::Node for the work/event surface. +// +// >>> BCH DIVERGENCE (standalone SHA256d parent, M1 4.x) <<< +// - No MWEB / extension-block payload (LTC-specific); full_block carries +// plain BlockType -- inherited from node_interface.hpp. +// - No AuxPoW hooks (BCH is not merged-mined). +// - Handshake protocol version is 70016, matching the btc reference and the +// bch p2p_node.hpp version handshake; no NODE_WITNESS / wtxidrelay. +// --------------------------------------------------------------------------- + +#include + +#include + +#include "rpc.hpp" +#include "p2p_node.hpp" +#include "node_interface.hpp" + +namespace bch +{ + +namespace coin +{ + +using p2p::NodeP2P; + +template +class Node : public bch::interfaces::Node +{ + using config_t = ConfigType; + + boost::asio::io_context* m_context; + config_t* m_config; + + std::unique_ptr m_rpc; + std::unique_ptr> m_p2p; + + void init_p2p() + { + m_p2p = std::make_unique>(m_context, this, m_config); + m_p2p->connect(m_config->coin()->m_p2p.address); + } + + void init_rpc() + { + m_rpc = std::make_unique(m_context, this, m_config->m_testnet); + m_rpc->connect(m_config->m_rpc.address, m_config->m_rpc.userpass); + + // work + work.set(m_rpc->getwork()); + } + +public: + + Node(auto* context, auto* config) : m_context(context), m_config(config) + { + } + + void run() + { + // RPC + init_rpc(); + } + + /// Start P2P connection to coin daemon for fast block relay. + /// Call after run() when P2P address is configured. + void start_p2p(const NetService& addr) + { + m_p2p = std::make_unique>(m_context, this, m_config); + m_p2p->connect(addr); + LOG_INFO << "Coin P2P broadcaster connecting to " << addr.to_string(); + } + + /// Submit a block via P2P directly (faster propagation than RPC). + void submit_block_p2p(BlockType& block) + { + if (m_p2p) + m_p2p->submit_block(block); + } + + /// Submit a pre-serialized block via P2P. Used by the stratum work + /// source which already has the full block bytes assembled from + /// (header || tx_count || coinbase || tx_data) and does not need to + /// round-trip through BlockType deserialization. + void submit_block_p2p_raw(const std::vector& block_bytes) + { + if (m_p2p) + m_p2p->submit_block_raw(block_bytes); + } + + bool has_p2p() const { return m_p2p != nullptr; } + + /// Send getheaders to drive header sync. + /// Locator should be hashes from chain tip back to genesis (sparsely); + /// for an empty chain pass {genesis_hash}. Stop = uint256::ZERO means + /// "send up to 2000 headers from locator first match forward". + /// version is the requester protocol_version (70016, matching the + /// bch p2p_node.hpp version handshake). + void send_getheaders(uint32_t version, + const std::vector& locator, + const uint256& stop) + { + if (m_p2p) + m_p2p->send_getheaders(version, locator, stop); + } + + /// True once the version+verack handshake completed with the peer. + bool is_handshake_complete() const + { + return m_p2p && m_p2p->is_handshake_complete(); + } +}; + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/node_interface.hpp b/src/impl/bch/coin/node_interface.hpp new file mode 100644 index 000000000..4710c97a9 --- /dev/null +++ b/src/impl/bch/coin/node_interface.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include + +#include "block.hpp" +#include "rpc_data.hpp" +#include "transaction.hpp" +#include "txidcache.hpp" + +#include +#include + +#include + +// --------------------------------------------------------------------------- +// bch::interfaces::Node -- ported from src/impl/btc/coin/node_interface.hpp. +// +// Coin-agnostic in shape; the per-coin types it aggregates are the bch::coin +// ports landed in M3 slices 1-2 (rpc::WorkData, Transaction, BlockHeaderType, +// BlockType, TXIDCache). +// +// >>> BCH DIVERGENCE (M1 4.1): NO MWEB <<< +// The BTC source documented full_block as "full block with txs + MWEB data". +// MWEB is a Litecoin extension-block construct and has no analogue on BCH, so +// full_block here carries the plain BlockType only -- no extension payload. +// --------------------------------------------------------------------------- + +namespace bch +{ + +namespace interfaces +{ + +struct Node +{ + 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 (no MWEB on BCH) + + coin::TXIDCache txidcache; + std::map known_txs; // TODO: move to other? +}; + +} // namespace interfaces + +} // namespace bch diff --git a/src/impl/bch/coin/p2p_connection.cpp b/src/impl/bch/coin/p2p_connection.cpp new file mode 100644 index 000000000..a85bfbfae --- /dev/null +++ b/src/impl/bch/coin/p2p_connection.cpp @@ -0,0 +1,40 @@ +#include "p2p_connection.hpp" + +namespace bch +{ + +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 bch diff --git a/src/impl/bch/coin/p2p_connection.hpp b/src/impl/bch/coin/p2p_connection.hpp new file mode 100644 index 000000000..c7b4ee15f --- /dev/null +++ b/src/impl/bch/coin/p2p_connection.hpp @@ -0,0 +1,97 @@ +#pragma once + +// BCH p2p Connection (M3 slice 11) — per-peer request/reply matcher + socket +// write path. Structurally identical to btc: the BCH wire divergences +// (no-witness serialization, txid-based short IDs, wtxid==txid) live entirely +// in p2p_messages.hpp + compact_blocks.hpp. The handshake / version negotiation +// (where wtxidrelay is deliberately NOT offered for BCH) lives in p2p_node.hpp, +// NOT here. This carrier therefore has no BCH-specific logic. + +#include "block.hpp" + +#include +#include +#include +#include + +namespace bch +{ + +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 bch diff --git a/src/impl/bch/coin/p2p_messages.hpp b/src/impl/bch/coin/p2p_messages.hpp new file mode 100644 index 000000000..874da7790 --- /dev/null +++ b/src/impl/bch/coin/p2p_messages.hpp @@ -0,0 +1,401 @@ +#pragma once + +// BCH P2P wire messages (M3 slice 7). +// +// Ported from btc/coin/p2p_messages.hpp with the SegWit/BIP-339 surface +// removed -- BCH has no witness, so there is no wtxid, no MSG_WTX, no +// witness inventory flag, and no wtxidrelay negotiation. tx payloads carry +// the plain (legacy) serialization that is already the only form BCH txs +// have (see bch/coin/transaction.hpp banner). +// +// The BIP-152 compact-block carrier messages (cmpctblock / getblocktxn / +// blocktxn) are wired below (M3 slice 10, cmpct-wireback). They wrap the +// CompactBlock / BlockTransactions{Request,Response} types from +// compact_blocks.hpp (slice 9). The BCH divergences -- no-witness tx +// bodies, short IDs over txid (compute_txid), and no CTOR re-sort -- live +// in those wrapped types, so the carrier wiring here is structurally the +// same as btc. The sendcmpct negotiation message has no CompactBlock +// dependency and has been present since slice 7 -- BCHN speaks BIP-152. +// Unlike btc there is NO wtxidrelay carrier: BCH has no wtxid. +// +// Source refs: bitcoin-cash-node/src/protocol.{h,cpp} (GetDataMsg enum, +// NetMsgType command strings) and src/net_processing.cpp. + +#include "transaction.hpp" +#include "block.hpp" +#include "compact_blocks.hpp" + +#include + +#include +#include +#include +#include + +namespace bch +{ +namespace coin +{ + +namespace p2p +{ + +// Bitcoin wire protocol uses uint32_t timestamp in addr messages, +// unlike the pool protocol which uses uint64_t. Identical to BTC -- the +// addr record wire format is fork-inherited. +struct bch_addr_record_t : addr_t +{ + uint32_t m_timestamp{}; + + bch_addr_record_t() : addr_t() {} + + C2POOL_SERIALIZE_METHODS(bch_addr_record_t) { READWRITE(obj.m_timestamp, AsBase(obj)); } +}; + +// 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 +{ + // BCH GetDataMsg (bitcoin-cash-node/src/protocol.h). No witness flag and + // no MSG_WTX -- BCH never adopted SegWit/BIP-339. doublespendproof is the + // one BCH-specific addition (DSPROOF, CHIP-2021-01). + enum inv_type : uint32_t + { + tx = 1, + block = 2, + filtered_block = 3, + cmpct_block = 4, + doublespendproof = 0x94a0, // MSG_DOUBLESPENDPROOF (BCH DSPROOF) + }; + + inv_type m_type; + uint256 m_hash; + + inventory_type() { } + inventory_type(inv_type type, uint256 hash) : m_type(type), m_hash(hash) { } + + 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) + ) + { + // No TX_WITH_WITNESS wrapper: BCH tx serialization is witness-free. + READWRITE(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 (still emitted by some BCH 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). Kept ahead of the +// compact-block carrier messages because it has no CompactBlock dependency. +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). Wraps CompactBlock from +// compact_blocks.hpp: legacy (no-witness) tx bodies, short IDs over txid. +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). Tx bodies are legacy +// (no-witness) per BlockTransactionsResponse in compact_blocks.hpp. +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 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_sendaddrv2 +>; + +} // namespace p2p + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/p2p_node.cpp b/src/impl/bch/coin/p2p_node.cpp new file mode 100644 index 000000000..3809d6099 --- /dev/null +++ b/src/impl/bch/coin/p2p_node.cpp @@ -0,0 +1,26 @@ +#include "p2p_node.hpp" + +namespace bch +{ +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 bch diff --git a/src/impl/bch/coin/p2p_node.hpp b/src/impl/bch/coin/p2p_node.hpp new file mode 100644 index 000000000..259761301 --- /dev/null +++ b/src/impl/bch/coin/p2p_node.hpp @@ -0,0 +1,946 @@ +#pragma once + +// --------------------------------------------------------------------------- +// bch::coin::p2p::NodeP2P (M3 slice 12) -- ported from +// src/impl/btc/coin/p2p_node.hpp. Drives the embedded BCHN peer connection: +// handshake, header/block sync, inv relay, BIP 152 compact blocks, and the +// BIP 35/130/133 niceties. +// +// >>> BCH DIVERGENCES from the BTC source <<< +// * NO SegWit: service flags advertise NODE_NETWORK | NODE_BITCOIN_CASH +// (NOT NODE_WITNESS). inv requests use plain MSG_TX (1) / MSG_BLOCK (2), +// never the witness-flagged variants, and there is NO MSG_WTX path. +// * NO BIP 339 wtxidrelay: BCH has no wtxid (wtxid == txid), so the +// wtxidrelay negotiation message and handler are dropped entirely +// (see p2p_messages.hpp -- it is absent from the Handler variant too). +// * Compact blocks negotiate version 1: short IDs are keyed on txid, not +// witness-txid (compact_blocks.hpp computes them via compute_txid()). +// * NO AuxPoW: BCH is a standalone parent (never merged-mined), so the +// DOGE raw_headers_parser / raw_block_parser hooks are removed -- BCH +// headers/blocks are the standard SHA256d serialization. +// * doublespendproof (DSPROOF, 0x94a0) inv is recognized but not requested. +// +// This class is templated on ConfigType and touches the per-coin config only +// through `m_config->coin()->m_p2p.prefix`. It carries NO concrete dependency +// on bch's config.hpp -- the config binding is deferred to the +// NodeP2P instantiation in node.hpp (slice 13). +// --------------------------------------------------------------------------- + +#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 bch +{ +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; + + bch::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}; + // 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. "/Bitcoin Cash Node:27.1.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; + +public: + NodeP2P(io::io_context* context, bch::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"); + }); + + // BCH service flags + protocol version: + // BCH (BCHN) advertises NODE_NETWORK | NODE_BITCOIN_CASH. There is NO + // NODE_WITNESS — BCH never adopted SegWit, so no witness-bearing inv + // types and no BIP 339 wtxidrelay negotiation. NODE_BITCOIN_CASH + // (1<<5) is the network-membership bit BCH peers require to peer. + static constexpr uint64_t NODE_NETWORK = 1; + static constexpr uint64_t NODE_BITCOIN_CASH = (1 << 5); + uint64_t our_services = NODE_NETWORK | NODE_BITCOIN_CASH; + 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-bch", + 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 = bch::coin::p2p::message_block::make_raw(block, {}); + m_peer->write(rmsg); + } else + { + LOG_ERROR << "No BCHN connection when block submittal attempted!"; + throw std::runtime_error("No BCHN 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); } + + /// 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 (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. + /// BCH: plain MSG_BLOCK (0x02) — there is no witness-bearing block inv, + /// so this is identical to request_block(). The method is kept distinct to + /// preserve the call surface inherited from the BTC reference. + void request_full_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); + } + } + + /// Request a block via plain MSG_BLOCK (0x02) getdata. + 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; } + /// 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 v1) for peers that support it, + /// falling back to full block otherwise. + void submit_block_raw(const std::vector& block_bytes) + { + if (!m_peer) return; + + // BCH negotiates compact block version 1 (txid short IDs); v2's + // witness-txid keying never applies. Accept any peer advertising >= 1. + if (m_peer_supports_cmpct && m_peer_cmpct_version >= 1) { + // 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. + 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(); + }); + + // BIP 130: request header-first block announcements + auto msg_sendheaders = message_sendheaders::make_raw(); + m_peer->write(msg_sendheaders); + + // BIP 152: compact blocks. BCH negotiates version 1 — short IDs are + // keyed on txid (BCH has no wtxid; v2's witness-txid keying does not + // apply). announce=false: serve on getdata, don't push unrequested. + auto msg_cmpct = message_sendcmpct::make_raw(false, 1); + m_peer->write(msg_cmpct); + + // BIP 133: advertise minimum feerate (0 = accept all transactions) + 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. Only send if peer advertises NODE_BLOOM. + // 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) + { + // BCH inv types carry no witness flag, so m_type is used directly + // (there is no base_type() mask as in the witness-bearing BTC fork). + switch (inv.m_type) + { + case inventory_type::tx: + // BCH: request the plain transaction (MSG_TX). There is no + // witness serialization, so no MSG_WITNESS_TX / MSG_WTX path. + vinv.push_back(inventory_type(inventory_type::tx, inv.m_hash)); + break; + case inventory_type::block: + m_coin->new_block.happened(inv.m_hash); + // BCH: getdata uses plain MSG_BLOCK (0x02) — no BIP 144 + // witness-bearing block on BCH. + vinv.push_back(inventory_type( + inventory_type::block, inv.m_hash)); + break; + case inventory_type::filtered_block: + case inventory_type::cmpct_block: + case inventory_type::doublespendproof: + // Recognized but not requested — ignore. doublespendproof + // (DSPROOF, CHIP-2021-01) is BCH-specific and not consumed here. + 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) + { + // BCH blocks are the standard SHA256d serialization — no AuxPoW + // extended payload, so the raw-block re-parser the BTC fork carried + // for DOGE is not needed here. + BlockType 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; + + // Standard path: BCH headers are 80-byte SHA256d block headers parsed + // as BlockType. No AuxPoW raw-parser fallback (BCH is not merged-mined). + 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. + // BCH: plain MSG_BLOCK (0x02) — no witness-bearing block inv. + 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::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.m_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. + // BCH: short IDs are keyed by txid (wtxid == txid). + std::map known; + + // Gather from node's known_txs (txid-keyed) + for (const auto& [txid, tx] : m_coin->known_txs) { + known[txid] = MutableTransaction(tx); + } + + // Gather from mempool. all_txs_map_wtxid() aliases all_txs_map() on + // BCH (no wtxid distinction) — both are txid-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) — txid-keyed on BCH + 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(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 coin + +} // namespace bch diff --git a/src/impl/bch/coin/rpc.cpp b/src/impl/bch/coin/rpc.cpp new file mode 100644 index 000000000..0c35efaef --- /dev/null +++ b/src/impl/bch/coin/rpc.cpp @@ -0,0 +1,467 @@ +#include "rpc.hpp" + +#include +#include + +#include +#include + +// --------------------------------------------------------------------------- +// bch::coin::NodeRPC -- external coin-RPC client BODY (M3 slice 5). +// Ported from src/impl/btc/coin/rpc.cpp; carries the BCH-divergent bits that +// have no 1:1 BTC mirror (see per-method notes): +// - check(): BCH-chain sentinel = the Aug-2017 fork block (478559); version +// floor 100000 per p2pool-merged-v36 bitcoincash.py VERSION_CHECK; the +// softfork-required gate is vacuous (bch::PoolConfig::SOFTFORKS_REQUIRED +// is empty -- BCH activations are MTP-based, not BIP9/GBT-signalled). +// - getwork(): getblocktemplate() takes NO "segwit" rule; transactions are +// unpacked with the LEGACY (witness-free) BCH serialization, so the GBT +// "txid" field == the data hash (no wtxid distinction). +// - submit_block(): no witness packing, no MWEB tail. +// - CashTokens-bearing outputs round-trip transparently (template carry). +// --------------------------------------------------------------------------- + +namespace bch +{ + +namespace coin +{ + +NodeRPC::NodeRPC(io::io_context* context, bch::interfaces::Node* coin, bool testnet) + : m_context(context), IS_TESTNET(testnet), m_resolver(*context), m_stream(*context), + m_client(*this, RPC_VER), m_coin(coin) +{ +} + +void NodeRPC::connect(NetService address, std::string userpass) +{ + m_address = address; + m_userpass = userpass; + + m_auth = std::make_unique(); + m_http_request = {http::verb::post, "/", 11}; + + m_auth->host = address.to_string(); + m_http_request.set(http::field::host, m_auth->host); + + m_http_request.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + m_http_request.set(http::field::content_type, "application/json"); + m_http_request.set(http::field::connection, "keep-alive"); + + std::string encoded_login2; + encoded_login2.resize(boost::beast::detail::base64::encoded_size(userpass.size())); + const auto result = boost::beast::detail::base64::encode(&encoded_login2[0], userpass.data(), userpass.size()); + encoded_login2.resize(result); + m_auth->authorization = "Basic " + encoded_login2; + + m_http_request.set(http::field::authorization, m_auth->authorization); + + // Async DNS resolve -- must NOT use the blocking m_resolver.resolve() overload + // as that stalls the entire io_context thread for the DNS round-trip. + m_resolver.async_resolve(address.address(), address.port_str(), + [this](boost::system::error_code ec, boost::asio::ip::tcp::resolver::results_type results) + { + if (ec) + { + LOG_ERROR << "CoindRPC DNS resolve failed: " << ec.message() << ", retrying in 15s"; + m_reconnect_timer = std::make_unique(m_context, false); + m_reconnect_timer->start(15, [this]() { connect(m_address, m_userpass); }); + return; + } + boost::asio::ip::tcp::endpoint endpoint = *results.begin(); + m_stream.async_connect(endpoint, + [this](boost::system::error_code ec) + { + if (ec) + { + if (ec == boost::system::errc::operation_canceled) + return; + + LOG_ERROR << "CoindRPC error when try connect: [" << ec.message() << "]."; + } else + { + try + { + if (check()) + { + m_connected = true; + LOG_INFO << "...CoindRPC connected!"; + return; + } + } + catch(const std::runtime_error& ec) + { + LOG_ERROR << "Error when try check CoindRPC: " << ec.what(); + } + } + + LOG_INFO << "Retry after 15 seconds..."; + m_connected = false; + m_stream.close(); + m_reconnect_timer = std::make_unique(m_context, false); + m_reconnect_timer->start(15, [this]() { connect(m_address, m_userpass); }); + } + ); + }); +} + +NodeRPC::~NodeRPC() +{ + beast::error_code ec; + m_stream.socket().shutdown(io::ip::tcp::socket::shutdown_both, ec); + if (ec) + { + // shutdown errors on close are typically benign; ignore + } +} + +void NodeRPC::reconnect() +{ + if (!m_connected) + return; // already reconnecting or never connected + m_connected = false; + LOG_WARNING << "RPC connection lost — reconnecting in 15 seconds..."; + m_stream.close(); + m_reconnect_timer = std::make_unique(m_context, false); + m_reconnect_timer->start(15, [this]() { connect(m_address, m_userpass); }); +} + +void NodeRPC::sync_reconnect() +{ + beast::error_code ec; + m_stream.socket().shutdown(io::ip::tcp::socket::shutdown_both, ec); + m_stream.close(); + + // Blocking resolve + connect for immediate retry + auto results = m_resolver.resolve(m_address.address(), m_address.port_str(), ec); + if (ec) { + LOG_WARNING << "CoindRPC sync_reconnect resolve failed: " << ec.message(); + return; + } + m_stream.connect(*results.begin(), ec); + if (ec) { + LOG_WARNING << "CoindRPC sync_reconnect connect failed: " << ec.message(); + return; + } + LOG_INFO << "CoindRPC reconnected (sync)"; +} + +std::string NodeRPC::Send(const std::string &request) +{ + // Retry once after synchronous reconnect on write/read failure + for (int attempt = 0; attempt < 2; ++attempt) + { + m_http_request.body() = request; + m_http_request.prepare_payload(); + try + { + http::write(m_stream, m_http_request); + } + catch(const std::exception& e) + { + LOG_WARNING << "CoindRPC write failed: " << e.what() + << (attempt == 0 ? " — reconnecting..." : ""); + if (attempt == 0) { + sync_reconnect(); + continue; + } + return {}; + } + + beast::flat_buffer buffer; + boost::beast::http::response response; + + try + { + boost::beast::http::read(m_stream, buffer, response); + } + catch (const std::exception& ex) + { + LOG_WARNING << "CoindRPC read failed: " << ex.what() + << (attempt == 0 ? " — reconnecting..." : ""); + if (attempt == 0) { + sync_reconnect(); + continue; + } + return {}; + } + + auto body = boost::beast::buffers_to_string(response.body().data()); + if (body.empty()) { + static int _empty_count = 0; + if (_empty_count++ < 5) + LOG_WARNING << "CoindRPC empty response: HTTP " << response.result_int() + << " content-length=" << response[http::field::content_length] + << " connection=" << response[http::field::connection]; + if (attempt == 0 && response.result_int() != 200) { + sync_reconnect(); + continue; + } + } + return body; + } + return {}; +} + +nlohmann::json NodeRPC::CallAPIMethod(const std::string& method, const jsonrpccxx::positional_parameter& params) +{ + return m_client.CallMethod(ID, method, params); +} + +bool NodeRPC::check() +{ + // BCH-chain sentinel: the Aug-2017 fork block at height 478559. A daemon on + // the BCH chain has this header; a BTC daemon has a DIFFERENT 478559 and + // will fail the lookup -- this is what distinguishes BCH from BTC here. + // (bitcoin/networks/bitcoincash.py RPC_CHECK fork-block hash.) + bool has_block = check_blockheader(uint256S("000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec")); + bool is_main_chain = getblockchaininfo()["chain"].get() == "main"; + nlohmann::json blockchaininfo; + + if (is_main_chain && !has_block) + { + LOG_ERROR << "Check failed! Make sure that you're connected to the right BCH node with --bitcoind-rpc-port, and that it has finished syncing!" << std::endl; + return false; + } + + try + { + auto networkinfo = getnetworkinfo(); + // BCH version floor 100000 per bitcoincash.py VERSION_CHECK + // (Bitcoin 0.11.2-era; BCHN reports a higher value). + bool version_check_result = (100000 <= networkinfo["version"].get()); + if (!version_check_result) + { + LOG_ERROR << "Coin daemon too old! Upgrade!"; + return false; + } + } catch (const jsonrpccxx::JsonRpcException& ex) + { + LOG_WARNING << "NodeRPC::check() exception: " << ex.what(); + return false; + } + + try + { + blockchaininfo = getblockchaininfo(); + } catch (const jsonrpccxx::JsonRpcException& ex) + { + return false; + } + + std::set softforks_supported; + + if (blockchaininfo.contains("softforks")) + bch::coin::collect_softfork_names(blockchaininfo["softforks"], softforks_supported); + if (blockchaininfo.contains("bip9_softforks")) + bch::coin::collect_softfork_names(blockchaininfo["bip9_softforks"], softforks_supported); + + // Fallback for daemons that don't populate getblockchaininfo softfork fields. + // BCH GBT takes NO "segwit" rule -- request with empty rules. + if (softforks_supported.empty()) + { + try + { + auto gbt = getblocktemplate({}); + if (gbt.contains("rules") && gbt["rules"].is_array()) + { + for (const auto& rule : gbt["rules"]) + { + if (!rule.is_string()) continue; + auto r = rule.get(); + if (!r.empty() && r[0] == '!') r.erase(r.begin()); + softforks_supported.insert(r); + } + } + } + catch (const std::exception&) + { + // Keep empty set; missing-fork check below is vacuous for BCH anyway. + } + } + + // bch::PoolConfig::SOFTFORKS_REQUIRED is EMPTY -- BCH activations are + // MTP-based, not GBT-signalled -- so this gate always passes. Kept for + // structural parity with the BTC port + future diagnostics. + std::vector missing; + for (const auto& req : bch::PoolConfig::SOFTFORKS_REQUIRED) + { + if (!softforks_supported.contains(req)) + missing.push_back(req); + } + + if (!missing.empty()) + { + std::string joined; + for (size_t i = 0; i < missing.size(); ++i) + { + if (i) joined += ", "; + joined += missing[i]; + } + LOG_ERROR << "Coin daemon missing required softfork features: " << joined; + LOG_ERROR << "Refusing to start to avoid mining invalid/non-consensus blocks."; + return false; + } + + return true; +} + +bool NodeRPC::check_blockheader(uint256 header) +{ + try + { + getblockheader(header); + return true; + } catch (const jsonrpccxx::JsonRpcException& ex) + { + return false; + } +} + +rpc::WorkData NodeRPC::getwork() +{ + auto start = core::timestamp(); + // BCH: NO "segwit" rule (SegWit rejected at the Aug-2017 fork). + auto work = getblocktemplate({}); + auto end = core::timestamp(); + + if (!m_coin->txidcache.is_started()) + m_coin->txidcache.start(); + + std::vector txhashes; + std::vector unpacked_transactions; + for (auto& packed_tx : work["transactions"]) + { + PackStream ps_tx; + + uint256 txid; + std::string x; + + if (packed_tx.contains("data")) + x = packed_tx["data"].get(); + else + x = packed_tx.get(); + + // BCH has NO witness, so there is no wtxid/txid distinction: hashing + // "data" directly yields the canonical txid. Prefer the GBT "txid" + // field when present (identical value, saves a hash). + if (packed_tx.is_object() && packed_tx.contains("txid")) { + txid.SetHex(packed_tx["txid"].get()); + txhashes.push_back(txid); + } else if (m_coin->txidcache.exist(x)) + { + txid = m_coin->txidcache.get(x); + txhashes.push_back(txid); + } else + { + ps_tx = PackStream(ParseHex(x)); + txid = Hash(ps_tx.get_span()); + m_coin->txidcache.add(x, txid); + txhashes.push_back(txid); + } + + bch::coin::MutableTransaction unpacked_tx; + if (m_coin->known_txs.contains(txid)) + { + unpacked_tx = bch::coin::MutableTransaction(m_coin->known_txs.at(txid)); + } else + { + if (ps_tx.empty()) + ps_tx = PackStream(ParseHex(x)); + // BCH legacy (witness-free) unserialization -- no TX_WITH_WITNESS flag. + UnserializeTransaction(unpacked_tx, ps_tx); + } + unpacked_transactions.push_back(bch::coin::Transaction(unpacked_tx)); + } + + if ((core::timestamp() - m_coin->txidcache.time()) > 1800) + { + std::map keepers; + for (int i = 0; i < txhashes.size(); i++) + { + auto x = work["transactions"].at(i); + std::string keep; + if (x.contains("data")) + keep = x["data"].get(); + else + keep = x.get(); + uint256 txid = txhashes[i]; + keepers[keep] = txid; + } + m_coin->txidcache.clear(); + m_coin->txidcache.add(keepers); + } + + if (!work.contains("height")) + { + uint256 previous_block_hash = work["previousblockhash"].get(); + work["height"] = getblock(previous_block_hash)["height"].get() + 1; + } + + return rpc::WorkData{work, unpacked_transactions, txhashes, end - start}; +} + +void NodeRPC::submit_block(BlockType& block, bool ignore_failure) +{ + // BCH: legacy block serialization -- no witness data, no MWEB tail. + PackStream packed_block = pack(block); + auto result = m_client.CallMethod(ID, "submitblock", {HexStr(packed_block.get_span())}); + bool success = result.is_null(); + + auto block_header = pack(block); // cast to header? + // We always expect the submit to succeed (non-null result means rejection). + auto success_expected = true; + + if ((!success && success_expected && !ignore_failure) || (success && !success_expected)) + LOG_ERROR << "Block submittal result: " << success << "(" << result.dump() << ") Expected: " << success_expected; +} + +bool NodeRPC::submit_block_hex(const std::string& block_hex, bool ignore_failure) +{ + auto result = m_client.CallMethod(ID, "submitblock", {block_hex}); + bool success = result.is_null(); + if (!success && !ignore_failure) + LOG_ERROR << "submit_block_hex result: " << result.dump(); + else if (success) + LOG_INFO << "submit_block_hex accepted"; + return success; +} + +// RPC Methods + +nlohmann::json NodeRPC::getblocktemplate(std::vector rules) +{ + nlohmann::json j = nlohmann::json::object({{"rules", rules}}); + return CallAPIMethod("getblocktemplate", {j}); +} + +nlohmann::json NodeRPC::getnetworkinfo() +{ + return CallAPIMethod("getnetworkinfo"); +} + +nlohmann::json NodeRPC::getblockchaininfo() +{ + return CallAPIMethod("getblockchaininfo"); +} + +nlohmann::json NodeRPC::getmininginfo() +{ + return CallAPIMethod("getmininginfo"); +} + +// verbose: true -- json result, false -- hex-encode result; +nlohmann::json NodeRPC::getblockheader(uint256 header, bool verbose) +{ + return CallAPIMethod("getblockheader", {header, verbose}); +} + +// verbosity: 0 for hex-encoded data, 1 for a json object, and 2 for json object with transaction data +nlohmann::json NodeRPC::getblock(uint256 blockhash, int verbosity) +{ + return CallAPIMethod("getblock", {blockhash, verbosity}); +} + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/rpc.hpp b/src/impl/bch/coin/rpc.hpp new file mode 100644 index 000000000..b56a91dd7 --- /dev/null +++ b/src/impl/bch/coin/rpc.hpp @@ -0,0 +1,110 @@ +#pragma once + +// --------------------------------------------------------------------------- +// bch::coin::NodeRPC -- external coin-RPC client for BCH. Declaration ported +// from src/impl/btc/coin/rpc.hpp; the bch::interfaces::Node aggregate and the +// bch::coin::{BlockType, rpc::WorkData} ports (M3 slices 1-3) supply its types. +// +// >>> SCOPE (M3 slice 4) <<< +// This lands the NodeRPC *declaration* only, so the concrete CoinNode seam +// (coin_node.{hpp,cpp}) can name NodeRPC* and call getwork()/submit_block_hex(). +// The network BODY (rpc.cpp) is the NEXT slice -- it carries the BCH-divergent +// bits that have no 1:1 BTC mirror and must be authored, not copied: +// - getblocktemplate rules: BCH has NO "segwit"/witness rule; the GBT rule +// set and version-bits differ from BTC Core. +// - softfork_check.hpp: BCH activations (ASERT DAA Nov-2020, CTOR Nov-2018, +// CashTokens+P2SH32 May-2023, ABLA CHIP-2023-01 May-2024) -- NOT BIP9. +// - block header / getblock(header) parsing per BCH consensus. +// - CashTokens-bearing outputs round-trip transparently (template carry). +// rpc.cpp also pulls impl/bch/config_pool.hpp + impl/bch/coin/softfork_check.hpp, +// neither of which is ported yet -- both are prerequisites of that next slice. +// +// Source-only; impl_bch stays unregistered in CMake (bch = skip-green). +// --------------------------------------------------------------------------- + +#include "block.hpp" +#include "rpc_data.hpp" +#include "node_interface.hpp" + +#include + +#include +#include + +#include +#include +#include +#include + +namespace io = boost::asio; +namespace beast = boost::beast; +namespace http = beast::http; + +namespace bch +{ + +namespace coin +{ + +struct RPCAuthData; +class NodeRPC : public jsonrpccxx::IClientConnector +{ + const std::string ID = "curltest"; + const jsonrpccxx::version RPC_VER = jsonrpccxx::version::v2; + + const bool IS_TESTNET; +private: + bch::interfaces::Node* m_coin; + + io::io_context* m_context; + beast::tcp_stream m_stream; + boost::asio::ip::tcp::resolver m_resolver; + http::request m_http_request; + + std::unique_ptr m_auth; + jsonrpccxx::JsonRpcClient m_client; + + // Reconnection state + NetService m_address; + std::string m_userpass; + bool m_connected = false; + std::unique_ptr m_reconnect_timer; + + std::string Send(const std::string &request) override; + nlohmann::json CallAPIMethod(const std::string& method, const jsonrpccxx::positional_parameter& params = {}); + +public: + NodeRPC(io::io_context* context, bch::interfaces::Node* coin, bool testnet); + ~NodeRPC(); + + void connect(NetService address, std::string userpass); + void reconnect(); + void sync_reconnect(); + bool check(); + bool check_blockheader(uint256 header); + rpc::WorkData getwork(); + void submit_block(BlockType& block, bool ignore_failure); + // Submit a pre-serialised block passed in as a hex string (avoids re-packing) + // Returns true if the daemon accepted the block (result is null), false otherwise. + bool submit_block_hex(const std::string& block_hex, bool ignore_failure); + + // RPC Methods + nlohmann::json getblocktemplate(std::vector rules); + nlohmann::json getnetworkinfo(); + nlohmann::json getblockchaininfo(); + nlohmann::json getmininginfo(); + // verbose: true -- json result, false -- hex-encode result; + nlohmann::json getblockheader(uint256 header, bool verbose = true); + // verbosity: 0 for hex-encoded data, 1 for a json object, and 2 for json object with transaction data + nlohmann::json getblock(uint256 blockhash, int verbosity = 1); +}; + +struct RPCAuthData +{ + std::string authorization; + std::string host; +}; + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/rpc_data.hpp b/src/impl/bch/coin/rpc_data.hpp new file mode 100644 index 000000000..8eda87548 --- /dev/null +++ b/src/impl/bch/coin/rpc_data.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include "transaction.hpp" + +#include + +// bch::coin getwork/GBT result -- ported from src/impl/btc/coin/rpc_data.hpp. +// Coin-agnostic slice (m_data/m_hashes/m_latency) is what crosses the +// core::coin::WorkView seam; the full WorkData (incl. m_txs) stays coin-side. +// +// BCH notes (vs the BTC source): +// - No MWEB: the LTC mweb extension field is N/A and omitted (BCH/btc neither). +// - m_txs holds bch::coin::Transaction (legacy, no-witness; see transaction.hpp). +// - GBT field mapping (version/previousblockhash/transactions/coinbasevalue/ +// time|curtime/coinbaseflags|coinbaseaux/bits/height/rules) is shared with +// BTC and lands when the template builder/rpc slice is ported (later in M3). + +namespace bch +{ + +namespace coin +{ + +namespace rpc +{ + +struct WorkData +{ + nlohmann::json m_data; + std::vector m_txs; + std::vector m_hashes; // transaction hashes + time_t m_latency; + + WorkData() {} + WorkData(nlohmann::json data, std::vector txs, std::vector txhashes, time_t latency) + : m_data(data), m_txs(txs), m_hashes(txhashes), m_latency(latency) + { + + } + + bool operator==(const WorkData& rhs) const { return m_data == rhs.m_data; } + bool operator!=(const WorkData& rhs) const { return !(*this == rhs); } +}; + +} // namespace rpc + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/softfork_check.hpp b/src/impl/bch/coin/softfork_check.hpp new file mode 100644 index 000000000..091abaf3b --- /dev/null +++ b/src/impl/bch/coin/softfork_check.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include + +// --------------------------------------------------------------------------- +// bch::coin::collect_softfork_names -- ported from src/impl/btc/coin/softfork_check.hpp. +// +// >>> BCH NOTE: the softfork gate is informational/vacuous on BCH <<< +// bch::PoolConfig::SOFTFORKS_REQUIRED is EMPTY (BCH activations are MTP-based, +// not BIP9/GBT-signalled), so the missing-fork check in rpc.cpp always passes. +// This collector is kept for parity + diagnostics: BCHN's getblockchaininfo +// still reports a "softforks" object, and harvesting its names is harmless. +// +// Handles the formats BCHN/bitcoind variants produce: +// - Array of objects: [{"id":"csv",...}, ...] +// - Array of strings: ["csv", ...] +// - Object with keys: {"csv":{...}, ...} (BCHN getblockchaininfo style) +// --------------------------------------------------------------------------- + +namespace bch::coin { + +inline void collect_softfork_names(const nlohmann::json& value, + std::set& out) +{ + if (value.is_array()) + { + for (const auto& item : value) + { + if (item.is_object() && item.contains("id") && item["id"].is_string()) + out.insert(item["id"].get()); + else if (item.is_string()) + out.insert(item.get()); + } + } + else if (value.is_object()) + { + for (auto it = value.begin(); it != value.end(); ++it) + out.insert(it.key()); + } +} + +} // namespace bch::coin diff --git a/src/impl/bch/coin/template_builder.hpp b/src/impl/bch/coin/template_builder.hpp index fad8b75f6..1f61e5f71 100644 --- a/src/impl/bch/coin/template_builder.hpp +++ b/src/impl/bch/coin/template_builder.hpp @@ -1,13 +1,63 @@ #pragma once // BCH block-template builder. Mirrors src/impl/btc/coin/template_builder.hpp. -// Must emit p2pool-merged-v36-compatible templates (V36 master compat). // -// >>> CTOR INSERTION POINT (M1 §4.3) <<< +// >>> CTOR INSERTION POINT (M1 4.3) <<< // BCH requires CTOR: canonical (lexicographic txid) ordering of mempool txs // in the block body, coinbase first. Net-new c2pool code (second BCH slice). -// TODO(M3): CTOR re-sort pass over selected txs before template assembly. +// TODO(M3/M4): CTOR re-sort pass over selected txs before template assembly. // // CashTokens (May 2023) awareness: token-bearing outputs must round-trip -// intact through template assembly. TODO(M3): preserve token prefix bytes. +// intact through template assembly. TODO(M3/M4): preserve token prefix bytes. // HogEx: NOT applicable (see config_coin.hpp scope note). -namespace c2pool::bch { /* TODO(M3): TemplateBuilder w/ CTOR re-sort */ } +// +// M3 slice 3: CoinNodeInterface (the abstract work-source/submit seam) is +// ported here -- mirroring the BTC source, which co-locates it with the +// builder. It is independent of TemplateBuilder (depends only on rpc::WorkData +// + BlockType) and is the base inherited by bch::coin::CoinNode. The concrete +// TemplateBuilder body (merkle helpers + GBT assembly + CTOR re-sort) remains +// the M4 deliverable below. + +#include "block.hpp" +#include "rpc_data.hpp" + +#include + +namespace bch +{ + +namespace coin +{ + +// CoinNodeInterface -- ported from src/impl/btc/coin/template_builder.hpp. +// +// Abstract interface for obtaining work and submitting blocks. Allows swapping +// between RPC (legacy), embedded, or hybrid implementations without changing +// downstream code (share creation, Stratum, etc.). BCH carries the same shape +// as BTC: getwork() returns the coin-agnostic WorkData, submit_block() takes a +// BlockType (no MWEB extension on BCH). +class CoinNodeInterface { +public: + virtual ~CoinNodeInterface() = default; + + /// Return a block template as WorkData. + /// Throws std::runtime_error if no template can be produced. + virtual rpc::WorkData getwork() = 0; + + /// Submit a found block. + virtual void submit_block(BlockType& block) = 0; + + /// Return chain info (analogous to getblockchaininfo RPC). + virtual nlohmann::json getblockchaininfo() = 0; + + /// True when the embedded chain is up to date with the network + /// AND has enough UTXO depth for coinbase maturity validation. + virtual bool is_synced() const { return false; } +}; + +// TODO(M4): class TemplateBuilder -- GBT assembly from HeaderChain + Mempool +// with CTOR re-sort and CashTokens-transparent tx carry. (was: namespace +// c2pool::bch TemplateBuilder stub) + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/transaction.cpp b/src/impl/bch/coin/transaction.cpp new file mode 100644 index 000000000..4c19a9bfb --- /dev/null +++ b/src/impl/bch/coin/transaction.cpp @@ -0,0 +1,24 @@ +#include "transaction.hpp" + +// bch::coin transaction ctors -- ported from src/impl/btc/coin/transaction.cpp. +// BCH has no SegWit, so there is no ComputeHasWitness()/m_has_witness (M1 §4.1). + +namespace bch +{ + +namespace coin +{ + +Transaction::Transaction(const MutableTransaction& tx) + : vin(tx.vin), vout(tx.vout), version(tx.version), locktime(tx.locktime) {} +Transaction::Transaction(MutableTransaction&& tx) + : vin(std::move(tx.vin)), vout(std::move(tx.vout)), version(tx.version), locktime(tx.locktime) {} + +MutableTransaction::MutableTransaction() + : version(Transaction::CURRENT_VERSION), locktime(0) {} +MutableTransaction::MutableTransaction(const Transaction& tx) + : vin(tx.vin), vout(tx.vout), version(tx.version), locktime(tx.locktime) {} + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/transaction.hpp b/src/impl/bch/coin/transaction.hpp new file mode 100644 index 000000000..71deb38f8 --- /dev/null +++ b/src/impl/bch/coin/transaction.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include +#include +#include + +// --------------------------------------------------------------------------- +// bch::coin transaction -- ported from src/impl/btc/coin/transaction.hpp. +// +// >>> BCH DIVERGENCE (M1 §4.1): NO SegWit <<< +// Bitcoin Cash rejected SegWit at the Aug 2017 fork. BCH transactions use the +// LEGACY serialization only: +// int32_t version | vector vin | vector vout | uint32_t locktime +// There is no marker/flag byte, no witness stack, and no extended format. The +// BTC source carried TxParams{allow_witness}, TxIn::scriptWitness, the dummy/ +// flags branch, and HasWitness() -- ALL removed here. BCH txids are therefore +// computed over this single canonical serialization (no wtxid distinction). +// +// CashTokens (CHIP-2022-02, May 2023) wrap token data as a prefix inside the +// TxOut scriptPubKey region; that round-trips transparently through OPScript +// and needs no struct change here. Token-aware template handling is a later +// slice (coin/template_builder.hpp CTOR/token insertion point). +// --------------------------------------------------------------------------- + +namespace bch +{ + +namespace coin +{ + +struct MutableTransaction; + +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; + // NOTE: no scriptWitness -- BCH has no SegWit (see file banner). + + 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; + + /** 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); + } +}; + +/** 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); + } + + template + inline void Unserialize(StreamType& os) + { + UnserializeTransaction(*this, os); + } +}; + +/** + * BCH (legacy) transaction serialization format -- no SegWit: + * - int32_t version + * - std::vector vin + * - std::vector vout + * - uint32_t locktime + */ +template +void UnserializeTransaction(TxType& tx, StreamType& s) +{ + s >> tx.version; + s >> tx.vin; + s >> tx.vout; + s >> tx.locktime; +} + +template +void SerializeTransaction(const TxType& tx, StreamType& s) +{ + s << tx.version; + s << tx.vin; + s << tx.vout; + s << tx.locktime; +} + +} // namespace coin + +} // namespace bch diff --git a/src/impl/bch/coin/txidcache.hpp b/src/impl/bch/coin/txidcache.hpp new file mode 100644 index 000000000..43b29961a --- /dev/null +++ b/src/impl/bch/coin/txidcache.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +#include +#include + +// bch::coin TXIDCache -- ported verbatim from src/impl/btc/coin/txidcache.hpp. +// Coin-agnostic: maps getblocktemplate transaction.data -> hash256(packed data). +// No BCH divergence (CashTokens data rides inside the packed tx and hashes +// transparently; CTOR ordering does not affect the cache keying). + +namespace bch +{ + +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 bch diff --git a/src/impl/bch/config.hpp b/src/impl/bch/config.hpp new file mode 100644 index 000000000..30b5607e2 --- /dev/null +++ b/src/impl/bch/config.hpp @@ -0,0 +1,16 @@ +#pragma once +// bch::Config -- BCH coin+pool config aggregator. Ported from +// src/impl/btc/config.hpp (M3 slice 14). Binds the BCH PoolConfig (sharechain +// params, slice 5) and CoinConfig (embedded-daemon P2P/RPC params, slice 14) +// into the shared core::Config carrier -- structurally identical to the btc +// reference; all BCH divergence lives inside the two component configs. + +#include "config_pool.hpp" +#include "config_coin.hpp" + +namespace bch +{ + +using Config = core::Config; + +} // namespace bch diff --git a/src/impl/bch/config_coin.cpp b/src/impl/bch/config_coin.cpp new file mode 100644 index 000000000..eeb992bea --- /dev/null +++ b/src/impl/bch/config_coin.cpp @@ -0,0 +1,39 @@ +#include "config_coin.hpp" + +#include + +// bch::CoinConfig load/default -- ported from src/impl/btc/config_coin.cpp +// (M3 slice 15). Coin-agnostic in shape; the BCH-specific parameter values are +// supplied by the on-disk YAML and the bch::config types in config_coin.hpp. + +namespace bch +{ + +std::ofstream& CoinConfig::get_default(std::ofstream& file) +{ + YAML::Node out; + + out["symbol"] = "defaultNet"; + out["p2p"] = config::P2PData(); + out["rpc"] = config::RPCData(); + out["share_period"] = 0; + out["testnet"] = false; + + file << out; + return file; +} + +void CoinConfig::load() +{ + YAML::Node node = YAML::LoadFile(m_filepath.string()); + + PARSE_CONFIG(node, symbol, std::string); + + m_p2p = node["p2p"].as(); + m_rpc = node["rpc"].as(); + + PARSE_CONFIG(node, share_period, int); + PARSE_CONFIG(node, testnet, bool); +} + +} // namespace bch diff --git a/src/impl/bch/config_coin.hpp b/src/impl/bch/config_coin.hpp index 972aeabfa..0c0446420 100644 --- a/src/impl/bch/config_coin.hpp +++ b/src/impl/bch/config_coin.hpp @@ -1,23 +1,109 @@ #pragma once -// c2pool-bch :: BCH coin parameters (V36) -// Skeleton per c2pool-bch-embedded-impl-plan.md (frstrtr/the docs/v36) §3. -// Embedded daemon forked from Bitcoin Cash Node (BCHN). SHA256d family. +// bch::CoinConfig -- BCH coin parameters (V36). Ported from +// src/impl/btc/config_coin.hpp (M3 slice 14). Namespace bch (was the M2 +// skeleton's c2pool::bch CoinParams -- reconciled to bch::CoinConfig here so +// config.hpp's core::Config resolves; see the M3 +// slice-1 namespace note). // -// SCOPE NOTE (M1 §4.2): HogEx is a SmartBCH sidechain construct, NOT BCH -// mainchain. It is explicitly OUT OF SCOPE for this template/coin module. -// Do not add HogEx commitment handling here. See feedback: hogex-not-bch. +// SCOPE NOTE (M1 4.2): HogEx is a SmartBCH sidechain construct, NOT BCH +// mainchain. It is explicitly OUT OF SCOPE for this coin module. Do not add +// HogEx commitment handling here. See feedback: hogex-not-bch. // // CashAddr scaffolding: BCH address encoding diverges from BTC base58/bech32. -// TODO(M3): port CashAddr (prefix "bitcoincash:") encode/decode at vendoring. +// TODO(M4): port CashAddr (prefix "bitcoincash:") encode/decode at the +// template/address layer -- the wire/config shape below is coin-agnostic and +// matches the btc reference 1:1 (P2P prefix + NetService + RPC userpass). -namespace c2pool::bch { +#include +#include +#include -struct CoinParams { - // TODO(M3): fill from BCHN chainparams at vendoring (confirm commit/tag). - static constexpr const char* ticker = "BCH"; - static constexpr const char* cashaddr_hrp = "bitcoincash"; - // ASERT DAA (May 2020) anchor — see coin/header_chain.hpp insertion point. - // CTOR (Nov 2018) canonical tx ordering — see coin/template_builder.hpp. +#include + +namespace bch +{ +namespace config +{ + struct P2PData + { + std::vector prefix; + NetService address; + }; + + struct RPCData + { + NetService address; + std::string userpass; + }; + +} // config +} // namespace bch + +namespace YAML +{ +template<> struct convert +{ + static Node encode(const bch::config::P2PData& rhs) + { + Node node; + node["prefix"] = HexStr(rhs.prefix); + node["address"] = rhs.address; + return node; + } + + static bool decode(const Node& node, bch::config::P2PData& rhs) + { + // prefix + rhs.prefix = ParseHexBytes(node["prefix"].as()); + // address + rhs.address = node["address"].as(); + return true; + } +}; + +template<> struct convert +{ + static Node encode(const bch::config::RPCData& rhs) + { + Node node; + node["address"] = rhs.address; + node["userpass"] = rhs.userpass; + return node; + } + + static bool decode(const Node& node, bch::config::RPCData& rhs) + { + rhs.address = node["address"].as(); + rhs.userpass = node["userpass"].as(); + return true; + } +}; +} + +namespace bch +{ + +class CoinConfig : protected core::Fileconfig +{ + +protected: + std::ofstream& get_default(std::ofstream& file) override; + void load() override; + +public: + CoinConfig(const std::filesystem::path& path) : core::Fileconfig(path) + { + + } + +public: + + config::P2PData m_p2p; + config::RPCData m_rpc; + + std::string m_symbol; + int m_share_period{}; + bool m_testnet {false}; }; -} // namespace c2pool::bch +} // namespace bch diff --git a/src/impl/bch/config_pool.cpp b/src/impl/bch/config_pool.cpp new file mode 100644 index 000000000..02b4edb32 --- /dev/null +++ b/src/impl/bch/config_pool.cpp @@ -0,0 +1,48 @@ +#include "config_pool.hpp" + +#include +#include + +// bch::PoolConfig load/default -- ported from src/impl/btc/config_pool.cpp. +// Coin-agnostic in shape; the BCH-specific values live in config_pool.hpp. + +namespace bch +{ + +std::ofstream& PoolConfig::get_default(std::ofstream& file) +{ + YAML::Node out; + + out["prefix"] = m_prefix.empty() ? prefix_hex() : HexStr(m_prefix); + out["worker"] = m_worker; + + YAML::Node addrs_node; + for (const auto& host : DEFAULT_BOOTSTRAP_HOSTS) + addrs_node.push_back(host + ":" + std::to_string(P2P_PORT)); + out["bootstrap_addrs"] = addrs_node; + + file << out; + return file; +} + +void PoolConfig::load() +{ + YAML::Node node = YAML::LoadFile(m_filepath.string()); + + m_prefix = ParseHexBytes(node["prefix"].as()); + + PARSE_CONFIG(node, worker, std::string); + + if (node["bootstrap_addrs"] && node["bootstrap_addrs"].IsSequence()) + { + for (const auto& item : node["bootstrap_addrs"]) + m_bootstrap_addrs.emplace_back(item.as()); + } + else + { + for (const auto& host : DEFAULT_BOOTSTRAP_HOSTS) + m_bootstrap_addrs.emplace_back(host + ":" + std::to_string(P2P_PORT)); + } +} + +} // namespace bch diff --git a/src/impl/bch/config_pool.hpp b/src/impl/bch/config_pool.hpp new file mode 100644 index 000000000..be493a031 --- /dev/null +++ b/src/impl/bch/config_pool.hpp @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// --------------------------------------------------------------------------- +// bch::PoolConfig -- BCH p2pool sharechain config. Ported from +// src/impl/btc/config_pool.hpp; all network constants resauthored from the +// p2pool-merged-v36 BCH reference (authoritative for V36 master-compat): +// p2pool/networks/bitcoincash.py + p2pool/bitcoin/networks/bitcoincash.py +// +// >>> BCH DIVERGENCES FROM BTC (M1 4.x) <<< +// - NO SegWit: there is NO SEGWIT_ACTIVATION_VERSION and SOFTFORKS_REQUIRED +// is EMPTY. BCH consensus changes (CTOR Nov-2018, ASERT DAA Nov-2020, +// CashTokens+P2SH32 May-2023, ABLA CHIP-2023-01 May-2024) activate by MTP, +// NOT via BIP9 versionbits / getblocktemplate "rules" -- so none are +// GBT-signalled and none belong in the softfork-required gate. +// - Block size: BCH carries 32 MB (BLOCK_MAX_SIZE) -- ABLA raises this over +// time, but the p2pool template-size bookkeeping floor stays at the +// reference value. "WEIGHT" is a p2pool bookkeeping field (4x size); BCH has +// no witness weight. +// - SHARE_PERIOD 60s (BTC 30s, LTC 15s); CHAIN_LENGTH 3*24*60 shares (3 days). +// - Donation: VERSION-GATED (operator ruling 2026-06-16, V36 master-compat). +// share_version < 36 -> BCH-native forrestv P2PK (static), byte-for-byte +// from p2poolBCH @6603b79 (memory donation-verification-closed). +// share_version >= 36 -> COMBINED_DONATION_SCRIPT (1-of-2 P2MS->P2SH +// transition + AutoRatchet 95%/50% + tail-guard), byte-identical to the +// BTC/LTC merged-path combined script -- merged-path is always COMBINED for +// cross-coin V36 parity (project_v36_donation_transition_mechanism). +// --------------------------------------------------------------------------- + +namespace bch +{ + +class PoolConfig : protected core::Fileconfig +{ +protected: + std::ofstream& get_default(std::ofstream& file) override; + void load() override; + +public: + PoolConfig(const std::filesystem::path& path) : core::Fileconfig(path) + { + + } + + // ----------------------------------------------------------------------- + // BCH p2pool network constants -- p2pool-merged-v36 bitcoincash.py. + // Must match the live BCH p2pool sharechain (P2P_PORT 9349). BCH is a + // STANDALONE parent in V36 (not merged-mined), but the share format is V36. + // ----------------------------------------------------------------------- + static constexpr uint16_t P2P_PORT = 9349; // bitcoincash.py P2P_PORT + static constexpr uint32_t SPREAD = 3; // blocks (PPLNS window) + static constexpr uint32_t TARGET_LOOKBEHIND = 200; + // 3301 floor matches p2pool-merged-v36 bitcoincash.py MINIMUM_PROTOCOL_VERSION. + static constexpr uint32_t MINIMUM_PROTOCOL_VERSION = 3301; + // Our capability: V36 shares (matches LTC/DOGE advertised version). + static constexpr uint32_t ADVERTISED_PROTOCOL_VERSION = 3600; + // NOTE: NO SEGWIT_ACTIVATION_VERSION on BCH -- SegWit was rejected at the + // Aug-2017 fork. (BTC carries one; intentionally absent here.) + static constexpr uint32_t BLOCK_MAX_SIZE = 32000000; // 32 MB (BCH) + static constexpr uint32_t BLOCK_MAX_WEIGHT = 128000000; // 4x size bookkeeping + + // Mainnet constants -- bitcoincash.py + static constexpr uint32_t SHARE_PERIOD = 60; // seconds (one minute) + static constexpr uint32_t CHAIN_LENGTH = 4320; // 3*24*60 shares (3 days) + static constexpr uint32_t REAL_CHAIN_LENGTH = 4320; + + // DUST_THRESHOLD: minimum payout per share output. + // BCH: PARENT.DUST_THRESHOLD = 0.001e8 = 100000 sat (bitcoin/networks/bitcoincash.py). + static constexpr uint64_t DUST_THRESHOLD = 100000; // satoshis (BCH mainnet) + static constexpr uint64_t TESTNET_DUST_THRESHOLD = 100000; // mirrors mainnet pending parent confirm + static uint64_t dust_threshold() { return is_testnet ? TESTNET_DUST_THRESHOLD : DUST_THRESHOLD; } + + // Testnet constants -- bitcoincash_testnet.py (same cadence as mainnet). + static constexpr uint32_t TESTNET_SHARE_PERIOD = 60; + static constexpr uint32_t TESTNET_CHAIN_LENGTH = 4320; + static constexpr uint32_t TESTNET_REAL_CHAIN_LENGTH = 4320; + + // Runtime testnet flag -- set once at startup + static inline bool is_testnet = false; + + // Accessors that return correct value for current network + static uint32_t share_period() { return is_testnet ? TESTNET_SHARE_PERIOD : SHARE_PERIOD; } + static uint32_t chain_length() { return is_testnet ? TESTNET_CHAIN_LENGTH : CHAIN_LENGTH; } + static uint32_t real_chain_length() { return is_testnet ? TESTNET_REAL_CHAIN_LENGTH : REAL_CHAIN_LENGTH; } + + // MAX_TARGET: share difficulty floor. BCH = 2**256//2**32 - 1 (bdiff 1), + // identical to BTC (bitcoincash.py MAX_TARGET). + static uint256 max_target() + { + static const uint256 MAINNET_MAX = [] { + uint256 t; + t.SetHex("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + return t; + }(); + static const uint256 TESTNET_MAX = [] { + uint256 t; + t.SetHex("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + return t; + }(); + return is_testnet ? TESTNET_MAX : MAINNET_MAX; + } + + // ----------------------------------------------------------------------- + // Consensus-critical donation scripts -- VERSION-GATED per operator ruling + // 2026-06-16 (V36 master-compat with p2pool-merged-v36). + // + // Pre-V36 (share_version < 36): BCH-native forrestv P2PK (static). Bytes are + // forrestv's uncompressed pubkey, bit-identical to the BTC/LTC pre-V36 P2PK, + // verified first-hand vs p2poolBCH @6603b79 (memory donation-verification-closed). + // ----------------------------------------------------------------------- + static constexpr std::array DONATION_SCRIPT = { + 0x41, // OP_PUSHBYTES_65 + 0x04, 0xff, 0xd0, 0x3d, 0xe4, 0x4a, 0x6e, 0x11, + 0xb9, 0x91, 0x7f, 0x3a, 0x29, 0xf9, 0x44, 0x32, + 0x83, 0xd9, 0x87, 0x1c, 0x9d, 0x74, 0x3e, 0xf3, + 0x0d, 0x5e, 0xdd, 0xcd, 0x37, 0x09, 0x4b, 0x64, + 0xd1, 0xb3, 0xd8, 0x09, 0x04, 0x96, 0xb5, 0x32, + 0x56, 0x78, 0x6b, 0xf5, 0xc8, 0x29, 0x32, 0xec, + 0x23, 0xc3, 0xb7, 0x4d, 0x9f, 0x05, 0xa6, 0xf9, + 0x5a, 0x8b, 0x55, 0x29, 0x35, 0x26, 0x56, 0x66, + 0x4b, + 0xac // OP_CHECKSIG + }; + + // V36+ (share_version >= 36): COMBINED_DONATION_SCRIPT -- P2SH wrapping the + // 1-of-2 P2MS (forrestv + maintainer) transition redeem; AutoRatchet 95%/50% + // + tail-guard semantics are carried in the redeem script. Byte-identical to + // the BTC/LTC merged-path combined script (coin-independent hash160) -- + // merged-path is always COMBINED for cross-coin V36 parity. + static constexpr std::array COMBINED_DONATION_SCRIPT = { + 0xa9, // OP_HASH160 + 0x14, // PUSH 20 bytes + 0x8c, 0x62, 0x72, 0x62, 0x1d, 0x89, 0xe8, 0xfa, + 0x52, 0x6d, 0xd8, 0x6a, 0xcf, 0xf6, 0x0c, 0x71, + 0x36, 0xbe, 0x8e, 0x85, + 0x87 // OP_EQUAL + }; + + // Returns the correct donation script for a share version (operator ruling + // 2026-06-16). Pre-V36 shares use the BCH-native forrestv P2PK; V36+ shares + // use the combined P2SH 1-of-2 multisig script. Same shape as BTC/LTC. + static std::vector get_donation_script(int64_t share_version) + { + if (share_version >= 36) + return {COMBINED_DONATION_SCRIPT.begin(), COMBINED_DONATION_SCRIPT.end()}; + return {DONATION_SCRIPT.begin(), DONATION_SCRIPT.end()}; + } + + // Message framing -- bitcoincash.py: + // IDENTIFIER = 'b826c0a51ddc2d2b' PREFIX = 'ac9a8fda9a911bce' + static inline const std::string DEFAULT_PREFIX_HEX = "ac9a8fda9a911bce"; + static inline const std::string DEFAULT_IDENTIFIER_HEX = "b826c0a51ddc2d2b"; + // bitcoincash_testnet.py: + // IDENTIFIER = 'c9f3de8d9508faef' PREFIX = '08c5541df85a8a65' + static inline const std::string TESTNET_PREFIX_HEX = "08c5541df85a8a65"; + static inline const std::string TESTNET_IDENTIFIER_HEX = "c9f3de8d9508faef"; + + // Private chain overrides -- set once at startup via --network-id + static inline std::string override_identifier_hex; + static inline std::string override_prefix_hex; + + /// Set private network identity. IDENTIFIER is the consensus secret + /// (hashed into ref_hash). PREFIX is derived from it for transport framing. + static void set_network_id(const std::string& network_id_hex) { + if (network_id_hex.empty() || network_id_hex == "0" || network_id_hex == "00000000") + return; // public network, use defaults + + std::string padded = network_id_hex; + while (padded.size() < 16) padded = "0" + padded; + if (padded.size() > 16) padded = padded.substr(0, 16); + + override_identifier_hex = padded; + + auto id_bytes = ParseHex(padded); + static const char* HEX = "0123456789abcdef"; + override_prefix_hex.clear(); + override_prefix_hex.reserve(16); + for (size_t i = 0; i < 8 && i < id_bytes.size(); ++i) { + uint8_t b = id_bytes[i] ^ id_bytes[(i + 3) % id_bytes.size()] ^ 0x5A; + override_prefix_hex += HEX[b >> 4]; + override_prefix_hex += HEX[b & 0x0f]; + } + } + + static const std::string& identifier_hex() { + if (!override_identifier_hex.empty()) + return override_identifier_hex; + return is_testnet ? TESTNET_IDENTIFIER_HEX : DEFAULT_IDENTIFIER_HEX; + } + + static const std::string& prefix_hex() { + if (!override_prefix_hex.empty()) + return override_prefix_hex; + return is_testnet ? TESTNET_PREFIX_HEX : DEFAULT_PREFIX_HEX; + } + + /// Chain fingerprint: SHA256d(PREFIX || IDENTIFIER)[0:8] + static uint64_t chain_fingerprint_u64() { + if (override_identifier_hex.empty()) + return 0; // public network + + auto pfx_bytes = ParseHex(override_prefix_hex); + auto id_bytes = ParseHex(override_identifier_hex); + std::vector preimage; + preimage.reserve(pfx_bytes.size() + id_bytes.size()); + preimage.insert(preimage.end(), pfx_bytes.begin(), pfx_bytes.end()); + preimage.insert(preimage.end(), id_bytes.begin(), id_bytes.end()); + + unsigned char hash1[32], hash2[32]; + CSHA256().Write(preimage.data(), preimage.size()).Finalize(hash1); + CSHA256().Write(hash1, 32).Finalize(hash2); + + uint64_t fp = 0; + for (int i = 0; i < 8; ++i) + fp |= uint64_t(hash2[i]) << (8 * i); + return fp; + } + + // BCH softfork-required set is EMPTY (bitcoincash.py SOFTFORKS_REQUIRED = set()). + // BCH consensus rules are MTP-activated, NOT BIP9/GBT-signalled, so there is + // nothing for the getblockchaininfo/GBT-rules softfork gate to enforce. The + // gate in rpc.cpp consequently always passes for BCH -- intentional. + static inline const std::set SOFTFORKS_REQUIRED = {}; + + // Default bootstrap peers -- bitcoincash.py BOOTSTRAP_ADDRS. + static inline const std::vector DEFAULT_BOOTSTRAP_HOSTS = { + "ml.toom.im", + "bch.p2pool.leblancnet.us", + "siberia.mine.nu", + "5.8.79.155", + "18.209.181.17", + "95.79.35.133", + "193.29.58.47", + }; + + // ----------------------------------------------------------------------- + // Runtime config loaded from pool.yaml + // ----------------------------------------------------------------------- + std::vector m_prefix; + std::string m_worker; + std::vector m_bootstrap_addrs; +}; + +} // namespace bch diff --git a/src/impl/bch/donation_consensus.hpp b/src/impl/bch/donation_consensus.hpp new file mode 100644 index 000000000..2e3a47c60 --- /dev/null +++ b/src/impl/bch/donation_consensus.hpp @@ -0,0 +1,120 @@ +#pragma once +// +// donation_consensus.hpp -- Consensus-level donation script validation for BCH +// p2pool shares. +// +// Validates that coinbase transactions include the correct donation output as +// required by the p2pool protocol. The donation script receives the rounding +// remainder from PPLNS payout distribution. +// +// Donation script is VERSION-GATED (operator ruling 2026-06-16, V36 master-compat +// with frstrtr/p2pool-merged-v36): +// share_version < 36 -> BCH-native forrestv P2PK (static, p2poolBCH @6603b79) +// share_version >= 36 -> COMBINED_DONATION_SCRIPT (1-of-2 P2MS->P2SH transition +// + AutoRatchet 95%/50% + tail-guard; merged-path is +// always COMBINED for cross-coin V36 parity) +// Selection is owned by bch::PoolConfig::get_donation_script(share_version); this +// module consumes it so the share validator and the template builder agree. +// +// Mirrors src/impl/ltc/donation_consensus.hpp. NOTE: build_expected_payouts() +// (the ShareTracker entry point in the LTC reference) is deferred to the BCH +// PPLNS/share_tracker port slice -- share_tracker is not yet present on +// bch/m3-coin-node. The two validators below are self-contained. +// +// Reference: frstrtr/p2pool-merged-v36 p2pool/data.py lines 114-200 +// + +#include "config_pool.hpp" + +#include +#include +#include +#include + +namespace bch::consensus +{ + +// A single coinbase output (scriptPubKey + value). +struct CoinbaseOutput +{ + std::vector script; + uint64_t value{0}; +}; + +// Result of donation validation. +struct DonationValidationResult +{ + bool valid{false}; + std::string error; +}; + +// Validate that a coinbase transaction contains the correct donation output. +// +// Rules (from p2pool consensus): +// 1. The (version-gated) donation script MUST appear as a coinbase output. +// 2. The donation amount >= expected_donation (from the PPLNS remainder). +// 3. Pre-V36 shares use the P2PK script; V36+ shares use the combined P2SH. +// +// Parameters: +// coinbase_outputs: all outputs from the coinbase transaction +// expected_payouts: result of ShareTracker::get_expected_payouts() +// share_version: the share version number (selects the donation script) +inline DonationValidationResult validate_donation_output( + const std::vector& coinbase_outputs, + const std::map, double>& expected_payouts, + int64_t share_version) +{ + auto donation_script = PoolConfig::get_donation_script(share_version); + + // Look up the expected donation amount. + auto it = expected_payouts.find(donation_script); + if (it == expected_payouts.end()) + return {true, {}}; // No donation expected (e.g. all miners set donation=0). + + double expected_amount = it->second; + if (expected_amount <= 0.0) + return {true, {}}; + + // Find the donation output(s) in the coinbase. + uint64_t actual_donation = 0; + bool found = false; + for (const auto& out : coinbase_outputs) + { + if (out.script == donation_script) + { + actual_donation += out.value; + found = true; + } + } + + if (!found) + return {false, "coinbase missing donation output for script"}; + + // Allow 1-satoshi tolerance for integer rounding. + if (actual_donation + 1 < static_cast(expected_amount)) + return {false, "donation output value too low"}; + + return {true, {}}; +} + +// Verify that the sum of all coinbase outputs does not exceed the subsidy. +// The donation output is included in this sum. +inline DonationValidationResult validate_coinbase_total( + const std::vector& coinbase_outputs, + uint64_t subsidy) +{ + uint64_t total = 0; + for (const auto& out : coinbase_outputs) + { + if (total + out.value < total) // overflow check + return {false, "coinbase output sum overflow"}; + total += out.value; + } + + if (total > subsidy) + return {false, "coinbase outputs exceed block subsidy"}; + + return {true, {}}; +} + +} // namespace bch::consensus diff --git a/src/impl/bch/share.hpp b/src/impl/bch/share.hpp new file mode 100644 index 000000000..c1fac299b --- /dev/null +++ b/src/impl/bch/share.hpp @@ -0,0 +1,316 @@ +#pragma once + +#include "coin/block.hpp" +#include "share_types.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace bch +{ + +// min_header [small_block_header_type] +// +// share_info_type: +// share_data: +// prev_hash +// coinbase +// nonce +// address[>=34] or pubkey_hash[<34] +// subsidy +// donation +// stale_info +// desired_version +// +// segwit_data [if segwit_activated] +// +// if version < 34: +// new_transaction_hashes +// transaction_hash_refs +// +// far_share_hash +// max_bits +// bits +// timestamp +// absheight +// abswork +// +// ref_merkle_link +// last_txout_nonce +// hash_link +// merkle_link + +template +struct BaseShare : chain::BaseShare +{ + coin::SmallBlockHeaderType m_min_header; + // prev_hash + BaseScript m_coinbase; // coinbase + uint32_t m_nonce; // nonce + // [x]address[>=34] or [x]pubkey_hash[<34] + uint64_t m_subsidy; // subsidy + uint16_t m_donation; // donation + bch::StaleInfo m_stale_info; // stale_info + uint64_t m_desired_version; // desired_version + // + // [x] segwit_data [if segwit_activated] + // + uint256 m_far_share_hash; // far_share_hash + uint32_t m_max_bits; // max_bits; bitcoin_data.FloatingIntegerType() + uint32_t m_bits; // bits; bitcoin_data.FloatingIntegerType() + uint32_t m_timestamp; // timestamp + uint32_t m_absheight; // absheight + uint128 m_abswork; // abswork + + // ref_merkle_link + MerkleLink m_ref_merkle_link; + // last_txout_nonce + uint64_t m_last_txout_nonce; + // hash_link + HashLinkType m_hash_link; + // merkle_link + MerkleLink m_merkle_link; + + NetService peer_addr; // WHERE? + + BaseShare() {} + BaseShare(const uint256& hash, const uint256& prev_hash) : chain::BaseShare(hash, prev_hash) {} + +}; + +struct Share : BaseShare<17> +{ + uint160 m_pubkey_hash; + std::optional m_segwit_data; + bch::ShareTxInfo m_tx_info; // new_transaction_hashes; transaction_hash_refs + + Share() {} + Share(const uint256& hash, const uint256& prev_hash) : BaseShare<17>(hash, prev_hash) {} + +}; + +struct NewShare : BaseShare<33> +{ + uint160 m_pubkey_hash; + std::optional m_segwit_data; + bch::ShareTxInfo m_tx_info; // new_transaction_hashes; transaction_hash_refs + + NewShare() {} + NewShare(const uint256& hash, const uint256& prev_hash) : BaseShare<33>(hash, prev_hash) {} +}; + +namespace types +{ + +struct DataSegwitShare +{ + BaseScript m_address; // Todo (check): VarStrType + std::optional m_segwit_data; +}; + +} // namespace types + +struct SegwitMiningShare : BaseShare<34>, types::DataSegwitShare +{ + SegwitMiningShare() {} + SegwitMiningShare(const uint256& hash, const uint256& prev_hash) : BaseShare<34>(hash, prev_hash) {} +}; + +struct PaddingBugfixShare : BaseShare<35>, types::DataSegwitShare +{ + PaddingBugfixShare() {} + PaddingBugfixShare(const uint256& hash, const uint256& prev_hash) : BaseShare<35>(hash, prev_hash) {} +}; + +struct MergedMiningShare : BaseShare<36> +{ + uint160 m_pubkey_hash; + uint8_t m_pubkey_type{0}; // 0=P2PKH, 1=P2WPKH, 2=P2SH + std::optional m_segwit_data; + std::vector m_merged_addresses; // empty = none + std::vector m_merged_coinbase_info; // empty = none + uint256 m_merged_payout_hash; // zero = none + V36HashLinkType m_hash_link; // shadows BaseShare::m_hash_link + BaseScript m_message_data; // empty = none + + MergedMiningShare() {} + MergedMiningShare(const uint256& hash, const uint256& prev_hash) : BaseShare<36>(hash, prev_hash) {} +}; + +struct Formatter +{ + SHARE_FORMATTER() + { + // small_block_header_type: + READWRITE(obj->m_min_header); + // share_info_type: + READWRITE( + obj->m_prev_hash, + obj->m_coinbase, + obj->m_nonce + ); + + // Address handling — version-dependent + if constexpr (version >= 36) + { + READWRITE(obj->m_pubkey_hash); // IntType(160) + READWRITE(obj->m_pubkey_type); // IntType(8) + } + else if constexpr (version >= 34) + { + READWRITE(obj->m_address); + } + else + { + READWRITE(obj->m_pubkey_hash); // pubkey_hash + } + + // Subsidy — V36 uses VarInt, others use fixed uint64 + if constexpr (version >= 36) + { + READWRITE(VarInt(obj->m_subsidy)); + } + else + { + READWRITE(obj->m_subsidy); + } + + READWRITE( + obj->m_donation, + Using>>(obj->m_stale_info), + VarInt(obj->m_desired_version) + ); + + if constexpr (is_segwit_activated(version)) + { + READWRITE(Optional(obj->m_segwit_data, SegwitDataDefault)); + } + + // V36: merged_addresses (after segwit_data, before far_share_hash) + if constexpr (version >= 36) + { + READWRITE(obj->m_merged_addresses); + } + + if constexpr (version < 34) + { + READWRITE(obj->m_tx_info); + } + + READWRITE( + obj->m_far_share_hash, + obj->m_max_bits, + obj->m_bits, + obj->m_timestamp, + obj->m_absheight + ); + + // Abswork — V36 uses VarInt-encoded uint64, others use fixed uint128 + if constexpr (version >= 36) + { + READWRITE(Using(obj->m_abswork)); + } + else + { + READWRITE(obj->m_abswork); + } + + // V36: merged_coinbase_info + merged_payout_hash (after abswork) + if constexpr (version >= 36) + { + READWRITE(obj->m_merged_coinbase_info); + READWRITE(obj->m_merged_payout_hash); + } + + // ref_merkle_link + READWRITE( + MERKLE_LINK_SMALL(obj->m_ref_merkle_link) + ); + // last_txout_nonce + READWRITE(obj->m_last_txout_nonce); + // hash_link (V36: V36HashLinkType with extra_data; others: HashLinkType) + READWRITE(obj->m_hash_link); + // merkle_link + READWRITE( + MERKLE_LINK_SMALL(obj->m_merkle_link) + ); + + // V36: message_data (at the end) + if constexpr (version >= 36) + { + READWRITE(obj->m_message_data); + } + } +}; + +using ShareType = chain::ShareVariants; + +inline ShareType load_share(chain::RawShare& rshare, NetService peer_addr) +{ + auto stream = rshare.contents.as_stream(); + auto share = ShareType::load(rshare.type, stream); + share.ACTION({ obj->peer_addr = peer_addr; }); + return share; +} + +template +inline ShareType load_share(int64_t version, StreamType& is, NetService peer_addr) +{ + auto share = ShareType::load(version, is); + share.ACTION({ obj->peer_addr = peer_addr; }); + return share; +} + +struct ShareHasher +{ + // this used to call `GetCheapHash()` in uint256, which was later moved; the + // cheap hash function simply calls ReadLE64() however, so the end result is + // identical + size_t operator()(const uint256& hash) const + { + return hash.GetLow64(); + } +}; + +class ShareIndex : public chain::ShareIndex +{ + using base_index = chain::ShareIndex; + +public: + // Per-share fields (NOT accumulated — each share stores only its own values). + // Accumulated values are computed on-the-fly by walking m_shares[tail]. + uint288 work; // target_to_average_attempts(bits) + uint288 min_work; // target_to_average_attempts(max_bits) + + // Per-share metadata + int64_t time_seen{0}; + int32_t naughty{0}; + bool is_block_solution{false}; // pow_hash <= block_target (set during init_verify) + uint256 pow_hash; // SHA256d hash, cached at reception (for block scan) + + ShareIndex() : base_index(), work(0), min_work(0) {} + + template ShareIndex(ShareT* share) : base_index(share) + { + work = chain::target_to_average_attempts(chain::bits_to_target(share->m_bits)); + min_work = chain::target_to_average_attempts(chain::bits_to_target(share->m_max_bits)); + time_seen = std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count(); + } +}; + +struct ShareChain : chain::ShareChain +{ + +}; + +} // namespace bch + diff --git a/src/impl/bch/share_check.hpp b/src/impl/bch/share_check.hpp new file mode 100644 index 000000000..870452df9 --- /dev/null +++ b/src/impl/bch/share_check.hpp @@ -0,0 +1,3279 @@ +#pragma once + +// P2: Share verification — check_hash_link, check_merkle_link, share init/check +// Ported from legacy sharechains/data.cpp + sharechains/share.cpp + +#include "config_pool.hpp" +#include "share.hpp" +#include "share_messages.hpp" +#include "share_types.hpp" + +#include +#include +#include +#include +#include +#include +#include +// (LTC's btclibs/crypto/scrypt.h removed — BTC PoW is SHA256d via core/hash.hpp) +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace bch +{ + +// P2Pool witness nonce: '[P2Pool]' repeated 4 times = 32 bytes +// Used for witness commitment: SHA256d(wtxid_merkle_root || P2POOL_WITNESS_NONCE) +static const unsigned char P2POOL_WITNESS_NONCE[32] = { + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, + 0x5b, 0x50, 0x32, 0x50, 0x6f, 0x6f, 0x6c, 0x5d, +}; + +// Compute P2Pool witness commitment hash from raw wtxid merkle root. +// Returns SHA256d(root || '[P2Pool]'*4) +inline uint256 compute_p2pool_witness_commitment(const uint256& wtxid_merkle_root) { + uint256 nonce; + std::memcpy(nonce.data(), P2POOL_WITNESS_NONCE, 32); + return Hash(wtxid_merkle_root, nonce); +} + +// ============================================================================ +// check_hash_link() +// +// Restores SHA256 mid-state from hash_link, continues hashing with +// (data + extra), then double-SHA256 finalises to the gentx_hash. +// +// Legacy: sharechains/data.cpp check_hash_link() +// ============================================================================ +template +inline uint256 check_hash_link(const HashLinkT& hash_link, + const std::vector& data, + const std::vector& const_ending = {}) +{ + const uint64_t extra_length = hash_link.m_length % 64; // 512/8 = 64 + + // hash_link.extra_data handling: + // Python reference (check_hash_link in p2pool/data.py): + // extra = (extra_data + const_ending)[len(extra_data) + len(const_ending) - extra_length:] + // This takes the LAST extra_length bytes of (extra_data + const_ending), + // i.e. extra_data first, then const_ending tail — matching the original + // byte order in the coinbase prefix (after the full SHA256 blocks). + std::vector extra; + if constexpr (requires { hash_link.m_extra_data.m_data; }) + { + // V36HashLinkType: extra_data is BaseScript (VarStr) + extra.assign(hash_link.m_extra_data.m_data.begin(), + hash_link.m_extra_data.m_data.end()); + if (extra.size() < extra_length) + { + // Append const_ending tail AFTER extra_data (matching Python's order) + auto needed = extra_length - extra.size(); + if (const_ending.size() >= needed) + extra.insert(extra.end(), const_ending.end() - needed, const_ending.end()); + } + } + else + { + // Pre-V36: extra_data always empty, use const_ending tail + extra.assign(const_ending.begin(), const_ending.end()); + if (extra.size() > extra_length) + extra.erase(extra.begin(), extra.begin() + (extra.size() - extra_length)); + } + if (extra.size() != extra_length) + throw std::runtime_error("check_hash_link: extra size mismatch"); + + // Restore SHA256 mid-state from hash_link.m_state (32 bytes, big-endian) + const auto& state_bytes = hash_link.m_state.m_data; + uint32_t init_state[8] = { + ReadBE32(state_bytes.data() + 0), + ReadBE32(state_bytes.data() + 4), + ReadBE32(state_bytes.data() + 8), + ReadBE32(state_bytes.data() + 12), + ReadBE32(state_bytes.data() + 16), + ReadBE32(state_bytes.data() + 20), + ReadBE32(state_bytes.data() + 24), + ReadBE32(state_bytes.data() + 28), + }; + + // Continue hashing from mid-state: Write(data) fills the partial + // block (extra is already in buf via the constructor), then processes + // remaining full blocks. Single SHA256 pass. + unsigned char out1[CSHA256::OUTPUT_SIZE]; + CSHA256(init_state, extra, hash_link.m_length) + .Write(data.data(), data.size()) + .Finalize(out1); + + // Second SHA256 pass → double-SHA256 + unsigned char out2[CSHA256::OUTPUT_SIZE]; + CSHA256().Write(out1, CSHA256::OUTPUT_SIZE).Finalize(out2); + + // Write raw double-SHA256 output directly into uint256 + // (matches Bitcoin Core's Hash() which does the same) + uint256 result; + std::memcpy(result.data(), out2, 32); + return result; +} + +// ============================================================================ +// prefix_to_hash_link() +// +// Forward computation of hash_link: given a coinbase prefix (everything up to +// and including the const_ending), capture the SHA256 mid-state so that +// check_hash_link() can resume and produce the coinbase txid. +// +// Python reference (p2pool): +// def prefix_to_hash_link(prefix, const_ending=''): +// x = sha256(prefix) +// return dict(state=x.state, extra_data=x.buf[:max(0,len(x.buf)-len(const_ending))], +// length=x.length//8) +// ============================================================================ +inline V36HashLinkType prefix_to_hash_link( + const std::vector& prefix, + const std::vector& const_ending) +{ + // Feed the entire prefix into a CSHA256 + CSHA256 hasher; + hasher.Write(prefix.data(), prefix.size()); + + V36HashLinkType result; + + // Extract mid-state as big-endian bytes (matches check_hash_link's ReadBE32) + result.m_state.m_data.resize(32); + for (int i = 0; i < 8; ++i) { + WriteBE32(result.m_state.m_data.data() + i * 4, hasher.s[i]); + } + + // extra_data = buf[0 .. bufsize - const_ending_len] + size_t bufsize = hasher.bytes % 64; + size_t extra_len = (bufsize > const_ending.size()) ? (bufsize - const_ending.size()) : 0; + result.m_extra_data.m_data.assign(hasher.buf, hasher.buf + extra_len); + + // length = total bytes processed so far + result.m_length = hasher.bytes; + + return result; +} + +// prefix_to_hash_link for V35: returns HashLinkType (no extra_data field) +inline HashLinkType prefix_to_hash_link_v35( + const std::vector& prefix, + const std::vector& const_ending) +{ + auto v36_link = prefix_to_hash_link(prefix, const_ending); + HashLinkType result; + result.m_state = v36_link.m_state; + result.m_length = v36_link.m_length; + // V35: extra_data is always empty (FixedStrType(0)) — the donation script + // is long enough to consume the entire SHA256 buffer tail. + return result; +} + +// ============================================================================ +// check_merkle_link() +// +// Walk a Merkle branch to compute the root from a given tip_hash. +// +// Legacy: libcoind/data.cpp check_merkle_link() +// ============================================================================ +inline uint256 check_merkle_link(const uint256& tip_hash, const MerkleLink& link) +{ + if (link.m_branch.size() > 0 && + link.m_index >= (1u << link.m_branch.size())) + throw std::invalid_argument("check_merkle_link: index too large"); + + uint256 cur = tip_hash; + for (size_t i = 0; i < link.m_branch.size(); ++i) + { + // Combine: if bit i of index is set, branch[i] is on the left + PackStream ps; + if ((link.m_index >> i) & 1) + { + ps << link.m_branch[i]; + ps << cur; + } + else + { + ps << cur; + ps << link.m_branch[i]; + } + + // double-SHA256 of the 64-byte concatenation + auto sp = std::span( + reinterpret_cast(ps.data()), ps.size()); + cur = Hash(sp); + } + return cur; +} + +// ============================================================================ +// compute_gentx_before_refhash() +// +// Computes the constant ending bytes that appear after the coinbase outputs +// and before the ref_hash in the serialised coinbase transaction. +// +// Legacy: networks/network.cpp "init gentx_before_refhash" +// Formula: VarStr(DONATION_SCRIPT) + int64(0) + VarStr(0x6a28 + int256(0) + int64(0))[:3] +// ============================================================================ +inline std::vector compute_gentx_before_refhash(int64_t share_version) +{ + std::vector result; + + // 1. VarStr(DONATION_SCRIPT) + auto donation_script = PoolConfig::get_donation_script(share_version); + { + PackStream s; + BaseScript bs; + bs.m_data = donation_script; + s << bs; + auto* p = reinterpret_cast(s.data()); + result.insert(result.end(), p, p + s.size()); + } + + // 2. int64(0) + { + uint64_t zero64 = 0; + auto* p = reinterpret_cast(&zero64); + result.insert(result.end(), p, p + 8); + } + + // 3. VarStr(0x6a 0x28 + int256(0) + int64(0)) — but only the first 3 bytes + { + PackStream inner; + // raw bytes: OP_RETURN (0x6a) + PUSH_40 (0x28) + unsigned char prefix[2] = {0x6a, 0x28}; + inner.write(std::span(reinterpret_cast(prefix), 2)); + // 32 zero bytes (uint256(0)) + uint256 zero256; + inner << zero256; + // 8 zero bytes (uint64(0)) + uint64_t zero64 = 0; + inner.write(std::span(reinterpret_cast(&zero64), 8)); + + // Pack as VarStr + PackStream outer; + BaseScript bs; + bs.m_data.resize(inner.size()); + std::memcpy(bs.m_data.data(), inner.data(), inner.size()); + outer << bs; + + // Take only the first 3 bytes + auto* p = reinterpret_cast(outer.data()); + result.insert(result.end(), p, p + std::min(3, outer.size())); + } + + return result; +} + +// ============================================================================ +// pubkey_hash_to_address() +// +// Convert (pubkey_hash, pubkey_type) to a Litecoin address string. +// Used for V35 share_data.address field (VarStr). +// pubkey_type: 0=P2PKH, 1=P2WPKH, 2=P2SH (same as V36 encoding) +// ============================================================================ +inline std::string pubkey_hash_to_address(const uint160& pubkey_hash, uint8_t pubkey_type) +{ + bool testnet = PoolConfig::is_testnet; + if (pubkey_type == 0) { + // P2PKH: Base58Check with version byte + uint8_t ver = testnet ? 0x6f : 0x30; + std::vector payload(21); + payload[0] = ver; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); + } else if (pubkey_type == 1) { + // P2WPKH: Bech32 segwit v0 — BTC mainnet "bc", testnet "tb" + std::string hrp = testnet ? "tb" : "bc"; + std::vector prog(20); + std::memcpy(prog.data(), pubkey_hash.data(), 20); + return bech32::encode_segwit(hrp, 0, prog); + } else if (pubkey_type == 2) { + // P2SH: Base58Check with version byte + uint8_t ver = testnet ? 0xc4 : 0x32; + std::vector payload(21); + payload[0] = ver; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); + } + // Fallback: P2PKH + uint8_t ver = testnet ? 0x6f : 0x30; + std::vector payload(21); + payload[0] = ver; + std::memcpy(payload.data() + 1, pubkey_hash.data(), 20); + return EncodeBase58Check({payload.data(), payload.size()}); +} + +// ============================================================================ +// compute_ref_hash_for_work() +// +// Computes the p2pool ref_hash for a set of share fields. Used at Stratum +// work generation time to build the OP_RETURN commitment per connection. +// +// Parameters mirror the share fields that feed into the ref_stream. +// Returns (ref_hash, last_txout_nonce). +// ============================================================================ +struct RefHashParams { + int64_t share_version{36}; // 35 or 36 — determines serialization format + uint256 prev_share; + std::vector coinbase_scriptSig; + uint32_t share_nonce{0}; + // V36: pubkey_hash + pubkey_type; V35: address (string bytes) + uint160 pubkey_hash; + uint8_t pubkey_type{0}; + std::string address; // V35: base58/bech32 address string + uint64_t subsidy{0}; + uint16_t donation{50}; + uint8_t stale_info{0}; + uint64_t desired_version{36}; + bool has_segwit{false}; + SegwitData segwit_data; + std::vector merged_addresses; // V36 only + uint256 far_share_hash; + uint32_t max_bits{0}; + uint32_t bits{0}; + uint32_t timestamp{0}; + uint32_t absheight{0}; + uint128 abswork; + std::vector merged_coinbase_info; // V36: per-chain DOGE header + merkle proof + uint256 merged_payout_hash; // V36 only + BaseScript message_data; // V36 PossiblyNoneType(b'', VarStrType()) +}; + +inline std::pair compute_ref_hash_for_work(const RefHashParams& p) +{ + PackStream ref_stream; + + // IDENTIFIER + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + ref_stream << p.prev_share; + + // coinbase as VarStr + { + BaseScript bs; + bs.m_data = p.coinbase_scriptSig; + ref_stream << bs; + } + + ref_stream << p.share_nonce; + + if (p.share_version >= 36) { + // V36: pubkey_hash (uint160) + pubkey_type (uint8) + ref_stream << p.pubkey_hash; + ref_stream << p.pubkey_type; + ::Serialize(ref_stream, VarInt(p.subsidy)); + } else if (p.share_version >= 34) { + // V34-V35: address as VarStr + BaseScript addr_bs; + addr_bs.m_data.assign(p.address.begin(), p.address.end()); + ref_stream << addr_bs; + ref_stream << p.subsidy; // fixed uint64 + } else { + // Pre-V34: pubkey_hash only + ref_stream << p.pubkey_hash; + ref_stream << p.subsidy; // fixed uint64 + } + + ref_stream << p.donation; + ref_stream << p.stale_info; + ::Serialize(ref_stream, VarInt(p.desired_version)); + + if (p.has_segwit) + ref_stream << p.segwit_data; + + // V36: merged_addresses (after segwit_data, before far_share_hash) + if (p.share_version >= 36) + ref_stream << p.merged_addresses; + + ref_stream << p.far_share_hash; + ref_stream << p.max_bits; + ref_stream << p.bits; + ref_stream << p.timestamp; + ref_stream << p.absheight; + + if (p.share_version >= 36) { + ::Serialize(ref_stream, Using(p.abswork)); + ref_stream << p.merged_coinbase_info; + ref_stream << p.merged_payout_hash; + // V36 ref_type includes message_data as PossiblyNoneType(b'', VarStrType()) + ref_stream << p.message_data; + } else { + // Pre-V36: abswork as fixed uint128 + ref_stream << p.abswork; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 ref_hash = Hash(ref_span); + + { + static int rfn_log = 0; + static int rfn_v36_log = 0; + bool should_log = (rfn_log < 3) || (p.share_version >= 36 && rfn_v36_log < 5); + if (should_log) { + rfn_log++; + if (p.share_version >= 36) rfn_v36_log++; + static const char* HX = "0123456789abcdef"; + std::string hex; + auto* rd = reinterpret_cast(ref_stream.data()); + for (size_t i = 0; i < ref_stream.size(); ++i) { hex += HX[rd[i]>>4]; hex += HX[rd[i]&0xf]; } + LOG_INFO << "[REF-FN] v=" << p.share_version + << " ref_packed_len=" << ref_stream.size() + << " ref_hash=" << ref_hash.GetHex() + << " absheight=" << p.absheight << " prev=" << p.prev_share.GetHex().substr(0,16); + LOG_INFO << "[REF-FN-FULL] " << hex; + } + } + + // Generate a random-ish last_txout_nonce + uint64_t nonce = static_cast(std::time(nullptr)) ^ + (static_cast(p.timestamp) << 32) ^ + (static_cast(p.absheight) << 16); + + return {ref_hash, nonce}; +} + +// Thread-local state from share_init_verify — read by attempt_verify() without re-computing scrypt. +inline thread_local bool g_last_init_is_block = false; +inline thread_local uint256 g_last_pow_hash; // scrypt hash of the share header + +// ============================================================================ +// share_init_verify() +// +// Performs the init()-phase verification of a share: +// 1. Basic field validation (coinbase size, merkle branch lengths) +// 2. Compute hash_link_data from ref serialisation +// 3. check_hash_link → gentx_hash +// 4. check_merkle_link → merkle_root +// 5. Build full block header +// 6. Compute hash (double-SHA256 of header) +// 7. Verify pow_hash <= target +// +// Returns the share hash (double-SHA256 of the reconstructed header). +// Throws on any validation failure. +// +// NOTE: The full GenerateShareTransaction reconstruction from check() is +// deferred to a later PR (P2.5). This function covers the PoW + hash-link +// verification path from legacy Share::init(). +// ============================================================================ +template +uint256 share_init_verify(const ShareT& share, bool check_pow = true) +{ + // --- Basic validation --- + if (share.m_coinbase.size() < 2 || share.m_coinbase.size() > 100) + throw std::invalid_argument("bad coinbase size"); + + if (share.m_merkle_link.m_branch.size() > 16) + throw std::invalid_argument("merkle branch too long"); + + constexpr int64_t ver = ShareT::version; + + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + if (share.m_segwit_data->m_txid_merkle_link.m_branch.size() > 16) + throw std::invalid_argument("segwit txid merkle branch too long"); + } + } + } + + // --- Compute ref_hash --- + // RefType serialisation: IDENTIFIER + share_info fields + segwit_data + // Then hash256 it, then check_merkle_link with ref_merkle_link + // + // For now we serialise the minimal fields the same way the legacy code + // does (share_data + share_info + optional segwit_data) via a PackStream. + PackStream ref_stream; + + // IDENTIFIER bytes (8 bytes from IDENTIFIER_HEX) + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + // share_info serialisation — we re-serialise the share's share_info fields + // through the same Formatter path that was used to decode them. + // For ref_hash we need: share_data + share_info fields + segwit_data + // We serialise the relevant fields into the ref_stream. + { + // prev_hash + ref_stream << share.m_prev_hash; + // coinbase + ref_stream << share.m_coinbase; + // nonce (uint32_t LE) + ref_stream << share.m_nonce; + + // address or pubkey_hash — V34/V35 use m_address (VarStr), + // V36+ uses m_pubkey_hash (uint160) + m_pubkey_type (uint8_t), + // pre-V34 uses m_pubkey_hash only. + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + // subsidy: VarInt for V36+, raw uint64_t LE for older + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + // stale_info as EnumType> — single byte + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + // desired_version as VarInt + { + uint64_t dv = share.m_desired_version; + ::Serialize(ref_stream, VarInt(dv)); + } + + // segwit_data (optional) + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None) + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + // merged_addresses (V36+) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + // tx info (pre-v34) + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + // far_share_hash, max_bits, bits, timestamp, absheight + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + // abswork: AbsworkV36Format for V36+, raw uint128 LE for older + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + // V36+ merged mining commitment fields + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data as PossiblyNoneType(b'', VarStrType()) + // When m_message_data is empty, BaseScript serialises as varint(0) = 0x00. + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + // hash256 of the ref_type serialisation + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + + // check_merkle_link with ref_merkle_link + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // --- Build hash_link_data --- + // hash_link_data = ref_hash bytes + pack(last_txout_nonce, LE64) + pack(0, LE32) + std::vector hash_link_data; + hash_link_data.insert(hash_link_data.end(), ref_hash.data(), ref_hash.data() + 32); + { + // last_txout_nonce as little-endian uint64 + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + hash_link_data.insert(hash_link_data.end(), p, p + 8); + } + { + // trailing zero uint32 + uint32_t zero = 0; + auto* p = reinterpret_cast(&zero); + hash_link_data.insert(hash_link_data.end(), p, p + 4); + } + + auto gentx_before_refhash = compute_gentx_before_refhash(ver); + + // --- check_hash_link → gentx_hash --- + uint256 gentx_hash = check_hash_link(share.m_hash_link, hash_link_data, gentx_before_refhash); + + // Diagnostic: compare hash_link-derived gentx_hash with expected + // Only log for self-validation (check_pow=true means local share) + if (check_pow) { + static int verify_diag = 0; + if (verify_diag < 10) { + LOG_INFO << "[verify-diag] gentx_hash(hash_link)=" << gentx_hash.GetHex() + << " hash_link_data_len=" << hash_link_data.size(); + ++verify_diag; + } + } + + // --- Merkle root --- + // For segwit-activated shares, use segwit_data.txid_merkle_link; otherwise merkle_link + uint256 merkle_root; + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + merkle_root = check_merkle_link(gentx_hash, share.m_segwit_data->m_txid_merkle_link); + else + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + else + { + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + } + else + { + merkle_root = check_merkle_link(gentx_hash, share.m_merkle_link); + } + + // --- Reconstruct full block header and compute hash --- + // BlockHeaderType: version(int32) + previous_block + merkle_root + timestamp + bits + nonce + // Note: the full block header uses fixed 4-byte version (not VarInt like SmallBlockHeaderType) + PackStream header_stream; + { + uint32_t hdr_version = static_cast(share.m_min_header.m_version); + header_stream << hdr_version; + } + header_stream << share.m_min_header.m_previous_block; + header_stream << merkle_root; + header_stream << share.m_min_header.m_timestamp; + header_stream << share.m_min_header.m_bits; + header_stream << share.m_min_header.m_nonce; + + // hash = double-SHA256 of the header (the share's identity) + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + uint256 share_hash = Hash(hdr_span); + + // --- PoW check (SHA256d) --- + // For Bitcoin POW_FUNC is SHA256d, identical to the block identity hash. + // (LTC was scrypt(1024,1,1,256); BTC's pow_hash == share_hash via Hash(hdr_span).) + if (check_pow) + { + uint256 target = chain::bits_to_target(share.m_bits); + if (target.IsNull()) + throw std::invalid_argument("share target is zero"); + if (target > PoolConfig::max_target()) + throw std::invalid_argument("share target exceeds MAX_TARGET"); + auto max_target = chain::bits_to_target(share.m_max_bits); + if (!max_target.IsNull() && target > max_target) + throw std::invalid_argument("share target exceeds max_target — too easy"); + + // BTC PoW: SHA256d(header). share_hash was already computed above. + uint256 pow_hash = share_hash; + g_last_pow_hash = pow_hash; // cache for attempt_verify merged check + + if (pow_hash > target) + { + // Expected for stratum pseudoshares: VARDIFF target is much lower + // than share target, so most submissions won't meet PoW. + // Only a real share (1 in ~20000 pseudoshares) passes this check. + LOG_TRACE << "PoW below share target: bits=" << share.m_bits + << " target=" << target.GetHex().substr(0,32) + << " pow_hash=" << pow_hash.GetHex().substr(0,32); + throw std::invalid_argument("share PoW hash does not meet target"); + } + + // Block detection: check if share's scrypt hash also meets the BLOCK target. + // min_header.m_bits = block difficulty from GBT (much harder than share target). + // When pow_hash <= block_target, this share IS a solved block! + uint256 block_target = chain::bits_to_target(share.m_min_header.m_bits); + g_last_init_is_block = false; + if (!block_target.IsNull() && pow_hash <= block_target) { + g_last_init_is_block = true; + static uint256 s_last_block_share; + if (share_hash != s_last_block_share) { + s_last_block_share = share_hash; + LOG_INFO << "[BLOCK] Peer share meets block target" + << " hash=" << share_hash.GetHex().substr(0,16); + } + } + } + + return share_hash; +} + +// ============================================================================ +// Helper: convert pubkey_hash + type to full scriptPubKey +// ============================================================================ +inline std::vector pubkey_hash_to_script(const uint160& hash, uint8_t type = 0) +{ + std::vector script; + auto h = hash.GetChars(); // little-endian (internal storage order) + switch (type) + { + case 1: // P2WPKH: OP_0 <20> + // Use GetChars() output directly — same as P2PKH and P2SH. + // The bytes in m_pubkey_hash are the raw witness program as + // deserialized from the wire. No reversal needed. + script.reserve(22); + script.push_back(0x00); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + break; + case 2: // P2SH: OP_HASH160 <20> OP_EQUAL + script.reserve(23); + script.push_back(0xa9); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + script.push_back(0x87); + break; + default: // P2PKH: OP_DUP OP_HASH160 <20> OP_EQUALVERIFY OP_CHECKSIG + script.reserve(25); + script.push_back(0x76); + script.push_back(0xa9); + script.push_back(0x14); + script.insert(script.end(), h.begin(), h.end()); + script.push_back(0x88); + script.push_back(0xac); + break; + } + return script; +} + +// ============================================================================ +// Normalize a parent chain script to merged chain P2PKH script. +// +// P2WPKH (00 14 ) → P2PKH (76 a9 14 88 ac) [same pubkey_hash] +// P2PKH (76 a9 14 88 ac) → passed through +// P2SH (a9 14 87) → P2SH (passed through) +// P2WSH (00 20 ) → empty (unconvertible) +// P2TR (51 20 ) → empty (unconvertible) +// +// Returns empty vector for unconvertible scripts (Tier 3: redistributed). +// Matches Python data.py:build_canonical_merged_coinbase() conversion logic. +// ============================================================================ +inline std::vector normalize_script_for_merged( + const std::vector& script) +{ + // P2PKH (25 bytes: 76 a9 14 <20> 88 ac) — already correct + if (script.size() == 25 && script[0] == 0x76 && script[1] == 0xa9 && + script[2] == 0x14 && script[23] == 0x88 && script[24] == 0xac) + return script; + + // P2WPKH (22 bytes: 00 14 <20>) — convert to P2PKH using same hash + if (script.size() == 22 && script[0] == 0x00 && script[1] == 0x14) + { + std::vector p2pkh; + p2pkh.reserve(25); + p2pkh.push_back(0x76); // OP_DUP + p2pkh.push_back(0xa9); // OP_HASH160 + p2pkh.push_back(0x14); // PUSH 20 + p2pkh.insert(p2pkh.end(), script.begin() + 2, script.end()); // + p2pkh.push_back(0x88); // OP_EQUALVERIFY + p2pkh.push_back(0xac); // OP_CHECKSIG + return p2pkh; + } + + // P2SH (23 bytes: a9 14 <20> 87) — pass through (DOGE supports P2SH) + if (script.size() == 23 && script[0] == 0xa9 && script[1] == 0x14 && + script[22] == 0x87) + return script; + + // P2WSH (34 bytes: 00 20 <32>) or P2TR (34 bytes: 51 20 <32>) — unconvertible + return {}; +} + +// MERGED: prefix for weight map keys — matches p2pool's 'MERGED:' + hex string. +// Keeps Tier 1/1.5 (explicit DOGE script) keys separate from raw LTC script keys +// in the same weight map, preventing V35+V36 weight collapse for the same miner. +// 7 bytes: 0x4d 0x45 0x52 0x47 0x45 0x44 0x3a = "MERGED:" +inline constexpr std::array MERGED_KEY_PREFIX = { + 0x4d, 0x45, 0x52, 0x47, 0x45, 0x44, 0x3a +}; + +// Prepend MERGED: prefix to a script for use as a weight map key. +inline std::vector make_merged_key( + const std::vector& script) +{ + std::vector key; + key.reserve(MERGED_KEY_PREFIX.size() + script.size()); + key.insert(key.end(), MERGED_KEY_PREFIX.begin(), MERGED_KEY_PREFIX.end()); + key.insert(key.end(), script.begin(), script.end()); + return key; +} + +// Check if a weight key has the MERGED: prefix. +inline bool is_merged_key(const std::vector& key) +{ + return key.size() > MERGED_KEY_PREFIX.size() && + std::equal(MERGED_KEY_PREFIX.begin(), MERGED_KEY_PREFIX.end(), + key.begin()); +} + +// Strip MERGED: prefix, returning the raw script bytes. +// Caller must check is_merged_key() first. +inline std::vector strip_merged_key( + const std::vector& key) +{ + return std::vector( + key.begin() + MERGED_KEY_PREFIX.size(), key.end()); +} + +// Resolve a weight map key to a DOGE-compatible scriptPubKey. +// MERGED:-prefixed keys: strip prefix, use directly (already a DOGE script). +// Raw keys: autoconvert (P2WPKH→P2PKH, P2PKH/P2SH pass through). +// Returns empty if unconvertible (P2WSH, P2TR, etc.). +inline std::vector resolve_merged_payout_script( + const std::vector& key) +{ + if (is_merged_key(key)) + return strip_merged_key(key); + return normalize_script_for_merged(key); +} + +// ============================================================================ +// Helper: extract full scriptPubKey from a share variant +// ============================================================================ +inline std::vector get_share_script(const auto* obj) +{ + if constexpr (requires { obj->m_pubkey_type; }) + return pubkey_hash_to_script(obj->m_pubkey_hash, obj->m_pubkey_type); + else if constexpr (requires { obj->m_address; }) + { + // V34/V35: m_address contains a human-readable address string. + // Convert to scriptPubKey for PPLNS weight computation. + std::string addr_str(obj->m_address.m_data.begin(), obj->m_address.m_data.end()); + auto script = core::address_to_script(addr_str); + if (!script.empty()) + return script; + // Fallback: return raw bytes (shouldn't happen with valid addresses) + return obj->m_address.m_data; + } + else + return pubkey_hash_to_script(obj->m_pubkey_hash, 0); +} + +// ============================================================================ +// generate_share_transaction() +// +// Reconstructs the expected coinbase transaction from a share's fields and +// the PPLNS weights computed from the share chain. Returns the expected +// gentx txid (double-SHA256 of the non-witness serialised transaction). +// +// This is the C++ port of p2pool v36's generate_transaction() / check(). +// +// The coinbase structure is: +// tx_ins: [ { prev_output: 0...0:ffffffff, script: coinbase } ] +// tx_outs: [ segwit_commitment?, +// ...pplns_payout_outputs..., +// donation_output, +// op_return_commitment ] +// lock_time: 0 +// +// Reference: frstrtr/p2pool-merged-v36 p2pool/data.py generate_transaction() +// ============================================================================ +template +uint256 generate_share_transaction(const ShareT& share, TrackerT& tracker, bool dump_diag = false, bool v36_active = false) +{ + auto gst_t0 = std::chrono::steady_clock::now(); + constexpr int64_t ver = ShareT::version; + // p2pool selects PPLNS formula by runtime AutoRatchet state, not compile-time + // share version. When v36_active is true (AutoRatchet ACTIVATED/CONFIRMED), + // use v36 PPLNS even for v35 shares. Ref: p2pool data.py:879, work.py:759. + const bool use_v36_pplns = v36_active || (ver >= 36); + const uint64_t subsidy = share.m_subsidy; + const uint16_t donation = share.m_donation; + + // Debug: log the PPLNS formula selection for cross-impl comparison + { + static int gst_pplns_log = 0; + if (gst_pplns_log++ % 50 == 0) { + LOG_DEBUG_DIAG << "[GST-PPLNS] v36_active=" << v36_active + << " use_v36_pplns=" << use_v36_pplns + << " ver=" << ver + << " start=" << share.m_prev_hash.GetHex().substr(0, 16) + << " subsidy=" << subsidy + << " donation=" << donation; + } + } + + // --- 1. Compute PPLNS weights with full scriptPubKey keys --- + // Walk from share's prev_hash (parent) backward through the chain. + // This matches the Python: weights are computed relative to the share's parent. + + auto prev_hash = share.m_prev_hash; + std::map, uint288> weights; + uint288 total_weight; + uint288 total_donation_weight; + + if (!prev_hash.IsNull() && tracker.chain.contains(prev_hash)) + { + // p2pool data.py:762-764 — refuse to compute PPLNS with insufficient depth. + // Without this guard, attempt_verify() (which allows CHAIN_LENGTH+1) can + // trigger a PPLNS walk that terminates early, producing wrong coinbase + // amounts and causing persistent GENTX-MISMATCH during bootstrap. + auto chain_len = static_cast(PoolConfig::real_chain_length()); + { + auto pplns_height = tracker.chain.get_height(prev_hash); + auto pplns_last = tracker.chain.get_last(prev_hash); + if (!(pplns_height >= chain_len || pplns_last.IsNull())) + throw std::invalid_argument( + "share chain not long enough for PPLNS verification (height=" + + std::to_string(pplns_height) + " need=" + + std::to_string(chain_len) + ")"); + } + + // block_target from block header bits (matches Python: self.header['bits'].target) + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * PoolConfig::SPREAD * 65535; + + // PPLNS formula selected by runtime v36_active (AutoRatchet state), + // not compile-time share version. Ref: p2pool data.py:879, work.py:759. + if (use_v36_pplns) { + // V36 PPLNS: exponential depth-decay, walk from parent + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + auto result = tracker.get_v36_decayed_cumulative_weights(prev_hash, chain_len, unlimited_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + total_donation_weight = result.total_donation_weight; + } else { + // Pre-V36 PPLNS: flat cumulative weights (no decay) + // CRITICAL: Walk from GRANDPARENT for HEIGHT-1 shares. + // p2pool data.py:884-885: + // _pplns_start = previous_share.share_data['previous_share_hash'] + // _pplns_max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) + uint256 pplns_start; + tracker.chain.get(prev_hash).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; // grandparent + }); + auto available = tracker.chain.get_height(prev_hash); + auto walk_count = static_cast( + std::max(0, std::min(chain_len, available) - 1)); + + if (!pplns_start.IsNull() && tracker.chain.contains(pplns_start) && walk_count > 0) { + auto result = tracker.get_cumulative_weights(pplns_start, walk_count, max_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + total_donation_weight = result.total_donation_weight; + } + } + } + + // --- 2. Convert weights to exact integer payout amounts --- + // Python formula: + // Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) + // amounts[finder] += subsidy // 200 + // V36: amounts[script] = subsidy * weight / total_weight + // donation = subsidy - sum(amounts) + + auto gst_t1 = std::chrono::steady_clock::now(); // after PPLNS walk + std::map, uint64_t> amounts; + + // Periodic dump of PPLNS weights for cross-impl comparison + { + static auto last_amt_dump = std::chrono::steady_clock::now() - std::chrono::seconds(60); + auto now_d = std::chrono::steady_clock::now(); + if (now_d - last_amt_dump > std::chrono::seconds(30) && weights.size() >= 2) { + last_amt_dump = now_d; + LOG_DEBUG_DIAG << "[PPLNS-AMT] subsidy=" << subsidy + << " total_weight=" << total_weight.GetLow64() + << " don_weight=" << total_donation_weight.GetLow64() + << " addrs=" << weights.size() + << " prev=" << prev_hash.GetHex().substr(0, 16); + for (auto& [s, w] : weights) { + uint64_t a = (total_weight.IsNull()) ? 0 : + (uint288(subsidy) * w / total_weight).GetLow64(); + LOG_DEBUG_DIAG << "[PPLNS-AMT] weight=" << w.GetLow64() + << " amount=" << a; + } + } + } + + if (!total_weight.IsNull()) + { + for (auto& [script, weight] : weights) + { + uint64_t amount; + if (use_v36_pplns) + { + // V36: amounts[script] = subsidy * weight / total_weight + uint288 num = uint288(subsidy) * weight; + amount = (num / total_weight).GetLow64(); + } + else + { + // Pre-V36: amounts[script] = subsidy * (199 * weight) / (200 * total_weight) + uint288 num = uint288(subsidy) * (weight * 199); + uint288 den = total_weight * 200; + amount = (num / den).GetLow64(); + } + if (amount > 0) + amounts[script] = amount; + } + } + + // Pre-V36: add 0.5% finder fee to share creator + if (!use_v36_pplns) + { + auto finder_script = get_share_script(&share); + amounts[finder_script] += subsidy / 200; + } + + // Donation output = subsidy minus sum of all payout amounts + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) + sum_amounts += a; + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + + // Dump amounts for cross-impl debugging + if (dump_diag) { + LOG_DEBUG_DIAG << "[GST-AMOUNTS] subsidy=" << subsidy << " addrs=" << amounts.size() + << " sum=" << sum_amounts << " donation=" << donation_amount + << " prev=" << prev_hash.GetHex().substr(0,16); + for (auto& [s, a] : amounts) { + static const char* HX = "0123456789abcdef"; + std::string sh; for (size_t i = 0; i < std::min(s.size(), size_t(10)); ++i) { sh += HX[s[i]>>4]; sh += HX[s[i]&0xf]; } + LOG_DEBUG_DIAG << "[GST-AMOUNTS] " << sh << "... = " << a; + } + } + + // V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f) + if (use_v36_pplns) { + if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) { + // Deduct 1 sat from the largest miner payout + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(amounts.begin(), amounts.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != amounts.end() && largest->second > 0) { + largest->second -= 1; + sum_amounts -= 1; + donation_amount = subsidy - sum_amounts; + } + } + } + + // --- 3. Build sorted output list --- + auto gst_t2 = std::chrono::steady_clock::now(); // after amounts + // Python: sorted(dests, key=lambda a: (amounts[a], a))[-4000:] + // = ascending by (amount, script), keep last 4000 (highest amounts) + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; // asc by amount + return a.first < b.first; // asc by script for tie-breaking + }); + + // Keep last MAX_OUTPUTS (highest amounts), matching Python's [-4000:] + constexpr size_t MAX_OUTPUTS = 4000; + if (payout_outputs.size() > MAX_OUTPUTS) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - MAX_OUTPUTS); + + // --- 4. Serialise the coinbase transaction --- + // Non-witness serialization (for txid computation): + // version(4) + vin_count(varint) + vin + vout_count(varint) + vouts + locktime(4) + PackStream tx; + + // tx version = 1 + uint32_t tx_version = 1; + tx.write(std::span(reinterpret_cast(&tx_version), 4)); + + // vin count = 1 + { + unsigned char one = 1; + tx.write(std::span(reinterpret_cast(&one), 1)); + } + + // vin[0]: prev_output = 0...0:ffffffff, script = coinbase, sequence = 0 + { + // prev_hash (32 zero bytes) + uint256 zero_hash; + tx << zero_hash; + // prev_index (0xffffffff) + uint32_t prev_idx = 0xffffffff; + tx.write(std::span(reinterpret_cast(&prev_idx), 4)); + // script (VarStr) + tx << share.m_coinbase; + // sequence (0xffffffff — standard coinbase sequence, matches Python) + uint32_t seq = 0xffffffff; + tx.write(std::span(reinterpret_cast(&seq), 4)); + } + + // Count total outputs + size_t n_outs = payout_outputs.size() + 1 /* donation */ + 1 /* OP_RETURN commitment */; + // Segwit commitment output (if applicable) + bool has_segwit = false; + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + has_segwit = true; + n_outs += 1; + } + } + } + + // vout count (varint — for < 253 outputs, it's a single byte) + if (n_outs < 253) + { + uint8_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 1)); + } + else + { + uint8_t marker = 0xfd; + tx.write(std::span(reinterpret_cast(&marker), 1)); + uint16_t cnt = static_cast(n_outs); + tx.write(std::span(reinterpret_cast(&cnt), 2)); + } + + // Helper to write a single tx_out: value(8LE) + script(VarStr) + auto write_txout = [&](uint64_t value, const std::vector& script) { + tx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; + bs.m_data = script; + tx << bs; + }; + + // Segwit commitment output (value=0, script=OP_RETURN + witness_commitment) + if (has_segwit) + { + if constexpr (requires { share.m_segwit_data; }) + { + if (share.m_segwit_data.has_value()) + { + // witness commitment: 0x6a24aa21a9ed + SHA256d(wtxid_merkle_root || '[P2Pool]'*4) + std::vector wscript; + wscript.push_back(0x6a); // OP_RETURN + wscript.push_back(0x24); // PUSH 36 + wscript.push_back(0xaa); + wscript.push_back(0x21); + wscript.push_back(0xa9); + wscript.push_back(0xed); + auto& sd = share.m_segwit_data.value(); + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto commitment_bytes = commitment.GetChars(); + wscript.insert(wscript.end(), commitment_bytes.begin(), commitment_bytes.end()); + write_txout(0, wscript); + if (dump_diag) { + LOG_INFO << "[WC-GST] wtxid_root=" << sd.m_wtxid_merkle_root.GetHex() + << " commitment=" << commitment.GetHex(); + } + } + } + } + + // PPLNS payout outputs + for (auto& [script, amount] : payout_outputs) + write_txout(amount, script); + + // Donation output — V35 shares always use P2PK DONATION_SCRIPT, + // V36 shares always use P2SH COMBINED_DONATION_SCRIPT. + // Each share was created with the donation script matching its version. + auto donation_script = PoolConfig::get_donation_script(ver); + write_txout(donation_amount, donation_script); + + // OP_RETURN commitment: value=0, script = 0x6a28 + ref_hash(32) + last_txout_nonce(8) + { + // We need the ref_hash — recompute it from the share the same way share_init_verify does + PackStream ref_stream; + + // IDENTIFIER bytes + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + + // share_info fields (same as share_init_verify) + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None). + // Must match share_init_verify — both paths must produce identical ref_hash. + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data (must match verify_share) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Build OP_RETURN script: 0x6a 0x28 + ref_hash(32) + last_txout_nonce(8) + std::vector op_return_script; + op_return_script.push_back(0x6a); // OP_RETURN + op_return_script.push_back(0x28); // PUSH 40 bytes + op_return_script.insert(op_return_script.end(), ref_hash.data(), ref_hash.data() + 32); + { + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + op_return_script.insert(op_return_script.end(), p, p + 8); + } + write_txout(0, op_return_script); + } + + // lock_time = 0 + { + uint32_t locktime = 0; + tx.write(std::span(reinterpret_cast(&locktime), 4)); + } + + // --- 5. Compute txid (double-SHA256 of non-witness serialization) --- + auto tx_span = std::span( + reinterpret_cast(tx.data()), tx.size()); + auto txid = Hash(tx_span); + + // V36 hash_link cross-check: compute prefix hash_link from our coinbase + // and compare with the share's stored hash_link. If states differ, + // the prefix (outputs/amounts) differs from what p2pool built. + if (dump_diag && use_v36_pplns) + { + if constexpr (requires { share.m_hash_link.m_extra_data; }) + { + auto gbr = compute_gentx_before_refhash(ver); + // prefix = full coinbase minus last 44 bytes (ref_hash 32 + nonce 8 + locktime 4) + size_t prefix_len = tx.size() - 44; + std::vector prefix( + reinterpret_cast(tx.data()), + reinterpret_cast(tx.data()) + prefix_len); + auto computed_hl = prefix_to_hash_link(prefix, gbr); + + bool state_match = (computed_hl.m_state.m_data == share.m_hash_link.m_state.m_data); + bool extra_match = (computed_hl.m_extra_data.m_data == share.m_hash_link.m_extra_data.m_data); + bool len_match = (computed_hl.m_length == share.m_hash_link.m_length); + + static const char* HXD = "0123456789abcdef"; + auto hex_fn = [&](const auto& v) { + std::string h; for (auto b : v) { h += HXD[(uint8_t)b >> 4]; h += HXD[(uint8_t)b & 0xf]; } return h; + }; + + LOG_WARNING << "[HASHLINK-CMP] state_match=" << (state_match ? "YES" : "NO") + << " extra_match=" << (extra_match ? "YES" : "NO") + << " len_match=" << (len_match ? "YES" : "NO") + << " c2pool_len=" << computed_hl.m_length + << " share_len=" << share.m_hash_link.m_length; + if (!state_match) { + LOG_WARNING << "[HASHLINK-CMP] c2pool_state=" << hex_fn(computed_hl.m_state.m_data); + LOG_WARNING << "[HASHLINK-CMP] share_state =" << hex_fn(share.m_hash_link.m_state.m_data); + } + if (!extra_match) { + LOG_WARNING << "[HASHLINK-CMP] c2pool_extra=" << hex_fn(computed_hl.m_extra_data.m_data) + << " (" << computed_hl.m_extra_data.m_data.size() << " bytes)"; + LOG_WARNING << "[HASHLINK-CMP] share_extra =" << hex_fn(share.m_hash_link.m_extra_data.m_data) + << " (" << share.m_hash_link.m_extra_data.m_data.size() << " bytes)"; + } + // Also dump prefix length and the last 60 bytes for comparison + LOG_WARNING << "[HASHLINK-CMP] prefix_len=" << prefix_len + << " gbr_len=" << gbr.size() + << " prefix_tail=" << hex_fn(std::vector( + prefix.end() - std::min(prefix.size(), size_t(60)), prefix.end())); + } + } + + // One-time full coinbase hex dump for cross-implementation debugging + { + static int coinbase_dump_count = 0; + if (coinbase_dump_count++ < 3) { + const char* HX = "0123456789abcdef"; + auto to_hex_fn = [&](const unsigned char* p, size_t len) { + std::string h; h.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { h += HX[p[i] >> 4]; h += HX[p[i] & 0xf]; } + return h; + }; + auto* cp = reinterpret_cast(tx.data()); + LOG_INFO << "[COINBASE-HEX] len=" << tx.size() + << " txid=" << txid.GetHex() + << " share=" << share.m_hash.GetHex().substr(0, 16) + << " hex=" << to_hex_fn(cp, tx.size()); + } + } + + if (dump_diag) + { + const char* HX = "0123456789abcdef"; + auto to_hex = [&](const unsigned char* p, size_t len) { + std::string h; h.reserve(len * 2); + for (size_t i = 0; i < len; ++i) { h += HX[p[i] >> 4]; h += HX[p[i] & 0xf]; } + return h; + }; + + auto* cp = reinterpret_cast(tx.data()); + LOG_WARNING << "[GENTX-DIAG] coinbase_len=" << tx.size() << " txid=" << txid.GetHex(); + LOG_WARNING << "[GENTX-DIAG] coinbase_hex=" << to_hex(cp, tx.size()); + LOG_WARNING << "[GENTX-DIAG] pplns_outputs=" << payout_outputs.size() + << " donation_amount=" << donation_amount + << " n_outs=" << n_outs + << " has_segwit=" << has_segwit; + LOG_WARNING << "[GENTX-DIAG] total_weight=" << total_weight.GetHex() + << " total_don_weight=" << total_donation_weight.GetHex(); + for (size_t i = 0; i < payout_outputs.size(); ++i) { + auto& [s, a] = payout_outputs[i]; + LOG_WARNING << "[GENTX-DIAG] payout[" << i << "] amount=" << a + << " script=" << to_hex(s.data(), s.size()); + } + // First 5 + last 5 shares in PPLNS walk (for cross-impl comparison) + if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) { + auto cl = std::min(tracker.chain.get_height(share.m_prev_hash), + static_cast(PoolConfig::real_chain_length())); + LOG_WARNING << "[GENTX-DIAG] PPLNS walk_count=" << cl + << " total_weight=" << total_weight.GetHex() + << " total_don_weight=" << total_donation_weight.GetHex() + << " n_addrs=" << weights.size(); + // First 5 shares + auto wv = tracker.chain.get_chain(share.m_prev_hash, std::min(cl, int32_t(5))); + int si = 0; + for (auto [h, d] : wv) { + auto hash_copy = h; // Apple Clang: structured bindings can't be captured in lambdas + d.share.invoke([&, hash_copy](auto* obj) { + auto att = chain::target_to_average_attempts(chain::bits_to_target(obj->m_bits)); + auto sc = get_share_script(obj); + LOG_WARNING << "[GENTX-DIAG] walk[" << si << "] hash=" << hash_copy.ToString().substr(0,16) + << " bits=0x" << std::hex << obj->m_bits + << " max_bits=0x" << obj->m_max_bits << std::dec + << " att=" << att.GetHex() + << " don=" << obj->m_donation + << " script=" << to_hex(sc.data(), sc.size()); + }); + ++si; + } + // Last 5 shares in walk (tail of PPLNS window) + if (cl > 10) { + auto full_view = tracker.chain.get_chain(share.m_prev_hash, cl); + int si2 = 0; + for (auto [h, d] : full_view) { + if (si2 >= cl - 5) { + auto hash_copy = h; // Apple Clang: structured bindings can't be captured + d.share.invoke([&, hash_copy](auto* obj) { + auto att = chain::target_to_average_attempts(chain::bits_to_target(obj->m_bits)); + auto sc = get_share_script(obj); + LOG_WARNING << "[GENTX-DIAG] walk_tail[" << si2 << "/" << cl << "] hash=" << hash_copy.ToString().substr(0,16) + << " bits=0x" << std::hex << obj->m_bits + << " max_bits=0x" << obj->m_max_bits << std::dec + << " att=" << att.GetHex() + << " don=" << obj->m_donation + << " script=" << to_hex(sc.data(), sc.size()); + }); + } + ++si2; + } + LOG_WARNING << "[GENTX-DIAG] walk iterated " << si2 << " shares (expected " << cl << ")"; + } + } + } + + { + auto gst_t3 = std::chrono::steady_clock::now(); + static int64_t sp = 0, sa = 0, sc = 0, sn = 0; + sp += std::chrono::duration_cast(gst_t1 - gst_t0).count(); + sa += std::chrono::duration_cast(gst_t2 - gst_t1).count(); + sc += std::chrono::duration_cast(gst_t3 - gst_t2).count(); + ++sn; + if (sn % 50 == 0) + LOG_INFO << "[GST-SPLIT] pplns=" << (sp/sn) << "us amounts=" << (sa/sn) + << "us coinbase=" << (sc/sn) << "us count=" << sn; + } + return txid; +} + +// ============================================================================ +// verify_merged_coinbase_commitment() +// +// Full 7-step chain verification of merged coinbase (Python data.py:329-458). +// Ensures the merged coinbase committed in the share actually matches the +// canonical PPLNS construction. Without this, a node could commit valid +// m_merged_payout_hash (weights match) but build a DOGE coinbase that pays +// differently — the merkle proof would be for the real (malicious) coinbase. +// +// Verification chain: +// 1. Re-derive canonical DOGE coinbase from PPLNS weights +// 2. canonical_txid = hash256(canonical_coinbase) +// 3. check_merkle_link(canonical_txid, coinbase_merkle_link) == header.merkle_root +// 4. hash256(header) == doge_block_hash +// 5. doge_block_hash matches aux_merkle_root in LTC coinbase mm_data +// +// Returns empty string on success, error message on failure. +// ============================================================================ +template +std::string verify_merged_coinbase_commitment( + const ShareT& share, TrackerT& tracker) +{ + if constexpr (ShareT::version < 36) + return {}; + if constexpr (!requires { share.m_merged_coinbase_info; }) + return {}; + + // No merged coinbase info → nothing to verify + if (share.m_merged_coinbase_info.empty()) + return {}; + + // Need enough chain history for reliable PPLNS verification + if (share.m_prev_hash.IsNull() || !tracker.chain.contains(share.m_prev_hash)) + return {}; + auto height = tracker.chain.get_height(share.m_prev_hash); + if (height < static_cast(PoolConfig::real_chain_length())) + return {}; // Insufficient depth — skip (match Python behavior) + + auto block_target = chain::bits_to_target(share.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * 65535 * PoolConfig::SPREAD; + int32_t chain_len = std::min(height, + static_cast(PoolConfig::real_chain_length())); + + // Parse mm_data from LTC coinbase scriptSig + const auto& coinbase = share.m_coinbase.m_data; + static const uint8_t MM_MAGIC[] = {0xfa, 0xbe, 0x6d, 0x6d}; + auto mm_pos = std::search(coinbase.begin(), coinbase.end(), + std::begin(MM_MAGIC), std::end(MM_MAGIC)); + if (mm_pos == coinbase.end()) { + return "merged_coinbase_info present but no mm_data marker in coinbase scriptSig"; + } + size_t mm_offset = std::distance(coinbase.begin(), mm_pos) + 4; + if (coinbase.size() - mm_offset < 40) + return "mm_data too short in coinbase scriptSig"; + + // aux_merkle_root: 32 bytes big-endian + uint256 aux_merkle_root; + { + const uint8_t* p = coinbase.data() + mm_offset; + // MM root is stored big-endian in the coinbase — reverse for internal uint256 + uint8_t* dst = reinterpret_cast(aux_merkle_root.begin()); + for (int i = 31; i >= 0; --i) + dst[i] = *p++; + } + uint32_t aux_size = 0; + { + const uint8_t* p = coinbase.data() + mm_offset + 32; + aux_size = p[0] | (p[1] << 8) | (p[2] << 16) | (p[3] << 24); + } + + // Get finder script for canonical coinbase construction + auto finder_script = get_share_script(&share); + auto finder_merged = normalize_script_for_merged(finder_script); + + for (const auto& info : share.m_merged_coinbase_info) { + uint32_t chain_id = info.m_chain_id; + + // Step 1: Get PPLNS weights for this merged chain + auto mw = tracker.get_merged_cumulative_weights( + share.m_prev_hash, chain_len, max_weight, chain_id); + + if (mw.weights.empty() || mw.total_weight.IsNull()) + continue; // No V36 shares → can't verify + + // Build payout list (same logic as payout_provider) + auto donation_script = PoolConfig::get_donation_script(36); + uint64_t coinbase_value = info.m_coinbase_value; + + // Convert weights to integer payouts + std::map, uint64_t> output_amounts; + uint64_t total_distributed = 0; + double total_d = mw.total_weight.IsNull() ? 0.0 + : static_cast(mw.total_weight.GetLow64()); + for (auto& [key, weight] : mw.weights) { + auto script = resolve_merged_payout_script(key); + if (script.empty()) continue; + double frac = weight.IsNull() ? 0.0 + : static_cast(weight.GetLow64()) / total_d; + uint64_t amount = static_cast(coinbase_value * frac); + if (amount > 0) { + output_amounts[script] += amount; + total_distributed += amount; + } + } + uint64_t donation_amount = coinbase_value - total_distributed; + // Donation >= 1 satoshi + if (donation_amount < 1 && !output_amounts.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto it = std::max_element(output_amounts.begin(), output_amounts.end(), + [](auto& a, auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + it->second -= 1; + donation_amount += 1; + } + + // Coinbase reconstruction removed: the merged_payout_hash check + // (Step 1 above) already verifies PPLNS weight correctness via the + // skip list. Reconstructing the full coinbase TX to verify merkle_root + // is redundant and fails on cross-implementation shares because: + // - scriptSig differs ("/c2pool/" vs "/P2Pool/") + // - OP_RETURN text differs + // - THE state_root presence differs + // - Float vs integer rounding in amount calculation + // All of these change txid → merkle_root without affecting PPLNS fairness. + + // Step 2: Verify header structure and extract block hash + if (info.m_block_header.m_data.size() < 80) + return "merged block header too short"; + + // Step 3: hash256(header) == doge_block_hash + auto hdr_span = std::span( + info.m_block_header.m_data.data(), 80); + uint256 doge_block_hash = Hash(hdr_span); + + // Step 6: Verify doge_block_hash against aux_merkle_root + if (aux_size == 1) { + // Single merged chain: aux_merkle_root == block_hash + if (doge_block_hash != aux_merkle_root) { + return "merged block hash " + doge_block_hash.GetHex() + + " != aux_merkle_root " + aux_merkle_root.GetHex() + + " for chain " + std::to_string(chain_id); + } + } + // Multi-chain: would need aux tree reconstruction (future) + } + + return {}; +} + +// ============================================================================ +// share_check() +// +// The check()-phase verification after init: +// 1. Timestamp not too far in the future +// 2. Version counting (stub — version upgrade enforcement) +// 3. Transaction hash resolution (for pre-v34 shares) +// 4. GenerateShareTransaction reconstruction & comparison +// 5. Merged payout hash + coinbase commitment verification +// +// Returns true if the share passes all checks. +// Throws on validation failure. +// ============================================================================ +template +bool share_check(const ShareT& share, + const uint256& share_hash, + const uint256& gentx_hash, + TrackerT& tracker) +{ + // 1. Timestamp check — must not be more than 600s in the future + auto now = static_cast( + std::chrono::duration_cast( + std::chrono::system_clock::now().time_since_epoch()).count()); + if (share.m_timestamp > now + 600) + throw std::invalid_argument("share timestamp is too far in the future"); + + // 2. Version counting — AutoRatchet upgrade enforcement + // p2pool data.py:1400-1414: version switch validation only at BOUNDARIES + // (when share.VERSION != parent.VERSION). Shares that match their parent's + // version are always valid — they were correct when created. + // Previous code rejected ALL v35 shares retroactively after 95% activation. + { + auto lookbehind = static_cast(PoolConfig::chain_length()); + if (tracker.chain.contains(share.m_hash) && + !share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) + { + // Get parent's share version + int64_t parent_version = 0; + tracker.chain.get_share(share.m_prev_hash).invoke([&](auto* obj) { + parent_version = std::remove_pointer_t::version; + }); + + // Only enforce version obsolescence at version BOUNDARIES + if (share.version != parent_version) + { + auto height = tracker.chain.get_height(share.m_hash); + if (height >= lookbehind) + { + if (tracker.should_punish_version(share.m_hash, share.version, lookbehind)) + throw std::invalid_argument("share version too old — newer version has 95%+ activation"); + } + } + } + } + + // 3. GenerateShareTransaction reconstruction & comparison + // Rebuild the expected coinbase from PPLNS weights and share fields, + // then verify its txid matches the gentx_hash from share_init_verify(). + // p2pool check(): v36_active = (self.VERSION >= 36) + // Use the SHARE's own version, not the tracker's runtime AutoRatchet state. + // This ensures V35 shares always verify with V35 PPLNS formula, even after + // the AutoRatchet transitions to ACTIVATED. + constexpr int64_t share_ver = ShareT::version; + bool v36_active = (share_ver >= 36); + if (!share.m_prev_hash.IsNull() && tracker.chain.contains(share.m_prev_hash)) + { + uint256 expected_gentx = generate_share_transaction(share, tracker, false, v36_active); + if (expected_gentx != gentx_hash) + { + LOG_WARNING << "GENTX-MISMATCH detail:" + << " share=" << share_hash.ToString().substr(0,16) + << " ver=" << share.version + << " subsidy=" << share.m_subsidy + << " donation=" << share.m_donation + << " prev=" << share.m_prev_hash.ToString().substr(0,16); + LOG_WARNING << " expected_gentx=" << expected_gentx.ToString().substr(0,32) + << " actual_gentx=" << gentx_hash.ToString().substr(0,32); + + auto chain_len = std::min( + tracker.chain.get_height(share.m_prev_hash), + static_cast(PoolConfig::real_chain_length())); + // V35: log grandparent + height-1 window info + uint256 gp_hash; + if (tracker.chain.contains(share.m_prev_hash)) + tracker.chain.get(share.m_prev_hash).share.invoke([&](auto* s){ gp_hash = s->m_prev_hash; }); + int32_t v35_walk = std::max(0, chain_len - 1); + LOG_WARNING << " PPLNS chain_len=" << chain_len + << " v35_walk=" << v35_walk + << " grandparent=" << (gp_hash.IsNull() ? "null" : gp_hash.GetHex().substr(0,16)) + << " prev_height=" << tracker.chain.get_height(share.m_prev_hash) + << " real_chain_length=" << PoolConfig::real_chain_length(); + + // Compare share target: what c2pool computes vs what the share has + { + static int target_diag = 0; + if (target_diag++ < 10) { + auto [cst_max_bits, cst_bits] = tracker.compute_share_target( + share.m_prev_hash, share.m_timestamp, uint256()); + auto share_aps = tracker.get_pool_attempts_per_second( + share.m_prev_hash, PoolConfig::TARGET_LOOKBEHIND, true); + LOG_WARNING << "[GENTX-TARGET] share_bits=0x" << std::hex << share.m_bits + << " share_max_bits=0x" << share.m_max_bits + << " c2pool_bits=0x" << cst_bits + << " c2pool_max_bits=0x" << cst_max_bits << std::dec + << " aps_min=" << share_aps.GetLow64() + << " prev=" << share.m_prev_hash.GetHex().substr(0,16); + + // APS walk component dump for cross-impl comparison + int32_t dist = PoolConfig::TARGET_LOOKBEHIND; + auto near_hash = share.m_prev_hash; + auto far_hash = near_hash; + int32_t actual_dist = 0; + for (int32_t i = 0; i < dist - 1; ++i) { + if (far_hash.IsNull() || !tracker.chain.contains(far_hash)) break; + auto* idx = tracker.chain.get_index(far_hash); + far_hash = idx ? idx->tail : uint256(); + ++actual_dist; + } + uint32_t near_ts = 0, far_ts = 0; + if (tracker.chain.contains(near_hash)) + tracker.chain.get_share(near_hash).invoke([&](auto* s){ near_ts = s->m_timestamp; }); + if (!far_hash.IsNull() && tracker.chain.contains(far_hash)) + tracker.chain.get_share(far_hash).invoke([&](auto* s){ far_ts = s->m_timestamp; }); + + // Compare skip-list far vs naive-walk far + auto skip_far = tracker.chain.get_nth_parent_via_skip( + share.m_prev_hash, dist - 1); + bool skip_match = (!far_hash.IsNull() && skip_far == far_hash); + + // Also get delta via TrackerView for comparison + uint288 delta_min_work; + int32_t delta_height = 0; + if (!skip_far.IsNull() && tracker.chain.contains(skip_far)) { + auto dv = tracker.chain.get_delta(share.m_prev_hash, skip_far); + delta_min_work = dv.min_work; + delta_height = dv.height; + } + + LOG_WARNING << "[GENTX-APS] near=" << near_hash.GetHex().substr(0,16) + << " far_naive=" << (far_hash.IsNull() ? "null" : far_hash.GetHex().substr(0,16)) + << " far_skip=" << (skip_far.IsNull() ? "null" : skip_far.GetHex().substr(0,16)) + << " skip_match=" << (skip_match ? "YES" : "NO") + << " actual_dist=" << actual_dist + << " expected_dist=" << (dist - 1) + << " timespan=" << (int32_t(near_ts) - int32_t(far_ts)) + << " near_ts=" << near_ts << " far_ts=" << far_ts + << " chain_height=" << tracker.chain.get_height(share.m_prev_hash) + << " delta_h=" << delta_height + << " delta_min_work=" << delta_min_work.GetLow64(); + + // Previous share's max_target (clamp reference) + uint256 prev_max_target; + tracker.chain.get_share(share.m_prev_hash).invoke([&](auto* obj) { + prev_max_target = chain::bits_to_target(obj->m_max_bits); + }); + LOG_WARNING << "[GENTX-CLAMP] prev_max_bits=0x" << std::hex + << chain::target_to_bits_upper_bound(prev_max_target) << std::dec + << " clamp_lo=" << chain::target_to_bits_upper_bound(prev_max_target * 9 / 10) + << " clamp_hi=" << chain::target_to_bits_upper_bound(prev_max_target * 11 / 10); + } + } + // --- Detailed diagnostics: re-run with full dump --- + static int s_diag_count = 0; + if (s_diag_count++ < 5) + { + LOG_WARNING << "[GENTX-DIAG] Re-running generate_share_transaction with full dump (v36_active=" << v36_active << "):"; + generate_share_transaction(share, tracker, true, v36_active); + + // Per-share PPLNS walk dump — compare with p2pool's [PARENT-PPLNS] output. + // Uses same parameters as generate_share_transaction's V36 path. + if (v36_active && !share.m_prev_hash.IsNull()) { + auto diag_chain_len = static_cast(PoolConfig::real_chain_length()); + LOG_WARNING << "[GENTX-DIAG] Per-share V36 PPLNS walk from prev=" + << share.m_prev_hash.GetHex().substr(0, 16) << ":"; + tracker.dump_v36_pplns_walk(share.m_prev_hash, diag_chain_len); + } + } + + throw std::invalid_argument("GenerateShareTransaction mismatch — coinbase does not match PPLNS payouts"); + } + } + + // 4. V36+ merged_payout_hash verification + // Verify that the share's committed merged PPLNS hash matches what we + // independently compute from the share chain. Without this, a malicious + // node could steal all merged chain (DOGE) rewards while appearing honest + // on the parent chain (LTC payouts are consensus-enforced via gentx above). + if constexpr (ShareT::version >= 36) + { + if constexpr (requires { share.m_merged_payout_hash; }) + { + if (!share.m_merged_payout_hash.IsNull() && + !share.m_prev_hash.IsNull() && + tracker.chain.contains(share.m_prev_hash)) + { + // Use BLOCK target (from header bits), not share target. + // p2pool: block_target = self.header['bits'].target + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto expected_hash = tracker.compute_merged_payout_hash( + share.m_prev_hash, block_target); + + if (!expected_hash.IsNull() && share.m_merged_payout_hash != expected_hash) + { + LOG_WARNING << "merged_payout_hash REJECT: claimed " + << share.m_merged_payout_hash.GetHex() + << " != expected " << expected_hash.GetHex(); + throw std::invalid_argument( + "merged_payout_hash mismatch — merged chain reward theft attempt"); + } + } + } + } + + // 5. V36+ merged coinbase commitment verification (7-step chain) + // Verifies the actual merged coinbase matches canonical PPLNS construction. + if constexpr (ShareT::version >= 36) + { + auto mcv_err = verify_merged_coinbase_commitment(share, tracker); + if (!mcv_err.empty()) + throw std::invalid_argument("merged coinbase verification: " + mcv_err); + } + + return true; +} + +// ============================================================================ +// verify_share() +// +// Combined entry point: runs both init-phase and check-phase verification. +// Returns the computed share hash. +// ============================================================================ +template +uint256 verify_share(const ShareT& share, TrackerT& tracker) +{ + auto vt0 = std::chrono::steady_clock::now(); + // share_init_verify computes gentx_hash along the way — we need it + // for the GenerateShareTransaction comparison in share_check. + // Skip scrypt PoW re-check when hash was already computed in Phase 1 + // (processing_shares offloads scrypt to m_verify_pool; no need to repeat). + uint256 hash = share_init_verify(share, share.m_hash.IsNull()); + auto vt1 = std::chrono::steady_clock::now(); + + // Verify recomputed hash matches stored hash (informational). + // For locally created shares the hash was set during create_local_share; + // a mismatch means the header reconstruction diverged (e.g., genesis PPLNS + // race). The share_check phase uses share.m_hash for chain lookups. + if (!share.m_hash.IsNull() && hash != share.m_hash) { + static int hash_mismatch_log = 0; + if (hash_mismatch_log++ < 10) + LOG_WARNING << "[verify_share] hash mismatch: recomputed=" + << hash.GetHex().substr(0, 16) + << " stored=" << share.m_hash.GetHex().substr(0, 16); + } + + // Re-derive gentx_hash for the check phase + constexpr int64_t ver = ShareT::version; + auto gentx_before_refhash = compute_gentx_before_refhash(ver); + + // Rebuild ref_hash + hash_link_data the same way init does + PackStream ref_stream; + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) + { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + { + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + + if constexpr (requires { share.m_address; }) + ref_stream << share.m_address; + else if constexpr (requires { share.m_pubkey_type; }) + { + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + } + else + ref_stream << share.m_pubkey_hash; + + if constexpr (ver >= 36) + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + else + ref_stream << share.m_subsidy; + + ref_stream << share.m_donation; + { + uint8_t si = static_cast(share.m_stale_info); + ref_stream << si; + } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + + if constexpr (requires { share.m_segwit_data; }) + { + if constexpr (ver >= bch::SEGWIT_ACTIVATION_VERSION) + { + // PossiblyNoneType: ALWAYS serialize (p2pool writes default when None) + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + } + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_addresses; }) + ref_stream << share.m_merged_addresses; + } + + if constexpr (ver < 34) + { + if constexpr (requires { share.m_tx_info; }) + ref_stream << share.m_tx_info; + } + + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_abswork; }) + ::Serialize(ref_stream, Using(share.m_abswork)); + } + else + { + ref_stream << share.m_abswork; + } + + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_merged_coinbase_info; }) + ref_stream << share.m_merged_coinbase_info; + if constexpr (requires { share.m_merged_payout_hash; }) + ref_stream << share.m_merged_payout_hash; + } + } + + // V36 ref_type includes message_data + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + ref_stream << share.m_message_data; + } + + auto ref_span = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span); + + std::vector hash_link_data; + { + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + hash_link_data.insert(hash_link_data.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t nonce = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&nonce); + hash_link_data.insert(hash_link_data.end(), p, p + 8); + uint32_t zero = 0; + auto* z = reinterpret_cast(&zero); + hash_link_data.insert(hash_link_data.end(), z, z + 4); + } + + uint256 gentx_hash = check_hash_link(share.m_hash_link, hash_link_data, gentx_before_refhash); + + // V36+: Validate message_data (reject shares with invalid encrypted messages) + if constexpr (ver >= 36) + { + if constexpr (requires { share.m_message_data; }) + { + auto err = validate_message_data(share.m_message_data.m_data); + if (!err.empty()) + throw std::invalid_argument("share " + err); + } + } + + share_check(share, hash, gentx_hash, tracker); + { + auto vt2 = std::chrono::steady_clock::now(); + auto init_us = std::chrono::duration_cast(vt1 - vt0).count(); + auto check_us = std::chrono::duration_cast(vt2 - vt1).count(); + static int64_t s_init = 0, s_check = 0, s_cnt = 0; + s_init += init_us; s_check += check_us; ++s_cnt; + if (s_cnt % 50 == 0) + LOG_INFO << "[VERIFY-SPLIT] init_avg=" << (s_init/s_cnt) + << "us check_avg=" << (s_check/s_cnt) << "us count=" << s_cnt; + } + return hash; +} + +// ============================================================================ +// create_local_share_v35() +// +// Constructs a PaddingBugfixShare (V35) from locally-generated block data. +// This is the V35 counterpart of create_local_share (V36). +// Key differences from V36: +// - Uses m_address (string) instead of m_pubkey_hash + m_pubkey_type +// - No merged_addresses, merged_coinbase_info, merged_payout_hash +// - Fixed uint64 subsidy (not VarInt) +// - Fixed uint128 abswork (not VarInt) +// - HashLinkType (no extra_data) instead of V36HashLinkType +// - DONATION_SCRIPT (P2PK 67b) instead of COMBINED_DONATION_SCRIPT (P2SH 23b) +// ============================================================================ +template +uint256 create_local_share_v35( + TrackerT& tracker, + const coin::SmallBlockHeaderType& min_header, + const BaseScript& coinbase, + uint64_t subsidy, + const uint256& prev_share, + const std::vector& merkle_branches, + const std::vector& payout_script, + uint16_t donation = 50, + StaleInfo stale_info = StaleInfo::none, + bool segwit_active = false, + const std::string& witness_commitment_hex = {}, + const std::vector& actual_coinbase_bytes = {}, + const uint256& witness_root = uint256(), + uint32_t override_max_bits = 0, + uint32_t override_bits = 0, + uint32_t frozen_absheight = 0, + uint128 frozen_abswork = uint128(), + uint256 frozen_far_share_hash = uint256(), + uint32_t frozen_timestamp = 0, + bool has_frozen = false, + const std::vector& frozen_merkle_branches = {}, + const uint256& frozen_witness_root = uint256(), + uint64_t desired_version = 36) +{ + PaddingBugfixShare share; + share.m_min_header = min_header; + share.m_coinbase = coinbase; + share.m_subsidy = subsidy; + share.m_prev_hash = prev_share; + share.m_donation = donation; + share.m_stale_info = stale_info; + share.m_desired_version = desired_version; + + // Timestamp: clip to at least previous_share.timestamp + 1 + share.m_timestamp = min_header.m_timestamp; + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + uint32_t prev_ts = 0; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_ts = prev->m_timestamp; + }); + if (share.m_timestamp <= prev_ts) + share.m_timestamp = prev_ts + 1; + } + + // Compute share target + auto desired_target = chain::bits_to_target(min_header.m_bits); + auto [share_max_bits, share_bits] = tracker.compute_share_target( + prev_share, share.m_timestamp, desired_target); + share.m_max_bits = share_max_bits; + share.m_bits = share_bits; + share.m_nonce = 0; + + // V35: address as string (VarStr), convert from payout_script + { + uint160 pubkey_hash; + uint8_t pubkey_type = 0; + if (payout_script.size() == 25 && payout_script[0] == 0x76) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 3, 20); + pubkey_type = 0; + } else if (payout_script.size() == 23 && payout_script[0] == 0xa9) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 2, 20); + pubkey_type = 2; + } else if (payout_script.size() == 22 && payout_script[0] == 0x00) { + std::memcpy(pubkey_hash.data(), payout_script.data() + 2, 20); + pubkey_type = 1; + } else if (payout_script.size() >= 20) { + std::memcpy(pubkey_hash.data(), payout_script.data(), 20); + } + std::string addr_str = pubkey_hash_to_address(pubkey_hash, pubkey_type); + share.m_address.m_data.assign(addr_str.begin(), addr_str.end()); + { + auto roundtrip = core::address_to_script(addr_str); + static const char* H = "0123456789abcdef"; + std::string ps_hex, rt_hex; + for (auto b : payout_script) { ps_hex += H[b>>4]; ps_hex += H[b&0xf]; } + for (auto b : roundtrip) { rt_hex += H[b>>4]; rt_hex += H[b&0xf]; } + LOG_INFO << "[V35-ADDR] type=" << (int)pubkey_type + << " addr=" << addr_str + << " payout_script=" << ps_hex + << " roundtrip=" << rt_hex + << " match=" << (payout_script == roundtrip ? "YES" : "NO"); + } + } + + // Chain position: absheight and abswork + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + share.m_absheight = prev->m_absheight + 1; + }); + { + auto current_attempts = chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)); + uint128 prev_abswork; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_abswork = prev->m_abswork; + }); + share.m_abswork = prev_abswork + uint128(current_attempts.GetLow64()); + } + // far_share_hash: 99th ancestor + { + auto [prev_height, last] = tracker.chain.get_height_and_last(prev_share); + if (last.IsNull() && prev_height < 99) { + share.m_far_share_hash = uint256(); + } else { + try { + share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99); + } catch (const std::exception&) { + share.m_far_share_hash = uint256(); + } + } + } + } else { + share.m_absheight = 1; + share.m_abswork = uint128(chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)).GetLow64()); + share.m_far_share_hash = uint256(); + } + + // Apply frozen fields from template time + if (has_frozen) { + share.m_absheight = frozen_absheight; + share.m_abswork = frozen_abswork; + share.m_far_share_hash = frozen_far_share_hash; + share.m_timestamp = frozen_timestamp; + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; + } + + // Random last_txout_nonce + share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ + (static_cast(min_header.m_nonce) << 32); + + // ref_merkle_link: empty + share.m_ref_merkle_link.m_branch.clear(); + share.m_ref_merkle_link.m_index = 0; + // merkle_link: from Stratum + share.m_merkle_link.m_branch = merkle_branches; + share.m_merkle_link.m_index = 0; + + // Segwit data + if (segwit_active && !witness_commitment_hex.empty()) + { + SegwitData sd; + sd.m_txid_merkle_link.m_branch = (has_frozen && !frozen_merkle_branches.empty()) + ? frozen_merkle_branches : merkle_branches; + sd.m_txid_merkle_link.m_index = 0; + // Priority: frozen > direct. The zero root (0x00..00) is VALID for + // coinbase-only blocks — p2pool uses it to compute the OP_RETURN + // witness commitment. Never treat IsNull() as "not set" here. + sd.m_wtxid_merkle_root = !frozen_witness_root.IsNull() + ? frozen_witness_root : witness_root; + share.m_segwit_data = sd; + } + + // --- Compute ref_hash (V35 format) --- + PackStream ref_stream; + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + // V35: address as VarStr + ref_stream << share.m_address; + // V35: subsidy as fixed uint64 + ref_stream << share.m_subsidy; + ref_stream << share.m_donation; + { uint8_t si = static_cast(share.m_stale_info); ref_stream << si; } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + // segwit_data + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + std::vector empty_branch; + ref_stream << empty_branch; + uint256 zero_root; + ref_stream << zero_root; + } + // V35: NO merged_addresses + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + // V35: abswork as fixed uint128 + ref_stream << share.m_abswork; + // V35: NO merged_coinbase_info, NO merged_payout_hash, NO message_data + + auto ref_span_v = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span_v); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Use ref_hash from actual coinbase if available + if (!actual_coinbase_bytes.empty() && actual_coinbase_bytes.size() > 44) { + uint256 coinbase_ref_hash; + std::memcpy(coinbase_ref_hash.data(), + actual_coinbase_bytes.data() + actual_coinbase_bytes.size() - 44, 32); + ref_hash = coinbase_ref_hash; + } + + // --- Derive hash_link (V35: HashLinkType, no extra_data) --- + auto gentx_before_refhash = compute_gentx_before_refhash(int64_t(35)); + + std::vector coinbase_bytes_for_hashlink; + if (!actual_coinbase_bytes.empty()) { + coinbase_bytes_for_hashlink = actual_coinbase_bytes; + } else { + // Fallback: reconstruct coinbase from V35 PPLNS walk + // V35: flat weights, grandparent start, height-1 window, 199/200 formula, finder fee + // Reference: p2pool data.py lines 878-965 + std::map, uint288> weights; + uint288 total_weight; + + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + // V35: start from grandparent (prev_share.prev_hash) + uint256 pplns_start; + tracker.chain.get(prev_share).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; + }); + + if (!pplns_start.IsNull()) { + auto height = tracker.chain.get_height(prev_share); + int32_t max_shares = std::max(0, std::min(height, + static_cast(PoolConfig::real_chain_length())) - 1); + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto desired_weight = chain::target_to_average_attempts(block_target) + * uint288(PoolConfig::SPREAD) * uint288(65535); + // Flat weight accumulation (not decayed) + auto result = tracker.get_cumulative_weights(pplns_start, max_shares, desired_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + } + } + + std::map, uint64_t> amounts; + if (!total_weight.IsNull()) { + for (auto& [script, weight] : weights) { + // V35: 99.5% to PPLNS — subsidy * 199 * weight / (200 * total_weight) + uint64_t amount = (uint288(subsidy) * uint288(199) * weight + / (uint288(200) * total_weight)).GetLow64(); + if (amount > 0) amounts[script] = amount; + } + } + // V35: add 0.5% finder fee to the share creator's payout script + amounts[payout_script] = (amounts.count(payout_script) ? amounts[payout_script] : 0) + + subsidy / 200; + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) sum_amounts += a; + // V35: no minimum donation enforcement (unlike v36) + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (payout_outputs.size() > 4000) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); + + PackStream gentx; + { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } + { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } + { uint256 z; gentx << z; } + { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } + gentx << share.m_coinbase; + { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } + size_t n_outs = payout_outputs.size() + 1 + 1; + bool has_segwit_fb = share.m_segwit_data.has_value(); + if (has_segwit_fb) n_outs += 1; + if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } + else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } + auto write_txout = [&](uint64_t value, const std::vector& script) { + gentx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; bs.m_data = script; gentx << bs; + }; + if (has_segwit_fb) { + auto& sd = share.m_segwit_data.value(); + std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto cb = commitment.GetChars(); + wscript.insert(wscript.end(), cb.begin(), cb.end()); + write_txout(0, wscript); + } + for (auto& [script, amount] : payout_outputs) write_txout(amount, script); + // V35: use pre-V36 DONATION_SCRIPT + write_txout(donation_amount, PoolConfig::get_donation_script(int64_t(35))); + { std::vector op; op.push_back(0x6a); op.push_back(0x28); + op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op.insert(op.end(), p, p + 8); write_txout(0, op); } + { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } + + coinbase_bytes_for_hashlink.assign( + reinterpret_cast(gentx.data()), + reinterpret_cast(gentx.data()) + gentx.size()); + } + + // Compute hash_link (V35: HashLinkType) + constexpr size_t suffix_len = 32 + 8 + 4; + if (coinbase_bytes_for_hashlink.size() > suffix_len) { + std::vector prefix( + coinbase_bytes_for_hashlink.begin(), coinbase_bytes_for_hashlink.end() - suffix_len); + share.m_hash_link = prefix_to_hash_link_v35(prefix, gentx_before_refhash); + + size_t nonce_offset = coinbase_bytes_for_hashlink.size() - 4 - 8; + uint64_t extracted_nonce = 0; + std::memcpy(&extracted_nonce, coinbase_bytes_for_hashlink.data() + nonce_offset, 8); + share.m_last_txout_nonce = extracted_nonce; + } + + // --- Compute share hash (block header double-SHA256) --- + PackStream header_stream; + { uint32_t v = static_cast(min_header.m_version); + header_stream << v; } + header_stream << min_header.m_previous_block; + + uint256 gentx_hash_for_header; + if (!actual_coinbase_bytes.empty()) { + auto actual_span = std::span( + actual_coinbase_bytes.data(), actual_coinbase_bytes.size()); + gentx_hash_for_header = Hash(actual_span); + } else { + auto cb_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + gentx_hash_for_header = Hash(cb_span); + } + uint256 merkle_root = check_merkle_link(gentx_hash_for_header, share.m_merkle_link); + + header_stream << merkle_root; + header_stream << min_header.m_timestamp; + header_stream << min_header.m_bits; + header_stream << min_header.m_nonce; + + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + + uint256 share_hash = Hash(hdr_span); + share.m_hash = share_hash; + + // PoW check against share target — BTC pow_hash = SHA256d(header) = share_hash. + { + uint256 target = chain::bits_to_target(share.m_bits); + if (!target.IsNull()) { + uint256 pow_hash = share_hash; + + if (pow_hash > target) { + // Diagnostic so the silent-ZERO return path is visible. + // Bitaxe-class miners @ ~1.7 TH/s hit this on virtually + // every submission against BTC mainnet sharechain + // difficulty (~2e8), so without a log it looks like + // create_local_share is "broken" when in fact the share + // simply doesn't clear the actual share target. + static std::atomic miss_diag{0}; + if (miss_diag.fetch_add(1) < 20) { + LOG_INFO << "[V35-SHARE-MISS] pow=" << pow_hash.GetHex().substr(0, 16) + << " target=" << target.GetHex().substr(0, 16) + << " bits=0x" << std::hex << share.m_bits << std::dec + << " diff_share=" << chain::target_to_difficulty(target) + << " diff_pow=" << chain::target_to_difficulty(pow_hash) + << " absheight=" << share.m_absheight; + } + return uint256(); // didn't meet share target + } + LOG_INFO << "[Pool] V35 SHARE CREATED! pow=" << pow_hash.GetHex().substr(0, 16) + << " target=" << target.GetHex().substr(0, 16) + << " diff=" << chain::target_to_difficulty(target); + } + } + + // Add to tracker + auto* heap_share = new PaddingBugfixShare(share); + tracker.add(heap_share); + LOG_INFO << "create_local_share_v35: added share " << share_hash.GetHex() + << " height=" << share.m_absheight + << " prev=" << prev_share.GetHex().substr(0, 16) << "..."; + + return share_hash; +} + +// ============================================================================ +// create_local_share() +// +// Constructs a share (V35 PaddingBugfixShare or V36 MergedMiningShare) from +// locally-generated block data and adds it to the share tracker. +// Returns the share hash (block header double-SHA256). +// +// Parameters: +// tracker — the ShareTracker to insert the new share into +// min_header — parsed SmallBlockHeaderType from the found block +// coinbase — the p2pool coinbase scriptSig (BIP34 height + pool marker) +// subsidy — block reward (coinbasevalue) +// prev_share — previous best share hash from the tracker +// merkle_branches — Stratum merkle branches (coinbase txid → merkle root) +// payout_script — finder's scriptPubKey +// donation — donation bps (e.g. 50 = 0.5%) +// merged_addrs — optional merged mining addresses +// share_version — 35 or 36 (default 36) +// +// This builds the p2pool coinbase in the same format as +// generate_share_transaction() and computes the hash_link so remote peers +// can verify the share. +// ============================================================================ +template +uint256 create_local_share( + TrackerT& tracker, + const coin::SmallBlockHeaderType& min_header, + const BaseScript& coinbase, + uint64_t subsidy, + const uint256& prev_share, + const std::vector& merkle_branches, + const std::vector& payout_script, + uint16_t donation = 50, + const std::vector& merged_addrs = {}, + StaleInfo stale_info = StaleInfo::none, + bool segwit_active = false, + const std::string& witness_commitment_hex = {}, + const std::vector& message_data = {}, + const std::vector& actual_coinbase_bytes = {}, + const uint256& witness_root = uint256(), + uint32_t override_max_bits = 0, + uint32_t override_bits = 0, + // Frozen share fields from template time — when set, override computed values + // to ensure ref_hash matches the one embedded in the coinbase. + uint32_t frozen_absheight = 0, + uint128 frozen_abswork = uint128(), + uint256 frozen_far_share_hash = uint256(), + uint32_t frozen_timestamp = 0, + uint256 frozen_merged_payout_hash = uint256(), + bool has_frozen = false, + const std::vector& frozen_merkle_branches = {}, + const uint256& frozen_witness_root = uint256(), + const std::vector& frozen_merged_coinbase_info = {}, + int64_t share_version = 35, + uint64_t desired_version = 35) +{ + // V35 path: delegate to version-specific implementation + if (share_version <= 35) + return create_local_share_v35( + tracker, min_header, coinbase, subsidy, prev_share, merkle_branches, + payout_script, donation, stale_info, segwit_active, witness_commitment_hex, + actual_coinbase_bytes, witness_root, override_max_bits, override_bits, + frozen_absheight, frozen_abswork, frozen_far_share_hash, frozen_timestamp, + has_frozen, frozen_merkle_branches, frozen_witness_root, desired_version); + + MergedMiningShare share; + share.m_min_header = min_header; + share.m_coinbase = coinbase; + share.m_subsidy = subsidy; + share.m_prev_hash = prev_share; + share.m_donation = donation; + share.m_stale_info = stale_info; + share.m_desired_version = desired_version; + + // Timestamp: clip to at least previous_share.timestamp + 1 (matches Python) + share.m_timestamp = min_header.m_timestamp; + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + uint32_t prev_ts = 0; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_ts = prev->m_timestamp; + }); + if (share.m_timestamp <= prev_ts) + share.m_timestamp = prev_ts + 1; + } + + // Compute pool-level share target AFTER timestamp clipping (matches ref_hash_fn). + auto desired_target = chain::bits_to_target(min_header.m_bits); + auto [share_max_bits, share_bits] = tracker.compute_share_target( + prev_share, share.m_timestamp, desired_target); + share.m_max_bits = share_max_bits; + share.m_bits = share_bits; + + share.m_nonce = 0; // share commitment nonce (not block nonce) + share.m_merged_addresses = merged_addrs; + + // Diagnostic: confirm merged_addresses are populated for DOGE skiplist Tier 1 + { + LOG_INFO << "[create_local_share] merged_addresses=" << merged_addrs.size(); + for (const auto& entry : merged_addrs) { + auto to_hex = [](const std::vector& s) { + static const char* H = "0123456789abcdef"; + std::string r; for (auto b : s) { r += H[b>>4]; r += H[b&0xf]; } return r; + }; + LOG_INFO << "[create_local_share] chain_id=" << entry.m_chain_id + << " script=" << to_hex(entry.m_script.m_data); + } + } + + // Embed encrypted message_data (from create_message_data()) if provided + if (!message_data.empty()) + share.m_message_data.m_data = message_data; + + // Compute merged_payout_hash: deterministic hash of V36-only PPLNS + // weight distribution so peers can verify merged mining payouts. + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + auto block_target = chain::bits_to_target(min_header.m_bits); + share.m_merged_payout_hash = tracker.compute_merged_payout_hash( + prev_share, block_target); + } + + // Payout identity — extract pubkey_hash + pubkey_type from scriptPubKey. + // Must match p2pool V36: 0=P2PKH, 1=P2WPKH, 2=P2SH. + if (payout_script.size() >= 20) { + if (payout_script.size() == 25 && + payout_script[0] == 0x76 && payout_script[1] == 0xa9 && + payout_script[2] == 0x14 && payout_script[23] == 0x88 && + payout_script[24] == 0xac) { + // P2PKH: 76 a9 14 88 ac + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 3, 20); + share.m_pubkey_type = 0; + } else if (payout_script.size() == 23 && + payout_script[0] == 0xa9 && payout_script[1] == 0x14 && + payout_script[22] == 0x87) { + // P2SH: a9 14 87 + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 2, 20); + share.m_pubkey_type = 2; + } else if (payout_script.size() == 22 && + payout_script[0] == 0x00 && payout_script[1] == 0x14) { + // P2WPKH: 00 14 + // P2WPKH: store raw witness program bytes directly. + // No reversal — c2pool uses raw bytes throughout, unlike p2pool's + // IntType(160) LE integer convention. The wire serialization of + // uint160 preserves the byte order from memcpy. + std::memcpy(share.m_pubkey_hash.data(), payout_script.data() + 2, 20); + share.m_pubkey_type = 1; + } else { + // Fallback: store first 20 bytes as P2PKH + std::memcpy(share.m_pubkey_hash.data(), payout_script.data(), 20); + share.m_pubkey_type = 0; + } + } + + // Chain position: absheight and abswork from previous share + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) { + auto [prev_height, last] = tracker.chain.get_height_and_last(prev_share); + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + share.m_absheight = prev->m_absheight + 1; + }); + + // abswork: prev_abswork + target_to_average_attempts(THIS share's bits) + // Python: abswork = (prev.abswork + target_to_average_attempts(bits.target)) % 2^128 + { + auto current_attempts = chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)); + uint128 prev_abswork; + tracker.chain.get(prev_share).share.invoke([&](auto* prev) { + prev_abswork = prev->m_abswork; + }); + share.m_abswork = prev_abswork + uint128(current_attempts.GetLow64()); + } + + // far_share_hash: 99th ancestor (matches Python: get_nth_parent_hash(prev_hash, 99)) + if (last.IsNull() && prev_height < 99) { + // Chain is complete and shorter than 99 → None (zero) + share.m_far_share_hash = uint256(); + } else { + share.m_far_share_hash = tracker.chain.get_nth_parent_key(prev_share, 99); + } + } else { + // Genesis: p2pool always does (prev_absheight + 1), (prev_abswork + aps) + // With prev=None: absheight = 0 + 1 = 1, abswork = 0 + aps(bits) + share.m_absheight = 1; + share.m_abswork = uint128(chain::target_to_average_attempts( + chain::bits_to_target(share.m_bits)).GetLow64()); + share.m_far_share_hash = uint256(); + } + + // Override with frozen fields from template time (if available). + // These must match what ref_hash_fn computed when building the coinbase. + if (has_frozen) { + LOG_INFO << "[frozen] Applying: absheight=" << frozen_absheight + << " bits=" << std::hex << override_bits + << " max_bits=" << override_max_bits << std::dec + << " ts=" << frozen_timestamp; + share.m_absheight = frozen_absheight; + share.m_abswork = frozen_abswork; + share.m_far_share_hash = frozen_far_share_hash; + share.m_timestamp = frozen_timestamp; + share.m_merged_payout_hash = frozen_merged_payout_hash; + if (override_max_bits) share.m_max_bits = override_max_bits; + if (override_bits) share.m_bits = override_bits; + // Deserialize frozen merged_coinbase_info blob → vector + if (!frozen_merged_coinbase_info.empty()) { + try { + PackStream ps; + ps.write(std::span( + reinterpret_cast(frozen_merged_coinbase_info.data()), + frozen_merged_coinbase_info.size())); + ps >> share.m_merged_coinbase_info; + } catch (const std::exception& e) { + LOG_WARNING << "[frozen] Failed to deserialize merged_coinbase_info (" + << frozen_merged_coinbase_info.size() << " bytes): " << e.what(); + } + } + } + + // Random last_txout_nonce for OP_RETURN uniqueness + share.m_last_txout_nonce = static_cast(std::time(nullptr)) ^ + (static_cast(min_header.m_nonce) << 32); + + // --- Build the p2pool coinbase in the same format as generate_share_transaction --- + // This is needed to compute hash_link and to verify the share locally. + // The coinbase format is: version(4) + vin(1 input) + vout(outputs...) + locktime(4) + // + // For the hash_link, we need to split the coinbase at the ref_hash boundary: + // prefix = everything up to (and including) gentx_before_refhash + // suffix = ref_hash + last_txout_nonce + locktime (= hash_link_data) + // + // We compute generate_share_transaction's coinbase from the share fields, + // then extract the prefix and compute the hash_link. + + // ref_merkle_link: empty branch (ref_hash = hash_ref directly) + share.m_ref_merkle_link.m_branch.clear(); + share.m_ref_merkle_link.m_index = 0; + + // merkle_link: from Stratum merkle branches + share.m_merkle_link.m_branch = merkle_branches; + share.m_merkle_link.m_index = 0; + + // Populate segwit_data for V36 when segwit is active + if (segwit_active && !witness_commitment_hex.empty()) + { + SegwitData sd; + // txid_merkle_link: use FROZEN merkle branches from template time if available. + // The ref_hash was computed with these branches at template time. If new transactions + // arrived between template creation and share submission, the branches change → + // ref_hash mismatch → p2pool rejects the share as "PoW invalid". + sd.m_txid_merkle_link.m_branch = (has_frozen && !frozen_merkle_branches.empty()) + ? frozen_merkle_branches : merkle_branches; + sd.m_txid_merkle_link.m_index = 0; + // Debug: log only when frozen/current diverge (indicates the fix is active) + if (has_frozen && !frozen_merkle_branches.empty() && + frozen_merkle_branches.size() != merkle_branches.size()) { + LOG_INFO << "[segwit-freeze] branches changed: frozen=" + << frozen_merkle_branches.size() + << " current=" << merkle_branches.size(); + } + // wtxid_merkle_root: use frozen witness root if available. + // Priority: frozen > direct witness_root > zero (SegwitDataDefault). + // NEVER extract from witness_commitment_hex — that contains the + // p2pool commitment (Hash(root, nonce)), not the raw root. + // Storing it in m_wtxid_merkle_root causes generate_share_transaction + // to double-hash: Hash(Hash(root, nonce), nonce) → GENTX mismatch. + // Priority: frozen > direct. Zero root (0x00..00) is VALID — it's + // the correct witness root for coinbase-only blocks. p2pool uses it + // to compute SHA256d(0x00..00 || '[P2Pool]'*4) as the OP_RETURN commitment. + sd.m_wtxid_merkle_root = !frozen_witness_root.IsNull() + ? frozen_witness_root : witness_root; + share.m_segwit_data = sd; + } + + // --- Compute the ref_hash --- + PackStream ref_stream; + { + auto hex = PoolConfig::identifier_hex(); + for (size_t i = 0; i + 1 < hex.size(); i += 2) { + unsigned char byte = static_cast( + std::stoul(hex.substr(i, 2), nullptr, 16)); + ref_stream.write(std::span( + reinterpret_cast(&byte), 1)); + } + } + ref_stream << share.m_prev_hash; + ref_stream << share.m_coinbase; + ref_stream << share.m_nonce; + ref_stream << share.m_pubkey_hash; + ref_stream << share.m_pubkey_type; + ::Serialize(ref_stream, VarInt(share.m_subsidy)); + ref_stream << share.m_donation; + { uint8_t si = static_cast(share.m_stale_info); ref_stream << si; } + ::Serialize(ref_stream, VarInt(share.m_desired_version)); + // segwit_data: PossiblyNoneType in p2pool — ALWAYS serialize. + // When None, write default: {txid_merkle_link: {branch: [], index: 0}, wtxid_merkle_root: 0} + // = varint(0) [empty branch list] + uint256(0) [wtxid_merkle_root] = 33 bytes. + if (share.m_segwit_data.has_value()) { + ref_stream << share.m_segwit_data.value(); + } else { + // Write PossiblyNoneType default: empty branch list + zero wtxid_merkle_root + std::vector empty_branch; + ref_stream << empty_branch; // varint(0) + uint256 zero_root; + ref_stream << zero_root; // 32 zero bytes + } + ref_stream << share.m_merged_addresses; + ref_stream << share.m_far_share_hash; + ref_stream << share.m_max_bits; + ref_stream << share.m_bits; + ref_stream << share.m_timestamp; + ref_stream << share.m_absheight; + ::Serialize(ref_stream, Using(share.m_abswork)); + ref_stream << share.m_merged_coinbase_info; + ref_stream << share.m_merged_payout_hash; + // V36 ref_type includes message_data (empty BaseScript → varint(0) = 0x00) + ref_stream << share.m_message_data; + + auto ref_span_v = std::span( + reinterpret_cast(ref_stream.data()), ref_stream.size()); + uint256 hash_ref = Hash(ref_span_v); + uint256 ref_hash = check_merkle_link(hash_ref, share.m_ref_merkle_link); + + // Dump ref_stream for cross-impl comparison (always, for diagnostics) + { + if (!actual_coinbase_bytes.empty()) { + static const char* HX = "0123456789abcdef"; + std::string full_hex; + auto* rd = reinterpret_cast(ref_stream.data()); + for (size_t i = 0; i < ref_stream.size(); ++i) { full_hex += HX[rd[i]>>4]; full_hex += HX[rd[i]&0xf]; } + LOG_INFO << "[REF-HASH] ref_packed_len=" << ref_stream.size() + << " ref_hash=" << hash_ref.GetHex() + << " prev=" << share.m_prev_hash.GetHex().substr(0, 16) + << " abs=" << share.m_absheight + << " bits=" << std::hex << share.m_bits + << " maxbits=" << share.m_max_bits << std::dec; + LOG_INFO << "[REF-HASH-FULL] " << full_hex; + } + } + + // If we have actual coinbase bytes, extract the ref_hash from the coinbase. + // The coinbase has ref_hash embedded at position [len-44 : len-44+32]. + // This is the ref_hash computed at template time (by ref_hash_fn) — it + // uses the FROZEN tracker state, not the current state which may have changed. + // Using the coinbase's ref_hash ensures hash_link consistency. + // Always use ref_hash from the actual coinbase (frozen at template creation time). + // The recomputed ref_hash may differ because share chain state changed between + // template creation and share submission (absheight, far_share_hash, bits, etc.). + // This is the EXACT same approach as p2pool: the gentx is captured once in a + // closure and never re-derived. + if (!actual_coinbase_bytes.empty() && actual_coinbase_bytes.size() > 44) { + uint256 coinbase_ref_hash; + std::memcpy(coinbase_ref_hash.data(), + actual_coinbase_bytes.data() + actual_coinbase_bytes.size() - 44, 32); + ref_hash = coinbase_ref_hash; + } + + // Diagnostic: log ref_hash match/mismatch (first few only to avoid log spam) + { + static int ref_diag = 0; + bool match = (hash_ref == ref_hash); + if (!match || ref_diag < 3) { + LOG_INFO << "[ref_stream] total=" << ref_stream.size() + << " ref_hash_match=" << (match ? "YES" : "NO"); + ++ref_diag; + } + } + + // --- Derive hash_link from the actual mined coinbase --- + // Python p2pool captures the gentx at template creation time in a closure + // and NEVER re-computes PPLNS. We follow the same pattern: the coinbase + // bytes from refresh_work() are the single source of truth. + // + // When actual_coinbase_bytes is provided (normal mining path), we use it + // directly. This eliminates the race where the share chain changes between + // template creation and share submission, which caused GENTX mismatches + // and p2pool peer bans. + auto gentx_before_refhash = compute_gentx_before_refhash(int64_t(36)); + + std::vector coinbase_bytes_for_hashlink; + if (!actual_coinbase_bytes.empty()) { + // Use the actual mined coinbase — matches exactly what the miner solved. + coinbase_bytes_for_hashlink = actual_coinbase_bytes; + } else { + // Fallback (no actual bytes): must reconstruct from PPLNS. + // This path is only used in unit tests or if the caller doesn't + // provide actual_coinbase_bytes. + std::map, uint288> weights; + uint288 total_weight; + + if (!prev_share.IsNull() && tracker.chain.contains(prev_share)) + { + // Pass REAL_CHAIN_LENGTH — walk naturally stops at chain end. + auto chain_len = static_cast(PoolConfig::real_chain_length()); + auto block_target = chain::bits_to_target(share.m_min_header.m_bits); + auto max_weight = chain::target_to_average_attempts(block_target) + * PoolConfig::SPREAD * 65535; + auto result = tracker.get_v36_decayed_cumulative_weights(prev_share, chain_len, max_weight); + weights = std::move(result.weights); + total_weight = result.total_weight; + } + + std::map, uint64_t> amounts; + if (!total_weight.IsNull()) { + for (auto& [script, weight] : weights) { + uint64_t amount = (uint288(subsidy) * weight / total_weight).GetLow64(); + if (amount > 0) + amounts[script] = amount; + } + } + uint64_t sum_amounts = 0; + for (auto& [s, a] : amounts) sum_amounts += a; + uint64_t donation_amount = (subsidy > sum_amounts) ? (subsidy - sum_amounts) : 0; + if (donation_amount < 1 && subsidy > 0 && !amounts.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(amounts.begin(), amounts.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != amounts.end() && largest->second > 0) { + largest->second -= 1; sum_amounts -= 1; + donation_amount = subsidy - sum_amounts; + } + } + + std::vector, uint64_t>> payout_outputs( + amounts.begin(), amounts.end()); + std::sort(payout_outputs.begin(), payout_outputs.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (payout_outputs.size() > 4000) + payout_outputs.erase(payout_outputs.begin(), payout_outputs.end() - 4000); + + PackStream gentx; + { uint32_t v = 1; gentx.write(std::span(reinterpret_cast(&v), 4)); } + { unsigned char one = 1; gentx.write(std::span(reinterpret_cast(&one), 1)); } + { uint256 z; gentx << z; } + { uint32_t idx = 0xffffffff; gentx.write(std::span(reinterpret_cast(&idx), 4)); } + gentx << share.m_coinbase; + { uint32_t seq = 0xffffffff; gentx.write(std::span(reinterpret_cast(&seq), 4)); } + size_t n_outs = payout_outputs.size() + 1 + 1; + bool has_segwit_fb = share.m_segwit_data.has_value(); + if (has_segwit_fb) n_outs += 1; + if (n_outs < 253) { uint8_t cnt = (uint8_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 1)); } + else { uint8_t m = 0xfd; gentx.write(std::span(reinterpret_cast(&m), 1)); uint16_t cnt = (uint16_t)n_outs; gentx.write(std::span(reinterpret_cast(&cnt), 2)); } + auto write_txout = [&](uint64_t value, const std::vector& script) { + gentx.write(std::span(reinterpret_cast(&value), 8)); + BaseScript bs; bs.m_data = script; gentx << bs; + }; + if (has_segwit_fb) { + auto& sd = share.m_segwit_data.value(); + std::vector wscript = {0x6a, 0x24, 0xaa, 0x21, 0xa9, 0xed}; + uint256 commitment = compute_p2pool_witness_commitment(sd.m_wtxid_merkle_root); + auto cb = commitment.GetChars(); + wscript.insert(wscript.end(), cb.begin(), cb.end()); + write_txout(0, wscript); + } + for (auto& [script, amount] : payout_outputs) write_txout(amount, script); + write_txout(donation_amount, PoolConfig::get_donation_script(int64_t(36))); + { std::vector op; op.push_back(0x6a); op.push_back(0x28); + op.insert(op.end(), ref_hash.data(), ref_hash.data() + 32); + uint64_t n = share.m_last_txout_nonce; auto* p = reinterpret_cast(&n); + op.insert(op.end(), p, p + 8); write_txout(0, op); } + { uint32_t lt = 0; gentx.write(std::span(reinterpret_cast(<), 4)); } + + coinbase_bytes_for_hashlink.assign( + reinterpret_cast(gentx.data()), + reinterpret_cast(gentx.data()) + gentx.size()); + } + + // The split point: everything before ref_hash + last_txout_nonce + locktime + // = coinbase minus last (32 + 8 + 4) = 44 bytes + constexpr size_t suffix_len = 32 + 8 + 4; // ref_hash + last_txout_nonce + locktime + if (coinbase_bytes_for_hashlink.size() > suffix_len) { + std::vector prefix( + coinbase_bytes_for_hashlink.begin(), coinbase_bytes_for_hashlink.end() - suffix_len); + share.m_hash_link = prefix_to_hash_link(prefix, gentx_before_refhash); + + // Verify hash_link round-trip: does check_hash_link(hash_link, suffix) == Hash(full)? + { + static int hl_diag = 0; + if (hl_diag < 5) { + std::vector suffix( + coinbase_bytes_for_hashlink.end() - suffix_len, + coinbase_bytes_for_hashlink.end()); + uint256 hl_result = check_hash_link(share.m_hash_link, suffix, gentx_before_refhash); + auto full_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + uint256 direct_result = Hash(full_span); + bool match = (hl_result == direct_result); + + // Check if prefix ends with const_ending + size_t ce_len = gentx_before_refhash.size(); + bool ce_match = (prefix.size() >= ce_len) && + std::equal(gentx_before_refhash.begin(), gentx_before_refhash.end(), + prefix.end() - ce_len); + + LOG_INFO << "[hash_link-roundtrip] MATCH=" << (match ? "YES" : "NO") + << " CE_match=" << (ce_match ? "YES" : "NO") + << " prefix=" << prefix.size() + << " cb_total=" << coinbase_bytes_for_hashlink.size() + << " CE=" << ce_len; + + if (!match || !ce_match) { + LOG_WARNING << "[hash_link-roundtrip] MISMATCH! direct=" << direct_result.GetHex() + << " hashlink=" << hl_result.GetHex(); + // Dump boundary bytes for debugging + static const char* HX = "0123456789abcdef"; + if (prefix.size() >= ce_len) { + std::string pfx_tail, ce_hex; + for (size_t i = prefix.size() - ce_len; i < prefix.size(); ++i) { + pfx_tail += HX[prefix[i] >> 4]; + pfx_tail += HX[prefix[i] & 0xf]; + } + for (size_t i = 0; i < ce_len; ++i) { + ce_hex += HX[gentx_before_refhash[i] >> 4]; + ce_hex += HX[gentx_before_refhash[i] & 0xf]; + } + LOG_WARNING << "[hash_link-roundtrip] prefix_tail=" << pfx_tail; + LOG_WARNING << "[hash_link-roundtrip] expected_CE=" << ce_hex; + } + } + ++hl_diag; + } + } + + // Extract last_txout_nonce (8 bytes before locktime) + size_t nonce_offset = coinbase_bytes_for_hashlink.size() - 4 - 8; + uint64_t extracted_nonce = 0; + std::memcpy(&extracted_nonce, coinbase_bytes_for_hashlink.data() + nonce_offset, 8); + share.m_last_txout_nonce = extracted_nonce; + { + static int nonce_log = 0; + if (nonce_log++ < 5) { + static const char* HX = "0123456789abcdef"; + std::string nonce_hex; + for (int i = 0; i < 8; ++i) { + uint8_t b = coinbase_bytes_for_hashlink[nonce_offset + i]; + nonce_hex += HX[b>>4]; nonce_hex += HX[b&0xf]; + } + LOG_INFO << "[NONCE-EXTRACT] offset=" << nonce_offset + << " nonce_hex=" << nonce_hex + << " nonce_u64=0x" << std::hex << extracted_nonce << std::dec + << " cb_total=" << coinbase_bytes_for_hashlink.size() + << " src=" << (actual_coinbase_bytes.empty() ? "reconstructed" : "actual_mined"); + // Dump FULL actual coinbase hex for byte-by-byte comparison with p2pool + std::string full_cb_hex; + for (size_t i = 0; i < coinbase_bytes_for_hashlink.size(); ++i) { + full_cb_hex += HX[coinbase_bytes_for_hashlink[i]>>4]; + full_cb_hex += HX[coinbase_bytes_for_hashlink[i]&0xf]; + } + LOG_INFO << "[ACTUAL-CB] hex=" << full_cb_hex; + } + } + } + + // --- Compute share hash --- + // Build the full 80-byte block header and double-SHA256 it + PackStream header_stream; + { uint32_t v = static_cast(min_header.m_version); + header_stream << v; } + header_stream << min_header.m_previous_block; + + // Compute merkle root for the block header. + // The MINER hashed the coinbase with the REAL extranonce2, so we must use + // actual_coinbase_bytes for the merkle root in the block header. + uint256 gentx_hash_for_header; + if (!actual_coinbase_bytes.empty()) { + auto actual_span = std::span( + actual_coinbase_bytes.data(), actual_coinbase_bytes.size()); + gentx_hash_for_header = Hash(actual_span); + } else { + // Use the coinbase bytes we already computed for hash_link (either actual or reconstructed) + auto cb_span = std::span( + coinbase_bytes_for_hashlink.data(), coinbase_bytes_for_hashlink.size()); + gentx_hash_for_header = Hash(cb_span); + } + // For the BLOCK HEADER merkle root, always use the actual job merkle branches + // (share.m_merkle_link), NOT the frozen segwit_data.txid_merkle_link. + // The frozen branches are for the ref_hash/share_info serialization only. + // The block header must match the actual transactions in the template. + uint256 merkle_root = check_merkle_link(gentx_hash_for_header, share.m_merkle_link); + + header_stream << merkle_root; + header_stream << min_header.m_timestamp; + header_stream << min_header.m_bits; + header_stream << min_header.m_nonce; + + auto hdr_span = std::span( + reinterpret_cast(header_stream.data()), header_stream.size()); + + // Diagnostic: dump header for PoW debugging + { + static int diag_count = 0; + if (diag_count < 5 && !actual_coinbase_bytes.empty()) { + std::string hdr_hex; + static const char* HX = "0123456789abcdef"; + for (size_t i = 0; i < hdr_span.size(); ++i) { + hdr_hex += HX[hdr_span[i] >> 4]; + hdr_hex += HX[hdr_span[i] & 0xf]; + } + LOG_INFO << "[create_local_share-diag] header(80)=" << hdr_hex; + LOG_INFO << "[create_local_share-diag] gentx_hash=" << gentx_hash_for_header.GetHex(); + LOG_INFO << "[create_local_share-diag] merkle_root=" << merkle_root.GetHex(); + LOG_INFO << "[create_local_share-diag] prev_block=" << min_header.m_previous_block.GetHex(); + LOG_INFO << "[create_local_share-diag] nonce=" << min_header.m_nonce + << " timestamp=" << min_header.m_timestamp + << " bits=" << std::hex << min_header.m_bits << std::dec; + ++diag_count; + } + } + + uint256 share_hash = Hash(hdr_span); + + // Set the share's identity hash + share.m_hash = share_hash; + + // Self-validation: PoW check against share target. + // Also run share_init_verify to confirm peers will accept this share + // (same check they'll run when they receive it). + // BTC: pow_hash = SHA256d(header) = share_hash already computed. + { + uint256 target = chain::bits_to_target(share.m_bits); + if (!target.IsNull()) { + uint256 pow_hash = share_hash; + + if (pow_hash > target) { + // Expected: most stratum pseudoshares don't meet share target + return uint256(); + } + LOG_INFO << "[Pool] REAL SHARE CREATED! pow=" << pow_hash.GetHex().substr(0, 16) + << " target=" << target.GetHex().substr(0, 16) + << " diff=" << chain::target_to_difficulty(target); + // Dump raw 80-byte header for byte-level comparison with p2pool + { + static int hdr_dump = 0; + if (hdr_dump++ < 5) { + static const char* HX = "0123456789abcdef"; + std::string hex; + auto* rd = reinterpret_cast(hdr_span.data()); + for (size_t i = 0; i < hdr_span.size() && i < 80; ++i) { + hex += HX[rd[i]>>4]; hex += HX[rd[i]&0xf]; + } + LOG_INFO << "[HDR-DUMP] " << hex + << " hash=" << share.m_hash.GetHex().substr(0, 16); + } + } + // Log fields for comparison with p2pool's SHARE-REJECT + { + static int sl = 0; + if (sl++ < 5) { + LOG_INFO << "[SHARE-FIELDS] header_bits=" << std::hex << share.m_min_header.m_bits + << " share_bits=" << share.m_bits + << " max_bits=" << share.m_max_bits << std::dec + << " absheight=" << share.m_absheight + << " ts=" << share.m_timestamp + << " donation=" << share.m_donation + << " segwit=" << (share.m_segwit_data.has_value() ? "YES" : "NO") + << " prev=" << share.m_prev_hash.GetHex().substr(0, 16); + } + } + + // Cross-check: run the SAME verification that peers will run. + // If this fails, peers will reject the share as "PoW invalid". + // Cross-check: verify hash_link round-trip with the COINBASE ref_hash. + // The coinbase ref_hash was frozen at template time. If check_hash_link + // with this ref_hash produces the same gentx_hash as direct Hash(coinbase), + // peers will accept the share. + { + auto gentx_before_refhash_xc = compute_gentx_before_refhash(int64_t(36)); + + // Use the ref_hash from the coinbase (same as what hash_link was built with) + std::vector xc_data; + xc_data.insert(xc_data.end(), ref_hash.data(), ref_hash.data() + 32); + { uint64_t n = share.m_last_txout_nonce; + auto* p = reinterpret_cast(&n); + xc_data.insert(xc_data.end(), p, p + 8); } + { uint32_t z = 0; + auto* p = reinterpret_cast(&z); + xc_data.insert(xc_data.end(), p, p + 4); } + + uint256 xc_gentx = check_hash_link(share.m_hash_link, xc_data, gentx_before_refhash_xc); + + if (xc_gentx != gentx_hash_for_header) { + // Cross-check failure is diagnostic only — p2pool has no such check. + // The share was already constructed with consistent hash_link + coinbase. + // Mismatch here means gentx_before_refhash() doesn't match the frozen + // template (PPLNS changed between template and submission). This is + // expected during genesis or when chain state changes rapidly. + // The share is still valid — peers verify via their own check_hash_link. + static int xc_warn = 0; + if (xc_warn++ < 10) + LOG_WARNING << "[Pool] Cross-check mismatch (non-blocking)" + << " hl_gentx=" << xc_gentx.GetHex().substr(0, 16) + << " direct_gentx=" << gentx_hash_for_header.GetHex().substr(0, 16); + } else { + LOG_INFO << "[Pool] Cross-check PASSED"; + } + } + } + } + + // One-time hex dump of the share_info portion for wire format debugging + { + static int wire_dump = 0; + if (wire_dump++ < 1) { + // Pack just the share_info fields to see exact wire bytes + // Dump share_data fields manually for wire format comparison + { + PackStream sd_pack; + // Serialize share_data portion (same as share.hpp lines 155-190) + // prev_hash (uint256 = 32 bytes) + sd_pack << share.m_prev_hash; + // coinbase (VarStr) + sd_pack << share.m_coinbase; + // nonce (uint32) + sd_pack.write(std::span( + reinterpret_cast(&share.m_nonce), 4)); + auto sd_span = sd_pack.get_span(); + auto* sp = reinterpret_cast(sd_span.data()); + std::string sd_hex; + for (size_t i = 0; i < sd_span.size(); ++i) { + static const char* H = "0123456789abcdef"; + sd_hex += H[sp[i] >> 4]; sd_hex += H[sp[i] & 0xf]; + } + LOG_INFO << "[WIRE-DUMP] share_data_head(" << sd_span.size() + << "): " << sd_hex.substr(0, 160); + LOG_INFO << "[WIRE-DUMP] m_coinbase(" << share.m_coinbase.size() + << "): " << sd_hex.substr(64, std::min(size_t(100), sd_hex.size()-64)); + } + LOG_INFO << "[WIRE-DUMP] m_bits=0x" << std::hex << share.m_bits + << " m_max_bits=0x" << share.m_max_bits << std::dec + << " m_timestamp=" << share.m_timestamp + << " m_absheight=" << share.m_absheight + << " m_donation=" << share.m_donation + << " m_subsidy=" << share.m_subsidy; + // Dump the share_info fields as they would be serialized + PackStream info_pack; + // far_share_hash (PossiblyNoneType(0, IntType(256))) + if (share.m_far_share_hash.IsNull()) { + uint8_t z = 0; info_pack.write(std::span(reinterpret_cast(&z), 1)); + } else { + uint8_t one = 1; info_pack.write(std::span(reinterpret_cast(&one), 1)); + info_pack.write(std::span(reinterpret_cast(share.m_far_share_hash.data()), 32)); + } + // max_bits, bits, timestamp, absheight (4 bytes each, LE) + info_pack.write(std::span(reinterpret_cast(&share.m_max_bits), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_bits), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_timestamp), 4)); + info_pack.write(std::span(reinterpret_cast(&share.m_absheight), 4)); + auto info_span = info_pack.get_span(); + std::string info_hex; + auto* ip = reinterpret_cast(info_span.data()); + for (size_t i = 0; i < info_span.size(); ++i) { + static const char* H = "0123456789abcdef"; + info_hex += H[ip[i] >> 4]; info_hex += H[ip[i] & 0xf]; + } + LOG_INFO << "[WIRE-DUMP] share_info_tail=" << info_hex; + } + } + + // Add to tracker (heap-allocate; ShareChain takes ownership via raw pointer) + auto* heap_share = new MergedMiningShare(share); + tracker.add(heap_share); + LOG_INFO << "create_local_share: added share " << share_hash.GetHex() + << " height=" << share.m_absheight + << " prev=" << prev_share.GetHex().substr(0, 16) << "..."; + + // Cross-check: call generate_share_transaction on the share we just created + // to verify p2pool would produce the same gentx_hash. + { + static int xcheck_count = 0; + if (true) { // Always cross-check (was: xcheck_count < 5) + uint256 verify_hash = generate_share_transaction(*heap_share, tracker, true, (MergedMiningShare::version >= 36)); + bool xcheck_ok = (verify_hash == gentx_hash_for_header); + if (xcheck_ok) { + LOG_INFO << "[Pool] Cross-check PASSED"; + } else { + LOG_WARNING << "[Pool] Cross-check FAILED! mined_gentx=" << gentx_hash_for_header.GetHex() + << " verify_gentx=" << verify_hash.GetHex(); + // Dump actual mined coinbase hex for byte-level comparison + if (!actual_coinbase_bytes.empty()) { + static const char* H = "0123456789abcdef"; + std::string mined_hex; + mined_hex.reserve(actual_coinbase_bytes.size() * 2); + for (unsigned char b : actual_coinbase_bytes) { mined_hex += H[b>>4]; mined_hex += H[b&0xf]; } + LOG_WARNING << "[XCHECK-MINED] len=" << actual_coinbase_bytes.size() + << " hex=" << mined_hex; + } + } + ++xcheck_count; + } + } + + return share_hash; +} + +} // namespace bch diff --git a/src/impl/bch/share_messages.hpp b/src/impl/bch/share_messages.hpp new file mode 100644 index 000000000..701c34901 --- /dev/null +++ b/src/impl/bch/share_messages.hpp @@ -0,0 +1,579 @@ +// share_messages.hpp — V36 share-embedded messaging: decryption + validation +// +// Port of p2pool/share_messages.py (consensus-critical portions). +// Messages are embedded in V36 shares' message_data field, included +// in the ref_hash computation (PoW-protected). +// +// Validation path for incoming shares: +// 1. If message_data is empty → valid (no messages) +// 2. Decrypt outer envelope using DONATION_AUTHORITY_PUBKEYS +// 3. If decryption fails (MAC mismatch) → REJECT share +// 4. Parse inner envelope: version, flags, msg_count, messages +// 5. If inner envelope is malformed or empty → REJECT share +// 6. Verify ECDSA signatures via libsecp256k1 + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace bch { + +// ============================================================================ +// Authority public keys (compressed secp256k1) +// From COMBINED_DONATION_REDEEM_SCRIPT: +// OP_1 PUSH33 PUSH33 OP_2 OP_CHECKMULTISIG +// ============================================================================ + +using AuthorityPubkey = std::array; + +inline const AuthorityPubkey& DONATION_PUBKEY_FORRESTV() +{ + static const AuthorityPubkey k = { + 0x03, 0xff, 0xd0, 0x3d, 0xe4, 0x4a, 0x6e, 0x11, + 0xb9, 0x91, 0x7f, 0x3a, 0x29, 0xf9, 0x44, 0x32, + 0x83, 0xd9, 0x87, 0x1c, 0x9d, 0x74, 0x3e, 0xf3, + 0x0d, 0x5e, 0xdd, 0xcd, 0x37, 0x09, 0x4b, 0x64, + 0xd1 + }; + return k; +} + +inline const AuthorityPubkey& DONATION_PUBKEY_MAINTAINER() +{ + static const AuthorityPubkey k = { + 0x02, 0xfe, 0x65, 0x78, 0xf8, 0x02, 0x1a, 0x7d, + 0x46, 0x67, 0x87, 0x82, 0x7b, 0x3f, 0x26, 0x43, + 0x7a, 0xef, 0x88, 0x27, 0x9e, 0xf3, 0x80, 0xaf, + 0x32, 0x6f, 0x87, 0xec, 0x36, 0x26, 0x33, 0x29, + 0x3a + }; + return k; +} + +inline const std::array& DONATION_AUTHORITY_PUBKEYS() +{ + static const std::array keys = { + &DONATION_PUBKEY_FORRESTV(), + &DONATION_PUBKEY_MAINTAINER(), + }; + return keys; +} + +// ============================================================================ +// Constants +// ============================================================================ + +// Message types +constexpr uint8_t MSG_NODE_STATUS = 0x01; +constexpr uint8_t MSG_MINER_MESSAGE = 0x02; +constexpr uint8_t MSG_POOL_ANNOUNCE = 0x03; +constexpr uint8_t MSG_VERSION_SIGNAL = 0x04; +constexpr uint8_t MSG_MERGED_STATUS = 0x05; +constexpr uint8_t MSG_EMERGENCY = 0x10; +constexpr uint8_t MSG_TRANSITION_SIGNAL = 0x20; + +// Flags +constexpr uint8_t FLAG_HAS_SIGNATURE = 0x01; +constexpr uint8_t FLAG_BROADCAST = 0x02; +constexpr uint8_t FLAG_PERSISTENT = 0x04; +constexpr uint8_t FLAG_PROTOCOL_AUTHORITY = 0x08; + +// Limits +constexpr size_t MAX_MESSAGE_PAYLOAD = 220; +constexpr size_t MAX_MESSAGES_PER_SHARE = 3; +constexpr size_t MAX_TOTAL_MESSAGE_BYTES = 512; + +// Encryption envelope +constexpr uint8_t ENCRYPTED_ENVELOPE_VERSION = 0x01; +constexpr size_t ENCRYPTION_NONCE_SIZE = 16; +constexpr size_t ENCRYPTION_MAC_SIZE = 32; +constexpr size_t ENCRYPTION_HEADER_SIZE = 1 + ENCRYPTION_NONCE_SIZE + ENCRYPTION_MAC_SIZE; // 49 + +// ============================================================================ +// Parsed message (lightweight — just what we need for validation) +// ============================================================================ + +struct ShareMessage +{ + uint8_t msg_type{0}; + uint8_t flags{0}; + uint8_t wire_flags{0}; // original flags for hash + uint32_t timestamp{0}; + std::vector payload; + std::array signing_id{}; + std::vector signature; + + bool has_signature() const { return (flags & FLAG_HAS_SIGNATURE) != 0; } + + // Unpack one message from data at offset. Returns new offset or nullopt on failure. + static std::optional unpack(const unsigned char* data, size_t len, + size_t offset, ShareMessage& out) + { + if (len - offset < 8) return std::nullopt; + + std::memcpy(&out.msg_type, data + offset, 1); + std::memcpy(&out.wire_flags, data + offset + 1, 1); + out.flags = out.wire_flags & ~FLAG_PROTOCOL_AUTHORITY; + + uint32_t ts; + std::memcpy(&ts, data + offset + 2, 4); // LE + out.timestamp = ts; + + uint16_t payload_len; + std::memcpy(&payload_len, data + offset + 6, 2); // LE + offset += 8; + + if (payload_len > MAX_MESSAGE_PAYLOAD) return std::nullopt; + if (len - offset < payload_len) return std::nullopt; + out.payload.assign(data + offset, data + offset + payload_len); + offset += payload_len; + + // signing_id (20 bytes) + if (len - offset < 20) return std::nullopt; + std::memcpy(out.signing_id.data(), data + offset, 20); + offset += 20; + + // sig_len + signature + if (len - offset < 1) return std::nullopt; + uint8_t sig_len = data[offset++]; + if (len - offset < sig_len) return std::nullopt; + out.signature.assign(data + offset, data + offset + sig_len); + offset += sig_len; + + return offset; + } +}; + +// ============================================================================ +// secp256k1 context (singleton, thread-safe after init) +// ============================================================================ + +inline const secp256k1_context* get_secp256k1_context() +{ + static const secp256k1_context* ctx = + secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); + return ctx; +} + +// ============================================================================ +// Crypto helpers +// ============================================================================ + +// HMAC-SHA256(key, data) → 32 bytes +inline std::array hmac_sha256( + const unsigned char* key, size_t keylen, + const unsigned char* data, size_t datalen) +{ + std::array result; + CHMAC_SHA256(key, keylen).Write(data, datalen).Finalize(result.data()); + return result; +} + +// Counter-mode SHA256 stream generator +inline void generate_stream(const unsigned char* enc_key, + unsigned char* out, size_t length) +{ + size_t produced = 0; + uint32_t counter = 0; + while (produced < length) + { + unsigned char block_input[36]; // 32 (key) + 4 (counter LE) + std::memcpy(block_input, enc_key, 32); + std::memcpy(block_input + 32, &counter, 4); // LE on little-endian host + + unsigned char block[32]; + CSHA256().Write(block_input, 36).Finalize(block); + + size_t copy = std::min(32, length - produced); + std::memcpy(out + produced, block, copy); + produced += copy; + ++counter; + } +} + +// Double-SHA256 of message content for ECDSA signing/verification. +// Matches Python: SHA256(SHA256(pack(' compute_message_hash( + uint8_t msg_type, uint8_t wire_flags, uint32_t timestamp, + const unsigned char* payload, size_t payload_len) +{ + unsigned char header[6]; + header[0] = msg_type; + header[1] = wire_flags; + std::memcpy(header + 2, ×tamp, 4); // LE + + unsigned char first[32]; + CSHA256().Write(header, 6).Write(payload, payload_len).Finalize(first); + + std::array result; + CSHA256().Write(first, 32).Finalize(result.data()); + return result; +} + +// Verify ECDSA signature against a compressed secp256k1 public key. +// pubkey_compressed: 33 bytes (0x02/0x03 prefix) +// msghash32: 32-byte double-SHA256 (from compute_message_hash) +// sig_der: DER-encoded ECDSA signature +// Returns true if signature is valid. +inline bool ecdsa_verify(const unsigned char* pubkey_compressed, size_t pubkey_len, + const unsigned char* msghash32, + const unsigned char* sig_der, size_t sig_len) +{ + const auto* ctx = get_secp256k1_context(); + + secp256k1_pubkey pubkey; + if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, pubkey_compressed, pubkey_len)) + return false; + + secp256k1_ecdsa_signature sig; + if (!secp256k1_ecdsa_signature_parse_der(ctx, &sig, sig_der, sig_len)) + return false; + + // Normalize to lower-S form (secp256k1_ecdsa_verify requires this) + secp256k1_ecdsa_signature_normalize(ctx, &sig, &sig); + + return secp256k1_ecdsa_verify(ctx, &sig, msghash32, &pubkey) == 1; +} + +// ============================================================================ +// Decryption result +// ============================================================================ + +struct DecryptResult +{ + std::vector inner_data; + const AuthorityPubkey* authority_pubkey{nullptr}; // which key succeeded +}; + +// Decrypt encrypted envelope. Returns nullopt if all authority keys fail MAC. +inline std::optional decrypt_message_data( + const unsigned char* data, size_t len) +{ + if (len < ENCRYPTION_HEADER_SIZE + 1) return std::nullopt; + + if (data[0] != ENCRYPTED_ENVELOPE_VERSION) return std::nullopt; + + const unsigned char* nonce = data + 1; + const unsigned char* mac_recv = data + 1 + ENCRYPTION_NONCE_SIZE; + const unsigned char* ciphertext = data + ENCRYPTION_HEADER_SIZE; + size_t ct_len = len - ENCRYPTION_HEADER_SIZE; + + if (ct_len == 0) return std::nullopt; + + for (const auto* pubkey_ptr : DONATION_AUTHORITY_PUBKEYS()) + { + // enc_key = HMAC-SHA256(pubkey, nonce) + auto enc_key = hmac_sha256( + pubkey_ptr->data(), pubkey_ptr->size(), nonce, ENCRYPTION_NONCE_SIZE); + + // MAC = HMAC-SHA256(enc_key, ciphertext) + auto mac_computed = hmac_sha256( + enc_key.data(), enc_key.size(), ciphertext, ct_len); + + // Constant-time compare + unsigned char diff = 0; + for (size_t i = 0; i < 32; ++i) diff |= mac_computed[i] ^ mac_recv[i]; + if (diff != 0) continue; // Wrong key + + // MAC matches — decrypt (XOR with stream) + DecryptResult result; + result.inner_data.resize(ct_len); + generate_stream(enc_key.data(), result.inner_data.data(), ct_len); + for (size_t i = 0; i < ct_len; ++i) + result.inner_data[i] ^= ciphertext[i]; + result.authority_pubkey = pubkey_ptr; + return result; + } + + return std::nullopt; +} + +// ============================================================================ +// Unpack result +// ============================================================================ + +struct UnpackResult +{ + std::vector messages; + const AuthorityPubkey* authority_pubkey{nullptr}; + bool decrypted{false}; +}; + +// Main entry point: decrypt + parse inner envelope. +// Returns empty result on failure; caller checks fields. +inline UnpackResult unpack_share_messages(const unsigned char* data, size_t len) +{ + UnpackResult result; + + if (!data || len < ENCRYPTION_HEADER_SIZE + 4) + return result; + + auto dec = decrypt_message_data(data, len); + if (!dec.has_value()) + return result; // Decryption failed → signing_key_info = nullptr + + result.decrypted = true; + result.authority_pubkey = dec->authority_pubkey; + + const auto& inner = dec->inner_data; + if (inner.size() < 4) + return result; + + uint8_t inner_version = inner[0]; + uint8_t inner_flags = inner[1]; + uint8_t msg_count = inner[2]; + uint8_t ann_len = inner[3]; + size_t offset = 4; + + if (inner_version != 1) + return result; // Unknown version — skip gracefully + + // Skip signing key announcement if present + if ((inner_flags & 0x01) && ann_len > 0) + offset += ann_len; + + if (offset > inner.size()) + return result; + + // Cap message count + if (msg_count > MAX_MESSAGES_PER_SHARE) + msg_count = MAX_MESSAGES_PER_SHARE; + + for (uint8_t i = 0; i < msg_count; ++i) + { + ShareMessage msg; + auto new_offset = ShareMessage::unpack(inner.data(), inner.size(), offset, msg); + if (!new_offset.has_value()) break; + result.messages.push_back(std::move(msg)); + offset = *new_offset; + } + + return result; +} + +// Convenience: validate message_data from a share. +// Returns empty string on success; returns error reason on failure. +// Empty message_data is always valid. +inline std::string validate_message_data(const std::vector& message_data) +{ + if (message_data.empty()) + return {}; // No messages — always valid + + auto result = unpack_share_messages(message_data.data(), message_data.size()); + + if (!result.decrypted) + return "message_data failed decryption against all " + "COMBINED_DONATION_SCRIPT authority keys"; + + if (result.authority_pubkey == nullptr) + return "message_data decrypted but authority_pubkey unknown"; + + // Check authority_pubkey is one we recognise + bool known = false; + for (const auto* pk : DONATION_AUTHORITY_PUBKEYS()) + { + if (pk == result.authority_pubkey) { known = true; break; } + } + if (!known) + return "message_data authority_pubkey not in COMBINED_DONATION_SCRIPT"; + + if (result.messages.empty()) + return "message_data decrypted but contains no valid messages"; + + // Verify each message has a valid ECDSA signature from the authority key + // that encrypted the envelope. This is defense-in-depth on top of the + // MAC-based decryption (which already proves authority created the envelope). + for (const auto& msg : result.messages) + { + if (!msg.has_signature() || msg.signature.empty()) + return "message contains unsigned message (type 0x" + + std::to_string(msg.msg_type) + ") inside encrypted envelope"; + + auto msg_hash = compute_message_hash( + msg.msg_type, msg.wire_flags, msg.timestamp, + msg.payload.data(), msg.payload.size()); + + if (!ecdsa_verify(result.authority_pubkey->data(), result.authority_pubkey->size(), + msg_hash.data(), msg.signature.data(), msg.signature.size())) + return "message ECDSA signature verification failed (type 0x" + + std::to_string(msg.msg_type) + ")"; + } + + return {}; // All checks passed +} + +// ============================================================================ +// Message creation: signing, packing, encryption +// ============================================================================ + +// ECDSA sign a 32-byte hash with a 32-byte private key. +// Returns DER-encoded signature, empty on failure. +inline std::vector ecdsa_sign( + const unsigned char* msghash32, + const unsigned char* seckey32) +{ + const auto* ctx = get_secp256k1_context(); + + secp256k1_ecdsa_signature sig; + if (!secp256k1_ecdsa_sign(ctx, &sig, msghash32, seckey32, nullptr, nullptr)) + return {}; + + unsigned char der[72]; + size_t der_len = sizeof(der); + if (!secp256k1_ecdsa_signature_serialize_der(ctx, der, &der_len, &sig)) + return {}; + + return {der, der + der_len}; +} + +// HASH160 = RIPEMD160(SHA256(data)) — standard Bitcoin hash160. +inline std::array hash160( + const unsigned char* data, size_t len) +{ + unsigned char sha256_buf[32]; + CSHA256().Write(data, len).Finalize(sha256_buf); + + std::array result; + CRIPEMD160().Write(sha256_buf, 32).Finalize(result.data()); + return result; +} + +// Serialize a ShareMessage to wire format. +// Wire: [type:1][flags:1][timestamp:4 LE][payload_len:2 LE][payload:N] +// [signing_id:20][sig_len:1][signature:M] +inline std::vector pack_message(const ShareMessage& msg) +{ + std::vector buf; + buf.reserve(8 + msg.payload.size() + 20 + 1 + msg.signature.size()); + + buf.push_back(msg.msg_type); + buf.push_back(msg.wire_flags); + + uint32_t ts = msg.timestamp; + buf.push_back(static_cast(ts)); + buf.push_back(static_cast(ts >> 8)); + buf.push_back(static_cast(ts >> 16)); + buf.push_back(static_cast(ts >> 24)); + + uint16_t pl = static_cast(msg.payload.size()); + buf.push_back(static_cast(pl)); + buf.push_back(static_cast(pl >> 8)); + + buf.insert(buf.end(), msg.payload.begin(), msg.payload.end()); + buf.insert(buf.end(), msg.signing_id.begin(), msg.signing_id.end()); + + buf.push_back(static_cast(msg.signature.size())); + buf.insert(buf.end(), msg.signature.begin(), msg.signature.end()); + + return buf; +} + +// Encrypt inner envelope data with an authority public key. +// Returns encrypted blob: [0x01][nonce:16][mac:32][ciphertext:N] +inline std::vector encrypt_message_envelope( + const std::vector& inner, + const AuthorityPubkey& authority_pubkey) +{ + // 16-byte random nonce + std::array nonce; + std::random_device rd; + for (size_t i = 0; i < ENCRYPTION_NONCE_SIZE; i += 4) + { + auto val = rd(); + auto bytes = std::min(4, ENCRYPTION_NONCE_SIZE - i); + std::memcpy(nonce.data() + i, &val, bytes); + } + + // enc_key = HMAC-SHA256(authority_pubkey, nonce) + auto enc_key = hmac_sha256( + authority_pubkey.data(), authority_pubkey.size(), + nonce.data(), nonce.size()); + + // stream = counter-mode SHA256 + std::vector stream(inner.size()); + generate_stream(enc_key.data(), stream.data(), inner.size()); + + // ciphertext = inner XOR stream + std::vector ciphertext(inner.size()); + for (size_t i = 0; i < inner.size(); ++i) + ciphertext[i] = inner[i] ^ stream[i]; + + // mac = HMAC-SHA256(enc_key, ciphertext) + auto mac = hmac_sha256( + enc_key.data(), enc_key.size(), + ciphertext.data(), ciphertext.size()); + + // Assemble: [0x01][nonce:16][mac:32][ciphertext] + std::vector result; + result.reserve(1 + ENCRYPTION_NONCE_SIZE + ENCRYPTION_MAC_SIZE + ciphertext.size()); + result.push_back(ENCRYPTED_ENVELOPE_VERSION); + result.insert(result.end(), nonce.begin(), nonce.end()); + result.insert(result.end(), mac.begin(), mac.end()); + result.insert(result.end(), ciphertext.begin(), ciphertext.end()); + return result; +} + +// Create encrypted message_data blob for embedding in a V36 share. +// +// seckey: 32-byte private key +// authority_pubkey: 33-byte compressed pubkey corresponding to seckey +// messages: ShareMessages with msg_type, wire_flags, timestamp, payload set. +// signature and signing_id are computed and filled in. +// +// Returns the complete encrypted message_data blob, or empty on failure. +inline std::vector create_message_data( + const unsigned char* seckey, + const AuthorityPubkey& authority_pubkey, + std::vector& messages) +{ + if (messages.empty() || messages.size() > MAX_MESSAGES_PER_SHARE) + return {}; + + // Compute signing_id = HASH160(authority_pubkey) + auto signing_id = hash160(authority_pubkey.data(), authority_pubkey.size()); + + // Sign each message and serialize + std::vector packed_messages; + for (auto& msg : messages) + { + msg.signing_id = signing_id; + + auto msg_hash = compute_message_hash( + msg.msg_type, msg.wire_flags, msg.timestamp, + msg.payload.data(), msg.payload.size()); + + msg.signature = ecdsa_sign(msg_hash.data(), seckey); + if (msg.signature.empty()) + return {}; + + auto packed = pack_message(msg); + packed_messages.insert(packed_messages.end(), packed.begin(), packed.end()); + } + + // Inner envelope: [version=1][flags=0][msg_count][reserved=0] + packed_messages + std::vector inner; + inner.reserve(4 + packed_messages.size()); + inner.push_back(0x01); + inner.push_back(0x00); + inner.push_back(static_cast(messages.size())); + inner.push_back(0x00); + inner.insert(inner.end(), packed_messages.begin(), packed_messages.end()); + + if (inner.size() > MAX_TOTAL_MESSAGE_BYTES) + return {}; + + return encrypt_message_envelope(inner, authority_pubkey); +} + +} // namespace bch diff --git a/src/impl/bch/share_tracker.hpp b/src/impl/bch/share_tracker.hpp new file mode 100644 index 000000000..cb117005a --- /dev/null +++ b/src/impl/bch/share_tracker.hpp @@ -0,0 +1,585 @@ +#pragma once + +// share_tracker.hpp -- BCH PPLNS / expected-payouts engine (M3 slice 16). +// +// Ports the PPLNS window accumulator and the expected-payouts entry points from +// the LTC reference (src/impl/ltc/share_tracker.hpp). The PPLNS window +// structures (CumulativeWeights / PPLNSEntry / DensePPLNSWindow / HeadPPLNS) and +// the payout entry points (get_expected_payouts / get_v35_expected_payouts) plus +// their direct weight helpers (get_cumulative_weights / +// get_v36_decayed_cumulative_weights) are BYTE-IDENTICAL across the BTC and LTC +// references -- i.e. coin-independent. They are reproduced here verbatim so the +// BCH payout math is bit-exact with p2pool-merged-v36 (V36 master-compat law). +// +// SCOPE (M3 slice 16): this file lands ONLY the PPLNS/payout surface of +// ShareTracker. The remaining ShareTracker body (verify_share, think(), +// head-scoring, version counting, block scanning, and skip-list maintenance +// beyond the weights skiplist) is ported in subsequent M3 slices and extends +// THIS class. +// +// STANDALONE-PARENT NOTE: BCH is a standalone SHA256d parent (NOT merged-mined). +// The LTC reference additionally carries merged-mining machinery +// (merged_weights_delta, m_miner_merged_addr, get_merged_expected_payouts, +// CoinParams-driven verify, get_desired_version_weights) that BTC -- the other +// standalone SHA256d parent -- does NOT. Whether the FULL BCH ShareTracker +// mirrors LTC (merged infra carried inert) or BTC (merged infra absent) is a +// structural decision deferred to integrator; it does NOT affect this slice, +// whose ported surface is byte-identical across both references. +// +// source-only: impl_bch stays CMake-unregistered (bch stays skip-green). +// +// Reference: frstrtr/p2pool-merged-v36 p2pool/data.py +// (get_expected_payouts, get_decayed_cumulative_weights). + +#include // mul128_shift below uses uint64_t before the include block + +// Portable 128-bit multiply-shift: (a * b) >> shift +// GCC/Clang have __uint128_t; MSVC uses _umul128 intrinsic. +// shift must be in [1, 63] to avoid undefined behavior from 64-bit shifts. +#ifdef _MSC_VER +#include +inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) { + uint64_t hi; + uint64_t lo = _umul128(a, b, &hi); + if (shift == 0) return lo; // (a*b) >> 0 — just the low 64 bits + if (shift >= 64) return hi >> (shift - 64); + return (hi << (64 - shift)) | (lo >> shift); +} +#else +inline uint64_t mul128_shift(uint64_t a, uint64_t b, unsigned shift) { + return static_cast((static_cast<__uint128_t>(a) * b) >> shift); +} +#endif + +#include "share.hpp" +#include "share_check.hpp" +#include "config_pool.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bch +{ + +struct CumulativeWeights +{ + std::map, uint288> weights; + uint288 total_weight; + uint288 total_donation_weight; +}; + +// ── Dense PPLNS Ring Buffer ────────────────────────────────────────────── +// Stores PPLNS-relevant data for each share in the sliding window as a +// contiguous array. Eliminates hash map pointer-chasing during PPLNS walks: +// 8640 sequential reads over ~500KB (fits L2 cache) vs 8640 random hash map +// lookups. ~10x faster per PPLNS computation. +// +// The precomputed decay table guarantees bit-exact results: it uses the same +// iterative truncation as the walk-based code (decay_fp[d+1] = (decay_fp[d] +// * decay_per) >> PRECISION), just stored in a table instead of recomputed. + +struct PPLNSEntry { + uint288 att; // target_to_average_attempts(bits_to_target(bits)) + uint32_t donation{0}; // share donation field + std::vector script; // payout script (variable-length) +}; + +class DensePPLNSWindow { +public: + static constexpr uint64_t DECAY_PRECISION = 40; + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; + static constexpr uint64_t LN2_MICRO = 693147; + + // Precomputed decay table: decay_table[d] = iterative decay factor at depth d. + // Computed once, reused for every PPLNS walk. Thread-safe after init. + // + // Out-of-line definitions mirror ltc/node.cpp:16-18 and land with the BCH + // node.cpp port slice: + // std::vector bch::DensePPLNSWindow::s_decay_table; + // uint64_t bch::DensePPLNSWindow::s_decay_per = 0; + // bool bch::DensePPLNSWindow::s_table_initialized = false; + static std::vector s_decay_table; + static uint64_t s_decay_per; + static bool s_table_initialized; + + static void init_decay_table(uint32_t chain_len) { + if (s_table_initialized && s_decay_table.size() == chain_len) + return; + uint32_t half_life = std::max(chain_len / 4, uint32_t(1)); + s_decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + s_decay_table.resize(chain_len); + s_decay_table[0] = DECAY_SCALE; + for (uint32_t d = 1; d < chain_len; ++d) + s_decay_table[d] = mul128_shift(s_decay_table[d-1], s_decay_per, DECAY_PRECISION); + s_table_initialized = true; + } + + // Deque of share entries: index 0 = depth 0 (shallowest/newest in window). + // Deque gives O(1) push/pop at both ends for forward and backward slides. + std::deque m_entries; + uint256 m_tip_hash; // hash of the share whose prev_hash starts this window + int32_t m_chain_len{0}; // target window size + + // Build ring from chain data. O(chain_len) hash map lookups — done once. + // After rebuild, m_entries[0] = share at start (depth 0), + // m_entries[n-1] = deepest share in window. + template + void rebuild(ChainT& chain, const uint256& start, int32_t chain_len) { + init_decay_table(static_cast(chain_len)); + m_entries.clear(); + m_tip_hash = start; + m_chain_len = chain_len; + + auto cur = start; + while (!cur.IsNull() && chain.contains(cur) + && static_cast(m_entries.size()) < chain_len) + { + PPLNSEntry entry; + chain.get_share(cur).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_entries.push_back(std::move(entry)); + + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + } + + // Slide window forward: new share enters at front (depth 0), oldest drops. + // Used when extending verification toward the chain tip. + void slide_forward(PPLNSEntry entering) { + m_entries.push_front(std::move(entering)); + if (static_cast(m_entries.size()) > m_chain_len) + m_entries.pop_back(); + } + + // Slide window backward: shallowest share drops, new deeper share added. + // Used by think() Phase 2 which walks backward via get_chain(last, N). + // Each successive share to verify needs PPLNS shifted 1 step deeper. + void slide_backward(PPLNSEntry deeper_entry) { + if (!m_entries.empty()) + m_entries.pop_front(); + m_entries.push_back(std::move(deeper_entry)); + } + + // Compute V36 decayed PPLNS weights from ring buffer. + // Bit-exact with the walk-based code: uses precomputed s_decay_table. + // Sequential-ish memory access (deque blocks) — ~80μs vs ~500μs hash map. + CumulativeWeights compute_v36_weights() const { + CumulativeWeights result; + int32_t n = static_cast(m_entries.size()); + for (int32_t d = 0; d < n; ++d) { + auto& e = m_entries[d]; + uint288 decayed_att = (e.att * uint288(s_decay_table[d])) >> DECAY_PRECISION; + auto addr_w = decayed_att * static_cast(65535 - e.donation); + auto don_w = decayed_att * e.donation; + auto this_total = addr_w + don_w; + result.weights[e.script] += addr_w; + result.total_weight += this_total; + result.total_donation_weight += don_w; + } + return result; + } + + bool empty() const { return m_entries.empty(); } + int32_t size() const { return static_cast(m_entries.size()); } +}; + +// ── Head-Level Incremental PPLNS ───────────────────────────────────────── +// Maintains a DensePPLNSWindow + cached CumulativeWeights per active head. +// When verifying consecutive shares along a chain: +// 1. rebuild() at first share's parent: O(chain_len) — one hash map walk +// 2. extend() for each subsequent share: O(1) ring push + O(chain_len) dense recompute +// Total for N shares: O(chain_len + N × chain_len) sequential ops +// vs current: O(N × chain_len) random hash map ops — ~10x faster per op. + +class HeadPPLNS { + DensePPLNSWindow m_ring; + CumulativeWeights m_cached; + bool m_dirty{true}; + +public: + // Cold start: build ring from chain. O(chain_len) hash map lookups. + template + void rebuild(ChainT& chain, const uint256& start, int32_t chain_len) { + m_ring.rebuild(chain, start, chain_len); + m_dirty = true; + } + + // Extend forward: new share enters at depth 0. O(1) ring update. + // Used when verifying toward chain tip (new shares extending verified head). + template + void extend_forward(ChainT& chain, const uint256& new_share_hash) { + PPLNSEntry entry; + chain.get_share(new_share_hash).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_ring.slide_forward(std::move(entry)); + m_dirty = true; + } + + // Extend backward: depth-0 drops, new deeper share added. O(1) ring update. + // Used by think() Phase 2 which walks backward via get_chain(last, N). + template + void extend_backward(ChainT& chain, const uint256& deeper_share_hash) { + PPLNSEntry entry; + chain.get_share(deeper_share_hash).invoke([&](auto* obj) { + entry.att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + entry.donation = obj->m_donation; + entry.script = get_share_script(obj); + }); + m_ring.slide_backward(std::move(entry)); + m_dirty = true; + } + + // Get cached weights. Recomputes from ring if dirty. + // O(chain_len) dense sequential reads, ~50μs. + const CumulativeWeights& weights() { + if (m_dirty) { + m_cached = m_ring.compute_v36_weights(); + m_dirty = false; + } + return m_cached; + } + + bool valid() const { return !m_ring.empty(); } + int32_t window_size() const { return m_ring.size(); } + const DensePPLNSWindow& ring() const { return m_ring; } +}; + +// --- ShareTracker (M3 slice 16: PPLNS / expected-payouts surface only) --- +// Subsequent M3 slices extend this class with verify_share(), think(), +// head-scoring, version counting and block scanning. +class ShareTracker +{ +public: + ShareChain chain; + + // -- PPLNS cumulative weights computation (O(log n) via skip list) -- + CumulativeWeights get_cumulative_weights(const uint256& start, int32_t max_shares, const uint288& desired_weight) + { + if (start.IsNull()) + return {}; + + ensure_weights_skiplist(); + auto sl_result = m_weights_skiplist->query(start, max_shares, desired_weight); + return CumulativeWeights{ + std::move(sl_result.weights), + sl_result.total_weight, + sl_result.total_donation_weight + }; + } + + // -- V36 PPLNS with exponential depth-decay -- + // Matches Python: get_decayed_cumulative_weights() + // half_life = CHAIN_LENGTH // 4 + // Each share's weight is multiplied by 2^(-depth/half_life) + // Fixed-point arithmetic with 40-bit precision. + // + // Result is cached keyed by (start_hash, max_shares) — invalidated + // when the chain head changes. This makes repeated calls for the + // same share (e.g. during verify_share + get_expected_payouts) O(1). + CumulativeWeights get_v36_decayed_cumulative_weights( + const uint256& start, int32_t max_shares, const uint288& desired_weight) + { + if (start.IsNull()) + return {}; + + // Cache: keyed by (start_hash, max_shares, desired_weight). + // Same inputs always produce same outputs (deterministic walk). + // Invalidated when chain head changes (new shares added/removed). + // No consensus risk — cached result is byte-identical to fresh computation. + if (m_decayed_cache_valid && m_decayed_cache_start == start + && m_decayed_cache_shares == max_shares + && m_decayed_cache_desired == desired_weight) + return m_decayed_cache_result; + + static constexpr uint64_t DECAY_PRECISION = 40; + static constexpr uint64_t DECAY_SCALE = uint64_t(1) << DECAY_PRECISION; + static constexpr uint64_t LN2_MICRO = 693147; + + uint32_t half_life = std::max(PoolConfig::chain_length() / 4, uint32_t(1)); + uint64_t decay_per = DECAY_SCALE - (DECAY_SCALE * LN2_MICRO) / (uint64_t(1000000) * half_life); + + CumulativeWeights result; + int32_t share_count = 0; + uint64_t decay_fp = DECAY_SCALE; // starts at 1.0 + + // Single-pass walk matching p2pool's while loop in + // get_decayed_cumulative_weights. No pre-collection needed. + auto cur = start; + while (!cur.IsNull() && chain.contains(cur) && share_count < max_shares) + { + chain.get_share(cur).invoke([&](auto* obj) { + auto att = chain::target_to_average_attempts( + chain::bits_to_target(obj->m_bits)); + uint32_t don = obj->m_donation; + + uint288 decayed_att = (att * uint288(decay_fp)) >> DECAY_PRECISION; + + auto addr_w = decayed_att * static_cast(65535 - don); + auto don_w = decayed_att * don; + auto this_total = addr_w + don_w; // = decayed_att * 65535 + + if (result.total_weight + this_total > desired_weight) { + auto remaining = desired_weight - result.total_weight; + if (!this_total.IsNull()) { + addr_w = addr_w * remaining / this_total; + don_w = don_w * remaining / this_total; + } + this_total = remaining; + } + + auto script = get_share_script(obj); + result.weights[script] += addr_w; + result.total_weight += this_total; + result.total_donation_weight += don_w; + }); + + ++share_count; + if (result.total_weight >= desired_weight) + break; + + decay_fp = mul128_shift(decay_fp, decay_per, DECAY_PRECISION); + + auto* idx = chain.get_index(cur); + cur = idx ? idx->tail : uint256(); + } + + // Cache result (single-entry, invalidated on chain change) + m_decayed_cache_start = start; + m_decayed_cache_shares = max_shares; + m_decayed_cache_desired = desired_weight; + m_decayed_cache_result = result; + m_decayed_cache_valid = true; + + return result; + } + + // -- Expected payouts from PPLNS weights -- + // Uses exact integer arithmetic matching generate_share_transaction(): + // V36: amount = (uint288(subsidy) * weight / total_weight).GetLow64() + // donation = subsidy - sum(amounts) + std::map, double> + get_expected_payouts(const uint256& best_share_hash, const uint256& block_target, uint64_t subsidy, + const std::vector& donation_script) + { + // Pass REAL_CHAIN_LENGTH as max_shares — the walk naturally stops + // when the chain ends, matching p2pool's direct iteration pattern. + // Do NOT use cached get_height() which can be stale in multi-threaded context. + auto chain_len = static_cast(PoolConfig::real_chain_length()); + // V36: remove desired_weight cap — exponential decay handles windowing. + // See generate_share_transaction() for detailed rationale. + uint288 unlimited_weight; + unlimited_weight.SetHex("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + { + static int ep_log = 0; + if (ep_log++ % 20 == 0) { + LOG_DEBUG_DIAG << "[EP-PPLNS] v36 start=" << best_share_hash.GetHex().substr(0, 16) + << " chain_len=" << chain_len + << " subsidy=" << subsidy; + } + } + auto [weights, total_weight, donation_weight] = get_v36_decayed_cumulative_weights(best_share_hash, chain_len, unlimited_weight); + + std::map, double> result; + uint64_t sum = 0; + + if (!total_weight.IsNull()) + { + for (const auto& [script, weight] : weights) + { + // Exact integer division matching generate_share_transaction (V36) + uint64_t amount = (uint288(subsidy) * weight / total_weight).GetLow64(); + if (amount > 0) + { + result[script] = static_cast(amount); + sum += amount; + } + } + } + + // Remainder goes to donation (matches generate_share_transaction) + uint64_t donation_amount = (subsidy > sum) ? (subsidy - sum) : 0; + + // V36 consensus: donation output must carry >= 1 satoshi (a60f7f7f) + if (donation_amount < 1 && subsidy > 0 && !result.empty()) { + // Deterministic tiebreak: (amount, script) — largest script wins when equal + auto largest = std::max_element(result.begin(), result.end(), + [](const auto& a, const auto& b) { + if (a.second != b.second) return a.second < b.second; + return a.first < b.first; + }); + if (largest != result.end() && largest->second >= 1.0) { + largest->second -= 1.0; + sum -= 1; + donation_amount = subsidy - sum; + } + } + + result[donation_script] = (result.contains(donation_script) ? result[donation_script] : 0.0) + + static_cast(donation_amount); + + return result; + } + + // -- V35 PPLNS expected payouts -- + // Flat (non-decayed) weights, GRANDPARENT start, height-1 window. + // Returns amounts WITHOUT finder fee — caller adds subsidy/200 to the + // share creator's script. Donation absorbs the remainder. + // Reference: p2pool data.py lines 878-965 + std::map, double> + get_v35_expected_payouts(const uint256& best_share_hash, const uint256& block_target, uint64_t subsidy, + const std::vector& donation_script) + { + // V35: PPLNS starts from the GRANDPARENT (prev_share.prev_hash) + // Reference: data.py line 884 + uint256 pplns_start; + if (!best_share_hash.IsNull() && chain.contains(best_share_hash)) { + chain.get(best_share_hash).share.invoke([&](auto* s) { + pplns_start = s->m_prev_hash; // grandparent + }); + } + + if (pplns_start.IsNull()) { + // No grandparent: all subsidy to donation + std::map, double> result; + result[donation_script] = static_cast(subsidy); + return result; + } + + // V35: max_shares = max(0, min(height, REAL_CHAIN_LENGTH) - 1) + // Reference: data.py line 885 + auto height = chain.get_height(best_share_hash); + int32_t max_shares = std::max(0, std::min(height, static_cast(PoolConfig::real_chain_length())) - 1); + + // V35: desired_weight = 65535 * SPREAD * target_to_average_attempts(block_target) + // Reference: data.py line 886 + uint288 desired_weight = chain::target_to_average_attempts(block_target) + * uint288(PoolConfig::SPREAD) * uint288(65535); + + { + static int ep35_log = 0; + if (ep35_log++ % 20 == 0) { + LOG_DEBUG_DIAG << "[EP-PPLNS] v35 start=" << pplns_start.GetHex().substr(0, 16) + << " max_shares=" << max_shares + << " desired_w=" << desired_weight.GetLow64() + << " subsidy=" << subsidy + << " best=" << best_share_hash.GetHex().substr(0, 16); + } + } + // Flat weight accumulation with hard cap (existing get_cumulative_weights) + auto [weights, total_weight, donation_weight] = get_cumulative_weights(pplns_start, max_shares, desired_weight); + + std::map, double> result; + uint64_t sum = 0; + + if (!total_weight.IsNull()) + { + for (const auto& [script, weight] : weights) + { + // V35: 99.5% to PPLNS — subsidy * 199 * weight / (200 * total_weight) + // Reference: data.py line 924 + uint64_t amount = (uint288(subsidy) * uint288(199) * weight / (uint288(200) * total_weight)).GetLow64(); + if (amount > 0) + { + result[script] = static_cast(amount); + sum += amount; + } + } + } + + // Remainder goes to donation (includes the ~0.5% finder fee portion; + // caller subtracts subsidy/200 for the finder and assigns it per-connection) + // V35: NO minimum donation enforcement (unlike v36) + uint64_t donation_amount = (subsidy > sum) ? (subsidy - sum) : 0; + result[donation_script] = (result.contains(donation_script) ? result[donation_script] : 0.0) + + static_cast(donation_amount); + + // Periodic diagnostic dump for cross-impl comparison + { + static int v35_dump = 0; + if (v35_dump++ < 10 || v35_dump % 60 == 0) { + LOG_DEBUG_DIAG << "[V35-PPLNS] subsidy=" << subsidy << " addrs=" << weights.size() + << " total_w=" << total_weight.GetLow64() + << " max_shares=" << max_shares << " sum=" << sum + << " donation=" << donation_amount + << " prev=" << best_share_hash.GetHex().substr(0, 16) + << " grandparent=" << pplns_start.GetHex().substr(0, 16); + } + } + + return result; + } + +private: + // Previous-share lambda for RAW chain (work templates, general PPLNS) + auto make_previous_fn() + { + return [this](const uint256& hash) -> uint256 { + if (!chain.contains(hash)) return uint256{}; + return chain.get_index(hash)->tail; + }; + } + + void ensure_weights_skiplist() + { + if (m_weights_skiplist) + return; + m_weights_skiplist.emplace( + [this](const uint256& hash) -> chain::WeightsDelta { + chain::WeightsDelta delta; + if (!chain.contains(hash)) return delta; + delta.share_count = 1; + chain.get_share(hash).invoke([&](auto* obj) { + auto target = chain::bits_to_target(obj->m_bits); + auto att = chain::target_to_average_attempts(target); + delta.total_weight = att * 65535; + delta.total_donation_weight = att * static_cast(obj->m_donation); + auto addr_bytes = get_share_script(obj); + delta.weights[addr_bytes] = att * static_cast(65535 - obj->m_donation); + }); + return delta; + }, + make_previous_fn() + ); + } + + // -- Skip list caches for O(log n) weight queries -- + std::optional m_weights_skiplist; + + // -- Decayed weights result cache -- + // Avoids recomputing the O(chain_length) walk when the same + // (start, max_shares) is queried multiple times between chain changes + // (e.g. verify_share + get_expected_payouts for the same share). + bool m_decayed_cache_valid{false}; + uint256 m_decayed_cache_start; + int32_t m_decayed_cache_shares{0}; + uint288 m_decayed_cache_desired; + CumulativeWeights m_decayed_cache_result; +}; + +} // namespace bch diff --git a/src/impl/bch/share_types.hpp b/src/impl/bch/share_types.hpp new file mode 100644 index 000000000..c86926382 --- /dev/null +++ b/src/impl/bch/share_types.hpp @@ -0,0 +1,196 @@ +#pragma once + +#include +#include +#include + +namespace bch +{ + +const uint64_t SEGWIT_ACTIVATION_VERSION = 17; + +constexpr bool is_segwit_activated(uint64_t version) +{ + return version >= SEGWIT_ACTIVATION_VERSION; +} + +enum StaleInfo +{ + none = 0, + orphan = 253, + doa = 254 +}; + +struct MerkleLinkParams +{ + const bool allow_index; + + SER_PARAMS_OPFUNC +}; + +constexpr static MerkleLinkParams MERKLE_LINK_SMALL {.allow_index = false}; +constexpr static MerkleLinkParams MERKLE_LINK_FULL {.allow_index = true}; + +struct MerkleLink +{ + std::vector m_branch; + uint32_t m_index{0}; + + MerkleLink() { } + + template + void UnserializeMerkleLink(StreamType& s, const MerkleLinkParams& params) + { + s >> m_branch; + if (params.allow_index) + s >> m_index; + } + + template + void SerializeMerkleLink(StreamType& s, const MerkleLinkParams& params) const + { + s << m_branch; + if (params.allow_index) + s << m_index; + } + + template + inline void Serialize(StreamType& os) const + { + SerializeMerkleLink(os, os.GetParams()); + } + + template + inline void Unserialize(StreamType& is) + { + UnserializeMerkleLink(is, is.GetParams()); + } +}; + +struct SegwitData +{ + MerkleLink m_txid_merkle_link; + uint256 m_wtxid_merkle_root; + + SegwitData() {} + SegwitData(MerkleLink txid_merkle_link, uint256 wtxid) : m_txid_merkle_link(txid_merkle_link), m_wtxid_merkle_root(wtxid) { } + + C2POOL_SERIALIZE_METHODS(SegwitData) { READWRITE(MERKLE_LINK_SMALL(obj.m_txid_merkle_link), obj.m_wtxid_merkle_root); } +}; + +struct SegwitDataDefault +{ + static SegwitData get() + { + // Sentinel must match p2pool's PossiblyNoneType sentinel: + // dict(txid_merkle_link=dict(branch=[], index=0), wtxid_merkle_root=0) + // Using all-0xff caused p2pool to interpret "None" as a valid wtxid root, + // producing different witness commitment → different coinbase txid. + return SegwitData{{}, uint256()}; // zero = None sentinel + } +}; + +struct TxHashRefs +{ + uint64_t m_share_count; + uint64_t m_tx_count; + + TxHashRefs() = default; + TxHashRefs(uint64_t share, uint64_t tx) : m_share_count(share), m_tx_count(tx) {} + + C2POOL_SERIALIZE_METHODS(TxHashRefs) { READWRITE(VarInt(obj.m_share_count), VarInt(obj.m_tx_count)); } +}; + +struct ShareTxInfo +{ + std::vector m_new_transaction_hashes; + std::vector m_transaction_hash_refs; //pack.ListType(pack.VarIntType(), 2)), # pairs of share_count, tx_count + + ShareTxInfo() = default; + + ShareTxInfo(const auto& new_tx_hashes, const auto& tx_hash_refs) + : m_new_transaction_hashes(new_tx_hashes), m_transaction_hash_refs(tx_hash_refs) { } + + C2POOL_SERIALIZE_METHODS(ShareTxInfo) { READWRITE(obj.m_new_transaction_hashes, obj.m_transaction_hash_refs); } +}; + +struct HashLinkType +{ + FixedStrType<32> m_state; //pack.FixedStrType(32) + // FixedStrType<0> m_extra_data; //pack.FixedStrType(0) # bit of a hack, but since the donation script is at the end, const_ending is long enough to always make this empty + uint64_t m_length; //pack.VarIntType() + + C2POOL_SERIALIZE_METHODS(HashLinkType) { READWRITE(obj.m_state, /*obj.m_extra_data,*/ VarInt(obj.m_length)); } +}; + +// V36 hash link — extra_data becomes VarStr (was FixedStr(0) pre-V36) +struct V36HashLinkType +{ + FixedStrType<32> m_state; + BaseScript m_extra_data; // VarStr in V36 + uint64_t m_length; + + C2POOL_SERIALIZE_METHODS(V36HashLinkType) { READWRITE(obj.m_state, obj.m_extra_data, VarInt(obj.m_length)); } +}; + +// V36 merged mining: per-chain address entry +struct MergedAddressEntry +{ + uint32_t m_chain_id; + BaseScript m_script; + + C2POOL_SERIALIZE_METHODS(MergedAddressEntry) { READWRITE(obj.m_chain_id, obj.m_script); } +}; + +// V36 merged mining: per-chain coinbase verification entry +struct MergedCoinbaseEntry +{ + uint32_t m_chain_id; + uint64_t m_coinbase_value; + uint32_t m_block_height; + FixedStrType<80> m_block_header; + MerkleLink m_coinbase_merkle_link; + BaseScript m_coinbase_script; // V36: actual scriptSig (allows custom tags + THE state_root) + + template + void Serialize(StreamType& os) const + { + ::Serialize(os, m_chain_id); + ::Serialize(os, Using(m_coinbase_value)); + ::Serialize(os, Using(m_block_height)); + ::Serialize(os, m_block_header); + ParamPackStream pstream{MERKLE_LINK_SMALL, os}; + ::Serialize(pstream, m_coinbase_merkle_link); + ::Serialize(os, m_coinbase_script); + } + + template + void Unserialize(StreamType& is) + { + ::Unserialize(is, m_chain_id); + ::Unserialize(is, Using(m_coinbase_value)); + ::Unserialize(is, Using(m_block_height)); + ::Unserialize(is, m_block_header); + ParamPackStream pstream{MERKLE_LINK_SMALL, is}; + ::Unserialize(pstream, m_coinbase_merkle_link); + ::Unserialize(is, m_coinbase_script); + } +}; + +// V36: abswork is VarInt-encoded on the wire but stored as uint128 +struct AbsworkV36Format +{ + template + static void Write(StreamType& os, const uint128& value) + { + WriteCompactSize(os, value.GetLow64()); + } + + template + static void Read(StreamType& is, uint128& value) + { + value = uint128(ReadCompactSize(is, false)); + } +}; + +} // namespace bch