btc-embedded combined head: Family-1 CoinAdapter + web_server rewire + V36 leak fix (4-coin)#69
Merged
Merged
Conversation
Cloned src/impl/ltc/ -> src/impl/btc/, swept namespaces (ltc -> btc, ltc:: -> btc::), renamed CMake targets (ltc_coin -> btc_coin, library ltc -> btc), wired add_subdirectory(btc) in src/impl/CMakeLists.txt, and added a c2pool-btc binary target with a stub main_btc.cpp returning 0. Real embedded SPV entry point + BTC constants + jtoomim/SPB-compat share v35 + protocol 3502 land in B1+ per frstrtr/the/docs/c2pool-btc-embedded-impl-plan.md. MWEB code carried as dead code; surgical strip is in B3. LTC test subdir disabled in btc/CMakeLists.txt (share_test references LTC types; restore in later test phase). Compile-verified on Release+Conan: c2pool-btc + c2pool both build green. NodeP2P keeps LTC's legacy raw-pointer ctor — Phase 4b ownership inversion is a Dash-broadcaster-specific fix and applies to BTC only if BTC ever grows a multi-peer broadcaster (plan v2 section 5). Branch base: origin/master @ 61a5c87.
…5 + protocol 3502)
config_pool.hpp:
- P2P_PORT 9326 -> 9333 (BTC p2pool sharechain)
- MINIMUM_PROTOCOL_VERSION 3301 -> 3500 (admits jtoomim 3501 + SPB 3502)
- ADVERTISED_PROTOCOL_VERSION 3600 -> 3502 (matches SPB cluster)
- SEGWIT_ACTIVATION_VERSION 17 -> 33 (jtoomim BTC bitcoin.py:35)
- SHARE_PERIOD 15 -> 30 (BTC slower than LTC)
- DUST_THRESHOLD 3000000 -> 100000 satoshis (jtoomim 0.001 BTC)
- MAX_TARGET 0x00000fff... -> 0x00000000ffff... (BTC bdiff 1 = 2^256/2^32-1)
- DEFAULT_PREFIX_HEX 7208c1a53ef629b0 -> 2472ef181efcd37b (jtoomim bitcoin.py:14)
- DEFAULT_IDENTIFIER_HEX e037d5b8c6923410 -> fc70035c7a81bc6f (jtoomim bitcoin.py:13)
- SOFTFORKS_REQUIRED drop {mweb}, keep {bip65,csv,segwit,taproot}
- DEFAULT_BOOTSTRAP_HOSTS replaced LTC list with SPB cluster (4 nodes)
+ jtoomim BOOTSTRAP_ADDRS (ml.toom.im, btc-fork.coinpool.pw, btc.p2pool.leblancnet.us)
Live network probe 2026-04-28 confirmed these via /peer_versions.
share_check.hpp:
- 3x scrypt_1024_1_1_256() -> reuse share_hash (BTC pow_hash = SHA256d(header))
- bech32 hrp "tltc"/"ltc" -> "tb"/"bc" (BTC mainnet/testnet)
- default share_version 36 -> 35 (BTC v35 PaddingBugfixShare per jtoomim
data.py:635 — V36 was LTC's MWEB-aware; BTC stays at v35)
- removed btclibs/crypto/scrypt.h include (no longer used)
share.hpp: pow_hash comment updated scrypt -> SHA256d.
coin/p2p_node.hpp:
- bitcoind protocol version 70017 -> 70016 (BIP 339 wtxidrelay activation,
Bitcoin Core 0.21+; LTC uses 70017 for MWEB)
- service flags: keep NODE_NETWORK | NODE_WITNESS, drop NODE_MWEB
- removed is_doge LTC-DOGE conditional (BTC-specific module now)
- subver "c2pool" -> "c2pool-btc"
- placeholder addr_from port 12024 -> 8333 (cosmetic)
coin/chain_seeds.hpp: full rewrite from ref/bitcoin/src/kernel/chainparams.cpp:
- mainnet: 8 seeds (sipa, bluematt, jonasschnelli, petertodd, sprovoost,
emzy, wiz, achownodes) on port 8333
- testnet3: 5 seeds on port 18333
- testnet4: 2 seeds (sprovoost, wiz) on port 48333 — preferred B2 target
- functions renamed ltc_* -> btc_*
Build verified: c2pool-btc + c2pool both build green on Release+Conan.
Branch: btc-embedded @ HEAD-1 (cumulative on PR-B0 e107b10).
Live network alignment: jtoomim share v35 + protocol 3502 (per
reference_btc_p2pool_live_network.md memory + plan v2 section 3).
Offline B2 work — the network-test portion (header sync against testnet4
bitcoind, UTXO bootstrap) is deferred to a session with a bitcoind
available. main_btc.cpp stays a stub returning 0; real entry-point port
from c2pool_refactored.cpp is owed.
coin/header_chain.hpp:
- LTCChainParams -> BTCChainParams (10 occurrences)
- MAINNET_TARGET_TIMESPAN 302400 -> 1209600 (2 weeks; BTC retarget window)
- MAINNET_TARGET_SPACING 150 -> 600 (10 min)
- TESTNET equivalents: same swap (BTC testnet uses mainnet window with
min-diff override flag)
- pow_limit 0x00000fff... -> 0x00000000ffff... (BTC mainnet powLimit)
- Genesis hashes set to BTC values per ref/bitcoin/src/kernel/chainparams.cpp:
mainnet 000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f
testnet3 000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943
testnet4 00000000da84f2bafbbc53dee25a72ae507ff4914b867c565be350b0da8bf043
- New testnet4() factory (preferred B2-net integration target)
- scrypt_hash() now computes SHA256d via Hash() — function name retained
for API symmetry across coins; LTC's btclibs/crypto/scrypt.h include dropped
- header docstring + retarget header comment updated LTC -> BTC
- IndexEntry.hash field comment: "scrypt for PoW, SHA256d for identity"
-> "SHA256d (same as block_hash on BTC)"
Log prefix sweep across btc/:
- [EMB-LTC] -> [EMB-BTC] (header_chain.hpp, template_builder.hpp, mempool.hpp)
- [LTC] -> [BTC]
Behavioral no-op; cosmetic fix for log greppability.
node.hpp:
- net_name "litecoin"/"litecoin_testnet" -> "bitcoin"/"bitcoin_testnet"
(used as SharechainStorage subdir; isolates BTC sharechain from LTC's)
coin/p2p_node.hpp: comment "/LitecoinCore:0.21.4/" -> "/Satoshi:28.0.0/"
protocol version comment 70017 -> 70016
coin/block_json.hpp: bech32 hrp comment "ltc"/"tltc" -> "bc"/"tb",
P2PKH/P2SH version bytes for BTC
main_btc.cpp: improved stub with phase-status documentation + commented-out
wiring sketch showing the intended io_context + HeaderChain +
coin::Node + start_p2p flow.
Build verified: c2pool-btc + c2pool both build green on Release+Conan.
Out of scope this session (offline limitations):
- Real entry-point port from c2pool_refactored.cpp
- Actual header sync test against testnet4 bitcoind on port 48333
- UTXO bootstrap rolling-288 verification
- MWEB code surgical removal (deferred to B3 per plan §4.4)
Branch: btc-embedded @ HEAD-2 cumulative on B0 (e107b10) + B1 (711af10).
Subsidy formula was the highest-value remaining LTC-ism — c2pool-btc would otherwise compute a 50 BTC -> 25 BTC halving at height 840000 instead of the correct 210000. This is consensus-impacting: every block template would encode the wrong coinbase value at any height >= 210000 and bitcoind would reject the submitted block. template_builder.hpp: - get_block_subsidy() HALVING_INTERVAL 840000 -> 210000 (BTC standard) - doc comments updated LTC -> BTC + reference to ref/bitcoin's GetBlockSubsidy() in src/validation.cpp - top-of-file API doc 'LTC halving schedule' -> 'BTC halving schedule' config_pool.hpp: - DONATION_SCRIPT comments clarified: same script bytes as forrestv/jtoomim BTC p2pool data.py:68 (verified bit-identical 2026-04-28). Encoded address differs by network: 1... on BTC mainnet, L... on LTC. Comment no longer claims it's an LTC-only address. - COMBINED_DONATION_SCRIPT now noted as LTC v36-specific (BTC stays at v35 per plan v2 §3); kept as inert byte data so get_donation_script() compiles for both code paths. softfork_check.hpp: comments 'litecoind / bitcoind' -> 'bitcoind' (this file lives in btc/, c2pool-btc only ever talks to bitcoind). Build verified: c2pool-btc + c2pool both build green on Release+Conan. What still needs B3 surgical attention (deferred to bitcoind-available session): - block.hpp m_mweb_raw, transaction.hpp m_hogEx (MWEB extension) - template_builder.hpp MWEBBuilder calls + 'mweb' rule string - rpc.cpp 'mweb' getblocktemplate parameter (RPC-fallback path; dormant while c2pool-btc is embedded-only) Branch: btc-embedded @ HEAD-3 cumulative on B0/B1/B2.
…oind reject Two MWEB code paths were sending getdata(MSG_MWEB_BLOCK = 0x60000002), an LTC/litecoind-specific inv type that bitcoind does not recognise. bitcoind's net_processing.cpp would respond with peer-disconnect, breaking header-driven block fetching for c2pool-btc. Both callsites (request_full_block + BIP 130 small-batch headers handler) now use inventory_type::witness_block (0x40000002) — BIP 144's witness- bearing block inv type, which bitcoind serves correctly to peers that advertised NODE_WITNESS in their version handshake (we do, set in B1's coin/p2p_node.hpp service flags). Why this isn't surgical MWEB strip (deferred to B3 proper alongside template_builder/mempool/block.hpp surgery in a bitcoind-available session): the MWEB serialization paths in block.hpp + transaction.hpp + template_builder.hpp are already DORMANT for BTC because they're gated on m_hogEx (never set) and mweb_tracker (never instantiated). The getdata code paths above are NOT dormant — they fire on every block fetch — so they had to be fixed now to make B2-net testing viable. LTC code paths in src/impl/ltc/ are unchanged (LTC still uses MSG_MWEB_BLOCK as before; BTC fork only). Build verified: c2pool-btc + c2pool both build green on Release+Conan.
Replaces stub `int main(){return 0;}` with focused header-sync entry point
(~225 lines) modelled on c2pool_refactored.cpp:1500-1900 (LTC's
HeaderChain + EmbeddedCoinNode wiring), pruned to a single-peer
non-broadcaster shape suitable for B2-net smoke testing.
Functionality:
- argv: --testnet | --testnet4 | --bitcoind HOST:PORT
- BTCChainParams selected via mainnet/testnet/testnet4 factory
- HeaderChain at ~/.c2pool/<bitcoin|bitcoin_testnet|bitcoin_testnet4>/
embedded_headers/ with init() retry-on-fail
- btc::Config (composite PoolConfig+CoinConfig) with BTC magic bytes set
per network from ref/bitcoin/src/kernel/chainparams.cpp:
mainnet f9beb4d9
testnet3 0b110907
testnet4 1c163f28
(NodeP2P reads m_config->coin()->m_p2p.prefix to frame outbound bitcoind
messages — wrong magic = peer disconnect.)
- btc::coin::Node<btc::Config> + start_p2p(NetService) — single-peer
connection to specified bitcoind
- new_headers callback forwards to HeaderChain.add_headers() inline
(BTC PoW is SHA256d, fast enough vs LTC's scrypt thread-pool dispatch)
- new_block callback logs received block hashes
- Ctrl-C / SIGTERM clean shutdown via io_context.stop()
What this does NOT do (deferred to later B-phases):
- Sharechain join (no broadcaster, no DNS seeds wiring) — B4
- Stratum server / mining work distribution — B4
- UTXO bootstrap / mempool / template builder — B3
- Web dashboard — later
- RPC fallback — out-of-scope (we're embedded P2P only)
Build verified: c2pool-btc + c2pool both build green on Release+Conan.
Binary 3.3 MB, --help works correctly.
Smoke-test command (when bitcoind testnet4 is reachable):
c2pool-btc --testnet4 --bitcoind 127.0.0.1:48333
Branch: btc-embedded @ HEAD-5 cumulative on B0/B1/B2/B3.4/B3-protocol.
…t fixes
Driving HeaderChain to height 122,000 in 30s smoke test against bitcoind
testnet3 — sustained 2000 headers/sec, zero rejections.
Three changes, all surfaced by the live smoke test:
1. coin/node.hpp: expose `send_getheaders(version, locator, stop)` and
`is_handshake_complete()` as public proxies through the unique_ptr
m_p2p. Previously NodeP2P had send_getheaders but Node template wrapper
didn't expose it — only a broadcaster could drive sync. The simple
single-peer entry point now can.
2. main_btc.cpp: drive header sync proactively. NodeP2P's verack handler
sends sendheaders/sendcmpct/feefilter but NOT getheaders (verified by
reading p2p_node.hpp:485-494). Without external trigger, HeaderChain
stayed at height 0 even though new_block invs flowed. Solution:
3-second post-handshake timer fires getheaders([genesis], 0); on each
new_headers callback, chain locator forward via send_getheaders([
last_hash], 0) when batch is full (>=2000) — peer streams the next
2000. Loop ends when batch < 2000 (caught up). Inline add_headers()
instead of LTC's thread-pool dispatch since BTC PoW is SHA256d (~us
per header) not scrypt (~20ms per header).
3. coin/header_chain.hpp: TWO consensus-critical fixes:
(a) BTCChainParams mainnet/testnet/testnet4 factories now seed
fast_start_checkpoint = {height=0, hash=genesis}. Without this,
a fresh HeaderChain has m_tip=null and rejects every header in
the first batch (no prev_block resolves to anything). The
checkpoint mechanism normally points at a recent height to skip
early IBD; using {0, genesis} just makes genesis the trusted
anchor.
(b) Removed the LTC-specific "Art Forz fix" from get_next_work_required:
int64_t blocks_to_go_back = interval - 1;
if (new_height != interval) blocks_to_go_back = interval;
Replaced with always `interval - 1`. Bitcoin Core's pow.cpp uses
`nHeightFirst = pindexLast->nHeight - (interval-1)` ALWAYS. The
Art Forz adjustment is LTC-only — LTC shipped with the off-by-one,
patched it via height gating because the chain locked the bug in.
BTC has no such history; the LTC inheritance was a bug. Manifested
as "[EMB-BTC] Difficulty FAIL at height=10080 hash=000000007c4fc01a
bits=0x1d00ffff prev_bits=0x473956288" — wrong nBits at the 5th
testnet3 retarget point, freezing chain at height 10079.
Smoke-test before/after:
before: chain_height stuck at 10079 (retarget failure), all subsequent
batches accepted=0
after: chain_height progresses 2000 -> 4000 -> ... -> 122000 in 30s,
accepted=2000 per batch, no failures
Reference: ref/bitcoin/src/pow.cpp GetNextWorkRequired() +
CalculateNextWorkRequired().
Branch: btc-embedded @ HEAD-7 cumulative on B0..B2-net.
Subscribe to coin_node.full_block to maintain a persistent UTXO set as tip blocks arrive. The p2p_node already auto-requests every inv'd block with MSG_WITNESS_BLOCK (B3-protocol fix), so witness data is intact — txid (BIP 144 non-witness SHA256d) is the UTXO key. Per-block flow on full_block: 1. Look up height in HeaderChain via get_header(block_hash) 2. utxo_cache.connect_block(block, height, btc_txid) → BlockUndo 3. utxo_db.put_block_undo(height, undo) 4. utxo_cache.flush(block_hash, height) 5. utxo_cache.prune_undo(height, BTC_KEEP_DEPTH=288) Reuses core::coin::LTC_LIMITS — both chains share max_money≤2.1e15<8.4e15 (LTC's bound never falsely rejects a BTC value) and 100-block coinbase maturity. pegout_maturity=6 is moot for BTC (no MWEB → no pegouts). Smoke-validated on VM 420 (192.168.86.121) bitcoind testnet3 4,952,593: [UTXO-DB] opened at true best_height=0 best_block=0000000000000000 [BTC] UTXO loaded: best_height=0 best_block=0000000000000000 [BTC] HeaderChain initialized: size=1 height=0 [BTC] new_headers: ... (~5000 hdr/s, no regression) Closing LevelDB store at: ~/.c2pool/bitcoin_testnet/utxo_view_db Closing LevelDB store at: ~/.c2pool/bitcoin_testnet/embedded_headers
BTC has no MWEB. The dormant MWEB code was inherited from the LTC clone
during Phase B0 scaffold; it's a confusion hazard and a serialization
landmine — bitcoind would not send a HogEx tx, but the parser branches
were still live. Strip:
block.hpp — drop m_mweb_raw field + serialize/unserialize MWEB tail
transaction.hpp — drop m_hogEx flag (Transaction + MutableTransaction)
+ drop flags & 0x08 MWEB branch in (Un)SerializeTransaction
transaction.cpp — drop m_hogEx out of all 4 ctors
template_builder — drop #include "mweb_builder.hpp" + MWEBTracker arg
+ drop the ~30-line MWEB HogEx + empty-mweb-block build
+ drop "mweb"/"!segwit"/"taproot"/"csv" from rules array
+ drop data["mweb"] field
+ EmbeddedCoinNode no longer takes/holds MWEBTracker*
rpc.hpp/.cpp — drop the 'mweb' arg from submit_block + submit_block_hex
(no MWEB tail to append to submitblock hex)
+ getblocktemplate rules → just {"segwit"} (was {"segwit","mweb"})
mweb_builder.hpp — DELETED (now orphan; no remaining caller)
Side fix in p2p_node.hpp inv handler: case inventory_type::block now
upgrades to MSG_WITNESS_BLOCK (0x40000002) on getdata, matching what
request_full_block already does. We advertise NODE_WITNESS in version,
so the peer is entitled to expect us to honor BIP 144 for inv-driven
fetches too — otherwise witness data is stripped.
core/coin/utxo_view_cache.hpp::connect_block now gates tx.m_hogEx via
`if constexpr (requires { tx.m_hogEx; })` so it still compiles for LTC
(which has the field) and for BTC (which no longer does).
Smoke-validated on VM 420 (192.168.86.121) bitcoind testnet3 4,952,593:
[UTXO-DB] opened at true
[BTC] HeaderChain initialized: size=1 height=0
[CoinP2P] version: start_height=4952593 services=0xc09
[CoinP2P] Peer supports compact blocks v2
[BTC] new_headers: ... (~5000 hdr/s, no regression)
Scaffold-only entry point for joining the live BTC p2pool network at
p2p-spb.xyz:9333 (jtoomim/SPB v35 + protocol 3502). Adds:
--p2pool HOST:PORT CLI flag (parsed; outbound dial deferred to B4-net)
When provided, c2pool-btc logs the full broadcaster identity contract on
startup so we can sanity-check what we'd advertise BEFORE wiring the full
NodeBridge:
advertised protocol: 3502 (PoolConfig::ADVERTISED_PROTOCOL_VERSION)
minimum accepted protocol: 3500 (PoolConfig::MINIMUM_PROTOCOL_VERSION)
share format: VERSION 35 (PaddingBugfixShare)
listen P2P_PORT: 9333 (PoolConfig::P2P_PORT)
prefix (hex): 2472ef181efcd37b
identifier (hex): fc70035c7a81bc6f
The full c2pool sharechain peer (pool::NodeBridge<NodeImpl, Legacy, Actual>)
already exists at src/impl/btc/node.{hpp,cpp} from the B0 scaffold —
2230 LOC cloned from LTC and re-namespaced. Wiring it requires
btc::Config(yaml) + btc::ShareChain + listener bind + accept loop +
ShareTracker + outbound peer dialer. That instantiation is B4-net scope.
Verified on VM 420 with --p2pool p2p-spb.xyz:9333 — all eight identity
fields render with the expected B1 constants.
Replaces the B4-scaffold log block with real wiring:
config.pool()->m_prefix = ParseHexBytes(prefix_hex()) [2472ef181efcd37b]
config.pool()->m_bootstrap_addrs = --p2pool target | DEFAULT_BOOTSTRAP_HOSTS
btc::Node = pool::NodeBridge<NodeImpl, Legacy, Actual> over (&ioc, &config)
node->set_target_outbound_peers(--p2pool ? 1 : 4)
node->core::Server::listen(9333)
node->start_outbound_connections()
NodeImpl ctor opens ~/.c2pool/<net>/sharechain_leveldb for share
persistence, seeds the addr store from m_bootstrap_addrs, and registers
all p2pool wire handlers (addrs/shares/sharereq/bestblock/etc.). Listener
accepts inbound peers; outbound timer dials target peers every 30s.
Side fix: initial getheaders timer in main_btc.cpp was passing
uint256::ZERO as locator on warm restart. Replaced placeholder with
header_chain.tip()->block_hash so the next batch starts from our actual
tip rather than re-sending headers we already have.
Smoke-validated on VM 420 (192.168.86.121):
[BTC] HeaderChain initialized: size=40001 height=40000 (warm restart)
[Pool] LevelDB sharechain storage opened: bitcoin_testnet (0 shares)
Factory started for port: 9333 (listener)
[BTC] Sharechain peer listening on port 9333 —
proto adv=3502 min=3500 share=v35 prefix=2472ef181efcd37b
[Pool] Dialing outbound peer p2p-spb.xyz:9333 (outbound dial)
[BTC] Sending initial getheaders, locator=000000007c0cee02
(chain_height=40000) (locator fix)
[BTC] new_headers: chain_height=42000..50000 (sync continues)
Note: live target p2p-spb.xyz:9333 returns "Connection refused" — likely
peer-whitelisting or the SPB cluster only accepts inbound from known
operators. Wire identity values (prefix/proto/share-version) are bit-
correct vs PoolConfig — actual handshake validation requires a peer
that accepts us.
When `--p2pool HOST:PORT` is given, the user has explicitly chosen which peer they want. Before this commit, that target was just one entry in the bootstrap_addrs list — but `core::AddrStore` ctor reads `addrs.json` from disk on construction, MERGING any saved peers from prior runs. NodeImpl's get_good_peers scores by first_seen/last_seen, so a saved peer with older timestamps could outrank our just-added explicit target, leading c2pool-btc to dial something other than what the user named. Verified during 3-min soak validation 2026-04-29: with `--p2pool 192.168.86.122:9333`, c2pool-btc actually connected to a saved `31.25.241.224:9335` from a prior 25-s test, never reaching .122. The result was still useful (real wild-network validation) but not what the flag's name implies. Fix: when --p2pool is given, delete `~/.c2pool/<net>/addrs.json` before constructing the Node. AddrStore ctor sees no file, creates an empty one, and only our 1-entry bootstrap_addrs populates it. The store re-fills naturally as the named peer shares its addr list. Verification: - Pre-seed addrs.json with competing peer 31.25.241.224:9335 (year-2024 first_seen for high score) - Run with --p2pool 192.168.86.122:9333 - Log: "Dialing outbound peer 192.168.86.122:9333 → connected" (NOT the seeded competitor)
Standalone parity-check harness covering the three algorithmic primitives
underlying template_builder:
test_subsidy — 12 cases across every halving boundary (genesis through
epoch 5 + post-64th-halving zero) verifying right-shift
halving math, satoshi unit, and off-by-one at boundaries.
test_merkle — 5 cases: 1-tx tree (genesis case: root==txid), 2-tx
determinism + non-pass-through, 3-tx-duplicate-last rule
(root(3) == root(4-with-dup-last)), empty-tree zero.
test_daa — 2 LIVE BTC mainnet retargets (heights 945504 and 943488)
captured 2026-04-29 from bitcoind 28.1.0 on .40, fed
through calculate_next_work_required(), verified bit-exact
against the actual on-chain bits values.
Result: 19/19 pass. Notably the DAA bit-exact match against
0x17021369 and 0x17020684 retarget outcomes confirms our Art-Forz removal
fix from B2-net++ ships correct math for current BTC retarget windows
(interval - 1 = 2015 back-step, no LTC-style off-by-one for non-first
retargets).
Build instructions in the file's docstring — standalone compile via g++
linking against build/src/core/libcore.a + build/src/btclibs/libbtclibs.a.
Not yet wired to CMake's `make test` because src/impl/btc/test/ subdir is
disabled (share_test.cpp still references LTC types from B0 scaffold —
tracked in MEMORY.md test rot section).
Wires submit_block(block, height) lambda + pending_submits map + new_headers-based roundtrip detector + 30s stale-submit warn timer into main_btc.cpp. No producer yet — submit_block is [[maybe_unused]] and dormant until stratum / found-block hook lands; the path is build-validated and runtime-validated (clean startup, no spurious [BTC-SUBMIT] noise). Confirmation comes via HeaderChain / new_headers, not on_full_block: bitcoind tracks per-peer m_inv_known_blocks and won't echo our own MSG_BLOCK back to us, so the natural arrival point is the next header-sync batch that includes our submitted hash. Logs: [BTC-SUBMIT] sending block H height=N (on submit) [BTC-SUBMIT] roundtrip CONFIRMED ... latency=Nms [BTC-SUBMIT] STALE: block H pending Ns (>60s, bitcoind likely rejected) std::map (not unordered_map) — uint256 has operator< via base_uint ::CompareTo, no std::hash<uint256> specialization in this codebase. Recursive warn-timer scheduler uses weak_ptr to itself to break a self-referencing shared_ptr cycle.
Stage 1.1 of B4-stratum / IWorkSource extraction. Pure additive: a new header that defines the 6 data types crossing the stratum API boundary (StratumConfig, RefHashResult, JobSnapshot, WorkSnapshot, CoinbaseResult, WorkerInfo) in `core::stratum::` namespace. Nothing yet uses them — they will replace the nested struct definitions inside `core::MiningInterface` in stage 1.2 via `using` aliases. Why hoist: B4-stratum needs both LTC's `core::MiningInterface` and BTC's `btc::stratum::BTCWorkSource` to implement the same `IWorkSource` interface that drives `core::StratumServer`. The interface methods take/ return these types, so they must live in a header both implementors can include without circular deps. Defining them inside MiningInterface forces every consumer to drag in the full 8500-LOC `web_server.hpp`, which is wrong for BTC and a layering inversion in general. Shape neutrality verified: every field is a primitive, uint256/uint128, or a stdlib container of those. No coin-specific types. The `mweb` field on JobSnapshot/WorkSnapshot stays empty for non-MWEB chains. Defaults `share_version=36` `desired_version=36` are LTC's defaults; non-LTC implementors override at construction. Build: c2pool + c2pool-btc both green. No behavior change.
Stage 1.2 of B4-stratum / IWorkSource extraction. Replaces 5 nested struct definitions inside MiningInterface (RefHashResult, JobSnapshot, WorkSnapshot, CoinbaseResult, WorkerInfo) plus the file-scope StratumConfig with `using` aliases pointing at the canonical definitions in core::stratum (added in stage 1.1, e47a2a4). External call sites are unaffected — `MiningInterface::JobSnapshot`, `core::StratumConfig`, etc. all continue to resolve. Aliases preserve binary + source compatibility. Build green: c2pool (LTC) + c2pool-btc both link clean. Pre-existing memcmp size-warning in c2pool_refactored.cpp:5830 is unrelated.
Stage 1.3 of B4-stratum / IWorkSource extraction. Pure additive: a new header defining the abstract interface that core::StratumServer will hold instead of a concrete LTC MiningInterface. No implementor yet — this commit only declares the API. Surface area: 13 methods + 2 atomic getters, derived by exhaustive audit of every `mining_interface_->*` call in stratum_server.cpp plus the three direct `m_share_bits` / `m_share_max_bits` member accesses (now expressed as virtual `get_share_bits()` / `get_share_max_bits()`). Each method documented with its contract: lifetime, threading, the "snapshot consistent with template state" invariant for work generation, and the share/block target classification for mining_submit. Why an interface (not a structural copy of stratum_server.cpp into src/impl/btc/): single source of truth for 1500 LOC of stratum protocol code. Bug fixes flow to LTC and BTC simultaneously. Future BCH/DGB ports get this for free. The runtime cost is a virtual call on a path measured in shares-per-second — well below the floor of meaningful overhead. Build green: c2pool + c2pool-btc both link clean. No behavior change.
Stage 2.1 of B4-stratum / IWorkSource extraction. The compiler-checked
moment: MiningInterface now inherits from core::stratum::IWorkSource and
every one of the 15 IWorkSource methods is annotated `override`. Build
gates pass — all 15 signatures exactly matched their existing
non-virtual MiningInterface counterparts on first compile, with no
adjustments needed. (One pre-commit fix to IWorkSource: changed
get_current_gbt_prevhash return type from uint256 to std::string to
match MiningInterface's actual return — the real method returns BE
display-hex form, not the raw uint256.)
Two new accessors added to MiningInterface for the atomic state that
stratum_server.cpp previously read via direct member access:
uint32_t get_share_bits() const override { return m_share_bits.load(); }
uint32_t get_share_max_bits() const override { return m_share_max_bits.load(); }
The atomic members themselves stay on MiningInterface for now — moving
them to a SharedState struct would force write-side refactoring for
zero benefit since IWorkSource only needs read access.
Behavior change for LTC: NONE. Single concrete IWorkSource implementor
means devirt-by-vtable is functionally identical to non-virtual call.
Vtable adds ~16 bytes to MiningInterface object footprint and ~1 ns
per virtual call; both negligible.
Build green: c2pool (LTC) + c2pool-btc both link clean. LTC binary's
--help renders normally (smoke).
Stage 3 next: refactor stratum_server.{cpp,hpp} to hold IWorkSource*
instead of MiningInterface*, replace the 3 direct m_share_bits.load()
accesses with the new virtual getters.
Stage 3 of B4-stratum / IWorkSource extraction. The architectural
extraction is COMPLETE — core::StratumServer no longer depends on any
specific coin's MiningInterface.
Changes:
- stratum_server.hpp: forward decl `class MiningInterface;` replaced
with `using IWorkSource = core::stratum::IWorkSource;`. All three
shared_ptr<MiningInterface> member/parameter declarations changed to
shared_ptr<IWorkSource>.
- stratum_server.cpp: matching constructor signature updates +
3 atomic-state accesses migrated:
mining_interface_->m_share_bits.load() → get_share_bits()
mining_interface_->m_share_max_bits.load() → get_share_max_bits() (×2)
LTC integration: callers passing std::shared_ptr<MiningInterface> work
unchanged via implicit shared_ptr<Derived> → shared_ptr<Base> conversion
(MiningInterface : public IWorkSource since b2b4478). Vtable dispatch
on the 13 method calls + 2 atomic getters adds ~1 ns each — well below
floor of meaningful overhead on the per-share-submission hot path.
BTC integration: btc::stratum::BTCWorkSource (Stage 4, next) only needs
to implement IWorkSource. main_btc.cpp can then instantiate
core::StratumServer directly with a BTCWorkSource shared_ptr. No
forking of stratum_server.cpp needed — single source of truth for 1500
LOC of stratum protocol, vardiff, NiceHash, BIP 310 ext, etc.
Build green: c2pool (LTC) + c2pool-btc both link clean. LTC binary
--help renders normally. No behavior change for LTC.
Stage 4a of B4-stratum / IWorkSource extraction. Adds a new btc_stratum
library at src/impl/btc/stratum/ with the skeleton BTCWorkSource — a
concrete core::stratum::IWorkSource implementation for c2pool-btc.
Skeleton scope (intentionally non-functional):
- All 15 IWorkSource methods declared + stubbed to safe defaults
- Worker registry methods (register/unregister/update) IMPLEMENTED for
real now — they're just std::map bookkeeping under workers_mutex_
- Read-only getters return defaults (empty string, 0, false, empty fn)
- Work generation methods return empty json/vectors
- mining_submit logs + rejects everything as low-difficulty
- get_share_bits / get_share_max_bits return atomics (real)
Constructor takes (HeaderChain&, Mempool&, is_testnet, SubmitBlockFn,
StratumConfig). The submit callback is the bridge to B5 — main_btc.cpp
will pass a lambda that calls coin_node.submit_block_p2p + adds to
the pending_submits map (the B5 dormant infrastructure that's been
waiting for a producer since 2176710).
Threading: workers_/best_share_/template_ each have their own mutex;
work_generation_ + share_bits_ + share_max_bits_ are atomics. Designed
for the multi-threaded io_context that core::StratumServer runs on.
Lifetime: holds non-owning references to chain_ + mempool_; main_btc.cpp
owns those. Fwd-declared in work_source.hpp; full headers included only
in work_source.cpp to keep the header light.
Sub-stages remaining:
4b: real read-only getters (prevhash from chain_.tip(), worker stats)
4c: real work generation (TemplateBuilder::build_template + GBT shape)
4d: real mining_submit (SHA256d PoW classify + B5 dispatch)
Build: btc_stratum library links clean. c2pool-btc still builds (btc_stratum
not yet linked into the binary — that's Stage 5).
…ches
Stage 4b + 4c-i + 4c-ii of B4-stratum / IWorkSource extraction.
Implements three of the read methods on BTCWorkSource:
4b) get_current_gbt_prevhash()
→ BE display-hex of chain_.tip()->block_hash, empty if pre-IBD
4c-i) get_current_work_template()
→ TemplateBuilder::build_template(chain_, mempool_, is_testnet_)
already returns GBT-shaped nlohmann::json via WorkData::m_data,
so this just forwards. Returns empty object if no tip yet.
4c-ii) get_stratum_merkle_branches()
→ computes Stratum-format branches: at each level, the SIBLING
of the left-most (coinbase-descended) node. Bitcoin-Core-
compatible pad-on-odd. Uses btc::coin::merkle_hash_pair from
template_builder.hpp (same helper B6 parity-tested).
Build green: btc_stratum links clean.
Remaining stage 4 work:
4c-iii) get_coinbase_parts + build_connection_coinbase
(needs c2pool sharechain payout output construction — the
p2pool ref_hash dance over coinbase scriptSig)
4d) mining_submit hot path
(SHA256d PoW classify + B5 dispatch + sharechain accept)
Stage 4c-iii: build_connection_coinbase + get_coinbase_parts
Sophisticated stub that produces a stratum-compatible coinbase split
paying the FULL subsidy + fees to the miner's payout_script:
coinb1 = tx_version || vin || prev || vout || scriptSig_len ||
BIP34_height_push
[extranonce slot — 4+4 bytes]
coinb2 = /c2pool-btc/ tag || sequence || 1-output(value, payout) ||
locktime
WorkSnapshot frozen with subsidy + share_version=35 (jtoomim BTC) +
frozen merkle branches. CLEAR TODO documented for c2pool sharechain
payout outputs (PPLNS distribution + ref_hash) — that requires
careful adaptation of v35 share format coinbase layout in a follow-up.
Helpers added: push_u32_le / push_u64_le / push_varint /
bip34_height_push / parse_be_hex_u32 — all in anon namespace, used
for both coinbase + header construction.
Stage 4d: mining_submit (the hot path)
Real PoW classification:
1. Reconstruct full coinbase from JobSnapshot.coinb1 || extranonce1
|| extranonce2 || JobSnapshot.coinb2
2. coinbase_txid = SHA256d(coinbase)
3. Ascend frozen merkle branches → merkle_root
4. Build 80-byte header: version || prev_hash_LE || merkle_root ||
ntime || nbits || nonce
5. pow_hash = SHA256d(header)
6. Classify:
pow_hash <= block_target → BLOCK FOUND
Build full block bytes (header || tx_count || coinbase ||
tx_data), call submit_block_fn_(bytes, height). Logs as
[BTC-STRATUM-BLOCK].
pow_hash <= share_target → share accepted (logs [BTC-STRATUM-
SHARE]; PPLNS recording is a TODO until sharechain wiring)
otherwise → reject 23 "Low difficulty share"
Worker stats updated (accepted/rejected) under workers_mutex_.
SubmitBlockFn signature changed from (BlockType&, height) to
(const vector<unsigned char>&, height) — matches existing
btc::coin::p2p_node::submit_block_raw and avoids dragging BlockType
serialization into the work source.
Build green: btc_stratum library links clean.
Stage 5: c2pool-btc now optionally exposes a stratum TCP listener.
- New CLI flag: --stratum [HOST:]PORT (HOST defaults to 0.0.0.0)
- Adds: btc::coin::Mempool (default-constructed, unwired for MVP — TODO
bitcoind P2P inv_tx → mempool.add_tx; coinbase-only templates work
for now)
- Adds: btc::stratum::BTCWorkSource via shared_ptr; bumps work
generation on both new_headers and full_block events
- Adds: core::StratumServer (the same coin-agnostic server LTC uses,
via the IWorkSource interface — zero code duplication)
- submit_block_fn lambda bridges work source → coin_node.submit_block_p2p_raw
+ adds to B5 pending_submits map (the dormant infrastructure that
has been waiting for a producer since 2176710 finally has one)
Helper added to btc::coin::Node: submit_block_p2p_raw(bytes) wrapping
m_p2p->submit_block_raw — keeps BTCWorkSource decoupled from BlockType
serialization.
CMake: c2pool-btc now links btc_stratum.
Stage 6 — smoke test PASSES:
$ c2pool-btc --bitcoind .233 --p2pool .122 --stratum 127.0.0.1:9332
ss -tnlp: LISTEN 127.0.0.1:9332 (c2pool-btc)
LISTEN 0.0.0.0:9333 (sharechain peer)
TCP subscribe roundtrip clean:
→ {"id":1,"method":"mining.subscribe","params":["smoke-test/0.1"]}
← {"error":null,"id":1,"result":[[["mining.set_difficulty","sub_0"],
["mining.notify","sub_0"]],"00000000",4]}
← {"id":null,"method":"mining.set_difficulty","params":[32.768]}
Sharechain peering active alongside (9347 verified shares during the
test). think-P2 verification loop runs, async think cycles complete,
GST/PPLNS/verify split logs all healthy.
Known issue (doesn't gate Stage 6): SIGTERM teardown produces a
core-dumped Abort. Likely a destruction-order race between
StratumServer / BTCWorkSource / coin_node / pending_submits-mu shared
state. Functional during runtime; needs RAII audit in a follow-up.
…lose Fixes the SIGTERM "Aborted (core dumped)" observed at the end of the B7-stratum 5+6 smoke test (7935818). Two layers, both architectural: LAYER 1 — boost::asio::signal_set replaces std::signal handler. std::signal handlers run in async-signal-only context; calling io_context::stop() from there is UB-adjacent. signal_set's handler runs in an ordinary asio callback on the io_context thread, thread-safe + signal-safe by construction. The handler now drives an EXPLICIT graceful shutdown — stop the stratum acceptor + close every active session FIRST, THEN ioc.stop(). By the time ioc is destroyed at end-of-main, no async op references torn-down state. LAYER 2 — StratumServer::stop() closes every session. Previously it only closed the acceptor and left sessions_ alive in the set. Their work_push_timer + pending async_read still queued handlers in the io_context, which were released only when ioc itself was destroyed during RAII at end-of-main — long after StratumServer had been torn down. Race window ⇒ core dump. StratumServer::stop() now snapshots the sessions_ set under sessions_mutex_, clears it, then OUTSIDE the lock invokes shutdown() on each (cancel work_push_timer + close socket via the existing private cancel_timers, exposed publicly as shutdown()). The pending async_read fails with operation_aborted, the read-error path runs cancel_timers + unregister_stratum_worker, and the session-bound shared_ptr count drops to 0 cleanly. Snapshot-then-iterate-outside-lock pattern prevents unregister_session()'s own sessions_mutex_ acquisition from deadlocking against our held lock when the read-error handler races back through. Explicit teardown order in main(): After ioc.run() returns from the signal handler's ioc.stop(): stratum_server.reset() → invokes ~StratumServer (already stopped, now releases sessions_ shared_ptrs) work_source.reset() → drops main()'s ref; coin_node subscribers still hold shared_ptrs, work_source dies when coin_node destructs subscribers later p2p_node.reset() → sharechain peer NodeP2P closes peers RAII at end-of-scope handles coin_node, ioc, mempool, header_chain, utxo_db in reverse construction order — clean. Smoke test: SIGTERM → exit code 0 (was: 134 SIGABRT). Logs a clean "[BTC] Shutdown complete." and LevelDB stores close in order.
Adds two new callback types on BTCWorkSource for the c2pool sharechain
payout dance:
PplnsFn — main_btc.cpp wires this to a lambda that calls
btc::ShareTracker::get_v35_expected_payouts(prev_share_hash,
block_target, subsidy, donation_script) under a TrackerReadGuard.
Returns {script_pubkey_bytes → satoshi_amount}.
RefHashFn — main_btc.cpp wires this to a lambda that calls
btc::compute_ref_hash_for_work(RefHashParams) — already exists
in btc/share_check.hpp from B0. Returns (uint256 ref_hash,
uint64_t last_txout_nonce).
Also: set_donation_script() — bytes of the c2pool donation
scriptPubKey, used by build_connection_coinbase as residual recipient.
All three callbacks are optional: if unset, build_connection_coinbase
gracefully degrades to single-output coinbase (full subsidy → miner,
no OP_RETURN, no c2pool sharechain participation but valid BTC blocks
still produced).
Threading: callback_mutex_ guards the std::function members. Setters
take the lock + move-assign; readers (Phase 8b) snapshot under the lock
then invoke unlocked.
Build green: btc_stratum links clean. No behavioral change yet —
Phase 8b will use these callbacks in build_connection_coinbase.
…ETURN
8b — port build_coinbase_parts algorithm (core/web_server.cpp:1576-1735)
into BTCWorkSource::build_connection_coinbase, simplified for BTC v35:
no DOGE merged-mining (chain_id=30), no V37 state_root, no MWEB.
c2pool v35 coinbase layout produced now:
ScriptSig (deterministic, no extranonce — c2pool puts en1+en2 in
OP_RETURN nonce slot for hash_link prefix stability):
[BIP 34 height push] [/c2pool-btc/ tag with 1B push opcode]
Outputs:
PPLNS payouts (sorted asc by amount, asc by script bytes)
v35 finder fee: subsidy/200 added to miner's payout, deducted
from donation. Reference: share_tracker.hpp v35 PPLNS docs
("amounts WITHOUT finder fee — caller adds subsidy/200 ...").
Dust filter: <1 sat amounts dropped (matches Bitcoin policy).
OP_RETURN: 0 sats, script = 6a 28 [ref_hash 32B] [nonce 8B-slot]
The 8B nonce slot is filled at submit time by en1(4) || en2(4).
coinb1 = full tx prefix up to and including ref_hash (NOT the 8B nonce)
coinb2 = "00000000" (locktime only)
Graceful degradation: if pplns_fn_ unset (cold start, ShareTracker not
ready) → single-output coinbase paying full subsidy to miner. If
ref_hash_fn_ unset → no OP_RETURN. Both produce valid BTC blocks but
no c2pool sharechain credit. The architecture is in place for stage
8d main_btc.cpp wiring to flip these on.
Snapshot: frozen_ref now populated with real ref_hash + last_txout_nonce
(returned by ref_hash_fn) + share_version=35 + bits + max_bits + timestamp.
Stratum sessions store this in JobEntry and pass back via JobSnapshot to
mining_submit.
Pre-existing pre-commit memcmp warning in stl_algobase.h:1848 (triggered
by std::map<vector<uint8_t>, double>::operator<= via three-way) is a
GCC 13 false-positive on an unrelated translation unit; not introduced
here.
8c — mining_submit cleanup. The new c2pool coinbase layout is
SHAPE-INVARIANT to the existing reconstruction
coinbase = coinb1 || en1 || en2 || coinb2
(extranonce just lives at a different byte position, but the concat is
the same). Removed dead boilerplate from earlier confusion in the
header-build path. PoW classification + block-bytes assembly unchanged.
Build green: btc_stratum links clean.
Remaining: Phase 8d (main_btc.cpp wiring of pplns_fn + ref_hash_fn
lambdas to btc::ShareTracker via TrackerReadGuard) + Phase 8e smoke.
Phase 8d — main_btc.cpp wiring:
set_donation_script(get_donation_script(35)) — 67B P2PK v35 donation
set_pplns_fn(...) — calls p2p_node->read_tracker()->get_v35_expected_payouts()
under TrackerReadGuard (try_to_lock; falls back to
empty map on busy → degraded single-output coinbase)
set_ref_hash_fn(...) — calls btc::compute_ref_hash_for_work() with v35
RefHashParams. SOPHISTICATED STUB: tracker-walked
fields (absheight, abswork, far_share_hash,
share_nonce) get placeholder values. Resulting
ref_hash WON'T match what live SPB peers expect,
so c2pool-btc-built shares are produced locally
but won't be accepted by the wider sharechain.
That's the next concrete TODO; the wiring is done.
P2PKH heuristic in ref_hash_fn extracts pubkey_hash from the standard
76 a9 14 [20B] 88 ac script. Bech32 P2WSH falls through (zeroed).
Phase 8e — full stratum smoke test PASS against oracle .122 + bitcoind .233:
• TCP listener up at 127.0.0.1:9332 (sharechain still on 9333)
• mining.subscribe → standard reply with extranonce assignment
• mining.set_difficulty 32.768 pushed
• mining.notify with REAL c2pool v35 coinbase!
coinb1 decoded: version=1, scriptSig=[BIP34_h=947415][/c2pool-btc/],
outputs={[donation P2PK 312.5M sats][OP_RETURN ref_hash 32B+nonce_slot]}
coinb2 = "00000000" (locktime)
• mining.authorize → true
• Worker registered/unregistered cleanly
• SIGTERM → exit 0 (graceful shutdown still works post-PPLNS wiring)
• [V35-PPLNS] subsidy=312500000 addrs=57 shows real sharechain reads
Defensive fixes from smoke-test findings:
(1) Empty-script PPLNS entries: when a share's miner had an unsupported
address (e.g. bech32 P2WSH not decoded by current address validator),
get_v35_expected_payouts produces a map entry with empty key. Without
filtering, this becomes a 0-script output in the coinbase — burned
sats. Now: filter empty-script entries, reabsorb their value to
donation. Logged as `[BTC-STRATUM] PPLNS: dropped N empty-script
entries (V sats reabsorbed)`.
(2) Finder fee math hardening: the v35 PPLNS docs say "caller adds
subsidy/200 to share creator's script". Original code added it
UNCONDITIONALLY then attempted to deduct from donation. If donation
< finder_fee (rare but possible at chain edges), donation went to 0
but the miner still got the bonus → total > subsidy → bitcoind
rejects block. Now: only add finder_fee if donation can absorb it
AND payout_script is non-empty. Otherwise skip silently — total
payouts stays at subsidy per get_v35_expected_payouts post-cond.
Build green + smoke PASS + clean exit 0.
Remaining TODOs for live-sharechain participation (logged in MEMORY.md):
- Walk tracker for ref_hash absheight/abswork/far_share_hash
- Bech32 P2WSH decoder for stratum address validator (so payout_script
is populated for modern miners)
- Segwit witness commitment OP_RETURN in coinbase Output 0
- bitcoind P2P inv_tx → Mempool wiring (currently coinbase-only templates)
… closer)
Closes the biggest remaining correctness gap from Phase 8d's sophisticated
stub: the four tracker-walked fields in RefHashParams that determine
whether peers running the same share chain accept our ref_hash.
Walk algorithm (mirrors c2pool_refactored.cpp:4460-4520, simplified for
v35 — no merged_addresses, no merged_payout_hash, no desired_target
vardiff dance since BTCWorkSource's bits are derived directly from the
GBT template):
1. set_best_share_hash_fn — wires the work source's prev_share_hash
query path to p2p_node->best_share_hash() so build_connection_coinbase
gets a REAL chain head, not uint256::ZERO. Without this, every
ref_hash call hit the genesis branch and produced a hash that no
peer would accept regardless of how correct the rest of the math was.
2. ref_hash_fn lambda updated:
- read_tracker() guard (try_to_lock; falls through to genesis if busy)
- if (chain.contains(prev_share_hash)):
absheight = prev->m_absheight + 1
timestamp = max(timestamp, prev->m_timestamp + 1) ← clip up
abswork = prev->m_abswork + uint128(target_to_average_attempts(
bits_to_target(bits)).GetLow64())
far_share = chain.get_nth_parent_key(prev_share, 99)
(uint256::ZERO if chain too short — matches LTC ref)
- else (genesis): absheight=1, abswork=u128(attempts), far_share=ZERO
Smoke test against oracle .122 with our 17,300-share local chain:
[REF-FN] v=35 ref_packed_len=139
ref_hash=f9f9cc4419449c59ee5cf326b55b614a29d198d37045d601a75d19531fc8d38d
absheight=13389029 prev=0000000000000006
[EP-PPLNS] v35 start=0000000000000015 max_shares=8639
desired_w=399061810843194318 subsidy=312500000
best=0000000000000006
absheight matches the local chain's actual height (13389029). prev hash is
a real share. PPLNS walking is reading real ancestor weights. The
ref_hash bytes are deterministic from this state — any peer with the same
chain segment should compute the same hash and accept the share.
Caveat (acknowledged): peers running v35 with merged-mining hooks
(jtoomim's reference) may need additional fields (frozen_mm_commitment,
share_nonce derivation) that we still default. Those are next-step
verifications against actual peer rejection logs. The wire shape is now
correct end-to-end; what remains is field-by-field empirical checking.
Build green: c2pool-btc links clean. Smoke PASS, exit 0.
Phase 10a — BIP 141 witness commitment in coinbase Output 0:
• Detect segwit_active from GBT template "rules" array.
• Compute wtxid merkle root: leftmost = uint256::ZERO (coinbase wtxid
convention per BIP 141), rest from template's "transactions[].hash"
field (TemplateBuilder line 226 emits wtxid here).
• commitment = SHA256d(witness_root || witness_reserved_value),
where witness_reserved_value = 32 zero bytes per convention.
• Emit OP_RETURN as Output 0 of coinbase: 0x6a 0x24 0xaa21a9ed [commit32]
(38 bytes total).
• WorkSnapshot.witness_commitment_hex + witness_root populated so
JobSnapshot carries the segwit context to mining_submit.
Phase 10b — BIP 144 witness serialization in submitted block bytes:
When mining_submit detects a block hit and assembles block_bytes for
submit_block_p2p_raw, the coinbase MUST include witness data (the 32
zero-byte witness reserved value) so bitcoind's commitment validation
succeeds. Without it, blocks reject as bad-witness-merkle-match.
Inject marker(0x00)+flag(0x01) after version, append witness data
before locktime: [stack_count=1][item_len=0x20][32 zero bytes].
Non-witness coinb1/coinb2 sent to miners stay unchanged — txid math
is the non-witness form.
Phase 10c — Mempool feed from bitcoind P2P:
coin_node.new_tx.subscribe → mempool.add_tx(MutableTransaction(tx),
&utxo_cache)
coin_node.full_block.subscribe → mempool.remove_for_block(block)
TemplateBuilder now produces real fee-bearing templates instead of
coinbase-only. Smoke test confirmed: "fees=672 ... txs=1" on first
tip after startup.
Smoke test against oracle .122 + bitcoind .233:
mining.notify coinb1 contains:
[BIP34_h947419][/c2pool-btc/] scriptSig
[Output 0] 6a24aa21a9ed [SHA256d witness commit] BIP 141
[Outputs 1..50+] real PPLNS payouts to sharechain v35 PPLNS
[Output last] 6a28 [ref_hash 32B] [nonce_slot 8B] c2pool ref_hash
bitcoind tip caught up in real time, mempool fed by inv stream,
SIGTERM → exit 0. All three Phase 10 components working live.
Build green: c2pool-btc + btc_stratum link clean.
Documented TODOs remaining for live-sharechain participation:
• Sharechain submission write path (mining_submit → btc::ShareTracker.add)
• Periodic Mempool::recompute_unknown_fees driver (initial txs missing
fees fall back to base-subsidy until UTXO catches up)
• Per-peer empirical ref_hash verification (try submit, check reject log)
… + broadcast
mining_submit's share-target hit path no longer just logs accept and
discards. It now constructs a v35 PaddingBugfixShare, adds it to the
local btc::ShareTracker, and broadcasts to peers. This is the
difference between c2pool-btc as a stratum proxy vs. as a real c2pool
node participating in the sharechain.
Architecture (mirrors LTC's c2pool_refactored.cpp:4580-4784, simplified
for v35 — no merged_addresses, no merged-coinbase auxpow):
BTCWorkSource gets a new CreateShareFn callback:
using CreateShareFn = std::function<uint256(
const std::vector<unsigned char>& full_coinbase,
const std::vector<uint8_t>& header_80b,
const core::stratum::JobSnapshot& job,
const std::vector<unsigned char>& payout_script)>;
mining_submit invocation:
- Recompute payout_script from username via core::address_to_script
(handles bech32 P2WPKH/P2WSH + base58 P2PKH/P2SH)
- Call create_share_fn(full_coinbase, header, *job, payout_script)
- Log [BTC-STRATUM-SHARE] ACCEPTED+ADDED on non-zero hash; otherwise
log accepted/deferred so miner still gets a success reply
main_btc.cpp lambda implementation:
1. Parse 80-byte header → btc::coin::SmallBlockHeaderType (the v35
"min_header" — version, prev_block, timestamp, bits, nonce; no
merkle_root since it's reconstructible)
2. Wrap full_coinbase in BaseScript
3. Convert merkle_branches strings → vector<uint256>
4. Acquire EXCLUSIVE tracker lock via try_to_lock — non-blocking;
defer via uint256::ZERO if compute thread is mid-think (matches
LTC's read-write pattern in c2pool_refactored.cpp:4615-4627)
5. Call btc::create_local_share<TrackerT>(...) with all 25+ params
including frozen ref-hash fields from JobSnapshot.frozen_ref so
the on-disk share's ref_hash matches the one embedded in the
coinbase OP_RETURN at template-issue time
6. On success: drop lock → broadcast_share(hash) +
notify_local_share(hash) so peers learn our new tip and miners
get fresh work pinned to it
Threading discipline:
- Single exclusive lock acquisition via try_to_lock — never blocks
the io_context thread
- Lock dropped before broadcast_share / notify_local_share, those
acquire their own locks (or post to io_context) and we don't
want exclusive held during async dispatch
- Worker stats updated under workers_mutex_ (separate from tracker)
Smoke test: build clean, c2pool-btc starts up with three new wire-up
logs ("PPLNS + ref_hash callbacks wired" / "sharechain write path
wired"), accepts subscribe+authorize, exits 0 on SIGTERM. No
[BTC-CREATE-SHARE] logs without a real miner submitting at low
difficulty — that's expected and tests the actual path.
Remaining for live SPB acceptance:
- Test against real miner (cgminer with low-diff config) to
actually exercise the create_share path
- Mempool::recompute_unknown_fees periodic driver (initial txs
still missing fees)
- Empirical ref_hash field-by-field verification (try submit, watch
oracle reject log to identify any remaining diffs)
…le byte order) Two consensus-critical bugs that made every bitaxe submission look like diff ~1e-9 garbage and reject at the vardiff gate, even though the miner's local PoW computation was correct. 1. BIP 320 version-rolling convention: ESP-Miner / bitaxe firmware sends `version_bits` as the XOR-difference between the rolled version and the job version (`version_bits = rolled ^ job` in asic_result_task.c:60). The pool must recover via `effective = job ^ version_bits` — NOT the cgminer-style REPLACE `(job & ~mask) | (bits & mask)` we had before. The two diverge whenever `job.version & mask != 0`, which is the common case on BTC where bitcoind's GBT version carries multiple BIP 9 deployment bits (e.g. 0x24238000 has bits 28/21/17/16/15 inside the 0x1FFFE000 mask region). REPLACE silently zeroed those bits, producing a header version the miner never hashed → every share rejected. (LTC was accidentally correct because its job.version is typically 0x20000000 with no mask bits set, so REPLACE and XOR coincide.) 2. Merkle branch wire encoding: cgminer convention (and LTC's working compute_merkle_branches at web_server.cpp:1299) sends each branch as the hex of the **LE-internal** 32-byte hash — the miner does hex2bin and feeds the bytes directly into SHA256d. Our BTC code used `level[1].GetHex()` which produces the **BE-display** hex (reverse byte order). The miner then computed merkle root from reversed bytes vs our LE-internal-byte tree, so coinbase_txid → merkle_root → header → SHA256d all diverged. compute_share_difficulty compounded this by reading via `b.SetHex()` (which reverses again), internally consistent but wrong vs the miner. Fix: encode with `HexStr(level[1].data(), 32)` and decode with `ParseHex+memcpy`, matching LTC's reconstruct_merkle_root path. Live-validated on btc-embedded against bitaxe BM1366 @ 444 GH/s pointed at c2pool-btc:9332 (vardiff diff 1000): - pre-fix: every submit gave diff=3.6e-9..5.3e-10 (header reconstruction mismatch); bestSession=842 in 4 min, acc=0 - post-fix: pow=0000000000352eba diff=1232.27 → [BTC-STRATUM-SHARE] accepted (deferred), bitaxe acc=1 rej=0 bestSession=1232 (exact match pool↔miner) Also: initial share-target 0x1d00ffff (diff 1) wired in main_btc.cpp for first-light testing; IWorkSource gains a virtual compute_share_difficulty so the coin-agnostic stratum server dispatches to per-coin PoW (LTC scrypt vs BTC SHA256d) instead of always-scrypt. Diagnostic [BTC-DIFF] / [BTC-DIFF-CB] / [BTC-DIFF-MR] logging kept (gated to first 5 calls) so future regressions surface immediately.
… convention Companion to 74c7abe. The create_share lambda in main_btc.cpp also parsed job.merkle_branches via b.SetHex() which reverses bytes (BE display → LE internal). After the wire convention switched to LE-internal hex, this re-parsed branches in the wrong byte order, so the v35 share that create_local_share built carried merkle siblings inconsistent with the bytes the miner actually hashed. Fix: ParseHex+memcpy here too, matching get_stratum_merkle_branches / compute_share_difficulty / build_connection_coinbase. Field-only effect: the existing live binary still proxy-mines fine (stratum loop is correct end-to-end), but any successfully-created share would have been rejected by peers on consensus check until this fix lands. With cold-start sharechain (no peer-supplied prev_share) the create_local_share path has been silently returning ZERO regardless, which is why no [BTC-STRATUM-SHARE] ACCEPTED+ADDED has fired yet. This fix is the next gate to clear once a peer connection lands a parent share into the tracker.
Empirical 2026-05-02: with 12 (74c7abe) + 12b (75da8ec) applied, the sharechain WRITE path actually fires — but my hardcoded set_share_target(0x1d00ffff) (diff-1) was propagating through snap.frozen_ref.bits → override_bits in create_local_share_v35, which trivially passed the in-function PoW check (line 2447) on virtually every bitaxe submission. The pool then add+broadcasted those diff-1 shares to peers; oracle peer 192.168.86.122:9333 RST'd us 14ms after the first broadcast (reasonable: those shares fail every peer's own compute_share_target check → low-diff → kick). Fixes: 1. Drop the explicit set_share_target call. share_bits_=0 → frozen_ref .bits/.max_bits=0 → override_bits=0 in create_local_share_v35 → share.m_bits comes from tracker.compute_share_target(prev_share, ...), the REAL network share target (~2e8 currently). Bitaxe-class submissions can't beat that and create_local_share_v35 returns ZERO silently — exactly the right gate. 2. Pass has_frozen=false in the create_share lambda. snap.frozen_ref .{absheight,abswork,far_share_hash,timestamp} were never populated (only ref_hash_fn computes them, into a local RefHashParams), so has_frozen=true was overriding live-walked values with 0 — produced share.m_absheight=0 which peers reject. The lambda holds tracker_mutex unique_lock, so the in-function walks at share_check.hpp:2127-2208 are safe. 3. Add [V35-SHARE-MISS] diagnostic so the silent-ZERO return path at share_check.hpp:2447 is observable (capped at 20 instances, one-shot logged with pow vs target vs share_bits vs absheight). Side-effect of (1): pool_difficulty=0 in stratum_server → is_pool_share=false → BTCWorkSource::mining_submit is NOT called for ordinary pseudoshares (block-target check still runs independently). That means chain-share creation effectively never triggers from bitaxe-class miners against BTC mainnet sharechain difficulty — which is *correct* behavior, not a regression: 1.7 TH/s vs network ~16 PH/s shouldn't produce real chain shares except by extreme luck. Phase 12 TODO is to drive set_share_target from tracker.compute_share_target on each new tip so that frozen_ref + RefHashParams + share.m_bits all carry the live network target — that lets pool_difficulty become non-zero without creating the override-back-to-diff-1 trap this commit fixes. Verified peer-safe post-fix: peer .122 stayed connected, real diff~2.22e8 shares continued flowing in (chain=17300+), zero "Marking share as rejected" / "Peer disconnected" warnings during bitaxe submit window.
Closes the architectural gap left by 12c (5a5aec5). With share_bits_=0 forced as the safe default, pool_difficulty stayed 0, is_pool_share was always false in stratum_server, and mining_submit was never called for ordinary pseudoshares — chain-share creation could only fire from the block-target hop. That's safe (no peer spam) but wrong: real-network luck-shares meeting compute_share_target would never even be attempted. Phase 12 wires the live network share target end-to-end via the existing ref_hash_fn callback (which already walks the tracker) — extended to return the full core::stratum::RefHashResult instead of just (ref_hash, nonce). One callback, one tracker walk, all five derived values reused in three places that previously each had to either rewalk or use stale defaults. Mechanics: 1. RefHashFn return: std::pair<uint256,uint64_t> → core::stratum:: RefHashResult. The lambda already had to compute absheight, abswork, far_share_hash, and the timestamp clip for compute_ref_hash_for_work — now it also calls tracker.compute_share_target with the same clipped timestamp create_local_share_v35 will use later, and reports {bits, max_bits, absheight, abswork, far_share_hash, timestamp_clip} in the result. 2. build_connection_coinbase consumes RefHashResult: updates the share_bits_/share_max_bits_ atomics (made `mutable` so the const method can write — these are already concurrent by virtue of being atomics) and populates snap.frozen_ref completely. Stratum_server's pool_difficulty gate now reads a live network share target instead of 0. 3. create_share lambda: has_frozen=false → has_frozen=true. With frozen_ref now correctly populated, the override values match what create_local_share_v35 would compute itself — so we use the FROZEN values to avoid races between template-build and submit time (best_share could move under us mid-submit otherwise). Lambda ordering subtlety: create_local_share_v35 (share_check.hpp:2127- 2138) clips share.m_timestamp BEFORE calling compute_share_target with that clipped value. The new ref_hash_fn lambda matches that ordering — walk for absheight + clip timestamp first, THEN compute_share_target with the clipped value. Without this, our reported (bits, max_bits) would diverge from create_local_share_v35's by one tick when the share falls before its prev's timestamp. Empirical verification on btc-embedded: - pre-Phase-12 (12c): pool=0 in stratum log → mining_submit never called → 0 chain-share path attempts in 30 minutes - post-Phase-12: pool=2.20011e+08 (real network share difficulty) → is_pool_share = (1157 >= 2.2e8) = false for bitaxe-class hashrate → pseudoshares accumulate for vardiff/PPLNS, but no broadcasts go to peers; verified zero "Marking share as rejected" / "Peer disconnected" in 30 minutes with 3 bitaxes (~1.7 TH/s) submitting steadily. Reaching [V35 SHARE CREATED] from this fleet against BTC mainnet sharechain difficulty requires extreme luck (~1 share per 6 days for the whole 1.7 TH/s fleet vs network ~16 PH/s) — that's correct behavior, not a regression. When it fires, it'll fire with values that peers verify cleanly.
…uts on full_block
The Mempool methods exist (mempool.hpp:401 + 432) but were never called.
With them dormant, txs that landed in the mempool before their inputs
were visible to the UTXO cache stayed at fee=UNKNOWN forever and were
silently skipped by TemplateBuilder's fee-sorted include path —
templates produced before the catching-up tip would miss legitimate
fees that were just one block behind. This was the last open MVP item
in project_btc_embedded_kickoff.md ("Mempool::recompute_unknown_fees
periodic driver").
Hook is on the existing coin_node.full_block.subscribe lambda where
remove_for_block already runs. Subscriber-call order from line 434
(UTXO connect_block) → 643 (this lambda) is preserved by
boost::signal2 registration order, so by the time we run the UTXO
already has the new block applied — both maintenance passes operate
on the post-connect snapshot.
Two passes, both self-gating (no-op when state is clean):
- revalidate_inputs(utxo_cache): evicts txs whose inputs the new block
spent out from under us, catching the case where remove_for_block's
conflict detection didn't see the spend (e.g. spending tx wasn't
tracked in m_spent_outputs, parent-of-CPFP, etc.). Operates only on
fee_known entries.
- recompute_unknown_fees(utxo_cache): re-attempts fee computation for
every fee_known=false entry. Any whose inputs are now visible flip
to fee_known=true and join the feerate-sorted index for template
building. Operates only on fee_known=false entries — disjoint from
revalidate_inputs's set.
Smoke test 2026-05-08 against btc-oracle-122 + bitcoind-233:
[BTC] UTXO connect: h=948356 txs=3618 undo_added=8323 undo_spent=3617
[EMB] Mempool fee revalidation: resolved=16 still_unknown=76 ...
[EMB] post-tip mempool maintenance: evicted=0 resolved_fees=16
First block after start resolved 16/92 unknown-fee txs (17% rate),
zero false-evictions — wiring works.
c2pool-btc uses a single blocking ioc.run() instead of LTC's while + run_for(100ms) loop, so the freeze-diag commit (cb69c1d / 1c4c4bd on the LTC RC branch) doesn't cherry-pick directly. Port the same intent: replace ioc.run() with the slice loop pattern, time each slice, log [IOC-LAT] when one exceeds 2s. Loop terminates on the same shutdown_initiated bool the SIGINT/SIGTERM handler already sets, so graceful shutdown is preserved. This is the BTC half of the freeze-diag test setup — bitaxes (SHA256) keep mining into c2pool-btc on VM 422 .53 at full hashrate, and any event-loop wedge under the live mining load shows up as [IOC-LAT] warnings well before the 30s watchdog fires.
The c2pool-btc binary running on VM 422 (.53) has no in-process memory instrumentation. Visible RSS growth was only observable via external `ps` polling — and we just caught the previous instance climbing from 1.5 GB → 3.6 GB over 2 days before the user killed it, with zero in-process trace of the trajectory. Now every 60 s the timer reads /proc/self/status and emits: [MEM] vmrss=<kB> vmsize=<kB> vmdata=<kB> VmData is the truly useful one for leak hunting — it's the private writable segment, so it tracks heap (anonymous mmap + brk()) without the noise from shared libraries, leveldb mmaps, etc. Steady linear growth in VmData = real leak. Pair with `MemoryMax=7G` on the systemd unit so a runaway gets killed at the cgroup layer instead of OOMing the whole VM. Lightweight (~50 µs per fire — one fopen + a few fgets + an sscanf loop). Logged at info level so it shows up in journalctl by default. Smoke-tested: starts cleanly, first emission ~60s after start with expected initial RSS during header-sync warmup.
Heaptrack on .53 (42-min trace) attributed 341M of c2pool-btc's 506M peak heap to two HeaderChain maps: 273.37M std::map<uint256, IndexEntry> m_headers (load_from_db) 68.34M std::map<uint32_t, uint256> m_height_index (rebuild_height_index) Both keyed by data with already-uniform distribution: m_headers by SHA256d block hash; m_height_index by sequential height. Neither needs ordered iteration — every callsite in this file is .find/.count plus an end-iterator check, no .begin/.lower_bound/.upper_bound/range- for. Switching to std::unordered_map removes the Rb_tree node overhead (~32 B/node down to ~8 B/node), saving ~24 B per entry × 949k entries × 2 maps = ~45 MB peak heap. Also added .reserve(total_keys) at the top of load_from_db so the bulk insert doesn't trigger ~20 rehashes. m_dirty_headers (std::set<uint256>) also doesn't need ordering — the batch commit walks the set blindly. Switched to std::unordered_set. No LevelDB format change. Existing on-disk header data loads unchanged. Validated by a clean build (RelWithDebInfo, --target c2pool-btc) and a stop/cp/start cycle on VM 422 (.53); 3 bitaxes reconnected within 6s of restart. Local Uint256Hasher uses GetLow64() directly — the input is already a cryptographic hash, no further mixing needed; matches the pattern of chain::Uint256Hasher in src/sharechain/weights_skiplist.hpp. Phase 1A of three header-footprint mitigations identified by the heaptrack analysis. 1B (pack IndexEntry to drop duplicate hash/block_hash/prev_hash fields, ~60 MB, format change) and 1C (LRU cache instead of full-RAM residency, ~250 MB, major refactor) deferred.
Heaptrack v1 attributed 273M peak heap to std::map<uint256, IndexEntry> in HeaderChain::load_from_db. Phase 1A (bd0752c, std::map → unordered_map) cut that to 258M in the v2 trace. Phase 1B takes the next layer: the IndexEntry payload itself. Two of the six pre-Phase-1A fields are derivable from the rest on BTC: - "hash" was the PoW hash. On LTC this is scrypt(header); on BTC PoW is SHA256d, which equals block_hash. The file already documented this ("SHA256d(header) — same as block_hash on BTC"). 32 B/entry of dead duplication. - "prev_hash" is always header.m_previous_block, including for the fast-start checkpoint, dynamic checkpoint, and genesis seed sites (all three already set header.m_previous_block to null in parallel with the now-removed prev_hash = uint256::ZERO assignment). Another 32 B/entry of duplication. Drop both fields from the in-memory IndexEntry. Saves ~64 B/entry over the rb_tree-node + hashtable-node footprint × ~1M headers = ~60 MB peak. On-disk format is preserved bit-for-bit. The original six-field layout is now codified as IndexEntryDiskV1, with from_entry() / to_entry() converters used by flush_dirty() (write) and load_from_db() (read). A roll-back to a Phase-1A binary can still parse what this writes, and this binary parses what Phase-1A wrote — both directions tested via the deploy on VM 422 (.53) where the 949k-header LevelDB store loaded without a single corrupt-entry warning. Two callsites that previously read the duplicates are migrated to the canonical fields: - Incremental height-index update (~line 772): entry.prev_hash → entry.header.m_previous_block. - rebuild_height_index chain walk (~line 849): same migration. The genesis-block + checkpoint seed paths drop redundant zero assignments to prev_hash since the in-place SetNull() on header.m_previous_block already serves as the "trusted root" marker that terminates the chain walk in rebuild_height_index. Validated by clean build (RelWithDebInfo) and live deploy on VM 422 (.53) at 11:12 UTC: c2pool-btc 14858 came up cleanly, reloaded sharechain 17291/17280, resumed peer share processing and mempool intake. Heaptrack v3 (40-min trace) in progress to measure the additional peak-heap delta against v2 (303.85M HeaderChain baseline). Phase 1A + 1B together target ~115 MB out of the original 506M peak. Phase 1C (LRU header cache instead of full RAM residency, ~250 MB further savings) deferred — significantly more invasive.
Heaptrack v1 attributed 36.25M peak heap (the third-largest peak consumer overall, 53k calls × ~157 string allocs per call = 8.34M string allocations in a 42-minute window) to send_notify_work, at the line that assigns cbr.snapshot.tx_data into the per-job entry stored in active_jobs_. tx_data is std::vector<std::string> of raw tx hex strings — for a typical BTC block that's 100-200 strings, each ~hundreds of bytes. cbr is a function-local std::variant<...> here, and snapshot is a sub-member of that local. The previous code copy-assigned the vector (deep-copying every string), even though snapshot is never read again after the assignment. Switch to std::move to elide the deep copy. Two adjacent assignments fall in the same pattern and are also moved (both were already proven by the same heaptrack analysis to be hot, just not labeled at the top): - merkle_branches_vec = cbr.snapshot.merkle_branches (vec of hex) - je.merkle_branches = merkle_branches_vec (consumed next) All three sites are the last reads of the source vector before scope exit or before the source becomes a moved-from husk that goes out of scope, so std::move is provably safe. Per heaptrack v1, this single function was the #3 peak consumer with ~36 MB allocated per ~42-minute window (call rate scales with stratum load). Removing the copies cuts the per-notify allocation cost roughly in half on hot paths. Companion to the HeaderChain Phase 1A (bd0752c) + 1B (63385f2) optimizations. These three changes together address all top-3 peak heap consumers identified in the heaptrack analysis.
The Phase 1A+1B refactor shrank one IndexEntry from a fat 6-field
struct in an rb_tree to a slim 4-field struct in an unordered_map,
cutting peak heap from 506M (v1) to 376M (v3) — but ~243M of that
was still HeaderChain residency: every one of the ~1M BTC mainnet
headers was held in RAM at all times.
Phase 1C addresses the residency itself by moving m_headers from
"authoritative store" to "bounded LRU cache" (HEADER_CACHE_CAP =
16384 entries, ~3 MB) backed by the existing LevelDB store. The
authoritative best-chain projection is now m_height_index, which is
small enough to keep entirely in RAM (uint32_t -> uint256, ~45 MB
for 1M entries) but is now also persisted under a new "i" key
prefix so startup no longer needs to walk the chain to rebuild it.
New private helpers (all under m_mutex):
- lookup_header_internal(hash)
returns std::optional<IndexEntry> by value, cache hit OR
on-miss-load from LevelDB. Returns by-value (not pointer)
to avoid pointer-stability hazards if a follow-up lookup
evicts the result.
- has_header_internal(hash)
cheap existence check that uses LevelDBStore::exists()
on miss — no full entry load.
- put_header_internal(hash, entry)
insert into LRU + mark dirty (replaces the old
m_headers[h] = entry; persist_header(entry); pattern).
- touch_lru_internal / evict_lru_if_full_internal /
insert_into_cache_internal / try_load_header_from_db_internal
the LRU mechanics themselves. Eviction skips dirty entries
(rotates them back to MRU rather than dropping unwritten
state). m_lru_order is mutable so const accessors can refresh
LRU position without weakening the public API.
Persistence:
- flush_dirty() now batches BOTH m_dirty_headers ("h:" entries)
and m_dirty_heights ("i:" entries) in one atomic WriteBatch
along with the tip pointer. Two clear() at the end so a partial
commit on the next iteration doesn't lose anything.
- load_from_db() fast path: scan "i:" entries directly into
m_height_index, load the tip header into the LRU, done.
- First-time migration: if "i:" entries don't exist yet (legacy
on-disk data), walk back from tip via lookup_header_internal()
to rebuild m_height_index, mark all heights dirty, flush. This
is a one-time cost paid on the first startup after deploying
Phase 1C and is functionally identical to what the pre-Phase-1C
binary did at every startup.
Migrated callsites:
Public: tip(), get_header(), get_header_by_height(), is_synced(),
is_connected(), has_header(), size() (now backed by
m_height_index — it was the only honest measure of the chain
anyway).
Internal: checkpoint seed (init), dynamic checkpoint seed,
genesis, add_header_internal (new-header insert + prev lookup),
validate_difficulty (size guard now uses m_height_index, prev
lookup uses lookup helper), rebuild_height_index (chain walk via
lookup helper, dirties only changed heights, leaves stale rows
for entries no longer on the best chain).
Performance trade-offs:
- Lookups: cache hit unchanged; miss = one LevelDB Get(~50 us).
Hot paths (new-header arrivals, recent-block PoW chain) stay in
cache.
- Eviction: lowest-recently-used wins. Dirty entries are pinned
until flush, so a high-write burst can briefly push the cache
over the cap.
- Memory: m_headers shrinks from 197M (v3) to ~3M (cache).
m_height_index stays at ~45M (now also persisted, so startup
walk is a one-time legacy migration).
- First startup post-deploy: same as pre-Phase-1C (full walk to
build m_height_index, persist it). Subsequent startups skip the
walk entirely.
Expected peak heap after Phase 1A+1B+1C: ~190M (down from 506M),
of which 45M is m_height_index, ~30M LevelDB block cache, ~3M
m_headers LRU, rest scattered.
Two production findings from today's fleet health check, both with quick fixes that ship together. 1) contabo (LTC, freeze-diag binary cb69c1d) caught its own slow handler today via [IOC-LAT]: 8 events in a 3-minute burst at 09:56–09:59 UTC May 24, iterations of 2.4-7.5 seconds (target 100ms), all well under the 30s watchdog SIGABRT threshold but the same class of wedge the watchdog catches when they stack. The slow handler is the [think-P2] verify loop in ShareTracker::think(): it emits LOG_INFO every 50 verified entries — 173 lines per think cycle on contabo's 8639-share chain. Boost::log's internal mutex contention starves the io_context handler driving the same think cycle, so a single compute pass wedges its own host thread for seconds. Same root-cause class as the prior boost::log freezes fixed by 8c15ae3 (startup hang on oversized debug.log) and 15281fe (runtime recurrence isolated by moving rotation to a subdir). This one is the same lock at a different log site. Bumped throttle 50 → 1000. ~9 lines/cycle instead of 173. The verify loop runs at >1 share/ms so a line/second is plenty for diagnostics. Applied to both src/impl/ltc/share_tracker.hpp (contabo's LTC binary) and src/impl/btc/share_tracker.hpp (.53's c2pool-btc binary), even though .53 is currently below the threshold — keeps the two trackers in sync. 2) c2pool-btc on VM 422 (.53) heaptrack v4 (post Phase 1A+1B+1C, bcf46af) showed peak heap pinned at 182 MB as designed, but the production RSS kept climbing at the same ~140 MB/h rate it had pre-optimization. PID 19600 hit 4 GB RSS at 23:13 UTC May 18, ~27h after Phase 1C deploy, then SIGABRT'd at 16:09:52 May 23. Heap is bounded but RSS is not → glibc malloc-pool fragmentation, not a true heap leak. Added malloc_trim(0) to the existing [MEM] timer (already firing every 60s) and report its return value alongside vmrss/vmsize/vmdata. malloc_trim walks the top chunk of each arena and madvise(MADV_DONTNEED)s pages that are free; takes ~ms on typical heap sizes. Cheap and bounds steady-state RSS without touching the allocator pool. Both fixes are runtime-throttle/cleanup tweaks — no API change, no on-disk format change, no behavioral change beyond reduced log volume and bounded RSS climb. Build clean on RelWithDebInfo, smoke-tested c2pool + c2pool-btc binaries.
…rialize at submit Hold a shared_ptr to the per-tx hex vector on the work snapshot instead of deep-copying tx_data into every connection coinbase template. The hex is materialized lazily at block-submission time (work_source.cpp:789), the only reader. Fixes the linear, load-scaling heap growth observed on .53 (RSS ~3GB @48h, ~58MB/hr) traced to build_connection_coinbase json->string churn. Build: BUILD_OK 2026-06-04T15:04:41Z (release, boost 1.90.0, cobalt off). Touches src/core/* + src/impl/btc/stratum only; no src/impl/ltc/* footprint.
CMake link fix (CI family 2). HashrateTracker vardiff API (set_difficulty_bounds/set_target_time_per_mining_share/enable_vardiff/ set_difficulty_hint/get_current_difficulty/record_mining_share_submission/ difficulty_changed_since/get_current_hashrate) is defined in src/c2pool/hashrate/tracker.cpp and built into the c2pool_hashrate OBJECT library, but the btc executable target did not name it directly. OBJECT-lib objects are only pulled into the final link when named directly on the consuming target (#22/#39); core+stratum_server.cpp references were therefore unresolved at link. Matches the test-target precedent. Family 1 (ltc::coin in shared core) still outstanding; full link verification pending that fix.
…nk fix
P2 foundation only -- two new inert headers, NOT yet wired into web_server:
- src/core/coin/work_view.hpp: coin-agnostic 3-field slice
{json m_data; vector<uint256> m_hashes; time_t m_latency} that web_server reads
- src/core/coin/node_iface.hpp: non-template core::coin::ICoinNode base
{get_work_view(); submit_block_hex(hex,ignore); is_embedded(); has_rpc()}
Nothing includes these yet, so this commit is build-inert and does NOT make the
btc target link or go green. web_server wiring + per-coin concrete nodes (ltc/doge
by ltc-doge, btc mirror) land in follow-up cluster commits.
Add src/impl/btc/coin/coin_node.{hpp,cpp}: the BTC concrete node
implementing the shared core::coin::ICoinNode seam (node_iface.hpp).
- get_work_view(): embedded-preferred / RPC-fallback; retains the full
per-coin rpc::WorkData (incl. m_txs) coin-side via work.set(wd),
sequenced before moving the agnostic slice (m_data/m_hashes/m_latency)
into the returned core::coin::WorkView. Full WorkData never crosses
the seam.
- submit_block_hex(): guards on a null RPC client, then forwards the
agnostic 2-arg form directly (BTC NodeRPC::submit_block_hex is already
2-arg/no-mweb; MWEB is LTC-specific and absent here).
- is_embedded()/has_rpc(): orthogonal booleans backing the web_server
status labels and JSON keys.
Build-inert: compiled into btc_coin but not yet wired into web_server
(member-wiring is a separate cluster). BTC is not green at this commit;
the seam gate flips only after wiring + a clean nm census.
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.
Combined head for the V36 BTC embedded arc + the family-1 coin-adapter (WorkView) seam.
What
core::coin::ICoinNode(CoinNode, 4c4b08b). dgbm_coin_node = nullptr(same null-tolerant pattern as dash, attached via setter not ctor). dgb concrete ICoinNode is a decoupled follow-up commit (CC dgb-scrypt-steward), NOT in this PR.src/impl/bitcoin_family/UNTOUCHED. HARD-STOP shared filessrc/core/packet.hpp(A4 protocol cap) andsrc/core/coin_params.hpp(A3 scaffolding) UNTOUCHED.Shared-core scope — exceptional, justified per file (10 files = the family-1 P2 WorkView seam)
core::coin::ICoinNodeinterface (get_work_view / submit_block_hex 2-arg / is_embedded / has_rpc) — the seam btc CoinNode binds to; replaces per-coin templated node coupling in shared core.if constexpr (requires { tx.m_hogEx; })so BTC MutableTransaction (no MWEB) compiles through the shared cache; LTC behavior unchanged.core::StratumServerfrom coin-specific work-gen/share-validation; LTC MiningInterface and BTC BTCWorkSource both implement; virtual dispatch (~1ns) off the hot path.shared_ptr<MiningInterface>toIWorkSource*via virtual dispatch; coin node attached by setter (null-tolerant), no ctor change.ICoinNode*, drop ltc fwd-decls; getwork/submit/status paths nullptr-guarded so dgb and dash ride nullptr.Merge gates (not blocking PR creation)
No third-party attribution.