Skip to content

bch(m5): embedded BCHN daemon body — IBD + block-download + ABLA live-feed + dynamic-budget soak - #156

Merged
frstrtr merged 50 commits into
masterfrom
bch/m5-embedded-body-wip
Jun 19, 2026
Merged

bch(m5): embedded BCHN daemon body — IBD + block-download + ABLA live-feed + dynamic-budget soak#156
frstrtr merged 50 commits into
masterfrom
bch/m5-embedded-body-wip

Conversation

@frstrtr

@frstrtr frstrtr commented Jun 18, 2026

Copy link
Copy Markdown
Owner

M5 embedded-daemon body for the c2pool-bch binary. BCH-only diff (plus additive c2pool-bch binary wiring in src/c2pool/CMakeLists.txt, src/impl/CMakeLists.txt, and new src/c2pool/main_bch.cpp). master 708f644 folded in non-destructively; no peer-coin code touched.

Scope:

  • Headers-first windowed IBD over the P2P front-end with in-flight timeout/eviction and re-issue accounting.
  • BlockConnector: full_block -> mempool reconciliation, UTXO-connect leg, BIP130 ordering edges (orphan-reject, idempotent re-deliver), reorg/disconnect, compact-block (BIP152) ingest.
  • EmbeddedDaemon entrypoint wiring Node/EmbeddedCoinNode/AblaRuntime; external-RPC (BCHN) fallback retained.
  • Live ABLA budget feed: AblaTracker fed from full-block sizes; cursor advances on real VM300 bchn-bch data (955700->955822). Cold-start floor anchor pinned to BCHN block @955700 (operator-approved, floor-equivalent).
  • ABLA dynamic-rise proof: synthetic full-block soak grows budget 32,000,000 -> 41,305,530 over 5000 blocks (strict 4999/5000), testnet fixedSize control pinned. Floor-invariant preserved.

Conformance / isolation:

  • Header-only over coin/abla*.hpp for the budget path; zero p2pool-merged-v36 surface (p2poolBCH carries no ABLA -> purely additive).
  • All changed BCH headers syntax-clean (c++20 -fsyntax-only).
  • ctest seam tests land under src/impl/bch/test (registered in CMake).

Opening to run the full CI rollup (Linux x86_64 ctest, ASan/UBSan, Windows, macOS) ahead of the operator-gated master merge.

frstrtr added 30 commits June 18, 2026 04:22
Split assemble() out of run() so the embedded daemon graph (ABLA loop close
+ embedded-primary CoinNode seam build) can be instantiated and verified
against the REAL EmbeddedCoinNode without the external BCHN-RPC/P2P bring-up.

- embedded_daemon_assembly_test: drives the daemon CLUSTER via its own real
  members -- pre/post seam_ready + is_wired, embedded-primary (is_embedded),
  offline RPC-fallback absent (has_rpc=false), REAL-not-fake (is_synced=false
  on fresh chain), assemble() idempotency (stable CoinNode addr), cold-start
  anchor dry-run floor no-op (VM300 untouched, operator-gated).
- embedded_seam_workview_test: DRIVES the daemon production seam at cold start
  -- get_work_view() propagates the embedded sync gate as a thrown exception
  across the ICoinNode seam; submit_block_hex() is the false guard on the
  no-RPC-sink offline seam (not a throw).

src/impl/bch only; impl_bch stays unregistered (bch=skip-green). Both files
syntax-clean (-fsyntax-only). p2pool-merged-v36 surface: NONE.
Cover the size feed safety claim left unexercised end-to-end through the
feed: a block resolving to a non-contiguous height (missed/replaced block,
header index ahead of the folded cursor) must DROP the build budget to the
32 MB floor and never raise it on data it cannot fold. abla_block_feed_test
only feeds contiguous blocks; abla_floor_invariant_test drives the tracker
directly, bypassing the feed height-resolution seam where a real reorg turns
into a gap. This test routes the gap through AblaBlockFeed::on_full_block and
asserts: gap -> stale, cursor not advanced, budget == floor for every tip;
stale latches (re-feed does not recover); recovery is reanchor()-only.

Source-only, impl_bch unregistered (bch skip-green). Verified -fsyntax-only
EXIT=0 and compile+link+run ALL PASS EXIT=0 (real core objects, no daemon-
graph stubbing). p2pool-merged-v36 surface: NONE.
…-PASS via ctest

Wire src/impl/bch into the build (add_subdirectory(bch) in src/impl) and add
src/impl/bch/test/CMakeLists.txt registering the three ABLA seam tests as plain
int-main CTest executables: bch_abla_floor_invariant_test, bch_abla_block_feed_test,
bch_abla_block_feed_gap_test. Converts syntax-only TUs into real ctest evidence.

