Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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; 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 @@ -695,7 +695,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 @@ -705,23 +705,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
68 changes: 53 additions & 15 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 Down Expand Up @@ -202,6 +208,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 @@ -267,6 +274,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 @@ -331,7 +341,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 @@ -366,11 +400,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