This page explains why the node is built this way and how it compares to
Bitcoin Core and to external Electrum indexers. The documentation map is
docs/README.md. Normative layouts and role tables live in
the linked owner docs.
Status: experimental 0.x. On-disk format and APIs are unstable until 1.0.
Peers (BIP324 v2)
│
▼
IBD densify getdata ──► in-RAM body queue (process-local FIFO)
│ │
│ ▼
│ Confirm lookup → load → scripts → write
│ (sole Class A appender + Class C tip)
│ │
└──── Mempool / tip follow ────┘
│
Reconstruct wire ◄────┤
Electrum joins ◄────┘ Class A + SH + mempool
IBD height-ordered path (current): peer offers raw framed wire into the
body queue and notes readiness on the confirm feed; confirm lookup decodes,
TipOnly-resolves, and take_raw onto loadq; load stamps with that
pres then pins; scripts are pure CPU; write is the only Class A
appender. Roles: concurrency.md. Stage IO:
invariants.md. RAM: ibd-memory.md.
Invariant — Class A never leads tip: there is no dual-track “archive Class A
far ahead of confirmed tip.” Wire plan + Class A append + Class C tip advance
are one confirm-write era. Do not reintroduce plan-time “archive lead” heuristics
(e.g. body posix_fadvise(DONTNEED) when body count ≫ tip) — under this path
just-written body pages stay tip-hot. No ContigPark / archive-job fallback
for unknown-height bodies (mark missing → re-getdata).
- Storage center is a transaction-relational archive on map-free
tables (pread/pwrite + fallocate; no process
mmapof Class A/B/CTableFiles), not a UTXO set + LevelDB chainstate. Sealed.fuse8sidecars are a read-only mmap exception. Packed MPHFgstays FdOnly. IO modality:io-modality.md. - Consensus scripts are verified in pure Rust (secp256k1 only as the
crypto primitive via the rust-bitcoin stack — no
libbitcoinconsensusdual-eval). - Electrum is native to the store (optional scripthash tables via
--sh-index, default off), not a second process re-indexing blk files. Tip-follow does not wait on SH materialize; Electrum/Esplora do. SH tip materialize is unsorted per-shard files then unique-sort; cold megakey pages stream into 4 KiB delta pages (schema 17). 0.8: Esplora covers mempool/electrs/internal/*bulk HTTP so this binary can replace electrs behind mempool.space/api/(COMPAT.md). - JSON-RPC (optional) is a Core-class subset over archive + mempool —
see
rpc.md.
| Concern | rbitcoin | Bitcoin Core (typical) |
|---|---|---|
| Primary store | Map-free Class A/B/C tables (fd pread/pwrite + heads; page cache L0) | blocks/blk*.dat + undo + LevelDB chainstate (UTXO) |
| Historical block serve | Reconstruct from txout+seqsigwit; tip uses body queue + peer wire |
Serve raw blk files / undo |
| Spentness | Annotations on create outputs (+ rare multi-list); no mutable UTXO set as truth | Coins view / UTXO mutations |
| Concurrency during IBD | Fixed roles (one Class A appender, separate confirm pipeline); HWM publish order — no map epochs | More global chainstate coupling |
| Transport | BIP324 v2 only | v1 + v2 |
| Script verification | Pure Rust in-tree (rbitcoin-consensus::script) |
libbitcoinconsensus / script interpreter in C++ |
| Electrum | In-process index on confirm | External (Fulcrum, ElectrumX, …) |
| IBD vs “current” | Dedicated densify loop exits when the connected work path has no remainder and is at (or one chatter block from) advertised peer height; then disconnect and enter_tip_mode. A restart whose confirmed tip is above genesis, already meets minimum chain work and --max-tip-age (default 24h), and whose header store has no heavier chain at any fork depth, enters tip follow without that loop. Peer-advertised height is not an input to the skip. After the switch, relay-inhibited is --min-chain-work + --max-tip-age. RPC initialblockdownload is the Core alias for that latch (not “still densifying”). |
Same header/block pipeline throughout. IsInitialBlockDownload latches false on min chain work + tip timestamp recency (nMaxTipAge). btcd/libbitcoin also latch on tip time, not empty getdata. |
| Product scope | Full node + Electrum/Esplora; 0.8 Core+electrs drop-in (COMPAT.md); no wallet/GUI/prune |
Full Core product surface |
Product / wire intentional differences: COMPAT.md.
| Concern | rbitcoin | Typical external indexer |
|---|---|---|
| Data source | Same process as the validating node; Class A is authoritative | Reads Core RPC or blk files after the fact |
| SH index | Written on confirm (runs in Direct IBD → bulk at tip) | Separate DB built by scanning history |
| Unconfirmed | Mempool attached in-process | Depends on Core mempool RPC |
| Consensus | This binary validates blocks/scripts | Trusts the node it indexes |
Core / Fulcrum stay in the tables above. Snapshot of Hornet Node and
satd (tests, designs, explicit non-copies):
peer-clients.md. Do not paste that comparison here.
Deep layout and current bytes: SCHEMA.md. Crash / tip
commit: docs/crash-recovery.md.
| Class | Role | Mutation style |
|---|---|---|
| A | Canonical archive: headers, split txs (txout / seqsigwit / spent + txid.body / tx.head/) |
Append bodies; publish via HWM / heads (allocate-then-publish) |
| B | Forever-open indexes (e.g. Electrum scripthash) | Append + head updates; may grow forever per key |
| C | Tip / confirmation: confirmed[], strong_tx, height fence |
Tip advance is the commit; may lead/lag slightly across crash |
Spend model: do not rewrite old output rows as a UTXO set. Spends are recorded as annotations (and rare multi-spender lists), with best-chain visibility defined by confirmation / strong flags — not by deleting coins from a LevelDB bag.
- Historical blocks are rebuilt from Class A (zip
txout+seqsigwit) rather than kept forever as raw wireblkfiles. - Tip serve / reorg uses the in-RAM body queue and peer wire.
There is no on-disk tip wire ring (
rbitcoin-wire-cacheis gone). - Epoch finalize fsyncs buried archive prefixes in steady state; IBD itself does not promise Core-class durability mid-catch-up.
The node follows the fully valid chain with strictly most cumulative
work (Bitcoin rule). Header work only ranks candidates; full block
connect decides the tip. IBD rewinds at most 1,024 blocks
(REWIND_MAX_DEPTH). Tip-follow cannot assemble a side branch that forks
more than 288 blocks below the tip (held-body window) and does not hand
off to the IBD rewind. Deeper reorgs are open
(088).
Tip-follow pending cap is 128 (MAX_PENDING_BLOCKS) so a ≥99-block
divergence can still be assembled. Catch-up getdata is windowed to
16 (MAX_SERVE_BLOCKS) so it matches per-session reconstruct serve;
drain continues the header path after that window connects. An inbound
getdata serves until 16 bodies or 4 MiB sit in the writer queue (RAM
cap). The rest stays on the session, which reads no new message until it
is served, as Core ProcessGetData does on fPauseSend
(090). A getdata body
and a getblocktxn reply each take a serve_inflight slot and each
frees one; a compact tip announce is not counted. blocktxn is charged
its serialized payload. Archive reconstruct for that reply runs on the
blocking pool.
# IBD
headers prove heavier branch
→ rewind_to_height(LCA) (DisconnectTip per dropped block)
→ plant winning hashes as the linear work path
→ lookup → load → scripts → write (normal confirm)
# Tip-follow
headers / pending
→ MostWorkSelector (skip invalid-marked)
→ gather bodies (held-by-hash · Class A)
→ ChainHub::accept_branch (snapshot → disconnect → connect)
Two layers:
# Layer 1 — candidate ranking (headers only)
prefer A over B iff chain work at A's tip > chain work at B's tip
(fork-point chain work + header work along the branch)
and no apply-path hash is invalid-marked; equal work keeps the first seen
# Layer 2 — most-work *valid* (full blocks)
connect block by block. On fail: mark the block invalid; keep the connected
prefix if it has strictly more work than the old tip, else restore the old
tip; re-rank.
| Path | Behavior |
|---|---|
| IBD | Up to 1,024 blocks (REWIND_MAX_DEPTH). When headers prove a strictly heavier branch, rewind the confirmed tip to the LCA (ChainHub::rewind_to_height) and plant that branch as the linear work path. The shipped lookup→load→scripts→write pipeline then confirms it — no side-channel gather, no HELD_CAP apply, no accept_branch of 40 mid bodies. Resume seed does this before confirm starts. BadPrev / competing tip+1 is the same helper (backstop). Work-path slots stay first-wins and prev-anchored. Offer will not stamp tip+1 unless prev == store tip. Do not mark_missing the winning-path hash. lookup_taken_hi rewinds to the LCA. |
| Tip-follow | Pending cap 128. Complete bodies: accept_received_block → hold by hash → accept_branch. |
| Resume | resume_work_path_after_tip: child score = subtree header work, then depth; Class A body only tie-breaks. A greater-work sibling rewinds to the LCA and becomes the linear path. Body preference alone must never re-elect an archived losing fork. |
| Invalid heavy | Heavier header path that fails connect does not win; re-rank remaining candidates (may adopt a third valid chain). Invalid marks are process-local. |
L = current tip, valid, work 100
M = peer header chain, work 150, connect fails mid-path
N = other peer chain, work 120, all blocks valid
Attempt M → fail; the failed block is invalid-marked. Then:
M's valid prefix at 130 > L → keep the prefix → re-rank → tip stays (130 > N)
M's valid prefix at 110 > L → keep the prefix → re-rank → tip = N
M's valid prefix at 100 ≤ L → restore L → re-rank → tip = N
IBD may disconnect on header work (losing bodies stay in Class A; the
winner is confirmed through the pipeline). Tip-follow still gathers bodies
before accept_branch. Orchestration only — never confirm
lookup/load/scripts/write. Code: most_work, ChainHub::rewind_to_height,
accept_branch, ibd::reorg.
Do not reintroduce soft-only BadPrev handling for a known competing prev — that livelocked confirm on a losing sibling tip.
tx.head/ is a segmented keyless address table (mixed txid → relative
create_fk; verify on txid.body). Header hashes use header.head
(HashHead). Scripthash uses a third shape. Which module/file:
docs/heads.md. Bytes: SCHEMA.
| Piece | Implementation |
|---|---|
| Headers, structure, connect, BIP68, sigops, … | rbitcoin-consensus |
| Script interpreter + typed paths (P2PKH/WPKH/WSH/TR/…) | rbitcoin-consensus::script (no bitcoinconsensus / libbitcoinconsensus) |
| ECDSA / Schnorr primitives | secp256k1 via the rust-bitcoin dependency stack only |
| Types / wire at edges | rust-bitcoin |
Consensus workarounds where rust-bitcoin is not Core-faithful: living list
rust-bitcoin-limitations.md.
Workspace Cargo.toml explicitly avoids enabling bitcoin’s bitcoinconsensus
feature. Script verification is a pure function of (tx, input_index, prevout)
after connect resolves prevouts.
Milestone (assumevalid-style): default mainnet names block 840000
(testnet3: block 2500000) and skips script/sig checks only when that
header path contains the anchor hash and this block is the path hash at
its height. Chain work does not have to meet the floor first. An explicit
--milestone HEIGHT is height-only. Signet’s default is full scripts. Flags and honesty: OPERATOR.md.
Test matrix for rules we own: docs/consensus-tests.md.
Map of every owner: docs/README.md.