Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,12 @@ before 1.0).
- **Lightning chain backends (`Q-69` Open):** [`docs/lightning.md`](docs/lightning.md)
owns CLN `bcli` and ldk-node Esplora/Electrum.

- **Schema 25 `txstat.body`:** 8 B/create ULEB confirm-time econ
(`n_in`/`fee_sat`/`base`/`wit_extra`) with per-header remaining-byte overflow.
Occupied 24 open rewrites `meta` and zero-extends the file to loc count (no
`txout.body` rewrite). Unreleased leftover `txfixed.body` is unlinked. A 24
binary refuses 25 `meta`.

- **Core functional `mempool_packages.py`:** inventory `run`. Verbose mempool
`vsize` / ancestor-descendant size use Core ceil-vsize; `wtxid` is on both
`getmempoolentry` and verbose `getrawmempool`; non-verbose txid list is
Expand Down
4 changes: 2 additions & 2 deletions COMPAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ Per-method notes, auth, and the shindex matrix live in
| Method group | Status |
|--------------|--------|
| Control (`help`, `uptime`, `stop`, `getrpcinfo`, `echo`) | done (`syncwithvalidationinterfacequeue` omitted; functional proxy no-op for Core `sync_mempools`) |
| Blockchain (`getblockchaininfo`, `getblockcount`, `getbestblockhash`, `getblockhash`, `getblock`/`header`, `getdifficulty`, `getblockstats`) | done (archive reconstruct; disk/progress real) |
| Blockchain (`getblockchaininfo`, `getblockcount`, `getbestblockhash`, `getblockhash`, `getblock`/`header`, `getdifficulty`, `getblockstats`) | done (`getblockstats` from `txstat` when stamped; size and count fields match Core, including `utxo_increase_actual`; omit coins-DB `utxo_size_*`) |
| Network (`getnetworkinfo`, `getconnectioncount`, `getpeerinfo`, `addnode`, `disconnectnode`, `addconnection`) | done (BIP324 v2-only; peer `timeoffset` / `synced_*` from session state) |
| Mempool / rawtx (`getmempool*`, `getrawtransaction`, `sendrawtransaction`, `testmempoolaccept`) | done (Libre; RPC `maxfeerate` / `maxburnamount` / `"version"` only) |
| Coin / MiniWallet (`gettxout`, `scantxoutset` `raw(HEX)`) | done (Class A unspent walk — not a coins-DB) |
Expand Down Expand Up @@ -223,7 +223,7 @@ App `ServeLimits` always on (same model as Electrum).
| Tip | done | `/blocks/tip/height`, `/blocks/tip/hash`. REST stamps `X-Bitcoin-Chain-Tip` / `X-Bitcoin-Chain-Tip-Height` (CORS-exposed): **live tip** for block/tx/header routes; **SH watermark** for `/address/` and `/scripthash/` so wallet JSON matches the SH join. Empty chain omits them (existing 503). If the pin dies mid-request: **503** `chain view moved`. |
| Blocks list | done | `/blocks`, `/blocks/:start_height` (10 summaries, newest-first) |
| Block | done | `/block/:hash` JSON, `/raw`, `/status`, `/header`, `/txids`, `/txid/:i`, `/txs[/:start]`. JSON `bits` is the compact-target **u32** (Esplora schema, not Core hex). `size` / `weight` are BIP144 total size and BIP141 weight (witness included). |
| Tx | done | `/tx/:txid` full JSON, `/hex`, `/raw`, `/status`, Electrum `/merkle-proof`, BIP37 `/merkleblock-proof`, `/outspend(s)` (`vin` from the spent slot; unspent omits it). Mempool-only txs (not in Class A) use the wire body from the mempool hub (`vin`/`vout`/`size`/`weight`/`fee`/`sigops`, `status.confirmed` false) including `GET /tx/:txid/status`. Live `/outspend(s)` overlay mempool spends of confirmed coins; `?asof=` omits mempool. `?asof=<hash>` on `/status` and `/outspend(s)`: confirmed/spent as of that ancestor; 404 if not on chain. |
| Tx | done | `/tx/:txid` full JSON, `/hex`, `/raw`, `/status`, Electrum `/merkle-proof`, BIP37 `/merkleblock-proof`, `/outspend(s)` (`vin` from the spent slot; unspent omits it). Below inwit prune: JSON is 200 partial (`pruned: true`, vout+status, empty `vin`); stamped `txstat` fills `fee`/`size`/`weight`. Mempool-only txs (not in Class A) use the wire body from the mempool hub (`vin`/`vout`/`size`/`weight`/`fee`/`sigops`, `status.confirmed` false) including `GET /tx/:txid/status`. Live `/outspend(s)` overlay mempool spends of confirmed coins; `?asof=` omits mempool. `?asof=<hash>` on `/status` and `/outspend(s)`: confirmed/spent as of that ancestor; 404 if not on chain. |
| Address / scripthash | done | stats + `/utxo` + `/txs` + `/txs/mempool` + `/txs/chain[/:last_seen_txid]` + `/txs/summary[/:last_seen_txid]` (dialect; next row). `/utxo` matches Electrum listunspent (mempool funding + drop mempool-spent confirmed); `/txs` and `/txs/mempool` use full Esplora tx JSON for mempool-only rows (wire from the hub). `?after_txid=` on `/txs` and `/txs/summary` skips through that tx (mempool then chain); unknown or unparseable → **422** `after_txid not found`. `POST /addresses/txs` and `POST /scripthashes/txs` (and `/txs/summary`) take a JSON array (max **300**; over → **422** `body too long`); unique scripts join once per request, merge newest-first. Unix/loopback `X-Rbitcoin-Client`: last-1 GET join + last-bulk POST (16 MiB packed/client), 30s idle, 256 clients, singleflight on the same `(client, sh)`. Public TCP ignores the header (stack-local join). Concurrent different GET scripts replace last-1. Unbounded process LRU stays **X-M3**. Needs SH finalize. Stamp is visible SH (durable + pending write-behind), matching live tip while jobs sit in RAM. `?asof=<hash>` on `/`, `/utxo`, `/txs`, `/txs/chain`, `/txs/summary`: confirmed join at that ancestor **at or behind visible SH**, **no** mempool; headers are the asof hash; 404 if not on chain or ahead of visible SH. |
| `/txs/summary` | dialect | **Not** in Blockstream Esplora [`API.md`](https://github.com/Blockstream/esplora/blob/master/API.md). Compact `{txid, value, height, time}` like mempool.space `/address/:addr/txs/summary`. Confirmed only (25/page, newest first); path cursor `/:last_seen_txid` and mempool.space `?after_txid=` (unknown → **422**). `value` is net sats for that script in that tx (funded − spent). `time` is the confirming header timestamp (`0` if the header is missing). Mempool rows stay on `/txs` and `/txs/mempool`. Over `--max-sh-creates` → **503**. |
| Mempool / fees | done | `/mempool`, `/mempool/txids`, `/mempool/txids/page[/:last]`, `/mempool/recent` (accept-order ring), `/fee-estimates`, mempool.space `/fees/recommended` and `/v1/fees/recommended` (sat/vB tiers) |
Expand Down
21 changes: 11 additions & 10 deletions OPERATOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,7 @@ counts, ingest OA, and refuse lines: [`SCHEMA.md`](./SCHEMA.md) and

## Schema upgrade

Live bytes: [`SCHEMA.md`](./SCHEMA.md) (`SCHEMA_VERSION = 24`). This section is
Live bytes: [`SCHEMA.md`](./SCHEMA.md) (`SCHEMA_VERSION = 25`). This section is
the operator copy-paste only — do not treat it as a second layout map.

Open **never silently wipes** a populated store (policy:
Expand All @@ -708,23 +708,24 @@ names the dirs. Corrupt files are **not** repaired in-process.

| Incoming `meta` | What this binary does |
|-----------------|------------------------|
| **24** | Open. |
| **23** | Rewrite `meta` to 24 first, then rewrite `header.body` 88 B rows to 96 B (size/weight 0) on open. Class A tx stems kept. A torn `header.body` rewrite is retried. |
| **22**, occupied Class A | Rewrite `meta` to 24 first, then `create.loc.ovf` 12 B→16 B on `TxTable::open`, then `header.body` 88→96. Crash window is 24 `meta` + old ovf/header; this binary retries those file rewrites. |
| **22**, empty Class A | Rewrite `meta` to 24, then open. |
| **21**, empty Class A | Unlink leftover `spent.off`, rewrite `meta` to 24, then open. |
| **25** | Open. |
| **24** | Rewrite `meta` to 25, then create/extend zeroed `txstat.body` to `create.loc` count (no `txout.body` rewrite). Unlink leftover `txfixed.body`. |
| **23** | Rewrite `meta` to 25 first, then rewrite `header.body` 88 B rows to 96 B (size/weight 0) on open. Class A tx stems kept. A torn `header.body` rewrite is retried. Zero-extend `txstat.body`. |
| **22**, occupied Class A | Rewrite `meta` to 25 first, then `create.loc.ovf` 12 B→16 B on `TxTable::open`, then `header.body` 88→96. Crash window is 25 `meta` + old ovf/header; this binary retries those file rewrites. Zero-extend `txstat.body`. |
| **22**, empty Class A | Rewrite `meta` to 25, then open. |
| **21**, empty Class A | Unlink leftover `spent.off`, rewrite `meta` to 25, then open. |
| **21**, occupied Class A | **Refuse.** Wipe datadir and redo IBD. |
| **20**, empty Class A | Unlink leftover `spent.off`, rewrite `meta` to 24, then open. |
| **20**, empty Class A | Unlink leftover `spent.off`, rewrite `meta` to 25, then open. |
| **20**, occupied Class A | **Refuse.** Wipe datadir and redo IBD. |
| **19** or **18**, empty Class A and empty `tx.head` / no `scripthash*` data | Rewrite `meta` to 24, then open. |
| **19** or **18**, empty Class A and empty `tx.head` / no `scripthash*` data | Rewrite `meta` to 25, then open. |
| **19** or **18**, occupied Class A | **Refuse.** Wipe datadir and redo IBD. |
| **19** or **18**, empty Class A, occupied `tx.head` or any `scripthash*` | **Refuse.** Wipe `store/tx.head` and `store/scripthash*`, keep Class A, restart. |
| **17**, empty Class A and empty `tx.head` / no `scripthash*` data | Rewrite `meta` to 24, then open. |
| **17**, empty Class A and empty `tx.head` / no `scripthash*` data | Rewrite `meta` to 25, then open. |
| **17**, occupied Class A | **Refuse.** Wipe datadir and redo IBD. |
| **17**, empty Class A, populated `tx.head` or any `scripthash*` | **Refuse.** Wipe those index dirs, keep Class A, restart. |
| Older than 17 with creates / leftover catalogs | **Refuse.** The error names files; often a full datadir wipe + IBD. Details: SCHEMA.md **13/14→17**, **15→17**, **16→17**. |

A **23 binary** refuses 24 `meta` (do not downgrade in place). A **22 binary** refuses 23+ `meta`. A **21 binary** refuses 22+ `meta`. A **19 binary** refuses 20+ `meta`.
A **24 binary** refuses 25 `meta` (do not downgrade in place). A **23 binary** refuses 24+ `meta`. A **22 binary** refuses 23+ `meta`. A **21 binary** refuses 22+ `meta`. A **19 binary** refuses 20+ `meta`.

When the schema-22 Class A refuse fires, the log line is:

Expand Down
72 changes: 55 additions & 17 deletions SCHEMA.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# On-disk schema (current)

**Version:** `SCHEMA_VERSION = 24` (`rbitcoin_primitives`).
**Status:** 24 is `header.body` 96 B (trailing `size:u32` + `weight:u32`). Occupied
**Version:** `SCHEMA_VERSION = 25` (`rbitcoin_primitives`).
**Status:** 25 is `txstat.body` 8 B/create (canonical ULEB `n_in`/`fee_sat`/`base`/`wit_extra`;
per-header remaining-byte overflow in `txstat.ovf` + `txstat.blk`). Occupied
24 rewrites `meta` and zero-extends `txstat.body` to `create.loc` count (**no**
`txout.body` rewrite; leftover LAYOUT17 still has uleb `input_count`). Unreleased
leftover `txfixed.body` is unlinked. A 24
binary refuses 25 `meta`. 24 is `header.body` 96 B (trailing `size:u32` + `weight:u32`). Occupied
23 rewrites 88 B rows via `header.body.grow` then rename (size/weight 0 until
confirm stamps or lazy fill). SH extent last-page reserved (offset 20) is create
count (`0` = unknown; readers walk, appender stamps on pack/append). 23 is `create.loc.ovf` 16 B (`fk:u64` + strides/`n_out`
Expand All @@ -13,13 +18,13 @@ slots flags + u40 spend fk + u16 vin (still 8 bytes). `txout` amount is flags bi
is canonical compact: strip trailing tens up to `e=9` (`e<9` and mantissa
divisible by 10 is Corrupt; zero is `e=0`, mantissa 0). Occupied
15–21 LAYOUT17 Class A with creates is **refused**
(wipe datadir and redo IBD). Empty 15–23 rewrite `meta` to 24 and unlink leftover
(wipe datadir and redo IBD). Empty 15–24 rewrite `meta` to 25 and unlink leftover
`spent.off` and leftover `*.idx`. A 23 binary refuses 24 `meta`. Occupied schema
18/19 `tx.head` or `scripthash*` (empty Class A) is **refused** (wipe those index
dirs, keep Class A). Empty 18/19 indexes rewrite `meta` to 24; `tx.head` rebuilds
dirs, keep Class A). Empty 18/19 indexes rewrite `meta` to 25; `tx.head` rebuilds
from Class A; SH rematerializes with `--sh-index`. An 19 binary refuses 20+
`meta`. A 17 datadir with populated `tx.head` or `scripthash*` and empty
Class A is **refused**. Empty 17 indexes rewrite `meta` to 24.
Class A is **refused**. Empty 17 indexes rewrite `meta` to 25.

Operator copy-paste (which dirs to wipe; kill-9 is not a migrate):
[`OPERATOR.md`](./OPERATOR.md#schema-upgrade).
Expand Down Expand Up @@ -73,11 +78,12 @@ index refuses pack8 Paged (mode 10) scripthash heads; wipe store/scripthash* the
```
**21→22 open:** occupied Class A with creates:
`schema 22 refuses schema-21 Class A with creates; wipe datadir and redo IBD`.
Empty 21 rewrites `store/meta` to 24 and unlinks leftover `spent.off`.
Table file headers 13–24 remain `schema_file_openable`. A 22 binary refuses 23 `meta`.
Occupied 15–20 LAYOUT17 Class A with creates hits the same refuse (old flags+u56-fk / no vin pack). Empty 15–20 rewrite `meta` to 24.
Empty 21 rewrites `store/meta` to 25 and unlinks leftover `spent.off`.
Table file headers 13–25 remain `schema_file_openable`. A 22 binary refuses 23 `meta`.
Occupied 15–20 LAYOUT17 Class A with creates hits the same refuse (old flags+u56-fk / no vin pack). Empty 15–20 rewrite `meta` to 25.
**22→23 open:** occupied Class A rewrites `create.loc.ovf` 12 B rows (`fk:u64` + two u16) to 16 B (`fk:u64` + two u32) and `store/meta` to 23. Empty 22 rewrites `meta`. A 22 binary refuses 23 `meta`. Spent vin stays u16 (stripped input ≥ ~41 B ⇒ ≲24k vins in a 1 MB block; widening would bump the 8 B spent slot).
**23→24 open:** rewrite `header.body` 88 B rows to 96 B (`size:u32` + `weight:u32` = 0) via `header.body.grow` then rename; rewrite `meta` to 24. Class A tx stems kept. Empty 23 rewrites `meta`. A 23 binary refuses 24 `meta`. Crash with leftover `.grow` discards it and retries; 96-byte body with meta 23 only rewrites `meta`.
**23→24 open:** rewrite `header.body` 88 B rows to 96 B (`size:u32` + `weight:u32` = 0) via `header.body.grow` then rename; rewrite `meta` to 25. Class A tx stems kept. Empty 23 rewrites `meta`. A 23 binary refuses 24 `meta`. Crash with leftover `.grow` discards it and retries; 96-byte body with meta 23 only rewrites `meta`.
**24→25 open:** rewrite `meta` to 25; create or zero-extend `txstat.body` to `create.loc` count. Do **not** rewrite `txout.body`. Unlink leftover `txfixed.body`. A 24 binary refuses 25 `meta`.
**Endianness:** little-endian for all multi-byte integers.

Older versions and migration notes live in [`SCHEMA_HISTORY.md`](./SCHEMA_HISTORY.md).
Expand All @@ -87,7 +93,7 @@ Older versions and migration notes live in [`SCHEMA_HISTORY.md`](./SCHEMA_HISTOR
## Schema 17 freeze

Class A shape settled here at schema 17 and is still the live layout under
the `SCHEMA_VERSION` at the top of this file (24). Schemas 18–24 already
the `SCHEMA_VERSION` at the top of this file (25). Schemas 18–24 already
shipped; they are open rules above and history in
[`SCHEMA_HISTORY.md`](./SCHEMA_HISTORY.md). A byte-incompatible change bumps
from that live constant
Expand Down Expand Up @@ -214,6 +220,7 @@ itself changed.
spent.body # sole-spender 8 B × n_out; leftover spent.off unlinked
tx.body / tx.idx.* # schema ≤14 packed (refused if non-empty)
txid.body # dense create_fk-ordered txids (schema 13+)
txstat.body / txstat.ovf / txstat.blk # 8 B/create ULEB econ + per-header tails (schema 25)
tx.head/ # meta + open OA NNNNNN; sealed NNNNNN.mphf|.fuse8
spent.ovf # multi-spender overflow (was spenders.body)
confirmed.body # Class C: height → header_fk
Expand Down Expand Up @@ -257,7 +264,7 @@ the hot volume.
| Offset | Size | Field |
|--------|------|-------|
| 0 | 4 | Magic `RBT1` |
| 4 | 2 | Schema version (u16) — live **24** (`SCHEMA_VERSION`). Occupied files keep the version they were written; **13–24** remain `schema_file_openable` |
| 4 | 2 | Schema version (u16) — live **25** (`SCHEMA_VERSION`). Occupied files keep the version they were written; **13–25** remain `schema_file_openable` |
| 6 | 2 | Table kind (u16) |
| 8 | 8 | Logical length (bytes), including this header |

Expand All @@ -282,6 +289,9 @@ the hot volume.
| 16 | inwit (`inwit.body`) |
| 17 | spent (`spent.body`) |
| 18 | delta loc (`create.loc` / `inwit.loc` and `.ovf`) |
| 19 | txstat (`txstat.body`, 8 B/create) |
| 20 | txstat overflow (`txstat.ovf`) |
| 21 | txstat per-header locator (`txstat.blk`, 16 B/header) |

---

Expand Down Expand Up @@ -346,7 +356,31 @@ offset 0..32 — 32-byte file header (standard 16-byte TableFile header + 16
offset 32+(fk-1)*32 — txid for create_fk = fk (1-based)
```

Append-published with Class A body/idx on the sole Class A write path. Count must match `txout` / `inwit` / `spent` / `txid.body`. Head-resolve multi-cand identity peeks this file (fixed offset), **not** a body prefix.
Append-published with Class A body/idx on the sole Class A write path. Count must match `txout` / `inwit` / `spent` / `txid.body` / `txstat.body`. Head-resolve multi-cand identity peeks this file (fixed offset), **not** a body prefix.

### Confirm-time econ (`txstat.body`, schema 25)

```text
txstat.body offset 0..32 — TableFile 16 + 16 pad
offset 32+(fk-1)×8 — 8-byte cell
txstat.ovf append-only — remaining ULEB bytes when the stream exceeds 8 B
txstat.blk offset 32+(header_fk-1)×16 — off:u64, len:u32, n_ovf:u32
```

Cell payload is four canonical ULEBs: `n_in`, `fee_sat`, `base` (non-witness
size), `wit_extra` (`total_size − base`). Readers derive `size = base + wit_extra`
and `weight = 4×base + wit_extra`. `n_in` is first and ≤ 3 B (`≤ u16::MAX`, same
cap as `spent.vin`) so pin / SH / tweaks parse it from the cell without opening
`.ovf`. All-zero cell = unstamped. A truncated ULEB or fewer than four fields
means the rest of the stream is in that header's overflow blob (`encoded[8..]`
only). Missing tail is `Corrupt("invariant: txstat overflow missing")`. Overlong
ULEB is Corrupt. Trailing non-zero after four fields is Corrupt. Placeholders
(`n_in ≠ 0`, rest zero) always fit. Class A append of placeholders never overflows;
only a confirm stamp can emit tails. Blob entries are `u16 index_in_block` +
`u8 nrest` + rest, in block-index order. Empty header: `len=0`. Occupied 24 open
extends zeros to loc count (~11.3 GiB at the 2026-08-13 census if fully allocated).
Pin / SH / tweaks do **not** open these files. Unreleased leftover `txfixed.body`
is unlinked on open.

### Split bodies (schema 15)

Expand Down Expand Up @@ -381,11 +415,15 @@ Decode walks meta + runs to a logical end; any remaining bytes in the loc span m

**Body meta (schema 22 LAYOUT17, variable):** first byte bit 7 = `LAYOUT17`
(required). Bits 0–2 encode version 1/2/3 (else explicit i32 LE); bit 3 =
locktime 0 (else uleb locktime); then uleb `input_count` only (no
`output_count`). Typical v2+locktime 0 is **2 B**. `CreateLocPair.n_out` (≥ 1)
fills `TxRecord.output_count`. Schema-15 16-byte prefixes (v1 starts
`01 00 00 00`) are not accepted. `input_start_fk` / `output_start_fk` stay null
in RAM. Soft `TxRecord.txid` is filled from the sidefile on get paths.
locktime 0 (else uleb locktime). Bit 4 (`N_IN_TXSTAT`) omits the following
uleb `input_count` (`n_in` is on `txstat.body`; decode reports 0 until a
reader fills from the stamped row). Bits 5–6 reserved (nonzero → Corrupt).
New 25 writes omit the uleb (v2+locktime 0 is **1 B**). Leftover 24 rows
keep the uleb (typical v2+locktime 0 is **2 B**).
`CreateLocPair.n_out` (≥ 1) fills `TxRecord.output_count`. Schema-15 16-byte
prefixes (v1 starts `01 00 00 00`) are not accepted. `input_start_fk` /
`output_start_fk` stay null in RAM. Soft `TxRecord.txid` is filled from the
sidefile on get paths.

### Create / inwit locators (`create.loc` / `inwit.loc`)

Expand Down
Loading
Loading