Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8b0e7d8
bch(m5): share/pool p2p protocol scaffold — peer + messages (M5 slice 1)
frstrtr Jun 17, 2026
c15f392
bch(m5): pool share node + monitor — node.hpp + pool_monitor.hpp (M5 …
frstrtr Jun 17, 2026
fa243fa
bch: M5 sC — live full-block size feed drives ABLA template budget
frstrtr Jun 17, 2026
8965c9a
bch: M5 sD — AblaBlockFeed wires full_block event to ABLA size tracker
frstrtr Jun 17, 2026
fc592b8
bch(m5): emit full_block on blocktxn completion path
frstrtr Jun 17, 2026
e36871e
bch(m5): pin block-arrival emit conformance vs p2poolBCH @6603b79
frstrtr Jun 17, 2026
774426e
bch(m5): merkle-root accept gate before full_block emit; share merkle…
frstrtr Jun 17, 2026
be5cbb2
bch(m5): disable SegWit in share format to conform with p2poolBCH
frstrtr Jun 17, 2026
19c2b74
bch(m5): pin stratum conformance vs p2poolBCH @6603b79 (final sweep leg)
frstrtr Jun 17, 2026
e464cff
bch(m5): AblaRuntime — close embedded-daemon ABLA size loop
frstrtr Jun 17, 2026
a77ccb9
bch(m5): EmbeddedDaemon entrypoint assembly — own+wire coin::Node/Emb…
frstrtr Jun 17, 2026
6afa896
bch(m5): wire CoinNode seam into EmbeddedDaemon — embedded-primary + …
frstrtr Jun 17, 2026
48a4534
bch(m5): wire apply_bchn_anchor() cold-start dry-run from recorded VM…
frstrtr Jun 17, 2026
baa10a5
Merge remote-tracking branch 'origin/master' into bch/m5-share-layer
frstrtr Jun 17, 2026
24c243b
bch(m5): CoinNode seam-contract test — embedded-primary / RPC-fallbac…
frstrtr Jun 17, 2026
994e8f2
bch(m5): ABLA floor-invariant test — cold-start budget never undercut…
frstrtr Jun 18, 2026
821ec05
bch(m4): CashAddr codec port + KAT test (config_coin TODO closed)
frstrtr Jun 18, 2026
d166285
bch(m5): BCHN cold-start anchor-record test — floor no-op + non-floor…
frstrtr Jun 18, 2026
66e4d9d
bch(m5): AblaBlockFeed test — full-block->ABLA wiring (height-resolve…
frstrtr Jun 18, 2026
95c8040
bch(m5): EmbeddedCoinNode::getwork() contract test — sync gate + GBT …
frstrtr Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions src/impl/bch/coin/abla_block_feed.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#pragma once
// ---------------------------------------------------------------------------
// bch::coin::AblaBlockFeed -- M5: drives the ABLA size feed from the REAL
// block-connect path. This is the wiring that was missing after M5 slice C:
// AblaTracker.record_block_size() existed but nothing called it; the running
// State was only ever exercised by the out-of-tree roundtrip harness. This
// feed closes that gap by subscribing to the embedded daemon's full_block
// event and folding each best-chain block's actual serialized size into the
// tracker.
//
// WHY HERE (full-block / daemon layer, not the header chain):
// ABLA advances per block by that block's *actual serialized size*, which
// the headers-only SPV header_chain structurally does not carry (M4 s3 pin /
// M5 PINNED acceptance item). The size therefore must be fed as full blocks
// arrive at the embedded-daemon layer -- exactly where interfaces::Node's
// full_block event fires (p2p_node block + cmpctblock handlers).
//
// CONTIGUOUS-TIP-ONLY by construction. We resolve each block's height from the
// best-chain HeaderChain index (header sync precedes full-block download in the
// BIP130 flow). A block whose hash is not on the indexed best chain (unsolicited
// block, side branch, or one whose header has not been indexed yet) is skipped:
// we cannot assign it a trustworthy height, so we hand the tracker nothing and
// let its own gap-detect do the rest. The tracker already enforces ordering:
// height == cursor+1 -> fold (hot path)
// height <= cursor -> idempotent ignore (duplicate / replayed old block)
// height > cursor+1 -> GAP -> stale -> builder falls back to the 32 MB floor
// until reanchor() supplies a fresh known-good State.
// So a reorg or a skipped block can only ever DROP the budget to the safe
// floor, never raise it on bad data. Reanchoring (BCHN pin / explicit) is a
// separate path; until it runs the floor holds -- the never-undercut invariant.
//
// p2pool-merged-v36 SURFACE: NONE. Identical to abla.hpp / abla_tracker.hpp,
// this governs only the LOCAL build-time block-size budget. It never touches
// PoW hash, share format, coinbase commitment, or PPLNS math -- zero interop
// risk.
//
// PER-COIN ISOLATION: everything here is src/impl/bch/coin/ only. The block
// accessor (full_block event), the height source (HeaderChain), and the sink
// (AblaTracker) are all bch-owned. No bitcoin_family/ or src/core block
// primitive is reached for -- size comes from this module's own block.hpp
// Serialize via pack(), height from this module's own header index.
//
// Build-INERT / source-only: header-only, no impl_bch CMake registration
// (bch stays skip-green; don't race ci-steward).
// ---------------------------------------------------------------------------

#include "abla_tracker.hpp"
#include "block.hpp"
#include "header_chain.hpp" // bch::coin::block_hash(), HeaderChain
#include "node_interface.hpp" // bch::interfaces::Node (full_block event)

#include <core/events.hpp> // Event, EventDisposable
#include <core/pack.hpp> // pack() -> serialized span
#include <core/uint256.hpp>
#include <core/log.hpp>

#include <cstdint>
#include <memory>

namespace bch {
namespace coin {

/// Subscribes to interfaces::Node::full_block and folds each best-chain block's
/// real serialized size into an AblaTracker. Single-threaded use from the
/// daemon's block-processing context is assumed (mirrors how the header chain
/// advances and how AblaTracker documents its own threading contract).
class AblaBlockFeed {
public:
/// @param tracker the ABLA size tracker to feed (daemon-owned; must outlive this).
/// @param chain best-chain header index, the height source for received blocks.
AblaBlockFeed(AblaTracker& tracker, const HeaderChain& chain)
: m_tracker(tracker), m_chain(chain) {}

AblaBlockFeed(const AblaBlockFeed&) = delete;
AblaBlockFeed& operator=(const AblaBlockFeed&) = delete;

/// Wire this feed to a node's full_block event. The returned subscription
/// is retained internally and torn down on destruction (or detach()).
/// Call once, after the node and header chain exist.
void attach(bch::interfaces::Node& node) {
m_sub = node.full_block.subscribe(
[this](const BlockType& block) { on_full_block(block); });
}

/// Drop the subscription early (idempotent). Destruction does this anyway.
void detach() {
if (m_sub) {
m_sub->dispose();
m_sub.reset();
}
}

/// Fold one received full block into the ABLA tracker. Public so the
/// out-of-tree harness can drive it without a live Event/socket; in
/// production it is invoked only by the full_block subscription.
void on_full_block(const BlockType& block) {
// Identity: SHA256d of the 80-byte header (the hash used for inv/getdata
// and the key the header index stores).
const uint256 hash = block_hash(static_cast<const BlockHeaderType&>(block));

// Height source: the best-chain header index. Absent => not on our best
// chain (yet) => skip and let the tracker's gap-detect handle the
// resulting discontinuity (=> floor) rather than guess a height.
const auto entry = m_chain.get_header(hash);
if (!entry) {
LOG_DEBUG_COIND << "[EMB-BCH] ABLA feed: block " << hash.GetHex().substr(0, 16)
<< "... not on indexed best chain -- skipped (gap-detect will floor)";
return;
}

// Size: the block's ACTUAL serialized byte length via this module's own
// legacy (no-witness) block Serialize. This is the consensus serialized
// size ABLA folds through NextBlockState.
const uint64_t serialized_size =
static_cast<uint64_t>(pack(block).size());

m_tracker.record_block_size(entry->height, serialized_size);

LOG_DEBUG_COIND << "[EMB-BCH] ABLA feed: height=" << entry->height
<< " size=" << serialized_size
<< " current=" << (m_tracker.is_current(entry->height) ? "yes" : "no")
<< " budget=" << m_tracker.budget_for_tip(m_tracker.cursor_height());
}

bool is_attached() const { return static_cast<bool>(m_sub); }

~AblaBlockFeed() { detach(); }

private:
AblaTracker& m_tracker;
const HeaderChain& m_chain;
std::shared_ptr<EventDisposable> m_sub;
};

} // namespace coin
} // namespace bch
100 changes: 100 additions & 0 deletions src/impl/bch/coin/abla_runtime.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
#pragma once
// ---------------------------------------------------------------------------
// bch::coin::AblaRuntime -- M5: the CLOSING assembly of the embedded-daemon
// ABLA size loop. The four pieces existed in isolation after slices C/sG:
// AblaTracker -- folds each best-chain block size, holds running State
// AblaBlockFeed -- subscribes to interfaces::Node::full_block, sinks size
// EmbeddedCoinNode -- consumes the tracker for the dynamic build budget
// full_block event -- fires from all 3 p2p delivery paths (direct / cmpct /
// blocktxn) AFTER the merkle-root accept gate (sG)
// ...but nothing OWNED a tracker, owned a feed, and tied them to a live node +
// EmbeddedCoinNode. This runtime is that owner: one object the embedded-daemon
// front-end (coin::Node) holds, constructed once the HeaderChain exists and
// wired once the node + EmbeddedCoinNode exist. After wire():
// full_block --> AblaBlockFeed --> AblaTracker --> EmbeddedCoinNode::getwork
// --> TemplateBuilder dynamic ABLA budget (else 32 MB floor).
//
// COLD START = floor anchor. Absent a BCHN-pinned {height,State}, we anchor at
// the activation/floor State (limit == the 32 MB floor); folding live sizes
// forward can only RAISE the budget, never undercut the floor -- the
// never-undercut invariant holds from the first block. A later BCHN pin is a
// reanchor() passthrough, no reconstruction.
//
// LIFETIME: m_tracker is declared before m_feed so the feed`s AblaTracker&
// binds to a fully-constructed member, and the tracker outlives both the feed
// subscription and the raw pointer handed to EmbeddedCoinNode. The runtime must
// outlive the node it wired (the daemon owns both for its whole run).
//
// p2pool-merged-v36 SURFACE: NONE. Pure local build-time block-size budget --
// no PoW hash, share format, coinbase commitment, or PPLNS math is touched.
// PER-COIN ISOLATION: src/impl/bch/coin/ only; every type reached is bch-owned.
// Build-INERT / source-only: header-only, no impl_bch CMake registration
// (bch stays skip-green; don`t race ci-steward).
// ---------------------------------------------------------------------------

#include "abla.hpp" // abla::State (reanchor / pinned-anchor ctor)
#include "abla_tracker.hpp" // AblaTracker
#include "abla_block_feed.hpp" // AblaBlockFeed
#include "header_chain.hpp" // HeaderChain (height source for the feed)
#include "node_interface.hpp" // bch::interfaces::Node (full_block source)
#include "template_builder.hpp" // EmbeddedCoinNode (set_abla_tracker sink)

#include <core/log.hpp>

#include <cstdint>

namespace bch {
namespace coin {

/// Owns the ABLA tracker + full_block feed for one embedded daemon instance and
/// wires them to a live node + EmbeddedCoinNode. Single-threaded use from the
/// daemon block-processing context (same contract as AblaTracker / feed).
class AblaRuntime {
public:
/// Cold-start: anchor at the activation/floor State for `anchor_height`.
/// Safe default when no BCHN-pinned anchor is available yet.
AblaRuntime(bool is_testnet, uint32_t anchor_height, const HeaderChain& chain)
: m_tracker(AblaTracker::floor_anchored(is_testnet, anchor_height)),
m_feed(m_tracker, chain) {}

/// BCHN-pinned anchor: start from a known-good {height, State} captured at
/// daemon start. Lets the live budget track the real consensus limit from
/// block one instead of climbing from the floor.
AblaRuntime(bool is_testnet, uint32_t anchor_height, abla::State anchor_state,
const HeaderChain& chain)
: m_tracker(is_testnet, anchor_height, anchor_state),
m_feed(m_tracker, chain) {}

AblaRuntime(const AblaRuntime&) = delete;
AblaRuntime& operator=(const AblaRuntime&) = delete;

/// Close the loop: attach the feed to the node`s full_block event and hand
/// the tracker to the EmbeddedCoinNode. Call once, after both exist.
void wire(bch::interfaces::Node& node, EmbeddedCoinNode& embedded) {
m_feed.attach(node);
embedded.set_abla_tracker(&m_tracker);
LOG_INFO << "[EMB-BCH] ABLA runtime wired: full_block -> feed -> tracker"
<< " -> template builder (anchor_height=" << m_tracker.cursor_height()
<< ", budget=" << m_tracker.budget_for_tip(m_tracker.cursor_height())
<< ", " << (m_tracker.is_stale() ? "stale" : "current") << ")";
}

/// Re-establish a known-good anchor after a gap/reorg or on a fresh BCHN
/// pin. Thin passthrough to the tracker; the feed keeps its subscription.
void reanchor(uint32_t height, abla::State state) {
m_tracker.reanchor(height, state);
LOG_INFO << "[EMB-BCH] ABLA runtime reanchored at height=" << height
<< " budget=" << m_tracker.budget_for_tip(height);
}

AblaTracker& tracker() { return m_tracker; }
const AblaTracker& tracker() const { return m_tracker; }
bool is_wired() const { return m_feed.is_attached(); }

private:
AblaTracker m_tracker; // declared first: outlives m_feed + the EmbeddedCoinNode pointer
AblaBlockFeed m_feed;
};

} // namespace coin
} // namespace bch
141 changes: 141 additions & 0 deletions src/impl/bch/coin/abla_tracker.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
#pragma once
// BCH ABLA size-feed tracker (CHIP-2023-01) -- M5 slice C.
//
// This is the FULL-BLOCK / EMBEDDED-DAEMON-LAYER home of the live block-size
// feed that drives ABLA. It exists because of a structural constraint pinned
// in M4 s3: ABLA is advanced by each block's *actual serialized size*, which
// the headers-only SPV `header_chain` does not carry (it stores headers, not
// block bodies / sizes). So the running ABLA State cannot be derived from the
// header chain; it must be fed real per-block sizes as full blocks arrive at
// the daemon layer. That feed re-homes HERE, not in the share layer and not in
// the header chain (M5 PINNED acceptance item).
//
// Relationship to the rest of the size path:
// abla.hpp -- the consensus algorithm (State/Config/replay),
// a 1:1 BCHN v29 port. CONSENSUS-CRITICAL, frozen.
// AblaTracker (this file) -- a thin LOCAL bookkeeping wrapper: it keeps the
// running State at the chain tip by folding live
// block sizes through abla::State::NextBlockState,
// and hands a tip State* to the template builder.
// template_builder.hpp -- consumes the tip State* as a dynamic build-time
// byte budget (build_template's `tip_state` arg).
//
// p2pool-merged-v36 SURFACE: NONE. Like abla.hpp, this governs only the LOCAL
// block-size build budget (how large a template we may assemble). It never
// touches PoW hash, share format, coinbase commitment, or PPLNS math, so there
// is zero interop risk. The hard floor below guarantees the budget can only
// ever EQUAL-OR-EXCEED the 32 MB activation/floor limit -- ABLA only raises it,
// and when the feed is absent or has a gap we fall back to the floor outright.
//
// Build-INERT / source-only: header-only, no impl_bch CMake registration
// (bch stays skip-green; don't race ci-steward).

#include "abla.hpp"

#include <cstdint>

namespace bch {
namespace coin {

/// Keeps the running ABLA State at the chain tip by folding live full-block
/// sizes (oldest-first, contiguous) through the consensus control function.
///
/// Lifecycle / ownership: lives at the embedded-daemon / full-block layer
/// (alongside the block-download path). On each fully-received best-chain block
/// the daemon calls record_block_size(height, serialized_size); the template
/// builder asks state_for_tip(tip_height) when assembling work. Single-threaded
/// use from the daemon's block-processing context is assumed (mirrors how the
/// header chain advances); add external synchronisation if that changes.
class AblaTracker {
public:
/// Construct from a known-good anchor: the ABLA State of `anchor_height`
/// and the matching per-network Config. The natural BCH anchor is a
/// BCHN-pinned {height, State} captured at daemon start; absent that, use
/// floor_anchored() below, which anchors at the activation/floor State.
AblaTracker(bool is_testnet, uint32_t anchor_height, abla::State anchor_state)
: m_config(is_testnet ? abla::testnet_config() : abla::mainnet_config()),
m_is_testnet(is_testnet),
m_cursor_height(anchor_height),
m_cursor(anchor_state),
m_valid(true) {}

/// Anchor at the activation/floor State for `anchor_height`. This is the
/// safe cold-start: the floor State's limit is exactly the 32 MB floor, and
/// folding live sizes forward can only raise it (ABLA never drops below the
/// floor). Use when no BCHN-pinned anchor is available yet.
static AblaTracker floor_anchored(bool is_testnet, uint32_t anchor_height) {
const abla::Config cfg =
is_testnet ? abla::testnet_config() : abla::mainnet_config();
return AblaTracker(is_testnet, anchor_height, abla::State(cfg, 0));
}

/// Live feed: record the serialized size of the best-chain block at
/// `height`. Sizes MUST arrive contiguously (each height one greater than
/// the last); ABLA cannot skip blocks. Behaviour:
/// height == cursor+1 : fold it in, advance the tip State (the hot path).
/// height <= cursor : already incorporated -> ignored (idempotent).
/// height > cursor+1 : a GAP -> the running State is no longer trustable;
/// mark stale so the builder falls back to the floor
/// until reanchor() supplies a fresh known-good State.
/// While stale, further records are ignored until reanchor().
void record_block_size(uint32_t height, uint64_t serialized_size) {
if (!m_valid)
return; // awaiting reanchor() after a gap/reorg
if (height <= m_cursor_height)
return; // duplicate / out-of-order replay of an old block
if (height != m_cursor_height + 1) {
m_valid = false; // gap: cannot fold non-contiguous sizes
return;
}
m_cursor = m_cursor.NextBlockState(m_config, serialized_size);
m_cursor_height = height;
}

/// Re-establish a known-good anchor (after a gap, reorg, or initial BCHN
/// pin). Clears the stale flag and resumes folding from `height`.
void reanchor(uint32_t height, abla::State state) {
m_cursor_height = height;
m_cursor = state;
m_valid = true;
}

/// State governing the block we are about to build on top of `tip_height`.
/// That is the running State AFTER the tip block's size has been folded in,
/// i.e. cursor must sit exactly at the tip. Returns nullptr -- so the
/// template builder uses the floor fallback -- when the feed is stale (a
/// gap occurred) or has not yet caught up to (or has overrun) the tip.
/// The returned pointer is valid until the next record/reanchor call.
const abla::State* state_for_tip(uint32_t tip_height) const {
if (!m_valid || m_cursor_height != tip_height)
return nullptr;
return &m_cursor;
}

/// The dynamic build budget the builder would use right now for `tip_height`
/// -- the live ABLA limit when the feed is current, else the floor. This is
/// the SAME value build_template computes from state_for_tip(); exposed for
/// logging/monitoring. By the ABLA invariant (control >= epsilon0, buffer >=
/// beta0) the live limit is always >= the floor, so this never undercuts the
/// 32 MB floor regardless of feed state.
uint64_t budget_for_tip(uint32_t tip_height) const {
const abla::State* s = state_for_tip(tip_height);
const uint64_t live = s ? s->GetBlockSizeLimit()
: abla::floor_block_size_limit(m_is_testnet);
const uint64_t floor = abla::floor_block_size_limit(m_is_testnet);
return live < floor ? floor : live; // hard floor guard (belt + braces)
}

bool is_current(uint32_t tip_height) const { return m_valid && m_cursor_height == tip_height; }
bool is_stale() const { return !m_valid; }
uint32_t cursor_height() const { return m_cursor_height; }

private:
abla::Config m_config;
bool m_is_testnet;
uint32_t m_cursor_height;
abla::State m_cursor;
bool m_valid;
};

} // namespace coin
} // namespace bch
Loading
Loading