Link set mirrors src/impl/ltc/test OBJECT-lib SCC direct-naming (#22/#39):
core pool sharechain c2pool_payout c2pool_merged_mining c2pool_hashrate
c2pool_storage c2pool_difficulty btclibs. No coin lib linked — ABLA tests are
header-only over coin/*.hpp + <core/*>, so per-coin isolation stays clean.

ctest -R bch_abla: 3/3 passed.
…-connect)

M5 full-block body, slice 1. AblaBlockFeed wired full_block -> ABLA budget,
but the block-connect path itself was open: Mempool::remove_for_block() had
ZERO call sites and nothing tied a received best-chain block to the header
index or a mempool reconciliation.

BlockConnector subscribes to interfaces::Node::full_block (the sink all three
P2P delivery paths funnel into) and performs the connect sequence:
  1. block-connect: HeaderChain::add_header (idempotent; no-op on the common
     BIP130 headers-first path, indexes the connect on direct-block delivery);
  2. BIP130 best-chain gate: reconcile ONLY when the block is the current best
     tip (tip->block_hash == block hash, read back from the index, never
     guessed) — side/stale/unconnected blocks leave the mempool untouched;
  3. mempool reconciliation: set_tip_height + remove_for_block (Phase 1 txid,
     Phase 2 same-outpoint conflicts, Phase 3 orphans) + revalidate_inputs
     (Phase 4 stale-input sweep, no-op until a UTXO view is wired).

Test (bch_block_connector_test, real ctest RUN-PASS) pins all three: best-chain
reconcile removes confirmed + conflicting txs while an unrelated tx survives;
off-tip block leaves the mempool fully intact even when it carries a would-be-
removed tx; replay is a clean no-op. 4/4 bch_ ctests green.

Single-coin src/impl/bch/ only; no shared-base / bitcoin_family touch.
p2pool-merged-v36 surface: NONE.
…direct-block connect-advances-tip + idempotent re-deliver

M5 full-block body, slice 2. block_connector_test (slice 1) pinned the
three happy-path decisions against a block that was ALREADY the seeded
checkpoint tip, leaving the connector step-1 add_header() ordering branches
untested. This regression closes them, real ctest RUN-PASS:

  1. OUT-OF-ORDER / ORPHAN: a block whose m_previous_block is not in the
     header index is rejected (orphan), tip does NOT move, mempool stays
     fully intact even though the orphan carries a tx that WOULD be removed
     on connect. No reconciliation off an unconnected block.
  2. DIRECT-BLOCK CONNECT: a block with prev == current tip (no prior synced
     header — mined/peer block msg) is indexed at H+1, becomes the new best
     tip, gate passes, mempool reconciled to it. PoW skipped via far-ahead
     peer-tip (structural-only fast-sync contract); difficulty trusted for
     the single block on the null-prev checkpoint seed.
  3. IDEMPOTENT RE-DELIVERY: replaying the connected tip is a clean no-op.

Single-coin src/impl/bch/ only; same link set + transaction.cpp TU as
block_connector_test (no impl_bch coin lib -> per-coin isolation clean).
p2pool-merged-v36 surface: NONE.
…pproved, floor-equivalent)

decisions@ 2026-06-18 approved the dry-run -> live flip. run() now calls
pin_cold_start_anchor(), which applies the recorded VM300 BCHN anchor
({height=955700, abla::State}) at startup. The recorded control state is at
the 32 MB floor, so the pin is floor-equivalent: it reanchors height/chainwork
provenance (so a future SPV cold-start trusts the recorded header origin
instead of climbing from genesis) without changing the ABLA budget. A future
above-floor capture would RAISE the budget to the real recorded limit; never
undercuts. VM300 stays read-only (static record; no qm op).

Register + wire embedded_daemon_assembly_test (was unregistered): asserts the
live pin keeps the daemon wired and budget_for_tip == 32 MB floor. bch ctest
6/6 RUN-PASS. Zero p2pool-merged-v36 surface (ABLA is BCH embedded-internal).
…ct + live Phase-4 revalidate

BlockConnector now applies each best-chain block to a wired UTXO view via
connect_block() before running revalidate_inputs(), turning the previously
inert Phase-4 stale-input sweep into a real one: a mempool tx whose input
was double-spent/consumed in the authoritative UTXO set is rejected on
connect, while a tx with a live input survives. No-op headers-only when no
view is wired (cold-start contract held). BCH-only; no src/core or
bitcoin_family touch (connect_block is called, not modified). Zero
p2pool-merged-v36 surface.

New bch_utxo_connect_test (ctest -R bch_ now 7/7 RUN-PASS) pins: real sweep
+ double-spend rejection on connect, and the no-view inert contract.
…ector seam

on_compact_block reconstructs from prefilled txs + mempool (BCH short IDs
keyed on txid==wtxid); a missing tx parks the block and returns a
getblocktxn request that on_block_txn closes out, then drives the normal
block-connect path. Retains the BlockUndo connect_block already returned
and closes the reorg leg: on a more-work branch switch, disconnect the
orphaned branch to the fork point (restore inputs, drop created outputs,
return txs to mempool) and reconnect the new branch from a bounded
remembered-block ring. New-branch block deeper than retained history
leaves the view at the fork for resync, never half-applied.

Also gates the header-index equal-work reorg to min-difficulty params
(testnet) only: on mainnet an equal-work competitor no longer replaces
the incumbent tip, restoring BCHN/Bitcoin first-seen-wins and closing a
same-work flip-flop reorg vector. Per-coin (src/impl/bch/ only), calls
existing UTXOViewCache connect/disconnect APIs, zero p2pool-v36 surface.
Cold-start initial block download stalled after one headers batch: the
headers handler fired new_headers and only getdata-requested blocks for a
small BIP130 tip-announce batch (<=3). A maximal batch (== MAX_HEADERS_RESULTS)
-- the peer capping its response because it has more -- was a dead end, so the
header chain (which drives ASERT DAA and the ABLA size feed) could never sync
past the first 2000 headers from the cold-start anchor.

Factor the three-way follow-up policy into a PURE, unit-tested helper
(coin/header_sync.hpp): empty -> Idle; >= cap -> ContinueSync (re-issue
getheaders with a locator anchored at the last header to walk forward to the
peer tip); <= announce threshold -> RequestBlocks; otherwise (partial = caught
up) -> Idle. The headers handler dispatches on it; the existing BIP130 block-
request path is preserved unchanged.

ctest: bch suite 10/10 (new bch_header_sync_test). Header-only, build-inert,
per-coin src/impl/bch/ only; zero p2pool-merged-v36 surface.
…body gap

header_sync drove the HEADER chain to the peer tip (ContinueSync re-issues
getheaders), but those headers blocks were never getdata d: only the BIP130
tip-announce path (<=3 headers) pulled bodies. Cold-start IBD therefore walked
2000-header batches forward forever and downloaded zero block bodies — yet the
embedded daemon ABLA size feed and full-block->mempool reconciliation need real
block data, so IBD could never complete.

block_download.hpp: pure, peer-free BlockDownloadWindow — enqueue learned header
hashes in chain order, keep at most MAX_BLOCKS_IN_FLIGHT (16, BCHN/Bitcoin
per-peer default) getdata(MSG_BLOCK) outstanding, top the window up oldest-first
as each block arrives. Dedupes against queued/in-flight/received so an
overlapping locator batch or re-announce never double-downloads. In-flight
timeout/eviction deferred (robustness once blocks flow).

Wired into NodeP2P: ContinueSync enqueues the batch + drains the window;
the block handler frees the slot on arrival and tops up. bch stays skip-green
(header-only, build-inert). Zero p2pool-merged-v36 surface — pure SPV/IBD
wire-sync; per-coin isolation (src/impl/bch/ only).

bch_block_download_test: window bound, top-up ordering, dedupe, unsolicited,
degenerate-window. ctest 10/10 -> 11/11.
The windowed block download (2cc7de4) deferred stalling-peer handling until
block bodies actually flowed end-to-end -- they now do (NodeP2P enqueues each
synced headers batch and getdatas through the bounded window). This adds the
eviction leg: BlockDownloadWindow now records the issue tick per in-flight
request (next_requests(now_tick)) and exposes expire(now, timeout), which drops
any request older than the timeout, frees its window slot, and re-queues the
stalled hash at the FRONT so it is retried ahead of not-yet-requested tip
blocks. A block delivered after its eviction is reported unsolicited but still
applied. Without this a single non-delivering peer could pin a window slot
forever and wedge IBD.

Periodic-tick wiring into the NodeP2P poll loop is the next micro-slice; this
lands the pure, peer-free state machine + unit coverage first (same shape as the
windowed-download slice).

bch_block_download_test: +2 cases (full-window expiry + re-queue ahead of fresh
hashes; partial expiry leaving fresh requests; post-eviction delivery reported
unsolicited). ctest 11/11 green. Header-only, build-inert (bch skip-green). Zero
p2pool-merged-v36 surface -- pure SPV/IBD wire-sync; per-coin isolation.
…ed-body lane

Non-destructive merge of origin/master into bch/m5-embedded-body-wip to keep
the embedded daemon body lane on current master. No force-push, shared history
preserved. External-RPC fallback retained.
The in-flight block-download window already had a pure expire() state machine
(stalled getdata(MSG_BLOCK) eviction + requeue) but nothing drove it. Wire a
repeating steady_clock timer started at handshake that, every
BLOCK_DL_EXPIRE_TICK_SEC (5s), requeues any request outstanding
>= BLOCK_DL_TIMEOUT_SEC (60s) and re-issues from the freed slots via
drain_block_window(). drain now stamps real monotonic issue ticks
(now_tick_sec, steady_clock seconds since connect) so expiry is measured
against actual elapsed time, not the 0 placeholder. Timer torn down on
disconnect/error alongside ping/timeout.

Pure additive, src/impl/bch/ only, build-inert, zero p2pool-merged-v36 surface.
block_download_test +1 case (tick-driven re-issue cycle): a requeued request
gets a fresh issue tick on redrain and does not immediately re-expire.
expire() now accumulates a lifetime re-issue counter (== total stall
evictions, since every evicted in-flight hash is requeued-and-reissued by
the next drain). reissue_count() exposes it so the read-only VM300 IBD
writeup reports a real measured re-issue metric instead of an inferred
one, and so single-peer demotion heuristics have a stall signal to read.
block_download_test asserts the tally on both the bulk-evict and the
tick-driven re-issue cycles. src/impl/bch only, build-inert, zero
p2pool-merged-v36 surface.
Close the no-runnable-c2pool-bch-entrypoint gap: the M5 lane was pure
state-machine + ctest with no main_bch.cpp, so nothing could be stood up
to drive a live daemon path. Add an additive add_executable(c2pool-bch)
modeled on the c2pool-dgb skeleton, and a main_bch.cpp that drives the
LIVE header-only ABLA template-budget path (impl/bch/coin/abla.hpp, 1:1
BCHN consensus/abla.cpp port) at startup via --selftest.

abla.hpp is std-only, so the target links nothing beyond the standard
runtime and never drags in the core OBJECT-lib web_server/stratum tangle.
selftest proves the two size-loop invariants: GROWTH (5000 full blocks
raise 32MB floor -> 41MB) and FLOOR (5000 empty blocks hold 32MB).

CMake change confined to the new c2pool-bch block; no shared-base/core
edit; src/impl/bch only. Zero p2pool-merged-v36 surface (ABLA carries no
share/coinbase/PPLNS/PoW bytes). Embedded work source stays primary;
external BCHN-RPC fallback retained. The P2P IBD run-loop (read-only
sync vs VM300 .110:8333) and its synced-height/false-eviction/reissue
numbers are the next --ibd slice.
…ers->HeaderChain ingest

Closes the live-IBD-metrics gap ahead of the --ibd run-loop slice:

- block_download: track evicted hashes; on_block_received() flags a
  false-eviction (evicted-then-arrived) so a too-aggressive timeout is
  measurable. 0 on a clean sync.
- embedded_daemon: wire_chain_ingest() subscribes new_headers ->
  m_chain.add_headers() (headers-first IBD: tip tracks peer as batches
  stream; bodies backfill via the download window). Idempotent. Plus
  ibd_synced_height / ibd_peer_tip / ibd_reissue_count /
  ibd_false_evict_count / ibd_in_flight read-only accessors.
- node/p2p_node: p2p() handle + ibd_* counter pass-throughs.

src/impl/bch only; zero p2pool-merged-v36 surface (local SPV header
state only — no PoW/share/coinbase/PPLNS/WorkData change).
Add the --ibd run-loop that closes the exe-wire follow-up gap (integrator
2026-06-18): a read-only headers-first initial-block-download harness that
stands up EmbeddedDaemon over its P2P front-end against one BCHN peer (VM300
bchn-bch 192.168.86.110:8333 by default) and reports the live sync evidence
the M5 size loop rests on each 5s tick:
  - synced height advancing PAST the init() checkpoint (chain-ingest took)
  - false_evict_count (0 == clean sync; non-zero == premature-timeout evict)
  - in_flight / reissue_count (download-window health)

EmbeddedDaemon gains start_ibd() (chain.init + assemble + wire_chain_ingest +
pin_cold_start_anchor + start_p2p; deliberately NOT init_rpc -- this is the
read-only evidence harness, not the production daemon; run() still owns the
external BCHN-RPC fallback, external_fallback invariant intact), plus
ibd_handshake_ready()/ibd_kick_sync() to issue the first locator-anchored
getheaders once version/verack completes; the p2p_node ContinueSync follow-up
self-drives the rest. The run loop exits on caught-up (synced>=peer_tip and no
in-flight) or the --max-seconds deadline. --selftest stays the std-only ABLA
budget driver.

CMake: add ONLY link deps to the c2pool-bch target (core + btclibs + yaml-cpp +
json + Boost), matching the c2pool-dgb tier -- SAFE-ADDITIVE (integrator
cleared it), no shared-base/core SOURCE edit, src/impl/bch headers only.

VM300 stays read-only: a P2P peer connection issues no qm/control op. Per-coin
isolation held; zero p2pool-merged-v36 surface (ABLA + SPV header state carry
no share/coinbase/PPLNS/PoW bytes).
The --ibd harness builds Config without a YAML load, leaving
coin()->m_p2p.prefix empty; core::Socket then framed the version
message with a zero-length magic and BCHN dropped the peer with EOF
immediately after TCP connect (handshake never completed, synced=0).

Set m_p2p.prefix by hand from BCHN chainparams: mainnet e3e1f3e8,
testnet3 f4e5f3f4 — the only field beyond address the IBD path touches.
CMakeLists: link the bch coin TUs + core SCC set into c2pool-bch so the
harness resolves bch:: symbols (src/impl/bch only; per-coin isolation held).

Real read-only sync vs VM300 bchn-bch (BCHN 29.0.0, tip 955816):
handshake up, synced 0->28000+ past cold-start checkpoint, false_evict=0,
reissue=0. External BCHN-RPC fallback path untouched.
The ABLA size feed is wired into the read-only --ibd path (start_ibd ->
assemble -> AblaRuntime::wire), so full blocks streamed off the peer fold
their real serialized size into the tracker. But the evidence loop only
reported header sync + window stall counters, so the size feed was live
yet unobserved. Expose ibd_abla_budget()/ibd_abla_cursor() on
EmbeddedDaemon and print them per-tick and on the final SYNCED/DEADLINE
line. The cursor trails synced height by design (headers-first: only a
folded full block advances it); a moving cursor is the proof the
full_block -> AblaBlockFeed -> AblaTracker loop runs on real VM300 data,
not just the cold-start anchor. Read-only, no p2pool-merged-v36 surface.
…ercise live ABLA feed

The genesis-origin --ibd cold-start cannot advance the ABLA cursor: by
construction the cursor moves only when full blocks arrive contiguously
from the anchor (height == cursor+1), but the anchor sits at
BchnAnchorRecord::height 955700 while a harness-window genesis sync
reaches at most a few ten-thousand headers -- every downloaded block is
height <= cursor, idempotently ignored, cursor pinned at the 32MB floor
(the pre-tip state confirmed on the c4088df evidence loop).

--near-tip seeds the HeaderChain dynamic checkpoint AT the operator-
approved anchor {height,hash} before connecting, so the locator anchors
at 955700 and VM300 bchn-bch streams only the last handful of headers to
its tip; their block bodies fold into AblaTracker as REAL serialized
sizes, advancing the cursor 955700 -> tip. This is the fast-forwarded-
anchor path needed to prove full_block --> AblaBlockFeed --> AblaTracker
on live-network data (M5 live-size re-home acceptance (a): real, not
synthetic). Strictly read-only (seeded checkpoint + P2P pull issue no
qm/control op); no init_rpc() so the external BCHN-RPC fallback is
untouched; anchor hash is the same static record run() pins (no new VM
read). Per-coin isolation: src/impl/bch + c2pool-bch only. Zero
p2pool-merged-v36 surface (local SPV + ABLA budget).
…LA cursor advances

Headers-first block-body download was queued only inside the ContinueSync
branch of the headers handler. The --near-tip IBD seeds the locator at the
BCHN anchor (955700), so the anchor->tip span (~100 headers) classifies as
a non-maximal Idle batch and downloaded NO block bodies: the header chain
reached the peer tip but AblaTracker's cursor stayed pinned at the 32 MB
floor (no full blocks folded).

Hoist the block-body enqueue/drain out of ContinueSync to run for every
non-empty header batch (ContinueSync, RequestBlocks, Idle/partial). The
getheaders re-issue stays ContinueSync-only. block_download window dedupes
overlap and bounds in_flight while accounting reissue/false_evict.

Evidence (read-only vs VM300 bchn-bch 192.168.86.110:8333, mainnet):
  SYNCED synced=955822 advanced=yes abla_cursor 955700->955822
  in_flight=0 reissue=0 false_evict=0 abla_budget=32000000
Cursor motion proves full_block -> AblaBlockFeed -> AblaTracker is live on
real network blocks. Budget == 32 MB floor is correct: near-tip mainnet
blocks are << the ABLA growth threshold (budget = max(floor, computed)).

Conformance vs p2poolBCH/kr1z1sBCH: p2poolBCH carries NO ABLA (defers block
sizing to bitcoind GBT sizelimit), so the embedded ABLA cursor/budget path
is embedded-internal with ZERO p2pool-merged-v36 surface -- additive, the
external-RPC fallback path is retained and unchanged.

Scope: src/impl/bch only. No bitcoin_family/ or src/core/ touch.
Add test/genesis_conformance_test.cpp asserting mainnet/testnet3/testnet4
HeaderChain genesis roots equal the authoritative uint256S/BlockHash literals
in BCHN v29.0.0 src/chainparams.cpp (commit 89a591f) — transcribed from the
reference, not header_chain.hpp, so a silent drift of a genesis constant fails
the pin. Also checks fast_start_checkpoint seeds at height 0 with the genesis
hash, and that testnet4 diverges from the BTC-shared mainnet/testnet3 genesis.

Closes the header_chain.hpp testnet4 TODO(M3) by verifying against the BCHN
source the VM300 bchn-bch node runs. Header-only over coin/header_chain.hpp,
wired into the pure BCH_ABLA_TESTS set. Zero p2pool-merged-v36 surface;
per-coin isolation (src/impl/bch/ only).
…2 MB floor

abla_floor_invariant_test only ever observes the budget AT the 32 MB floor;
live near-tip BCH stays sub-floor so the dynamic-rise path is unprovable
against VM300 data. This fixture drives a synthetic stream of maximally-full
blocks through AblaTracker (the node->feed->tracker fold path) and pins the
GROWTH claim: budget is monotonic non-decreasing under sustained load, a
strong majority of steps strictly increase, and the end state sits strictly
above the floor. Testnet fixedSize ABLA stays pinned at floor under identical
load. Header-only over coin/abla*.hpp; wired into BCH_ABLA_TESTS; per-coin
isolated (no bitcoin_family/ or src/core/ touch); zero p2pool-merged-v36
surface (p2poolBCH carries no ABLA).
…ded-body lane — non-destructive, BCH-only ahead
Drive classify_headers_batch() in the real getheaders->ingest->classify
loop NodeP2P runs, and pin the COMPOSITIONAL properties a single-batch unit
test cannot observe: cold-start IBD converges to peer tip exactly, the loop
is bounded at floor(gap/cap)+1 rounds (exact-multiple gap halts on a trailing
round, never infinite ContinueSync), every mid-IBD round is ContinueSync with
the final round the sole convergence point, and a steady-state single-block
tip announce drives RequestBlocks rather than spurious IBD re-entry.

Header-only over coin/header_sync.hpp; impl_bch stays unregistered (bch =
skip-green). p2pool-merged-v36 surface: NONE. per-coin isolation: bch only.
Port scan_chain_for_blocks into the BCH share_tracker: walk the verified
best chain and fire m_on_block_found for any share whose cached pow_hash
meets the parent BCH block target. Standalone-parent variant of the btc
scan (no merged-coinbase leg; BCH is SHA256d, not merged-mined). O(1)/share
off the init_verify pow_hash cache; returns found-block count for tests.

The m_on_block_found sink is the entry to BOTH block-broadcast paths per the
fleet acceptance gate: embedded P2P (Node::submit_block_p2p) and external
submitblock RPC fallback (CoinNode::submit_block_hex). This lands the EMIT;
real-time verified-path fire + production main_bch wiring + e2e both-path
verify follow. Zero p2pool-v36 surface.
broadcast_won_block() fires BOTH sinks on a won block: submit_block_p2p_raw
(PRIMARY, embedded P2P relay) AND CoinNode::submit_block_hex (FALLBACK, external
BCHN submitblock -- always fired per the dual-path rule, not a P2P-only catch).
A duplicate on the RPC leg after a P2P accept still proves both paths reached
the node; landed_first records which won the race. This is the production sink
the pool node wires tracker().m_on_block_found to, so an in-operation win emits
immediately (not only on the init sweep, 30d7f2c). BCH=SHA256d standalone
parent: no merged-coinbase leg. Read-only vs VM300 (relay/submit issues no qm).
Zero p2pool-merged-v36 surface (block dispatch, not share/PPLNS/coinbase bytes).

embedded_block_broadcast_test: offline contract slice -- with no network sink
live the guard fires (any()=false, landed_first=none, no throw). Live
both-paths-fire+accept vs VM300 192.168.86.110:8333 is gate criterion C, a
separate read-only soak -- NOT claimed here.
Wire NodeImpl.m_tracker.m_on_block_found to a live broadcaster sink so an
in-operation won share fires both broadcast paths (embedded P2P primary +
external BCHN submitblock fallback) via the entrypoint-supplied
set_block_broadcaster() -- the live sink the dispatcher (90a3553) was
waiting on. Mirrors the m_on_share_verified wiring; pool node stays
coin-daemon-agnostic (entrypoint binds to EmbeddedDaemon::broadcast_won_block).

Define reconstruct_won_block (p2pool data.py as_block): probes other_tx
readiness against m_known_txs and returns nullopt until gentx (coinbase)
full-body reconstruction lands -- never broadcasts a partial/invalid block.
if-constexpr guards m_tx_info for non-tx share variants. No p2pool-v36
surface (block dispatch, not share/PPLNS/coinbase bytes).
…ool side)

wire_won_block_sink(NodeImpl&, EmbeddedDaemon&) — THE single startup call
binding tracker().m_on_block_found sink -> dual-path broadcast_won_block
(embedded P2P primary + external BCHN submitblock fallback). Source-only,
ordered before gentx; sink live but emits nothing until reconstruct_won_block
yields a complete block (gentx pending). Zero p2pool-merged-v36 surface.
The three gentx serialization sites (generate_share_transaction +
create_local_share v35/v36) clamped miner payout outputs to 4000 then
appended the donation output separately, yielding 4001 payout vouts when
a PPLNS window exceeds 4000 distinct dest scripts. The oracle (p2pool
data.py) applies [-4000:] over the donation-INCLUSIVE dest list. Reserve
one slot for the always-appended donation so total payout vouts stay
<= 4000, byte-matching oracle. Residual trim-out edge documented in-code;
not reachable in a BCH PPLNS window. Broadcaster-gate gentx prerequisite.
frstrtr added 20 commits June 18, 2026 19:05
Add a defaulted out_gentx_bytes out-param that copies the exact serialized
coinbase buffer the txid is hashed from. Reuses the validated byte path
rather than re-deriving in a second codepath, so the won-block
reconstruction emits a coinbase byte-identical to what consensus and the
sharechain validated. Enables reconstruct_won_block (broadcaster-gate)
to assemble header+gentx+other_txs; existing callers unchanged (param
defaults to nullptr). Coinbase byte-layout conformance vs p2poolBCH
already adjudicated -- no re-derivation, no divergence risk.
Resolve other_txs via the share transaction_hash_refs ref-chain walk
(p2pool data.py get_other_tx_hashes): each (share_count, tx_count)
indexes new_transaction_hashes of the ancestor share_count steps back;
strict equality, nullopt (as_block None) if any ancestor share or tx
body is absent -- never relay a partial block. Reconstruct the coinbase
from the exact out_gentx_bytes buffer the txid was hashed from (no
re-derive), merkle_root via check_merkle_link, 80-byte header per
share_check layout, then serialize header || CompactSize || coinbase ||
other txs. Returns (block_bytes, block_hex) for the dual-path sink.
…ol side)

standup_pool_run(ioc, config, anchor): constructs EmbeddedDaemon<Config> +
NodeImpl on the shared io_context, calls daemon.run() (external BCHN-RPC
fallback + assemble + ABLA + operator-approved cold-start anchor pin), binds
the won-block sink via wire_won_block_sink, and ASSERTS has_block_broadcaster()
is live before ioc.run() -- the structural half of broadcaster-gate criterion
C. A null sink throws (wiring bug, not runtime condition); no won block is
silently dropped. Daemon+node owned for the run-loop lifetime so the
by-reference sink lambda never dangles.

Source-only (impl_bch skip-green). p2pool-merged-v36 surface: NONE -- block
dispatch + run-loop bring-up, not share/PPLNS/coinbase/AuxPoW bytes.
Lets the binary entrypoint assert the broadcaster-gate criterion-C sink is
wired (set_block_broadcaster called) before ioc.run() — the structural half
of the dual-path evidence; the live VM300 e2e is the behavioural half.
Mirror of btc protocol_legacy.cpp/protocol_actual.cpp namespace-swapped to bch.
parse RawMessage via m_handler -> std::visit-dispatch to the typed
handle(msg,peer) HANDLER overload. Routing matches p2poolBCH @6603b79 + btc
oracle exactly. Per-message parse/handler faults logged and swallowed; peer
loop is never torn down on one bad frame. HANDLER bodies follow in later
slices. Both TUs -fsyntax-only clean.
Fill the deferred peer-book handler bodies routed by the Legacy
handle_message std::visit dispatcher. Mechanical mirror of the btc oracle
peer-book handlers, namespace-swapped: gossip-relay learned addrs, answer
getaddrs from get_good_peers, no-op ping. Pure pool-p2p address-book
maintenance; no share/PPLNS/coinbase/AuxPoW surface. Share/tx HANDLER
bodies follow in later slices.
…estblock/have_tx/losing_tx/remember_tx/forget_tx)

Mechanical mirror of btc::Legacy share/tx handlers, namespace-swapped to bch,
with the two standing BCH divergences applied at the type seam:
 - shares: tx-info probed via requires{ obj->m_tx_info; } (version-agnostic,
   segwit-free) instead of btc version>=13 && <34 range.
 - remember_tx: plain pack(tx) for the txid -- no TX_WITH_WITNESS (no witness
   on BCH; wtxid == txid so hash unchanged).
Pure P2P transport/dispatch: share ingest -> processing_shares, sharereq serve
via handle_get_share, tx-relay book-keeping. Zero p2pool-merged-v36 surface;
serialization + PPLNS conformance already verified vs p2poolBCH @6603b79.
Source-only, impl_bch CMake-unregistered (bch=skip-green). -fsyntax-only clean.
…ctual)

Actual-generation handler cluster: addrs/addrme/ping/getaddrs +
shares/sharereq/sharereply/bestblock/have_tx/losing_tx/remember_tx/forget_tx.
Mirrors btc::Actual (NOT a qualifier-swap of bch::Legacy): preserves the
Actual-generation differences (addrme 127.0.0.1 + !m_peers.empty(); shares
raw_copy + 3-arg result.add). Two standing BCH divergences applied at the
type seam: requires{} tx-info probe (segwit-free) and witness-free pack(tx)
in remember_tx. -fsyntax-only EXIT=0. Zero p2pool-merged-v36 surface.
Co-located regtest broadcaster harness prerequisite (integrator 2026-06-18:
leg-C host = co-located self-provisioned regtest, NOT VM300). Generates
RPCUSER=c2pool_bch + openssl-rand RPCPASS, writes localhost-only daemon conf
and matching c2pool external-RPC fallback conf, mode 600, idempotent. Password
never echoed and never enters a coordination card (operator self-provision rule
2026-06-19). BCH-only tooling, zero p2pool-merged-v36 surface.
…y-wip

# Conflicts:
#	src/impl/CMakeLists.txt
…ture

The broadcaster-gate dual-path close requires ONE regtest capture showing
submitblock=accept AND a verbatim BCHN UpdateTip connect-block line. Opaque
dummy bytes prove both sinks fire but not that the node ACCEPTS, so a
consensus-valid block is the load-bearing slice: GBT-derived coinbase + BIP34
height + merkle + nBits + regtest PoW solve.

coin/regtest_block.hpp constructs a solo-coinbase block (empty regtest mempool
=> single coinbase is a complete block), solves the trivially-large regtest
target via a bounded nonce sweep, and emits both wire forms the sinks consume:
raw bytes for the embedded P2P relay and hex for the submitblock RPC fallback.
Coinbase scriptSig layout (BIP34 height push first) matches coinbase_commitment
.hpp, pinned vs BCHN ContextualCheckBlock. build_from_gbt() adapts a BCHN
getblocktemplate result. Construction half only; the live RPC drive +
debug.log capture lands next in main_bch.cpp --leg-c-capture.

Per-coin isolation held: src/impl/bch only, reuses bch BIP34/merkle/block/tx
primitives, no src/core or bitcoin_family edit. p2pool-merged-v36 surface: NONE
(parent BCH block for relay; no share/PPLNS bytes).
…itblock sink

Drives the RPC leg of EmbeddedDaemon::broadcast_won_block
(NodeRPC::submit_block_hex) against the co-located self-provisioned
regtest node. Sources block params off the live tip via
getblockchaininfo (GBT is peer-gated on isolated regtest; submitblock
is not), builds+solves via regtest::build_and_solve, fires submitblock.
RPC conf parsed from bitcoin.conf-style file; password never echoed.
P2P-relay leg (submit_block_p2p_raw) reuses this built blocks raw bytes
as the follow-on sub-slice. src/impl/bch + main_bch only; zero
p2pool-merged-v36 surface.
… broadcaster gate

Closes broadcaster-gate item (1): proves Node::submit_block_p2p_raw (the
PRIMARY sink EmbeddedDaemon::broadcast_won_block fires first) relays a won
block over the live P2P front-end and the node connects it (UpdateTip).

Per integrator verification catch: builds a FRESH block on the current tip
(height N+1), relays P2P-ONLY (no submitblock), so the UpdateTip is
attributable solely to the embedded relay path -- not a re-submit of the
RPC-leg tip (already-have-block false positive). Regtest magic dab5bffa,
default P2P port 18444.

Per-coin isolation: src/impl/bch + main_bch.cpp only; zero p2pool-merged-v36
surface (parent BCH block dispatch, no share/PPLNS bytes).
…icate pin

Pins the one real DGB<->BCH coinbase divergence: BCH gates the SegWit
commitment vout on is_segwit_activated(version), which the p2poolBCH oracle
derives from getattr(net,"SEGWIT_ACTIVATION_VERSION",0) -> 0 -> false for
every share version. Asserts the predicate is OFF across v17..v36 (and the
sentinel stays disabled), so a regression re-introducing a non-zero
SEGWIT_ACTIVATION_VERSION (the old stray-17 port -> sharechain fork) fails CI.
Wired into BCH_ABLA_TESTS (pure, header-only over share_types.hpp).
Byte-vector half (full coinbase vs oracle ground-truth) is the follow slice.

Separate BCH-lane PR per per-coin isolation; #171 stays DGB-tree only.
p2pool-merged-v36 surface: none.
…deImpl

pool_entrypoint.hpp built a bare NodeImpl, which is abstract: the pure
virtual core::ICommunicator::handle(RawMessage, NetService) is provided by
pool::NodeBridge<NodeImpl,Legacy,Actual> (the bch::Node alias), not by
BaseNode. Direct NodeImpl instantiation therefore failed to compile
(cannot declare variable of abstract type). Switch to bch::Node, matching
btc/ltc/dgb. wire_won_block_sink(NodeImpl&) binds fine (Node is-a NodeImpl).
BCH-isolated; zero p2pool-merged-v36 surface.
build.yml configured only -DCOIN_DGB=ON, so the 15 bch_ M5 targets never
compiled and a gh rollup reported GREEN while exercising ZERO BCH (the DGB
137 NOT_BUILT shape, scaled to 15 targets). Add a per-coin COIN_BCH leg
(Linux Release + ASan, mirroring DGB coverage) that configures -DCOIN_BCH=ON,
builds c2pool-bch plus all 15 bch_ targets, and runs ctest scoped to ^bch_.

Coin flags are additive independent if() guards, so -DCOIN_BCH composes with
COIN_DGB in one configure; BCH ships its own binary + own test targets, so a
separate leg gives clean per-coin signal (a BCH break reads as COIN_BCH leg
red, not buried in DGB). Does not relax the per-coin source-presence guards
(PR 47). Network-free: the live VM300 IBD path stays in the c2pool-bch --ibd
harness, not in ctest.
embedded_getwork/seam_workview/block_broadcast/coin_node_seam were
authored alongside the #156 body but never added to CMake or build.yml,
so they built locally yet never ran in CI (authored-but-unrun =
false-green). Add bch_embedded_getwork_test, bch_embedded_seam_workview_test,
bch_embedded_block_broadcast_test, bch_coin_node_seam_test to the bch
test CMakeLists and to BOTH the Linux Release and ASan/UBSan target
allowlists in build.yml, mirroring the #137 NOT_BUILT lesson (test in
CMake must also be in build.yml). Single-coin src/impl/bch-isolated;
no impl_bch coin lib; zero p2pool-merged-v36 surface.
node.hpp generate_share_transaction v36 flag and config_pool.hpp
get_donation_script selection were still using literal (ver >= 36);
#160 covered share.hpp/share_check.hpp/share_tracker.hpp but missed
these two sites. Delegate both to core::version_gate::is_v36_active
(V36_ACTIVATION_VERSION == 36, predicate-identical, zero divergence)
and add explicit includes. Completes the SSOT delegation; no behaviour
change.
…BTC)

The get_donation_script version gate was deliberately left bare in #160
as bucket-3 pre-V36 transition compat (per-coin, temporary, dropped after
the crossing-soak) and to mirror BTC 1:1 (BTC did not route its donation
gate through core::version_gate either). Routing it through the SSOT in
268eeec silently diverged BCH from BTC; revert. Whether the donation
predicate should standardize cross-coin is a [decision-needed] for the
operator/integrator, not a unilateral BCH change. node.hpp SSOT
delegation (genuine #160 miss) is retained.
@frstrtr
frstrtr merged commit cb16987 into master Jun 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant