bch(m5): embedded BCHN daemon body — IBD + block-download + ABLA live-feed + dynamic-budget soak - #156
Merged
Merged
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Conformance / isolation:
Opening to run the full CI rollup (Linux x86_64 ctest, ASan/UBSan, Windows, macOS) ahead of the operator-gated master merge.