diff --git a/src/impl/bch/coin/abla.hpp b/src/impl/bch/coin/abla.hpp new file mode 100644 index 000000000..5c3f2e0f6 --- /dev/null +++ b/src/impl/bch/coin/abla.hpp @@ -0,0 +1,214 @@ +#pragma once +// BCH ABLA (Adaptive Blocksize-Limit Algorithm, CHIP-2023-01) -- net-new +// BCH-specific code. Activated at the May 2024 upgrade; it replaces the static +// 32 MB excessive-blocksize (EB) with a control function that lets the network +// block-size limit grow with sustained demand. +// +// This is a 1:1 fixed-point port of Bitcoin Cash Node: +// src/consensus/abla.cpp (State::NextBlockState, Config::MakeDefault/SetMax) +// src/consensus/abla.h (Config / State data types) +// pinned at BCHN tag v29.0.0 (clone @89a591f). It is consensus-critical and +// MUST stay byte-exact with BCHN -- validate against BCHN gold vectors +// src/test/data/abla_test_vectors (gen: generate_abla_test_vectors.py). +// +// p2pool-merged-v36 SURFACE: NONE. ABLA governs only the *block-size limit*, +// i.e. how large a block a node will accept / how large a template we may +// build. It does NOT touch PoW hash, share format, coinbase commitment, or +// PPLNS math. In the template builder it is used purely as a LOCAL build-time +// byte budget; using a conservative (floor) value can never produce an +// invalid block, so there is zero compat risk. -- M4 size-slice (M1 ss4.4). +// +// Mainnet: MakeDefault(32 MB, fixedSize=false) -> grows. +// testnet3 / testnet4: MakeDefault(32 MB, fixedSize=true) -> no-op, fixed 32 MB. + +#include +#include +#include +#include + +namespace bch { +namespace coin { +namespace abla { + +// ONE_MEGABYTE / consensus block-size constants -- BCHN src/consensus/consensus.h +inline constexpr uint64_t ONE_MEGABYTE = 1000000u; +inline constexpr uint64_t DEFAULT_CONSENSUS_BLOCK_SIZE = 32u * ONE_MEGABYTE; +inline constexpr uint64_t MAX_CONSENSUS_BLOCK_SIZE = uint64_t(2000) * ONE_MEGABYTE; + +// 2^7 fixed precision for the "asymmetry factor" (zeta). BCHN abla.h B7. +inline constexpr uint64_t B7 = 1u << 7u; + +// muldiv(x,y,z) = x*y/z in 128-bit intermediate (BCHN abla.cpp). The platform +// is gcc/clang C++20 on linux -> use the native __int128 path BCHN selects. +inline uint64_t muldiv(uint64_t x, uint64_t y, uint64_t z) { + assert(z != 0); + const unsigned __int128 res = + (static_cast(x) * static_cast(y)) + / static_cast(z); + assert(res <= static_cast(std::numeric_limits::max())); + return static_cast(res); +} + +// Algorithm configuration -- part of a chain's consensus params (BCHN abla.h). +struct Config { + uint64_t epsilon0{}; // initial/floor control block size + uint64_t beta0{}; // initial/floor elastic buffer size + uint64_t gammaReciprocal{}; // reciprocal of control "forget factor" + uint64_t zeta_xB7{}; // control "asymmetry factor" (x B7) + uint64_t thetaReciprocal{}; // reciprocal of elastic buffer decay rate + uint64_t delta{}; // elastic buffer "gear factor" + uint64_t epsilonMax{}; // max control block size + uint64_t betaMax{}; // max elastic buffer size + + // Set epsilonMax / betaMax so internal ops can't overflow UINT64_MAX. + // 1:1 BCHN Config::SetMax. + void SetMax() { + const uint64_t maxSafeBlocksizeLimit = + std::numeric_limits::max() / zeta_xB7 * B7; + const uint64_t maxElasticBufferRatioNumerator = + delta * ((zeta_xB7 - B7) * thetaReciprocal / gammaReciprocal); + const uint64_t maxElasticBufferRatioDenominator = + (zeta_xB7 - B7) * thetaReciprocal / gammaReciprocal + B7; + epsilonMax = maxSafeBlocksizeLimit + / (maxElasticBufferRatioNumerator + maxElasticBufferRatioDenominator) + * maxElasticBufferRatioDenominator; + betaMax = maxSafeBlocksizeLimit - epsilonMax; + } + + bool IsFixedSize() const { + return epsilon0 == epsilonMax && beta0 == betaMax; + } + + // 1:1 BCHN Config::MakeDefault. + static Config MakeDefault(uint64_t defaultBlockSize = DEFAULT_CONSENSUS_BLOCK_SIZE, + bool fixedSize = false) { + Config ret; + ret.epsilon0 = defaultBlockSize / 2u; + ret.beta0 = defaultBlockSize / 2u; + ret.gammaReciprocal = 37938; + ret.zeta_xB7 = 192; + ret.thetaReciprocal = 37938; + ret.delta = 10; + if (!fixedSize) { + ret.SetMax(); + } else { + ret.epsilonMax = ret.epsilon0; + ret.betaMax = ret.beta0; + } + return ret; + } +}; + +// Algorithm internal state for block N. The limit for block N is +// state_N.GetBlockSizeLimit(); for N+1 use NextBlockState(...).GetBlockSizeLimit(). +class State { + uint64_t blockSize{}; + uint64_t controlBlockSize{}; + uint64_t elasticBufferSize{}; + +public: + State() = default; + + // Defaults from Config -- suitable for all blocks before ABLA activation + // (the activation/floor state). 1:1 BCHN State(config, blkSize). + State(const Config& config, uint64_t blkSize) + : blockSize(blkSize), + controlBlockSize(config.epsilon0), + elasticBufferSize(config.beta0) {} + + uint64_t GetBlockSizeLimit(bool disable2GBCap = false) const { + if (disable2GBCap) + return controlBlockSize + elasticBufferSize; + return std::min(controlBlockSize + elasticBufferSize, + MAX_CONSENSUS_BLOCK_SIZE); + } + + uint64_t GetNextBlockSizeLimit(const Config& config, bool disable2GBCap = false) const { + return NextBlockState(config, 0).GetBlockSizeLimit(disable2GBCap); + } + + uint64_t GetBlockSize() const { return blockSize; } + uint64_t GetControlBlockSize() const { return controlBlockSize; } + uint64_t GetElasticBufferSize() const { return elasticBufferSize; } + + // Advance state to block N+1 given its size. 1:1 BCHN State::NextBlockState. + State NextBlockState(const Config& config, const uint64_t nextBlockSize) const { + State ret; + ret.blockSize = nextBlockSize; + + // control function + const uint64_t clampedBlockSize = + std::min(this->blockSize, this->controlBlockSize + this->elasticBufferSize); + const uint64_t amplifiedCurrentBlockSize = + muldiv(config.zeta_xB7, clampedBlockSize, B7); + + if (amplifiedCurrentBlockSize > this->controlBlockSize) { + const uint64_t bytesToAdd = amplifiedCurrentBlockSize - this->controlBlockSize; + const uint64_t amplifiedBlockSizeLimit = + muldiv(config.zeta_xB7, this->controlBlockSize + this->elasticBufferSize, B7); + const uint64_t bytesMax = amplifiedBlockSizeLimit - this->controlBlockSize; + const uint64_t scalingOffset = + muldiv(muldiv(config.zeta_xB7, this->elasticBufferSize, B7), bytesToAdd, bytesMax); + ret.controlBlockSize = + this->controlBlockSize + (bytesToAdd - scalingOffset) / config.gammaReciprocal; + } else { + const uint64_t bytesToRemove = this->controlBlockSize - amplifiedCurrentBlockSize; + ret.controlBlockSize = this->controlBlockSize - bytesToRemove / config.gammaReciprocal; + ret.controlBlockSize = std::max(ret.controlBlockSize, config.epsilon0); + } + + // elastic buffer function + const uint64_t bufferDecay = this->elasticBufferSize / config.thetaReciprocal; + if (amplifiedCurrentBlockSize > this->controlBlockSize) { + const uint64_t bytesToAdd = + (ret.controlBlockSize - this->controlBlockSize) * config.delta; + ret.elasticBufferSize = this->elasticBufferSize - bufferDecay + bytesToAdd; + } else { + ret.elasticBufferSize = this->elasticBufferSize - bufferDecay; + } + ret.elasticBufferSize = std::max(ret.elasticBufferSize, config.beta0); + + ret.controlBlockSize = std::min(ret.controlBlockSize, config.epsilonMax); + ret.elasticBufferSize = std::min(ret.elasticBufferSize, config.betaMax); + return ret; + } +}; + +// Per-network config. Mainnet grows; testnets are fixed at 32 MB. +inline Config mainnet_config() { return Config::MakeDefault(DEFAULT_CONSENSUS_BLOCK_SIZE, /*fixedSize=*/false); } +inline Config testnet_config() { return Config::MakeDefault(DEFAULT_CONSENSUS_BLOCK_SIZE, /*fixedSize=*/true); } + +// Replay ABLA forward from a known-good anchor State over a contiguous run of +// block sizes (oldest-first), returning the State *after the last size supplied* +// -- i.e. the State whose GetBlockSizeLimit() governs the block we are about to +// build. 1:1 BCHN: repeated State::NextBlockState. The anchor MUST be the ABLA +// State of the block immediately preceding sizes[0]. +// +// NOTE on the size feed: ABLA is driven by each block's *actual serialized +// size*, which the headers-only SPV header_chain structurally does not carry. +// The natural anchor on BCH is therefore a BCHN-pinned {height,State}; the live +// per-block sizes enter at the full-block / embedded-daemon layer (M5+), not +// here. Until that feed exists the template builder stays on the safe floor +// (see floor_block_size_limit / template_builder build budget). +inline State replay(State anchor, const Config& config, + const uint64_t* sizes, size_t n) { + State s = anchor; + for (size_t i = 0; i < n; ++i) + s = s.NextBlockState(config, sizes[i]); + return s; +} + +// Conservative LOCAL build budget for the template builder when per-tip ABLA +// state is not yet replayed through the header chain: the activation/floor +// state. ABLA only ever raises the limit above this floor, so building to the +// floor can never exceed the live consensus limit. The dynamic path is +// abla::replay (above) fed a BCHN-pinned anchor + full-block sizes; that feed +// lives at the full-block/daemon layer, not the headers-only SPV chain. +inline uint64_t floor_block_size_limit(bool is_testnet) { + const Config cfg = is_testnet ? testnet_config() : mainnet_config(); + return State(cfg, 0).GetBlockSizeLimit(); +} + +} // namespace abla +} // namespace coin +} // namespace bch diff --git a/src/impl/bch/coin/asert.hpp b/src/impl/bch/coin/asert.hpp new file mode 100644 index 000000000..7aea68851 --- /dev/null +++ b/src/impl/bch/coin/asert.hpp @@ -0,0 +1,194 @@ +#pragma once +// BCH ASERT (aserti3-2d) difficulty adjustment — net-new BCH-specific code. +// +// BCH replaced BTC's 2016-block 2-week retarget with ASERT at the Nov 2020 +// upgrade (CHIP-2020-05). This is one of the two BCH-specific validation +// slices (M1 §4.3); it has NO analogue in src/impl/btc/. +// +// CalculateASERT() is a 1:1 fixed-point port of Bitcoin Cash Node +// src/pow.cpp:185 CalculateASERT() (decl src/pow.h:43). It is consensus +// critical and MUST stay byte-exact with BCHN — it is validated directly +// against BCHN's gold vectors (contrib/testgen/gen_asert_test_vectors.cpp, +// src/test/pow_tests.cpp). Anchor constants are from BCHN src/chainparams.cpp. +// +// NOTE: PoW HASH is unchanged (SHA256d, byte-identical to BTC — BCH is a +// standalone SHA256d parent). ASERT governs only the *target* a header must +// meet, i.e. which headers the SPV engine accepts as valid chain. It does +// NOT touch share format / PoW hash, so there is no p2pool-merged-v36 +// surface change. + +#include + +#include +#include +#include // llabs + +namespace bch { +namespace coin { + +// ─── ASERT anchor block ────────────────────────────────────────────────── +// Per BCHN absolute-ASERT formulation the reference timestamp is the anchor +// block's *parent* timestamp (block M-1 when the anchor is block M). +struct ASERTAnchor { + int64_t height{0}; // anchor block height + uint32_t nBits{0}; // anchor block nBits (compact target) + int64_t prev_block_time{0}; // anchor block PREVIOUS block timestamp +}; + +// Canonical anchors (BCHN src/chainparams.cpp). +struct ASERTParams { + ASERTAnchor anchor; + int64_t half_life; // nASERTHalfLife (seconds) + int64_t target_spacing; // nPowTargetSpacing (seconds) = 600 + bool allow_min_difficulty{false}; + uint256 pow_limit; +}; + +// nPowTargetSpacing is 600 s (10 min) on every BCH chain. +static constexpr int64_t BCH_TARGET_SPACING = 10 * 60; + +inline uint256 bch_pow_limit() { + // BCH mainnet/testnet powLimit (BCHN chainparams.cpp). + uint256 v; + v.SetHex("00000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffff"); + return v; +} + +inline ASERTParams asert_mainnet() { + return ASERTParams{ + ASERTAnchor{661647, 0x1804dafe, 1605447844}, + 2 * 24 * 60 * 60, // nASERTHalfLife = 2 days + BCH_TARGET_SPACING, + false, + bch_pow_limit(), + }; +} + +inline ASERTParams asert_testnet3() { + return ASERTParams{ + ASERTAnchor{1421481, 0x1d00ffff, 1605445400}, + 60 * 60, // nASERTHalfLife = 1 hour + BCH_TARGET_SPACING, + true, + bch_pow_limit(), + }; +} + +inline ASERTParams asert_testnet4() { + return ASERTParams{ + ASERTAnchor{16844, 0x1d00ffff, 1605451779}, + 60 * 60, + BCH_TARGET_SPACING, + true, + bch_pow_limit(), + }; +} + +// ─── CalculateASERT ──────────────────────────────────────────────────────── +// 1:1 port of BCHN src/pow.cpp CalculateASERT(). Integer (fixed-point) math +// only — MUST remain byte-exact with BCHN. +// +// new_target = old_target * 2^((time_diff - spacing*(height_diff+1)) / halflife) +inline uint256 CalculateASERT(const uint256& refTarget, + const int64_t nPowTargetSpacing, + const int64_t nTimeDiff, + const int64_t nHeightDiff, + const uint256& powLimit, + const int64_t nHalfLife) noexcept { + // Input target must never be zero nor exceed powLimit. + assert(refTarget > uint256::ZERO && refTarget <= powLimit); + + // 32 leading zero bits in powLimit give room to handle overflows easily. + assert((powLimit >> 224) == uint256::ZERO); + + // Height diff should NOT be negative. + assert(nHeightDiff >= 0); + + // Compute the exponent (fixed-point, 16 fractional bits). + assert(llabs(nTimeDiff - nPowTargetSpacing * nHeightDiff) < (1ll << (63 - 16))); + const int64_t exponent = + ((nTimeDiff - nPowTargetSpacing * (nHeightDiff + 1)) * 65536) / nHalfLife; + + // Arithmetic right shift of negative numbers is standard in C++20. + static_assert(int64_t(-1) >> 1 == int64_t(-1), + "ASERT algorithm needs arithmetic shift support"); + + // Decompose exponent into integer and fractional parts. + int64_t shifts = exponent >> 16; + const auto frac = uint16_t(exponent); + assert(exponent == (shifts * 65536) + frac); + + // multiply target by 65536 * 2^(fractional part) + // 2^x ~= (1 + 0.695502049*x + 0.2262698*x**2 + 0.0782318*x**3), 0 <= x < 1 + const uint32_t factor = 65536 + (( + + 195766423245049ull * frac + + 971821376ull * frac * frac + + 5127ull * frac * frac * frac + + (1ull << 47) + ) >> 48); + // always < 2^241 since refTarget < 2^224 + uint256 nextTarget = refTarget * factor; + + // multiply by 2^(integer part) / 65536 + shifts -= 16; + if (shifts <= 0) { + nextTarget >>= int(-shifts); + } else { + const uint256 nextTargetShifted = nextTarget << int(shifts); + if ((nextTargetShifted >> int(shifts)) != nextTarget) { + // Would have overflowed 2^256 → clamps to powLimit anyway. + nextTarget = powLimit; + } else { + nextTarget = nextTargetShifted; + } + } + + if (nextTarget == uint256::ZERO) { + nextTarget = uint256::ONE; // 0 is not a valid target, but 1 is. + } else if (nextTarget > powLimit) { + nextTarget = powLimit; + } + + return nextTarget; +} + +// ─── Anchor-formulated next-work entry ────────────────────────────────────── +// Mirrors btc::coin::get_next_work_required()'s role but uses the absolute +// ASERT anchor formulation (BCHN GetNextASERTWorkRequired, src/pow.cpp:101). +// Returns the compact nBits the block at (tip_height+1) must meet. +// +// @param tip_height height of the block we build on (>= anchor height) +// @param tip_time timestamp of the tip block +// @param new_time timestamp of the new block (for testnet min-diff rule) +inline uint32_t get_next_work_required_asert(uint32_t tip_height, + int64_t tip_time, + int64_t new_time, + const ASERTParams& p) { + // Testnet special rule: >2*spacing since tip → allow a min-difficulty block. + if (p.allow_min_difficulty && + new_time > tip_time + 2 * p.target_spacing) { + return p.pow_limit.GetCompact(); + } + + assert(static_cast(tip_height) >= p.anchor.height); + + uint256 refBlockTarget; + refBlockTarget.SetCompact(p.anchor.nBits); + + // Time diff is measured from the anchor's PARENT timestamp (absolute ASERT). + const int64_t nTimeDiff = tip_time - p.anchor.prev_block_time; + const int64_t nHeightDiff = static_cast(tip_height) - p.anchor.height; + + const uint256 nextTarget = CalculateASERT(refBlockTarget, + p.target_spacing, + nTimeDiff, + nHeightDiff, + p.pow_limit, + p.half_life); + + // CalculateASERT() already clamps to powLimit. + return nextTarget.GetCompact(); +} + +} // namespace coin +} // namespace bch diff --git a/src/impl/bch/coin/header_chain.hpp b/src/impl/bch/coin/header_chain.hpp index 12f0b06c7..e73b44825 100644 --- a/src/impl/bch/coin/header_chain.hpp +++ b/src/impl/bch/coin/header_chain.hpp @@ -1,8 +1,1109 @@ #pragma once -// BCH header-chain validation. Mirrors src/impl/btc/coin/header_chain.hpp. -// -// >>> ASERT INSERTION POINT (M1 §4.3) <<< -// BCH uses ASERT (aserti3-2d) difficulty adjustment since the Nov 2020 -// upgrade, NOT BTC retarget. Net-new c2pool code (one of two BCH-specific -// validation slices). TODO(M3): port aserti3-2d target calc + anchor block. -namespace c2pool::bch { /* TODO(M3): HeaderChain w/ ASERT validate_target() */ } + +/// BCH Header Chain +/// +/// Validated header-only chain for Bitcoin Cash, implementing headers-first +/// sync from BCHN P2P peers. Tracks chain tip, height, and cumulative work. +/// Persistence via LevelDB for fast restarts. +/// +/// Mirrors src/impl/btc/coin/header_chain.hpp — same store/sync/persistence +/// machinery and the same PoW hash (SHA256d; BCH is a standalone SHA256d +/// parent). The ONE divergence is difficulty: BCH replaced Bitcoin's +/// 2016-block 2-week retarget with ASERT (aserti3-2d, CHIP-2020-05, Nov 2020). +/// The retarget math lives in asert.hpp (1:1 BCHN pow.cpp port); this file +/// only wires validate_difficulty() to it. ASERT governs which headers the +/// SPV engine accepts — it does NOT touch share format or PoW hash, so there +/// is no p2pool-merged-v36 surface change. + +#include "asert.hpp" // BCH ASERT (aserti3-2d) difficulty — s18a +#include "block.hpp" +#include // DEFAULT_MAX_TIP_AGE + +#include +#include +#include +#include +#include + +// (BCH PoW is SHA256d via core/hash.hpp — byte-identical to BTC.) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bch { +namespace coin { + +// uint256 hasher for unordered containers (low-64 bits is already a uniform +// distribution over a cryptographic hash, no further mixing needed). +struct Uint256Hasher { + size_t operator()(const uint256& h) const { return h.GetLow64(); } +}; + +// ─── Index Entry ──────────────────────────────────────────────────────────── + +/// Status flags for header validation state +enum HeaderStatus : uint32_t { + HEADER_VALID_UNKNOWN = 0, + HEADER_VALID_HEADER = 1, // Header parsed and PoW valid + HEADER_VALID_TREE = 2, // Connected to genesis via prev_hash chain + HEADER_VALID_CHAIN = 3, // Difficulty validated +}; + +/// A validated header with chain metadata (in-memory representation). +/// +/// On BTC, two fields that the on-disk layout carries are computable from +/// the rest and are not stored in RAM: +/// - "PoW hash" is identical to block_hash (both SHA256d(header)). +/// - "prev_hash" is always header.m_previous_block. +/// Dropping them saves 64 B/entry × ~1M headers = ~60 MB peak heap. +/// +/// On-disk format is unchanged — see IndexEntryDiskV1 below. +struct IndexEntry { + BlockHeaderType header; + uint256 block_hash; // SHA256d(header) — the block hash used for getdata/inv + uint32_t height{0}; + uint256 chain_work; // cumulative work up to this header + HeaderStatus status{HEADER_VALID_UNKNOWN}; +}; + +/// Legacy 6-field on-disk layout. Kept verbatim for backward read compat AND +/// forward write compat (so a roll-back to a pre-Phase-1B binary can still +/// parse what we wrote). The duplicate `hash` and `prev_hash` fields are +/// derived from the slim IndexEntry at write time. +struct IndexEntryDiskV1 { + BlockHeaderType header; + uint256 hash; // SHA256d(header) — same as block_hash on BTC + uint256 block_hash; // SHA256d(header) — the block hash used for getdata/inv + uint32_t height{0}; + uint256 chain_work; // cumulative work up to this header + uint256 prev_hash; // == header.m_previous_block on BTC + HeaderStatus status{HEADER_VALID_UNKNOWN}; + + template + void Serialize(Stream& s) const { + ::Serialize(s, header); + ::Serialize(s, hash); + ::Serialize(s, block_hash); + ::Serialize(s, height); + ::Serialize(s, chain_work); + ::Serialize(s, prev_hash); + ::Serialize(s, static_cast(status)); + } + template + void Unserialize(Stream& s) { + ::Unserialize(s, header); + ::Unserialize(s, hash); + ::Unserialize(s, block_hash); + ::Unserialize(s, height); + ::Unserialize(s, chain_work); + ::Unserialize(s, prev_hash); + uint32_t st; + ::Unserialize(s, st); + status = static_cast(st); + } + + /// Materialize the slim in-memory form (drops the duplicate fields). + IndexEntry to_entry() const { + IndexEntry e; + e.header = header; + e.block_hash = block_hash; + e.height = height; + e.chain_work = chain_work; + e.status = status; + return e; + } + + /// Build the legacy on-disk form from a slim entry (computes duplicates). + static IndexEntryDiskV1 from_entry(const IndexEntry& e) { + IndexEntryDiskV1 d; + d.header = e.header; + d.hash = e.block_hash; + d.block_hash = e.block_hash; + d.height = e.height; + d.chain_work = e.chain_work; + d.prev_hash = e.header.m_previous_block; + d.status = e.status; + return d; + } +}; + +// ─── BCH Chain Parameters ─────────────────────────────────────────────────── +// Unlike BTC's 2016-block retarget, BCH difficulty is governed by ASERT +// (aserti3-2d). The difficulty state therefore is NOT a (timespan, spacing, +// interval) tuple but the ASERTParams anchor block carried in asert.hpp. The +// PoW hash is still SHA256d and the mainnet/testnet3 genesis blocks are shared +// with Bitcoin (BCH forked from BTC at height 478558), so those hashes match +// btc/coin/header_chain.hpp verbatim. + +struct BCHChainParams { + ASERTParams asert; // aserti3-2d difficulty params (asert.hpp) + uint256 pow_limit; // == asert.pow_limit (mirror for check_pow) + uint256 genesis_hash; // SHA256d genesis block hash (identification) + bool allow_min_difficulty{false}; // == asert.allow_min_difficulty (testnet) + + // Fast-start checkpoint: skip syncing from genesis, start from a recent height. + // The header chain seeds this checkpoint as if it were the genesis block. + // All headers before this height are implicitly trusted. + struct Checkpoint { uint32_t height{0}; uint256 hash; }; + std::optional fast_start_checkpoint; + + /// BCH mainnet params (port 8333). Genesis shared with Bitcoin (pre-fork + /// block 0). ASERT anchor + powLimit from BCHN chainparams via asert.hpp. + static BCHChainParams mainnet() { + BCHChainParams p; + p.asert = asert_mainnet(); + p.pow_limit = p.asert.pow_limit; + p.allow_min_difficulty = p.asert.allow_min_difficulty; + // Genesis: identical to Bitcoin mainnet (shared pre-fork history). + p.genesis_hash.SetHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"); + // Seed genesis as the chain root (height=0) — same rationale as BTC. + p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash}; + return p; + } + + /// BCH testnet3 params (port 18333) — primary M3 parity target (VM300). + /// Genesis: identical to Bitcoin testnet3 (shared pre-fork history). + static BCHChainParams testnet() { + BCHChainParams p; + p.asert = asert_testnet3(); + p.pow_limit = p.asert.pow_limit; + p.allow_min_difficulty = p.asert.allow_min_difficulty; + p.genesis_hash.SetHex("000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"); + p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash}; + return p; + } + + /// BCH testnet4 params (port 28333). NOTE: BCH testnet4 has its OWN genesis + /// (distinct from BTC testnet4) — this is a consensus constant. + /// TODO(M3): verify genesis_hash against VM300 bchn-bch chainparams.cpp + /// before any testnet4 integration run. + static BCHChainParams testnet4() { + BCHChainParams p; + p.asert = asert_testnet4(); + p.pow_limit = p.asert.pow_limit; + p.allow_min_difficulty = p.asert.allow_min_difficulty; + p.genesis_hash.SetHex("000000001dd410c49a788668ce26751718cc797474d3152a5fc073dd44fd9f7b"); + p.fast_start_checkpoint = Checkpoint{0, p.genesis_hash}; + return p; + } +}; + +// ─── PoW Functions ────────────────────────────────────────────────────────── + +/// Compute SHA256d PoW hash of an 80-byte block header. +/// On BCH (like BTC) the PoW hash and the block-identity hash are the same +/// value (both SHA256d). BCH is a standalone SHA256d parent — no PoW/aux. +inline uint256 sha256d_hash(const BlockHeaderType& header) { + auto packed = pack(header); + return Hash(packed.get_span()); +} + +/// Compute SHA256d hash of an 80-byte block header (block identification). +inline uint256 block_hash(const BlockHeaderType& header) { + auto packed = pack(header); + return Hash(packed.get_span()); +} + +/// Compute the target from compact nBits representation. +inline uint256 target_from_bits(uint32_t bits) { + uint256 target; + target.SetCompact(bits); + return target; +} + +/// Check that a SHA256d PoW hash meets the target specified by nBits. +inline bool check_pow(const uint256& sha256d_hash_val, uint32_t bits, const uint256& pow_limit) { + bool negative, overflow; + uint256 target; + target.SetCompact(bits, &negative, &overflow); + + if (negative || target.IsNull() || overflow || target > pow_limit) + return false; + + return sha256d_hash_val <= target; +} + +/// Compute work represented by a compact target. +/// Work = 2^256 / (target + 1) +inline uint256 get_block_proof(uint32_t bits) { + bool negative, overflow; + uint256 target; + target.SetCompact(bits, &negative, &overflow); + + if (negative || target.IsNull() || overflow) + return uint256::ZERO; + + // work = ~target / (target + 1) + 1 + return (~target / (target + uint256::ONE)) + uint256::ONE; +} + +// ─── BCH Difficulty (ASERT / aserti3-2d) ───────────────────────────────────── +// BCH does NOT use Bitcoin's 2016-block retarget. Since the Nov 2020 upgrade +// (CHIP-2020-05) the required target for every block is computed directly from +// the ASERT anchor block via an absolute exponential formula — there is no +// 2016-block window, no "first block of period" lookback, and no off-by-one +// Art-Forz history to track. All the fixed-point math lives in asert.hpp +// (get_next_work_required_asert / CalculateASERT), a 1:1 BCHN src/pow.cpp port +// that is validated against BCHN's gold vectors. This file only adapts the +// call shape to header_chain's validate_difficulty(). + +/// Calculate the nBits the block at (tip_height + 1) must meet, per ASERT. +/// Unlike BTC's retarget this needs NO ancestor walk — the absolute ASERT +/// formulation references only the fixed anchor block (asert.hpp) plus the +/// tip's height/time and the new block's time (for the testnet min-diff rule). +/// @param tip_height Height of the current tip (the block we build on). +/// @param tip_time Timestamp of the current tip. +/// @param new_time Timestamp of the new block being validated. +/// @param params Chain parameters (carries the ASERT anchor). +inline uint32_t get_next_work_required( + uint32_t tip_height, + uint32_t tip_time, + uint32_t new_time, + const BCHChainParams& params) +{ + return get_next_work_required_asert( + tip_height, + static_cast(tip_time), + static_cast(new_time), + params.asert); +} + +// ─── HeaderChain ──────────────────────────────────────────────────────────── + +class HeaderChain { +public: + HeaderChain(const BCHChainParams& params, const std::string& db_path = "") + : m_params(params) + , m_db_path(db_path) + { + } + + ~HeaderChain() = default; + + // Disable copy + HeaderChain(const HeaderChain&) = delete; + HeaderChain& operator=(const HeaderChain&) = delete; + + /// Initialize: open LevelDB (if path given), load persisted state. + /// Returns false if LevelDB open fails. + bool init() { + LOG_INFO << "[EMB-BCH] HeaderChain::init() db_path=" << (m_db_path.empty() ? "(in-memory)" : m_db_path) + << " genesis=" << m_params.genesis_hash.GetHex().substr(0, 16) << "..." + << " pow_limit=" << m_params.pow_limit.GetHex().substr(0, 16) << "..." + << " asert_anchor_h=" << m_params.asert.anchor.height + << " asert_halflife=" << m_params.asert.half_life << "s" + << " spacing=" << m_params.asert.target_spacing << "s" + << " allow_min_diff=" << m_params.allow_min_difficulty; + if (!m_db_path.empty()) { + core::LevelDBOptions opts; + opts.write_buffer_size = 2 * 1024 * 1024; // 2MB + opts.block_cache_size = 4 * 1024 * 1024; // 4MB + m_db = std::make_unique(m_db_path, opts); + if (!m_db->open()) { + LOG_WARNING << "[EMB-BCH] HeaderChain LevelDB open FAILED at " << m_db_path; + return false; + } + LOG_INFO << "[EMB-BCH] HeaderChain LevelDB opened at " << m_db_path; + load_from_db(); + } + // Fast-start checkpoint: if chain is empty and a checkpoint is configured, + // seed it as the starting point. All headers before this height are + // implicitly trusted. The chain will sync forward from this point. + if (m_tip.IsNull() && m_params.fast_start_checkpoint.has_value()) { + auto& cp = m_params.fast_start_checkpoint.value(); + IndexEntry entry; + entry.block_hash = cp.hash; + entry.height = cp.height; + entry.chain_work = uint256::ONE; // minimal non-zero work + entry.status = HEADER_VALID_CHAIN; + // Minimal header — we don't have the actual header data, but we + // have the hash. Peers will send headers AFTER this point. The + // null prev_block doubles as the "trusted root" marker for the + // chain walk in get_header_by_height_internal(). + entry.header.m_previous_block.SetNull(); + + put_header_internal(cp.hash, entry); + m_height_index[cp.height] = cp.hash; + mark_height_dirty_internal(cp.height); + m_tip = cp.hash; + m_tip_height = cp.height; + m_best_work = entry.chain_work; + persist_tip(); + LOG_INFO << "HeaderChain: fast-start from checkpoint height=" + << cp.height << " hash=" << cp.hash.GetHex().substr(0, 16) << "..."; + } + return true; + } + + /// Current chain tip (best header). + std::optional tip() const { + std::lock_guard lock(m_mutex); + if (m_tip.IsNull()) return std::nullopt; + return lookup_header_internal(m_tip); + } + + /// Current chain tip height. Returns 0 if empty. + uint32_t height() const { + std::lock_guard lock(m_mutex); + return m_tip_height; + } + + /// Cumulative work of the best chain. + uint256 cumulative_work() const { + std::lock_guard lock(m_mutex); + return m_best_work; + } + + /// Number of headers stored on the best chain. Backed by m_height_index + /// after Phase 1C — m_headers is a bounded cache, not authoritative. + size_t size() const { + std::lock_guard lock(m_mutex); + return m_height_index.size(); + } + + /// Check if we have a header by its SHA256d block hash. Looks in the LRU + /// cache first, then the LevelDB store. + bool has_header(const uint256& block_hash) const { + std::lock_guard lock(m_mutex); + return has_header_internal(block_hash); + } + + /// Get header by SHA256d block hash. Lazy-loads from disk on cache miss. + std::optional get_header(const uint256& block_hash) const { + std::lock_guard lock(m_mutex); + return lookup_header_internal(block_hash); + } + + /// Get header by height (only works for headers on the best chain). + std::optional get_header_by_height(uint32_t h) const { + std::lock_guard lock(m_mutex); + auto it = m_height_index.find(h); + if (it == m_height_index.end()) return std::nullopt; + return lookup_header_internal(it->second); + } + + /// Add a single header. Returns true if accepted (new + valid). + bool add_header(const BlockHeaderType& header) { + PendingTipChange ptc; + { + std::lock_guard lock(m_mutex); + bool ok = add_header_internal(header); + if (ok) { + persist_tip(); + LOG_INFO << "[EMB-BCH] Single header accepted: height=" << m_tip_height + << " hash=" << m_tip.GetHex().substr(0, 16) << "..." + << " bits=0x" << std::hex << header.m_bits << std::dec + << " ts=" << header.m_timestamp; + } + ptc = m_pending_tip_change; + m_pending_tip_change.fired = false; + if (!ok) return false; + } + // Fire tip-changed callback OUTSIDE the mutex to avoid deadlock + if (ptc.fired && m_on_tip_changed) + m_on_tip_changed(ptc.old_tip, ptc.old_height, ptc.new_tip, ptc.new_height); + return true; + } + + /// Set the estimated network tip height (from peer's version message). + /// Used to skip SHA256d PoW validation for old headers during initial sync. + /// Headers below (tip - POW_VALIDATION_DEPTH) are validated structurally + /// only (prev_hash linkage + difficulty retarget), making bulk sync ~100x faster. + /// Set the estimated network tip height (from peer's version message). + void set_peer_tip_height(uint32_t height) { m_peer_tip_height.store(height); } + + /// Dynamically seed a checkpoint at runtime (e.g., from RPC getblockhash). + /// Only applied if the chain is still at or below the hardcoded checkpoint. + /// This allows an accessible daemon to provide a more recent starting point. + void set_dynamic_checkpoint(uint32_t height, const uint256& hash) { + std::lock_guard lock(m_mutex); + if (height > m_tip_height) { + IndexEntry entry; + entry.block_hash = hash; + entry.height = height; + entry.chain_work = uint256::ONE; + entry.status = HEADER_VALID_CHAIN; + entry.header.m_previous_block.SetNull(); + put_header_internal(hash, entry); + m_height_index[height] = hash; + mark_height_dirty_internal(height); + m_tip = hash; + m_tip_height = height; + m_best_work = entry.chain_work; + persist_tip(); + LOG_INFO << "HeaderChain: dynamic checkpoint at height=" + << height << " hash=" << hash.GetHex().substr(0, 16) << "..."; + } + } + + /// Add a batch of headers (from a `headers` P2P message). + /// Returns the number of new headers accepted. + /// Processes in sub-batches to avoid holding the mutex for the entire batch. + /// With fast-sync (PoW skipped for old headers), structural validation + /// is microseconds per header, so large batches are fine. Near the tip + /// (PoW active), each header takes ~20ms, so we use smaller batches. + int add_headers(const std::vector& headers) { + uint32_t peer_tip = m_peer_tip_height.load(std::memory_order_relaxed); + // Large batches during fast-sync (no PoW), small near tip (PoW active) + size_t BATCH_SIZE = (peer_tip > 0 && m_tip_height + 2100 < peer_tip) ? 500 : 50; + int accepted = 0; + PendingTipChange last_ptc; + for (size_t offset = 0; offset < headers.size(); offset += BATCH_SIZE) { + { + std::lock_guard lock(m_mutex); + size_t end = std::min(offset + BATCH_SIZE, headers.size()); + for (size_t i = offset; i < end; ++i) { + if (add_header_internal(headers[i])) + ++accepted; + } + // Capture any pending tip change from this batch + if (m_pending_tip_change.fired) + last_ptc = m_pending_tip_change; + m_pending_tip_change.fired = false; + } + // Yield between batches so ioc-thread callers (get_height, + // is_synced, get_tip) can acquire the mutex. + if (offset + BATCH_SIZE < headers.size()) { + // Short yield during fast-sync (structural only), longer near tip (PoW) + auto ms = (BATCH_SIZE >= 500) ? 1 : 10; + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } + } + if (accepted > 0) { + std::lock_guard lock(m_mutex); + persist_tip(); + // Progress indicator for header sync + uint32_t peer_tip = m_peer_tip_height.load(std::memory_order_relaxed); + if (peer_tip > 0 && m_tip_height > 0) { + double pct = 100.0 * m_tip_height / peer_tip; + // Log every 1% or every 2000 blocks + static uint32_t s_last_logged = 0; + if (m_tip_height - s_last_logged >= 2000 || pct >= 99.9) { + s_last_logged = m_tip_height; + LOG_INFO << "[BCH] Header sync: " << m_tip_height << "/" << peer_tip + << " (" << std::fixed << std::setprecision(1) << pct << "%)"; + } + } + } + // Fire tip-changed callback OUTSIDE the mutex to avoid deadlock + if (last_ptc.fired && m_on_tip_changed) + m_on_tip_changed(last_ptc.old_tip, last_ptc.old_height, last_ptc.new_tip, last_ptc.new_height); + return accepted; + } + + /// Build a block locator for getheaders (BIP 31-style exponential backoff). + /// Returns a list of block hashes from tip back to genesis. + std::vector get_locator() const { + std::lock_guard lock(m_mutex); + return get_locator_internal(); + } + + /// Check if a prev_hash connects to our chain. + bool is_connected(const uint256& prev_hash) const { + std::lock_guard lock(m_mutex); + return has_header_internal(prev_hash); + } + + /// Whether the chain is synced (tip timestamp within DEFAULT_MAX_TIP_AGE of wall clock). + /// Both litecoind and dogecoind use 24 hours (86400s) as DEFAULT_MAX_TIP_AGE. + /// Reference: litecoin/src/validation.h DEFAULT_MAX_TIP_AGE = 24 * 60 * 60 + /// p2pool uses the same gate implicitly: getblocktemplate() fails until + /// litecoind considers itself synced (tip within nMaxTipAge). + bool is_synced() const { + std::lock_guard lock(m_mutex); + if (m_tip.IsNull()) return false; + auto tip_opt = lookup_header_internal(m_tip); + if (!tip_opt) return false; + auto now = static_cast(std::time(nullptr)); + uint32_t age = now - tip_opt->header.m_timestamp; + bool synced = age < core::coin::DEFAULT_MAX_TIP_AGE; // 24 hours (86400s) + // Log state changes (throttled via static) + static bool s_last_synced = false; + if (synced != s_last_synced) { + LOG_INFO << "[EMB-BCH] Sync state changed: synced=" << synced + << " tip_age=" << age << "s height=" << m_tip_height; + s_last_synced = synced; + } + return synced; + } + + /// Get params (for external difficulty validation tests). + const BCHChainParams& params() const { return m_params; } + + /// Register a callback fired when the chain tip changes (reorg or equal-work switch). + /// Signature: void(old_tip_hash, old_height, new_tip_hash, new_height) + using TipChangedCallback = std::function; + void set_on_tip_changed(TipChangedCallback cb) { m_on_tip_changed = std::move(cb); } + +private: + /// Add a single header (caller holds mutex). + bool add_header_internal(const BlockHeaderType& header) { + // Compute hashes + uint256 bhash = block_hash(header); + + // Skip if already known + if (has_header_internal(bhash)) + return false; + + // Genesis block special case: accept if it matches known genesis + if (header.m_previous_block.IsNull()) { + if (bhash != m_params.genesis_hash) { + LOG_WARNING << "[EMB-BCH] REJECT genesis: hash=" << bhash.GetHex().substr(0, 16) + << " expected=" << m_params.genesis_hash.GetHex().substr(0, 16); + return false; // wrong genesis + } + + IndexEntry entry; + entry.header = header; + entry.block_hash = bhash; + entry.height = 0; + entry.chain_work = get_block_proof(header.m_bits); + entry.status = HEADER_VALID_CHAIN; + + put_header_internal(bhash, entry); + m_height_index[0] = bhash; + mark_height_dirty_internal(0); + m_tip = bhash; + m_tip_height = 0; + m_best_work = entry.chain_work; + LOG_INFO << "[EMB-BCH] Genesis accepted: hash=" << bhash.GetHex() + << " bits=0x" << std::hex << header.m_bits << std::dec; + return true; + } + + // Must connect to an existing header + auto prev_opt = lookup_header_internal(header.m_previous_block); + if (!prev_opt) { + LOG_DEBUG_COIND << "[EMB-BCH] ORPHAN header: hash=" << bhash.GetHex().substr(0, 16) + << " prev=" << header.m_previous_block.GetHex().substr(0, 16) + << " — not connected to chain"; + return false; // orphan — not connected + } + + const auto& prev = *prev_opt; + uint32_t new_height = prev.height + 1; + + // Validate PoW — skip expensive SHA256d PoW for old headers during initial sync. + // Headers below (peer_tip - POW_VALIDATION_DEPTH) are validated + // structurally only (prev_hash + difficulty retarget). This makes bulk + // sync ~100x faster: 170k headers × 0.01ms vs 170k × 20ms. + // The depth threshold (2100) covers: 1 difficulty retarget interval (2016) + // + margin for block_rel_height scoring (~72 blocks). + static constexpr uint32_t POW_VALIDATION_DEPTH = 2100; + uint32_t peer_tip = m_peer_tip_height.load(std::memory_order_relaxed); + bool need_pow = (peer_tip == 0) // unknown tip → validate everything + || (new_height + POW_VALIDATION_DEPTH >= peer_tip); + uint256 pow_hash; + if (need_pow) { + pow_hash = sha256d_hash(header); + if (!check_pow(pow_hash, header.m_bits, m_params.pow_limit)) { + LOG_WARNING << "[EMB-BCH] PoW FAIL at height=" << new_height + << " hash=" << bhash.GetHex().substr(0, 16) + << " pow=" << pow_hash.GetHex().substr(0, 16) + << " bits=0x" << std::hex << header.m_bits << std::dec; + return false; + } + } else { + // Structural-only: trust PoW, store zero hash (not needed for old blocks) + pow_hash = block_hash(header); // cheap placeholder (PoW already trusted for old headers) + } + + // Validate difficulty + if (!validate_difficulty(header, new_height)) { + LOG_WARNING << "[EMB-BCH] Difficulty FAIL at height=" << new_height + << " hash=" << bhash.GetHex().substr(0, 16) + << " bits=0x" << std::hex << header.m_bits << std::dec + << " prev_bits=0x" << prev.header.m_bits << std::dec; + return false; + } + + // Build index entry + IndexEntry entry; + entry.header = header; + entry.block_hash = bhash; + entry.height = new_height; + entry.chain_work = prev.chain_work + get_block_proof(header.m_bits); + entry.status = HEADER_VALID_CHAIN; + (void)pow_hash; // PoW already checked above; not stored on BTC since == block_hash + + put_header_internal(bhash, entry); + + // Update tip if this chain has more work, OR if a competing block + // at the same height has equal work (equal-work reorg). + // + // On testnet (min-difficulty), ALL blocks have identical per-block work. + // When our miner and the network both find a block at the same height, + // the `>` check alone never fires — the chain permanently diverges. + // The peer sending us this header represents network consensus (litecoind + // accepted their block), so we switch to it. + // + // Flip-flop is impossible: once both headers are stored, re-receiving + // either is a no-op (line 551 skips known hashes). After switching, + // our mining builds on the new tip, so no new blocks appear on the + // old fork. + bool dominated = entry.chain_work > m_best_work; + bool equal_at_tip = entry.chain_work == m_best_work + && new_height == m_tip_height + && bhash != m_tip; + if (dominated || equal_at_tip) { + uint32_t old_height = m_tip_height; + uint256 old_tip = m_tip; + m_best_work = entry.chain_work; + m_tip = bhash; + m_tip_height = new_height; + // Incremental height index update: if this header extends the + // previous tip (common case during sync), just add one entry. + // Only do a full rebuild on reorgs (tip changed branch). + if (new_height == old_height + 1 && entry.header.m_previous_block == m_height_index[old_height]) { + m_height_index[new_height] = bhash; + mark_height_dirty_internal(new_height); + } else { + rebuild_height_index(bhash); + if (equal_at_tip) { + LOG_WARNING << "[EMB-BCH] EQUAL-WORK REORG at height " << new_height + << ": old_tip=" << old_tip.GetHex().substr(0, 16) + << " new_tip=" << bhash.GetHex().substr(0, 16); + } else if (new_height <= old_height && old_height > 0) { + LOG_WARNING << "[EMB-BCH] REORG detected: old_height=" << old_height + << " new_height=" << new_height << " hash=" << bhash.GetHex().substr(0, 16); + } + } + // Defer reorg callback — will fire AFTER mutex is released by caller. + // Firing inside the lock causes deadlock (callback → getwork → get_header → lock). + m_pending_tip_change.fired = true; + m_pending_tip_change.old_tip = old_tip; + m_pending_tip_change.old_height = old_height; + m_pending_tip_change.new_tip = bhash; + m_pending_tip_change.new_height = new_height; + } + + return true; + } + + /// Validate that the header's nBits matches the expected ASERT difficulty. + /// Unlike BTC there is no 2016-block lookback window — ASERT is computed + /// from the fixed anchor (asert.hpp) plus the tip's height/time, so we need + /// only the immediate predecessor, not an ancestor walk. + bool validate_difficulty(const BlockHeaderType& header, uint32_t new_height) { + if (new_height < 2) return true; // genesis + first block + + // Get tip (the block we're building on). + auto prev_opt = lookup_header_internal(header.m_previous_block); + if (!prev_opt) return false; + const auto& tip = *prev_opt; + + // ASERT is only defined from the anchor block forward. Pre-anchor + // history used BCH's earlier DAA (and pre-Nov-2020 EDA); those blocks + // sit far below any realistic integration checkpoint and are trusted + // structurally during fast-sync. We also MUST NOT call into ASERT below + // the anchor — asert.hpp asserts tip_height >= anchor.height. + if (static_cast(tip.height) < m_params.asert.anchor.height) + return true; + + // Fast-start / dynamic checkpoint seeds carry a synthetic header (null + // prev, zero timestamp/bits). The block building directly on such a seed + // has no real tip time to anchor the ASERT delta — trust it for one + // block; its successors validate against a real tip header. (The true + // genesis also has a null prev but height 0, already handled above.) + if (tip.header.m_previous_block.IsNull() && tip.height != 0) + return true; + + uint32_t expected_bits = get_next_work_required( + tip.height, tip.header.m_timestamp, header.m_timestamp, m_params); + + return header.m_bits == expected_bits; + } + + /// Internal get_header_by_height (caller holds mutex). + std::optional get_header_by_height_internal(uint32_t h) const { + auto it = m_height_index.find(h); + if (it == m_height_index.end()) return std::nullopt; + return lookup_header_internal(it->second); + } + + /// Rebuild height index from a new tip back to genesis (chain walk). + /// Marks every changed height dirty so flush_dirty() persists it. + /// On a one-time legacy migration this walks the full chain — subsequent + /// reorgs walk only as far as the divergence depth. + void rebuild_height_index(const uint256& new_tip) { + // Snapshot old mapping so we only dirty heights that actually change. + std::unordered_map old_index; + old_index.swap(m_height_index); + uint256 current = new_tip; + while (!current.IsNull()) { + auto cur_opt = lookup_header_internal(current); + if (!cur_opt) break; + uint32_t h = cur_opt->height; + m_height_index[h] = current; + auto oit = old_index.find(h); + if (oit == old_index.end() || oit->second != current) + mark_height_dirty_internal(h); + current = cur_opt->header.m_previous_block; + } + // Any heights that were in the old index but not in the new one are now + // orphan-chain entries; we don't have a way to delete LevelDB rows yet, + // so they're left as stale records. Acceptable: reads through + // m_height_index never see them, and the next reorg through the same + // height will overwrite. + } + + /// Block locator: exponential backoff from tip (caller holds mutex). + std::vector get_locator_internal() const { + std::vector locator; + if (m_tip.IsNull()) return locator; + + int64_t step = 1; + int64_t h = static_cast(m_tip_height); + + while (h >= 0) { + auto it = m_height_index.find(static_cast(h)); + if (it != m_height_index.end()) + locator.push_back(it->second); + + if (h == 0) break; + h -= step; + if (h < 0) h = 0; + if (locator.size() > 10) + step *= 2; + } + return locator; + } + + // ─── LevelDB persistence ────────────────────────────────────────────── + // Schema: + // "h" + block_hash(32 bytes) → IndexEntry (serialized as IndexEntryDiskV1) + // "i" + height(4 bytes BE) → block_hash(32 bytes) (Phase 1C — m_height_index persistence) + // "tip" → block_hash(32 bytes) + // "height" → uint32_t (4-byte BE) + + static std::string make_height_key(uint32_t h) { + std::string k = "i"; + char buf[4]; + buf[0] = static_cast((h >> 24) & 0xFF); + buf[1] = static_cast((h >> 16) & 0xFF); + buf[2] = static_cast((h >> 8) & 0xFF); + buf[3] = static_cast(h & 0xFF); + k.append(buf, 4); + return k; + } + + static uint32_t parse_height_key(const std::string& k) { + if (k.size() != 5 || k[0] != 'i') return UINT32_MAX; + return (static_cast(static_cast(k[1])) << 24) + | (static_cast(static_cast(k[2])) << 16) + | (static_cast(static_cast(k[3])) << 8) + | static_cast(static_cast(k[4])); + } + + // ─── Phase 1C: LRU cache helpers ────────────────────────────────────── + + /// Move an existing cache entry to the front of the LRU list (most recent). + void touch_lru_internal(const uint256& hash) const { + auto pit = m_lru_iter.find(hash); + if (pit == m_lru_iter.end()) return; + if (pit->second == m_lru_order.begin()) return; + m_lru_order.splice(m_lru_order.begin(), m_lru_order, pit->second); + } + + /// Evict from the back of the LRU until we're at or below the cap. + /// Dirty entries are never evicted — they have unwritten changes. + void evict_lru_if_full_internal() const { + auto bound = static_cast(HEADER_CACHE_CAP); + size_t guard = 0; + while (m_headers.size() > bound && !m_lru_order.empty() && guard++ < bound) { + const uint256 victim = m_lru_order.back(); + // Don't evict if dirty (would lose unflushed write). + if (m_dirty_headers.count(victim)) { + // Rotate dirty victim to the front so we try a different one. + m_lru_order.splice(m_lru_order.begin(), m_lru_order, std::prev(m_lru_order.end())); + auto it = m_lru_iter.find(victim); + if (it != m_lru_iter.end()) it->second = m_lru_order.begin(); + continue; + } + m_lru_order.pop_back(); + m_lru_iter.erase(victim); + m_headers.erase(victim); + } + } + + /// Insert into cache (or replace existing). Returns pointer into m_headers + /// for the inserted entry. Triggers eviction if over cap. Pointer is stable + /// only until the next cache mutation. + const IndexEntry* insert_into_cache_internal(const uint256& hash, IndexEntry&& entry) const { + auto pit = m_lru_iter.find(hash); + if (pit != m_lru_iter.end()) { + m_lru_order.splice(m_lru_order.begin(), m_lru_order, pit->second); + auto& slot = m_headers[hash]; + slot = std::move(entry); + return &slot; + } + m_lru_order.push_front(hash); + m_lru_iter[hash] = m_lru_order.begin(); + auto [it, inserted] = m_headers.emplace(hash, std::move(entry)); + (void)inserted; + evict_lru_if_full_internal(); + // After eviction, the iterator might still be valid (we don't evict the + // entry we just inserted — it's at the front, eviction starts at back). + return &it->second; + } + + /// Read a single header from LevelDB by block_hash. Returns true if found. + bool try_load_header_from_db_internal(const uint256& hash, IndexEntry& out) const { + if (!m_db || !m_db->is_open()) return false; + std::string key = "h"; + key.append(reinterpret_cast(hash.data()), 32); + std::vector data; + if (!m_db->get(key, data)) return false; + try { + PackStream ps(data); + IndexEntryDiskV1 disk; + ps >> disk; + out = disk.to_entry(); + return true; + } catch (const std::exception& e) { + LOG_WARNING << "[EMB-BCH] try_load_header_from_db: corrupt entry hash=" + << hash.GetHex().substr(0, 16) << " err=" << e.what(); + return false; + } + } + + /// Cache-then-DB lookup. Returns a copy (avoids pointer-stability hazards + /// when callers do follow-up lookups). nullopt = not present anywhere. + std::optional lookup_header_internal(const uint256& hash) const { + auto it = m_headers.find(hash); + if (it != m_headers.end()) { + touch_lru_internal(hash); + return it->second; + } + IndexEntry tmp; + if (!try_load_header_from_db_internal(hash, tmp)) return std::nullopt; + IndexEntry copy = tmp; + insert_into_cache_internal(hash, std::move(tmp)); + return copy; + } + + /// Existence check using cache + DB. Avoids loading the full entry into the + /// cache for transient checks (count() / has_header() pattern). + bool has_header_internal(const uint256& hash) const { + if (m_headers.count(hash) > 0) return true; + if (!m_db || !m_db->is_open()) return false; + std::string key = "h"; + key.append(reinterpret_cast(hash.data()), 32); + return m_db->exists(key); + } + + /// Store an entry in the cache and mark it dirty for the next flush. + /// Use this in place of `m_headers[hash] = entry` to keep LRU + persistence + /// state in sync. + void put_header_internal(const uint256& hash, IndexEntry entry) { + insert_into_cache_internal(hash, std::move(entry)); + m_dirty_headers.insert(hash); + } + + /// Persist m_height_index entry to LevelDB on next flush. + void mark_height_dirty_internal(uint32_t h) { + m_dirty_heights.insert(h); + } + + void persist_header(const IndexEntry& entry) { + // Write-back model (matches Litecoin Core's setDirtyBlockIndex): + // Mark header as dirty — actual DB write happens in flush_dirty(). + m_dirty_headers.insert(entry.block_hash); + } + + /// Flush all dirty headers + height_index + tip to LevelDB in a single + /// atomic WriteBatch with sync=true (fsync). Matches Litecoin Core's + /// FlushStateToDisk() pattern. Caller must hold m_mutex. + void flush_dirty() { + if (!m_db || !m_db->is_open()) return; + if (m_dirty_headers.empty() && m_dirty_heights.empty()) return; + + auto batch = m_db->create_batch(); + int header_count = 0; + int height_count = 0; + + for (const auto& hash : m_dirty_headers) { + auto it = m_headers.find(hash); + if (it == m_headers.end()) continue; // evicted before flush — should never happen given evict_lru protects dirty + + // Serialize as legacy V1 layout (with hash + prev_hash duplicates) + // so a pre-Phase-1B binary can still read what we write. + auto disk = IndexEntryDiskV1::from_entry(it->second); + auto packed = pack(disk); + std::vector data( + reinterpret_cast(packed.data()), + reinterpret_cast(packed.data()) + packed.size()); + + std::string key = "h"; + key.append(reinterpret_cast(hash.data()), 32); + batch.put(key, data); + ++header_count; + } + + // Phase 1C: persist m_height_index dirty entries under "i" + BE_height + for (uint32_t h : m_dirty_heights) { + auto it = m_height_index.find(h); + if (it == m_height_index.end()) continue; + std::vector data(it->second.data(), it->second.data() + 32); + batch.put(make_height_key(h), data); + ++height_count; + } + + // Include tip in the same atomic batch + { + std::vector tip_data(m_tip.data(), m_tip.data() + 32); + batch.put("tip", tip_data); + + uint32_t h = m_tip_height; + std::vector height_data(4); + height_data[0] = (h >> 24) & 0xFF; + height_data[1] = (h >> 16) & 0xFF; + height_data[2] = (h >> 8) & 0xFF; + height_data[3] = h & 0xFF; + batch.put("height", height_data); + } + + if (batch.commit_sync()) { + m_dirty_headers.clear(); + m_dirty_heights.clear(); + LOG_DEBUG_COIND << "[EMB-BCH] flush_dirty: wrote " << header_count + << " headers + " << height_count << " height-index entries (synced)"; + } else { + LOG_ERROR << "[EMB-BCH] flush_dirty: WriteBatch FAILED for " << header_count + << " headers + " << height_count << " height-index entries"; + } + } + + void persist_tip() { + // Write-back: flush all dirty headers + tip atomically. + flush_dirty(); + } + + void load_from_db() { + if (!m_db || !m_db->is_open()) return; + + auto t0 = std::chrono::steady_clock::now(); + + // Phase 1C fast path: load m_height_index directly from "i:" entries. + // This skips the full m_headers RAM residency that pre-Phase-1C used. + auto i_keys = m_db->list_keys("i", 10000000); + int hi_loaded = 0; + if (!i_keys.empty()) { + m_height_index.reserve(i_keys.size()); + for (auto& k : i_keys) { + uint32_t h = parse_height_key(k); + if (h == UINT32_MAX) continue; + std::vector data; + if (!m_db->get(k, data) || data.size() != 32) continue; + uint256 hash; + memcpy(hash.data(), data.data(), 32); + m_height_index[h] = hash; + ++hi_loaded; + } + LOG_INFO << "[EMB-BCH] load_from_db: loaded " << hi_loaded + << " height-index entries directly (Phase 1C fast path)"; + } + + // Load tip + bool need_migration = false; + std::vector tip_data; + if (m_db->get("tip", tip_data) && tip_data.size() == 32) { + memcpy(m_tip.data(), tip_data.data(), 32); + // Lazy-load tip header into the cache so subsequent queries (e.g. + // is_synced()) don't need to hit disk. + auto tip_opt = lookup_header_internal(m_tip); + if (tip_opt) { + m_tip_height = tip_opt->height; + m_best_work = tip_opt->chain_work; + if (hi_loaded == 0) { + // Phase 1C migration: older on-disk data has no "i:" entries. + // Do the legacy walk ONCE to populate m_height_index, persist, + // and let subsequent restarts use the fast path. + need_migration = true; + } + } else { + LOG_WARNING << "[EMB-BCH] Tip hash in DB not found — resetting"; + m_tip.SetNull(); + } + } + + if (need_migration) { + LOG_INFO << "[EMB-BCH] load_from_db: legacy data detected, running one-time " + << "height-index migration (walks back from tip)"; + rebuild_height_index(m_tip); + for (auto& [h, _] : m_height_index) mark_height_dirty_internal(h); + // Flush right away so the next restart hits the fast path. + flush_dirty(); + LOG_INFO << "[EMB-BCH] load_from_db: migration done, " << m_height_index.size() + << " height-index entries persisted"; + } + + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - t0).count(); + LOG_INFO << "[EMB-BCH] load_from_db: ready in " << elapsed << "ms" + << " tip_height=" << m_tip_height + << " tip=" << (m_tip.IsNull() ? "(null)" : m_tip.GetHex().substr(0, 16) + "...") + << " m_headers_cached=" << m_headers.size() + << " m_height_index=" << m_height_index.size(); + } + + // ─── State ──────────────────────────────────────────────────────────── + + BCHChainParams m_params; + std::string m_db_path; + + mutable std::mutex m_mutex; + + // Phase 1C: bounded LRU cache for IndexEntry. The full m_height_index is + // kept in RAM (~46 MB for the BTC mainnet tip), but m_headers is a + // ~3 MB working-set cache backed by LevelDB. Front of m_lru_order is most + // recently used; back is the eviction candidate. m_lru_iter gives O(1) + // moves to front. All three are mutable so that const lookup helpers can + // refresh LRU position / lazy-load on miss. + static constexpr size_t HEADER_CACHE_CAP = 16384; + mutable std::unordered_map m_headers; // block_hash → entry (LRU-bounded) + mutable std::list m_lru_order; + mutable std::unordered_map::iterator, + Uint256Hasher> m_lru_iter; + + std::unordered_map m_height_index; // height → block_hash (best chain only) + + uint256 m_tip; // best chain tip hash + uint32_t m_tip_height{0}; + uint256 m_best_work; + + // Peer-reported tip height for fast-sync PoW skip. + // Set from version message; headers below (tip - 2100) skip SHA256d PoW. + std::atomic m_peer_tip_height{0}; + + std::unique_ptr m_db; + + /// Dirty sets: items modified since last flush (write-back model). + std::unordered_set m_dirty_headers; + std::unordered_set m_dirty_heights; + + /// Callback fired on tip change (reorg / equal-work switch). + TipChangedCallback m_on_tip_changed; + + /// Deferred tip change — fired OUTSIDE mutex to avoid deadlock. + struct PendingTipChange { + bool fired{false}; + uint256 old_tip, new_tip; + uint32_t old_height{0}, new_height{0}; + }; + PendingTipChange m_pending_tip_change; +}; + +} // namespace coin +} // namespace bch diff --git a/src/impl/bch/coin/template_builder.hpp b/src/impl/bch/coin/template_builder.hpp index 1f61e5f71..4c56b2f19 100644 --- a/src/impl/bch/coin/template_builder.hpp +++ b/src/impl/bch/coin/template_builder.hpp @@ -1,27 +1,58 @@ #pragma once -// BCH block-template builder. Mirrors src/impl/btc/coin/template_builder.hpp. +// BCH block-template builder. Mirrors src/impl/btc/coin/template_builder.hpp, +// diverging where BCH consensus requires (M1 §4): +// * No SegWit / witness commitment -- txs serialize as a single canonical +// form; txid == "hash" (no wtxid distinction). `rules` carries no segwit. +// * CTOR (CHIP-2018-11) -- block body txs are re-sorted into +// canonical (ascending txid) order AFTER fee selection, coinbase excluded. +// * ASERT (aserti3-2d, CHIP-2020-05)-- next-block bits come from the ASERT +// DAA (asert.hpp), not BTC's 2016-block retarget. +// * CashTokens (May 2023) -- token-prefixed outputs are carried +// transparently: they live in the tx bytes and round-trip unchanged. +// * HogEx -- NOT applicable (SmartBCH; struck §4.2). // -// >>> 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/M4): CTOR re-sort pass over selected txs before template assembly. +// Coinbase: like the BTC builder, this body does NOT assemble the coinbase tx +// itself -- it emits `coinbasevalue` + `height` and downstream share/work code +// builds the coinbase. The BCH coinbase scriptSig commitment (BIP34 height + +// "/c2pool/" + 32B state_root) is produced by bch::consensus (see +// ../coinbase_commitment.hpp, M3 s19); binding it into the coinbase builder is +// the next M4 slice in the work-source path, NOT here. // -// CashTokens (May 2023) awareness: token-bearing outputs must round-trip -// intact through template assembly. TODO(M3/M4): preserve token prefix bytes. -// HogEx: NOT applicable (see config_coin.hpp scope note). -// -// 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. +// PIN / VERIFY (vs VM300 bchn-bch + p2pool-merged-v36 python ref, staged next): +// * block-size budget is sourced via abla.hpp (CHIP-2023-01): a caller- +// supplied per-tip ABLA State (abla::replay over full-block sizes) raises +// it dynamically, else the safe activation/floor limit (32 MB). The size +// feed is a full-block/daemon-layer concern (M5+) -- the headers-only SPV +// chain does not carry block sizes. +// * `rules` array contents -- confirm against BCHN getblocktemplate output. +#include "header_chain.hpp" +#include "mempool.hpp" +#include "transaction.hpp" #include "block.hpp" #include "rpc_data.hpp" +#include "../coinbase_commitment.hpp" // s19 seam (commitment built downstream) +#include "abla.hpp" // s2: ABLA block-size limit (CHIP-2023-01) + +#include +#include +#include + +#include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + namespace bch { @@ -54,10 +85,256 @@ class CoinNodeInterface { 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) +// ─── BCH Subsidy ───────────────────────────────────────────────────────────── -} // namespace coin +/// BCH block subsidy in satoshis at a given height. BCH inherited Bitcoin's +/// emission verbatim: 50 BCH initial, halving every 210,000 blocks. Identical +/// to the BTC schedule (BCH forked at height 478,558 with the same curve). +/// Reference: BCHN src/validation.cpp GetBlockSubsidy(). +inline uint64_t get_block_subsidy(uint32_t height) { + static constexpr uint64_t COIN = 100'000'000ULL; // satoshis per BCH + static constexpr uint64_t INITIAL_SUBSIDY = 50ULL * COIN; // 50 BCH + static constexpr uint32_t HALVING_INTERVAL = 210'000u; + + int halvings = static_cast(height / HALVING_INTERVAL); + if (halvings >= 64) return 0; + return INITIAL_SUBSIDY >> halvings; +} + +// ─── Merkle Tree (SHA256d, BCH == BTC) ─────────────────────────────────────── + +inline uint256 merkle_hash_pair(const uint256& left, const uint256& right) { + auto sl = std::span(left.data(), 32); + auto sr = std::span(right.data(), 32); + return Hash(sl, sr); +} + +/// Merkle root over txids (SHA256d pairwise, last element duplicated for odd +/// counts). Identical algorithm to BTC -- BCH did not change the merkle rule. +inline uint256 compute_merkle_root(std::vector hashes) { + if (hashes.empty()) return uint256::ZERO; + while (hashes.size() > 1) { + if (hashes.size() & 1u) + hashes.push_back(hashes.back()); + std::vector next; + next.reserve(hashes.size() / 2); + for (size_t i = 0; i < hashes.size(); i += 2) + next.push_back(merkle_hash_pair(hashes[i], hashes[i + 1])); + hashes = std::move(next); + } + return hashes[0]; +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +/// Compact bits as 8-char lowercase hex, matching bchn getblocktemplate. +inline std::string bits_to_hex(uint32_t bits) { + char buf[9]; + std::snprintf(buf, sizeof(buf), "%08x", bits); + return std::string(buf); +} + +// ─── TemplateBuilder ───────────────────────────────────────────────────────── + +/// Builds a BCH block template (WorkData) from a validated HeaderChain and +/// Mempool. WorkData stays layout-compatible with the GBT JSON downstream code +/// (share creation, Stratum) consumes, so p2pool-merged-v36 interop is held. +class TemplateBuilder { +public: + // BCH has no SegWit weight accounting -- the budget is a flat byte size, + // sourced per-network from abla.hpp (see build_template / file head). + static constexpr uint32_t COINBASE_RESERVE = 1'000u; // bytes for coinbase + + static std::optional build_template( + const HeaderChain& chain, + const Mempool& pool, + bool is_testnet = false, + const abla::State* tip_state = nullptr) + { + auto t0 = std::chrono::steady_clock::now(); + + // Block-size byte budget. ABLA (CHIP-2023-01) makes the consensus + // limit dynamic. When the caller supplies a per-tip ABLA State + // (replayed via abla::replay from a BCHN-pinned anchor over full-block + // sizes -- a full-block/daemon-layer feed, NOT the headers-only SPV + // chain) we build to that dynamic limit; otherwise we fall back to the + // activation/floor limit, which ABLA only ever raises -- always a safe + // LOCAL build cap (see abla.hpp head). Either way this is a build-time + // byte budget only: zero p2pool-merged-v36 surface. + const uint64_t max_block_bytes = tip_state + ? tip_state->GetBlockSizeLimit() + : abla::floor_block_size_limit(is_testnet); + + auto tip_opt = chain.tip(); + if (!tip_opt) + return std::nullopt; // chain has no genesis yet + + const IndexEntry& tip = *tip_opt; + uint32_t next_h = tip.height + 1; + uint32_t now_ts = static_cast(std::time(nullptr)); + + // ── Next difficulty via ASERT (aserti3-2d) ───────────────────────── + const BCHChainParams& params = chain.params(); + uint32_t next_bits; + if (static_cast(tip.height) >= params.asert.anchor.height) { + next_bits = get_next_work_required_asert( + tip.height, + static_cast(tip.header.m_timestamp), + static_cast(now_ts), + params.asert); + } else { + // Chain shorter than the ASERT anchor (post-checkpoint cold start): + // fall back to the tip's bits, else pow_limit. + next_bits = (tip.header.m_bits != 0) + ? tip.header.m_bits + : params.pow_limit.GetCompact(); + LOG_INFO << "[EMB-BCH] TemplateBuilder: bits fallback to 0x" + << std::hex << next_bits << std::dec + << " (tip below ASERT anchor)"; + } + + // ── Block version ────────────────────────────────────────────────── + // BCH blocks use version 4 (no BIP9 version-bit signaling -- BCH does + // not soft-fork via version bits). Mirror the tip; floor at 4. + uint32_t block_version = static_cast(tip.header.m_version); + if (block_version < 4u) block_version = 4u; + + // ── Subsidy ──────────────────────────────────────────────────────── + uint64_t subsidy = get_block_subsidy(next_h); + + // ── Mempool selection (fee-sorted) ───────────────────────────────── + auto [selected_txs, total_fees] = + pool.get_sorted_txs_with_fees(max_block_bytes - COINBASE_RESERVE); + // ── CTOR re-sort (CHIP-2018-11) ──────────────────────────────────── + // After fee selection, the block body must be in canonical order: + // ascending txid, coinbase excluded (it is prepended downstream). This + // is consensus-critical on BCH -- a non-CTOR body is rejected. + std::sort(selected_txs.begin(), selected_txs.end(), + [](const Mempool::SelectedTx& a, const Mempool::SelectedTx& b) { + return compute_txid(a.tx) < compute_txid(b.tx); + }); + + uint64_t coinbasevalue = subsidy + total_fees; + + nlohmann::json tx_array = nlohmann::json::array(); + std::vector tx_objects; + std::vector tx_hashes; + + for (const auto& stx : selected_txs) { + uint256 txid = compute_txid(stx.tx); + auto packed = pack(stx.tx); // single canonical form + std::string hex_data = HexStr(packed.get_span()); + + nlohmann::json entry; + entry["data"] = hex_data; + entry["txid"] = txid.GetHex(); + entry["hash"] = txid.GetHex(); // BCH: no wtxid; hash == txid + // Per-tx fee -- p2pool adjusts subsidy when txs are excluded from a + // share (helper.py / data.py). null => python uses base subsidy. + if (stx.fee_known) + entry["fee"] = static_cast(stx.fee); + else + entry["fee"] = nullptr; + tx_array.push_back(std::move(entry)); + + tx_objects.push_back(Transaction(stx.tx)); + tx_hashes.push_back(txid); + } + + // ── GBT-compatible JSON ──────────────────────────────────────────── + nlohmann::json data; + data["version"] = static_cast(block_version); + data["previousblockhash"] = tip.block_hash.GetHex(); + data["bits"] = bits_to_hex(next_bits); + data["height"] = static_cast(next_h); + data["curtime"] = static_cast(now_ts); + data["coinbasevalue"] = static_cast(coinbasevalue); + data["transactions"] = std::move(tx_array); + // BCH: no segwit. ABLA/active-fork rules to be confirmed vs BCHN GBT. + data["rules"] = nlohmann::json::array(); + data["coinbaseflags"] = ""; + data["sizelimit"] = static_cast(max_block_bytes); + data["mintime"] = static_cast(tip.header.m_timestamp + 1); + + LOG_INFO << "[EMB-BCH] TemplateBuilder: height=" << next_h + << " version=" << block_version + << " prev=" << tip.block_hash.GetHex().substr(0, 16) << "..." + << " bits=" << bits_to_hex(next_bits) + << " subsidy=" << subsidy << " fees=" << total_fees + << " coinbasevalue=" << coinbasevalue << " sat" + << " txs=" << data["transactions"].size() + << " (CTOR-sorted) tip_ts=" << tip.header.m_timestamp + << " now=" << now_ts << " synced=" << chain.is_synced(); + + auto t1 = std::chrono::steady_clock::now(); + auto latency_ms = std::chrono::duration_cast(t1 - t0).count(); + return rpc::WorkData{std::move(data), std::move(tx_objects), std::move(tx_hashes), latency_ms}; + } +}; + +// ─── EmbeddedCoinNode ───────────────────────────────────────────────────────── + +/// Concrete CoinNodeInterface backed by a HeaderChain and Mempool. +class EmbeddedCoinNode : public CoinNodeInterface { +public: + EmbeddedCoinNode(HeaderChain& chain, Mempool& pool, bool testnet = false) + : m_chain(chain), m_pool(pool), m_testnet(testnet) {} + + rpc::WorkData getwork() override { + LOG_DEBUG_COIND << "[EMB-BCH] EmbeddedCoinNode::getwork()" + << " chain_height=" << m_chain.height() + << " mempool_size=" << m_pool.size() + << " synced=" << m_chain.is_synced(); + if (!m_chain.is_synced()) { + LOG_INFO << "[EMB-BCH] getwork() blocked: chain not synced (height=" + << m_chain.height() << ")"; + throw std::runtime_error("EmbeddedCoinNode::getwork: chain not synced — waiting for header sync"); + } + auto result = TemplateBuilder::build_template(m_chain, m_pool, m_testnet); + if (!result) { + LOG_WARNING << "[EMB-BCH] getwork() FAILED: no tip (chain empty)"; + throw std::runtime_error("EmbeddedCoinNode::getwork: chain has no tip (not yet synced to genesis)"); + } + return *result; + } + + /// Block relay in embedded mode is handled by CoinBroadcaster, not here. + void submit_block(BlockType& /*block*/) override { } + + nlohmann::json getblockchaininfo() override { + nlohmann::json info; + info["chain"] = m_testnet ? "test" : "main"; + info["blocks"] = static_cast(m_chain.height()); + info["headers"] = static_cast(m_chain.height()); + info["synced"] = m_chain.is_synced(); + + auto tip = m_chain.tip(); + if (tip) { + info["bestblockhash"] = tip->block_hash.GetHex(); + info["bits"] = bits_to_hex(tip->header.m_bits); + } else { + info["bestblockhash"] = std::string(64, '0'); + info["bits"] = "00000000"; + } + return info; + } + + /// UTXO-readiness gate (coinbase maturity = 100 blocks on BCH). + void set_utxo_ready_fn(std::function fn) { m_utxo_ready = std::move(fn); } + + bool is_synced() const override { + if (!m_chain.is_synced()) return false; + if (m_utxo_ready && !m_utxo_ready()) return false; + return true; + } + +private: + HeaderChain& m_chain; + Mempool& m_pool; + std::function m_utxo_ready; + bool m_testnet; +}; + +} // namespace coin } // namespace bch diff --git a/src/impl/bch/coinbase_commitment.hpp b/src/impl/bch/coinbase_commitment.hpp new file mode 100644 index 000000000..472c9da7e --- /dev/null +++ b/src/impl/bch/coinbase_commitment.hpp @@ -0,0 +1,227 @@ +#pragma once +// +// coinbase_commitment.hpp -- Coinbase / template commitment construction and +// validation for BCH p2pool shares (M3->M4 seam). +// +// BCH is a STANDALONE PARENT in V36 (no merged-mining aux module), so the +// coinbase commitment is the BTC c2pool layout MINUS the AuxPoW segment. The +// pinned commitment scriptSig layout (verified vs BCHN reference, 2026-06-16): +// +// [ BIP34 block-height push ] [ "/c2pool/" tag ] [ state_root (32B) ] [ TheMetadata ] +// +// * BIP34 height is CONSENSUS-REQUIRED and the first scriptSig push. BCHN +// enforces it in ContextualCheckBlock ("bad-cb-height", validation.cpp +// ~L3870); the template builder MUST emit it or BCHN rejects the block. +// * NO witness commitment -- SegWit is struck from BCH consensus. The BTC +// OP_RETURN witness-commitment output has no analog here. +// * CTOR (canonical tx ordering, Nov 2018) is a SEPARATE concern handled by +// the template builder's tx sort, NOT part of the coinbase commitment. +// * CashTokens outputs are transparent -- carried unchanged; they do not +// alter the coinbase commitment bytes. +// * state_root is the c2pool sharechain commitment (32 bytes, SHA256d-domain). +// * TheMetadata is the trailing p2pool metadata blob (variable length). +// +// This is the construction the M4 GBT template builder will emit and the share +// validator will check; both must agree, hence a single shared module. Layout +// matches frstrtr/p2pool-merged-v36 -- standalone-parent path, AuxPoW segment +// absent -> NO [decision-needed] (V36 master-compat preserved). +// +// Mirrors the BCH module convention of src/impl/bch/donation_consensus.hpp: +// self-contained, std-typed, header-only, source-only (NOT yet CMake-registered +// on bch/m3-coin-node). +// +// Reference: frstrtr/p2pool-merged-v36 p2pool/bitcoin/data.py (coinbase build), +// BCHN validation.cpp ContextualCheckBlock (BIP34 enforcement). +// + +#include +#include +#include +#include +#include + +namespace bch::consensus +{ + +// The c2pool coinbase tag pushed immediately after the BIP34 height. +inline constexpr char COINBASE_TAG[] = "/c2pool/"; +inline constexpr size_t COINBASE_TAG_LEN = sizeof(COINBASE_TAG) - 1; // exclude NUL +inline constexpr size_t STATE_ROOT_LEN = 32; // SHA256d digest + +// --------------------------------------------------------------------------- +// BIP34 height serialization +// +// BIP34 requires the block height as the first item of the coinbase scriptSig, +// minimally encoded as a CScriptNum (signed, little-endian, with the canonical +// sign-byte rule), preceded by its length as a direct push opcode. Heights are +// always positive and small enough to fit a direct (<=0x4b) push. +// --------------------------------------------------------------------------- + +// Encode `height` as a minimal little-endian CScriptNum byte vector (no opcode). +inline std::vector encode_script_num(int64_t height) +{ + std::vector out; + if (height == 0) + return out; // BIP34: height is never 0 (genesis has no BIP34), but be total + + const bool neg = height < 0; + uint64_t abs = neg ? static_cast(-height) : static_cast(height); + + while (abs) + { + out.push_back(static_cast(abs & 0xff)); + abs >>= 8; + } + + // If the MSB of the top byte is set, the number would read as negative, so + // append a sign byte (0x00 for positive, 0x80 for negative). + if (out.back() & 0x80) + out.push_back(neg ? 0x80 : 0x00); + else if (neg) + out.back() |= 0x80; + + return out; +} + +// Build the full BIP34 height push: [len opcode][little-endian height bytes]. +inline std::vector build_bip34_height_push(int64_t height) +{ + if (height < 0) + throw std::invalid_argument("bch::consensus: BIP34 height must be non-negative"); + + std::vector num = encode_script_num(height); + if (num.size() > 0x4b) + throw std::invalid_argument("bch::consensus: BIP34 height push too large"); + + std::vector push; + push.reserve(num.size() + 1); + push.push_back(static_cast(num.size())); // direct push opcode + push.insert(push.end(), num.begin(), num.end()); + return push; +} + +// Decode a BIP34 CScriptNum (no opcode) back to an integer. Used by the +// validator to confirm the height encoded in a received coinbase. +inline int64_t decode_script_num(const std::vector& num) +{ + if (num.empty()) + return 0; + if (num.size() > 8) + throw std::invalid_argument("bch::consensus: scriptnum too large"); + + int64_t result = 0; + for (size_t i = 0; i < num.size(); ++i) + result |= static_cast(num[i]) << (8 * i); + + // Apply sign from the top byte's high bit. + if (num.back() & 0x80) + { + const int64_t mask = static_cast(1) << (8 * num.size() - 1); + return -(result & ~mask); + } + return result; +} + +// --------------------------------------------------------------------------- +// Commitment construction +// --------------------------------------------------------------------------- + +// Build the BCH coinbase commitment scriptSig payload: +// [BIP34 height push] || "/c2pool/" || state_root(32B) || metadata +// `state_root` must be exactly 32 bytes. `metadata` may be empty. +inline std::vector build_coinbase_commitment( + int64_t height, + const std::vector& state_root, + const std::vector& metadata = {}) +{ + if (state_root.size() != STATE_ROOT_LEN) + throw std::invalid_argument("bch::consensus: state_root must be 32 bytes"); + + std::vector out = build_bip34_height_push(height); + out.insert(out.end(), COINBASE_TAG, COINBASE_TAG + COINBASE_TAG_LEN); + out.insert(out.end(), state_root.begin(), state_root.end()); + out.insert(out.end(), metadata.begin(), metadata.end()); + return out; +} + +// --------------------------------------------------------------------------- +// Commitment validation +// --------------------------------------------------------------------------- + +struct CommitmentValidationResult +{ + bool ok{false}; + std::string error; // human-readable reason when !ok + int64_t height{0}; // parsed BIP34 height + std::vector state_root; // parsed 32B sharechain commitment + std::vector metadata; // parsed trailing TheMetadata blob +}; + +// Parse and validate a coinbase scriptSig against the expected (height, +// state_root). On success, the parsed fields are returned for the caller to +// cross-check. `expected_height` < 0 means "do not enforce a specific height" +// (still parses and returns it). +inline CommitmentValidationResult validate_coinbase_commitment( + const std::vector& script_sig, + int64_t expected_height, + const std::vector& expected_state_root = {}) +{ + CommitmentValidationResult r; + size_t pos = 0; + + // 1. BIP34 height push: [len][bytes]. + if (script_sig.empty()) + { + r.error = "empty coinbase scriptSig (BIP34 height missing)"; + return r; + } + const size_t hlen = script_sig[0]; + if (hlen == 0 || hlen > 0x4b || 1 + hlen > script_sig.size()) + { + r.error = "malformed BIP34 height push"; + return r; + } + pos = 1; + std::vector hbytes(script_sig.begin() + pos, + script_sig.begin() + pos + hlen); + pos += hlen; + r.height = decode_script_num(hbytes); + + if (expected_height >= 0 && r.height != expected_height) + { + r.error = "BIP34 height mismatch (bad-cb-height)"; + return r; + } + + // 2. "/c2pool/" tag. + if (pos + COINBASE_TAG_LEN > script_sig.size() || + std::memcmp(script_sig.data() + pos, COINBASE_TAG, COINBASE_TAG_LEN) != 0) + { + r.error = "missing or wrong /c2pool/ commitment tag"; + return r; + } + pos += COINBASE_TAG_LEN; + + // 3. state_root (32B). + if (pos + STATE_ROOT_LEN > script_sig.size()) + { + r.error = "truncated state_root commitment"; + return r; + } + r.state_root.assign(script_sig.begin() + pos, script_sig.begin() + pos + STATE_ROOT_LEN); + pos += STATE_ROOT_LEN; + + if (!expected_state_root.empty() && r.state_root != expected_state_root) + { + r.error = "state_root commitment mismatch"; + return r; + } + + // 4. Trailing TheMetadata blob (may be empty). + r.metadata.assign(script_sig.begin() + pos, script_sig.end()); + + r.ok = true; + return r; +} + +} // namespace bch::consensus