diff --git a/.cargo-crap.toml b/.cargo-crap.toml index bda0d2883..abb018b63 100644 --- a/.cargo-crap.toml +++ b/.cargo-crap.toml @@ -15,7 +15,7 @@ allow = [ "NodeConfig::apply_kv", "StrongTxTable::count_ones_bits", "StrongTxTable::set_bits_range", - "TxTable::open_inwit", + "TxTable::open_seqsigwit", "TxTable::put_spend_batch_by_abs_meta", "UringSession::submit_and_wait_one", "cli_main", diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f060c2f6..cb52a3c2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,11 +103,17 @@ 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`. +- **Schema 25 econ stems:** `txstat.body` stays 8 B/create: three ULEBs + (`fee_sat`/`base`/`wit_extra`) plus per-header remaining-byte overflow. + `n_in` is `input.loc` (u16). `input.body` is the parent edge (create fk + and vout) per input. `seqsigwit` is the old `inwit` stem (sequence, + scriptSig, witness); open renames those files. `txstat.*` and `input.*` + sit next to `seqsigwit` (cold when split). Open with no `input.loc` + backfills the edges from `seqsigwit` prevouts. A four-ULEB cell that + started with `n_in` is not rewritten; resync that datadir. Occupied 24 + open rewrites `meta` and zero-extends `txstat.body` (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 @@ -228,7 +234,7 @@ musl (operator) + Windows CRT-static PE + Darwin aarch64. - **Faster IBD:** loc rides the InFlight pin (load never loc-by-fk); body-queue `header_fk` skips header ensure; tip+1 hole racing by drain time; sequential `header.body` size/weight; BIP30 same-txid across a load wave. -- **Smaller store:** schema 22 `create.loc` + `inwit.loc` (no three Class A +- **Smaller store:** schema 22 `create.loc` + `seqsigwit.loc` (no three Class A `*.idx`); compact txout amounts; sealed fuse8 + SH occupancy mmap (heap `fuse8=` / `mphf_occ=` only). - **Mempool persist:** packed schema 2 — 5 s dirty tail, one-record DEAD @@ -975,7 +981,7 @@ musl (operator) + Windows CRT-static PE + Darwin aarch64. `ibd_skips_dead_peer` run in `cargo test --workspace` and `coverage.sh`. The separate `multinode` job is gone. [`TESTING.md`](TESTING.md). -- **No loc-count echo:** packed decode already walks loc `n_out` / inwit +- **No loc-count echo:** packed decode already walks loc `n_out` / seqsigwit `input_count`; drop the tautological len==count Corrupt. `spent_record_len(0)` is `0×8` (no special case). Delete unused no-op `head_reserve_additional`. @@ -984,7 +990,7 @@ musl (operator) + Windows CRT-static PE + Darwin aarch64. `two_node_header_and_block_sync` runs under `coverage.sh`. [`TESTING.md`](TESTING.md). -- **Schema 22:** `create.loc` + `inwit.loc` (no Class A `{txout,spent,inwit}.idx`). +- **Schema 22:** `create.loc` + `seqsigwit.loc` (no Class A `{txout,spent,seqsigwit}.idx`). LAYOUT17 omits `output_count` (decode `n_out` from loc). Spent slot is flags + u40 spend fk + u16 vin. `txout` amount is flags bits 4–7 = decimal exponent (0–9) + ULEB mantissa (`sats = mantissa × 10^e`; canonical compact, @@ -1533,7 +1539,7 @@ musl (operator) + Windows CRT-static PE + Darwin aarch64. `notfound`, serve an announced tx from the tip block). - **IBD `getdata` serve reconstructs from Class A spans:** contiguous - `header_txs` loads `txout.body` + `inwit.body` as libc sequential preads + `header_txs` loads `txout.body` + `seqsigwit.body` as libc sequential preads (not per-tx `get_tx_full`), off the session reactor. Host probe: `scripts/ibd-serve-bench.py`. @@ -1741,7 +1747,7 @@ musl (operator) + Windows CRT-static PE + Darwin aarch64. ### Changed -- **IBD `getdata` span IO:** `txout.body` and `inwit.body` span preads run +- **IBD `getdata` span IO:** `txout.body` and `seqsigwit.body` span preads run in parallel (SSD one volume, or `--datadir-cold` split). BIP324 `write_v2_contents` takes owned bytes (no extra `to_vec` on the serve writer). @@ -2379,12 +2385,12 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. or incomplete row. See [`docs/core-functional.md`](docs/core-functional.md). No Core scripts run in default `cargo test` yet. -- **`--datadir-cold PATH`:** Class A `inwit.body` / `inwit.idx/` (cold; ~486 GiB +- **`--datadir-cold PATH`:** Class A `seqsigwit.body` / `seqsigwit.idx/` (cold; ~486 GiB on mainnet) live under `{PATH}/store` when set. `--datadir` still holds every other file (`txout`, `spent`, heads, mempool, peers, cookie). Omit the flag and both hot and cold files stay in `--datadir`. Conf: `datadir-cold=`. - Existing split: move `inwit.body` + `inwit.idx/` yourself; the hot store - records `inwit.reloc` so a later open without the flag refuses. + Existing split: move `seqsigwit.body` + `seqsigwit.idx/` yourself; the hot store + records `seqsigwit.reloc` so a later open without the flag refuses. - **CI musl artifacts:** after a green `ci` run on `master`/`main`, workflow `musl` builds `nix build .#rbitcoin-musl` and uploads @@ -2528,7 +2534,7 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. graph wtxids. - **`--sptweaks` backfill:** one-core completion machine (`txout`, then - `inwit`/parents only for P2TR) and batched height-blob + idx writes. + `seqsigwit`/parents only for P2TR) and batched height-blob + idx writes. Mainnet origin→tip on SSD is typically **about 1–2 hours** (was several hours at ~15–25 h/s serial `get_tx_full`). INFO every 10 s: `sptweaks: backfill next=… tip=… rate=…/s remain=…`. @@ -2781,7 +2787,7 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. open — wipe `store/scripthash*` and restart with `--shindex`. - **`getblockchaininfo` disk / progress:** `size_on_disk` is a walk of - store file lengths (plus cold inwit when split). `verificationprogress` + store file lengths (plus cold seqsigwit when split). `verificationprogress` is `blocks / headers` (1.0 when headers is 0), not a dummy 0.5 / 1.0. - **No soak program.** Signet-first remains ordinary run advice. Q-35 is @@ -3001,8 +3007,8 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. sleep in the same `select!` was reset by every perf/RPC tick, so a node that lost its last follow peer (mainnet 962723) never redialed. -- **Class A idx rolls:** each stem (`txout` / `inwit` / `spent`) rolls its - own idx at the soft span. Inwit no longer forces hot idx splits. +- **Class A idx rolls:** each stem (`txout` / `seqsigwit` / `spent`) rolls its + own idx at the soft span. SeqSigWit no longer forces hot idx splits. - **`strong_tx`:** always L2 (1 bit/fk). `RBITCOIN_CLASS_C_INRAM_MAX_MB` still caps `confirmed` / `header_txs_*` only. - **Schema 17 freeze note:** [`SCHEMA.md`](SCHEMA.md) @@ -3073,12 +3079,12 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. store that already has Class A creates (schema 15/16 16-byte meta / 9-byte spent) or leftover `key_len=32` SH runs is refused. Empty Class A still soft-opens. This is meant to be the last full-datadir - reindex for the Class A / B / C layout; later work (inwit Δfk, a new + reindex for the Class A / B / C layout; later work (seqsigwit Δfk, a new consensus script kind) would be schema 18 and should not require another wipe of `txout` / `spent` / heads. Layout in 17: SH runs unique `(scripthash, create_fk)` at `key_len=40`; megakey pages are uleb fk0+deltas; thin LAYOUT17 `txout` meta; script kinds 0–9; 8-byte - spent slots; overflow is `spent.ovf`; reserved inwit bits 4–7 and + spent slots; overflow is `spent.ovf`; reserved seqsigwit bits 4–7 and spent flags other than `MULTI_SPENDER` are Corrupt. Leftover `archive_epoch`, `store/wire`, and single-file `sp_tweaks.idx` / `sp_tweaks.body` are unlinked on open. Tweaks (when `--sptweaks`) are @@ -3098,7 +3104,7 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. lookup wave wall is `lookup_thr wave=`. - **Confirm write path:** Class C `strong_tx` flush already wrote only the dirty - suffix — now pinned. Class A `txout`/`inwit`/`spent` bodies submit as one + suffix — now pinned. Class A `txout`/`seqsigwit`/`spent` bodies submit as one `pwrite_batch` wave. `tx.head` insert is write-behind (page-grouped drain overlaps structural/Class C); resolve hits a pending txid→fk map until drain. Crash-open backfills a lagging head from Class A. @@ -3144,7 +3150,7 @@ nightly job (not a required PR check). P2P DoS is not Core-parity. Tip write-through only when `height == next_height`. Restart resumes from `next_height`. -- **Schema 15 Class A split:** `txout.body` (outs) + `inwit.body` (ins+witness) +- **Schema 15 Class A split:** `txout.body` (outs) + `seqsigwit.body` (ins+witness) + `spent.body` (9 B×n_out). Packed `tx.body` with creates is refused. Pin/SH read outs only; annotate RMW is `spent_off+9×vout`. Working-set census in [`SCHEMA.md`](./SCHEMA.md). diff --git a/COMPAT.md b/COMPAT.md index 834f78ec0..014372b5e 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -69,7 +69,7 @@ Full `/tx/:txid` JSON still has `vin[]`. Electrum has no outspend-vin surface. | WTx inventory | BIP339 when peer also sends `wtxidrelay` | BIP339 | | GetAddr | Core `MAX_ADDR_TO_SEND` / `MAX_PCT_ADDR_TO_SEND` (**1000** / **23%**), 24h per-bind cache. Named copies in `rbitcoin-net` — do not “improve” without a named reason to diverge. `MAX_ADDR_MAN` (8192) is **our** HashMap DoS cap and must stay above `1000/0.23` | Core new/tried buckets (~80k); same 1000 / 23% | | Package submit | RPC `submitpackage` / Esplora `POST /txs/package` (no P2P package command) | BIP331 wire | -| Pruning / GUI | Unpruned: `inwit.body`. `--prune-inwit`: watermark + `NETWORK_LIMITED`, kept witness is 288 height files under `store/inwit.window/` plus a RAM cap (`--prune-inwit-ram-threshold-bytes`, `0` = files only). Not a rolling stem. Not Core `-prune` of headers/txout | Supported | +| Pruning / GUI | Unpruned: `seqsigwit.body`. `--prune-seqsigwit`: watermark + `NETWORK_LIMITED`, kept witness is 288 height files under `store/seqsigwit.window/` plus a RAM cap (`--prune-seqsigwit-ram-threshold-bytes`, `0` = files only). Not a rolling stem. Not Core `-prune` of headers/txout | Supported | | Mining template RPC | `getblocktemplate` / `getmininginfo` / `prioritisetransaction` (selector; no stratum) | GBT + stratum / pool stack | | Wallets | Electrum clients (requires `--shindex`) | Descriptor + legacy | | Scripthash index | Optional (`--shindex`, default **off**); bulk at tip when on | External ElectrumX / Fulcrum; Core `-txindex` is different (txid→block) | @@ -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). 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=` 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 seqsigwit 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=` 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=` 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) | diff --git a/OPERATOR.md b/OPERATOR.md index 9c4d4356b..78fd3d1cc 100644 --- a/OPERATOR.md +++ b/OPERATOR.md @@ -133,7 +133,7 @@ Replace the hostname and email, then apply authentication and network policy to RPC for your deployment. nginx `virtualHosts` proxy HTTP; `streamConfig` proxies the Electrum TCP protocol. -Use `coldDataDir` to place the large `inwit` store on another volume. The +Use `coldDataDir` to place the large `seqsigwit` store on another volume. The service creates the directory but does not mount or size the volume. Use `environment` for documented advanced `RBITCOIN_*` settings and `extraArgs` for daemon flags not represented by module options. @@ -361,7 +361,7 @@ Clean smoke: | Flag | Conf | Default | |------|------|---------| | `--datadir PATH` | `datadir=` | cwd `datadir` (`./datadir` Unix, `.\datadir` Windows) | -| `--datadir-cold PATH` | `datadir_cold=` | unset — Class A `inwit.body` / `inwit.loc` under `{PATH}/store`; everything else stays in `--datadir` | +| `--datadir-cold PATH` | `datadir_cold=` | unset — Class A `seqsigwit.body` / `seqsigwit.loc` under `{PATH}/store`; everything else stays in `--datadir` | | `--network NET` | `network=` | `mainnet` | | `--signet-challenge HEX` | `signet_challenge=` | default global Signet challenge | | `--signet-block-time SECS` | `signet_block_time=` | 600; requires a custom challenge | @@ -390,8 +390,8 @@ Clean smoke: | `--asmap PATH` | `asmap=` | unset — try `{datadir}/ip_asn.dat` if present; else prefix groups | | `--no-seeds` | `no_seeds=` | seeds on | | `--sh-index` | `sh_index=` | **off** — Class B scripthash (address/history; Electrum/Esplora start without it) | -| `--prune-inwit` | `prune_inwit=` | **off** — unpruned reads `inwit.body`. On: refuse wire reconstruct below tip−288 **heights**, advertise `NETWORK_LIMITED`, and keep those heights as `store/inwit.window/{height}.bin` plus a RAM cache | -| `--prune-inwit-ram-threshold-bytes N` | `prune_inwit_ram_threshold_bytes=` | `268435456` (256 MiB). `0` keeps nothing in RAM: every height, including tiny IBD blocks, is read from its file | +| `--prune-seqsigwit` | `prune_seqsigwit=` | **off** — unpruned reads `seqsigwit.body`. On: refuse wire reconstruct below tip−288 **heights**, advertise `NETWORK_LIMITED`, and keep those heights as `store/seqsigwit.window/{height}.bin` plus a RAM cache | +| `--prune-seqsigwit-ram-threshold-bytes N` | `prune_seqsigwit_ram_threshold_bytes=` | `268435456` (256 MiB). `0` keeps nothing in RAM: every height, including tiny IBD blocks, is read from its file | | `--max-sh-creates N` | `max_sh_creates=` | **0** — unlimited SH join; `N>0` refuses over-cap Electrum/Esplora (503 / JSON-RPC error) | | `--sp-tweaks` | `sp_tweaks=` | **off** — thin BIP-352 tweak index (`sp_tweaks.*`) | | `--sp-tweaks-dust SATS` | `sp_tweaks_dust=` | **1000** — omit served P2TR outs with `value <= SATS` (`0` = serve all; **546** matches Cake electrs) | @@ -508,24 +508,24 @@ cjdns daemon in-process and no TUN in CI. NixOS: `cjdns.reachable`; `--datadir` holds the node root (`store/`, `mempool/`, `peers`, `rpc.token`, `rpc.sock`). Omit `--datadir-cold` and cold files live there too. Set it to put the large -rarely-read Class A **inwit** stem (`inwit.body` + `inwit.loc`, ~486 GiB + loc +rarely-read Class A **seqsigwit** stem (`seqsigwit.body` + `seqsigwit.loc`, ~486 GiB + loc on mainnet) on another volume. Pin / spend-annotate / Electrum / tweaks do not -read inwit; reconstruct / `getrawtransaction` / block serve do. +read seqsigwit; reconstruct / `getrawtransaction` / block serve do. ``` --datadir /mnt/nvme/rbtc --datadir-cold /mnt/hdd/rbtc-cold # hot: /mnt/nvme/rbtc/store/txout.body (and the rest) -# cold: /mnt/hdd/rbtc-cold/store/inwit.body -# /mnt/hdd/rbtc-cold/store/inwit.loc +# cold: /mnt/hdd/rbtc-cold/store/seqsigwit.body +# /mnt/hdd/rbtc-cold/store/seqsigwit.loc ``` -A hot-store sidecar `inwit.reloc` records the split. Opening without -`--datadir-cold` then refuses. Do not leave `inwit.*` in both places. Moving an +A hot-store sidecar `seqsigwit.reloc` records the split. Opening without +`--datadir-cold` then refuses. Do not leave `seqsigwit.*` in both places. Moving an existing datadir is operator `mv` (or copy+remove cross-device): ``` mkdir -p /mnt/hdd/rbtc-cold/store -mv /mnt/nvme/rbtc/store/inwit.body /mnt/nvme/rbtc/store/inwit.loc /mnt/nvme/rbtc/store/inwit.off /mnt/nvme/rbtc/store/inwit.loc.ovf /mnt/hdd/rbtc-cold/store/ +mv /mnt/nvme/rbtc/store/seqsigwit.body /mnt/nvme/rbtc/store/seqsigwit.loc /mnt/nvme/rbtc/store/seqsigwit.off /mnt/nvme/rbtc/store/seqsigwit.loc.ovf /mnt/hdd/rbtc-cold/store/ ``` **Advanced** IO/perf tunables may still use `RBITCOIN_*` (see below); they are @@ -622,7 +622,7 @@ uniformly slow pack). Slow or constrained uplinks: [Slow / constrained uplink **Archive head resolve:** streaming — **FdOnly** page-coalesced head probe + **FdOnly** `create.loc` + **`txid.body`** identity via **io_uring or pread** (deepest-cand-first). -**Class A `txout` / `inwit` / `spent` + `create.loc` / `inwit.loc`, `tx.head`, header head, +**Class A `txout` / `seqsigwit` / `spent` + `create.loc` / `seqsigwit.loc`, `tx.head`, header head, SH head/body, and spenders are fd pread/pwrite**. Full modality matrix: [`docs/io-modality.md`](docs/io-modality.md). @@ -654,7 +654,7 @@ Token meanings and ring depth: [`docs/io-modality.md`](docs/io-modality.md). | Milestone (skip scripts ≤ height) | mainnet **840000**, signet 2000000, … | `--milestone` (`0` = full scripts) | | ConfirmParentCache header plans | always on | Tip-ahead header + tx_fks for multi-block MTP (no create pin FIFO) | | Bulk store IO | **uring** (Linux) when available | `RBITCOIN_IO` only. Matrix: [`docs/io-modality.md`](docs/io-modality.md) | -| Archive Class A append | **pwrite** (always) | `txout` / `inwit` / `spent` + `*.idx` | +| Archive Class A append | **pwrite** (always) | `txout` / `seqsigwit` / `spent` + `*.idx` | | `tx.head` (segmented) | fixed geometry | Default **25-bit**. Rebuild env: [`docs/env-knobs.md`](docs/env-knobs.md). Bytes: [`SCHEMA.md`](SCHEMA.md) / [`docs/heads.md`](docs/heads.md). Legacy mono-head datadirs require reindex | | Confirm stages | **lookup · load · scripts · write** | Queues and pack/wave: [`docs/concurrency.md`](docs/concurrency.md). RAM: [`docs/ibd-memory.md`](docs/ibd-memory.md) | | Confirm batch inputs | **8000** soft | Hardcoded. Live line: `h= n= in=` (**n** = blocks in pack, **in** = Σ inputs) | @@ -708,8 +708,8 @@ names the dirs. Corrupt files are **not** repaired in-process. | Incoming `meta` | What this binary does | |-----------------|------------------------| -| **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`. | +| **25** | Open. Leftover `inwit.*` is renamed to `seqsigwit.*`. Missing `input.loc` with a matching `seqsigwit` count backfills parent edges from those prevouts. Leftover `inputs.*` is renamed to `input.*`. | +| **24** | Rewrite `meta` to 25, then create/extend zeroed `txstat.body` to `create.loc` count (no `txout.body` rewrite). Same `inwit` rename and `input` backfill as 25. 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. | @@ -725,7 +725,7 @@ names the dirs. Corrupt files are **not** repaired in-process. | **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 **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`. +A `txstat.body` cell written as four ULEBs starting with `n_in` (an unreleased 25 experiment) is not detected and is not rewritten. Resync that datadir. 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: @@ -757,11 +757,11 @@ Copy-paste (node stopped with SIGTERM): DATADIR=/path/to/datadir # fuse8 v1 / Shared SH body / Paged SH heads (same dirs as schema-20 index refuse): rm -rf "$DATADIR/store/tx.head" "$DATADIR/store/scripthash"* -# leftover Class A `*.idx` dirs (schema 22 uses create.loc / inwit.loc): -# rm -rf "$DATADIR/store/txout.idx" "$DATADIR/store/spent.idx" "$DATADIR/store/inwit.idx" +# leftover Class A `*.idx` dirs (schema 22 uses create.loc / seqsigwit.loc): +# rm -rf "$DATADIR/store/txout.idx" "$DATADIR/store/spent.idx" "$DATADIR/store/seqsigwit.idx" ``` -Keep Class A (`txout` / `inwit` / `spent` + `create.loc` / `inwit.loc`, `txid.body`, headers) and +Keep Class A (`txout` / `seqsigwit` / `spent` + `create.loc` / `seqsigwit.loc`, `txid.body`, headers) and Class C. Restart the same binary: `tx.head` rebuilds from Class A; with `--sh-index`, SH rematerializes. Do **not** `rm -rf store/`. @@ -778,7 +778,7 @@ DATADIR=/path/to/datadir rm -rf "$DATADIR/store/tx.head" "$DATADIR/store/scripthash"* ``` -Keep Class A (`txout` / `inwit` / `spent` + idx, `txid.body`, headers) and +Keep Class A (`txout` / `seqsigwit` / `spent` + idx, `txid.body`, headers) and Class C. Restart the same binary: `tx.head` rebuilds from Class A; with `--sh-index`, SH rematerializes from runs / Class A. Do **not** `rm -rf store/`. @@ -834,7 +834,7 @@ Do **not** wipe `store/` for mempool slot/full/schema errors. - **BIP324 v2 only** — plaintext v1 peers disconnect (`peer does not speak BIP324 v2`). - **IBD `getdata` serve** reconstructs witness blocks from contiguous Class A - spans (`txout.body` + `inwit.body`), off the session reactor. Serve volume + spans (`txout.body` + `seqsigwit.body`), off the session reactor. Serve volume is on DEBUG `tip: perf` (`serve n= bytes= tx= avg_us= max_us=`), not a per-block line. Host throughput probe: `python3 scripts/ibd-serve-bench.py 127.0.0.1:8333` (needs `cryptography` @@ -958,7 +958,7 @@ On disk (schema 17 dirs; leftover single files are unlinked on startup): **Not stored:** txids, Taproot outs, values, parent scripts. Notify `output_pubkeys` are joined from this block’s **`txout`** body (~12 ms -sequential on a 4k-tx 9p block; witness stays in `inwit`). Indexed serve does +sequential on a 4k-tx 9p block; witness stays in `seqsigwit`). Indexed serve does **not** parent-peek (~40–80 blk/s vs ~1.5–3 naive on that VM). Serve-time **`--sp-tweaks-dust SATS`** (conf `sp_tweaks_dust=`) omits P2TR outs @@ -969,7 +969,7 @@ figure Cake’s server used. The index is unchanged — only the Electrum JSON. Tip follow writes 65 B-class records from already-pinned parents when the cursor is caught up. Reorg truncates with tip. Post-IBD backfill is a -**one-core** completion machine: `txout` wave, then `inwit`/parent `txout` +**one-core** completion machine: `txout` wave, then `seqsigwit`/parent `txout` only for P2TR creates, secp on **idle** `rbtc-scripts-*` workers (block scripts and mempool accept still win), then **batched** height-blob + idx writes (one body pwrite + one idx pwrite per consecutive group — not @@ -1024,7 +1024,7 @@ proxy, or a public bind if the proxy sits elsewhere and you accept that risk). | Unconfirmed history/balance/mempool | from cluster mempool | | `transaction.get` | chain then mempool fallback | | `relayfee` / `estimatefee` / histogram | from Libre min + live mempool | -| Silent Payments tweaks | `blockchain.tweaks.subscribe` — with `--sp-tweaks` index: multi-height load (default ≤128 heights / ≤16384 eligible txs per wave) then per-height notifies, **one TCP flush per wave**. Indexed JSON-RPC result shares the first wave's Class A `txout` span; remaining heights of that wave are notifies; further waves overlap the next load with the previous write. Class A join is **one sequential `txout` span** from first..=last eligible fk in the wave (not one body pread per eligible tx; `inwit` stays out). Pre-taproot: **one** notify with ≤1024 empty height keys (no store; Cake last key = progress). Cake `historicalMode=false` (param `[2]`): omit confirmed-spent P2TR outs. `--sp-tweaks-dust` (default 1000; 546 = Cake electrs): omit P2TR outs with `value <=` the floor. Without index / hole: naive per height (Class A + parent outs). **Not** request/response: JSON-RPC result is the **first** height (1-height probe `[0,1,false]` → `{"0": {}}`); further heights are notifications, then `{"message":"done"}` at a **wave boundary after 60s wall** (or when `count`/tip finishes first). Cake resubscribes; kiss-bdk one-shot stops until it loops. `server.features.genesis_hash` is the chain check. `server.version[0]` contains `electrs` (Cake probe). On 9p-class IO expect slower than local disk. | +| Silent Payments tweaks | `blockchain.tweaks.subscribe` — with `--sp-tweaks` index: multi-height load (default ≤128 heights / ≤16384 eligible txs per wave) then per-height notifies, **one TCP flush per wave**. Indexed JSON-RPC result shares the first wave's Class A `txout` span; remaining heights of that wave are notifies; further waves overlap the next load with the previous write. Class A join is **one sequential `txout` span** from first..=last eligible fk in the wave (not one body pread per eligible tx; `seqsigwit` stays out). Pre-taproot: **one** notify with ≤1024 empty height keys (no store; Cake last key = progress). Cake `historicalMode=false` (param `[2]`): omit confirmed-spent P2TR outs. `--sp-tweaks-dust` (default 1000; 546 = Cake electrs): omit P2TR outs with `value <=` the floor. Without index / hole: naive per height (Class A + parent outs). **Not** request/response: JSON-RPC result is the **first** height (1-height probe `[0,1,false]` → `{"0": {}}`); further heights are notifications, then `{"message":"done"}` at a **wave boundary after 60s wall** (or when `count`/tip finishes first). Cake resubscribes; kiss-bdk one-shot stops until it loops. `server.features.genesis_hash` is the chain check. `server.version[0]` contains `electrs` (Cake probe). On 9p-class IO expect slower than local disk. | ### API request log diff --git a/README.md b/README.md index fb1efdff6..2d0eb070a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Most full nodes center a **UTXO set + block files** (Bitcoin Core). Most Electru backends are **external indexers** of another node. rbitcoin does neither: **no UTXO set** (relational archive), **Electrum + txindex in-process**. -- **~200 GiB** hot pin/annotate set (schema 17); **~700 GiB** with cold `inwit` — +- **~200 GiB** hot pin/annotate set (schema 17); **~700 GiB** with cold `seqsigwit` — census in [`SCHEMA.md`](./SCHEMA.md), `--sh-index` costs in [`OPERATOR.md`](./OPERATOR.md) - **Under ~30 h** IBD on a laptop-class host with **`--milestone 0`** - **Modest RAM** during sync — no multi‑GiB `dbcache` pause diff --git a/SCHEMA.md b/SCHEMA.md index 9b338c875..a40d7f1af 100644 --- a/SCHEMA.md +++ b/SCHEMA.md @@ -1,7 +1,7 @@ # On-disk schema (current) **Version:** `SCHEMA_VERSION = 25` (`rbitcoin_primitives`). -**Status:** 25 is `txstat.body` 8 B/create (canonical ULEB `n_in`/`fee_sat`/`base`/`wit_extra`; +**Status:** 25 is `txstat.body` 8 B/create (canonical ULEB `fee_sat`/`base`/`wit_extra`; `n_in` is `input.loc`; 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 @@ -11,8 +11,8 @@ binary refuses 25 `meta`. 24 is `header.body` 96 B (trailing `size:u32` + `weigh 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` u32) so a consensus-valid ~1 MiB txout (and `n_out > 65535`) stores. Occupied 22 -Class A rewrites 12 B ovf rows and `meta`. 22 is `create.loc` + `inwit.loc` (no -Class A `{txout,spent,inwit}.idx`), LAYOUT17 without `output_count`, and spent +Class A rewrites 12 B ovf rows and `meta`. 22 is `create.loc` + `seqsigwit.loc` (no +Class A `{txout,spent,seqsigwit}.idx`), LAYOUT17 without `output_count`, and spent slots flags + u40 spend fk + u16 vin (still 8 bytes). `txout` amount is flags bits 4–7 = decimal exponent (0–9) + ULEB mantissa (`sats = mantissa × 10^e`). Encoding is canonical compact: strip trailing tens up to `e=9` (`e<9` and mantissa @@ -44,7 +44,7 @@ format code: **13/14→17 open:** Empty Class A (no creates) + empty/missing SH may silently rewrite `meta` to 17. A packed `tx.body` **with creates**, or a durable page-era (or schema-13 slab) SH index, is refused (wipe + IBD). Schema 15 Class A is -`txout` + `inwit` + `spent` (not a single packed `tx.body`). +`txout` + `seqsigwit` + `spent` (not a single packed `tx.body`). **15→17 open:** leftover `tx_height.body` is unlinked (RAM fence). Class A with creates in the 16-byte-meta / 9-byte-spent layout is **refused** (wipe datadir and redo IBD). Empty Class A may rewrite `meta`. @@ -104,9 +104,9 @@ schema 18. | Object | Frozen choice | |--------|----------------| -| Class A | Split `txout` / `inwit` / `spent`; thin LAYOUT17 meta; kinds **0–9**; 8 B spent slots; `spent.ovf` | +| Class A | Split `txout` / `seqsigwit` / `spent`; thin LAYOUT17 meta; kinds **0–9**; 8 B spent slots; `spent.ovf` | | Identity | Dense `txid.body` (32 B/fk); segmented `tx.head` (25-bit + fuse8 v2) | -| Loc | `create.loc` (2 B/create + `create.off` + ovf) and cold `inwit.loc`; leftover Class A `{txout,spent,inwit}.idx/` unlinked on empty 21–24. Flat `*.idx.meta` **refused**. | +| Loc | `create.loc` (2 B/create + `create.off` + ovf) and cold `seqsigwit.loc`; leftover Class A `{txout,spent,seqsigwit}.idx/` unlinked on empty 21–24. Flat `*.idx.meta` **refused**. | | Class B | SH runs `key_len=40` unique `(sh, create_fk)`; megakey pages ULEB deltas (`ver=1`); body **dir** (sharded). Leftover file body **refused**. Slab **class** is the byte allocation (32…2048); `used` is the fk count and may exceed the old geometric `slab_cap(class)` when the ULEB stream fits. Decode `used` fks from the payload. | | Class C | `confirmed[]` + `header_txs_*`; no `tx_height.body`; `strong_tx` bitset | | Tweaks | Segmented `sp_tweaks.idx/` + `sp_tweaks.body/` (`off:u32`, body `0`/`33`) | @@ -120,7 +120,7 @@ catalogs, or 16-layout Class A with creates is **refused**. | Policy | Choice | |--------|--------| -| Loc | One loc pair per create; `inwit` is the fat stem (cold) and does not force `txout` splits. `tx.head` rolls at OA 80% slots only. | +| Loc | One loc pair per create; `seqsigwit` is the fat stem (cold) and does not force `txout` splits. `tx.head` rolls at OA 80% slots only. | | `strong_tx` | Always L2. `RBITCOIN_CLASS_C_INRAM_MAX_MB` (default 256) still caps **`confirmed`** and **`header_txs_*`** only. | | `RWF_DONTCACHE` | **Not used.** Annotate pwrites hit `spent.body` only; evicting those pages does not protect `txout`, and the next block wants the same spent pages. | @@ -137,8 +137,8 @@ at 17: Use RAW when the type is rare. Use a kind-nibble bump when the type is common enough to pay a width table. That bump is not a silent in-place rewrite of -existing files. Inwit `create_fk` Δfk (parked) is the same class: a -`SCHEMA_VERSION` bump or an inwit-only rewrite, not a silent mutate of +existing files. SeqSigWit `create_fk` Δfk (parked) is the same class: a +`SCHEMA_VERSION` bump or an seqsigwit-only rewrite, not a silent mutate of existing rows. ### Field widths (10 years) @@ -152,7 +152,7 @@ Assume ~400k–700k creates/day. Ten years ≈ +1.5e9…2.6e9 creates on top of | Spent spend fk | u40 | 2^40 ids ≈ 1.1e12 creates; census ~1.42e9 at h=962k. `fk ≥ 2^40` is Corrupt (no wrap) | | Spent vin | u16 | Consensus max inputs at 400 kWU is ~2.4k. `vin ≥ 2^16` is Corrupt | | Height / `confirmed[]` index | u32 | ~1e6 heights now; 10y adds ~0.5e6; year 2106 is **timestamp**, not height | -| Loc strides | u8 (create) / u16 (inwit) | Overflow sidecar: `create.loc.ovf` **u32** strides + **u32** `n_out` (schema 23); `inwit.loc.ovf` u32 strides. Sentinel when txout ≥ 2048 B aligned or `n_out ≥ 256`; inwit ≥ 512 KiB | +| Loc strides | u8 (create) / u16 (seqsigwit) | Overflow sidecar: `create.loc.ovf` **u32** strides + **u32** `n_out` (schema 23); `seqsigwit.loc.ovf` u32 strides. Sentinel when txout ≥ 2048 B aligned or `n_out ≥ 256`; seqsigwit ≥ 512 KiB | | `tx.head` bits | 25-bit segments | Roll + seal; no mono-file widen | | SH megakey page | 4 KiB delta stream | Page chain; not a single-integer cap | | `sp_tweaks` off | u32 per segment | Already segmented | @@ -170,7 +170,7 @@ live `SCHEMA_VERSION`, not from 17. | Change | Bump? | Notes | |--------|-------|-------| | New implicit-width script kind | Optional | RAW = no bump; nibble = next `SCHEMA_VERSION` (this binary refuses it) | -| Inwit Δfk | Yes or inwit-only rewrite | Parked; cold stem | +| SeqSigWit Δfk | Yes or seqsigwit-only rewrite | Parked; cold stem | | Idx not stride-8 / not u32 | Yes | Would retire the 8-align pad | | Packed Class A again / merge stems | Yes | Wipe | | SH `key_len` ≠ 40 or raw-u64 pages | Yes | 17 already refuses leftovers | @@ -178,9 +178,9 @@ live `SCHEMA_VERSION`, not from 17. | Fuse8 envelope v3 | No | Soft-migrate like v1→v2 (log + rewrite; no wipe) | | Independent rolls / L2 strong / no DONTCACHE | No | Writer/RAM only | -Parked size work that is **not** a live-layout tweak: inwit Δfk; drop 8-align -pad on empty inwit / zero-out spent (needs a different idx encoding); -`txid.body` compression. Do not chase `inwit` size as an IBD **hot-set** +Parked size work that is **not** a live-layout tweak: seqsigwit Δfk; drop 8-align +pad on empty seqsigwit / zero-out spent (needs a different idx encoding); +`txid.body` compression. Do not chase `seqsigwit` size as an IBD **hot-set** win — put it on a cold volume (`--datadir-cold`). Census: [Mainnet census](#mainnet-census-this-trees-reference-datadir-2026-08-13). @@ -195,7 +195,7 @@ itself changed. | Concern | Choice | Why | |---------|--------|-----| -| Class A body | **Split** `txout` (thin meta + template outs) + `inwit` + `spent` (8 B×n_out) | Pin/SH read outs only; annotate isolates scripts | +| Class A body | **Split** `txout` (thin meta + template outs) + `seqsigwit` + `spent` (8 B×n_out) | Pin/SH read outs only; annotate isolates scripts | | Class A identity | Dense **`txid.body`** sidefile (32 B header + 32 B/txid by create_fk) | Fixed `fk → offset`; head-resolve multi-cand via sidefile, not body peeks | | Non-coinbase prevout | On-disk **`create_fk:u64` + CompactSize vout** | Smaller than `prev_txid[32]`; archive stamps fk once; wire fills soft `prev_txid` from sidefile/create | | Txid → create | Segmented keyless **`tx.head.*`** (25-bit OA open + MPHF/fuse sealed) | Open page from `mix_txid`; seal-time value-assigned MPHF + fuse8; **txid.body** verifies identity | @@ -213,10 +213,10 @@ itself changed. meta # store magic + schema version header.body / header.head # Class A headers + hash index (overflow: header.head.gN) txout.body / create.loc / create.off / create.loc.ovf # Class A outs (hot loc) - inwit.body / inwit.loc / inwit.off / inwit.loc.ovf # Class A inputs+witness (cold loc) - inwit.prune # optional: u32 LE pruneheight sidecar (`--prune-inwit`; missing = off) - inwit.window/ # optional prune window: one {height}.bin per kept height - inwit.reloc # optional: inwit lives under --datadir-cold/store + seqsigwit.body / seqsigwit.loc / seqsigwit.off / seqsigwit.loc.ovf # Class A inputs+witness (cold loc) + seqsigwit.prune # optional: u32 LE pruneheight sidecar (`--prune-seqsigwit`; missing = off) + seqsigwit.window/ # optional prune window: one {height}.bin per kept height + seqsigwit.reloc # optional: seqsigwit lives under --datadir-cold/store 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+) @@ -244,12 +244,12 @@ itself changed. / # only when --datadir-cold is set store/ - inwit.body / inwit.loc / inwit.off / inwit.loc.ovf + seqsigwit.body / seqsigwit.loc / seqsigwit.off / seqsigwit.loc.ovf ``` `--datadir` holds both stems by default. `--datadir-cold PATH` places only -`inwit.body` + `inwit.loc` (and `inwit.off` / `inwit.loc.ovf`) under `PATH/store/` -(and writes `inwit.reloc` in the hot store). Pin / SH / spend-annotate stay on +`seqsigwit.body` + `seqsigwit.loc` (and `seqsigwit.off` / `seqsigwit.loc.ovf`) under `PATH/store/` +(and writes `seqsigwit.reloc` in the hot store). Pin / SH / spend-annotate stay on the hot volume. **Height → txs:** `confirmed[h]` → `header_fk` → contiguous Class A range @@ -286,12 +286,14 @@ the hot volume. | 13 | spender (`spent.ovf` multi-list) | | 14 | txid_body (`txid.body`) | | 15 | sp_tweaks (`sp_tweaks.body`; idx uses array_link) | -| 16 | inwit (`inwit.body`) | +| 16 | seqsigwit (`seqsigwit.body`) | | 17 | spent (`spent.body`) | -| 18 | delta loc (`create.loc` / `inwit.loc` and `.ovf`) | +| 18 | delta loc (`create.loc` / `seqsigwit.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) | +| 22 | inputs (`input.body`, 8 B/input) | +| 23 | inputs loc (`input.loc`, 2 B/create `n_in`) | --- @@ -304,13 +306,13 @@ the hot volume. ## Growable var records (`*.body` + loc) -Used for Class A `txout` / `inwit` / `spent` (and historically packed `tx.body`). +Used for Class A `txout` / `seqsigwit` / `spent` (and historically packed `tx.body`). - **body:** append-oriented **unframed** payloads (no per-record length prefix). -- **loc:** schema 22 `create.loc` (txout + `n_out`) and `inwit.loc` (cold); +- **loc:** schema 22 `create.loc` (txout + `n_out`) and `seqsigwit.loc` (cold); schema 23 ovf is 16 B u32 strides/`n_out`. Header hash lookup is a separate `HashHead`, not this loc. -- Record length = loc pair (txout/spent) or `inwit.loc` (inwit). Last record +- Record length = loc pair (txout/spent) or `seqsigwit.loc` (seqsigwit). Last record uses 8-aligned published body end. - FK = 1-based create id. @@ -356,7 +358,7 @@ 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` / `txstat.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` / `seqsigwit` / `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) @@ -367,21 +369,32 @@ txstat.ovf append-only — remaining ULEB bytes when the stream ex 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` + +Cell payload is three canonical ULEBs: `fee_sat`, `base` (non-witness +size), `wit_extra` (`total_size − base`). `n_in` is `input.loc` (u16). Readers +derive `size = base + wit_extra` and `weight = 4×base + wit_extra`. All-zero +cell = unstamped. A truncated ULEB or fewer than three 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 three fields is Corrupt. Class A append of +placeholders is all-zero and never overflows; only a confirm stamp can emit +tails. A cell written as four ULEBs starting with `n_in` (an unreleased +experiment) is not detected and is not rewritten; resync that datadir. +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. +### Spender → parent edges (`input.body`, schema 25) + +```text +input.off ArrayLink: per 1024 creates, u64 file offset of the next window's first input +input.loc 2 B/create: n_in as u16 LE. 0 = unstamped +input.body 8 B × input, vin order +``` + +`n_in > u16::MAX` is Corrupt (no `input.loc.ovf`). A coinbase is `n_in == 1` and one null edge. Body record: parent `create_fk` as u40 LE (`0` = coinbase), then parent vout as u24 LE. `fk ≥ 2^40` or `vout ≥ 2^24` is Corrupt. A null parent with nonzero vout is Corrupt. The spending `vin` is the record index. Prefix-sum of `n_in` from the window checkpoint is the body offset (`8 × edges before this window`). Confirm appends these rows with Class A. Open with creates, no `input.loc`, and a `seqsigwit` count that matches `create.loc` walks `seqsigwit.body` with `decode_prevout_at` (prevout only) and writes the edges. A null parent in that payload is the coinbase edge (`vout` 0). `input` and `txstat` are not pruned. + ### Split bodies (schema 15) Each create_fk has three 8-aligned var records (loc maps; independent @@ -389,13 +402,13 @@ stems; spent length is `8 × n_out` with `n_out ≥ 1`): ```text txout.body S: thin LAYOUT17 meta | outputs (kind nibble + template payload) -inwit.body Sw: per-input flags|create_fk+vout|seq?|script_sig?|witness? +seqsigwit.body Sw: per-input flags|seq?|script_sig?|witness? spent.body Ss: 8 B × n_out (flags + u40 fk + u16 vin). Multi overflow → spent.ovf ``` -Empty inwit: **8-byte zero pad** so loc strides stay strictly monotone. +New records set flag bit 4 (`PREV_ON_INPUTS`) and do not store parent `create_fk` or vout; that edge is `input.body`. A record without bit 4 is the legacy inline prevout (`NULL_PREV`, or `create_fk:u64` plus CompactSize vout). Open backfill of a missing `input.loc` still reads those legacy records. Empty seqsigwit: **8-byte zero pad** so loc strides stay strictly monotone. Pin / SH / Electrum tweaks read **`txout` only**. Annotate RMW is on **`spent`** (`abs = Ss + 8×vout`). -Reconstruct zips `txout` + `inwit`. First-wave Outs reads stay on the starting +Reconstruct zips `txout` + `seqsigwit`. First-wave Outs reads stay on the starting OS page unless `4+(max_need+1)×38` (LAYOUT17 meta + kind + 5 B uleb amount + P2TR; empty need: the loc span) is likely to spill; then the first wave is the full loc span. Extend still covers a missed need. @@ -416,8 +429,8 @@ 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). 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). +uleb `input_count` (`n_in` is on `input.loc`; decode reports 0 until a +reader fills from that locator). 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 @@ -425,22 +438,22 @@ 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`) +### Create / seqsigwit locators (`create.loc` / `seqsigwit.loc`) ```text create.off # ArrayLink: per 1024 creates, u64 txout_abs + u64 spent_abs create.loc # 2 B/create: (txout_strides:u8, n_out:u8) create.loc.ovf # sorted 16 B: fk:u64, strides:u32, n_out:u32 -inwit.loc / inwit.off / inwit.loc.ovf # u16 strides; cold with inwit.body +seqsigwit.loc / seqsigwit.off / seqsigwit.loc.ovf # u16 strides; cold with seqsigwit.body ``` `n_out` is the true output count, always ≥ 1. Spent length is `8 × n_out`. Loc byte **0** = overflow (`n_out ≥ 256` or txout aligned length ≥ 2048). `create.loc.ovf` holds the true u32 strides and `n_out` (a ~1 MiB OP_RETURN is ~125k strides; min-size outputs in a 1 MB stripped tx can exceed 65535). -`inwit.loc` **0** = overflow (`strides ≥ 65536`, i.e. ≥ 512 KiB). Missing ovf +`seqsigwit.loc` **0** = overflow (`strides ≥ 65536`, i.e. ≥ 512 KiB). Missing ovf when a sentinel is set is `Corrupt("invariant: create.loc overflow missing")` -(or inwit). Checkpoints (~22 MiB) are RAM; do not L2 `create.loc`. Lookup `range_batch` +(or seqsigwit). Checkpoints (~22 MiB) are RAM; do not L2 `create.loc`. Lookup `range_batch` reads and prefix-sums only through the highest fk in each 1024-create window (not the unused tail). Those window preads are **one** bulk batch (held head-resolve session, else `pread_batch`). Every window uses a SIMD prefix @@ -453,7 +466,7 @@ packs until write of the last height whose TipOnly had started at note (`lookup_started_hi`; just-written abs; fill of that write runs first). Write does not pread `create.loc`. Occupied 21 Class A is refused. Occupied 22 rewrites `create.loc.ovf` 12 B → 16 B. Leftover -`{txout,spent,inwit}.idx` and `spent.off` are unlinked on empty 21–24 open. +`{txout,spent,seqsigwit}.idx` and `spent.off` are unlinked on empty 21–24 open. `spent_abs(off, vout) = off + 8×vout`. @@ -811,15 +824,15 @@ Tip **962,298**, **1,416,970,187** creates, mean packed **502.2 B/tx**, | File | Packed 13/14 | Schema 15 | |------|--------------|-----------| | `tx.body` / `txout.body` | **662.73 GiB** | **~129 GiB** (schema 15; 17 thin meta + templates cut ~18–26 GiB) | -| `inwit.body` | — | **~486 GiB** (ins + witness; cold) | +| `seqsigwit.body` | — | **~486 GiB** (ins + witness; cold) | | `spent.body` | (9 B inside packed outs, ~32 GiB) | **~32 GiB** schema 15; **~21 GiB** after 8 B slots | -| `{stem}.idx` / loc | 5.28 GiB (`tx.idx`) | 5.28 GiB × **3** idx (schema 15–21); schema 22 is `create.loc` + `inwit.loc` | +| `{stem}.idx` / loc | 5.28 GiB (`tx.idx`) | 5.28 GiB × **3** idx (schema 15–21); schema 22 is `create.loc` + `seqsigwit.loc` | | `txid.body` / `tx.head` | 42.23 / 8.23 GiB | unchanged | Hot pin+annotate working set: **txout + spent + create.loc + txid + tx.head** (~129+21+3+42+8 ≈ **203 GiB**) vs packed **tx.body + idx + txid + head** (~663+5+42+8 ≈ **718 GiB**). Reconstruct / `getrawtransaction` also needs -`inwit` (~486 GiB), which pin/SH/tweaks do **not** open. +`seqsigwit` (~486 GiB), which pin/SH/tweaks do **not** open. --- diff --git a/SCHEMA_HISTORY.md b/SCHEMA_HISTORY.md index cc12bfa77..9d7d595b1 100644 --- a/SCHEMA_HISTORY.md +++ b/SCHEMA_HISTORY.md @@ -13,17 +13,17 @@ Versions below are listed **newest → oldest** after the summary table. | Version | Headline change | Still in current tree as… | |--------:|-----------------|---------------------------| -| **25** | `txstat.body` 8 B/create ULEB + per-header overflow. Occupied 24 zero-extends; no `txout` rewrite. | **Current** | +| **25** | `txstat.body` three ULEBs (`fee_sat`/`base`/`wit_extra`); `n_in` and parent edges on `input.*`. `seqsigwit` is the old `inwit` stem. Occupied 24 zero-extends `txstat`; no `txout` rewrite. | **Current** | | **24** | `header.body` 96 B (`size`/`weight` u32). Occupied 23 rewrites 88 B rows. Extent last-page reserved = create count. | Prior | | **23** | `create.loc.ovf` 16 B (u32 strides / `n_out`). Occupied 22 rewrites 12 B ovf. Occupied 15–21 Class A refused. | Prior | -| **22** | `create.loc` + `inwit.loc`; no Class A `*.idx`. LAYOUT17 drops `output_count`. Spent slot flags + u40 fk + u16 vin. `txout` amount is exp nibble + ULEB mantissa. Occupied 15–21 Class A refused. Empty 15–21 rewrite `meta` and unlink leftover `spent.off` + leftover `*.idx`. | Prior | +| **22** | `create.loc` + `seqsigwit.loc`; no Class A `*.idx`. LAYOUT17 drops `output_count`. Spent slot flags + u40 fk + u16 vin. `txout` amount is exp nibble + ULEB mantissa. Occupied 15–21 Class A refused. Empty 15–21 rewrite `meta` and unlink leftover `spent.off` + leftover `*.idx`. | Prior | | **21** | Drop `spent.idx`. Spent ranges are `8 × max(n_out,1)` from txout meta; sparse `spent.off`. Unlink leftover idx; rewrite `meta` 20→21. Table headers 13–20 remain openable. | Prior | | **20** | Sealed `tx.head` value-assigned packed BDZ (`BDZ2`, no `.rel`); sealed SH compact `BDZ3` (2-bit `g` + rank). Refuse occupied 18/19 `tx.head` / `scripthash*`. Leftover fuse8 v1, flat `*.idx.meta`, Shared SH body, pack8 Paged (mode 10) refuse. | Prior | | **19** | Megakey SH extent: pack8 mode 11 + `ver=2` last page (`extent_base`, `extent_n`). Soft-open 18 with occupied indexes. | Prior | | **18** | MPHF SH main (8 B values) + sealed `tx.head` MPHF; no IBD SH runs. Refuse 17 with `tx.head`/`scripthash*` data (wipe indexes, keep Class A). | Prior | | **17** | SH runs `key_len=40`; Class A thin meta + kinds 0–9 + 8 B spent; megakey pages delta-stream; `spent.ovf`; no `archive_epoch`; segmented tip-only `sp_tweaks.*` dirs. | Prior | | **16** | Drop `tx_height.body`; RAM fence from `confirmed[]` + `header_txs_*`. Soft-open 15 | Prior | -| **15** | Class A `txout`/`inwit`/`spent` split; SH slabs + sorted heads; refuse packed Class A with txs and page-era SH | Prior | +| **15** | Class A `txout`/`seqsigwit`/`spent` split; SH slabs + sorted heads; refuse packed Class A with txs and page-era SH | Prior | | **14** | SH head Empty/Inline/**Paged** (4 KiB page chains); seal @0.8 + overflow OA; refuse slab values | Prior | | **13** | Dense `txid.body` sidefile; packed body **without** leading txid; RWF_DONTCACHE policy | Prior | | **12** | Datadir `store.secret`; script/witness XOR at rest; keyed `tx.head` mix; head overflow; durable `block_queue/` | Prior | @@ -39,16 +39,24 @@ Versions below are listed **newest → oldest** after the summary table. --- -## v25 (`txstat.body` 8 B/create) - -Dense `create_fk`-addressed econ sidefile, same header pad as `txid.body` -(32 B prefix, 8 B ULEB cells). Stream is four canonical ULEBs: `n_in`, -`fee_sat`, `base`, `wit_extra`. Overflow remaining bytes live in a per-header -blob (`txstat.ovf` + `txstat.blk`). Occupied 24 rewrites `meta` and extends -zeros to `create.loc` count. `txout.body` is not rewritten. Unreleased leftover -`txfixed.body` is unlinked. A 24 binary refuses 25 `meta`. Named disk: ~11.3 GiB -at the 2026-08-13 census. Pin/SH/tweaks do not open this file. New writes set -LAYOUT17 bit 4 and omit the `input_count` uleb; leftover 24 rows still decode. +## v25 (`txstat.body` 8 B/create, `input.*`, `seqsigwit`) + +`txstat.body` is a dense `create_fk`-addressed econ sidefile, same header pad +as `txid.body` (32 B prefix, 8 B cells). The cell is three canonical ULEBs: +`fee_sat`, `base`, `wit_extra`. `n_in` is `input.loc` (u16 LE, `0` = +unstamped). `input.body` is 8 B per input: parent `create_fk` u40 LE +(`0` = coinbase) and parent vout u24 LE. Overflow remaining bytes live in a +per-header blob (`txstat.ovf` + `txstat.blk`). `seqsigwit.*` is the renamed +`inwit` stem (sequence, scriptSig, witness); open renames the files in place. +`txstat.*` and `input.*` sit next to `seqsigwit` (cold when `--datadir-cold` +is set). Open with no `input.loc` and a matching `seqsigwit` count backfills +parent edges from `seqsigwit` prevouts. A four-ULEB cell that started with +`n_in` is not detected; resync that experimental datadir. Occupied 24 +rewrites `meta` and extends zeros to `create.loc` count. `txout.body` is not +rewritten. Unreleased leftover `txfixed.body` is unlinked. A 24 binary +refuses 25 `meta`. Named disk: ~11.3 GiB for the 8 B cells at the 2026-08-13 +census. Pin/SH/tweaks do not open these files. New writes set LAYOUT17 bit 4 +and omit the `input_count` uleb; leftover 24 rows still decode. ## v24 (header size/weight + SH extent create count) @@ -69,14 +77,14 @@ A consensus-valid ~1 MiB OP_RETURN (mainnet 896696) is ~125k strides; a 1 MB stripped tx of min-size outputs can exceed 65535 outs. Slot sentinels stay 2 B (`0` at ≥256). Occupied 22 rewrites 12 B ovf via tmp+rename and `meta` to 23. Occupied 15–21 Class A still refuses. Spent vin stays u16 (8 B slot; -stripped input ≥ ~41 B). `inwit.loc.ovf` was already u32 strides. A 22 binary +stripped input ≥ ~41 B). `seqsigwit.loc.ovf` was already u32 strides. A 22 binary refuses 23 `meta`. Table file headers 13–22 remain openable. -## v22 (create.loc + inwit.loc + vin pack + amount exp) +## v22 (create.loc + seqsigwit.loc + vin pack + amount exp) `create.loc` (2 B/create: txout strides + `n_out`) plus RAM `create.off` -checkpoints replace `txout.idx` and `spent.idx`. `inwit.loc` (u16 strides) -on the cold volume replaces `inwit.idx`. `VarTable` is body-only. LAYOUT17 +checkpoints replace `txout.idx` and `spent.idx`. `seqsigwit.loc` (u16 strides) +on the cold volume replaces `seqsigwit.idx`. `VarTable` is body-only. LAYOUT17 meta is flags + version/locktime + uleb `input_count` only; decode takes `n_out` from loc (`n_out ≥ 1`). Spent length is `8 × n_out` (no zero-out pad). Spent slot is still 8 bytes: flags + u40 spend fk + u16 vin. `spent.ovf` @@ -94,7 +102,7 @@ Occupied 15–21 LAYOUT17 Class A with creates refuses because the old flags+u56-fk layout has no vin, LAYOUT17 still had `output_count`, locators are not idx, and reserved amount bits were 0. Empty 15–21 rewrite `store/meta` to 22 and unlink leftover `spent.off` plus -leftover `{txout,spent,inwit}.idx`. A 21 binary refuses 22 `meta`. Table file +leftover `{txout,spent,seqsigwit}.idx`. A 21 binary refuses 22 `meta`. Table file headers 13–21 remain openable when Class A is empty. ## v21 (drop spent.idx) @@ -144,9 +152,9 @@ Closed layout. SH run catalogs are unique `(scripthash, create_fk)` at Hot Class A: thin LAYOUT17 `txout` meta, script kinds **0–9** (unknown kind is Corrupt). At schema 17, a new consensus script was **RAW** (no bump) or a kind-nibble bump the then-current binary would refuse — not a 17 -datadir wipe. 8 B spent slots, `spent.ovf` overflow. Inwit flags bits 4–7 -and spent flags other than `MULTI_SPENDER` are Corrupt. Inwit prevout is -still `create_fk:u64` + CompactSize vout (Δfk was parked as an inwit-only +datadir wipe. 8 B spent slots, `spent.ovf` overflow. SeqSigWit flags bits 4–7 +and spent flags other than `MULTI_SPENDER` are Corrupt. SeqSigWit prevout is +still `create_fk:u64` + CompactSize vout (Δfk was parked as an seqsigwit-only or later-version follow-up). Writer/RAM on 17 (not on-disk): idx stems roll independently; `strong_tx` always L2; no `RWF_DONTCACHE`. See [`SCHEMA.md`](SCHEMA.md) (Schema 17 freeze). The live bump rule is that @@ -191,7 +199,7 @@ sealed sorted heads with idx (main has no fuse8; sealed ovf keeps fuse8), one global ingest OA. Empty 13/14 SH upgrades `meta` silently; a materialized page-era index is refused (wipe `store/scripthash*` and rematerialize). Class A is split: -`txout` + `inwit` + `spent` (refuse packed `tx.body` with creates). +`txout` + `seqsigwit` + `spent` (refuse packed `tx.body` with creates). ## v14 diff --git a/crates/rbitcoin-consensus/src/confirm_run/pin.rs b/crates/rbitcoin-consensus/src/confirm_run/pin.rs index 0e3ef3ae8..9ee751eaf 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/pin.rs +++ b/crates/rbitcoin-consensus/src/confirm_run/pin.rs @@ -288,6 +288,9 @@ fn denserels_by_stamped_range( "invariant: lookup stage miss (load parent create identity not stamped)", )))?; } + // Schema 25 meta leaves input_count at 0. That stays "unknown": + // coinbase_fk_at_heights still runs, and a pread of input.loc per + // parent would sit on this range-load loop. let cb = if tx.input_count > 1 { Some(false) } else { diff --git a/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs b/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs index e96df86f7..af0ce76eb 100644 --- a/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs +++ b/crates/rbitcoin-consensus/src/confirm_run/write_idempotent_tests.rs @@ -3203,7 +3203,7 @@ fn confirm_write_stamps_txstat_fee_size() { let g = q.get_txstat(Fk(1)).unwrap().expect("genesis txstat"); let gtx = &genesis.txdata[0]; assert_eq!(g.fee_sat, 0); - assert_eq!(g.n_in, 1); + assert_eq!(q.store().input_n_in(Fk(1)).unwrap(), Some(1)); assert_eq!(g.size() as usize, gtx.total_size()); assert_eq!(g.weight(), gtx.weight().to_wu()); @@ -3212,7 +3212,7 @@ fn confirm_write_stamps_txstat_fee_size() { let row = q.get_txstat(Fk(2)).unwrap().expect("height-1 txstat"); let tx = &b1.txdata[0]; assert_eq!(row.fee_sat, 0); - assert_eq!(row.n_in, 1); + assert_eq!(q.store().input_n_in(Fk(2)).unwrap(), Some(1)); assert_eq!(row.size() as usize, tx.total_size()); assert_eq!(row.weight(), tx.weight().to_wu()); let _ = std::fs::remove_dir_all(&path); diff --git a/crates/rbitcoin-consensus/src/silent_payments.rs b/crates/rbitcoin-consensus/src/silent_payments.rs index dca82b915..cb20f5907 100644 --- a/crates/rbitcoin-consensus/src/silent_payments.rs +++ b/crates/rbitcoin-consensus/src/silent_payments.rs @@ -284,7 +284,7 @@ pub fn tweaks_for_height( let mut jobs: Vec<([u8; 32], bitcoin::Transaction, Vec)> = Vec::new(); for t in &wave.txs { - if !t.need_inwit { + if !t.need_seqsigwit { continue; } let Some(inputs) = t.inputs.as_ref() else { @@ -1398,7 +1398,7 @@ mod tests { /// Fat non-P2TR sibling must not change the P2TR tweak (txout-first filter). #[test] - fn tweaks_for_height_skips_inwit_on_non_p2tr_sibling() { + fn tweaks_for_height_skips_seqsigwit_on_non_p2tr_sibling() { use bitcoin::hashes::hash160; use bitcoin::secp256k1::SecretKey; let (dir, q) = tmp_store(); diff --git a/crates/rbitcoin-esplora/src/server.rs b/crates/rbitcoin-esplora/src/server.rs index c83d6ddc0..c52b5ed51 100644 --- a/crates/rbitcoin-esplora/src/server.rs +++ b/crates/rbitcoin-esplora/src/server.rs @@ -3828,8 +3828,8 @@ mod tests { hashes.push(header.hash); prev = q.connect_block(Height(h), &header, &[ta]).unwrap(); } - q.set_prune_inwit(true).unwrap(); - q.apply_prune_inwit_tip().unwrap(); + q.set_prune_seqsigwit(true).unwrap(); + q.apply_prune_seqsigwit_tip().unwrap(); let q = Arc::new(q); let cfg = EsploraConfig::with_network("127.0.0.1:0".parse().unwrap(), Network::Regtest); let handle = run_esplora(cfg, Arc::clone(&q), None, None) @@ -3843,7 +3843,8 @@ mod tests { let row: serde_json::Value = serde_json::from_str(&body).unwrap(); assert_eq!(row["txid"], tx0); assert_eq!(row["pruned"], true); - assert!(row.get("vin").is_none(), "pruned JSON omits vin: {row}"); + assert_eq!(row["vin"][0]["is_coinbase"], true, "{row}"); + assert!(row["vin"][0].get("witness").is_none(), "{row}"); assert!(row.get("vout").is_some()); let (st, body) = http_get(addr, &format!("/tx/{tx0}/raw")).await; diff --git a/crates/rbitcoin-esplora/src/tx_json.rs b/crates/rbitcoin-esplora/src/tx_json.rs index 4adc17ac1..390949e59 100644 --- a/crates/rbitcoin-esplora/src/tx_json.rs +++ b/crates/rbitcoin-esplora/src/tx_json.rs @@ -172,6 +172,41 @@ pub fn build_tx_json(query: &Query, tx_fk: Fk, network: Network) -> Result Result>, QueryError> { + let Some(edges) = query.store().input_edges(tx_fk)? else { + return Ok(None); + }; + let mut vin = Vec::with_capacity(edges.len()); + for edge in edges { + if edge.parent.is_null() { + vin.push(json!({ + "txid": "0".repeat(64), + "vout": 0xFFFFFFFFu32, + "is_coinbase": true, + })); + continue; + } + let parent_txid = query.store().txs.body_txid(edge.parent)?; + let mut obj = json!({ + "txid": block_hash_hex(&parent_txid), + "vout": edge.vout, + "is_coinbase": false, + }); + if let Ok((_meta, outs)) = query.store().get_tx_meta_and_outputs(edge.parent) { + if let Some(o) = outs.get(edge.vout as usize) { + obj["prevout"] = vout_fields(&o.script, o.value, network); + } + } + vin.push(obj); + } + Ok(Some(vin)) +} + fn build_tx_json_pruned(query: &Query, tx_fk: Fk, network: Network) -> Result { let tx = query.store().get_tx(tx_fk)?; let (_meta, outs) = query.store().get_tx_meta_and_outputs(tx_fk)?; @@ -181,8 +216,6 @@ fn build_tx_json_pruned(query: &Query, tx_fk: Fk, network: Network) -> Result Result String { [--tor-control [HOST:PORT]] [--tor-control-cookie PATH] [--tor-control-password PASS] \\\n\ [--i2p-sam [HOST:PORT]] [--i2p-accept-incoming] \\\n\ [--electrum-listen ADDR] [--esplora-listen ADDR] [--esplora-onion[=0|1]] \\\n\ - [--sh-index] [--prune-inwit] [--prune-inwit-ram-threshold-bytes N] [--sp-tweaks] [--sp-tweaks-dust SATS] [--max-sh-creates N] [--esplora-block-template] \\\n\ + [--sh-index] [--prune-seqsigwit] [--prune-seqsigwit-ram-threshold-bytes N] [--sp-tweaks] [--sp-tweaks-dust SATS] [--max-sh-creates N] [--esplora-block-template] \\\n\ [--rpc] [--rpc-listen [ADDR]] [--rpc-token-file PATH] [--rpc-work-queue N] \\\n\ [--milestone HEIGHT] \\\n\ [--max-outbound N] [--max-inbound N] \\\n\ @@ -335,16 +335,16 @@ Peers: --max-outbound (default 16 live download), --max-inbound (default 125).\n --net-permission-relay (default on) / --net-permission-force-relay (default off) are implicit bits on a bare CIDR grant.\n\ Scripthash: --sh-index (default off) builds Class B for Electrum/Esplora address history.\n\ Electrum/Esplora start without it; scripthash/address methods fail closed.\n\ - --prune-inwit refuse inwit reconstruct below tip-288 heights; advertise NETWORK_LIMITED.\n\ - Kept heights are store/inwit.window/{{height}}.bin plus a RAM cache. Unpruned nodes read inwit.body.\n\ - --prune-inwit-ram-threshold-bytes N RAM cap for that cache (default 268435456; 0 keeps nothing in RAM).\n\ + --prune-seqsigwit refuse seqsigwit reconstruct below tip-288 heights; advertise NETWORK_LIMITED.\n\ + Kept heights are store/seqsigwit.window/{{height}}.bin plus a RAM cache. Unpruned nodes read seqsigwit.body.\n\ + --prune-seqsigwit-ram-threshold-bytes N RAM cap for that cache (default 268435456; 0 keeps nothing in RAM).\n\ --max-sh-creates N refuses Electrum/Esplora joins with more than N creates (0 = unlimited).\n\ --esplora-block-template enables GET /block-template (GBT template JSON; default off).\n\ --esplora-onion (default on) ADD_ONION for --esplora-listen when --tor-control is set.\n\ Silent payments: --sp-tweaks (default off) writes/serves the thin BIP-352 tweak index.\n\ --sp-tweaks-dust SATS omits served P2TR outs with value <= SATS (default 1000; 0 = all; 546 = Cake electrs).\n\ RPC: --rpc unix socket {{datadir}}/rpc.sock; --rpc-listen [ADDR] adds TCP (default 127.0.0.1 and Core-matching port). Token {{datadir}}/rpc.token (Bearer). No --rpcuser.\n\ -Cold files: --datadir-cold PATH puts Class A inwit.body/idx under PATH/store (HDD).\n\ +Cold files: --datadir-cold PATH puts Class A seqsigwit.body/idx under PATH/store (HDD).\n\ Default (flag omitted): hot and cold files both live under --datadir.\n\ Conf: --conf FILE (snake_case key=value; CLI kebab overrides conf). See OPERATOR.md and docs/rpc.md.\n\ Advanced debug/IO knobs remain RBITCOIN_* env (not required for normal sync; preserved if CLI omits).\n\ @@ -380,7 +380,7 @@ fn is_bool_key(key: &str) -> bool { matches!( key, "sh_index" - | "prune_inwit" + | "prune_seqsigwit" | "sp_tweaks" | "esplora_block_template" | "esplora_onion" @@ -582,7 +582,7 @@ mod tests { "--net-permission-force-relay", "--signet-block-time", "--sh-index", - "--prune-inwit", + "--prune-seqsigwit", "--sp-tweaks", "--sp-tweaks-dust", "--esplora-block-template", @@ -996,29 +996,29 @@ mod tests { } #[test] - fn prune_inwit_is_kebab() { + fn prune_seqsigwit_is_kebab() { let on = ready_config([ "rbitcoin-node", - "--prune-inwit", - "--prune-inwit-ram-threshold-bytes=4096", + "--prune-seqsigwit", + "--prune-seqsigwit-ram-threshold-bytes=4096", ]); - assert!(on.prune_inwit); - assert_eq!(on.prune_inwit_ram_threshold_bytes, 4096); + assert!(on.prune_seqsigwit); + assert_eq!(on.prune_seqsigwit_ram_threshold_bytes, 4096); let mut conf = NodeConfig::default(); - conf.apply_kv("prune_inwit", "1").unwrap(); - conf.apply_kv("prune_inwit_ram_threshold_bytes", "8192") + conf.apply_kv("prune_seqsigwit", "1").unwrap(); + conf.apply_kv("prune_seqsigwit_ram_threshold_bytes", "8192") .unwrap(); - assert!(conf.prune_inwit); - assert_eq!(conf.prune_inwit_ram_threshold_bytes, 8192); - conf.apply_kv("prune_inwit_ram_threshold_bytes", "0") + assert!(conf.prune_seqsigwit); + assert_eq!(conf.prune_seqsigwit_ram_threshold_bytes, 8192); + conf.apply_kv("prune_seqsigwit_ram_threshold_bytes", "0") .unwrap(); - assert_eq!(conf.prune_inwit_ram_threshold_bytes, 0); + assert_eq!(conf.prune_seqsigwit_ram_threshold_bytes, 0); let h = operator_usage(); - assert!(h.contains("--prune-inwit")); - assert!(h.contains("--prune-inwit-ram-threshold-bytes")); - assert!(!h.contains("--pruneinwit")); + assert!(h.contains("--prune-seqsigwit")); + assert!(h.contains("--prune-seqsigwit-ram-threshold-bytes")); + assert!(!h.contains("--pruneseqsigwit")); assert_exit( - cli_main(["rbitcoin-node", "--pruneinwit"]), + cli_main(["rbitcoin-node", "--pruneseqsigwit"]), ExitCode::from(2), ); } @@ -1567,7 +1567,7 @@ mod tests { } #[test] - fn smoke_datadir_cold_puts_inwit_on_cold_store() { + fn smoke_datadir_cold_puts_seqsigwit_on_cold_store() { let _g = OPERATOR_ENV_TEST_LOCK .lock() .unwrap_or_else(|e| e.into_inner()); @@ -1592,10 +1592,10 @@ mod tests { assert_exit(code, ExitCode::SUCCESS); assert!(hot.join("store").is_dir()); assert!(hot.join("store/txout.body").is_file()); - assert!(!hot.join("store/inwit.body").exists()); - assert!(cold.join("store/inwit.body").is_file()); - assert!(cold.join("store/inwit.loc").is_file()); - assert!(hot.join("store").join("inwit.reloc").is_file()); + assert!(!hot.join("store/seqsigwit.body").exists()); + assert!(cold.join("store/seqsigwit.body").is_file()); + assert!(cold.join("store/seqsigwit.loc").is_file()); + assert!(hot.join("store").join("seqsigwit.reloc").is_file()); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/rbitcoin-node/src/config.rs b/crates/rbitcoin-node/src/config.rs index 9e5f29d54..86080de44 100644 --- a/crates/rbitcoin-node/src/config.rs +++ b/crates/rbitcoin-node/src/config.rs @@ -62,7 +62,7 @@ pub(crate) fn parse_btc_to_sat(s: &str) -> Result { #[derive(Clone, Debug, PartialEq, Eq)] pub struct DatadirOpts { pub path: PathBuf, - /// When set, cold witness artifacts (`inwit.*`, `inwit.window/*`) live under `{cold}/store`. + /// When set, cold witness artifacts (`seqsigwit.*`, `seqsigwit.window/*`) live under `{cold}/store`. pub cold: Option, } @@ -261,10 +261,10 @@ pub struct NodeConfig { pub max_run_secs: Option, /// Build Class B scripthash index (Electrum/Esplora history). Default **off**. pub shindex: bool, - /// Drop Class A inwit below a 288-height watermark (`NETWORK_LIMITED`). - pub prune_inwit: bool, + /// Drop Class A seqsigwit below a 288-height watermark (`NETWORK_LIMITED`). + pub prune_seqsigwit: bool, /// RAM cap for the prune witness window. `0` keeps nothing in RAM. - pub prune_inwit_ram_threshold_bytes: u64, + pub prune_seqsigwit_ram_threshold_bytes: u64, /// Persist / serve BIP-352 tweaks from `sp_tweaks.*`. Default **off**. pub sptweaks: bool, /// Electrum tweaks: omit P2TR outs with `value <=` this (sats). `0` serves @@ -344,8 +344,8 @@ impl Default for NodeConfig { head_scale: HeadScale::Mainnet, max_run_secs: None, shindex: false, - prune_inwit: false, - prune_inwit_ram_threshold_bytes: 256 * 1024 * 1024, + prune_seqsigwit: false, + prune_seqsigwit_ram_threshold_bytes: 256 * 1024 * 1024, sptweaks: false, sptweaks_dust: rbitcoin_electrum::DEFAULT_TWEAKS_MIN_DUST, max_sh_creates: 0, @@ -896,13 +896,13 @@ impl NodeConfig { self.shindex = parse_conf_bool(val) .map_err(|e| NodeError::Config(format!("conf sh_index: {e}")))?; } - "prune_inwit" => { - self.prune_inwit = parse_conf_bool(val) - .map_err(|e| NodeError::Config(format!("conf prune_inwit: {e}")))?; + "prune_seqsigwit" => { + self.prune_seqsigwit = parse_conf_bool(val) + .map_err(|e| NodeError::Config(format!("conf prune_seqsigwit: {e}")))?; } - "prune_inwit_ram_threshold_bytes" => { - self.prune_inwit_ram_threshold_bytes = val.parse().map_err(|e| { - NodeError::Config(format!("conf prune_inwit_ram_threshold_bytes: {e}")) + "prune_seqsigwit_ram_threshold_bytes" => { + self.prune_seqsigwit_ram_threshold_bytes = val.parse().map_err(|e| { + NodeError::Config(format!("conf prune_seqsigwit_ram_threshold_bytes: {e}")) })?; } "sp_tweaks" => { diff --git a/crates/rbitcoin-node/src/run.rs b/crates/rbitcoin-node/src/run.rs index df480bf22..9d80f5e5e 100644 --- a/crates/rbitcoin-node/src/run.rs +++ b/crates/rbitcoin-node/src/run.rs @@ -481,7 +481,7 @@ pub async fn run_p2p(config: NodeConfig) -> Result<(), NodeError> { addrman.set_cjdns_reachable(config.listen.cjdns_reachable); node.peers .set_cjdns_reachable(config.listen.cjdns_reachable); - node.peers.set_pruned(config.prune_inwit); + node.peers.set_pruned(config.prune_seqsigwit); node.peers.set_asmap(asmap); for c in &config.listen.connect { addrman.add_addr(*c); @@ -1253,15 +1253,15 @@ fn apply_startup_index_mode( ) -> Result<(), NodeError> { query.set_sh_index_enabled(config.shindex); query.set_max_sh_creates(config.max_sh_creates); - query.set_inwit_ram_threshold_bytes(config.prune_inwit_ram_threshold_bytes)?; - if !config.prune_inwit && query.prune_inwit() { + query.set_seqsigwit_ram_threshold_bytes(config.prune_seqsigwit_ram_threshold_bytes)?; + if !config.prune_seqsigwit && query.prune_seqsigwit() { return Err(NodeError::Config( - "datadir is pruned-inwit; restart with --prune-inwit enabled".into(), + "datadir is pruned-seqsigwit; restart with --prune-seqsigwit enabled".into(), )); } - if config.prune_inwit { - query.set_prune_inwit(true)?; - query.apply_prune_inwit_tip()?; + if config.prune_seqsigwit { + query.set_prune_seqsigwit(true)?; + query.apply_prune_seqsigwit_tip()?; } if let Err(e) = query.set_sptweaks_enabled(config.sptweaks, rbitcoin_primitives::Height(taproot_height)) @@ -1944,16 +1944,16 @@ mod tests { std::fs::create_dir_all(&dir).unwrap(); let store = dir.join("store"); let q = Query::open_or_create_tiny(&store).unwrap(); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_pruneheight(Some(rbitcoin_primitives::Height(0))) .unwrap(); let mut cfg = tiny_regtest(&dir); - cfg.prune_inwit = false; + cfg.prune_seqsigwit = false; let err = apply_startup_index_mode(&q, &cfg, 0) .unwrap_err() .to_string(); assert!( - err.contains("pruned-inwit") || err.contains("--prune-inwit"), + err.contains("pruned-seqsigwit") || err.contains("--prune-seqsigwit"), "{err}" ); let _ = std::fs::remove_dir_all(&dir); diff --git a/crates/rbitcoin-primitives/src/lib.rs b/crates/rbitcoin-primitives/src/lib.rs index 27b42ecfb..a67e60b53 100644 --- a/crates/rbitcoin-primitives/src/lib.rs +++ b/crates/rbitcoin-primitives/src/lib.rs @@ -90,10 +90,10 @@ pub const STORE_MAGIC: [u8; 4] = *b"RBT1"; /// ~1 MiB consensus-valid txout (and `n_out > 65535`) stores. Occupied /// 22 Class A rewrites 12 B ovf rows and `meta`. Occupied 15–21 Class A /// still refused. Empty 13–22 rewrite `meta`. -/// **22:** `create.loc` + `inwit.loc`; LAYOUT17 omits `output_count`. Spent +/// **22:** `create.loc` + `seqsigwit.loc`; LAYOUT17 omits `output_count`. Spent /// slot is flags + u40 spend fk + u16 vin. Occupied 21 Class A /// refused (wipe + IBD). Empty 21 rewrites `meta` and unlinks -/// leftover `spent.off` and `{txout,spent,inwit}.idx`. +/// leftover `spent.off` and `{txout,spent,seqsigwit}.idx`. /// **21:** Drop `spent.idx`; leftover unlinked; rewrite `meta` 20→21. Spent /// ranges are `n_out` prefix of `txout` (sparse `spent.off`). /// **20:** Sealed `tx.head` value-assigned packed BDZ (no `.rel`). Occupied @@ -108,7 +108,7 @@ pub const STORE_MAGIC: [u8; 4] = *b"RBT1"; /// megakey pages are uleb deltas. /// **16:** Drop `tx_height.body`; create height is a RAM fence from `confirmed[]` + /// `header_txs_*`. Soft-open schema 15 (unlink leftover file). Class A unchanged. -/// **15:** Class A split (`txout` / `inwit` / `spent`) + Class B SH slabs / sorted heads. +/// **15:** Class A split (`txout` / `seqsigwit` / `spent`) + Class B SH slabs / sorted heads. /// Refuse packed schema-13/14 Class A with txs; refuse materialized page-era SH. /// **14:** Class B SH head = Empty/Inline/Paged (4 KiB page chains); refuse schema-13 slabs. /// **13:** dense `txid.body` sidefile; Class A packed body meta **without** leading txid. @@ -279,11 +279,11 @@ pub enum TableKind { TxidBody = 14, /// Optional BIP-352 thin tweak body (`sp_tweaks.body`). Schema 14 side product. SpTweaks = 15, - /// Class A input-side + witness (`inwit.body`). - Inwit = 16, + /// Class A input-side + witness (`seqsigwit.body`). + SeqSigWit = 16, /// Class A sole-spender slots (`spent.body`, 8 B × n_out). Spent = 17, - /// Create/inwit delta locators (`create.loc` / `inwit.loc` and `.ovf`). + /// Create/seqsigwit delta locators (`create.loc` / `seqsigwit.loc` and `.ovf`). DeltaLoc = 18, /// Dense create_fk-ordered confirm-time econ (`txstat.body`, 8 B). TxStat = 19, @@ -291,6 +291,10 @@ pub enum TableKind { TxStatOvf = 20, /// Per-header overflow locator (`txstat.blk`, 16 B/header). TxStatBlk = 21, + /// Spender → parent edges (`input.body`, 8 B/input). + Input = 22, + /// Per-create `n_in` (`input.loc`, 2 B/create). + InputLoc = 23, } impl TableKind { @@ -309,12 +313,14 @@ impl TableKind { 13 => Some(TableKind::Spender), 14 => Some(TableKind::TxidBody), 15 => Some(TableKind::SpTweaks), - 16 => Some(TableKind::Inwit), + 16 => Some(TableKind::SeqSigWit), 17 => Some(TableKind::Spent), 18 => Some(TableKind::DeltaLoc), 19 => Some(TableKind::TxStat), 20 => Some(TableKind::TxStatOvf), 21 => Some(TableKind::TxStatBlk), + 22 => Some(TableKind::Input), + 23 => Some(TableKind::InputLoc), _ => None, } } @@ -386,12 +392,14 @@ mod tests { assert_eq!(TableKind::Spender.as_u16(), 13); assert_eq!(TableKind::TxidBody.as_u16(), 14); assert_eq!(TableKind::SpTweaks.as_u16(), 15); - assert_eq!(TableKind::Inwit.as_u16(), 16); + assert_eq!(TableKind::SeqSigWit.as_u16(), 16); assert_eq!(TableKind::Spent.as_u16(), 17); assert_eq!(TableKind::DeltaLoc.as_u16(), 18); assert_eq!(TableKind::TxStat.as_u16(), 19); assert_eq!(TableKind::TxStatOvf.as_u16(), 20); assert_eq!(TableKind::TxStatBlk.as_u16(), 21); + assert_eq!(TableKind::Input.as_u16(), 22); + assert_eq!(TableKind::InputLoc.as_u16(), 23); } #[test] diff --git a/crates/rbitcoin-query/src/archive.rs b/crates/rbitcoin-query/src/archive.rs index 29f9f778d..b7f9bd3ca 100644 --- a/crates/rbitcoin-query/src/archive.rs +++ b/crates/rbitcoin-query/src/archive.rs @@ -712,10 +712,6 @@ pub(crate) fn txstat_row_from_tx( tx: &bitcoin::Transaction, in_sum: Option, ) -> Result { - let n_in = tx.input.len() as u32; - if n_in > u32::from(u16::MAX) { - return Err(StoreError::Corrupt("txstat n_in exceeds 16 bits")); - } let base_sz = tx.base_size(); let total = tx.total_size(); let base = u32::try_from(base_sz).map_err(|_| StoreError::Corrupt("txstat base"))?; @@ -732,7 +728,6 @@ pub(crate) fn txstat_row_from_tx( } }; Ok(rbitcoin_store::TxStatRow { - n_in, fee_sat: fee, base, wit_extra, @@ -740,10 +735,9 @@ pub(crate) fn txstat_row_from_tx( } fn txstat_placeholder_query(_n_in: u32) -> rbitcoin_store::TxStatRow { - // No wire body: leave the cell unstamped (all-zero). A row with n_in set - // and size 0 would make getblockstats report a real zero-size tx. + // No wire body: leave the cell unstamped (all-zero) so getblockstats + // reconstructs instead of reporting a zero-size row. rbitcoin_store::TxStatRow { - n_in: 0, fee_sat: 0, base: 0, wit_extra: 0, @@ -771,13 +765,11 @@ fn prevout_value( return Ok(val as u64); } if let Some(p) = parents { - if let Some(hit) = - p.get_parent_txout_parts(inp.create_fk, inp.prev_index, |v, s, _| (v, s.to_vec())) - { - if hit.0 < 0 { + if let Some(val) = p.get_parent_txout_parts(inp.create_fk, inp.prev_index, |v, _, _| v) { + if val < 0 { return Err(StoreError::Corrupt("txstat prevout negative")); } - return Ok(hit.0 as u64); + return Ok(val as u64); } } let o = query.tx_output_at_fk(inp.create_fk, inp.prev_index)?; @@ -1158,10 +1150,10 @@ impl Query { "tx put_full_batch fk mismatch (plan not committed in order)", )); } - if self.prune_inwit() { + if self.prune_seqsigwit() { let ins: Vec> = plan.packed.iter().map(|(_, v)| v.clone()).collect(); - self.note_appended_inwit_inputs(&got_tx_fks, &ins); + self.note_appended_seqsigwit_inputs(&got_tx_fks, &ins); } for ((pin, _), pair) in plan.packed.iter().zip(loc.iter()) { pin.set_loc(*pair); diff --git a/crates/rbitcoin-query/src/combined_stage.rs b/crates/rbitcoin-query/src/combined_stage.rs index cd7db7367..766310c95 100644 --- a/crates/rbitcoin-query/src/combined_stage.rs +++ b/crates/rbitcoin-query/src/combined_stage.rs @@ -80,7 +80,7 @@ pub fn load_creates_once( .map(|fk| IdxBodyJob::new(fk.get().unwrap_or(0), None)) .collect(); store.idx_body_pipeline(&mut jobs, mode)?; - let mut inwit_jobs: Vec = if mode == IdxBodyMode::Full { + let mut seqsigwit_jobs: Vec = if mode == IdxBodyMode::Full { fks.iter() .map(|fk| IdxBodyJob::new(fk.get().unwrap_or(0), None)) .collect() @@ -88,7 +88,7 @@ pub fn load_creates_once( Vec::new() }; if mode == IdxBodyMode::Full { - store.idx_inwit_pipeline(&mut inwit_jobs, IdxBodyMode::Full)?; + store.idx_seqsigwit_pipeline(&mut seqsigwit_jobs, IdxBodyMode::Full)?; } let secret: &StoreSecret = store.txs.store_secret(); let mut out = Vec::with_capacity(jobs.len()); @@ -107,21 +107,25 @@ pub fn load_creates_once( if let Ok((mut tx, _empty_ins, outs, rels)) = decode_packed_tx_with_spender_rels_secret(&job.body, job.n_out, Some(secret)) { - let Some(ij) = inwit_jobs.get(i) else { + let Some(ij) = seqsigwit_jobs.get(i) else { return Err(StoreError::Corrupt( - "invariant: Full create missing inwit job", + "invariant: Full create missing seqsigwit job", )); }; if !ij.ok { return Err(StoreError::Corrupt( - "invariant: Full create inwit body missing after load", + "invariant: Full create seqsigwit body missing after load", )); } - let ins = - rbitcoin_store::decode_inwit_secret(&ij.body, tx.input_count, Some(secret)) - .map_err(|_| { - StoreError::Corrupt("invariant: packed create inwit decode failed") - })?; + let mut ins = rbitcoin_store::decode_seqsigwit_secret( + &ij.body, + tx.input_count, + Some(secret), + ) + .map_err(|_| { + StoreError::Corrupt("invariant: packed create seqsigwit decode failed") + })?; + store.stamp_input_prevouts(*fk, &mut ins)?; if tx.input_count == 0 { tx.input_count = ins.len() as u32; } diff --git a/crates/rbitcoin-query/src/connect.rs b/crates/rbitcoin-query/src/connect.rs index 9edc70eb5..c51eed02f 100644 --- a/crates/rbitcoin-query/src/connect.rs +++ b/crates/rbitcoin-query/src/connect.rs @@ -249,18 +249,21 @@ impl Query { if let Some(tip) = self.tip_height() { let _ = self.ensure_height_by_hash_index(tip); } - self.record_confirmed_inwit_window(items)?; + self.record_confirmed_seqsigwit_window(items)?; Ok(out) } - fn record_confirmed_inwit_window(&self, items: &[ConfirmPrepared]) -> Result<(), QueryError> { - self.apply_prune_inwit_tip()?; - if !self.prune_inwit() { + fn record_confirmed_seqsigwit_window( + &self, + items: &[ConfirmPrepared], + ) -> Result<(), QueryError> { + self.apply_prune_seqsigwit_tip()?; + if !self.prune_seqsigwit() { return Ok(()); } for item in items { - self.note_inwit_ram_for_confirmed(item.height, &item.tx_fks)?; + self.note_seqsigwit_ram_for_confirmed(item.height, &item.tx_fks)?; } Ok(()) } @@ -679,7 +682,7 @@ impl Query { if tx.input_count == 0 { return Ok(Vec::new()); } - let inputs = if let Some(v) = self.inwit_cached_inputs(create_fk, tx.input_count)? { + let inputs = if let Some(v) = self.seqsigwit_cached_inputs(create_fk, tx.input_count)? { v } else { let (_, inputs, _) = self.store.get_tx_full(create_fk)?; @@ -693,7 +696,7 @@ impl Query { /// Output run from store (keyed by known create fk — no txid lookup). /// - /// Outs-only Class A (`get_tx_meta_and_outputs`); does not zip `inwit`. + /// Outs-only Class A (`get_tx_meta_and_outputs`); does not zip `seqsigwit`. pub(crate) fn tx_output_run_class_a( &self, create_fk: Fk, @@ -730,8 +733,8 @@ impl Query { let height = self .tip_height() .ok_or(StoreError::Corrupt("no tip to disconnect"))?; - self.require_inwit_at(height)?; - self.drop_inwit_ram_height(height.0); + self.require_seqsigwit_at(height)?; + self.drop_seqsigwit_ram_height(height.0); let _appender = self.sh.appender.lock().unwrap(); if drop_pending { self.drop_sh_pending_from(height); diff --git a/crates/rbitcoin-query/src/lib.rs b/crates/rbitcoin-query/src/lib.rs index 15e05dc77..eca5ba57a 100644 --- a/crates/rbitcoin-query/src/lib.rs +++ b/crates/rbitcoin-query/src/lib.rs @@ -221,7 +221,7 @@ impl std::ops::DerefMut for BodyQueueInner { } #[derive(Default)] -struct InwitRamWindow { +struct SeqSigWitRamWindow { by_height: BTreeMap>, by_fk: U64Map>, bytes: u64, @@ -328,18 +328,18 @@ pub struct Query { confirm_stats: Arc, /// Tip height of last in-process io_uring recover (`u32::MAX` = none). uring_recover_tip: AtomicU32, - /// Highest height whose inwit was dropped (`u32::MAX` = none dropped). + /// Highest height whose seqsigwit was dropped (`u32::MAX` = none dropped). pruneheight: AtomicU32, - /// Operator `--prune-inwit` (advertise NETWORK_LIMITED even before a drop). - prune_inwit: AtomicBool, + /// Operator `--prune-seqsigwit` (advertise NETWORK_LIMITED even before a drop). + prune_seqsigwit: AtomicBool, /// True while the net IBD engine is active. ibd_mode: AtomicBool, /// In prune+IBD mode, cap for recent witness kept in RAM. - inwit_ram_threshold_bytes: AtomicU64, + seqsigwit_ram_threshold_bytes: AtomicU64, /// Recent witness cache keyed by confirmed heights/create fks. - inwit_ram_window: Mutex, + seqsigwit_ram_window: Mutex, /// Inputs from the most recent Class A append wave (fk-keyed). - inwit_append_cache: Mutex>>, + seqsigwit_append_cache: Mutex>>, } /// In-process hash→height map for the confirmed tip chain (~33 MiB raw at 1e6 tips). @@ -352,7 +352,7 @@ struct HeightByHashIndex { } impl Query { - pub const DEFAULT_INWIT_RAM_THRESHOLD_BYTES: u64 = 256 * 1024 * 1024; + pub const DEFAULT_SEQSIGWIT_RAM_THRESHOLD_BYTES: u64 = 256 * 1024 * 1024; pub fn open_or_create(store_path: impl AsRef) -> Result { Self::open_or_create_layout(StoreLayout::single(store_path.as_ref().to_path_buf())) } @@ -442,11 +442,13 @@ impl Query { confirm_stats: Arc::new(ConfirmStats::default()), uring_recover_tip: AtomicU32::new(u32::MAX), pruneheight: AtomicU32::new(ph), - prune_inwit: AtomicBool::new(prune_on), + prune_seqsigwit: AtomicBool::new(prune_on), ibd_mode: AtomicBool::new(false), - inwit_ram_threshold_bytes: AtomicU64::new(Self::DEFAULT_INWIT_RAM_THRESHOLD_BYTES), - inwit_ram_window: Mutex::new(InwitRamWindow::default()), - inwit_append_cache: Mutex::new(U64Map::default()), + seqsigwit_ram_threshold_bytes: AtomicU64::new( + Self::DEFAULT_SEQSIGWIT_RAM_THRESHOLD_BYTES, + ), + seqsigwit_ram_window: Mutex::new(SeqSigWitRamWindow::default()), + seqsigwit_append_cache: Mutex::new(U64Map::default()), }; if let Some(tip) = q.tip_height() { let _ = q.ensure_height_by_hash_index(tip); @@ -466,13 +468,13 @@ impl Query { } fn load_pruneheight(store_path: &Path) -> Result<(u32, bool), QueryError> { - let path = store_path.join("inwit.prune"); + let path = store_path.join("seqsigwit.prune"); match std::fs::read(&path) { Ok(bytes) => { let arr: [u8; 4] = bytes .as_slice() .try_into() - .map_err(|_| StoreError::Corrupt("invariant: inwit.prune size"))?; + .map_err(|_| StoreError::Corrupt("invariant: seqsigwit.prune size"))?; Ok((u32::from_le_bytes(arr), true)) } Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok((u32::MAX, false)), @@ -480,8 +482,8 @@ impl Query { } } - pub fn prune_inwit(&self) -> bool { - self.prune_inwit.load(AtomicOrdering::Acquire) + pub fn prune_seqsigwit(&self) -> bool { + self.prune_seqsigwit.load(AtomicOrdering::Acquire) } pub fn ibd_mode(&self) -> bool { @@ -492,44 +494,45 @@ impl Query { self.ibd_mode.store(on, AtomicOrdering::Release); } - pub fn inwit_ram_threshold_bytes(&self) -> u64 { - self.inwit_ram_threshold_bytes.load(AtomicOrdering::Acquire) + pub fn seqsigwit_ram_threshold_bytes(&self) -> u64 { + self.seqsigwit_ram_threshold_bytes + .load(AtomicOrdering::Acquire) } - pub fn set_inwit_ram_threshold_bytes(&self, bytes: u64) -> Result<(), QueryError> { - self.inwit_ram_threshold_bytes + pub fn set_seqsigwit_ram_threshold_bytes(&self, bytes: u64) -> Result<(), QueryError> { + self.seqsigwit_ram_threshold_bytes .store(bytes, AtomicOrdering::Release); Ok(()) } #[inline] pub fn prune_ibd_mode(&self) -> bool { - self.prune_inwit() && self.ibd_mode() + self.prune_seqsigwit() && self.ibd_mode() } - pub fn set_prune_inwit(&self, on: bool) -> Result<(), QueryError> { - let was_on = self.prune_inwit(); - if !on && self.prune_inwit() { + pub fn set_prune_seqsigwit(&self, on: bool) -> Result<(), QueryError> { + let was_on = self.prune_seqsigwit(); + if !on && self.prune_seqsigwit() { return Err(StoreError::Layout( - "refusing to disable prune-inwit on a pruned datadir".into(), + "refusing to disable prune-seqsigwit on a pruned datadir".into(), )); } - self.prune_inwit.store(on, AtomicOrdering::Release); + self.prune_seqsigwit.store(on, AtomicOrdering::Release); if on { if self.pruneheight().is_none() { self.persist_pruneheight(u32::MAX)?; } if !was_on { - self.seed_recent_inwit_from_store()?; + self.seed_recent_seqsigwit_from_store()?; } } else { - self.clear_inwit_ram_window(); + self.clear_seqsigwit_ram_window(); self.set_pruneheight(None)?; } Ok(()) } - /// Durable-later watermark: creates at this height and below have no inwit. + /// Durable-later watermark: creates at this height and below have no seqsigwit. pub fn pruneheight(&self) -> Option { match self.pruneheight.load(AtomicOrdering::Acquire) { u32::MAX => None, @@ -541,23 +544,23 @@ impl Query { let v = height.map(|h| h.0).unwrap_or(u32::MAX); self.pruneheight.store(v, AtomicOrdering::Release); if height.is_some() { - self.prune_inwit.store(true, AtomicOrdering::Release); + self.prune_seqsigwit.store(true, AtomicOrdering::Release); self.persist_pruneheight(v)?; - self.prune_inwit_spill_below(v.saturating_add(1))?; + self.prune_seqsigwit_spill_below(v.saturating_add(1))?; Ok(()) } else { - self.prune_inwit.store(false, AtomicOrdering::Release); + self.prune_seqsigwit.store(false, AtomicOrdering::Release); self.persist_pruneheight_clear() } } fn persist_pruneheight(&self, v: u32) -> Result<(), QueryError> { - let path = self.store.path().join("inwit.prune"); + let path = self.store.path().join("seqsigwit.prune"); std::fs::write(&path, v.to_le_bytes()).map_err(|e| StoreError::io(path, e)) } fn persist_pruneheight_clear(&self) -> Result<(), QueryError> { - let path = self.store.path().join("inwit.prune"); + let path = self.store.path().join("seqsigwit.prune"); match std::fs::remove_file(&path) { Ok(()) => Ok(()), Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(()), @@ -565,23 +568,23 @@ impl Query { } } - pub const INWIT_KEEP_HEIGHTS: u32 = 288; + pub const SEQSIGWIT_KEEP_HEIGHTS: u32 = 288; - pub fn apply_prune_inwit_tip(&self) -> Result<(), QueryError> { - if !self.prune_inwit() { + pub fn apply_prune_seqsigwit_tip(&self) -> Result<(), QueryError> { + if !self.prune_seqsigwit() { return Ok(()); } let Some(tip) = self.tip_height() else { return Ok(()); }; - if tip.0 <= Self::INWIT_KEEP_HEIGHTS { + if tip.0 <= Self::SEQSIGWIT_KEEP_HEIGHTS { return Ok(()); } - self.set_pruneheight(Some(Height(tip.0 - Self::INWIT_KEEP_HEIGHTS))) + self.set_pruneheight(Some(Height(tip.0 - Self::SEQSIGWIT_KEEP_HEIGHTS))) } /// `false` when this create's connected height is at/below [`Self::pruneheight`]. - pub fn inwit_available(&self, fk: Fk) -> Result { + pub fn seqsigwit_available(&self, fk: Fk) -> Result { let Some(ph) = self.pruneheight() else { return Ok(true); }; @@ -591,7 +594,7 @@ impl Query { } } - fn require_inwit_at(&self, height: Height) -> Result<(), QueryError> { + fn require_seqsigwit_at(&self, height: Height) -> Result<(), QueryError> { if let Some(ph) = self.pruneheight() { if height.0 <= ph.0 { return Err(StoreError::Pruned { height: height.0 }); @@ -600,16 +603,16 @@ impl Query { Ok(()) } - fn require_inwit_fk(&self, fk: Fk) -> Result<(), QueryError> { - if self.inwit_available(fk)? { + fn require_seqsigwit_fk(&self, fk: Fk) -> Result<(), QueryError> { + if self.seqsigwit_available(fk)? { return Ok(()); } let height = self.store.tx_height_get(fk)?.unwrap_or(0); Err(StoreError::Pruned { height }) } - fn drop_inwit_ram_height(&self, height: u32) { - let mut g = self.inwit_ram_window.lock().unwrap(); + fn drop_seqsigwit_ram_height(&self, height: u32) { + let mut g = self.seqsigwit_ram_window.lock().unwrap(); let Some(old_fks) = g.by_height.remove(&height) else { return; }; @@ -625,20 +628,20 @@ impl Query { } } - pub(crate) fn clear_inwit_ram_window(&self) { - *self.inwit_ram_window.lock().unwrap() = InwitRamWindow::default(); - self.inwit_append_cache.lock().unwrap().clear(); + pub(crate) fn clear_seqsigwit_ram_window(&self) { + *self.seqsigwit_ram_window.lock().unwrap() = SeqSigWitRamWindow::default(); + self.seqsigwit_append_cache.lock().unwrap().clear(); } #[cfg(test)] - pub(crate) fn inwit_ram_window_stats(&self) -> (usize, usize, u64, u64) { - let g = self.inwit_ram_window.lock().unwrap(); + pub(crate) fn seqsigwit_ram_window_stats(&self) -> (usize, usize, u64, u64) { + let g = self.seqsigwit_ram_window.lock().unwrap(); (g.by_height.len(), g.by_fk.len(), g.bytes, g.evictions) } - pub(crate) fn inwit_ram_inputs(&self, fk: Fk) -> Option> { + pub(crate) fn seqsigwit_ram_inputs(&self, fk: Fk) -> Option> { let id = fk.get()?; - self.inwit_ram_window + self.seqsigwit_ram_window .lock() .unwrap() .by_fk @@ -646,31 +649,31 @@ impl Query { .cloned() } - fn inwit_spill_dir(&self) -> std::path::PathBuf { - self.store.path().join("inwit.window") + fn seqsigwit_spill_dir(&self) -> std::path::PathBuf { + self.store.path().join("seqsigwit.window") } - fn inwit_spill_file(&self, height: u32) -> Result { - let dir = self.inwit_spill_dir(); + fn seqsigwit_spill_file(&self, height: u32) -> Result { + let dir = self.seqsigwit_spill_dir(); let name = format!("{height}.bin"); let stem_ok = name .strip_suffix(".bin") .and_then(|s| s.parse::().ok()) == Some(height); if !stem_ok { - return Err(StoreError::Corrupt("invariant: inwit spill name")); + return Err(StoreError::Corrupt("invariant: seqsigwit spill name")); } let path = dir.join(&name); if !path.starts_with(&dir) { return Err(StoreError::Corrupt( - "invariant: inwit spill path escaped window dir", + "invariant: seqsigwit spill path escaped window dir", )); } Ok(path) } - fn prune_inwit_spill_below(&self, min_keep_height: u32) -> Result<(), QueryError> { - let dir = self.inwit_spill_dir(); + fn prune_seqsigwit_spill_below(&self, min_keep_height: u32) -> Result<(), QueryError> { + let dir = self.seqsigwit_spill_dir(); let Ok(rd) = std::fs::read_dir(&dir) else { return Ok(()); }; @@ -684,7 +687,7 @@ impl Query { continue; }; if h < min_keep_height { - let path = self.inwit_spill_file(h)?; + let path = self.seqsigwit_spill_file(h)?; match std::fs::remove_file(&path) { Ok(()) => {} Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} @@ -695,26 +698,26 @@ impl Query { Ok(()) } - fn persist_inwit_spill_height( + fn persist_seqsigwit_spill_height( &self, height: Height, rows: &[(Fk, Vec)], ) -> Result<(), QueryError> { - let dir = self.inwit_spill_dir(); + let dir = self.seqsigwit_spill_dir(); std::fs::create_dir_all(&dir).map_err(|e| StoreError::io(&dir, e))?; - let path = self.inwit_spill_file(height.0)?; + let path = self.seqsigwit_spill_file(height.0)?; let tmp_name = format!("{}.bin.tmp", height.0); if tmp_name .strip_suffix(".bin.tmp") .and_then(|s| s.parse::().ok()) != Some(height.0) { - return Err(StoreError::Corrupt("invariant: inwit spill name")); + return Err(StoreError::Corrupt("invariant: seqsigwit spill name")); } let tmp = dir.join(&tmp_name); if !tmp.starts_with(&dir) { return Err(StoreError::Corrupt( - "invariant: inwit spill path escaped window dir", + "invariant: seqsigwit spill path escaped window dir", )); } let mut out = Vec::new(); @@ -724,7 +727,7 @@ impl Query { }; out.extend_from_slice(&id.to_le_bytes()); let mut enc = Vec::new(); - rbitcoin_store::encode_inwit_with_secret(ins, &mut enc, None); + rbitcoin_store::encode_seqsigwit_with_secret(ins, &mut enc, None); out.extend_from_slice(&(enc.len() as u32).to_le_bytes()); out.extend_from_slice(&enc); } @@ -732,12 +735,12 @@ impl Query { std::fs::rename(&tmp, &path).map_err(|e| StoreError::io(&path, e)) } - fn inwit_spill_inputs_with_count( + fn seqsigwit_spill_inputs_with_count( &self, fk: Fk, input_count: u32, ) -> Result>, QueryError> { - if !self.prune_inwit() { + if !self.prune_seqsigwit() { return Ok(None); } let Some(height) = self.store.tx_height_get(fk)? else { @@ -746,19 +749,19 @@ impl Query { if self.pruneheight().is_some_and(|ph| height <= ph.0) { return Ok(None); } - let path = self.inwit_spill_file(height)?; + let path = self.seqsigwit_spill_file(height)?; let canon = match path.canonicalize() { Ok(p) => p, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(e) => return Err(StoreError::io(&path, e)), }; let root = self - .inwit_spill_dir() + .seqsigwit_spill_dir() .canonicalize() - .map_err(|e| StoreError::io(self.inwit_spill_dir(), e))?; + .map_err(|e| StoreError::io(self.seqsigwit_spill_dir(), e))?; if !canon.starts_with(&root) { return Err(StoreError::Corrupt( - "invariant: inwit spill path escaped window dir", + "invariant: seqsigwit spill path escaped window dir", )); } let raw = match std::fs::read(&canon) { @@ -774,10 +777,12 @@ impl Query { let n = u32::from_le_bytes(raw[i..i + 4].try_into().unwrap()) as usize; i += 4; if i.saturating_add(n) > raw.len() { - return Err(StoreError::Corrupt("inwit spill short row")); + return Err(StoreError::Corrupt("seqsigwit spill short row")); } if id == want { - let ins = rbitcoin_store::decode_inwit_secret(&raw[i..i + n], input_count, None)?; + let mut ins = + rbitcoin_store::decode_seqsigwit_secret(&raw[i..i + n], input_count, None)?; + self.store.stamp_input_prevouts(fk, &mut ins)?; return Ok(Some(ins)); } i += n; @@ -785,19 +790,19 @@ impl Query { Ok(None) } - pub(crate) fn inwit_cached_inputs( + pub(crate) fn seqsigwit_cached_inputs( &self, fk: Fk, input_count: u32, ) -> Result>, QueryError> { - if let Some(ins) = self.inwit_ram_inputs(fk) { + if let Some(ins) = self.seqsigwit_ram_inputs(fk) { return Ok(Some(ins)); } - self.inwit_spill_inputs_with_count(fk, input_count) + self.seqsigwit_spill_inputs_with_count(fk, input_count) } - pub(crate) fn note_appended_inwit_inputs(&self, fks: &[Fk], ins: &[Vec]) { - let mut cache = self.inwit_append_cache.lock().unwrap(); + pub(crate) fn note_appended_seqsigwit_inputs(&self, fks: &[Fk], ins: &[Vec]) { + let mut cache = self.seqsigwit_append_cache.lock().unwrap(); for (fk, inputs) in fks.iter().zip(ins.iter()) { if let Some(id) = fk.get() { cache.insert(id, inputs.clone()); @@ -805,17 +810,17 @@ impl Query { } } - pub(crate) fn note_inwit_ram_for_confirmed( + pub(crate) fn note_seqsigwit_ram_for_confirmed( &self, height: Height, tx_fks: &[Fk], ) -> Result<(), QueryError> { - if !self.prune_inwit() || tx_fks.is_empty() { + if !self.prune_seqsigwit() || tx_fks.is_empty() { return Ok(()); } - let threshold = self.inwit_ram_threshold_bytes(); + let threshold = self.seqsigwit_ram_threshold_bytes(); let mut staged: Vec<(Fk, Vec, u64)> = Vec::with_capacity(tx_fks.len()); - let mut appended = self.inwit_append_cache.lock().unwrap(); + let mut appended = self.seqsigwit_append_cache.lock().unwrap(); for &fk in tx_fks { let ins = if let Some(id) = fk.get() { if let Some(v) = appended.remove(&id) { @@ -832,16 +837,16 @@ impl Query { staged.push((fk, ins, bytes)); } drop(appended); - self.drop_inwit_ram_height(height.0); + self.drop_seqsigwit_ram_height(height.0); let spill_rows: Vec<(Fk, Vec)> = staged .iter() .map(|(fk, ins, _)| (*fk, ins.clone())) .collect(); - self.persist_inwit_spill_height(height, &spill_rows)?; + self.persist_seqsigwit_spill_height(height, &spill_rows)?; if threshold == 0 { return Ok(()); } - let mut g = self.inwit_ram_window.lock().unwrap(); + let mut g = self.seqsigwit_ram_window.lock().unwrap(); let mut at_height: Vec = Vec::with_capacity(staged.len()); for (fk, ins, bytes) in staged { if let Some(id) = fk.get() { @@ -855,7 +860,7 @@ impl Query { } } g.by_height.insert(height.0, at_height); - while g.by_height.len() > Self::INWIT_KEEP_HEIGHTS as usize || g.bytes > threshold { + while g.by_height.len() > Self::SEQSIGWIT_KEEP_HEIGHTS as usize || g.bytes > threshold { let Some((&old_h, old_fks)) = g.by_height.first_key_value() else { break; }; @@ -875,13 +880,13 @@ impl Query { Ok(()) } - fn seed_recent_inwit_from_store(&self) -> Result<(), QueryError> { + fn seed_recent_seqsigwit_from_store(&self) -> Result<(), QueryError> { let Some(tip) = self.tip_height() else { return Ok(()); }; let from = tip .0 - .saturating_sub(Self::INWIT_KEEP_HEIGHTS.saturating_sub(1)); + .saturating_sub(Self::SEQSIGWIT_KEEP_HEIGHTS.saturating_sub(1)); for h in from..=tip.0 { let tx_fks = match self.block_tx_fks(Height(h)) { Ok(v) => v, @@ -891,7 +896,7 @@ impl Query { if tx_fks.is_empty() { continue; } - self.note_inwit_ram_for_confirmed(Height(h), &tx_fks)?; + self.note_seqsigwit_ram_for_confirmed(Height(h), &tx_fks)?; } Ok(()) } @@ -1717,13 +1722,13 @@ impl Query { if i >= tx.input_count { return Err(StoreError::NotFound); } - if let Some(inputs) = self.inwit_cached_inputs(create_fk, tx.input_count)? { + if let Some(inputs) = self.seqsigwit_cached_inputs(create_fk, tx.input_count)? { return inputs.get(i as usize).cloned().ok_or(StoreError::NotFound); } - self.require_inwit_fk(create_fk)?; + self.require_seqsigwit_fk(create_fk)?; let (_, inputs, _) = match self.store.get_tx_full(create_fk) { Ok(v) => v, - Err(StoreError::NotFound) if self.prune_inwit() => { + Err(StoreError::NotFound) if self.prune_seqsigwit() => { let height = self.store.tx_height_get(create_fk)?.unwrap_or(0); return Err(StoreError::Pruned { height }); } @@ -1735,9 +1740,9 @@ impl Query { pub(crate) fn tx_prevouts_for_fk(&self, fk: Fk) -> Result, QueryError> { match self.store.get_tx_meta_and_prevouts(fk) { Ok((_, prevs)) => Ok(prevs), - Err(StoreError::NotFound) if self.prune_inwit() => { + Err(StoreError::NotFound) if self.prune_seqsigwit() => { let tx = self.get_tx(fk)?; - let Some(inputs) = self.inwit_cached_inputs(fk, tx.input_count)? else { + let Some(inputs) = self.seqsigwit_cached_inputs(fk, tx.input_count)? else { let height = self.store.tx_height_get(fk)?.unwrap_or(0); return Err(StoreError::Pruned { height }); }; @@ -1760,7 +1765,7 @@ impl Query { /// Output at `vout` for a known create fk (packed Class A works without head). /// - /// Outs-only Class A (`get_tx_meta_and_outputs`); does not zip `inwit`. + /// Outs-only Class A (`get_tx_meta_and_outputs`); does not zip `seqsigwit`. pub fn tx_output_at_fk(&self, create_fk: Fk, vout: u32) -> Result { let (meta, outs) = self.store.get_tx_meta_and_outputs(create_fk)?; if vout >= meta.output_count { diff --git a/crates/rbitcoin-query/src/query_tests.rs b/crates/rbitcoin-query/src/query_tests.rs index 928903404..4a5d13d1e 100644 --- a/crates/rbitcoin-query/src/query_tests.rs +++ b/crates/rbitcoin-query/src/query_tests.rs @@ -2108,7 +2108,7 @@ fn connect_chain_query_surface() { let out = q.tx_output_at_fk(fks[0], 0).unwrap(); assert!( q.store().tx_full_gets().is_empty(), - "tx_output_at_fk is outs-only (no inwit zip)" + "tx_output_at_fk is outs-only (no seqsigwit zip)" ); assert_eq!(out.value, 50_0000_0000); assert!(!q.is_outpoint_spent(&tx.txid, 0).unwrap()); @@ -2604,7 +2604,7 @@ fn reconstruct_pruned_returns_pruned_not_corrupt() { assert_eq!(q.pruneheight(), Some(Height(0))); let fks0 = q.block_tx_fks(Height(0)).unwrap(); assert!( - !q.inwit_available(fks0[0]).unwrap(), + !q.seqsigwit_available(fks0[0]).unwrap(), "height 0 must be at/below watermark" ); let err = q.reconstruct_archived_block(&hashes[0]).unwrap_err(); @@ -2625,7 +2625,7 @@ fn reconstruct_pruned_returns_pruned_not_corrupt() { let kept = q.reconstruct_archived_block(&hashes[1]).unwrap().unwrap(); assert_eq!(kept.txdata.len(), 1); let fks1 = q.block_tx_fks(Height(1)).unwrap(); - assert!(q.inwit_available(fks1[0]).unwrap()); + assert!(q.seqsigwit_available(fks1[0]).unwrap()); assert!(q.tx_wire_bytes(fks1[0]).is_ok()); let _ = std::fs::remove_dir_all(&dir); } @@ -2635,7 +2635,6 @@ fn unstamp_txstat(q: &Query, fk: Fk) { .write_txstat_row( fk, &TxStatRow { - n_in: 0, fee_sat: 0, base: 0, wit_extra: 0, @@ -2683,7 +2682,7 @@ fn stamp_txstat_from_block_coinbase_and_spend() { let b0 = q.reconstruct_archived_block(&hash0).unwrap().unwrap(); q.stamp_txstat_from_block(Height(0), &b0).unwrap(); let row0 = q.txstat_row(fk0).unwrap().expect("stamped coinbase"); - assert_eq!(row0.n_in, 1); + assert_eq!(q.store().input_n_in(fk0).unwrap(), Some(1)); assert_eq!(row0.fee_sat, 0); assert_eq!(row0.size() as usize, b0.txdata[0].total_size()); @@ -2716,7 +2715,7 @@ fn stamp_txstat_from_block_coinbase_and_spend() { let b1 = q.reconstruct_archived_block(&h1.hash).unwrap().unwrap(); q.stamp_txstat_from_block(Height(1), &b1).unwrap(); let foreign_row = q.txstat_row(fks1[1]).unwrap().expect("stamped spend"); - assert_eq!(foreign_row.n_in, 1); + assert_eq!(q.store().input_n_in(fks1[1]).unwrap(), Some(1)); assert_eq!(foreign_row.fee_sat, 1_0000_0000); let _ = std::fs::remove_dir_all(&dir); } @@ -2729,10 +2728,9 @@ fn txstat_row_merges_overflow_via_header_blob() { let fk = q.block_tx_fks(Height(0)).unwrap()[0]; assert!(q.store().txstat_row(fk).unwrap().is_some()); let fat = TxStatRow { - n_in: 2, - fee_sat: 2_000_000, - base: 400, - wit_extra: 200_000, + fee_sat: u64::from(u32::MAX), + base: 4_000_000, + wit_extra: 4_000_000, }; q.store() .write_txstat_block(hfk, fk.get().unwrap(), &[fat]) @@ -2773,7 +2771,7 @@ fn prune_watermark_survives_reopen() { drop(q); let q = Query::open_or_create_tiny(dir.path()).unwrap(); assert_eq!(q.pruneheight(), Some(Height(0))); - assert!(q.prune_inwit()); + assert!(q.prune_seqsigwit()); let err = q.reconstruct_block_at_height(Height(0)).unwrap_err(); assert!(matches!(err, StoreError::Pruned { height: 0 }), "{err:?}"); let _ = std::fs::remove_dir_all(&dir); @@ -2782,9 +2780,9 @@ fn prune_watermark_survives_reopen() { #[test] fn prune_ibd_ram_window_keeps_last_288_heights() { let (dir, q) = temp_query("prune-ibd-ram-window"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); - q.set_inwit_ram_threshold_bytes(1 << 30).unwrap(); + q.set_seqsigwit_ram_threshold_bytes(1 << 30).unwrap(); let mut prev = Fk::NULL; let mut parent_hash: Option<[u8; 32]> = None; for h in 0..320u32 { @@ -2793,9 +2791,9 @@ fn prune_ibd_ram_window_keeps_last_288_heights() { parent_hash = Some(header.hash); prev = q.connect_block(Height(h), &header, &[ta]).unwrap(); } - let (heights, fks, _bytes, evictions) = q.inwit_ram_window_stats(); - assert!(heights <= Query::INWIT_KEEP_HEIGHTS as usize); - assert!(fks <= Query::INWIT_KEEP_HEIGHTS as usize); + let (heights, fks, _bytes, evictions) = q.seqsigwit_ram_window_stats(); + assert!(heights <= Query::SEQSIGWIT_KEEP_HEIGHTS as usize); + assert!(fks <= Query::SEQSIGWIT_KEEP_HEIGHTS as usize); assert!(evictions > 0, "old heights must be evicted"); let _ = std::fs::remove_dir_all(&dir); } @@ -2803,9 +2801,9 @@ fn prune_ibd_ram_window_keeps_last_288_heights() { #[test] fn prune_ram_window_drops_fks_on_disconnect_and_replace() { let (dir, q) = temp_query("prune-ram-replace"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); - q.set_inwit_ram_threshold_bytes(1 << 30).unwrap(); + q.set_seqsigwit_ram_threshold_bytes(1 << 30).unwrap(); let (h0, mut t0) = coinbase_block(0, Fk::NULL, None); t0.inputs[0].witness = vec![vec![0x11; 16]]; let hash0 = h0.hash; @@ -2813,17 +2811,17 @@ fn prune_ram_window_drops_fks_on_disconnect_and_replace() { let (h1, mut t1) = coinbase_block(1, prev, Some(hash0)); t1.inputs[0].witness = vec![vec![0x22; 16]]; q.connect_block(Height(1), &h1, &[t1]).unwrap(); - assert_eq!(q.inwit_ram_window_stats().1, 2); + assert_eq!(q.seqsigwit_ram_window_stats().1, 2); q.disconnect_tip().unwrap(); assert_eq!( - q.inwit_ram_window_stats().1, + q.seqsigwit_ram_window_stats().1, 1, - "disconnect must drop that height's inwit" + "disconnect must drop that height's seqsigwit" ); let (h1b, mut t1b) = coinbase_block(1, prev, Some(hash0)); t1b.inputs[0].witness = vec![vec![0x33; 16]]; q.connect_block(Height(1), &h1b, &[t1b]).unwrap(); - let (heights, fks, _, _) = q.inwit_ram_window_stats(); + let (heights, fks, _, _) = q.seqsigwit_ram_window_stats(); assert_eq!(heights, 2); assert_eq!( fks, 2, @@ -2835,9 +2833,9 @@ fn prune_ram_window_drops_fks_on_disconnect_and_replace() { #[test] fn prune_ibd_ram_window_honors_byte_threshold() { let (dir, q) = temp_query("prune-ibd-ram-threshold"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); - q.set_inwit_ram_threshold_bytes(80).unwrap(); + q.set_seqsigwit_ram_threshold_bytes(80).unwrap(); let mut prev = Fk::NULL; let mut parent_hash: Option<[u8; 32]> = None; for h in 0..8u32 { @@ -2846,7 +2844,7 @@ fn prune_ibd_ram_window_honors_byte_threshold() { parent_hash = Some(header.hash); prev = q.connect_block(Height(h), &header, &[ta]).unwrap(); } - let (_heights, _fks, bytes, evictions) = q.inwit_ram_window_stats(); + let (_heights, _fks, bytes, evictions) = q.seqsigwit_ram_window_stats(); assert!(bytes <= 80, "bytes={bytes}"); assert!(evictions > 0); let _ = std::fs::remove_dir_all(&dir); @@ -2855,7 +2853,7 @@ fn prune_ibd_ram_window_honors_byte_threshold() { #[test] fn prune_ibd_ram_window_serves_recent_and_restart_uses_spill() { let (dir, q) = temp_query("prune-ibd-ram-serve"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); let mut prev = Fk::NULL; let mut parent_hash: Option<[u8; 32]> = None; @@ -2865,7 +2863,7 @@ fn prune_ibd_ram_window_serves_recent_and_restart_uses_spill() { parent_hash = Some(header.hash); prev = q.connect_block(Height(h), &header, &[ta]).unwrap(); } - q.apply_prune_inwit_tip().unwrap(); + q.apply_prune_seqsigwit_tip().unwrap(); let fk0 = q.block_tx_fks(Height(299)).unwrap()[0]; let tx0 = q.get_tx(fk0).unwrap(); assert_eq!( @@ -2873,7 +2871,7 @@ fn prune_ibd_ram_window_serves_recent_and_restart_uses_spill() { 1, "recent witness is served from RAM" ); - let window = q.store.path().join("inwit.window"); + let window = q.store.path().join("seqsigwit.window"); for ent in std::fs::read_dir(&window).unwrap() { let p = ent.unwrap().path(); assert_eq!(p.parent(), Some(window.as_path()), "{p:?}"); @@ -2896,17 +2894,17 @@ fn prune_ibd_ram_window_serves_recent_and_restart_uses_spill() { #[test] fn spill_symlink_outside_window_is_corrupt() { let (dir, q) = temp_query("prune-spill-symlink"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); let (header, mut ta) = coinbase_block(0, Fk::NULL, None); ta.inputs[0].witness = vec![vec![0x42; 16]]; q.connect_block(Height(0), &header, &[ta]).unwrap(); let fk = q.block_tx_fks(Height(0)).unwrap()[0]; - let spill = q.store.path().join("inwit.window").join("0.bin"); + let spill = q.store.path().join("seqsigwit.window").join("0.bin"); let outside = dir.path().join("outside.bin"); std::fs::rename(&spill, &outside).unwrap(); std::os::unix::fs::symlink(&outside, &spill).unwrap(); - q.clear_inwit_ram_window(); + q.clear_seqsigwit_ram_window(); let tx = q.get_tx(fk).unwrap(); let err = q.tx_input_at_fk(fk, &tx, 0).unwrap_err(); assert!( @@ -2919,13 +2917,13 @@ fn spill_symlink_outside_window_is_corrupt() { #[test] fn prune_ram_threshold_zero_spills_tiny_blocks() { let (dir, q) = temp_query("prune-ram-zero"); - q.set_inwit_ram_threshold_bytes(0).unwrap(); - q.set_prune_inwit(true).unwrap(); + q.set_seqsigwit_ram_threshold_bytes(0).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_ibd_mode(true); let (header, mut ta) = coinbase_block(0, Fk::NULL, None); ta.inputs[0].witness = vec![vec![0x11]]; q.connect_block(Height(0), &header, &[ta]).unwrap(); - let (heights, fks, bytes, _) = q.inwit_ram_window_stats(); + let (heights, fks, bytes, _) = q.seqsigwit_ram_window_stats(); assert_eq!(heights, 0, "threshold 0 keeps no RAM heights"); assert_eq!(fks, 0); assert_eq!(bytes, 0); @@ -2936,7 +2934,7 @@ fn prune_ram_threshold_zero_spills_tiny_blocks() { vec![vec![0x11]], "tiny block is served from the height file" ); - assert!(q.store.path().join("inwit.window/0.bin").is_file()); + assert!(q.store.path().join("seqsigwit.window/0.bin").is_file()); let _ = std::fs::remove_dir_all(&dir); } @@ -2952,8 +2950,8 @@ fn enable_prune_after_history_seeds_recent_spill_window() { prev = q.connect_block(Height(h), &header, &[ta]).unwrap(); } let tip_fk = q.block_tx_fks(Height(299)).unwrap()[0]; - q.set_prune_inwit(true).unwrap(); - q.apply_prune_inwit_tip().unwrap(); + q.set_prune_seqsigwit(true).unwrap(); + q.apply_prune_seqsigwit_tip().unwrap(); let tx = q.get_tx(tip_fk).unwrap(); assert_eq!(q.tx_input_at_fk(tip_fk, &tx, 0).unwrap().witness.len(), 1); drop(q); @@ -2966,10 +2964,10 @@ fn enable_prune_after_history_seeds_recent_spill_window() { #[test] fn prune_mode_refuses_disable() { let (dir, q) = temp_query("prune-disable-refuse"); - q.set_prune_inwit(true).unwrap(); + q.set_prune_seqsigwit(true).unwrap(); q.set_pruneheight(Some(Height(0))).unwrap(); - let err = q.set_prune_inwit(false).unwrap_err().to_string(); - assert!(err.contains("refusing to disable prune-inwit"), "{err}"); + let err = q.set_prune_seqsigwit(false).unwrap_err().to_string(); + assert!(err.contains("refusing to disable prune-seqsigwit"), "{err}"); let _ = std::fs::remove_dir_all(&dir); } @@ -3318,14 +3316,14 @@ fn sh_collect_and_disconnect_skip_get_tx_full() { assert_eq!(recs.len(), 1); assert!( q.store().tx_full_gets().is_empty(), - "cold SH collect must not zip inwit: {:?}", + "cold SH collect must not zip seqsigwit: {:?}", q.store().tx_full_gets() ); q.store().reset_tx_full_gets(); q.disconnect_tip().unwrap(); assert!( q.store().tx_full_gets().is_empty(), - "disconnect SH unlink must not zip inwit: {:?}", + "disconnect SH unlink must not zip seqsigwit: {:?}", q.store().tx_full_gets() ); diff --git a/crates/rbitcoin-query/src/reconstruct.rs b/crates/rbitcoin-query/src/reconstruct.rs index fa1882a4a..be0541dc8 100644 --- a/crates/rbitcoin-query/src/reconstruct.rs +++ b/crates/rbitcoin-query/src/reconstruct.rs @@ -19,18 +19,18 @@ impl Query { fk: Fk, ) -> Result<(TxRecord, Vec, Vec), QueryError> { let (tx, outs) = self.store.get_tx_meta_and_outputs(fk)?; - if let Some(inputs) = self.inwit_cached_inputs(fk, tx.input_count)? { + if let Some(inputs) = self.seqsigwit_cached_inputs(fk, tx.input_count)? { if inputs.len() as u32 != tx.input_count { return Err(StoreError::Corrupt("packed input count mismatch")); } return Ok((tx, outs, inputs)); } - self.require_inwit_fk(fk)?; + self.require_seqsigwit_fk(fk)?; let t0 = Instant::now(); crate::note_confirm(&self.confirm_stats().wf_body_store, 1); let (tx, inputs, outs) = match self.store.get_tx_full(fk) { Ok(v) => v, - Err(StoreError::NotFound) if self.prune_inwit() => { + Err(StoreError::NotFound) if self.prune_seqsigwit() => { let height = self.store.tx_height_get(fk)?.unwrap_or(0); return Err(StoreError::Pruned { height }); } @@ -267,10 +267,10 @@ impl Query { tx_fks: &[Fk], ) -> Result, Vec)>, QueryError> { if let Some(&fk) = tx_fks.first() { - self.require_inwit_fk(fk)?; + self.require_seqsigwit_fk(fk)?; } let mut prev_txid_cache: U64Map<[u8; 32]> = U64Map::default(); - if !self.prune_inwit() { + if !self.prune_seqsigwit() { if let Some((first, last)) = Self::contiguous_fk_run(tx_fks) { let mut rows = self.store.get_tx_full_span(first, last)?; if rows.len() != tx_fks.len() { @@ -300,7 +300,7 @@ impl Query { hash: &[u8; 32], ) -> Result>, QueryError> { if let Some(h) = self.height_of_hash(hash)? { - self.require_inwit_at(h)?; + self.require_seqsigwit_at(h)?; } let Some((header_fk, rec)) = self.get_header_by_hash(hash)? else { return Ok(None); @@ -329,7 +329,7 @@ impl Query { pub fn reconstruct_archived_block(&self, hash: &[u8; 32]) -> Result, QueryError> { self.note_reconstruct_archived(); if let Some(h) = self.height_of_hash(hash)? { - self.require_inwit_at(h)?; + self.require_seqsigwit_at(h)?; } let Some((header_fk, rec)) = self.get_header_by_hash(hash)? else { return Ok(None); @@ -403,7 +403,7 @@ impl Query { /// Reconstruct a full wire block at a confirmed height from the relational archive. pub fn reconstruct_block_at_height(&self, height: Height) -> Result { - self.require_inwit_at(height)?; + self.require_seqsigwit_at(height)?; let (_fk, rec) = self.header_at_height(height)?.ok_or(StoreError::NotFound)?; let tx_fks = self.block_tx_fks(height)?; let block = self.reconstruct_archived_block_from_parts_cached(rec.clone(), tx_fks, None)?; @@ -423,7 +423,7 @@ impl Query { /// Dense confirm-time econ for a confirmed height, or `None` if any row is unstamped. /// - /// Does not read inwit. Missing `header_txs` is `None` (header-only). + /// Does not read seqsigwit. Missing `header_txs` is `None` (header-only). pub fn stamped_txstat_block( &self, height: Height, diff --git a/crates/rbitcoin-query/src/sp_tweaks.rs b/crates/rbitcoin-query/src/sp_tweaks.rs index 015863f48..dd2310c62 100644 --- a/crates/rbitcoin-query/src/sp_tweaks.rs +++ b/crates/rbitcoin-query/src/sp_tweaks.rs @@ -298,7 +298,7 @@ impl Query { /// Empty `Ok(vec![])` means the first height is not indexed (caller falls /// back per-height). Eligible Class A join is **one sequential `txout` /// span** from first..=last eligible fk in the wave (ineligible txout in - /// the hole is included; `inwit` is not). `sp_tweaks` mutex is not held + /// the hole is included; `seqsigwit` is not). `sp_tweaks` mutex is not held /// during Class A IO. `limits.cut_through` drops confirmed-spent P2TR /// outs after the join (spent range from the same `create.loc` pair as the /// body join, then one spent-body walk per create; txs with none left are @@ -594,7 +594,7 @@ mod tests { (p2wpkh, p2tr, ser.to_vec()) } - /// Fat **inwit** between eligible txs must stay out of the `txout` span. + /// Fat **seqsigwit** between eligible txs must stay out of the `txout` span. #[test] fn load_thin_skips_fat_ineligible_between_eligible() { let (dir, q) = tmp_q(); @@ -697,12 +697,12 @@ mod tests { .unwrap(); let fks = q.block_tx_fks(Height(1)).unwrap(); - let elig_a = q.store().tx_inwit_range(fks[0]).unwrap(); - let fat = q.store().tx_inwit_range(fks[1]).unwrap(); - let elig_b = q.store().tx_inwit_range(fks[2]).unwrap(); + let elig_a = q.store().tx_seqsigwit_range(fks[0]).unwrap(); + let fat = q.store().tx_seqsigwit_range(fks[1]).unwrap(); + let elig_b = q.store().tx_seqsigwit_range(fks[2]).unwrap(); assert!( fat.1 > 8_000, - "fat ineligible inwit row too small: {}", + "fat ineligible seqsigwit row too small: {}", fat.1 ); let elig_sum = elig_a.1.saturating_add(elig_b.1); @@ -738,7 +738,7 @@ mod tests { /// Notify `output_pubkeys` come from `txout` only. One sequential span from /// first..=last eligible in the wave (ineligible txout in the hole is - /// included; fat **inwit** stays out). + /// included; fat **seqsigwit** stays out). #[test] fn load_thin_span_reads_ineligible_txout_between_eligible() { let (dir, q) = tmp_q(); diff --git a/crates/rbitcoin-rpc/src/blockstats.rs b/crates/rbitcoin-rpc/src/blockstats.rs index 9de9a2fc8..3a18587f3 100644 --- a/crates/rbitcoin-rpc/src/blockstats.rs +++ b/crates/rbitcoin-rpc/src/blockstats.rs @@ -24,6 +24,7 @@ pub fn is_unspendable(script: &[u8]) -> bool { struct StampedExtras { total_out: i64, spendable_outs: i64, + n_ins: Vec, } /// Outputs Core counts in `utxo_increase_actual`: not height 0, not a @@ -351,7 +352,7 @@ fn stats_from_txstat( if is_cb { continue; } - ins += i64::from(row.n_in); + ins += i64::from(*extras.n_ins.get(i).unwrap_or(&0)); let tx_size = i64::try_from(row.size()).unwrap_or(i64::MAX); let tx_weight = i64::try_from(row.weight()).unwrap_or(i64::MAX); let fee = i64::try_from(row.fee_sat).unwrap_or(i64::MAX); @@ -588,6 +589,19 @@ fn stats_for_stamped( &stamped.fks, ) .map_err(|e| rpc_error(ERR_MISC, e.to_string()))?, + n_ins: { + let mut v = Vec::with_capacity(stamped.fks.len()); + for &fk in &stamped.fks { + let n = ctx + .query + .store() + .input_n_in(fk) + .map_err(|e| rpc_error(ERR_MISC, e.to_string()))? + .unwrap_or(0); + v.push(n); + } + v + }, }; Ok(stats_from_txstat( height.0, diff --git a/crates/rbitcoin-rpc/src/methods/chain.rs b/crates/rbitcoin-rpc/src/methods/chain.rs index b1ceaaa41..bf6e31c8b 100644 --- a/crates/rbitcoin-rpc/src/methods/chain.rs +++ b/crates/rbitcoin-rpc/src/methods/chain.rs @@ -95,7 +95,7 @@ pub(crate) fn getblockchaininfo(ctx: &RpcContext) -> Result { "initialblockdownload": ibd, "chainwork": chainwork_hex(ctx, ctx.query.tip_height()), "size_on_disk": ctx.query.store().datadir_bytes(), - "pruned": ctx.query.prune_inwit(), + "pruned": ctx.query.prune_seqsigwit(), "warnings": rpc_warnings(ctx), }); if let Some(h) = ctx.query.pruneheight() { diff --git a/crates/rbitcoin-rpc/src/methods/mod.rs b/crates/rbitcoin-rpc/src/methods/mod.rs index 1a600c4da..a90197554 100644 --- a/crates/rbitcoin-rpc/src/methods/mod.rs +++ b/crates/rbitcoin-rpc/src/methods/mod.rs @@ -648,7 +648,7 @@ const NAMED_HELP: &[(&str, &str)] = &[ initialblockdownload is relay-inhibited after densify (min-chain-work +\n\ max-tip-age), not still catching up. chainwork is summed header work\n\ (regtest 2/block). size_on_disk is a walk of store file lengths (plus\n\ - cold inwit when split). verificationprogress is blocks/headers (1.0 when\n\ + cold seqsigwit when split). verificationprogress is blocks/headers (1.0 when\n\ headers is 0).", ), ( diff --git a/crates/rbitcoin-rpc/src/methods/net.rs b/crates/rbitcoin-rpc/src/methods/net.rs index 10f11140b..8a3b4bd72 100644 --- a/crates/rbitcoin-rpc/src/methods/net.rs +++ b/crates/rbitcoin-rpc/src/methods/net.rs @@ -347,7 +347,7 @@ pub(crate) fn getnetworkinfo(ctx: &RpcContext) -> Value { } else { (0, ctx.connections.load(Ordering::Relaxed), 0) }; - let flags = rbitcoin_net::local_service_flags_pruned(ctx.query.prune_inwit()); + let flags = rbitcoin_net::local_service_flags_pruned(ctx.query.prune_seqsigwit()); let svc_bits = flags.to_u64(); json!({ "version": rpc_client_version(env!("CARGO_PKG_VERSION")), diff --git a/crates/rbitcoin-rpc/src/methods_tests.rs b/crates/rbitcoin-rpc/src/methods_tests.rs index e5180ab92..e8ff597b9 100644 --- a/crates/rbitcoin-rpc/src/methods_tests.rs +++ b/crates/rbitcoin-rpc/src/methods_tests.rs @@ -1843,7 +1843,7 @@ fn getblock_verbosity_1_txids_skip_reconstruct() { let v1 = dispatch(&ctx, "getblock", vec![first.clone(), json!(1)]).unwrap(); assert!( ctx.query.store().tx_full_gets().is_empty(), - "verbosity 1 must not zip inwit: {:?}", + "verbosity 1 must not zip seqsigwit: {:?}", ctx.query.store().tx_full_gets() ); let txs = v1["tx"].as_array().unwrap(); @@ -1983,7 +1983,7 @@ fn miniwallet_raw_scan_and_gettxout() { let scan = dispatch(&ctx, "scantxoutset", vec![json!("start"), json!([desc])]).unwrap(); assert!( ctx.query.store().tx_full_gets().is_empty(), - "scantxoutset shindex must not zip inwit: {:?}", + "scantxoutset shindex must not zip seqsigwit: {:?}", ctx.query.store().tx_full_gets() ); assert_eq!(scan["success"], true); @@ -4149,7 +4149,6 @@ fn getblockstats_reconstructs_when_txstat_unstamped() { .write_txstat_row( fk, &TxStatRow { - n_in: 0, fee_sat: 0, base: 0, wit_extra: 0, diff --git a/crates/rbitcoin-store/src/compact.rs b/crates/rbitcoin-store/src/compact.rs index 222acae73..921c6425f 100644 --- a/crates/rbitcoin-store/src/compact.rs +++ b/crates/rbitcoin-store/src/compact.rs @@ -80,10 +80,12 @@ pub mod input_flags { pub const EMPTY_SCRIPT: u8 = 1 << 1; /// Empty witness stack pub const EMPTY_WITNESS: u8 = 1 << 2; - /// Null prevout (coinbase): no create_fk payload; prev_index = 0xffff_ffff + /// Legacy coinbase: no inline `create_fk` payload; `prev_index` is `u32::MAX`. + /// New records leave the parent edge on `input.body` and set [`PREV_ON_INPUTS`] instead. pub const NULL_PREV: u8 = 1 << 3; - /// Reserved / unused (v9 and earlier LOCAL_PREV; rejected if set). - pub const RESERVED4: u8 = 1 << 4; + /// Parent `create_fk` and vout are not in this record. They live on `input.body`. + /// Bit 4 was v9 `LOCAL_PREV`, then reserved. + pub const PREV_ON_INPUTS: u8 = 1 << 4; /// Bits 5–7 reserved (schema 17 freeze). Reject so a later Δfk writer /// cannot be silently misparsed. pub const RESERVED_HIGH: u8 = 0xE0; diff --git a/crates/rbitcoin-store/src/delta_loc.rs b/crates/rbitcoin-store/src/delta_loc.rs index 6d87cba81..50517693d 100644 --- a/crates/rbitcoin-store/src/delta_loc.rs +++ b/crates/rbitcoin-store/src/delta_loc.rs @@ -1,7 +1,7 @@ //! Shared delta-locator helpers: 1024-create windows, stride-8, overflow rows. //! //! [`CreateLoc`](crate::create_loc::CreateLoc) packs txout strides + `n_out`. -//! [`DeltaLoc`] is the single-plane u16 sibling used by `inwit.loc`. +//! [`DeltaLoc`] is the single-plane u16 sibling used by `seqsigwit.loc`. use crate::error::StoreError; use crate::file::{ @@ -17,7 +17,7 @@ pub const IDX_STRIDE: u64 = 8; /// Creates per loc window / checkpoint grain. pub const LOC_WINDOW: u64 = 1024; const CREATE_OVF_MISSING: &str = "invariant: create.loc overflow missing"; -const INWIT_OVF_MISSING: &str = "invariant: inwit.loc overflow missing"; +const SEQSIGWIT_OVF_MISSING: &str = "invariant: seqsigwit.loc overflow missing"; pub(crate) const CREATE_OVF_SLOT: u64 = 16; const CREATE_OVF_SLOT_V22: u64 = 12; @@ -81,7 +81,7 @@ fn create_loc_file(path: &Path, kind: TableKind) -> Result Result<(), StoreError> { let cur = self.count.load(Ordering::Acquire); if new_count > cur { - return Err(StoreError::Corrupt("inwit.loc truncate past count")); + return Err(StoreError::Corrupt("seqsigwit.loc truncate past count")); } if new_count == cur { return Ok(()); @@ -212,7 +212,7 @@ impl DeltaLoc { if i + 1 < starts.len() && starts[i + 1] != start.saturating_add(len) { return Err(StoreError::Corrupt("invariant: loc starts")); } - let (disk, ovf) = pack_u16_strides_inwit(strides)?; + let (disk, ovf) = pack_u16_strides_seqsigwit(strides)?; loc_bytes.extend_from_slice(&disk.to_le_bytes()); if let Some(true_s) = ovf { let mut row = [0u8; 12]; @@ -335,7 +335,7 @@ impl DeltaLoc { } } -fn pack_u16_strides_inwit(strides: u32) -> Result<(u16, Option), StoreError> { +fn pack_u16_strides_seqsigwit(strides: u32) -> Result<(u16, Option), StoreError> { if strides == 0 { return Err(StoreError::Corrupt("invariant: loc strides")); } @@ -579,7 +579,7 @@ mod tests { #[test] fn delta_loc_append_and_batch_order() { let dir = TempDir::labeled("delta-loc").unwrap(); - let loc = DeltaLoc::create(dir.path(), "inwit").unwrap(); + let loc = DeltaLoc::create(dir.path(), "seqsigwit").unwrap(); let starts: Vec = (0..4).map(|i| FILE_HEADER_LEN as u64 + i * 16).collect(); loc.append(&starts, &[16, 16, 16, 16]).unwrap(); assert_eq!(loc.count(), 4); @@ -595,14 +595,14 @@ mod tests { #[test] fn delta_loc_u16_overflow_and_missing() { let dir = TempDir::labeled("delta-ovf").unwrap(); - let loc = DeltaLoc::create(dir.path(), "inwit").unwrap(); + let loc = DeltaLoc::create(dir.path(), "seqsigwit").unwrap(); let fat = 65536 * IDX_STRIDE; loc.append(&[FILE_HEADER_LEN as u64], &[fat]).unwrap(); let got = loc.range_batch(&[Fk(1)]).unwrap(); assert_eq!(got[0], Some((FILE_HEADER_LEN as u64, fat))); drop(loc); - std::fs::remove_file(dir.path().join("inwit.loc.ovf")).unwrap(); - let loc = DeltaLoc::open(dir.path(), "inwit").unwrap(); + std::fs::remove_file(dir.path().join("seqsigwit.loc.ovf")).unwrap(); + let loc = DeltaLoc::open(dir.path(), "seqsigwit").unwrap(); match loc.range_batch(&[Fk(1)]) { Err(StoreError::Corrupt(m)) => assert!(m.contains("overflow missing"), "{m}"), other => panic!("{other:?}"), @@ -610,12 +610,12 @@ mod tests { } #[test] - fn pack_u16_inwit_overflow_at_512kib() { - let (d, ovf) = pack_u16_strides_inwit(1).unwrap(); + fn pack_u16_seqsigwit_overflow_at_512kib() { + let (d, ovf) = pack_u16_strides_seqsigwit(1).unwrap(); assert_eq!((d, ovf), (1, None)); - let (d, ovf) = pack_u16_strides_inwit(65535).unwrap(); + let (d, ovf) = pack_u16_strides_seqsigwit(65535).unwrap(); assert_eq!((d, ovf), (65535, None)); - let (d, ovf) = pack_u16_strides_inwit(65536).unwrap(); + let (d, ovf) = pack_u16_strides_seqsigwit(65536).unwrap(); assert_eq!(d, 0); assert_eq!(ovf, Some(65536)); } diff --git a/crates/rbitcoin-store/src/error.rs b/crates/rbitcoin-store/src/error.rs index 20e05140e..aeb654210 100644 --- a/crates/rbitcoin-store/src/error.rs +++ b/crates/rbitcoin-store/src/error.rs @@ -31,7 +31,7 @@ pub enum StoreError { Stale(&'static str), /// Request refused (DoS cap) — not on-disk corruption. Rejected(&'static str), - /// Class A inwit dropped at/below the prune watermark. Not corruption. + /// Class A seqsigwit dropped at/below the prune watermark. Not corruption. Pruned { height: u32, }, @@ -134,7 +134,7 @@ mod tests { StoreError::BudgetFull("block_queue"), StoreError::Cancelled("stop"), StoreError::Unavailable, - StoreError::Layout("inwit is on a cold datadir".into()), + StoreError::Layout("seqsigwit is on a cold datadir".into()), StoreError::Stale("chain view moved"), StoreError::Rejected("scripthash join exceeds --max-sh-creates"), StoreError::Pruned { height: 12 }, @@ -151,7 +151,7 @@ mod tests { assert!(texts[7].contains("budget full: block_queue")); assert!(texts[8].contains("cancelled: stop")); assert_eq!(texts[9], "io_uring unavailable"); - assert_eq!(texts[10], "inwit is on a cold datadir"); + assert_eq!(texts[10], "seqsigwit is on a cold datadir"); assert_eq!(texts[11], "chain view moved"); assert_eq!(texts[12], "scripthash join exceeds --max-sh-creates"); assert_eq!(texts[13], "pruned data at height 12"); diff --git a/crates/rbitcoin-store/src/idx_body_pipeline.rs b/crates/rbitcoin-store/src/idx_body_pipeline.rs index 449c1847b..9ed74aba9 100644 --- a/crates/rbitcoin-store/src/idx_body_pipeline.rs +++ b/crates/rbitcoin-store/src/idx_body_pipeline.rs @@ -1,4 +1,4 @@ -//! **loc → body** pipeline for confirm load (`txout` / `inwit` stems). +//! **loc → body** pipeline for confirm load (`txout` / `seqsigwit` stems). //! //! Ranges from [`crate::create_loc::CreateLoc`] / [`crate::delta_loc::DeltaLoc`] //! (FdOnly). Body backend from @@ -18,7 +18,7 @@ use rbitcoin_primitives::Fk; /// What body bytes to fetch after the range is known. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BodyMode { - /// Full record (txout or inwit). + /// Full record (txout or seqsigwit). Full, /// `txout` outs / pin: starting OS page, or full span if need is likely to spill. Outs, @@ -40,7 +40,7 @@ pub struct IdxBodyJob { pub ok: bool, /// Sparse Outs need (sorted unique). Empty = all outs (SH / ensure). pub need_vouts: Vec, - /// Loc `n_out` (≥ 1) for txout decode. Unused for inwit/full. + /// Loc `n_out` (≥ 1) for txout decode. Unused for seqsigwit/full. pub n_out: u32, } @@ -301,7 +301,7 @@ pub fn run_idx_body_pipeline_backend( if jobs.is_empty() { return Ok(IdxBodyIoStats::default()); } - // Callers stamp `range` from create.loc / inwit.loc. Missing range → skip. + // Callers stamp `range` from create.loc / seqsigwit.loc. Missing range → skip. let body_fd = table.body_read_fd(); let body_pub = table.body_published_len(); diff --git a/crates/rbitcoin-store/src/input.rs b/crates/rbitcoin-store/src/input.rs new file mode 100644 index 000000000..db172c661 --- /dev/null +++ b/crates/rbitcoin-store/src/input.rs @@ -0,0 +1,470 @@ +//! Spender → parent edges (`input.loc` / `input.off` / `input.body`). +//! +//! `input.loc` is `n_in` as u16 LE per create (`0` = unstamped). `input.body` +//! is 8 bytes per input in vin order: parent `create_fk` u40 LE, parent vout +//! u24 LE. `input.off` checkpoints the body file offset at each 1024-create +//! window after the first. + +use crate::delta_loc::{loc_window, loc_within, LOC_WINDOW}; +use crate::error::StoreError; +use crate::file::{TableFile, FILE_HEADER_LEN}; +use rbitcoin_primitives::{Fk, TableKind}; +use std::path::Path; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::RwLock; + +const LOC_SLOT: u64 = 2; +const OFF_SLOT: u64 = 8; +const REC_LEN: u64 = 8; +const FK_LIMIT: u64 = 1 << 40; +const VOUT_LIMIT: u32 = 1 << 24; + +/// One spending input's parent outpoint. `parent` null and `vout` 0 is coinbase. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct InputEdge { + pub parent: Fk, + pub vout: u32, +} + +impl InputEdge { + pub fn coinbase() -> Self { + Self { + parent: Fk::NULL, + vout: 0, + } + } + + pub fn pack(self) -> Result<[u8; 8], StoreError> { + if self.parent.0 >= FK_LIMIT { + return Err(StoreError::Corrupt("input create_fk")); + } + if self.vout >= VOUT_LIMIT { + return Err(StoreError::Corrupt("input vout")); + } + if self.parent.is_null() && self.vout != 0 { + return Err(StoreError::Corrupt("input coinbase vout")); + } + let mut b = [0u8; 8]; + let fk = self.parent.0.to_le_bytes(); + b[..5].copy_from_slice(&fk[..5]); + let v = self.vout.to_le_bytes(); + b[5] = v[0]; + b[6] = v[1]; + b[7] = v[2]; + Ok(b) + } + + pub fn unpack(b: [u8; 8]) -> Result { + let mut fk_b = [0u8; 8]; + fk_b[..5].copy_from_slice(&b[..5]); + let parent = Fk(u64::from_le_bytes(fk_b)); + let vout = u32::from(b[5]) | (u32::from(b[6]) << 8) | (u32::from(b[7]) << 16); + let edge = Self { parent, vout }; + if parent.is_null() && vout != 0 { + return Err(StoreError::Corrupt("input coinbase vout")); + } + Ok(edge) + } +} + +pub struct Input { + loc: TableFile, + off: TableFile, + body: TableFile, + /// Body file offset at the start of window `w + 1` (after create `1024*(w+1)`). + checkpoints: RwLock>, + count: AtomicU64, + body_end: AtomicU64, +} + +impl Input { + pub fn create(dir: &Path) -> Result { + Ok(Self { + loc: TableFile::create(dir.join("input.loc"), TableKind::InputLoc)?, + off: TableFile::create(dir.join("input.off"), TableKind::ArrayLink)?, + body: TableFile::create(dir.join("input.body"), TableKind::Input)?, + checkpoints: RwLock::new(Vec::new()), + count: AtomicU64::new(0), + body_end: AtomicU64::new(FILE_HEADER_LEN as u64), + }) + } + + pub fn open(dir: &Path) -> Result { + let loc = TableFile::open(dir.join("input.loc"), TableKind::InputLoc)?; + let off = TableFile::open(dir.join("input.off"), TableKind::ArrayLink)?; + let body = TableFile::open(dir.join("input.body"), TableKind::Input)?; + let loc_data = loc.data_len(); + if loc_data % LOC_SLOT != 0 { + return Err(StoreError::Corrupt("invariant: input.loc size")); + } + let count = loc_data / LOC_SLOT; + let n_win = count / LOC_WINDOW; + if off.data_len() != n_win * OFF_SLOT { + return Err(StoreError::Corrupt("invariant: input.off size")); + } + let body_len = body.logical_len(); + if body_len < FILE_HEADER_LEN as u64 + || !(body_len - FILE_HEADER_LEN as u64).is_multiple_of(REC_LEN) + { + return Err(StoreError::Corrupt("invariant: input.body size")); + } + let mut checkpoints = vec![0u64; n_win as usize]; + if n_win > 0 { + let mut bytes = vec![0u8; (n_win * OFF_SLOT) as usize]; + off.read_at(FILE_HEADER_LEN as u64, &mut bytes)?; + for (i, chunk) in bytes.chunks_exact(OFF_SLOT as usize).enumerate() { + checkpoints[i] = u64::from_le_bytes(chunk.try_into().unwrap()); + } + } + Ok(Self { + loc, + off, + body, + checkpoints: RwLock::new(checkpoints), + count: AtomicU64::new(count), + body_end: AtomicU64::new(body_len), + }) + } + + pub fn count(&self) -> u64 { + self.count.load(Ordering::Acquire) + } + + /// `None` when the create is unstamped (`n_in == 0`). + pub fn n_in(&self, fk: Fk) -> Result, StoreError> { + let id = self.checked_id(fk)?; + let n = self.read_n_in(id)?; + Ok(if n == 0 { None } else { Some(u32::from(n)) }) + } + + /// Parent edges in vin order. `None` when unstamped. + pub fn edges(&self, fk: Fk) -> Result>, StoreError> { + let Some(n) = self.n_in(fk)? else { + return Ok(None); + }; + let id = fk.get().ok_or(StoreError::InvalidFk)?; + let abs = self.body_abs(id)?; + let mut raw = vec![0u8; n as usize * REC_LEN as usize]; + self.body.read_at(abs, &mut raw)?; + let mut out = Vec::with_capacity(n as usize); + for chunk in raw.chunks_exact(REC_LEN as usize) { + let mut b = [0u8; 8]; + b.copy_from_slice(chunk); + out.push(InputEdge::unpack(b)?); + } + Ok(Some(out)) + } + + pub fn append(&self, txs: &[Vec]) -> Result<(), StoreError> { + if txs.is_empty() { + return Ok(()); + } + let base = self.count.load(Ordering::Acquire); + let mut loc_bytes = Vec::with_capacity(txs.len() * 2); + let mut body_bytes = Vec::new(); + let mut new_offs: Vec<(u64, u64)> = Vec::new(); + let mut body_at = self.body_end.load(Ordering::Acquire); + for (i, edges) in txs.iter().enumerate() { + if edges.len() > usize::from(u16::MAX) { + return Err(StoreError::Corrupt("input n_in")); + } + let n_in = edges.len() as u16; + let fk = base + 1 + i as u64; + loc_bytes.extend_from_slice(&n_in.to_le_bytes()); + for edge in edges { + body_bytes.extend_from_slice(&edge.pack()?); + } + body_at = body_at.saturating_add(u64::from(n_in) * REC_LEN); + if fk.is_multiple_of(LOC_WINDOW) { + new_offs.push((fk / LOC_WINDOW - 1, body_at)); + } + } + let loc_at = FILE_HEADER_LEN as u64 + base * LOC_SLOT; + self.loc.write_at(loc_at, &loc_bytes)?; + if !body_bytes.is_empty() { + let body_start = self.body_end.load(Ordering::Acquire); + self.body.write_at(body_start, &body_bytes)?; + } + if !new_offs.is_empty() { + { + let cps = self.checkpoints.read().unwrap_or_else(|e| e.into_inner()); + if new_offs[0].0 as usize != cps.len() { + return Err(StoreError::Corrupt("invariant: input.off index")); + } + } + let mut blob = Vec::with_capacity(new_offs.len() * OFF_SLOT as usize); + for &(_, abs) in &new_offs { + blob.extend_from_slice(&abs.to_le_bytes()); + } + let off_at = FILE_HEADER_LEN as u64 + new_offs[0].0 * OFF_SLOT; + self.off.write_at(off_at, &blob)?; + let mut cps = self.checkpoints.write().unwrap_or_else(|e| e.into_inner()); + for &(w, abs) in &new_offs { + if w as usize != cps.len() { + return Err(StoreError::Corrupt("invariant: input.off index")); + } + cps.push(abs); + } + } + self.body_end.store(body_at, Ordering::Release); + self.count.store(base + txs.len() as u64, Ordering::Release); + let last = Fk(base + txs.len() as u64); + let wrote = self.edges(last)?.map(|v| v.len()).unwrap_or(0); + if wrote != txs.last().map(|e| e.len()).unwrap_or(0) { + return Err(StoreError::Corrupt("invariant: input n_in")); + } + Ok(()) + } + + /// Catch an existing store up with unstamped `n_in == 0` rows. No body bytes. + pub fn append_unstamped(&self, n: u64) -> Result<(), StoreError> { + if n == 0 { + return Ok(()); + } + let base = self.count.load(Ordering::Acquire); + let body_at = self.body_end.load(Ordering::Acquire); + let mut written = 0u64; + while written < n { + let chunk = (n - written).min(1 << 20); + let loc_bytes = vec![0u8; chunk as usize * LOC_SLOT as usize]; + let at = FILE_HEADER_LEN as u64 + (base + written) * LOC_SLOT; + self.loc.write_at(at, &loc_bytes)?; + written += chunk; + } + let mut new_offs: Vec<(u64, u64)> = Vec::new(); + for i in 0..n { + let fk = base + 1 + i; + if fk.is_multiple_of(LOC_WINDOW) { + new_offs.push((fk / LOC_WINDOW - 1, body_at)); + } + } + if !new_offs.is_empty() { + let mut blob = Vec::with_capacity(new_offs.len() * OFF_SLOT as usize); + for &(_, abs) in &new_offs { + blob.extend_from_slice(&abs.to_le_bytes()); + } + let off_at = FILE_HEADER_LEN as u64 + new_offs[0].0 * OFF_SLOT; + self.off.write_at(off_at, &blob)?; + let mut cps = self.checkpoints.write().unwrap_or_else(|e| e.into_inner()); + for &(w, abs) in &new_offs { + if w as usize != cps.len() { + return Err(StoreError::Corrupt("invariant: input.off index")); + } + cps.push(abs); + } + } + self.count.store(base + n, Ordering::Release); + Ok(()) + } + + pub fn truncate_to_count(&self, new_count: u64) -> Result<(), StoreError> { + let cur = self.count.load(Ordering::Acquire); + if new_count > cur { + return Err(StoreError::Corrupt("input.loc truncate past count")); + } + if new_count == cur { + return Ok(()); + } + let dropped = (cur - new_count) as usize; + let mut raw = vec![0u8; dropped * LOC_SLOT as usize]; + let at = FILE_HEADER_LEN as u64 + new_count * LOC_SLOT; + self.loc.read_at(at, &mut raw)?; + let mut n_drop = 0u64; + for chunk in raw.chunks_exact(LOC_SLOT as usize) { + n_drop += u64::from(u16::from_le_bytes([chunk[0], chunk[1]])); + } + let body_end = self.body_end.load(Ordering::Acquire); + let new_body = body_end - n_drop * REC_LEN; + if new_body < FILE_HEADER_LEN as u64 { + return Err(StoreError::Corrupt("invariant: input.body truncate")); + } + self.loc + .set_logical_len(FILE_HEADER_LEN as u64 + new_count * LOC_SLOT)?; + self.body.set_logical_len(new_body)?; + let n_win = new_count / LOC_WINDOW; + self.off + .set_logical_len(FILE_HEADER_LEN as u64 + n_win * OFF_SLOT)?; + { + let mut cps = self.checkpoints.write().unwrap_or_else(|e| e.into_inner()); + cps.truncate(n_win as usize); + } + self.body_end.store(new_body, Ordering::Release); + self.count.store(new_count, Ordering::Release); + Ok(()) + } + + fn checked_id(&self, fk: Fk) -> Result { + let Some(id) = fk.get() else { + return Err(StoreError::InvalidFk); + }; + if id > self.count.load(Ordering::Acquire) { + return Err(StoreError::NotFound); + } + Ok(id) + } + + fn read_n_in(&self, id: u64) -> Result { + let mut buf = [0u8; 2]; + self.loc + .read_at(FILE_HEADER_LEN as u64 + (id - 1) * LOC_SLOT, &mut buf)?; + Ok(u16::from_le_bytes(buf)) + } + + fn body_abs(&self, id: u64) -> Result { + let w = loc_window(id); + let within = loc_within(id); + let base = if w == 0 { + FILE_HEADER_LEN as u64 + } else { + let cps = self.checkpoints.read().unwrap_or_else(|e| e.into_inner()); + *cps.get((w - 1) as usize) + .ok_or(StoreError::Corrupt("invariant: input.off checkpoint"))? + }; + if within == 0 { + return Ok(base); + } + let win_first = w * LOC_WINDOW; + let mut raw = vec![0u8; within * LOC_SLOT as usize]; + self.loc + .read_at(FILE_HEADER_LEN as u64 + win_first * LOC_SLOT, &mut raw)?; + let mut n = 0u64; + for chunk in raw.chunks_exact(LOC_SLOT as usize) { + n += u64::from(u16::from_le_bytes([chunk[0], chunk[1]])); + } + Ok(base + n * REC_LEN) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::testutil::TempDir; + + fn edge(parent: u64, vout: u32) -> InputEdge { + InputEdge { + parent: Fk(parent), + vout, + } + } + + #[test] + fn pack_rejects_wide_fk_and_vout() { + let err = InputEdge { + parent: Fk(FK_LIMIT), + vout: 0, + } + .pack(); + assert!(matches!(err, Err(StoreError::Corrupt("input create_fk")))); + let err = InputEdge { + parent: Fk(1), + vout: VOUT_LIMIT, + } + .pack(); + assert!(matches!(err, Err(StoreError::Corrupt("input vout")))); + let err = InputEdge { + parent: Fk::NULL, + vout: 1, + } + .pack(); + assert!(matches!( + err, + Err(StoreError::Corrupt("input coinbase vout")) + )); + } + + #[test] + fn roundtrip_coinbase_and_parents_across_reopen() { + let dir = TempDir::labeled("input-rt").unwrap(); + let t = Input::create(dir.path()).unwrap(); + t.append(&[ + vec![InputEdge::coinbase()], + vec![edge(1, 0), edge(1, 3)], + vec![], + ]) + .unwrap(); + assert_eq!(t.n_in(Fk(1)).unwrap(), Some(1)); + assert_eq!( + t.edges(Fk(1)).unwrap().unwrap(), + vec![InputEdge::coinbase()] + ); + assert_eq!( + t.edges(Fk(2)).unwrap().unwrap(), + vec![edge(1, 0), edge(1, 3)] + ); + assert_eq!(t.n_in(Fk(3)).unwrap(), None); + assert!(t.edges(Fk(3)).unwrap().is_none()); + drop(t); + let t = Input::open(dir.path()).unwrap(); + assert_eq!(t.count(), 3); + assert_eq!( + t.edges(Fk(2)).unwrap().unwrap(), + vec![edge(1, 0), edge(1, 3)] + ); + t.truncate_to_count(1).unwrap(); + assert_eq!(t.count(), 1); + assert!(matches!(t.edges(Fk(2)), Err(StoreError::NotFound))); + assert_eq!( + t.edges(Fk(1)).unwrap().unwrap(), + vec![InputEdge::coinbase()] + ); + } + + #[test] + fn window_checkpoint_serves_create_1025() { + let dir = TempDir::labeled("input-win").unwrap(); + let t = Input::create(dir.path()).unwrap(); + let one = vec![edge(7, 1)]; + let batch = vec![one; 1025]; + t.append(&batch).unwrap(); + assert_eq!(t.edges(Fk(1)).unwrap().unwrap(), vec![edge(7, 1)]); + assert_eq!(t.edges(Fk(1024)).unwrap().unwrap(), vec![edge(7, 1)]); + assert_eq!(t.edges(Fk(1025)).unwrap().unwrap(), vec![edge(7, 1)]); + drop(t); + let t = Input::open(dir.path()).unwrap(); + assert_eq!(t.edges(Fk(1025)).unwrap().unwrap(), vec![edge(7, 1)]); + assert_eq!(t.n_in(Fk(1025)).unwrap(), Some(1)); + } + + #[test] + fn append_unstamped_is_zero_n_in_until_a_later_edge() { + let dir = TempDir::labeled("input-unstamped").unwrap(); + let t = Input::create(dir.path()).unwrap(); + t.append_unstamped(0).unwrap(); + assert_eq!(t.count(), 0); + t.append_unstamped(3).unwrap(); + assert_eq!(t.count(), 3); + assert_eq!(t.n_in(Fk(1)).unwrap(), None); + assert!(t.edges(Fk(3)).unwrap().is_none()); + t.append_unstamped(1021).unwrap(); + assert_eq!(t.count(), 1024); + assert_eq!(t.n_in(Fk(1024)).unwrap(), None); + t.append(&[vec![edge(4, 2)]]).unwrap(); + assert_eq!(t.edges(Fk(1025)).unwrap().unwrap(), vec![edge(4, 2)]); + t.append_unstamped(1024).unwrap(); + assert_eq!(t.count(), 2049); + assert_eq!(t.n_in(Fk(2049)).unwrap(), None); + drop(t); + let t = Input::open(dir.path()).unwrap(); + assert_eq!(t.count(), 2049); + assert_eq!(t.edges(Fk(1025)).unwrap().unwrap(), vec![edge(4, 2)]); + assert_eq!(t.n_in(Fk(1)).unwrap(), None); + } + + #[test] + fn new_store_creates_inputs_stems() { + let (dir, _store) = crate::testutil::tiny_store_labeled("input-files"); + assert!(dir.path().join("input.loc").is_file()); + assert!(dir.path().join("input.off").is_file()); + assert!(dir.path().join("input.body").is_file()); + } + + #[test] + fn append_rejects_n_in_past_u16() { + let dir = TempDir::labeled("input-wide").unwrap(); + let t = Input::create(dir.path()).unwrap(); + let edges = vec![edge(1, 0); usize::from(u16::MAX) + 1]; + let err = t.append(&[edges]).unwrap_err(); + assert!(matches!(err, StoreError::Corrupt("input n_in"))); + assert_eq!(t.count(), 0); + } +} diff --git a/crates/rbitcoin-store/src/lib.rs b/crates/rbitcoin-store/src/lib.rs index 89e505a4c..c7828c90a 100644 --- a/crates/rbitcoin-store/src/lib.rs +++ b/crates/rbitcoin-store/src/lib.rs @@ -24,6 +24,7 @@ pub mod head_resolve_stats; mod header_table; mod height_fence; mod idx_body_pipeline; +mod input; mod int_map; mod integrity; mod io_backend; @@ -96,11 +97,11 @@ pub use store::{keep_unspent_vout_subsequence, Store, StoreLayout}; pub use store_secret::StoreSecret; pub use tx_table::HeadResizeSizeSnapshot; pub use tx_table::{ - decode_inwit_secret, decode_packed_tx_outs_with_spender_rels, - decode_packed_tx_outs_with_spender_rels_secret, decode_packed_tx_with_spender_rels_secret, - encode_inwit_with_secret, encode_packed_tx, encode_packed_tx_with_secret, - encode_txout_meta_and_outs, encode_unspent_output_into_secret, spend_meta_backend, spent_abs, - InputRecord, OutputRecord, PackedCreate, TxRecord, + decode_packed_tx_outs_with_spender_rels, decode_packed_tx_outs_with_spender_rels_secret, + decode_packed_tx_with_spender_rels_secret, decode_seqsigwit_secret, encode_packed_tx, + encode_packed_tx_with_secret, encode_seqsigwit_with_secret, encode_txout_meta_and_outs, + encode_unspent_output_into_secret, spend_meta_backend, spent_abs, InputRecord, OutputRecord, + PackedCreate, TxRecord, }; pub use txstat::TxStatRow; pub(crate) use uring_session::IoCtx; diff --git a/crates/rbitcoin-store/src/sp_tweaks_uring.rs b/crates/rbitcoin-store/src/sp_tweaks_uring.rs index 2a2eb6088..e1fcc3459 100644 --- a/crates/rbitcoin-store/src/sp_tweaks_uring.rs +++ b/crates/rbitcoin-store/src/sp_tweaks_uring.rs @@ -1,7 +1,7 @@ //! Completion-session load for BIP-352 tweak backfill. //! //! Reads: idx ranges **before** taking the TLS ring (no nested -//! `with_thread_local`), then S1 `txout` → S2 `inwit` / S3 parent `txout` only +//! `with_thread_local`), then S1 `txout` → S2 `seqsigwit` / S3 parent `txout` only //! for P2TR creates. //! //! Writes are **not** this machine: consecutive heights go through @@ -10,8 +10,8 @@ use crate::error::StoreError; use crate::io_handle::IoHandle; use crate::tx_table::{ - decode_inwit_secret, decode_packed_tx_outs_with_spender_rels_secret, InputRecord, OutputRecord, - TxRecord, TxTable, + decode_packed_tx_outs_with_spender_rels_secret, decode_seqsigwit_secret, InputRecord, + OutputRecord, TxRecord, TxTable, }; use crate::uring_session::{self, UringSession}; use crate::U64Map; @@ -23,7 +23,7 @@ pub struct LoadedTweakTx { pub fk: Fk, pub rec: TxRecord, pub outs: Vec, - pub need_inwit: bool, + pub need_seqsigwit: bool, pub inputs: Option>, } @@ -159,7 +159,7 @@ fn empty_rec(txid: [u8; 32]) -> TxRecord { } } -/// Load outs for `fks`; inwit + extra parents only for P2TR creates. +/// Load outs for `fks`; seqsigwit + extra parents only for P2TR creates. /// /// Idx resolve ([`crate::var_table::VarTable::record_range_batch`]) runs /// **outside** the TLS ring. Body preads are the machine. @@ -184,7 +184,7 @@ pub fn load_tweak_wave(table: &TxTable, fks: &[Fk]) -> Result Result Result = txs .iter() .enumerate() - .filter(|(_, t)| t.need_inwit) + .filter(|(_, t)| t.need_seqsigwit) .map(|(i, _)| i) .collect(); if !p2tr_i.is_empty() { - let inwit_fks: Vec = p2tr_i.iter().map(|&i| txs[i].fk).collect(); - let inwit_ranges = table.inwit_loc.range_batch(&inwit_fks)?; - let mut inwit_jobs = jobs_from_ranges(&inwit_ranges, None); + let seqsigwit_fks: Vec = p2tr_i.iter().map(|&i| txs[i].fk).collect(); + let seqsigwit_ranges = table.seqsigwit_loc.range_batch(&seqsigwit_fks)?; + let mut seqsigwit_jobs = jobs_from_ranges(&seqsigwit_ranges, None); run_stage( - table.inwit.body_read_fd(), - table.inwit.body_file_path(), - uring_session::KIND_SP_INWIT, - &mut inwit_jobs, + table.seqsigwit.body_read_fd(), + table.seqsigwit.body_file_path(), + uring_session::KIND_SP_SEQSIGWIT, + &mut seqsigwit_jobs, )?; for (j, &ti) in p2tr_i.iter().enumerate() { - if inwit_jobs[j].buf.is_empty() { + if seqsigwit_jobs[j].buf.is_empty() { continue; } - let ins = decode_inwit_secret( - &inwit_jobs[j].buf, + let mut ins = decode_seqsigwit_secret( + &seqsigwit_jobs[j].buf, txs[ti].rec.input_count, Some(&table.secret), )?; + table.stamp_seqsigwit_prevouts(txs[ti].fk, &mut ins)?; txs[ti].inputs = Some(ins); } } @@ -313,7 +314,7 @@ mod tests { } #[test] - fn filter_marks_only_p2tr_need_inwit() { + fn filter_marks_only_p2tr_need_seqsigwit() { let dir = tmp_dir(); let t = tiny_table(&dir); let p2tr = { @@ -352,9 +353,12 @@ mod tests { .unwrap(); let wave = load_tweak_wave(&t, &fks).unwrap(); assert_eq!(wave.txs.len(), 2); - assert!(!wave.txs[0].need_inwit, "OP_TRUE must not load inwit"); + assert!( + !wave.txs[0].need_seqsigwit, + "OP_TRUE must not load seqsigwit" + ); assert!(wave.txs[0].inputs.is_none()); - assert!(wave.txs[1].need_inwit, "P2TR must need inwit"); + assert!(wave.txs[1].need_seqsigwit, "P2TR must need seqsigwit"); assert_eq!(wave.txs[1].rec.input_count, 1); assert_eq!(wave.txs[1].inputs.as_ref().map(Vec::len), Some(1)); let _ = std::fs::remove_dir_all(&dir); diff --git a/crates/rbitcoin-store/src/store.rs b/crates/rbitcoin-store/src/store.rs index 52e27085c..19e11e417 100644 --- a/crates/rbitcoin-store/src/store.rs +++ b/crates/rbitcoin-store/src/store.rs @@ -12,14 +12,14 @@ use std::fs::OpenOptions; use std::io::Write; use std::path::{Path, PathBuf}; -/// Sidecar in the hot `{datadir}/store`: `inwit.body` / `inwit.loc` live under +/// Sidecar in the hot `{datadir}/store`: `seqsigwit.body` / `seqsigwit.loc` live under /// `{datadir-cold}/store`. Presence-only (path always comes from the operator). -pub const INWIT_RELOC_NAME: &str = "inwit.reloc"; +pub const SEQSIGWIT_RELOC_NAME: &str = "seqsigwit.reloc"; /// Where a store’s files live, plus open-time head geometry. /// /// `dir` is `{datadir}/store`. When `cold_dir` is set and distinct, Class A -/// `inwit.body` + `inwit.loc` live there (bulk / HDD). Everything else stays +/// `seqsigwit.body` + `seqsigwit.loc` live there (bulk / HDD). Everything else stays /// in `dir`. /// /// [`Self::single`] / [`Self::with_cold`] are **Mainnet** scale (production). @@ -107,12 +107,12 @@ impl StoreLayout { } } - /// True when inwit is configured on a different directory than the hot store. + /// True when seqsigwit is configured on a different directory than the hot store. pub fn is_split(&self) -> bool { self.cold_dir.as_ref().is_some_and(|c| c != &self.dir) } - pub fn inwit_dir(&self) -> &Path { + pub fn seqsigwit_dir(&self) -> &Path { self.cold_dir .as_deref() .filter(|c| *c != self.dir) @@ -120,44 +120,107 @@ impl StoreLayout { } } -fn inwit_files_present(dir: &Path) -> bool { - dir.join("inwit.body").exists() - || dir.join("inwit.loc").exists() - || dir.join("inwit.idx").exists() +pub(crate) fn rename_legacy_input_files(dir: &Path) -> Result<(), StoreError> { + if !dir.exists() { + return Ok(()); + } + for (old, new) in [ + ("inputs.loc", "input.loc"), + ("inputs.off", "input.off"), + ("inputs.body", "input.body"), + ] { + let from = dir.join(old); + let to = dir.join(new); + if !from.exists() { + continue; + } + if to.exists() { + return Err(StoreError::Layout(format!( + "{} and {} both exist; keep only {}", + from.display(), + to.display(), + new + ))); + } + std::fs::rename(&from, &to).map_err(|e| StoreError::io(&from, e))?; + } + Ok(()) +} + +pub(crate) fn rename_legacy_inwit_files(dir: &Path) -> Result<(), StoreError> { + if !dir.exists() { + return Ok(()); + } + for (old, new) in [ + ("inwit.body", "seqsigwit.body"), + ("inwit.loc", "seqsigwit.loc"), + ("inwit.off", "seqsigwit.off"), + ("inwit.loc.ovf", "seqsigwit.loc.ovf"), + ("inwit.idx", "seqsigwit.idx"), + ("inwit.reloc", "seqsigwit.reloc"), + ("inwit.prune", "seqsigwit.prune"), + ("inwit.window", "seqsigwit.window"), + ] { + let from = dir.join(old); + let to = dir.join(new); + if !from.exists() { + continue; + } + if to.exists() { + return Err(StoreError::Layout(format!( + "{} and {} both exist; keep only {}", + from.display(), + to.display(), + new + ))); + } + std::fs::rename(&from, &to).map_err(|e| StoreError::io(&from, e))?; + } + Ok(()) +} + +fn seqsigwit_files_present(dir: &Path) -> bool { + dir.join("seqsigwit.body").exists() + || dir.join("seqsigwit.loc").exists() + || dir.join("seqsigwit.idx").exists() } -fn inwit_reloc_path(hot: &Path) -> PathBuf { - hot.join(INWIT_RELOC_NAME) +fn seqsigwit_reloc_path(hot: &Path) -> PathBuf { + hot.join(SEQSIGWIT_RELOC_NAME) } -/// Decide the inwit VarTable directory. Split stores refuse leftovers in hot +/// Decide the seqsigwit VarTable directory. Split stores refuse leftovers in hot /// and dual copies; a reloc marker without `--datadir-cold` is a layout error /// (not `Corrupt`). -fn resolve_inwit_dir(layout: &StoreLayout) -> Result { +fn resolve_seqsigwit_dir(layout: &StoreLayout) -> Result { + rename_legacy_inwit_files(&layout.dir)?; + rename_legacy_input_files(&layout.dir)?; if !layout.is_split() { - if inwit_reloc_path(&layout.dir).exists() { + if seqsigwit_reloc_path(&layout.dir).exists() { return Err(StoreError::Layout(format!( - "inwit is on a cold datadir ({INWIT_RELOC_NAME} present); pass --datadir-cold" + "seqsigwit is on a cold datadir ({SEQSIGWIT_RELOC_NAME} present); pass --datadir-cold" ))); } return Ok(layout.dir.clone()); } - let cold = layout.inwit_dir(); + let cold = layout.seqsigwit_dir(); + rename_legacy_inwit_files(cold)?; + rename_legacy_input_files(cold)?; if !cold.exists() { std::fs::create_dir_all(cold).map_err(|e| StoreError::io(cold, e))?; } else if !cold.is_dir() { return Err(StoreError::NotDirectory(cold.to_path_buf())); } - let hot_has = inwit_files_present(&layout.dir); - let cold_has = inwit_files_present(cold); + let hot_has = seqsigwit_files_present(&layout.dir); + let cold_has = seqsigwit_files_present(cold); match (hot_has, cold_has) { (true, true) => Err(StoreError::Layout(format!( - "inwit.body exists in both {} and {}; keep it only under the cold store", + "seqsigwit.body exists in both {} and {}; keep it only under the cold store", layout.dir.display(), cold.display() ))), (true, false) => Err(StoreError::Layout(format!( - "inwit is still in {}; move inwit.body and inwit.loc to {} \ + "seqsigwit is still in {}; move seqsigwit.body and seqsigwit.loc to {} \ (copy+remove if cross-device)", layout.dir.display(), cold.display() @@ -188,18 +251,18 @@ fn dir_file_bytes(root: &Path) -> u64 { n } -fn write_inwit_reloc(hot: &Path) -> Result<(), StoreError> { - let p = inwit_reloc_path(hot); +fn write_seqsigwit_reloc(hot: &Path) -> Result<(), StoreError> { + let p = seqsigwit_reloc_path(hot); if p.exists() { return Ok(()); } - std::fs::write(&p, b"inwit\n").map_err(|e| StoreError::io(&p, e)) + std::fs::write(&p, b"seqsigwit\n").map_err(|e| StoreError::io(&p, e)) } /// Top-level store handle for a datadir `store/` directory. pub struct Store { path: PathBuf, - /// `{datadir-cold}/store` when inwit is split; `None` = inwit in [`Self::path`]. + /// `{datadir-cold}/store` when seqsigwit is split; `None` = seqsigwit in [`Self::path`]. cold_path: Option, head_scale: HeadScale, pub headers: HeaderTable, @@ -286,13 +349,13 @@ impl Store { std::fs::create_dir_all(&path).map_err(|e| StoreError::io(&path, e))?; } write_meta(&path)?; - let inwit_dir = resolve_inwit_dir(&layout)?; + let seqsigwit_dir = resolve_seqsigwit_dir(&layout)?; let opts = layout.open_opts(); - let txs = TxTable::create_with_head_layout_inwit(&path, &inwit_dir, head, opts)?; + let txs = TxTable::create_with_head_layout_seqsigwit(&path, &seqsigwit_dir, head, opts)?; if layout.is_split() { - write_inwit_reloc(&path)?; + write_seqsigwit_reloc(&path)?; } - let cold_path = layout.is_split().then_some(inwit_dir); + let cold_path = layout.is_split().then_some(seqsigwit_dir); Ok(Self { headers: HeaderTable::create_with_scale(&path, layout.head_scale)?, txs, @@ -348,12 +411,12 @@ impl Store { drop_leftover_tx_height(&path); crate::scripthash::sh_run_catalog_key_len_ok(&path)?; open_layout_rewrite_current(&path, meta_ver)?; - let inwit_dir = resolve_inwit_dir(&layout)?; - let txs = TxTable::open_inwit(&path, &inwit_dir, layout.open_opts())?; + let seqsigwit_dir = resolve_seqsigwit_dir(&layout)?; + let txs = TxTable::open_seqsigwit(&path, &seqsigwit_dir, layout.open_opts())?; if layout.is_split() { - write_inwit_reloc(&path)?; + write_seqsigwit_reloc(&path)?; } - let cold_path = layout.is_split().then_some(inwit_dir); + let cold_path = layout.is_split().then_some(seqsigwit_dir); let store = Self { headers: HeaderTable::open_with_scale(&path, layout.head_scale)?, txs, @@ -402,13 +465,13 @@ impl Store { &self.path } - /// Cold store directory when inwit is split (`{datadir-cold}/store`). + /// Cold store directory when seqsigwit is split (`{datadir-cold}/store`). pub fn cold_path(&self) -> Option<&Path> { self.cold_path.as_deref() } /// Sum of regular file lengths under the hot store and, when split, the - /// cold inwit directory. Used by `getblockchaininfo.size_on_disk`. + /// cold seqsigwit directory. Used by `getblockchaininfo.size_on_disk`. pub fn datadir_bytes(&self) -> u64 { let mut n = dir_file_bytes(&self.path); if let Some(cold) = &self.cold_path { @@ -632,7 +695,7 @@ impl Store { Vec::new() } - /// Full Class A body by fk: zip `txout` + `inwit`. + /// Full Class A body by fk: zip `txout` + `seqsigwit`. pub fn get_tx_full( &self, fk: Fk, @@ -644,7 +707,7 @@ impl Store { self.txs.get_full(fk) } - /// Contiguous `first..=last` Class A bodies: one txout span + one inwit span. + /// Contiguous `first..=last` Class A bodies: one txout span + one seqsigwit span. pub fn get_tx_full_span( &self, first: u64, @@ -705,6 +768,25 @@ impl Store { } /// Overwrite one existing `txstat` row that fits in 8 B (tests / placeholders). + /// `n_in` from `input.loc`, or `None` when that create is unstamped. + pub fn input_n_in(&self, fk: Fk) -> Result, StoreError> { + self.txs.input.n_in(fk) + } + + /// Parent edges in vin order, or `None` when `input.loc` is unstamped. + pub fn input_edges(&self, fk: Fk) -> Result>, StoreError> { + self.txs.input.edges(fk) + } + + /// Copy parent edges onto seqsigwit records that do not store them. + pub fn stamp_input_prevouts( + &self, + fk: Fk, + ins: &mut [crate::tx_table::InputRecord], + ) -> Result<(), StoreError> { + self.txs.stamp_seqsigwit_prevouts(fk, ins) + } + pub fn write_txstat_row(&self, fk: Fk, row: &crate::TxStatRow) -> Result<(), StoreError> { self.txs.txstat.write_row(fk, row) } @@ -753,21 +835,21 @@ impl Store { self.txs.spent_range(fk) } - /// Absolute `inwit.body` `(offset, len)` for `fk`. - pub fn tx_inwit_range(&self, fk: Fk) -> Result<(u64, u64), StoreError> { - self.txs.inwit_range(fk) + /// Absolute `seqsigwit.body` `(offset, len)` for `fk`. + pub fn tx_seqsigwit_range(&self, fk: Fk) -> Result<(u64, u64), StoreError> { + self.txs.seqsigwit_range(fk) } - pub fn prune_inwit_mode(&self) -> bool { - self.txs.prune_inwit_mode() + pub fn prune_seqsigwit_mode(&self) -> bool { + self.txs.prune_seqsigwit_mode() } - pub fn set_prune_inwit_mode(&self, on: bool) { - self.txs.set_prune_inwit_mode(on); + pub fn set_prune_seqsigwit_mode(&self, on: bool) { + self.txs.set_prune_seqsigwit_mode(on); } - pub fn clear_durable_inwit(&self) -> Result<(), StoreError> { - self.txs.clear_durable_inwit() + pub fn clear_durable_seqsigwit(&self) -> Result<(), StoreError> { + self.txs.clear_durable_seqsigwit() } /// Append packed full-tx Class A rows (preferred archive path). @@ -1058,13 +1140,13 @@ impl Store { crate::run_idx_body_pipeline(&self.txs.body, jobs, mode).map(|_| ()) } - pub fn idx_inwit_pipeline( + pub fn idx_seqsigwit_pipeline( &self, jobs: &mut [crate::IdxBodyJob], mode: crate::IdxBodyMode, ) -> Result<(), StoreError> { - self.txs.fill_inwit_job_ranges(jobs)?; - crate::run_idx_body_pipeline(&self.txs.inwit, jobs, mode).map(|_| ()) + self.txs.fill_seqsigwit_job_ranges(jobs)?; + crate::run_idx_body_pipeline(&self.txs.seqsigwit, jobs, mode).map(|_| ()) } /// Bulk 8-byte spender meta at absolute `spent.body` offsets. @@ -2578,7 +2660,7 @@ mod tests { std::fs::write(dir.join("spent.off"), b"leftover").unwrap(); std::fs::create_dir_all(dir.join("txout.idx")).unwrap(); std::fs::create_dir_all(dir.join("spent.idx")).unwrap(); - std::fs::create_dir_all(dir.join("inwit.idx")).unwrap(); + std::fs::create_dir_all(dir.join("seqsigwit.idx")).unwrap(); let s = Store::open_tiny(&dir).unwrap(); drop(s); assert_eq!(read_store_meta_ver(&dir), SCHEMA_VERSION); @@ -2589,9 +2671,9 @@ mod tests { ); assert!(!dir.join("txout.idx").exists()); assert!(!dir.join("spent.idx").exists()); - assert!(!dir.join("inwit.idx").exists()); + assert!(!dir.join("seqsigwit.idx").exists()); assert!(dir.join("create.loc").is_file()); - assert!(dir.join("inwit.loc").is_file()); + assert!(dir.join("seqsigwit.loc").is_file()); let _ = std::fs::remove_dir_all(&dir); } @@ -2640,7 +2722,7 @@ mod tests { } #[test] - fn put_full_and_inwit_prevouts_at() { + fn put_full_and_seqsigwit_prevouts_at() { let dir = tmp(); let s = Store::create_tiny(&dir).unwrap(); let item = ( @@ -2660,7 +2742,7 @@ mod tests { let (m, prevs) = s.get_tx_meta_and_prevouts(fk).unwrap(); assert_eq!(m.input_count, 1); assert_eq!(prevs.len(), 1); - assert!(s.tx_inwit_range(fk).unwrap().1 > 0); + assert!(s.tx_seqsigwit_range(fk).unwrap().1 > 0); let _ = std::fs::remove_dir_all(&dir); } @@ -4076,7 +4158,34 @@ mod tests { } #[test] - fn split_create_puts_inwit_only_on_cold() { + fn open_renames_legacy_inwit_stem() { + let dir = tmp(); + Store::create_tiny(&dir).unwrap(); + std::fs::rename(dir.join("seqsigwit.body"), dir.join("inwit.body")).unwrap(); + std::fs::rename(dir.join("seqsigwit.loc"), dir.join("inwit.loc")).unwrap(); + Store::open_tiny(&dir).unwrap(); + assert!(dir.join("seqsigwit.body").is_file()); + assert!(dir.join("seqsigwit.loc").is_file()); + assert!(!dir.join("inwit.body").exists()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn open_renames_legacy_inputs_stem() { + let dir = tmp(); + Store::create_tiny(&dir).unwrap(); + std::fs::rename(dir.join("input.loc"), dir.join("inputs.loc")).unwrap(); + std::fs::rename(dir.join("input.off"), dir.join("inputs.off")).unwrap(); + std::fs::rename(dir.join("input.body"), dir.join("inputs.body")).unwrap(); + Store::open_tiny(&dir).unwrap(); + assert!(dir.join("input.loc").is_file()); + assert!(dir.join("input.body").is_file()); + assert!(!dir.join("inputs.loc").exists()); + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn split_create_puts_seqsigwit_only_on_cold() { let root = tmp(); let hot = root.join("hot"); let cold = root.join("cold"); @@ -4085,11 +4194,16 @@ mod tests { assert_eq!(s.cold_path(), Some(cold.as_path())); assert!(hot.join("txout.body").is_file()); assert!(hot.join("spent.body").is_file()); - assert!(!hot.join("inwit.body").exists()); - assert!(!hot.join("inwit.loc").exists()); - assert!(cold.join("inwit.body").is_file()); - assert!(cold.join("inwit.loc").is_file()); - assert!(hot.join(INWIT_RELOC_NAME).is_file()); + assert!(!hot.join("seqsigwit.body").exists()); + assert!(!hot.join("seqsigwit.loc").exists()); + assert!(cold.join("seqsigwit.body").is_file()); + assert!(cold.join("seqsigwit.loc").is_file()); + assert!(!hot.join("txstat.body").exists()); + assert!(!hot.join("input.loc").exists()); + assert!(cold.join("txstat.body").is_file()); + assert!(cold.join("input.loc").is_file()); + assert!(cold.join("input.body").is_file()); + assert!(hot.join(SEQSIGWIT_RELOC_NAME).is_file()); drop(s); let s = Store::open_layout(StoreLayout::tiny(&hot).with_cold_dir(&cold)).unwrap(); assert_eq!(s.cold_path(), Some(cold.as_path())); @@ -4107,44 +4221,44 @@ mod tests { let cold = root.join("cold"); Store::create_layout(StoreLayout::tiny(&hot).with_cold_dir(&cold)).unwrap(); match Store::open_tiny(&hot) { - Ok(_) => panic!("must refuse when inwit.reloc is present"), + Ok(_) => panic!("must refuse when seqsigwit.reloc is present"), Err(err) => { let msg = err.to_string(); assert!(msg.contains("datadir-cold"), "{msg}"); - assert!(msg.contains(INWIT_RELOC_NAME), "{msg}"); + assert!(msg.contains(SEQSIGWIT_RELOC_NAME), "{msg}"); } } let _ = std::fs::remove_dir_all(&root); } #[test] - fn split_refuses_inwit_left_in_hot() { + fn split_refuses_seqsigwit_left_in_hot() { let root = tmp(); let hot = root.join("hot"); let cold = root.join("cold"); Store::create_tiny(&hot).unwrap(); std::fs::create_dir_all(&cold).unwrap(); match Store::open_layout(StoreLayout::tiny(&hot).with_cold_dir(&cold)) { - Ok(_) => panic!("must refuse leftover inwit in hot"), + Ok(_) => panic!("must refuse leftover seqsigwit in hot"), Err(err) => { let msg = err.to_string(); - assert!(msg.contains("move inwit.body"), "{msg}"); - assert!(msg.contains("inwit.loc"), "{msg}"); + assert!(msg.contains("move seqsigwit.body"), "{msg}"); + assert!(msg.contains("seqsigwit.loc"), "{msg}"); } } let _ = std::fs::remove_dir_all(&root); } #[test] - fn split_refuses_inwit_in_both_dirs() { + fn split_refuses_seqsigwit_in_both_dirs() { let root = tmp(); let hot = root.join("hot"); let cold = root.join("cold"); Store::create_tiny(&hot).unwrap(); std::fs::create_dir_all(&cold).unwrap(); - std::fs::copy(hot.join("inwit.body"), cold.join("inwit.body")).unwrap(); + std::fs::copy(hot.join("seqsigwit.body"), cold.join("seqsigwit.body")).unwrap(); match Store::open_layout(StoreLayout::tiny(&hot).with_cold_dir(&cold)) { - Ok(_) => panic!("must refuse dual inwit copies"), + Ok(_) => panic!("must refuse dual seqsigwit copies"), Err(err) => { let msg = err.to_string(); assert!(msg.contains("both"), "{msg}"); @@ -4154,7 +4268,7 @@ mod tests { } #[test] - fn split_roundtrip_create_fk_and_inwit_range() { + fn split_roundtrip_create_fk_and_seqsigwit_range() { let root = tmp(); let hot = root.join("hot"); let cold = root.join("cold"); @@ -4165,15 +4279,15 @@ mod tests { .put_full_batch_indexed(std::slice::from_ref(&item), true) .unwrap(); assert_eq!(fks.len(), 1); - let range = s.tx_inwit_range(fks[0]).unwrap(); + let range = s.tx_seqsigwit_range(fks[0]).unwrap(); assert!(range.1 > 0); s.flush().unwrap(); drop(s); let s = Store::open_or_create_layout(StoreLayout::tiny(&hot).with_cold_dir(&cold)).unwrap(); assert_eq!(s.txs.count(), 1); - let range = s.tx_inwit_range(fks[0]).unwrap(); + let range = s.tx_seqsigwit_range(fks[0]).unwrap(); assert!(range.1 > 0); - assert!(!hot.join("inwit.body").exists()); + assert!(!hot.join("seqsigwit.body").exists()); let _ = std::fs::remove_dir_all(&root); } diff --git a/crates/rbitcoin-store/src/tx_table/mod.rs b/crates/rbitcoin-store/src/tx_table/mod.rs index 003c38325..22718c490 100644 --- a/crates/rbitcoin-store/src/tx_table/mod.rs +++ b/crates/rbitcoin-store/src/tx_table/mod.rs @@ -49,7 +49,7 @@ pub(crate) fn tx_head_rebuild_workers_for_free_ram(cpus: usize, free_bytes: u64) crate::sorted_run::workers_for_free_ram(cpus, free_bytes, TX_HEAD_REBUILD_WORKER_FREE_RAM_BYTES) } -/// Class A tx row (no wire blob — reconstruct from txout + inwit). +/// Class A tx row (no wire blob — reconstruct from txout + seqsigwit). /// /// On-disk `txout.body` (schema **17**): thin LAYOUT17 meta then outputs (no spender). /// Identity lives in [`crate::txid_body::TxidBody`]. `txid` is filled in-memory @@ -209,9 +209,41 @@ pub(crate) fn decode_body_meta_v17(buf: &[u8]) -> Result<(TxRecord, usize), Stor )) } -fn txstat_placeholder(n_in: u32) -> crate::txstat::TxStatRow { +fn edges_from_seqsigwit_payload(raw: &[u8]) -> Result, StoreError> { + let mut off = 0usize; + let mut edges = Vec::new(); + while off < raw.len() && raw[off..].iter().any(|&b| b != 0) { + let (create_fk, prev_index, used) = InputRecord::decode_prevout_at(&raw[off..])?; + off += used; + if create_fk.is_null() { + edges.push(crate::input::InputEdge::coinbase()); + } else { + edges.push(crate::input::InputEdge { + parent: create_fk, + vout: prev_index, + }); + } + } + Ok(edges) +} + +fn input_edges(ins: &[InputRecord]) -> Vec { + ins.iter() + .map(|inp| { + if inp.is_coinbase() { + crate::input::InputEdge::coinbase() + } else { + crate::input::InputEdge { + parent: inp.create_fk, + vout: inp.prev_index, + } + } + }) + .collect() +} + +fn txstat_placeholder() -> crate::txstat::TxStatRow { crate::txstat::TxStatRow { - n_in, fee_sat: 0, base: 0, wit_extra: 0, @@ -221,10 +253,7 @@ fn txstat_placeholder(n_in: u32) -> crate::txstat::TxStatRow { fn txstat_placeholders( items: &[(TxRecord, Vec, Vec)], ) -> Vec { - items - .iter() - .map(|(tx, _, _)| txstat_placeholder(tx.input_count)) - .collect() + items.iter().map(|_| txstat_placeholder()).collect() } /// Class A output (addressed via `tx.output_start_fk` run + local vout). @@ -413,7 +442,7 @@ fn pread_two_spans( .unwrap_or(Err(StoreError::Corrupt("txout span thread")))?; let vb = tb .join() - .unwrap_or(Err(StoreError::Corrupt("inwit span thread")))?; + .unwrap_or(Err(StoreError::Corrupt("seqsigwit span thread")))?; Ok((va, vb)) }) } @@ -421,12 +450,12 @@ fn pread_two_spans( pub struct TxTable { /// `txout.body` — meta + outputs (hot). pub(crate) body: VarTable, - /// `inwit.body` — inputs + witness (cold). - pub(crate) inwit: VarTable, + /// `seqsigwit.body` — inputs + witness (cold). + pub(crate) seqsigwit: VarTable, /// `spent.body` — 8 B × n_out sole-spender slots. pub(crate) spent: VarTable, pub(crate) create_loc: crate::create_loc::CreateLoc, - pub(crate) inwit_loc: crate::delta_loc::DeltaLoc, + pub(crate) seqsigwit_loc: crate::delta_loc::DeltaLoc, /// Segmented fixed-bits heads + seal-time fuse8. /// Segmented fixed-bits heads + seal-time fuse8. pub(crate) head: SegmentedTxHead, @@ -434,13 +463,15 @@ pub struct TxTable { pub(crate) txids: crate::txid_body::TxidBody, /// Dense create_fk-ordered confirm-time econ (schema 25). pub(crate) txstat: crate::txstat::TxStat, + /// Spender → parent edges (`input.loc` / `input.body`). + pub(crate) input: crate::input::Input, /// Datadir secret: keyed head probes + script XOR (schema 12+). pub(crate) secret: crate::store_secret::StoreSecret, /// Unflushed head inserts (write-behind). Readers see published snapshot. pending_head: pending_head::PendingHeadInserts, rebuild_seal_bits: u32, rebuild_workers: usize, - prune_inwit_mode: std::sync::atomic::AtomicBool, + prune_seqsigwit_mode: std::sync::atomic::AtomicBool, } /// Structural-meta backend from env hierarchy. @@ -478,18 +509,20 @@ fn open_or_create_create_loc(dir: &Path) -> Result Result { - if inwit_dir.join("inwit.loc").exists() { - crate::delta_loc::DeltaLoc::open(inwit_dir, "inwit") - } else if class_a_body_occupied(inwit_dir, "inwit") { - Err(StoreError::Corrupt("invariant: inwit.loc missing")) +fn open_or_create_seqsigwit_loc( + seqsigwit_dir: &Path, +) -> Result { + if seqsigwit_dir.join("seqsigwit.loc").exists() { + crate::delta_loc::DeltaLoc::open(seqsigwit_dir, "seqsigwit") + } else if class_a_body_occupied(seqsigwit_dir, "seqsigwit") { + Err(StoreError::Corrupt("invariant: seqsigwit.loc missing")) } else { - crate::delta_loc::DeltaLoc::create(inwit_dir, "inwit") + crate::delta_loc::DeltaLoc::create(seqsigwit_dir, "seqsigwit") } } fn unlink_leftover_class_a_idx(dir: &Path) -> Result<(), StoreError> { - for stem in ["txout", "spent", "inwit"] { + for stem in ["txout", "spent", "seqsigwit"] { let p = dir.join(format!("{stem}.idx")); if p.exists() { if p.is_dir() { @@ -498,7 +531,7 @@ fn unlink_leftover_class_a_idx(dir: &Path) -> Result<(), StoreError> { std::fs::remove_file(&p).map_err(|e| StoreError::io(&p, e))?; } rbitcoin_log::warn!( - "store: dropping leftover {stem}.idx (schema 22 uses create.loc / inwit.loc)" + "store: dropping leftover {stem}.idx (schema 22 uses create.loc / seqsigwit.loc)" ); } } @@ -507,34 +540,35 @@ fn unlink_leftover_class_a_idx(dir: &Path) -> Result<(), StoreError> { struct ClassASkewStems<'a> { create_loc: &'a crate::create_loc::CreateLoc, - inwit_loc: &'a crate::delta_loc::DeltaLoc, + seqsigwit_loc: &'a crate::delta_loc::DeltaLoc, body: &'a VarTable, spent: &'a VarTable, - inwit: &'a VarTable, + seqsigwit: &'a VarTable, txids: &'a crate::txid_body::TxidBody, txstat: &'a crate::txstat::TxStat, + input: &'a crate::input::Input, } fn class_a_skew_target_count( n_loc: u64, n_txids: u64, - n_inwit_loc: u64, - prune_inwit_mode: bool, + n_seqsigwit_loc: u64, + prune_seqsigwit_mode: bool, ) -> Option { - if n_txids == n_loc && (prune_inwit_mode || n_inwit_loc == n_loc) { + if n_txids == n_loc && (prune_seqsigwit_mode || n_seqsigwit_loc == n_loc) { return None; } - Some(if prune_inwit_mode { + Some(if prune_seqsigwit_mode { n_loc.min(n_txids) } else { - n_loc.min(n_txids).min(n_inwit_loc) + n_loc.min(n_txids).min(n_seqsigwit_loc) }) } fn class_a_skew_stem_ends( stems: &ClassASkewStems<'_>, n: u64, - prune_inwit_mode: bool, + prune_seqsigwit_mode: bool, ) -> Result<(u64, u64, u64), StoreError> { if n == 0 { let h = crate::file::FILE_HEADER_LEN as u64; @@ -547,17 +581,17 @@ fn class_a_skew_stem_ends( .next() .flatten() .ok_or(StoreError::Corrupt("invariant: loc range for truncate"))?; - let in_end = if prune_inwit_mode { + let in_end = if prune_seqsigwit_mode { crate::file::FILE_HEADER_LEN as u64 } else { let ir = stems - .inwit_loc + .seqsigwit_loc .range_batch(&[Fk(n)])? .into_iter() .next() .flatten() .ok_or(StoreError::Corrupt( - "invariant: inwit.loc range for truncate", + "invariant: seqsigwit.loc range for truncate", ))?; ir.0.saturating_add(ir.1) }; @@ -575,16 +609,16 @@ fn class_a_skew_apply_truncate( tx_end: u64, sp_end: u64, in_end: u64, - prune_inwit_mode: bool, + prune_seqsigwit_mode: bool, ) -> Result<(), StoreError> { stems.create_loc.truncate_to_count(n)?; - if !prune_inwit_mode { - stems.inwit_loc.truncate_to_count(n)?; + if !prune_seqsigwit_mode { + stems.seqsigwit_loc.truncate_to_count(n)?; } stems.body.truncate_body_to(n, tx_end)?; stems.spent.truncate_body_to(n, sp_end)?; - if !prune_inwit_mode { - stems.inwit.truncate_body_to(n, in_end)?; + if !prune_seqsigwit_mode { + stems.seqsigwit.truncate_body_to(n, in_end)?; } if n_txids > n { stems.txids.truncate_to_count(n)?; @@ -592,12 +626,15 @@ fn class_a_skew_apply_truncate( if stems.txstat.count() > n { stems.txstat.truncate_to_count(n)?; } + if stems.input.count() > n { + stems.input.truncate_to_count(n)?; + } Ok(()) } fn class_a_skew_assert_aligned( stems: &ClassASkewStems<'_>, - prune_inwit_mode: bool, + prune_seqsigwit_mode: bool, ) -> Result<(), StoreError> { if stems.body.count() != stems.txids.count() || stems.create_loc.count() != stems.txids.count() { @@ -605,7 +642,7 @@ fn class_a_skew_assert_aligned( "Class A stem counts still mismatch after repair (reindex required)", )); } - if !prune_inwit_mode && stems.inwit_loc.count() != stems.txids.count() { + if !prune_seqsigwit_mode && stems.seqsigwit_loc.count() != stems.txids.count() { return Err(StoreError::Corrupt( "Class A stem counts still mismatch after repair (reindex required)", )); @@ -615,21 +652,30 @@ fn class_a_skew_assert_aligned( fn repair_class_a_count_skew( stems: ClassASkewStems<'_>, - prune_inwit_mode: bool, + prune_seqsigwit_mode: bool, ) -> Result<(), StoreError> { let n_loc = stems.create_loc.count(); let n_txids = stems.txids.count(); - let n_inwit_loc = stems.inwit_loc.count(); - let Some(n) = class_a_skew_target_count(n_loc, n_txids, n_inwit_loc, prune_inwit_mode) else { + let n_seqsigwit_loc = stems.seqsigwit_loc.count(); + let Some(n) = class_a_skew_target_count(n_loc, n_txids, n_seqsigwit_loc, prune_seqsigwit_mode) + else { return Ok(()); }; rbitcoin_log::warn!( - "store: Class A count skew loc={n_loc} inwit.loc={n_inwit_loc} \ + "store: Class A count skew loc={n_loc} seqsigwit.loc={n_seqsigwit_loc} \ txid.body={n_txids} — truncating to {n}" ); - let (tx_end, sp_end, in_end) = class_a_skew_stem_ends(&stems, n, prune_inwit_mode)?; - class_a_skew_apply_truncate(&stems, n, n_txids, tx_end, sp_end, in_end, prune_inwit_mode)?; - class_a_skew_assert_aligned(&stems, prune_inwit_mode) + let (tx_end, sp_end, in_end) = class_a_skew_stem_ends(&stems, n, prune_seqsigwit_mode)?; + class_a_skew_apply_truncate( + &stems, + n, + n_txids, + tx_end, + sp_end, + in_end, + prune_seqsigwit_mode, + )?; + class_a_skew_assert_aligned(&stems, prune_seqsigwit_mode) } impl TxTable { @@ -659,36 +705,37 @@ impl TxTable { layout: HeadLayout, opts: HeadOpenOpts, ) -> Result { - Self::create_with_head_layout_inwit(dir, dir, layout, opts) + Self::create_with_head_layout_seqsigwit(dir, dir, layout, opts) } - /// Create Class A stems; `inwit` may live in a different directory. - pub(crate) fn create_with_head_layout_inwit( + /// Create Class A stems; `seqsigwit` may live in a different directory. + pub(crate) fn create_with_head_layout_seqsigwit( dir: &Path, - inwit_dir: &Path, + seqsigwit_dir: &Path, layout: HeadLayout, opts: HeadOpenOpts, ) -> Result { - if inwit_dir != dir { - std::fs::create_dir_all(inwit_dir).map_err(|e| StoreError::io(inwit_dir, e))?; + if seqsigwit_dir != dir { + std::fs::create_dir_all(seqsigwit_dir).map_err(|e| StoreError::io(seqsigwit_dir, e))?; } let secret = crate::store_secret::StoreSecret::load_or_create(dir, true)?; let layout = HeadLayout::with_entry_bytes(layout.bits, 4)?; let (seal_bits, workers) = Self::resolve_open_opts(opts); Ok(Self { body: VarTable::create(dir, "txout", TableKind::TxOut)?, - inwit: VarTable::create(inwit_dir, "inwit", TableKind::Inwit)?, + seqsigwit: VarTable::create(seqsigwit_dir, "seqsigwit", TableKind::SeqSigWit)?, spent: VarTable::create(dir, "spent", TableKind::Spent)?, create_loc: crate::create_loc::CreateLoc::create(dir)?, - inwit_loc: crate::delta_loc::DeltaLoc::create(inwit_dir, "inwit")?, + seqsigwit_loc: crate::delta_loc::DeltaLoc::create(seqsigwit_dir, "seqsigwit")?, head: SegmentedTxHead::create(dir, layout)?, txids: crate::txid_body::TxidBody::create(dir)?, - txstat: crate::txstat::TxStat::create(dir)?, + txstat: crate::txstat::TxStat::create(seqsigwit_dir)?, + input: crate::input::Input::create(seqsigwit_dir)?, secret, pending_head: pending_head::PendingHeadInserts::new(), rebuild_seal_bits: seal_bits, rebuild_workers: workers, - prune_inwit_mode: std::sync::atomic::AtomicBool::new(false), + prune_seqsigwit_mode: std::sync::atomic::AtomicBool::new(false), }) } @@ -732,52 +779,56 @@ impl TxTable { } pub fn open_with_opts(dir: &Path, opts: HeadOpenOpts) -> Result { - Self::open_inwit(dir, dir, opts) + Self::open_seqsigwit(dir, dir, opts) } - /// Open Class A stems; `inwit` may live in a different directory. - pub(crate) fn open_inwit( + /// Open Class A stems; `seqsigwit` may live in a different directory. + pub(crate) fn open_seqsigwit( dir: &Path, - inwit_dir: &Path, + seqsigwit_dir: &Path, opts: HeadOpenOpts, ) -> Result { - let prune_inwit_mode = false; + let prune_seqsigwit_mode = false; + crate::store::rename_legacy_inwit_files(dir)?; + crate::store::rename_legacy_input_files(dir)?; + crate::store::rename_legacy_inwit_files(seqsigwit_dir)?; + crate::store::rename_legacy_input_files(seqsigwit_dir)?; refuse_schema15_packed_tx_body(dir)?; let (seal_bits, workers) = Self::resolve_open_opts(opts); unlink_leftover_class_a_idx(dir)?; - if inwit_dir != dir { - unlink_leftover_class_a_idx(inwit_dir)?; - std::fs::create_dir_all(inwit_dir).map_err(|e| StoreError::io(inwit_dir, e))?; + if seqsigwit_dir != dir { + unlink_leftover_class_a_idx(seqsigwit_dir)?; + std::fs::create_dir_all(seqsigwit_dir).map_err(|e| StoreError::io(seqsigwit_dir, e))?; } let had_txout = dir.join("txout.body").exists(); - let had_inwit = inwit_dir.join("inwit.body").exists(); + let had_seqsigwit = seqsigwit_dir.join("seqsigwit.body").exists(); let had_spent = dir.join("spent.body").exists(); let create_loc = open_or_create_create_loc(dir)?; - let inwit_loc = open_or_create_inwit_loc(inwit_dir)?; + let seqsigwit_loc = open_or_create_seqsigwit_loc(seqsigwit_dir)?; let loc_count = create_loc.count(); - if !prune_inwit_mode && inwit_loc.count() != loc_count { - return Err(StoreError::Corrupt("invariant: inwit.loc count")); + if !prune_seqsigwit_mode && seqsigwit_loc.count() != loc_count { + return Err(StoreError::Corrupt("invariant: seqsigwit.loc count")); } let body = if had_txout { VarTable::open_body_only(dir, "txout", TableKind::TxOut, loc_count)? } else { VarTable::create_body_only(dir, "txout", TableKind::TxOut)? }; - if had_txout && loc_count > 0 && (!had_spent || (!had_inwit && !prune_inwit_mode)) { + if had_txout && loc_count > 0 && (!had_spent || (!had_seqsigwit && !prune_seqsigwit_mode)) { return Err(StoreError::Corrupt( - "schema 15 Class A missing inwit/spent for existing txout creates; wipe + IBD \ - (or --datadir-cold if inwit is on a cold volume)", + "schema 15 Class A missing seqsigwit/spent for existing txout creates; wipe + IBD \ + (or --datadir-cold if seqsigwit is on a cold volume)", )); } - let inwit = if had_inwit { - let in_count = if prune_inwit_mode { - inwit_loc.count() + let seqsigwit = if had_seqsigwit { + let in_count = if prune_seqsigwit_mode { + seqsigwit_loc.count() } else { loc_count }; - VarTable::open_body_only(inwit_dir, "inwit", TableKind::Inwit, in_count)? + VarTable::open_body_only(seqsigwit_dir, "seqsigwit", TableKind::SeqSigWit, in_count)? } else { - VarTable::create_body_only(inwit_dir, "inwit", TableKind::Inwit)? + VarTable::create_body_only(seqsigwit_dir, "seqsigwit", TableKind::SeqSigWit)? }; let spent = if had_spent { VarTable::open_body_only(dir, "spent", TableKind::Spent, loc_count)? @@ -789,18 +840,46 @@ impl TxTable { } else { crate::txid_body::TxidBody::create(dir)? }; - let txstat = crate::txstat::TxStat::open(dir)?; + if seqsigwit_dir != dir { + for name in [ + "txstat.body", + "txstat.ovf", + "txstat.blk", + "input.loc", + "input.off", + "input.body", + ] { + if dir.join(name).exists() && !seqsigwit_dir.join(name).exists() { + return Err(StoreError::Layout(format!( + "{name} is still in {}; move it next to seqsigwit under {}", + dir.display(), + seqsigwit_dir.display() + ))); + } + } + } + let txstat = crate::txstat::TxStat::open(seqsigwit_dir)?; + let input = if seqsigwit_dir.join("input.loc").exists() { + crate::input::Input::open(seqsigwit_dir)? + } else if seqsigwit_dir.join("input.body").exists() + || seqsigwit_dir.join("input.off").exists() + { + return Err(StoreError::Corrupt("invariant: input stems partial")); + } else { + crate::input::Input::create(seqsigwit_dir)? + }; repair_class_a_count_skew( ClassASkewStems { create_loc: &create_loc, - inwit_loc: &inwit_loc, + seqsigwit_loc: &seqsigwit_loc, body: &body, spent: &spent, - inwit: &inwit, + seqsigwit: &seqsigwit, txids: &txids, txstat: &txstat, + input: &input, }, - prune_inwit_mode, + prune_seqsigwit_mode, )?; let n_bodies = create_loc.count(); if txstat.count() != n_bodies { @@ -874,21 +953,31 @@ impl TxTable { let secret = crate::store_secret::StoreSecret::load_or_create(dir, true)?; let t = Self { body, - inwit, + seqsigwit, spent, create_loc, - inwit_loc, + seqsigwit_loc, head, txids, txstat, + input, secret, pending_head: pending_head::PendingHeadInserts::new(), rebuild_seal_bits: seal_bits, rebuild_workers: workers, - prune_inwit_mode: std::sync::atomic::AtomicBool::new(prune_inwit_mode), + prune_seqsigwit_mode: std::sync::atomic::AtomicBool::new(prune_seqsigwit_mode), }; + if t.input.count() == 0 && n_bodies > 0 && t.seqsigwit.count() == n_bodies { + t.backfill_inputs_from_seqsigwit()?; + } else if t.input.count() < n_bodies { + t.input.append_unstamped(n_bodies - t.input.count())?; + } else if t.input.count() > n_bodies { + return Err(StoreError::Corrupt( + "invariant: input.loc ahead of create.loc", + )); + } if n_bodies > 0 { - let _ = t.txstat.n_in_at(Fk(1))?; + let _ = t.input.n_in(Fk(1))?; } if need_rebuild { let bits = t.head_bits(); @@ -930,19 +1019,19 @@ impl TxTable { Ok(t) } - pub fn prune_inwit_mode(&self) -> bool { - self.prune_inwit_mode + pub fn prune_seqsigwit_mode(&self) -> bool { + self.prune_seqsigwit_mode .load(std::sync::atomic::Ordering::Acquire) } - pub fn set_prune_inwit_mode(&self, on: bool) { - self.prune_inwit_mode + pub fn set_prune_seqsigwit_mode(&self, on: bool) { + self.prune_seqsigwit_mode .store(on, std::sync::atomic::Ordering::Release); } - pub fn clear_durable_inwit(&self) -> Result<(), StoreError> { - self.inwit_loc.truncate_to_count(0)?; - self.inwit + pub fn clear_durable_seqsigwit(&self) -> Result<(), StoreError> { + self.seqsigwit_loc.truncate_to_count(0)?; + self.seqsigwit .truncate_body_to(0, crate::file::FILE_HEADER_LEN as u64) } @@ -1123,11 +1212,11 @@ impl TxTable { Ok(()) } - pub(crate) fn fill_inwit_job_ranges( + pub(crate) fn fill_seqsigwit_job_ranges( &self, jobs: &mut [crate::IdxBodyJob], ) -> Result<(), StoreError> { - if self.prune_inwit_mode() { + if self.prune_seqsigwit_mode() { return Ok(()); } let mut need = Vec::new(); @@ -1141,7 +1230,7 @@ impl TxTable { if need.is_empty() { return Ok(()); } - let pairs = self.inwit_loc.range_batch(&need)?; + let pairs = self.seqsigwit_loc.range_batch(&need)?; for (slot, p) in slots.into_iter().zip(pairs) { jobs[slot].range = p; } @@ -1194,21 +1283,13 @@ impl TxTable { /// Meta + input prevouts only (no script/witness allocation, no outputs). /// - /// Stamped `txstat` supplies `n_in` so this path is 8 B + inwit (no txout). - /// Leftover unstamped rows still decode LAYOUT17 uleb from `txout`. + /// Stamped rows read `n_in` and the parent edge from `input`. Leftover + /// unstamped rows still take the inline prevout out of legacy `seqsigwit`. pub fn get_meta_and_prevouts(&self, fk: Fk) -> Result<(TxRecord, Vec<(Fk, u32)>), StoreError> { - if self.prune_inwit_mode() { + if self.prune_seqsigwit_mode() { return Err(StoreError::NotFound); } - let ir = self - .inwit_loc - .range_batch(&[fk])? - .into_iter() - .next() - .flatten() - .ok_or(StoreError::NotFound)?; - let inwit = self.inwit.with_bytes_at(ir.0, ir.1, |b| Ok(b.to_vec()))?; - let mut tx = if let Some(n_in) = self.txstat.n_in_at(fk)? { + let mut tx = if let Some(n_in) = self.input.n_in(fk)? { TxRecord { txid: [0u8; 32], version: 0, @@ -1221,17 +1302,31 @@ impl TxTable { } else { self.get(fk)? }; - let prevs = scan_inwit_prevouts(&inwit, tx.input_count)?; + let prevs = if let Some(edges) = self.input.edges(fk)? { + prevouts_from_edges(&edges) + } else { + let ir = self + .seqsigwit_loc + .range_batch(&[fk])? + .into_iter() + .next() + .flatten() + .ok_or(StoreError::NotFound)?; + let seqsigwit = self + .seqsigwit + .with_bytes_at(ir.0, ir.1, |b| Ok(b.to_vec()))?; + scan_seqsigwit_prevouts(&seqsigwit, tx.input_count)? + }; tx.txid = self.txids.get(fk)?; Ok((tx, prevs)) } pub(crate) fn overlay_stamped_n_in(&self, fk: Fk, tx: &mut TxRecord) -> Result<(), StoreError> { - let Some(n_in) = self.txstat.n_in_at(fk)? else { + let Some(n_in) = self.input.n_in(fk)? else { return Ok(()); }; if tx.input_count != 0 && tx.input_count != n_in { - return Err(StoreError::Corrupt("txstat n_in mismatch txout")); + return Err(StoreError::Corrupt("input n_in mismatch txout")); } tx.input_count = n_in; Ok(()) @@ -1423,12 +1518,35 @@ impl TxTable { .collect()) } - /// `inwit.body` range for one create. - pub fn inwit_range(&self, fk: Fk) -> Result<(u64, u64), StoreError> { - if self.prune_inwit_mode() { + fn backfill_inputs_from_seqsigwit(&self) -> Result<(), StoreError> { + let n = self.create_loc.count(); + rbitcoin_log::info!("store: input backfill from seqsigwit n={n}"); + let mut batch = Vec::new(); + for id in 1..=n { + let (off, len) = self.seqsigwit_range(Fk(id))?; + let raw = self.seqsigwit.with_bytes_at(off, len, |b| Ok(b.to_vec()))?; + batch.push(edges_from_seqsigwit_payload(&raw)?); + if batch.len() == 1024 { + self.input.append(&batch)?; + batch.clear(); + } + if id.is_multiple_of(1_000_000) { + rbitcoin_log::info!("store: input backfill progress {id}/{n}"); + } + } + if !batch.is_empty() { + self.input.append(&batch)?; + } + rbitcoin_log::info!("store: input backfill complete n={n}"); + Ok(()) + } + + /// `seqsigwit.body` range for one create. + pub fn seqsigwit_range(&self, fk: Fk) -> Result<(u64, u64), StoreError> { + if self.prune_seqsigwit_mode() { return Err(StoreError::NotFound); } - self.inwit_loc + self.seqsigwit_loc .range_batch(&[fk])? .into_iter() .next() @@ -1759,12 +1877,12 @@ impl TxTable { Ok(()) } - /// Full tx: `txout` + `inwit` zip. + /// Full tx: `txout` + `seqsigwit` zip. pub fn get_full( &self, fk: Fk, ) -> Result<(TxRecord, Vec, Vec), StoreError> { - if self.prune_inwit_mode() { + if self.prune_seqsigwit_mode() { return Err(StoreError::NotFound); } let pair = self @@ -1779,27 +1897,41 @@ impl TxTable { let (mut tx, _ins, outs, _) = decode_packed_tx_with_spender_rels_secret(&raw, pair.n_out, Some(&self.secret))?; let ir = self - .inwit_loc + .seqsigwit_loc .range_batch(&[fk])? .into_iter() .next() .flatten() .ok_or(StoreError::NotFound)?; - let inwit = self.inwit.with_bytes_at(ir.0, ir.1, |b| Ok(b.to_vec()))?; - let ins = if tx.input_count == 0 { - decode_inwit_secret_to_end(&inwit, Some(&self.secret))? + let seqsigwit = self + .seqsigwit + .with_bytes_at(ir.0, ir.1, |b| Ok(b.to_vec()))?; + let mut ins = if tx.input_count == 0 { + decode_seqsigwit_secret_to_end(&seqsigwit, Some(&self.secret))? } else { - decode_inwit_secret(&inwit, tx.input_count, Some(&self.secret))? + decode_seqsigwit_secret(&seqsigwit, tx.input_count, Some(&self.secret))? }; + self.stamp_seqsigwit_prevouts(fk, &mut ins)?; tx.input_count = ins.len() as u32; tx.txid = self.txids.get(fk)?; Ok((tx, ins, outs)) } + pub(crate) fn stamp_seqsigwit_prevouts( + &self, + fk: Fk, + ins: &mut [InputRecord], + ) -> Result<(), StoreError> { + let Some(edges) = self.input.edges(fk)? else { + return Ok(()); + }; + apply_input_edges(ins, &edges) + } + /// Contiguous create_fks `first..=last`: one libc span each of `txout.body` - /// and `inwit.body`, plus `txid.body` range. Not the confirm uring pipeline. + /// and `seqsigwit.body`, plus `txid.body` range. Not the confirm uring pipeline. pub fn get_full_span(&self, first: u64, last: u64) -> Result, StoreError> { - if self.prune_inwit_mode() { + if self.prune_seqsigwit_mode() { return Err(StoreError::NotFound); } if first == 0 { @@ -1818,12 +1950,12 @@ impl TxTable { txout_ranges.push(p.txout); n_outs.push(p.n_out); } - let inwit_pairs = self.inwit_loc.range_batch(&fks)?; - let mut inwit_ranges = Vec::with_capacity(n); - for p in inwit_pairs { - inwit_ranges.push(p.ok_or(StoreError::NotFound)?); + let seqsigwit_pairs = self.seqsigwit_loc.range_batch(&fks)?; + let mut seqsigwit_ranges = Vec::with_capacity(n); + for p in seqsigwit_pairs { + seqsigwit_ranges.push(p.ok_or(StoreError::NotFound)?); } - if txout_ranges.len() != n || inwit_ranges.len() != n { + if txout_ranges.len() != n || seqsigwit_ranges.len() != n { return Err(StoreError::Corrupt("invariant: full span range count")); } let (t0, _) = txout_ranges[0]; @@ -1832,31 +1964,32 @@ impl TxTable { .checked_add(tln) .and_then(|end| end.checked_sub(t0)) .ok_or(StoreError::Corrupt("txout span"))?; - let (i0, _) = inwit_ranges[0]; - let (inn, iln) = inwit_ranges[n - 1]; + let (i0, _) = seqsigwit_ranges[0]; + let (inn, iln) = seqsigwit_ranges[n - 1]; let ispan = inn .checked_add(iln) .and_then(|end| end.checked_sub(i0)) - .ok_or(StoreError::Corrupt("inwit span"))?; + .ok_or(StoreError::Corrupt("seqsigwit span"))?; let ids = self.txids.get_range(first, last)?; if ids.len() != n { return Err(StoreError::Corrupt("invariant: txid.body span length")); } - let (txout_span, inwit_span) = - pread_two_spans(&self.body, t0, tspan, &self.inwit, i0, ispan, true)?; + let (txout_span, seqsigwit_span) = + pread_two_spans(&self.body, t0, tspan, &self.seqsigwit, i0, ispan, true)?; let mut out = Vec::with_capacity(n); for i in 0..n { let (toff, tlen) = txout_ranges[i]; - let (ioff, ilen) = inwit_ranges[i]; + let (ioff, ilen) = seqsigwit_ranges[i]; let traw = span_rec(&txout_span, t0, toff, tlen)?; - let iraw = span_rec(&inwit_span, i0, ioff, ilen)?; + let iraw = span_rec(&seqsigwit_span, i0, ioff, ilen)?; let (mut tx, _ins, outs, _) = decode_packed_tx_with_spender_rels_secret(traw, n_outs[i], Some(&self.secret))?; - let ins = if tx.input_count == 0 { - decode_inwit_secret_to_end(iraw, Some(&self.secret))? + let mut ins = if tx.input_count == 0 { + decode_seqsigwit_secret_to_end(iraw, Some(&self.secret))? } else { - decode_inwit_secret(iraw, tx.input_count, Some(&self.secret))? + decode_seqsigwit_secret(iraw, tx.input_count, Some(&self.secret))? }; + self.stamp_seqsigwit_prevouts(Fk(first + i as u64), &mut ins)?; tx.input_count = ins.len() as u32; tx.txid = ids[i]; out.push((tx, ins, outs)); @@ -1953,7 +2086,7 @@ impl TxTable { Ok(()) } - /// Append Class A rows: `txout` + `inwit` + zero `spent` + `txid.body` + `txstat.body`. + /// Append Class A rows: `txout` + `seqsigwit` + zero `spent` + `txid.body` + `txstat.body`. pub fn put_full_batch_indexed( &self, items: &[(TxRecord, Vec, Vec)], @@ -1981,7 +2114,7 @@ impl TxTable { 16 + TxRecord::BODY_META_LEN + outs.iter().map(|o| o.encoded_len()).sum::() }) .sum(); - let est_inwit: usize = items + let est_seqsigwit: usize = items .iter() .map(|(_tx, ins, _outs)| 16 + ins.iter().map(|i| i.encoded_len()).sum::()) .sum(); @@ -1990,9 +2123,10 @@ impl TxTable { .map(|(_tx, _ins, outs)| 16 + outs.len() * OutputRecord::SPENT_SLOT_LEN) .sum(); let base = self.body.count(); - if (!self.prune_inwit_mode() && self.inwit.count() != base) + if (!self.prune_seqsigwit_mode() && self.seqsigwit.count() != base) || self.spent.count() != base || self.txstat.count() != base + || self.input.count() != base { return Err(StoreError::Corrupt("Class A stem count mismatch on append")); } @@ -2009,14 +2143,14 @@ impl TxTable { let (fks, _loc) = self.append_stems_one_wave( items.len(), est_out, - est_inwit, + est_seqsigwit, est_spent, &n_outs, |i, buf| { let (tx, ins, outs) = &items[i]; encode_packed_tx_with_secret(tx, ins, outs, buf, Some(&self.secret)); }, - |i, buf| encode_inwit_with_secret(&items[i].1, buf, Some(&self.secret)), + |i, buf| encode_seqsigwit_with_secret(&items[i].1, buf, Some(&self.secret)), |i, buf| encode_spent_zeros(items[i].2.len() as u32, buf), )?; let ids: Vec<[u8; 32]> = items.iter().map(|(tx, _, _)| tx.txid).collect(); @@ -2025,6 +2159,9 @@ impl TxTable { if !tails.is_empty() { return Err(StoreError::Corrupt("txstat overflow needs header blob")); } + let edges: Vec> = + items.iter().map(|(_, ins, _)| input_edges(ins)).collect(); + self.input.append(&edges)?; if index { let heads: Vec<([u8; 32], Fk)> = items .iter() @@ -2048,10 +2185,8 @@ impl TxTable { index: bool, spent_overlay: &[Vec<(u32, Fk, u32)>], ) -> Result<(Vec, Vec), StoreError> { - let rows: Vec = items - .iter() - .map(|(pin, _)| txstat_placeholder(pin.packed_tx().input_count)) - .collect(); + let rows: Vec = + items.iter().map(|_| txstat_placeholder()).collect(); self.put_full_batch_from_pins_with_txstat(items, index, spent_overlay, &rows, &[]) } @@ -2073,7 +2208,7 @@ impl TxTable { return Err(StoreError::Corrupt("spent overlay length")); } let est_out: usize = items.iter().map(|(pin, _ins)| pin.packed_outs_est()).sum(); - let est_inwit: usize = items + let est_seqsigwit: usize = items .iter() .map(|(_pin, ins)| 16 + ins.iter().map(|i| i.encoded_len()).sum::()) .sum(); @@ -2082,9 +2217,10 @@ impl TxTable { .map(|(pin, _ins)| 16 + spent_record_len(pin.packed_n_out()) as usize) .sum(); let base = self.body.count(); - if (!self.prune_inwit_mode() && self.inwit.count() != base) + if (!self.prune_seqsigwit_mode() && self.seqsigwit.count() != base) || self.spent.count() != base || self.txstat.count() != base + || self.input.count() != base { return Err(StoreError::Corrupt("Class A stem count mismatch on append")); } @@ -2108,13 +2244,13 @@ impl TxTable { let (fks, loc) = self.append_stems_one_wave( items.len(), est_out, - est_inwit, + est_seqsigwit, est_spent, &n_outs, |i, buf| { items[i].0.encode_txout_body(buf, Some(&self.secret)); }, - |i, buf| encode_inwit_with_secret(&items[i].1, buf, Some(&self.secret)), + |i, buf| encode_seqsigwit_with_secret(&items[i].1, buf, Some(&self.secret)), |i, buf| { let n_out = items[i].0.packed_n_out(); let pairs = spent_overlay.get(i).map(|v| v.as_slice()).unwrap_or(&[]); @@ -2126,6 +2262,9 @@ impl TxTable { let tails = self.txstat.append_batch(base, txstat)?; self.txstat .put_overflows_for_headers(header_ranges, &tails)?; + let edges: Vec> = + items.iter().map(|(_, ins)| input_edges(ins)).collect(); + self.input.append(&edges)?; if index { let heads: Vec<([u8; 32], Fk)> = items .iter() @@ -2138,7 +2277,7 @@ impl TxTable { } #[allow(clippy::too_many_arguments)] // IO/session args stay unbundled - /// Encode and write `txout` + `inwit` + `spent` bodies as one pwrite wave. + /// Encode and write `txout` + `seqsigwit` + `spent` bodies as one pwrite wave. /// /// Order is still body → loc → HWM per stem. Not the spend-annotate machine. /// Loc pairs are the append starts (write keeps them in RAM; no loc pread). @@ -2146,7 +2285,7 @@ impl TxTable { &self, n: usize, est_out: usize, - est_inwit: usize, + est_seqsigwit: usize, est_spent: usize, n_outs: &[u32], encode_out: impl FnMut(usize, &mut Vec), @@ -2180,7 +2319,7 @@ impl TxTable { }); } - if self.prune_inwit_mode() { + if self.prune_seqsigwit_mode() { crate::var_table::write_prepared_bodies_one_wave(&[ (&self.body, &p_out), (&self.spent, &p_sp), @@ -2196,18 +2335,22 @@ impl TxTable { return Ok((fks, loc)); } - let Some(p_in) = self.inwit.prepare_batch_encode(n, est_inwit, encode_in)? else { - return Err(StoreError::Corrupt("Class A inwit prepare empty")); + let Some(p_in) = self + .seqsigwit + .prepare_batch_encode(n, est_seqsigwit, encode_in)? + else { + return Err(StoreError::Corrupt("Class A seqsigwit prepare empty")); }; crate::var_table::write_prepared_bodies_one_wave(&[ (&self.body, &p_out), - (&self.inwit, &p_in), + (&self.seqsigwit, &p_in), (&self.spent, &p_sp), ])?; self.create_loc.append(&recs)?; - self.inwit_loc.append(&p_in.starts, &p_in.aligned_lens())?; + self.seqsigwit_loc + .append(&p_in.starts, &p_in.aligned_lens())?; let fks = self.body.finish_prepared(p_out)?; - let fks_in = self.inwit.finish_prepared(p_in)?; + let fks_in = self.seqsigwit.finish_prepared(p_in)?; let fks_sp = self.spent.finish_prepared(p_sp)?; if fks != fks_in || fks != fks_sp { return Err(StoreError::Corrupt( @@ -2632,7 +2775,7 @@ impl TxTable { pub fn flush(&self) -> Result<(), StoreError> { self.body.flush()?; - self.inwit.flush()?; + self.seqsigwit.flush()?; self.spent.flush()?; self.head.flush()?; Ok(()) @@ -2640,7 +2783,7 @@ impl TxTable { pub fn flush_async(&self) -> Result<(), StoreError> { self.body.flush_async()?; - self.inwit.flush_async()?; + self.seqsigwit.flush_async()?; self.spent.flush_async()?; self.head.flush_async()?; Ok(()) diff --git a/crates/rbitcoin-store/src/tx_table/packed.rs b/crates/rbitcoin-store/src/tx_table/packed.rs index b30dd3ec7..24fca7318 100644 --- a/crates/rbitcoin-store/src/tx_table/packed.rs +++ b/crates/rbitcoin-store/src/tx_table/packed.rs @@ -102,14 +102,15 @@ impl PackedCreate for std::sync::Arc<(TxRecord, Vec)> { } } -/// Class A input + BIP141 witness (schema v10). +/// Class A input + BIP141 witness. /// -/// On-disk prevout: -/// - coinbase: `NULL_PREV` (no payload) -/// - non-coinbase: **`create_fk:u64` LE** + CompactSize `vout` (not prev_txid) +/// `seqsigwit` stores sequence, scriptSig, and witness. The parent edge is +/// `input.body`. New records set [`input_flags::PREV_ON_INPUTS`] and omit +/// `create_fk` and vout. A record without that bit is the legacy layout: +/// coinbase `NULL_PREV`, otherwise `create_fk:u64` LE plus CompactSize vout. /// -/// [`Self::prev_txid`] is a soft cache for wire rebuild (zeros until filled from -/// the create body or from the wire convert path). Encoding never writes it. +/// [`Self::prev_txid`] is a soft cache for wire rebuild. Encoding never writes it. +/// [`Self::create_fk`] on a new record is filled from `input.body` after decode. #[derive(Clone, Debug, PartialEq, Eq)] pub struct InputRecord { /// Soft: wire txid of parent create (`[0;32]` if unknown / coinbase). @@ -141,8 +142,7 @@ impl InputRecord { } pub fn encode_into(&self, out: &mut Vec) { - let null_prev = self.create_fk.is_null() && self.prev_index == u32::MAX; - let mut flags = 0u8; + let mut flags = input_flags::PREV_ON_INPUTS; if self.sequence == u32::MAX { flags |= input_flags::SEQ_FINAL; } @@ -152,18 +152,7 @@ impl InputRecord { if self.witness.is_empty() { flags |= input_flags::EMPTY_WITNESS; } - if null_prev { - flags |= input_flags::NULL_PREV; - } out.push(flags); - if !null_prev { - debug_assert!( - !self.create_fk.is_null(), - "non-coinbase input requires create_fk before encode" - ); - out.extend_from_slice(&self.create_fk.0.to_le_bytes()); - write_compact_size(out, u64::from(self.prev_index)); - } if flags & input_flags::SEQ_FINAL == 0 { out.extend_from_slice(&self.sequence.to_le_bytes()); } @@ -186,62 +175,21 @@ impl InputRecord { out } - /// Skip past one input after reading create_fk + vout (no script/witness alloc). + /// Skip past one legacy input after reading its inline prevout. /// /// Returns `(create_fk, prev_index, bytes_consumed)`. Coinbase → `(NULL, u32::MAX, …)`. + /// A [`input_flags::PREV_ON_INPUTS`] record has no prevout here. pub fn decode_prevout_at(buf: &[u8]) -> Result<(Fk, u32, usize), StoreError> { if buf.is_empty() { return Err(StoreError::Corrupt("short input record")); } let flags = buf[0]; let mut off = 1usize; - check_inwit_flags(flags)?; - let (create_fk, prev_index) = if flags & input_flags::NULL_PREV != 0 { - (Fk::NULL, u32::MAX) - } else { - if buf.len() < off + 8 { - return Err(StoreError::Corrupt("input create_fk truncated")); - } - let id = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap()); - off += 8; - if id == 0 { - return Err(StoreError::Corrupt("non-coinbase create_fk is null")); - } - let (vout, n) = read_compact_size(&buf[off..])?; - off += n; - if vout > u64::from(u32::MAX) { - return Err(StoreError::Corrupt("prev_index too large")); - } - (Fk(id), vout as u32) + check_seqsigwit_flags(flags)?; + let Some((create_fk, prev_index)) = read_inline_prevout(buf, flags, &mut off)? else { + return Err(StoreError::Corrupt("seqsigwit prevout is on inputs")); }; - if flags & input_flags::SEQ_FINAL == 0 { - if buf.len() < off + 4 { - return Err(StoreError::Corrupt("input sequence truncated")); - } - off += 4; - } - if flags & input_flags::EMPTY_SCRIPT == 0 { - let (slen, n) = read_compact_size(&buf[off..])?; - off += n; - let slen = slen as usize; - if buf.len() < off + slen { - return Err(StoreError::Corrupt("input script truncated")); - } - off += slen; - } - if flags & input_flags::EMPTY_WITNESS == 0 { - let (nw, n) = read_compact_size(&buf[off..])?; - off += n; - for _ in 0..nw { - let (ilen, n) = read_compact_size(&buf[off..])?; - off += n; - let ilen = ilen as usize; - if buf.len() < off + ilen { - return Err(StoreError::Corrupt("witness item truncated")); - } - off += ilen; - } - } + skip_seq_script_witness(buf, flags, &mut off)?; Ok((create_fk, prev_index, off)) } @@ -254,24 +202,10 @@ impl InputRecord { } let flags = buf[0]; let mut off = 1usize; - check_inwit_flags(flags)?; - let (create_fk, prev_index) = if flags & input_flags::NULL_PREV != 0 { - (Fk::NULL, u32::MAX) - } else { - if buf.len() < off + 8 { - return Err(StoreError::Corrupt("input create_fk truncated")); - } - let id = u64::from_le_bytes(buf[off..off + 8].try_into().unwrap()); - off += 8; - if id == 0 { - return Err(StoreError::Corrupt("non-coinbase create_fk is null")); - } - let (vout, n) = read_compact_size(&buf[off..])?; - off += n; - if vout > u64::from(u32::MAX) { - return Err(StoreError::Corrupt("prev_index too large")); - } - (Fk(id), vout as u32) + check_seqsigwit_flags(flags)?; + let (create_fk, prev_index) = match read_inline_prevout(buf, flags, &mut off)? { + Some(pair) => pair, + None => (Fk::NULL, 0), }; let sequence = if flags & input_flags::SEQ_FINAL != 0 { u32::MAX @@ -350,12 +284,7 @@ impl InputRecord { #[inline] pub fn encoded_len_exact(&self) -> usize { use rbitcoin_primitives::compact_size_len; - let null_prev = self.create_fk.is_null() && self.prev_index == u32::MAX; let mut n = 1usize; - if !null_prev { - n += 8; - n += compact_size_len(u64::from(self.prev_index)); - } if self.sequence != u32::MAX { n += 4; } @@ -398,8 +327,8 @@ pub(super) fn xor_script_regions_in_input( } let flags = buf[start]; let mut off = start + 1; - let null_prev = flags & input_flags::NULL_PREV != 0; - if !null_prev { + let inline_prev = flags & (input_flags::NULL_PREV | input_flags::PREV_ON_INPUTS) == 0; + if inline_prev { if off + 8 > buf.len() { return; } @@ -528,7 +457,7 @@ pub const BODY_PAGE_SIZE: u64 = 4096; /// Encode `txout.body` payload (schema **17**): thin meta || output_run. /// -/// Inputs/witness go to [`encode_inwit_with_secret`]. Spender slots go to +/// Inputs/witness go to [`encode_seqsigwit_with_secret`]. Spender slots go to /// [`encode_spent_zeros`]. `inputs` is accepted for call-site compatibility /// (count assert only). pub fn encode_packed_tx( @@ -568,8 +497,8 @@ pub fn encode_txout_meta_and_outs( encode_output_run_secret(outputs, out, secret); } -/// Encode `inwit.body` payload (input-side + witness). -pub fn encode_inwit_with_secret( +/// Encode `seqsigwit.body` payload (input-side + witness). +pub fn encode_seqsigwit_with_secret( inputs: &[InputRecord], out: &mut Vec, secret: Option<&crate::store_secret::StoreSecret>, @@ -614,13 +543,129 @@ pub fn spent_record_len(n_out: u32) -> u64 { const SPENT_FK_U40_MAX: u64 = (1u64 << 40) - 1; const SPENT_VIN_U16_MAX: u32 = (1u32 << 16) - 1; -fn check_inwit_flags(flags: u8) -> Result<(), StoreError> { - if flags & (input_flags::RESERVED4 | input_flags::RESERVED_HIGH) != 0 { - return Err(StoreError::Corrupt("inwit reserved flags")); +fn check_seqsigwit_flags(flags: u8) -> Result<(), StoreError> { + if flags & input_flags::RESERVED_HIGH != 0 { + return Err(StoreError::Corrupt("seqsigwit reserved flags")); + } + Ok(()) +} + +fn skip_seq_script_witness(buf: &[u8], flags: u8, off: &mut usize) -> Result<(), StoreError> { + skip_sequence(buf, flags, off)?; + skip_script(buf, flags, off)?; + skip_witness(buf, flags, off) +} + +fn skip_sequence(buf: &[u8], flags: u8, off: &mut usize) -> Result<(), StoreError> { + if flags & input_flags::SEQ_FINAL != 0 { + return Ok(()); + } + if buf.len() < *off + 4 { + return Err(StoreError::Corrupt("input sequence truncated")); + } + *off += 4; + Ok(()) +} + +fn skip_script(buf: &[u8], flags: u8, off: &mut usize) -> Result<(), StoreError> { + if flags & input_flags::EMPTY_SCRIPT != 0 { + return Ok(()); + } + let (slen, n) = read_compact_size(&buf[*off..])?; + *off += n; + let slen = slen as usize; + if buf.len() < *off + slen { + return Err(StoreError::Corrupt("input script truncated")); + } + *off += slen; + Ok(()) +} + +fn skip_witness(buf: &[u8], flags: u8, off: &mut usize) -> Result<(), StoreError> { + if flags & input_flags::EMPTY_WITNESS != 0 { + return Ok(()); + } + let (nw, n) = read_compact_size(&buf[*off..])?; + *off += n; + for _ in 0..nw { + skip_witness_item(buf, off)?; + } + Ok(()) +} + +fn skip_witness_item(buf: &[u8], off: &mut usize) -> Result<(), StoreError> { + let (ilen, n) = read_compact_size(&buf[*off..])?; + *off += n; + let ilen = ilen as usize; + if buf.len() < *off + ilen { + return Err(StoreError::Corrupt("witness item truncated")); + } + *off += ilen; + Ok(()) +} + +/// `Some` is a legacy inline prevout. `None` means [`input_flags::PREV_ON_INPUTS`]. +fn read_inline_prevout( + buf: &[u8], + flags: u8, + off: &mut usize, +) -> Result, StoreError> { + if flags & input_flags::PREV_ON_INPUTS != 0 { + return Ok(None); + } + if flags & input_flags::NULL_PREV != 0 { + return Ok(Some((Fk::NULL, u32::MAX))); + } + if buf.len() < *off + 8 { + return Err(StoreError::Corrupt("input create_fk truncated")); + } + let id = u64::from_le_bytes(buf[*off..*off + 8].try_into().unwrap()); + *off += 8; + if id == 0 { + return Err(StoreError::Corrupt("non-coinbase create_fk is null")); + } + let (vout, n) = read_compact_size(&buf[*off..])?; + *off += n; + if vout > u64::from(u32::MAX) { + return Err(StoreError::Corrupt("prev_index too large")); + } + Ok(Some((Fk(id), vout as u32))) +} + +/// Fill `create_fk` and `prev_index` from `input.body`. Coinbase edge → null prevout. +pub fn apply_input_edges( + ins: &mut [InputRecord], + edges: &[crate::input::InputEdge], +) -> Result<(), StoreError> { + if ins.len() != edges.len() { + return Err(StoreError::Corrupt("input edge count")); + } + for (inp, edge) in ins.iter_mut().zip(edges.iter()) { + if edge.parent.is_null() { + inp.create_fk = Fk::NULL; + inp.prev_index = u32::MAX; + } else { + inp.create_fk = edge.parent; + inp.prev_index = edge.vout; + } } Ok(()) } +/// Prevouts in the shape [`scan_seqsigwit_prevouts`] returns. Coinbase → `(NULL, u32::MAX)`. +pub fn prevouts_from_edges(edges: &[crate::input::InputEdge]) -> Vec<(Fk, u32)> { + edges + .iter() + .map(|e| { + if e.parent.is_null() { + (Fk::NULL, u32::MAX) + } else { + (e.parent, e.vout) + } + }) + .collect() +} + fn check_spent_flags(flags: u8) -> Result<(), StoreError> { if flags & !output_flags::MULTI_SPENDER != 0 { return Err(StoreError::Corrupt("spent reserved flags")); @@ -677,14 +722,14 @@ pub fn spent_abs(spent_off: u64, vout: u32) -> u64 { spent_off.saturating_add(u64::from(vout).saturating_mul(OutputRecord::SPENT_SLOT_LEN as u64)) } -/// Decode `inwit.body` payload into input records (script_sig + witness). -pub fn decode_inwit_secret( +/// Decode `seqsigwit.body` payload into input records (script_sig + witness). +pub fn decode_seqsigwit_secret( raw: &[u8], in_count: u32, secret: Option<&crate::store_secret::StoreSecret>, ) -> Result, StoreError> { if in_count == 0 { - return decode_inwit_secret_to_end(raw, secret); + return decode_seqsigwit_secret_to_end(raw, secret); } let (mut inputs, used) = decode_input_run_prefix(raw, in_count)?; check_trailing_zero_pad(raw, used)?; @@ -701,7 +746,7 @@ pub fn decode_inwit_secret( Ok(inputs) } -pub(crate) fn decode_inwit_secret_to_end( +pub(crate) fn decode_seqsigwit_secret_to_end( raw: &[u8], secret: Option<&crate::store_secret::StoreSecret>, ) -> Result, StoreError> { @@ -777,10 +822,10 @@ pub fn txout_first_page_covers_need(raw: &[u8], n_out: u32, need_vouts: &[u32]) take_all || need_i == need_vouts.len() } -/// Prevout edges from an `inwit.body` payload (`in_count` from `txout` meta). +/// Prevout edges from an `seqsigwit.body` payload (`in_count` from `txout` meta). /// /// Each edge is `(create_fk, vout)`; coinbase → `(Fk::NULL, u32::MAX)`. -pub fn scan_inwit_prevouts(raw: &[u8], in_count: u32) -> Result, StoreError> { +pub fn scan_seqsigwit_prevouts(raw: &[u8], in_count: u32) -> Result, StoreError> { let mut off = 0usize; let mut prevouts = Vec::with_capacity(in_count as usize); for _ in 0..in_count { @@ -1060,14 +1105,14 @@ mod scan_p2tr_tests { } #[test] - fn decode_inwit_secret_zero_count_walks_payload() { + fn decode_seqsigwit_secret_zero_count_walks_payload() { let ins = vec![InputRecord::coinbase(u32::MAX, vec![0x01, 0x02], vec![])]; let mut raw = Vec::new(); - encode_inwit_with_secret(&ins, &mut raw, None); - let got = decode_inwit_secret(&raw, 0, None).unwrap(); + encode_seqsigwit_with_secret(&ins, &mut raw, None); + let got = decode_seqsigwit_secret(&raw, 0, None).unwrap(); assert_eq!(got.len(), 1); assert_eq!(got[0].script_sig, vec![0x01, 0x02]); - assert!(got[0].is_coinbase()); + assert!(!got[0].is_coinbase()); } fn three_out_packed() -> (Vec, usize) { diff --git a/crates/rbitcoin-store/src/tx_table/tests.rs b/crates/rbitcoin-store/src/tx_table/tests.rs index 88418f569..e3a8b1abc 100644 --- a/crates/rbitcoin-store/src/tx_table/tests.rs +++ b/crates/rbitcoin-store/src/tx_table/tests.rs @@ -132,10 +132,13 @@ fn open_refuses_txout_without_peer_stems() { t.put_full_batch_indexed(&meta_only_items(&[rec]), true) .unwrap(); } - let _ = std::fs::remove_file(dir.join("inwit.body")); + let _ = std::fs::remove_file(dir.join("seqsigwit.body")); match TxTable::open_tiny(&dir) { - Ok(_) => panic!("missing inwit must refuse"), - Err(err) => assert!(format!("{err}").contains("missing inwit/spent"), "{err}"), + Ok(_) => panic!("missing seqsigwit must refuse"), + Err(err) => assert!( + format!("{err}").contains("missing seqsigwit/spent"), + "{err}" + ), } let _ = std::fs::remove_dir_all(&dir); } @@ -190,13 +193,9 @@ fn put_full_batch_writes_txstat_and_reopen() { assert_eq!(t.txstat.count(), 1); assert_eq!( crate::txstat::parse_cell(t.txstat.get_cell(Fk(1)).unwrap()).unwrap(), - crate::txstat::CellParse::Complete(crate::txstat::TxStatRow { - n_in: 1, - fee_sat: 0, - base: 0, - wit_extra: 0, - }) + crate::txstat::CellParse::Unstamped ); + assert_eq!(t.input.n_in(Fk(1)).unwrap(), Some(1)); drop(t); let t2 = TxTable::open_tiny(&dir).unwrap(); assert_eq!(t2.count(), 1); @@ -204,13 +203,9 @@ fn put_full_batch_writes_txstat_and_reopen() { assert_eq!(t2.txstat.count(), 1); assert_eq!( crate::txstat::parse_cell(t2.txstat.get_cell(Fk(1)).unwrap()).unwrap(), - crate::txstat::CellParse::Complete(crate::txstat::TxStatRow { - n_in: 1, - fee_sat: 0, - base: 0, - wit_extra: 0, - }) + crate::txstat::CellParse::Unstamped ); + assert_eq!(t2.input.n_in(Fk(1)).unwrap(), Some(1)); let _ = std::fs::remove_dir_all(&dir); } @@ -254,6 +249,15 @@ fn prevouts_use_txstat_n_in_without_full_txout() { assert_eq!(prevs.len(), 2); assert_eq!(prevs[0], (Fk::NULL, u32::MAX)); assert_eq!(prevs[1], (Fk(1), 0)); + let edges = t.input.edges(fk).unwrap().unwrap(); + assert_eq!(edges[0], crate::input::InputEdge::coinbase()); + assert_eq!( + edges[1], + crate::input::InputEdge { + parent: Fk(1), + vout: 0, + } + ); let _ = std::fs::remove_dir_all(&dir); } @@ -291,7 +295,7 @@ fn new_create_txout_meta_omits_n_in() { assert_eq!(n, 1, "v1 locktime 0 omit n_in is one flag byte"); assert_eq!(meta.input_count, 0); let (got, gouts) = t.get_meta_and_outputs(fk).unwrap(); - assert_eq!(got.input_count, 1, "stamped txstat fills n_in"); + assert_eq!(got.input_count, 1, "input.loc fills n_in"); assert_eq!(gouts.len(), 1); let leftover = [0x89u8, 0x01]; let (old, on) = decode_body_meta_v17(&leftover).unwrap(); @@ -301,34 +305,45 @@ fn new_create_txout_meta_omits_n_in() { } #[test] -fn txstat_n_in_mismatch_inwit_is_corrupt() { - let dir = tempfile_dir("txstat-nin-mismatch"); +fn reopen_without_inputs_refuses_new_seqsigwit() { + let dir = tempfile_dir("input-backfill"); + let t = create_tiny(&dir); + let _fk = t.put_full_batch_indexed(&[two_input_item()], true).unwrap()[0]; + drop(t); + for name in ["input.loc", "input.off", "input.body"] { + std::fs::remove_file(dir.join(name)).unwrap(); + } + match TxTable::open_tiny(&dir) { + Err(StoreError::Corrupt("seqsigwit prevout is on inputs")) => {} + Err(e) => panic!("open without inputs: {e}"), + Ok(_) => panic!("open without inputs recovered a new seqsigwit"), + } + let _ = std::fs::remove_dir_all(&dir); +} + +#[test] +fn input_n_in_scans_both_prevouts() { + let dir = tempfile_dir("input-nin-prevouts"); let t = create_tiny(&dir); let fk = t.put_full_batch_indexed(&[two_input_item()], true).unwrap()[0]; t.txstat .write_row( fk, &crate::txstat::TxStatRow { - n_in: 1, fee_sat: 0, base: 0, wit_extra: 0, }, ) .unwrap(); - match t.get_meta_and_prevouts(fk) { - Err(StoreError::Corrupt(m)) => { - assert!( - m.contains("trailing") || m.contains("short") || m.contains("mismatch"), - "{m}" - ); - } - other => panic!("expected Corrupt, got {other:?}"), - } + let (meta, prevs) = t.get_meta_and_prevouts(fk).unwrap(); + assert_eq!(meta.input_count, 2); + assert_eq!(prevs.len(), 2); + assert_eq!(t.input.n_in(fk).unwrap(), Some(2)); let _ = std::fs::remove_dir_all(&dir); } -/// Class A append submits txout+inwit+spent bodies as one pwrite wave (not 3 serial). +/// Class A append submits txout+seqsigwit+spent bodies as one pwrite wave (not 3 serial). #[test] fn put_full_batch_one_body_write_wave() { let dir = tempfile_dir("one-wave"); @@ -616,14 +631,23 @@ fn decode_prevout_at_skips_script_and_witness() { witness: vec![vec![0x30; 70], vec![0x21; 33]], }; let enc = rec.encode(); - let (cfk, vout, used) = InputRecord::decode_prevout_at(&enc).unwrap(); - assert_eq!(cfk, Fk(1)); - assert_eq!(vout, 3); + assert!(matches!( + InputRecord::decode_prevout_at(&enc), + Err(StoreError::Corrupt("seqsigwit prevout is on inputs")) + )); + let (full, used) = InputRecord::decode_at(&enc).unwrap(); assert_eq!(used, enc.len()); - // Full decode still matches. - let (full, used2) = InputRecord::decode_at(&enc).unwrap(); - assert_eq!(used2, used); assert_eq!(full.script_sig.len(), 40); + assert_eq!(full.sequence, 0xffff_fffe); + assert!(full.create_fk.is_null()); + let mut legacy = + vec![input_flags::SEQ_FINAL | input_flags::EMPTY_SCRIPT | input_flags::EMPTY_WITNESS]; + legacy.extend_from_slice(&1u64.to_le_bytes()); + legacy.push(3); + let (cfk, vout, used) = InputRecord::decode_prevout_at(&legacy).unwrap(); + assert_eq!(cfk, Fk(1)); + assert_eq!(vout, 3); + assert_eq!(used, legacy.len()); } /// v10: non-coinbase prev is create_fk(8) + vout, not prev_txid(32) (−24 B). @@ -638,14 +662,20 @@ fn input_encode_create_fk_not_prev_txid() { witness: vec![], }; let enc = rec.encode(); - // flags(1) + create_fk(8) + compact vout(1 for 7) = 10 - assert_eq!(enc.len(), 10, "enc={:?}", enc); - // v9 would have been flags + 32-byte txid + vout = 34 for same case - assert!(enc.len() + 24 <= 34); + assert_eq!( + enc.len(), + 1, + "seqsigwit stores no parent outpoint, enc={enc:?}" + ); + assert_eq!( + enc[0] & input_flags::PREV_ON_INPUTS, + input_flags::PREV_ON_INPUTS + ); let dec = InputRecord::decode(&enc).unwrap(); - assert_eq!(dec.create_fk, Fk(42)); - assert_eq!(dec.prev_index, 7); + assert!(dec.create_fk.is_null()); + assert_eq!(dec.prev_index, 0); assert_eq!(dec.prev_txid, [0u8; 32]); + assert_eq!(dec.sequence, u32::MAX); } #[test] @@ -683,12 +713,23 @@ fn scan_packed_meta_and_prevouts_no_output_alloc() { let (meta, _) = TxRecord::decode_body_meta(&raw).unwrap(); assert_eq!(meta.txid, [0u8; 32], "body scan has no leading txid"); assert_eq!(meta.input_count, 0, "txout meta omits n_in"); - let mut inwit = Vec::new(); - encode_inwit_with_secret(&inputs, &mut inwit, None); - let prevouts = scan_inwit_prevouts(&inwit, tx.input_count).unwrap(); - assert_eq!(prevouts.len(), 2); - assert_eq!(prevouts[0], (Fk::NULL, u32::MAX)); - assert_eq!(prevouts[1], (Fk(1), 1)); + let mut seqsigwit = Vec::new(); + encode_seqsigwit_with_secret(&inputs, &mut seqsigwit, None); + assert!(matches!( + scan_seqsigwit_prevouts(&seqsigwit, tx.input_count), + Err(StoreError::Corrupt("seqsigwit prevout is on inputs")) + )); + let edges = vec![ + crate::input::InputEdge::coinbase(), + crate::input::InputEdge { + parent: Fk(1), + vout: 1, + }, + ]; + assert_eq!( + prevouts_from_edges(&edges), + vec![(Fk::NULL, u32::MAX), (Fk(1), 1)] + ); } #[test] @@ -914,8 +955,8 @@ fn body_txid_thin_prefix_matches_fat_packed_body() { .unwrap()[0]; let from_thin = t.body_txid(fk).unwrap(); assert_eq!(from_thin, txid, "sidefile thin identity"); - let (_off, len) = t.inwit_range(fk).unwrap(); - assert!(len > 50_000, "inwit should hold the fat witness"); + let (_off, len) = t.seqsigwit_range(fk).unwrap(); + assert!(len > 50_000, "seqsigwit should hold the fat witness"); // Head resolve still works. assert_eq!(t.probe_body_match_fk(&txid).unwrap(), Some(fk)); let _ = std::fs::remove_dir_all(&dir); @@ -1710,8 +1751,8 @@ fn input_witness_roundtrip() { }; let enc = rec.encode(); let dec = InputRecord::decode(&enc).unwrap(); - assert_eq!(dec.create_fk, Fk(1)); - assert_eq!(dec.prev_index, 2); + assert!(dec.create_fk.is_null()); + assert_eq!(dec.prev_index, 0); assert_eq!(dec.sequence, rec.sequence); assert_eq!(dec.script_sig, rec.script_sig); assert_eq!(dec.witness, rec.witness); @@ -1729,15 +1770,27 @@ fn input_flags_roundtrip() { witness: vec![], }; let enc = rec.encode(); - // flags only: null prev + final seq + empty script + empty witness - assert_eq!(enc.len(), 1); - assert_eq!(InputRecord::decode(&enc).unwrap(), rec); + assert_eq!( + enc, + vec![ + input_flags::PREV_ON_INPUTS + | input_flags::SEQ_FINAL + | input_flags::EMPTY_SCRIPT + | input_flags::EMPTY_WITNESS + ] + ); + let dec = InputRecord::decode(&enc).unwrap(); + assert_eq!(dec.sequence, rec.sequence); + assert!(dec.script_sig.is_empty()); + assert!(dec.witness.is_empty()); + assert!(dec.create_fk.is_null()); + assert_eq!(dec.prev_index, 0); } #[test] fn input_rejects_legacy_local_prev() { // flags: LOCAL_PREV | SEQ_FINAL | EMPTY_SCRIPT | EMPTY_WITNESS - let flags = input_flags::RESERVED4 + let flags = input_flags::PREV_ON_INPUTS | input_flags::SEQ_FINAL | input_flags::EMPTY_SCRIPT | input_flags::EMPTY_WITNESS; @@ -1777,16 +1830,29 @@ fn input_run_roundtrip() { ]; let mut enc = Vec::new(); encode_input_run_secret(&run, &mut enc, None); - let dec = decode_input_run(&enc, 3).unwrap(); + let mut dec = decode_input_run(&enc, 3).unwrap(); assert_eq!(dec.len(), 3); + assert!(!dec[0].is_coinbase()); + assert!(dec[1].create_fk.is_null()); + assert_eq!(dec[1].witness, vec![vec![0xab]]); + assert_eq!(dec[1].prev_txid, [0u8; 32]); + let edges = [ + crate::input::InputEdge::coinbase(), + crate::input::InputEdge { + parent: Fk(1), + vout: 0, + }, + crate::input::InputEdge { + parent: Fk(1), + vout: 3, + }, + ]; + apply_input_edges(&mut dec, &edges).unwrap(); assert!(dec[0].is_coinbase()); assert_eq!(dec[1].create_fk, Fk(1)); assert_eq!(dec[1].prev_index, 0); - assert_eq!(dec[1].witness, vec![vec![0xab]]); assert_eq!(dec[2].create_fk, Fk(1)); assert_eq!(dec[2].prev_index, 3); - // Soft prev_txid not on disk. - assert_eq!(dec[1].prev_txid, [0u8; 32]); } #[test] @@ -1984,15 +2050,17 @@ fn packed_tx_roundtrip() { assert_eq!(dtx.input_count, 0, "txout meta omits n_in"); assert_eq!(dtx.output_count, 2); assert!(dtx.input_start_fk.get().is_none()); - let mut inwit = Vec::new(); - encode_inwit_with_secret(&inputs, &mut inwit, None); - let dins = decode_inwit_secret(&inwit, tx.input_count, None).unwrap(); - assert_eq!(dins, inputs); + let mut seqsigwit = Vec::new(); + encode_seqsigwit_with_secret(&inputs, &mut seqsigwit, None); + let dins = decode_seqsigwit_secret(&seqsigwit, tx.input_count, None).unwrap(); + assert_eq!(dins[0].script_sig, inputs[0].script_sig); + assert_eq!(dins[0].sequence, inputs[0].sequence); + assert_eq!(dins[0].witness, inputs[0].witness); assert_eq!(douts, outputs); } #[test] -fn inwit_and_txout_secret_xor_roundtrip() { +fn seqsigwit_and_txout_secret_xor_roundtrip() { let secret = crate::store_secret::StoreSecret::from_bytes([0x5au8; 32]); let tx = TxRecord { txid: [3u8; 32], @@ -2014,11 +2082,11 @@ fn inwit_and_txout_secret_xor_roundtrip() { let outputs = vec![OutputRecord::unspent(42, vec![0x00, 0x14, 0x99])]; let mut txout = Vec::new(); encode_packed_tx_with_secret(&tx, &inputs, &outputs, &mut txout, Some(&secret)); - let mut inwit = Vec::new(); - encode_inwit_with_secret(&inputs, &mut inwit, Some(&secret)); - assert_ne!(inwit, { + let mut seqsigwit = Vec::new(); + encode_seqsigwit_with_secret(&inputs, &mut seqsigwit, Some(&secret)); + assert_ne!(seqsigwit, { let mut plain = Vec::new(); - encode_inwit_with_secret(&inputs, &mut plain, None); + encode_seqsigwit_with_secret(&inputs, &mut plain, None); plain }); let (dtx, douts, _) = decode_packed_tx_outs_with_spender_rels(&txout, 1).unwrap(); @@ -2029,7 +2097,7 @@ fn inwit_and_txout_secret_xor_roundtrip() { decode_packed_tx_outs_with_spender_rels_secret(&txout, 1, Some(&secret)).unwrap(); assert_eq!(dtx2.input_count, 0, "txout meta omits n_in"); assert_eq!(douts2[0].script, outputs[0].script); - let dins = decode_inwit_secret(&inwit, tx.input_count, Some(&secret)).unwrap(); + let dins = decode_seqsigwit_secret(&seqsigwit, tx.input_count, Some(&secret)).unwrap(); assert_eq!(dins[0].script_sig, inputs[0].script_sig); assert_eq!(dins[0].witness, inputs[0].witness); } @@ -2231,15 +2299,16 @@ fn packed_encode_decode_flags_and_error_arms() { for r in [&coin, &non_final] { let e = r.encode(); let d = InputRecord::decode(&e).unwrap(); - assert_eq!(d.create_fk, r.create_fk); - assert_eq!(d.prev_index, r.prev_index); + assert!(d.create_fk.is_null()); + assert_eq!(d.prev_index, 0); assert_eq!(d.sequence, r.sequence); assert_eq!(d.script_sig, r.script_sig); assert_eq!(d.witness, r.witness); - let (cfk, vout, used) = InputRecord::decode_prevout_at(&e).unwrap(); - assert_eq!(cfk, r.create_fk); - assert_eq!(vout, r.prev_index); - assert_eq!(used, e.len()); + assert!(matches!( + InputRecord::decode_prevout_at(&e), + Err(StoreError::Corrupt("seqsigwit prevout is on inputs")) + )); + assert_eq!(r.encoded_len_exact(), e.len()); let _ = r.encoded_len(); } assert!(matches!( @@ -2250,13 +2319,13 @@ fn packed_encode_decode_flags_and_error_arms() { InputRecord::decode_at(&[]), Err(StoreError::Corrupt(_)) )); - // RESERVED4 flag + // PREV_ON_INPUTS without a sequence payload is truncated. assert!(matches!( - InputRecord::decode_at(&[input_flags::RESERVED4]), + InputRecord::decode_at(&[input_flags::PREV_ON_INPUTS]), Err(StoreError::Corrupt(_)) )); assert!(matches!( - InputRecord::decode_prevout_at(&[input_flags::RESERVED4]), + InputRecord::decode_prevout_at(&[input_flags::PREV_ON_INPUTS]), Err(StoreError::Corrupt(_)) )); // non-coinbase create_fk truncated @@ -2313,12 +2382,12 @@ fn packed_encode_decode_flags_and_error_arms() { assert_eq!(outs.len(), 2); let (m2, _) = TxRecord::decode_body_meta(&raw).unwrap(); assert_eq!(m2.txid, [0u8; 32]); - let mut inwit = Vec::new(); - encode_inwit_with_secret(&inputs, &mut inwit, None); - assert_eq!( - scan_inwit_prevouts(&inwit, tx.input_count).unwrap().len(), - 2 - ); + let mut seqsigwit = Vec::new(); + encode_seqsigwit_with_secret(&inputs, &mut seqsigwit, None); + assert!(matches!( + scan_seqsigwit_prevouts(&seqsigwit, tx.input_count), + Err(StoreError::Corrupt("seqsigwit prevout is on inputs")) + )); let (m4, outs_rels, rels) = decode_packed_tx_outs_with_spender_rels(&raw, 2).unwrap(); assert_eq!(m4.txid, [0u8; 32]); assert_eq!(outs_rels.len(), 2); @@ -2688,20 +2757,20 @@ fn pread_two_spans_parallel_matches_serial() { let first = fks[0].get().unwrap(); let last = fks[3].get().unwrap(); let txout_ranges = t.body_ranges(first, last).unwrap(); - let inwit_fks: Vec = (first..=last).map(Fk).collect(); - let inwit_pairs = t.inwit_loc.range_batch(&inwit_fks).unwrap(); - let inwit_ranges: Vec<(u64, u64)> = inwit_pairs + let seqsigwit_fks: Vec = (first..=last).map(Fk).collect(); + let seqsigwit_pairs = t.seqsigwit_loc.range_batch(&seqsigwit_fks).unwrap(); + let seqsigwit_ranges: Vec<(u64, u64)> = seqsigwit_pairs .into_iter() - .map(|p| p.expect("inwit range")) + .map(|p| p.expect("seqsigwit range")) .collect(); let (t0, _) = txout_ranges[0]; let (tn, tln) = *txout_ranges.last().unwrap(); let tspan = tn + tln - t0; - let (i0, _) = inwit_ranges[0]; - let (inn, iln) = *inwit_ranges.last().unwrap(); + let (i0, _) = seqsigwit_ranges[0]; + let (inn, iln) = *seqsigwit_ranges.last().unwrap(); let ispan = inn + iln - i0; - let serial = pread_two_spans(&t.body, t0, tspan, &t.inwit, i0, ispan, false).unwrap(); - let parallel = pread_two_spans(&t.body, t0, tspan, &t.inwit, i0, ispan, true).unwrap(); + let serial = pread_two_spans(&t.body, t0, tspan, &t.seqsigwit, i0, ispan, false).unwrap(); + let parallel = pread_two_spans(&t.body, t0, tspan, &t.seqsigwit, i0, ispan, true).unwrap(); assert_eq!(serial, parallel); assert!(!serial.0.is_empty()); assert!(!serial.1.is_empty()); @@ -3769,13 +3838,13 @@ fn spent_span_matches_slot_len_times_n_out() { } #[test] -fn reserved_flag_v17_inwit_high_bits_are_corrupt() { +fn reserved_flag_v17_seqsigwit_high_bits_are_corrupt() { let rec = InputRecord::coinbase(u32::MAX, vec![], vec![]); let mut raw = rec.encode(); raw[0] |= 1 << 5; match InputRecord::decode_at(&raw) { Err(StoreError::Corrupt(m)) => { - assert!(m.contains("reserved") || m.contains("inwit"), "{m}"); + assert!(m.contains("reserved") || m.contains("seqsigwit"), "{m}"); } other => panic!("expected Corrupt, got {other:?}"), } @@ -3817,11 +3886,11 @@ fn script_kind_v17_kind_ten_is_corrupt() { } } -/// Fat inwit uses `inwit.loc` (no `{stem}.idx` dirs). +/// Fat seqsigwit uses `seqsigwit.loc` (no `{stem}.idx` dirs). #[test] -fn fat_inwit_uses_delta_loc_not_idx() { +fn fat_seqsigwit_uses_delta_loc_not_idx() { { - let dir = tempfile_dir("inwit-loc"); + let dir = tempfile_dir("seqsigwit-loc"); let t = create_tiny(&dir); let fat_script = vec![0x6au8; 1800]; for i in 0..6u8 { @@ -3842,14 +3911,17 @@ fn fat_inwit_uses_delta_loc_not_idx() { .unwrap(); } assert!(dir.join("create.loc").is_file()); - assert!(dir.join("inwit.loc").is_file()); + assert!(dir.join("seqsigwit.loc").is_file()); assert!(!dir.join("txout.idx").exists()); assert!(!dir.join("spent.idx").exists()); - assert!(!dir.join("inwit.idx").exists()); + assert!(!dir.join("seqsigwit.idx").exists()); let last = t.get(Fk(6)).unwrap(); assert_eq!(last.output_count, 1); - let (off, len) = t.inwit_range(Fk(6)).unwrap(); - let raw_in = t.inwit.with_bytes_at(off, len, |b| Ok(b.to_vec())).unwrap(); + let (off, len) = t.seqsigwit_range(Fk(6)).unwrap(); + let raw_in = t + .seqsigwit + .with_bytes_at(off, len, |b| Ok(b.to_vec())) + .unwrap(); assert!(raw_in.len() >= 1800, "len={}", raw_in.len()); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/rbitcoin-store/src/txstat.rs b/crates/rbitcoin-store/src/txstat.rs index 08a13efc3..59d099d46 100644 --- a/crates/rbitcoin-store/src/txstat.rs +++ b/crates/rbitcoin-store/src/txstat.rs @@ -9,11 +9,12 @@ //! 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`). `size = base + wit_extra`, -//! `weight = 4×base + wit_extra`. 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..]`). Pin / SH / tweaks do not open these files. +//! Cell payload is three canonical ULEBs: `fee_sat`, `base` (non-witness +//! size), `wit_extra` (`total_size − base`). `n_in` lives on `input.loc`. +//! `size = base + wit_extra`, `weight = 4×base + wit_extra`. All-zero cell = +//! unstamped. A truncated ULEB or fewer than three fields means the rest of +//! the stream is in that header's overflow blob (`encoded[8..]`). Pin / SH / +//! tweaks do not open these files. use crate::error::StoreError; use crate::file::{GrowPolicy, TableFile, FILE_HEADER_LEN}; @@ -30,7 +31,6 @@ const BLK_SLOT: u64 = 16; /// Packed confirm-time econ. All-zero on disk is unstamped (not this struct). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct TxStatRow { - pub n_in: u32, pub fee_sat: u64, pub base: u32, pub wit_extra: u32, @@ -53,11 +53,7 @@ impl TxStatRow { } pub fn encode_stream(row: &TxStatRow) -> Result, StoreError> { - if row.n_in > u32::from(u16::MAX) { - return Err(StoreError::Corrupt("txstat n_in exceeds 16 bits")); - } let mut v = Vec::new(); - write_uleb128(&mut v, u64::from(row.n_in)); write_uleb128(&mut v, row.fee_sat); write_uleb128(&mut v, u64::from(row.base)); write_uleb128(&mut v, u64::from(row.wit_extra)); @@ -108,7 +104,7 @@ enum StreamParse { fn parse_stream(buf: &[u8], allow_trunc: bool) -> Result { let mut off = 0usize; - let mut fields = [0u64; 4]; + let mut fields = [0u64; 3]; for slot in &mut fields { if off >= buf.len() { if allow_trunc { @@ -130,16 +126,11 @@ fn parse_stream(buf: &[u8], allow_trunc: bool) -> Result Result Result, StoreError> { - if cell == [0u8; 8] { - return Ok(None); - } - let (n, _) = read_canonical_uleb(&cell)?; - if n == 0 { - return Ok(None); - } - let n_in = u32::try_from(n).map_err(|_| StoreError::Corrupt("txstat n_in"))?; - Ok(Some(n_in)) -} - pub fn encode_ovf_blob(tails: &[(u16, Vec)]) -> Result, StoreError> { let mut out = Vec::new(); for (idx, rest) in tails { @@ -359,10 +338,6 @@ impl TxStat { Ok(buf) } - pub fn n_in_at(&self, fk: Fk) -> Result, StoreError> { - n_in_from_cell(self.get_cell(fk)?) - } - pub fn get_row_merged( &self, fk: Fk, @@ -592,9 +567,8 @@ mod tests { use super::*; use crate::testutil::TempDir; - fn tiny(n_in: u32, fee: u64, base: u32, wit: u32) -> TxStatRow { + fn tiny(fee: u64, base: u32, wit: u32) -> TxStatRow { TxStatRow { - n_in, fee_sat: fee, base, wit_extra: wit, @@ -617,25 +591,31 @@ mod tests { #[test] fn uleb_typical_fits_in_cell() { - let row = tiny(1, 1000, 110, 112); + let row = tiny(1000, 110, 112); let (cell, tail) = pack_cell(&row).unwrap(); assert!(tail.is_none()); assert_eq!(parse_cell(cell).unwrap(), CellParse::Complete(row)); - assert_eq!(n_in_from_cell(cell).unwrap(), Some(1)); assert_eq!(row.size(), 222); assert_eq!(row.weight(), 4 * 110 + 112); } + #[test] + fn former_n_in_byte_no_longer_forces_a_tail() { + let row = tiny(50_000, 200, 200_000); + let (cell, tail) = pack_cell(&row).unwrap(); + assert!(tail.is_none(), "three fields of this row fit in 8 B"); + assert_eq!(parse_cell(cell).unwrap(), CellParse::Complete(row)); + } + #[test] fn overflow_is_remaining_bytes_only() { - let row = tiny(1, 50_000, 200, 200_000); + let row = tiny(u64::from(u32::MAX), 4_000_000, 4_000_000); let stream = encode_stream(&row).unwrap(); - assert!(stream.len() > 8, "fat wit must miss 8 B"); + assert!(stream.len() > 8, "wide fee and sizes must miss 8 B"); let (cell, tail) = pack_cell(&row).unwrap(); let tail = tail.expect("tail"); assert_eq!(tail.as_slice(), &stream[8..]); assert_eq!(parse_cell(cell).unwrap(), CellParse::NeedTail); - assert_eq!(n_in_from_cell(cell).unwrap(), Some(1)); assert_eq!(parse_with_tail(cell, &tail).unwrap(), row); } @@ -643,8 +623,8 @@ mod tests { fn header_blob_roundtrip() { let dir = TempDir::labeled("txstat-ovf").unwrap(); let t = TxStat::create(&dir).unwrap(); - let small = tiny(1, 1000, 110, 0); - let fat = tiny(2, 2_000_000, 400, 200_000); + let small = tiny(1000, 110, 0); + let fat = tiny(u64::from(u32::MAX), 4_000_000, 4_000_000); t.append_batch(0, &[small, fat]).unwrap(); t.write_block_rows(Fk(1), 1, &[small, fat]).unwrap(); let blob = t.header_blob(Fk(1)).unwrap(); diff --git a/crates/rbitcoin-store/src/uring_session.rs b/crates/rbitcoin-store/src/uring_session.rs index 390717c67..52cdcb03d 100644 --- a/crates/rbitcoin-store/src/uring_session.rs +++ b/crates/rbitcoin-store/src/uring_session.rs @@ -1178,8 +1178,8 @@ pub const KIND_SPEND_PAGE_READ: u8 = 7; pub const KIND_SPEND_PAGE_WRITE: u8 = 8; /// SP-tweak machine: `txout.body` pread. pub const KIND_SP_TXOUT: u8 = 12; -/// SP-tweak machine: `inwit.body` pread (P2TR only). -pub const KIND_SP_INWIT: u8 = 13; +/// SP-tweak machine: `seqsigwit.body` pread (P2TR only). +pub const KIND_SP_SEQSIGWIT: u8 = 13; /// SP-tweak machine: parent `txout.body` pread. pub const KIND_SP_PARENT: u8 = 14; pub const KIND_MPHF_G: u8 = 15; @@ -1376,7 +1376,7 @@ mod tests { KIND_SPEND_PAGE_READ, KIND_SPEND_PAGE_WRITE, KIND_SP_TXOUT, - KIND_SP_INWIT, + KIND_SP_SEQSIGWIT, KIND_SP_PARENT, KIND_MPHF_G, ]; diff --git a/crates/rbitcoin-store/src/var_table.rs b/crates/rbitcoin-store/src/var_table.rs index 79f939e79..0958fa605 100644 --- a/crates/rbitcoin-store/src/var_table.rs +++ b/crates/rbitcoin-store/src/var_table.rs @@ -332,7 +332,7 @@ impl VarTable { let before = body_blob.len(); encode(i, &mut body_blob); // Loc strides must be strictly monotone. Zero-length payloads (empty - // inwit) get an 8-byte zero pad so the next start advances one stride. + // seqsigwit) get an 8-byte zero pad so the next start advances one stride. // Decode treats trailing zeros as pad. Spent `n_out ≥ 1` is 8×n. if body_blob.len() == before { body_blob.resize(body_blob.len().saturating_add(8), 0); diff --git a/crates/rbitcoin-store/tests/public_surface.rs b/crates/rbitcoin-store/tests/public_surface.rs index 06cf9b8d5..37d42ea15 100644 --- a/crates/rbitcoin-store/tests/public_surface.rs +++ b/crates/rbitcoin-store/tests/public_surface.rs @@ -5,11 +5,11 @@ use rbitcoin_store::{ block_header_hash, block_wire_input_count, clear_unsorted_shard_dir, - collect_unsorted_shard_files, decode_inwit_secret, decode_packed_tx_outs_with_spender_rels, + collect_unsorted_shard_files, decode_packed_tx_outs_with_spender_rels, decode_packed_tx_outs_with_spender_rels_secret, decode_packed_tx_with_spender_rels_secret, - encode_packed_tx, encode_packed_tx_with_secret, encode_txout_meta_and_outs, - encode_unspent_output_into_secret, ensure_nofile_budget, free_gib_label, - is_probe_exhausted_error, is_store_corrupt_display, leftover_probe_diag_ready, + decode_seqsigwit_secret, encode_packed_tx, encode_packed_tx_with_secret, + encode_txout_meta_and_outs, encode_unspent_output_into_secret, ensure_nofile_budget, + free_gib_label, is_probe_exhausted_error, is_store_corrupt_display, leftover_probe_diag_ready, leftover_probe_diag_recorded, list_materialize_claims, list_runs, load_tweak_wave, materialize_sh_unsorted_from_class_a, merkle_root_from_txids, next_run_path, output_flags, script_hash, sh_heads_insert_capped, spend_ann_backend, spend_meta_backend, spent_abs, @@ -72,7 +72,7 @@ fn crate_root_exports_cross_crate_names() { let _ = write_sorted_run; let _ = clear_unsorted_shard_dir; let _ = collect_unsorted_shard_files; - let _ = decode_inwit_secret; + let _ = decode_seqsigwit_secret; let _ = decode_packed_tx_outs_with_spender_rels; let _ = decode_packed_tx_outs_with_spender_rels_secret; let _ = decode_packed_tx_with_spender_rels_secret; diff --git a/docs/README.md b/docs/README.md index 46d5e8175..0495c6464 100644 --- a/docs/README.md +++ b/docs/README.md @@ -48,7 +48,7 @@ into `AGENTS.md`. | [`core-functional.md`](./core-functional.md) | Core v31.1 functional harness. | | [`overlay-functional.md`](./overlay-functional.md) | Private Tor / i2pd / cjdns mesh harness (labeled / nightly). | | [`how-we-plan.md`](./how-we-plan.md) | Agent contract (cycle, Agent RAM, keep-compiling), then human rationale. | -| [`personal-node-plans/`](./personal-node-plans/) | Home-node implementation plan group (SOCKS, overlays, wallet onions, ephemeral broadcast, inwit prune / `NETWORK_LIMITED`). Live flags stay OPERATOR/COMPAT when a slice ships. Do not copy step lists into quality.md until scheduled. | +| [`personal-node-plans/`](./personal-node-plans/) | Home-node implementation plan group (SOCKS, overlays, wallet onions, ephemeral broadcast, seqsigwit prune / `NETWORK_LIMITED`). Live flags stay OPERATOR/COMPAT when a slice ships. Do not copy step lists into quality.md until scheduled. | | [`releases.md`](./releases.md) | Tag `vX.Y.Z`, `vX.Y.x` patch line, `.99` bump, Highlights / GitHub notes. | | [`code-shape.md`](./code-shape.md) | Control flow, types, naming, composition (CONTRIBUTING principle 10). Named extracts: quality.md **Q-61** (Completed). Clippy: no workspace `allow` list; leftover lints are site-local with a reason. | | [`quality.md`](./quality.md) | Living quality roadmap (Open + Won't-fix + Parked + Protect). | diff --git a/docs/architecture.md b/docs/architecture.md index 918367e92..141a38eb2 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -69,7 +69,7 @@ for unknown-height bodies (mark missing → re-getdata). | 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`+`inwit`; tip uses body queue + peer wire | Serve raw blk files / undo | +| 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 | @@ -106,7 +106,7 @@ commit: [`docs/crash-recovery.md`](./crash-recovery.md). | Class | Role | Mutation style | |-------|------|----------------| -| **A** | Canonical archive: headers, split txs (`txout` / `inwit` / `spent` + `txid.body` / `tx.head/`) | Append bodies; publish via HWM / heads (**allocate-then-publish**) | +| **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 | @@ -117,7 +117,7 @@ a LevelDB bag. ### Reconstruct (no live wire ring) -- **Historical blocks** are rebuilt from Class A (zip `txout` + `inwit`) rather +- **Historical blocks** are rebuilt from Class A (zip `txout` + `seqsigwit`) rather than kept forever as raw wire `blk` files. - Tip serve / reorg uses the **in-RAM body queue** and **peer wire**. There is no on-disk tip wire ring (`rbitcoin-wire-cache` is gone). diff --git a/docs/concurrency.md b/docs/concurrency.md index 1dbf7f297..f3cecf1b4 100644 --- a/docs/concurrency.md +++ b/docs/concurrency.md @@ -10,7 +10,7 @@ Short map of who may write which tables. **Format is unstable until 1.0.** | Confirm **lookup** | 1 OS thread | pack/hold from BQ stamped Σ inputs; emit: decode BQ raw + TipOnly `head_fk` + **`take_raw` onto loadq** (`Arc` + `TxPrecompute`). Does **not** plan_batch / structure | | Confirm **load** | 1 OS thread | `confirm_wire_lookup_stamp` (structure with **loadq `pres`**, plan_batch binds carried BQ keys, skeleton bind) + pin `txout` + assemble | | Confirm **scripts** | 1 OS thread (`ibd-confirm`) publishes waves + `rbtc-scripts` steal (lock-free claim) | **none** — pure CPU | -| Confirm **write** | 1 OS thread (`ibd-confirm-write`) + 1 process-wide `ibd-confirm-head` | **sole Class A appender** (`txout`+`inwit`+`spent`; IBD encodes ins from `Arc` + SpendEdges) + structural + Class C + spend annotate on **`spent.body`** + tip GC; **`block_queue_dequeue_height`**. `tx.head` write-behind insert runs on **`ibd-confirm-head`** overlapping structural + Class C (not a per-batch spawn). Class A **never leads tip** (same commit era; no archive-ahead DONTNEED) | +| Confirm **write** | 1 OS thread (`ibd-confirm-write`) + 1 process-wide `ibd-confirm-head` | **sole Class A appender** (`txout`+`seqsigwit`+`spent`; IBD encodes ins from `Arc` + SpendEdges) + structural + Class C + spend annotate on **`spent.body`** + tip GC; **`block_queue_dequeue_height`**. `tx.head` write-behind insert runs on **`ibd-confirm-head`** overlapping structural + Class C (not a per-batch spawn). Class A **never leads tip** (same commit era; no archive-ahead DONTNEED) | | IBD main loop | 1 tokio task | none (orchestration only). Event drain + confirm-offer every turn; getdata assign ≤50 ms (immediate if inflight empty); main-loop `getheaders`/`locator_hashes` ≤500 ms (empty path immediate); peer stall/relative-slow and work-path hygiene ≤1 s. Full-batch header continuation stays on the Headers event. | **IoSession TLS:** one completion session per OS thread (`with_thread_local`). Harvest / poison / drain / do-not-flatten: [`io-modality.md`](./io-modality.md). `RBITCOIN_IO=pread` disables the session. SH k-way merge submits 256 KiB ahead preads on that TLS session and waits only when promote needs a page that has not completed. @@ -27,7 +27,7 @@ only when no foreground wave and no detached job are waiting. Process restart le **Tip follow / reorg:** peer reconstructs on the session task, then offers the body to the process-wide **`tip-accept`** OS thread (bounded queue 8, oneshot wait). That thread is the sole production owner of `connect_lock` / `accept_and_connect_block_preverified` / `confirm_wire_run_preverified` at tip (1-block, or one `accept_branch` run). Scripts still publish via `start_for_each_owned` → `rbtc-scripts-*` steal — same as IBD’s publisher, not a second interpreter. Disconnect keeps Class A archive; re-extension always supplies **wire** from the peer, not hash-only load. **IBD most-work reorg** rewinds the tip to the LCA (`rewind_to_height`) from the **IBD orchestration task only** — never from confirm lookup/load/scripts/write threads — then the confirm pipeline connects the winner as a linear extension. `accept_branch` remains for tip-follow / held-body apply (also on `tip-accept`). Selector / rewind / invalid-heavy: [`architecture.md`](./architecture.md#most-work-chain-selection-ibd--tip-follow). -**Wire retained on the pipeline batch only:** lookup/load pull `bitcoin::Block` from the body queue; that wire rides through scripts; **no Class-A wire rebuild**. Script jobs carry lookup `TxPrecompute` (no job `from_tx`). Split Class A (`txout` / `inwit` / `spent`) is planned once and committed in the write stage. +**Wire retained on the pipeline batch only:** lookup/load pull `bitcoin::Block` from the body queue; that wire rides through scripts; **no Class-A wire rebuild**. Script jobs carry lookup `TxPrecompute` (no job `from_tx`). Split Class A (`txout` / `seqsigwit` / `spent`) is planned once and committed in the write stage. **Body queue:** process-local **in-RAM** FIFO (id / height / hash / header_fk / raw charge) plus a first-wins **height → id** map. Height APIs (`is_resolve_complete`, `get_by_height`, `hash_at_height`, pack snapshot) do not walk `index.values()`. Peer copies wire **then** locks and enqueues **raw**; lookup decodes and parks resolved, then **dequeues** on load-batch send into **loadq** (`LoadBatch`). Load stamp consumes that `pres` — it does **not** read `block_queue_resolved`. **Never both** raw and decoded. **Offer** on peer Block → RAM; lookup takes; write **dequeue** is a no-op if the row is already gone. Restart starts empty (legacy `store/block_queue/` is best-effort removed). Soft densify assign, never-refuse-enqueue, why RAM: [`ibd-memory.md`](./ibd-memory.md). diff --git a/docs/crash-recovery.md b/docs/crash-recovery.md index 2121d704b..f58b8b069 100644 --- a/docs/crash-recovery.md +++ b/docs/crash-recovery.md @@ -61,7 +61,7 @@ Open revalidation runs in `Query::open_or_create` **before** P2P can extend tip. - Best-chain spentness: annotation + `is_confirmed_strong(spender)`. Same-batch pre-fill is not strong until this batch's Class C. - Kill-safe: stale/non-strong fields do not false-positive if filter is applied. - No `point.head` (v4 open-hash multimap removed). -- Class A is **three stems** (`txout` / `inwit` / `spent`); bare-meta puts are rejected. Packed `tx.body` with creates is refused on open. +- Class A is **three stems** (`txout` / `seqsigwit` / `spent`); bare-meta puts are rejected. Packed `tx.body` with creates is refused on open. ## Thin scripthash (Electrum outpoint pointers) diff --git a/docs/ibd-memory.md b/docs/ibd-memory.md index 166fd83bb..85002b615 100644 --- a/docs/ibd-memory.md +++ b/docs/ibd-memory.md @@ -177,10 +177,10 @@ touches. Census: [`SCHEMA.md`](../SCHEMA.md) (tip 962298, 1.42 B creates). | Mode | Must stay hot | Approx | Cold (fault OK) | |------|---------------|--------|-----------------| -| **Tip follow / Electrum serve** | Open `tx.head` + recent `txout`/`spent`/`txid` tails + SH main idx + mempool | **8–16 GiB** page cache + **~2–3 GiB** process | `inwit` (except `getrawtransaction`), sealed `tx.head` older than fuse-skip, archive `txout` | -| **Comfortable serve** (busy wallets, Electrum tweaks, RPC reconstruct) | Above + more `txout` + SH body slabs + `txid.body` | **16–32 GiB** | `inwit` except rawtx | -| **IBD pin+annotate (no thrash)** | **All** `txout` + **all** `spent` + `create.loc` (~3 GiB) + `txid.body` + `tx.head` | **~221 GiB** | **`inwit` (~486 GiB)** — wire still holds witness | -| **IBD + reconstruct/getdata** | Previous + `inwit` | **~710 GiB** (same order as old packed `tx.body`) | — | +| **Tip follow / Electrum serve** | Open `tx.head` + recent `txout`/`spent`/`txid` tails + SH main idx + mempool | **8–16 GiB** page cache + **~2–3 GiB** process | `seqsigwit` (except `getrawtransaction`), sealed `tx.head` older than fuse-skip, archive `txout` | +| **Comfortable serve** (busy wallets, Electrum tweaks, RPC reconstruct) | Above + more `txout` + SH body slabs + `txid.body` | **16–32 GiB** | `seqsigwit` except rawtx | +| **IBD pin+annotate (no thrash)** | **All** `txout` + **all** `spent` + `create.loc` (~3 GiB) + `txid.body` + `tx.head` | **~221 GiB** | **`seqsigwit` (~486 GiB)** — wire still holds witness | +| **IBD + reconstruct/getdata** | Previous + `seqsigwit` | **~710 GiB** (same order as old packed `tx.body`) | — | | **SH tip materialize** | Two `txout` scans (16 MiB libc `pread` spans + 1 MiB write buffers); extract workers min(CPUs, free RAM / 1.5 GiB); ingest OA **~768 MiB** (2²⁵×24 B) | **~1.5 GiB** heap per extract worker (BDZ `g` + fuse + maps); pack ~8 MiB packed `g` + 16 MiB `body_buf` | No catalog k-way pages; no 64 × `.val` random-read set on the scan | Packed schema 13/14 needed the whole **`tx.body` (~663 GiB)** hot for the same diff --git a/docs/invariants.md b/docs/invariants.md index 8f716d16e..7d8417cae 100644 --- a/docs/invariants.md +++ b/docs/invariants.md @@ -41,9 +41,9 @@ header plan after lookup planned it, stamped `create_fk`, etc.). ``` wire / body-queue → lookup (stamp create_fk + parent txout/spent ranges + parent txid; - IO: tx.head, create.loc, txid.body — NEVER outs/inwit decode) + IO: tx.head, create.loc, txid.body — NEVER outs/seqsigwit decode) → load / pin (BatchParents outs by known txout range only; - IO: txout.body — NEVER head / loc / txid.body / inwit) + IO: txout.body — NEVER head / loc / txid.body / seqsigwit) → scripts (pure CPU — NEVER any store IO) → Class A commit (if ArchiveWritePlan present; encode plan packed ins filled at stamp) → ensure abs (holes only: same-batch after Class A / missing stamp; post-condition: every spend has abs) @@ -70,8 +70,8 @@ the Wire CreatePin (`Arc` + tx index). Load still does not head/idx. | Stage | Allowed IO | Forbidden | |-------|------------|-----------| -| **lookup** | `tx.head`, `create.loc` (fk + body+spent ranges + `n_out`), `txid.body`, headers | **`txout.body`** (outs or spent-range peeks) / **`inwit` decode** | -| **load** | **`txout.body` outs by range** (from lookup stamp) | head, loc, `txid.body`, `inwit` | +| **lookup** | `tx.head`, `create.loc` (fk + body+spent ranges + `n_out`), `txid.body`, headers | **`txout.body`** (outs or spent-range peeks) / **`seqsigwit` decode** | +| **load** | **`txout.body` outs by range** (from lookup stamp) | head, loc, `txid.body`, `seqsigwit` | | **scripts** | none | any store IO | | Stage | Invariant | Soft path allowed? | @@ -79,7 +79,7 @@ the Wire CreatePin (`Arc` + tx index). Load still does not head/idx. | Lookup parent stamp | Every external spent parent has create_fk + body_range (or offline in_flight CreatePin) + reverse txid. Archived parents also have computed `spent.body` range on the stamp (same-wave in-flight outs skip loc, miss OK; later-wave InFlight takes loc from the same `CreatePin` Arc write set at Class A, or skeleton TipOnly loc). IBD (`skeleton = Some`) never loc-by-fk: pin loc unset leaves spent unset for write TLS. plan=None leftover still `fill_missing_parent_ranges` | Missing → hard Err at stamp / pin contract. **No** `finish_archive_plan` `fill_missing` after stamp loc. **No** IBD load `create.loc` | | Parent create_fk | **same-batch** planned fks (offline at pin) → **in-flight** (lookup snapshots `drain_and_fence_hi` **before** the wave's TipOnly read and passes it on the last load batch; load drops tagged map rows with pack height **below** that snapshot after that batch's in-flight read; equality keeps; not Class C tip, not `class_a_hi`, not write freeze; one load-thread HashMap, insert after stamp) → **skeleton** (`BatchParentIds` on the `LoadBatch`: lookup TipOnly fk + body_range + spent_range + per-chunk need-vouts) → **Corrupt** on IBD miss. plan=None / S0 (`skeleton = None`) is in-flight → leftover TipOnly. One helper: [`stamp_external_parents`](../crates/rbitcoin-query/src/stamp.rs). No leftover pending map, no process pin FIFO, no BQ-side hits map, no parent-store create_fk on stamp, no published live_union. Same-wave creates are omitted from TipOnly need. Header-cache GC polls store tip each load pack. One fk per txid — [`errata.md`](./errata.md). | Miss of in-flight and skeleton → `Corrupt("parent create_fk unresolved")` (**engine fault**: requeue once, then halt IBD; never blacklist). Identity without loc range → `Corrupt("invariant: loc range missing after identity")`, not a miss | | io_uring harvest | TLS session fail-closed ([`io-modality.md`](./io-modality.md)) | **No** silent success. `Corrupt("invariant: io_uring …")` (not `bdz g page bad slot`). Ring-unavailable still pread-fallback | -| Load body outs | By `txout` range only from lookup stamp; incomplete outs → hard Err. Pin **copies** lookup `spent_range` (no loc IO). Later-wave InFlight loc comes from the CreatePin Arc (Class A `set_loc`) | **No** loc cold outs on load; **no** `inwit` on pin; **no** IBD loc-by-fk | +| Load body outs | By `txout` range only from lookup stamp; incomplete outs → hard Err. Pin **copies** lookup `spent_range` (no loc IO). Later-wave InFlight loc comes from the CreatePin Arc (Class A `set_loc`) | **No** loc cold outs on load; **no** `seqsigwit` on pin; **no** IBD loc-by-fk | | Ensure (write) | Every non-null spend edge has `spent_range` abs after ensure returns. Lookup stamped archived parents; same-batch from append RAM loc; just-written from write-thread loc packs kept until write of the last height whose TipOnly had started at note (`lookup_started_hi`; `keep_until` never bumped; fill of that write already ran). Later-wave InFlight takes CreatePin loc set at Class A. Missing abs → `invariant:` | **No** write `create.loc` read | | Structural spentness | Abs required for every non-null spend create_fk after load; multi-list → confirmed-strong walk (reorg protocol) | **No** unpinned “wire-corrected create_fk” soft spentness. Multi flag alone is **not** hard `Err` | | Pin create identity | Pin must carry non-zero create txid from **lookup stamp** (plan reverse map / wire prev_txid / `txid.body`) | Soft zero-identity pin → assemble mismatch → cold recovery is **forbidden** | diff --git a/docs/io-modality.md b/docs/io-modality.md index ec68399a5..6ef549af2 100644 --- a/docs/io-modality.md +++ b/docs/io-modality.md @@ -28,7 +28,7 @@ Related: [`env-knobs.md`](./env-knobs.md), [`concurrency.md`](./concurrency.md), | Layer | Controlled by | Values | Purpose | |-------|---------------|--------|---------| -| **Bulk batch** | `RBITCOIN_IO` only | `uring` \| `pool` \| `iocp` \| `pread` | Multi-op **completion session** on file handles (`txout` pin/outs, `inwit` reconstruct, spend meta/ann on `spent`, Class C bulk) | +| **Bulk batch** | `RBITCOIN_IO` only | `uring` \| `pool` \| `iocp` \| `pread` | Multi-op **completion session** on file handles (`txout` pin/outs, `seqsigwit` reconstruct, spend meta/ann on `spent`, Class C bulk) | | **Table transport** | [`TableFile`](../crates/rbitcoin-store/src/file.rs) | **FdOnly always** | All payload via pread/pwrite; fallocate grow; no process maps. Sealed `.fuse8` and SH BDZ3 occ are sidecar maps, not TableFile | **`RBITCOIN_IO` selects the completion-session backend** (not per-path). @@ -123,7 +123,7 @@ If a change seems to require collapsing a machine, **stop and ask**. | **L1** | 4 KiB head pages / 3–4 KiB SH chunks (working-set caches) | Write-back dirty page/chunk with one pwrite | | **L2** | Compact Class C (`confirmed`, `header_txs_*`, `strong_tx`) full `Vec` in process | **Write-behind:** RAM mutate during commit; complete-or-fail body image on `flush_class_c_tip` **before** body-queue dequeue | -**Never L2:** `txout` / `inwit` / `spent`, full `tx.head` / `*.idx`. +**Never L2:** `txout` / `seqsigwit` / `spent`, full `tx.head` / `*.idx`. `RBITCOIN_CLASS_C_INRAM_MAX_MB` (default 256) caps **`confirmed`** and **`header_txs_*` only**. `strong_tx` stays L2 (1 bit/fk). Create height is a RAM fence (~15 MiB at 1M blocks), not a file. @@ -136,7 +136,7 @@ RAM fence (~15 MiB at 1M blocks), not a file. | Path | Env | Syscalls | |------|-----|----------| -| Pin outs / body pipeline | `RBITCOIN_IO` | uring/pread on **`txout.body` FD** (Full also zips `inwit`) | +| Pin outs / body pipeline | `RBITCOIN_IO` | uring/pread on **`txout.body` FD** (Full also zips `seqsigwit`) | | Head-resolve identity | `RBITCOIN_IO` | uring/pread on **`txid.body`** (not a packed body prefix) | | Spend-meta 8 B peeks | `RBITCOIN_IO` | uring/pread on **`spent.body` FD** | | Spend pure-write annotate | `RBITCOIN_IO` | uring/pwrite or pwrite on **`spent.body` FD** | @@ -144,7 +144,7 @@ RAM fence (~15 MiB at 1M blocks), not a file. | Class A body/idx **linear append** | always | **pwrite** (three stems + three idx) | | SH unsorted collect (Class A) | libc `pread` | **Two** sequential coalesced **16 MiB** `txout.body` scans (`sh_extract_workers`): static create-fk spans (no steal); per-worker identity maps spill-largest as `SHKSP01` under `keys/NN/` (one spill writer, 1-slot queue; tmp+rename, no `sync_all`; first-fk delta singles); merge folds those files into one map, one walk to `scripthash.head/NN` + `multi/NN.fuse8`; leftover keys shapes refuse. Then fuse-hit `SHPST01` post spills under `post/NN/` (`80n+8f` estimate, same 1.5 GiB worker cap and 1-slot writer; tmp+rename, no `sync_all`). Not TLS uring: one large positional read per span, not a completion machine. Writes are libc `pwrite` / tmp+rename. Pass 2 does not Fd-index the main MPHF; pack folds one shard's spills then `slot_for_key16` once per unique multi key. Packed 2-bit `g` is ~8 MiB per pack worker, not a 12 GiB `.val` page-cache set. | | SH Electrum/Esplora join | `RBITCOIN_IO` | Query-thread session: waved `idx_body_pipeline` on **`txout.body`**, optional page-grouped **`txid.body`** (history: creates+spenders; listunspent: unspent creates; balance/`/address` stats: none), `get_spender_meta_at_abs_batch` on **`spent.body`**. Megakey **extent**: one span pread of `extent_n` pages, then linked 4 KiB tail. Schema-18 mode 10 leftovers **refuse** on open. Does **not** share a confirm TLS ring. | -| SP-tweak backfill | `RBITCOIN_IO` | idx ranges **before** TLS ring; uring/pread `txout.body`, then `inwit` + parent `txout` for P2TR only. Writes are **not** the load ring: batched `sp_tweaks` body+idx pwrite (not per-tx CQEs). | +| SP-tweak backfill | `RBITCOIN_IO` | idx ranges **before** TLS ring; uring/pread `txout.body`, then `seqsigwit` + parent `txout` for P2TR only. Writes are **not** the load ring: batched `sp_tweaks` body+idx pwrite (not per-tx CQEs). | Default: Linux uring if the ring opens else pool; Darwin pool; Windows IOCP. Ring depth **128** (merge may grow). `RBITCOIN_IO=pread` forces libc. @@ -154,9 +154,9 @@ IOCP. Ring depth **128** (merge may grow). `RBITCOIN_IO=pread` forces libc. | Object | Tier | Notes | |--------|------|--------| | **`txout.body`** | L0 | Hot outs (pin / SH / Electrum tweaks); pread/pwrite/uring | -| **`inwit.body`** | L0 | Cold ins+witness; reconstruct / getdata only | +| **`seqsigwit.body`** | L0 | Cold ins+witness; reconstruct / getdata only | | **`spent.body`** | L0 | 8 B×n_out sole-spender; annotate RMW | -| **`create.loc` / `inwit.loc`** | L0 | FdOnly 2 B/create (hot) / u16 (cold); leftover `spent.off` unlinked. `create.loc` leftover stamp: batched window preads, sum/read through max fk in-window, running-sum + SIMD deinterleave (no loc L2) | +| **`create.loc` / `seqsigwit.loc`** | L0 | FdOnly 2 B/create (hot) / u16 (cold); leftover `spent.off` unlinked. `create.loc` leftover stamp: batched window preads, sum/read through max fk in-window, running-sum + SIMD deinterleave (no loc L2) | | **`tx.head` segments** | L0+L1 | Open OA: 4 KiB page-coalesced RMW. Sealed: mmap `.fuse8` (heap `fuse8=0`); packed BDZ `g` FdOnly 4 KiB page stream (`KIND_MPHF_G`); MPHF output is `rel−1` | | Header hash head | L0+L1 | 128-slot (~3 KiB) chunk cache | | Hash multi-list (`.mlt`) | L0 | Linear append | @@ -172,7 +172,7 @@ IOCP. Ring depth **128** (merge may grow). `RBITCOIN_IO=pread` forces libc. |------|------------|---------------------| | Pin outs | FdOnly `create.loc` ranges (batched window preads; sum/read only through max fk in-window) | uring/pread `txout` bytes (starting OS page; full span if need is likely to spill) | | Head resolve stream | FdOnly **page-batched** head probe on the held session; **one** FdOnly loc batch after identity (standalone bulk, not on the probe ring) | uring/pread `txid.body` identity | -| IBD **getdata serve** reconstruct | FdOnly `create.loc` / `inwit.loc` ranges for a contiguous `header_txs` run | libc span pread of `txout.body` + `inwit.body` in parallel (not confirm `idx_body_pipeline`) | +| IBD **getdata serve** reconstruct | FdOnly `create.loc` / `seqsigwit.loc` ranges for a contiguous `header_txs` run | libc span pread of `txout.body` + `seqsigwit.body` in parallel (not confirm `idx_body_pipeline`) | --- diff --git a/docs/overlay-functional.md b/docs/overlay-functional.md index e6297a286..0aa19a188 100644 --- a/docs/overlay-functional.md +++ b/docs/overlay-functional.md @@ -73,7 +73,7 @@ Timeout **45 minutes** (i2pd tunnel build on a tiny net is the long pole). 4. **CJDNS TUN** — A/B `--cjdns-reachable` on real `fc00::/8`. `getpeerinfo.network=cjdns` on both sides. `getnetworkinfo.localaddresses` lists each TUN address without `--external-ip`. TUN create failure is a hard fail. 5. **Ephemeral broadcast** — A `--proxy` (real Tor SOCKS); standing onion peer to B; `sendrawtransaction`; standing session must not INV that tx; a one-shot circuit delivers `tx`. -Out of this harness: 09 inwit-prune; public overlays; replacing NixOS +Out of this harness: 09 seqsigwit-prune; public overlays; replacing NixOS module dummy `tor`/`i2pd` units ([`nixos-module-runtime`](../nix/tests/nixos-module-runtime.nix) stays argv / `After=` only). diff --git a/docs/personal-node-plans/09-inwit-prune.md b/docs/personal-node-plans/09-inwit-prune.md index fccf653bd..277827500 100644 --- a/docs/personal-node-plans/09-inwit-prune.md +++ b/docs/personal-node-plans/09-inwit-prune.md @@ -1,10 +1,10 @@ -# 09 — Prune inputs and witnesses (`inwit`); advertise `NODE_NETWORK_LIMITED` +# 09 — Prune inputs and witnesses (`seqsigwit`); advertise `NODE_NETWORK_LIMITED` ## Goal -A home node can stop serving **Class A inwit** (scriptSig + witness + input +A home node can stop serving **Class A seqsigwit** (scriptSig + witness + input prevout encoding) below a 288-height watermark. Unpruned nodes keep -`inwit.body`. Pruned nodes serve only the kept window, while still: +`seqsigwit.body`. Pruned nodes serve only the kept window, while still: 1. Confirming new blocks (each connected height is recorded in the window). 2. Reorging within the kept window. @@ -16,19 +16,19 @@ This is **not** Core `-prune` of entire `blk*.dat` files. We keep headers, `txout`, `spent`, `txid.body`, SH, tweaks. We drop old **inputs and witnesses**. -**Layout:** two modes, not a rolling `inwit.body`. +**Layout:** two modes, not a rolling `seqsigwit.body`. -- **Unpruned** (default): Class A `inwit.body` is the witness archive. +- **Unpruned** (default): Class A `seqsigwit.body` is the witness archive. Reconstruct, getdata, and wire RPC read it. -- **`--prune-inwit`:** witness below the watermark is not served. The last - **288 heights** are one file each, `store/inwit.window/{height}.bin`, plus +- **`--prune-seqsigwit`:** witness below the watermark is not served. The last + **288 heights** are one file each, `store/seqsigwit.window/{height}.bin`, plus a RAM cache of those heights capped by - `--prune-inwit-ram-threshold-bytes` (default 256 MiB). **`0` keeps nothing + `--prune-seqsigwit-ram-threshold-bytes` (default 256 MiB). **`0` keeps nothing in RAM** — every height, including tiny IBD blocks, is read back from its file. `pruneheight = tip - 288` once `tip > 288`. Kept heights are `h > pruneheight`. A reorg at a kept height replaces that height's file. Disconnect at or below `pruneheight` fails closed. No `SCHEMA_VERSION` - bump: the mode is the `{store}/inwit.prune` sidecar. + bump: the mode is the `{store}/seqsigwit.prune` sidecar. **JSON vs wire:** serve **honest partial objects** (vout/status/txids we still have). Refuse **wire** (hex/raw/P2P block/tx) rather than invent vin, fee, @@ -46,7 +46,7 @@ BIP159 / Core: They are alternatives: a pruned Core node advertises `NETWORK_LIMITED|WITNESS`, **not** `NETWORK`. 288 is `MIN_BLOCKS_TO_KEEP` on the **active chain**. Our store window is the same 288 **heights** behind tip; orphan / stale blocks -at those heights are extra inwit, not a reason to drop below 288 heights. +at those heights are extra seqsigwit, not a reason to drop below 288 heights. A different constant, [`NODE_NETWORK_LIMITED_ALLOW_CONN_BLOCKS` = 144](../../crates/rbitcoin-net/src/peer.rs), is only for **outbound selection** (do not IBD-deep from a limited peer). Do @@ -60,23 +60,23 @@ asks `NETWORK` (full) peers; VERSION bits we *offer* are limited. ## Kept window (288 height files + RAM) -Unpruned nodes read `inwit.body`. Pruned nodes serve witness only from the +Unpruned nodes read `seqsigwit.body`. Pruned nodes serve witness only from the window: -- Confirm writes `store/inwit.window/{height}.bin` for the connected height. +- Confirm writes `store/seqsigwit.window/{height}.bin` for the connected height. - RAM holds those heights while the encoded input bytes stay under - `--prune-inwit-ram-threshold-bytes`. **`0` skips RAM** and every lookup + `--prune-seqsigwit-ram-threshold-bytes`. **`0` skips RAM** and every lookup reads the height file. - When the tip moves, files with `height <= pruneheight` are removed. `pruneheight` is `tip - 288` (a height, not a count of files). -- Enabling prune on an archive that already has `inwit.body` copies the kept +- Enabling prune on an archive that already has `seqsigwit.body` copies the kept heights into the window once. After that, serving does not depend on a - rolling stem. A datadir that already has `inwit.prune` and is opened - without `--prune-inwit` refuses to start. -- `--datadir-cold` still places `inwit.body` on the cold path. The window - stays under the hot store next to `inwit.prune`. + rolling stem. A datadir that already has `seqsigwit.prune` and is opened + without `--prune-seqsigwit` refuses to start. +- `--datadir-cold` still places `seqsigwit.body` on the cold path. The window + stays under the hot store next to `seqsigwit.prune`. -Do not punch `inwit.body`, and do not replace this window with a rolling +Do not punch `seqsigwit.body`, and do not replace this window with a rolling segment log. The operator choice is the full stem, or the 288-height window. ## Partial JSON vs 404 / `pruned` @@ -91,9 +91,9 @@ a coinbase; fake hex fails wallet verify. | Kind | Serve? | Why | |------|--------|-----| -| **Wire** (Electrum hex, Esplora `/hex` `/raw`, RPC `getrawtransaction` / `getblock` v0/v2, P2P `getdata` block/tx, BIP37 merkleblock) | **Refuse** | Not a Bitcoin tx without inwit. | +| **Wire** (Electrum hex, Esplora `/hex` `/raw`, RPC `getrawtransaction` / `getblock` v0/v2, P2P `getdata` block/tx, BIP37 merkleblock) | **Refuse** | Not a Bitcoin tx without seqsigwit. | | **Object fields we still own** (status, vout from txout, txids, outspend from `spent`, header JSON, SH history/utxo) | **Serve** | Independently true; wallets use these more than hex. | -| **vin / fee / size / weight / sigops / hex** | **Omit** (JSON) or refuse (wire) | Need inwit. Omit the keys; do not null them into `[]` / `0`. | +| **vin / fee / size / weight / sigops / hex** | **Omit** (JSON) or refuse (wire) | Need seqsigwit. Omit the keys; do not null them into `[]` / `0`. | Esplora JSON may add `"pruned": true` (extra key, same idea as Electrum `chain_tip`). Electrum `transaction.get` has no honest partial: both modes @@ -113,14 +113,14 @@ vout-only object that still claims to be `transaction.get`. - Confirm still records the height file (and RAM, unless the cap is 0) as the block connects. Dropping a height is deleting `{height}.bin` once it falls out of the window. -- COMPAT “Pruning / GUI | Not supported” becomes “inwit prune / NETWORK_LIMITED; +- COMPAT “Pruning / GUI | Not supported” becomes “seqsigwit prune / NETWORK_LIMITED; not Core `-prune` of headers/txout”. ## Out of scope Dropping `txout` / `spent` / headers / SH (that would break wallets). AssumeUTXO. BIP157. Core GUI. Pruning during IBD before the block is -connected. Arti/Tor (00–08). Thin-inwit (keep prevout refs, drop +connected. Arti/Tor (00–08). Thin-seqsigwit (keep prevout refs, drop scriptSig/witness) — that would unlock fee/vin-without-witness; not this plan. @@ -136,18 +136,18 @@ Heights **above** the watermark behave as today. Below: table. | `getblocktxn` | n/a (tip compact only) | Serve if the block is still in the window / cache | | Headers / `getheaders` | Unchanged | Unchanged | | VERSION `services` | — | Advertise `NETWORK_LIMITED\|WITNESS\|P2P_V2`, **not** `NETWORK` | -| Compact **receive** / mempool | Unchanged (wire + mempool, not historical inwit) | Unchanged | +| Compact **receive** / mempool | Unchanged (wire + mempool, not historical seqsigwit) | Unchanged | ### JSON-RPC ([`docs/rpc.md`](../rpc.md)) -| Method | Pruned (inwit gone) | Notes | +| Method | Pruned (seqsigwit gone) | Notes | |--------|---------------------|--------| | `getblockchaininfo` | `pruned: true`, `pruneheight: N` | Today hardcoded `pruned: false` | | `getblock` verbosity **0** (raw) | `-8` `Block not available (pruned data)` | Core needle | | `getblock` verbosity **1** | **Keep** (`block_txids` / `txid.body`) | txid list, no vin | -| `getblock` verbosity **2** | `-8` pruned | Full txs need inwit | +| `getblock` verbosity **2** | `-8` pruned | Full txs need seqsigwit | | `getblockheader` / `getblockhash` / `getblockcount` | Keep | Headers only | -| `getblockstats` | serve from `txstat` when stamped, else `-8` | Reconstruct + lazy-stamp if unstamped and inwit remains | +| `getblockstats` | serve from `txstat` when stamped, else `-8` | Reconstruct + lazy-stamp if unstamped and seqsigwit remains | | `getrawtransaction` | **`-8` `Transaction not available (pruned data)`** | Not `-5` unknown txid | | `decoderawtransaction` | n/a (client hex) | | | `gettxout` | **Keep** | Class A outs + spent | @@ -164,7 +164,7 @@ Heights **above** the watermark behave as today. Below: table. | `transaction.get_merkle` / `id_from_pos` | **Keep** | Merkle from txids + headers | | `scripthash.get_history` / `get_balance` / `listunspent` / subscribe | **Keep** | SH + txout + spent; history rows have no witness | | `blockchain.block.header(s)` | Keep | | -| `tweaks.subscribe` / `silentpayments.subscribe` | **Keep** if serve is txout-only (indexed path). Naive parent-inwit peek below watermark → skip/omit that height’s spend-side fields, do not Corrupt | Pin in tests | +| `tweaks.subscribe` / `silentpayments.subscribe` | **Keep** if serve is txout-only (indexed path). Naive parent-seqsigwit peek below watermark → skip/omit that height’s spend-side fields, do not Corrupt | Pin in tests | | `transaction.broadcast` | Keep | Mempool | ### Esplora REST (shipped + [#632](https://github.com/reardencode/rbitcoin/pull/632) 0.8 drop-in) @@ -173,7 +173,7 @@ Heights **above** the watermark behave as today. Below: table. (`/internal/*`, `?after_txid=`, `POST /addresses|scripthashes/txs`, `GET /broadcast`, `POST /txs/test`, tx JSON `sigops`, unix listen). -**Partial tx object** (when inwit is gone): `txid`, `version`, `locktime`, +**Partial tx object** (when seqsigwit is gone): `txid`, `version`, `locktime`, `vout[]` from txout, `status`, `"pruned": true`. **Omit** `vin`. Fill `fee` / `size` / `weight` from stamped `txstat` when those rows exist; unstamped pruned omits those keys too. Same shape on list rows so `/txs` @@ -183,10 +183,10 @@ paging matches `/txs/summary` (do not drop the txid from the page). |-------|---------------------------|-----| | `GET /tx/:txid` JSON | **200** partial object; `fee`/`size`/`weight` from `txstat` when stamped | vout+status still true; still omit `vin` | | `GET /tx/:txid/hex`, `/raw` | **404** body `pruned` | Wire | -| `GET /tx/:txid/status` | **Keep** (200) | Header + fk, no inwit | +| `GET /tx/:txid/status` | **Keep** (200) | Header + fk, no seqsigwit | | `GET /tx/:txid/merkle-proof` | **Keep** | txids | | `GET /tx/:txid/merkleblock-proof` | **404** | BIP37 needs full txs | -| `GET /tx/:txid/outspend(s)` | **Keep** | `spent` slot `vin` index, not inwit of the spent tx | +| `GET /tx/:txid/outspend(s)` | **Keep** | `spent` slot `vin` index, not seqsigwit of the spent tx | | `GET /block/:hash` JSON | **200**; omit `size`/`weight` (or only if we cannot compute them); `"pruned": true` | Header + txids; not a witness size | | `GET /block/:hash/raw` | **404** | Full witness block | | `GET /block/:hash/header` `/status` `/txids` `/txid/:i` | **Keep** | header + `txid.body` | @@ -196,7 +196,7 @@ paging matches `/txs/summary` (do not drop the txid from the page). | **632** `?after_txid=` | Keep (txid cursor) | | | **632** `POST /internal/txs` | Full JSON when in window; **partial** (not omitted) when pruned; unknown still omitted | Distinct from missing | | **632** `POST /internal/txs/outspends/*` | **Keep** | spent table | -| **632** `GET /broadcast`, `POST /txs/test`, mempool `/internal/mempool/*` | Keep | No archive inwit | +| **632** `GET /broadcast`, `POST /txs/test`, mempool `/internal/mempool/*` | Keep | No archive seqsigwit | | WS `address-transactions` / `block-transactions` | Partial object if reconstruct fails; do not send `vin: []` | Mempool path unchanged | ## Steps @@ -209,29 +209,29 @@ paging matches `/txs/summary` (do not drop the txid from the page). `Corrupt("invariant: …")`. `block_txids` still works. `txout` get still works below watermark. - **Red:** `cargo test -p rbitcoin-query reconstruct_pruned_returns_pruned_not_corrupt` - — tiny `/tmp` chain, set watermark, drop or stub inwit; reconstruct below + — tiny `/tmp` chain, set watermark, drop or stub seqsigwit; reconstruct below fails Pruned; above succeeds; `block_txids` both sides; outs still readable. - **Green:** watermark on Query/store (test stub first, durable in step 2). -- **Refactor:** one helper `inwit_available(fk) -> Result`. +- **Refactor:** one helper `seqsigwit_available(fk) -> Result`. - **Verify:** `cargo test -p rbitcoin-query reconstruct_pruned_` - **Done when:** the [cycle](../how-we-plan.md#the-cycle-red--green--refactor) closed and the slice is committed ### Step 2 — Durable pruneheight + 288 height files -- **Contract:** `--prune-inwit` / conf (default **off**). `{store}/inwit.prune` +- **Contract:** `--prune-seqsigwit` / conf (default **off**). `{store}/seqsigwit.prune` is a 4-byte LE `pruneheight` (`u32::MAX` = on, nothing dropped yet; missing file = off). After the tip passes 288 heights, `pruneheight = tip - 288`. - Kept witness is `store/inwit.window/{height}.bin` plus the RAM cache. - `--prune-inwit-ram-threshold-bytes 0` writes every height and retains none + Kept witness is `store/seqsigwit.window/{height}.bin` plus the RAM cache. + `--prune-seqsigwit-ram-threshold-bytes 0` writes every height and retains none in RAM. No schema bump. Reopen restores the sidecar. A pruned datadir opened without the flag refuses to start. Enabling prune on an existing - archive seeds the window from `inwit.body` for the kept heights. + archive seeds the window from `seqsigwit.body` for the kept heights. - **Red:** `prune_watermark_survives_reopen`; `prune_ram_window_drops_fks_on_disconnect_and_replace`; `prune_ram_threshold_zero_spills_tiny_blocks`; spill symlink outside the window is `Corrupt`. - **Green:** sidecar + height files + RAM cache. -- **Refactor:** no rolling `inwit.body`, no punch path on this table. +- **Refactor:** no rolling `seqsigwit.body`, no punch path on this table. - **Verify:** `cargo test -p rbitcoin-query prune_` - **Done when:** the [cycle](../how-we-plan.md#the-cycle-red--green--refactor) closed and the slice is committed @@ -287,17 +287,17 @@ paging matches `/txs/summary` (do not drop the txid from the page). ### Step 6 — OPERATOR / COMPAT / SCHEMA / rpc.md + NixOS module -- **Contract:** OPERATOR `--prune-inwit`, 288-height files under - `inwit.window/`, RAM cap (`0` = files only), `NETWORK_LIMITED`, cannot +- **Contract:** OPERATOR `--prune-seqsigwit`, 288-height files under + `seqsigwit.window/`, RAM cap (`0` = files only), `NETWORK_LIMITED`, cannot reorg through pruneheight. COMPAT prune row. `getblockchaininfo` fields. SCHEMA/sidecar bytes. Partial Esplora JSON. Do not copy this file into quality.md until scheduled. Module: - `pruneInwit`; `coldDataDir` already exists. Eval asserts `--prune-inwit`. + `pruneSeqSigWit`; `coldDataDir` already exists. Eval asserts `--prune-seqsigwit`. Runtime label only if tmpfiles / `ReadWritePaths` change. -- **Red:** eval assert for `--prune-inwit`. +- **Red:** eval assert for `--prune-seqsigwit`. - **Green:** module + eval + those doc owners. - **Verify:** `nix build .#checks.x86_64-linux.nixos-module-eval --no-link`; - grep `NETWORK_LIMITED`, `prune-inwit`, `inwit.window`. + grep `NETWORK_LIMITED`, `prune-seqsigwit`, `seqsigwit.window`. - **Done when:** the [cycle](../how-we-plan.md#the-cycle-red--green--refactor) closed and the slice is committed ## Test budget @@ -308,11 +308,11 @@ every OS. ## Risks / follow-ups -- Tweaks naive path parent inwit: must not Corrupt on pruned parents. +- Tweaks naive path parent seqsigwit: must not Corrupt on pruned parents. - mempool.space **frontend** may assume `vin` always present on `GET /tx`; personal-node wallets (Electrum history/utxo) do not. Partial JSON is for honest objects, not a claim that we are a full electrs archive. - Core functional prune scripts stay skip until we claim Core `-prune` (we do not). -- Thin-inwit (keep prevout fk+vout, drop scripts) would allow fee + vin +- Thin-seqsigwit (keep prevout fk+vout, drop scripts) would allow fee + vin without witness; separate plan if a wallet needs that. diff --git a/docs/personal-node-plans/README.md b/docs/personal-node-plans/README.md index 59061d8cf..52f06d56b 100644 --- a/docs/personal-node-plans/README.md +++ b/docs/personal-node-plans/README.md @@ -31,8 +31,8 @@ A node on CGNAT / no forwarded TCP still: not on the standing peer set. 5. Optionally **prunes witness** below a 288-height window. Kept heights are one file each plus a RAM cache (`0` means files only). The node advertises - BIP159 `NODE_NETWORK_LIMITED` ([09](./09-inwit-prune.md)). Unpruned nodes - keep `inwit.body`. + BIP159 `NODE_NETWORK_LIMITED` ([09](./09-seqsigwit-prune.md)). Unpruned nodes + keep `seqsigwit.body`. ## Constraints (all numbered files) @@ -74,7 +74,7 @@ A node on CGNAT / no forwarded TCP still: (see [06](./06-ephemeral-tor-broadcast.md)); public Electrum `hosts` gossip on P2P. - Core `-prune` of whole `blk` files / dropping `txout`+headers (09 is - **inwit-only**). + **seqsigwit-only**). ## Dandelion++ is not plan 06 @@ -123,7 +123,7 @@ any PR. **07** can land as soon as **01 + 03** exist. **08** can land as soon as | [06-ephemeral-tor-broadcast.md](./06-ephemeral-tor-broadcast.md) | Isolated SOCKS one-shot for locally submitted txs | | [07-p2p-onion-inbound.md](./07-p2p-onion-inbound.md) | P2P `--listen-onion`: ADD_ONION → loopback BIP324 accept | | [08-cjdns.md](./08-cjdns.md) | BIP155 CJDNS, `--cjdns-reachable`, `--only-net=cjdns` | -| [09-inwit-prune.md](./09-inwit-prune.md) | Unpruned `inwit.body`, or a 288-height window (`inwit.window/{height}.bin` + RAM; `0` = files only); BIP159 `NETWORK_LIMITED`; partial Esplora JSON, refuse wire | +| [09-seqsigwit-prune.md](./09-seqsigwit-prune.md) | Unpruned `seqsigwit.body`, or a 288-height window (`seqsigwit.window/{height}.bin` + RAM; `0` = files only); BIP159 `NETWORK_LIMITED`; partial Esplora JSON, refuse wire | NixOS first-class options (same PR as the flags). Label **`nixos-module-runtime`** when the row says runtime: @@ -139,7 +139,7 @@ when the row says runtime: | 06 | none if `--proxy` already implies isolated broadcast; document on `proxy` | eval | | 07 | `p2p.listenOnion`; compose with `p2p.listen = false` | **yes** | | 08 | `cjdns.reachable`; listen on cjdns; `After=cjdns.service` | **yes** | -| 09 | `pruneInwit` (cold dir already exists) | eval | +| 09 | `pruneSeqSigWit` (cold dir already exists) | eval | ## Follow-ups (not scheduled here) diff --git a/docs/rpc.md b/docs/rpc.md index bf477536a..79a810058 100644 --- a/docs/rpc.md +++ b/docs/rpc.md @@ -83,11 +83,11 @@ still wait for durable SH when shindex is on. |--------|-------| | `help` / `getrpcinfo` / `uptime` / `stop` | Control | | `echo` | Testing RPC. Returns arguments as a positional array. AuthServiceProxy `{args: [...], argN: ...}` is peeled only here. Mixed `submitpackage`/`sendrawtransaction`/`testmempoolaccept` `{args, maxfeerate}` is expanded in the Core-functional proxy, not on the node. | -| `getblockchaininfo` / `getblockcount` / `getbestblockhash` / `getblockhash` | Chain tip. `getblockcount` / `getbestblockhash` wait for the in-flight tip-accept job (not the rest of a catch-up burst). `headers` is the best known header height (`submitheader` / P2P headers may lead `blocks`). `chainwork` is summed header work (regtest 2 per block). `size_on_disk` is a walk of `{datadir}/store` file lengths (plus `--datadir-cold` inwit when split). `verificationprogress` is `blocks / headers` clamped to `[0, 1]` (`1.0` when `headers` is 0). `initialblockdownload` is the Core RPC name for **relay-inhibited**: `--min-chain-work` and `--max-tip-age` after densify/`enter_tip_mode`, not “still catching up”. `--prune-inwit` sets `pruned: true` and `pruneheight` (omitted when off). | +| `getblockchaininfo` / `getblockcount` / `getbestblockhash` / `getblockhash` | Chain tip. `getblockcount` / `getbestblockhash` wait for the in-flight tip-accept job (not the rest of a catch-up burst). `headers` is the best known header height (`submitheader` / P2P headers may lead `blocks`). `chainwork` is summed header work (regtest 2 per block). `size_on_disk` is a walk of `{datadir}/store` file lengths (plus `--datadir-cold` seqsigwit when split). `verificationprogress` is `blocks / headers` clamped to `[0, 1]` (`1.0` when `headers` is 0). `initialblockdownload` is the Core RPC name for **relay-inhibited**: `--min-chain-work` and `--max-tip-age` after densify/`enter_tip_mode`, not “still catching up”. `--prune-seqsigwit` sets `pruned: true` and `pruneheight` (omitted when off). | | `getblockheader` / `getblock` (verbosity 0/1/2) | Archive reconstruct. `getblockheader` includes `chainwork`. Verbosity 0/2 below `pruneheight` is `-8` `Block not available (pruned data)`; verbosity 1 (txids) stays. | | `getblockstats` | All networks. Stamped `txstat` fee/size/weight/`n_in`. Size and count fields match Core for the keys we return: coinbase is left out of `ins`, fees, `total_size`, `total_weight`, and the segwit totals. `utxo_increase` is every output minus those inputs. `utxo_increase_actual` drops outputs that do not enter the UTXO set (height 0, the two mainnet BIP30-repeat coinbases, unspendable scripts). Omits coins-DB `utxo_size_inc` / `utxo_size_inc_actual` (selecting those names is invalid statistic `-8`). Unstamped leftover reconstructs then lazy-stamps. Reconstruct miss is `block body not in store`. Dummy `blk00000.dat` is shim-only so `rpc_getblockstats.py`'s rename-file needle stays Core-phrased. Pruned unstamped height is `-8` Core pruned text. | | `getdifficulty` | From tip bits | -| `getnetworkinfo` / `getconnectioncount` / `getpeerinfo` | BIP324 v2-only; `getpeerinfo` is the live session table. `timeoffset` is VERSION clock minus connect time (`0` before handshake). `synced_headers` is the height of that peer's advertised best block when we know it, else `-1`. `synced_blocks` is that height when the hash is on our best chain, else `-1`. `getnetworkinfo.timeoffset` is the median of outbound handshake-complete offsets (`0` if none). `mapped_as` is present when `--asmap` / `{datadir}/ip_asn.dat` mapped the peer (Core field; omitted without a map or ASN 0). `version` is rbitcoin semver as a Core integer (`major*10000+minor*100+patch`: `0.1.0` → `100`, `0.5.0` → `500`, `0.6.0` → `600`, `0.6.99` → `699`, `0.7.0` → `700`, `0.7.99` → `799`), not a Core release. `localservices` matches advertised `NETWORK\|WITNESS\|P2P_V2`, or `NETWORK_LIMITED\|WITNESS\|P2P_V2` under `--prune-inwit`. `localaddresses` lists `--external-ip` (`score` = Core `LOCAL_MANUAL`) | +| `getnetworkinfo` / `getconnectioncount` / `getpeerinfo` | BIP324 v2-only; `getpeerinfo` is the live session table. `timeoffset` is VERSION clock minus connect time (`0` before handshake). `synced_headers` is the height of that peer's advertised best block when we know it, else `-1`. `synced_blocks` is that height when the hash is on our best chain, else `-1`. `getnetworkinfo.timeoffset` is the median of outbound handshake-complete offsets (`0` if none). `mapped_as` is present when `--asmap` / `{datadir}/ip_asn.dat` mapped the peer (Core field; omitted without a map or ASN 0). `version` is rbitcoin semver as a Core integer (`major*10000+minor*100+patch`: `0.1.0` → `100`, `0.5.0` → `500`, `0.6.0` → `600`, `0.6.99` → `699`, `0.7.0` → `700`, `0.7.99` → `799`), not a Core release. `localservices` matches advertised `NETWORK\|WITNESS\|P2P_V2`, or `NETWORK_LIMITED\|WITNESS\|P2P_V2` under `--prune-seqsigwit`. `localaddresses` lists `--external-ip` (`score` = Core `LOCAL_MANUAL`) | | `getnettotals` | All networks. Raw TCP `totalbytesrecv` / `totalbytessent` on live sessions. `uploadtarget` is a Core-shaped stub (`target` 0). | | `ping` | All networks. Queues a ping on each live session (`null`). | | `addpeeraddress` | Hidden Core name. Inserts `{address,port}` into addrman RAM (does not rewrite `peers` per call). | diff --git a/nix/modules/rbitcoin.nix b/nix/modules/rbitcoin.nix index 96af4e881..5e9738bb3 100644 --- a/nix/modules/rbitcoin.nix +++ b/nix/modules/rbitcoin.nix @@ -99,7 +99,7 @@ let ++ optional cfg.i2p.acceptIncoming "--i2p-accept-incoming" ++ optional cfg.cjdns.reachable "--cjdns-reachable" ++ optional cfg.esplora.hiddenService "--esplora-onion" - ++ optional cfg.pruneInwit "--prune-inwit" + ++ optional cfg.pruneSeqSigWit "--prune-seqsigwit" ++ cfg.extraArgs; in { @@ -126,7 +126,7 @@ in coldDataDir = mkOption { type = types.nullOr types.path; default = null; - description = "Optional directory for the large, rarely read Class A inwit store."; + description = "Optional directory for the large, rarely read Class A seqsigwit store."; }; user = mkOption { @@ -177,10 +177,10 @@ in description = "Build and serve the BIP-352 silent-payment tweak index."; }; - pruneInwit = mkOption { + pruneSeqSigWit = mkOption { type = types.bool; default = false; - description = "Drop Class A inwit below tip-288 heights and advertise NETWORK_LIMITED. Not Core -prune of headers/txout."; + description = "Drop Class A seqsigwit below tip-288 heights and advertise NETWORK_LIMITED. Not Core -prune of headers/txout."; }; environment = mkOption { diff --git a/nix/tests/nixos-module-eval.nix b/nix/tests/nixos-module-eval.nix index 01e43eb0d..9ae28ebad 100644 --- a/nix/tests/nixos-module-eval.nix +++ b/nix/tests/nixos-module-eval.nix @@ -45,7 +45,7 @@ let acceptIncoming = true; }; cjdns.reachable = true; - pruneInwit = true; + pruneSeqSigWit = true; p2p = { address = "127.0.0.1"; openFirewall = true; @@ -151,7 +151,7 @@ assert builtins.match ".*--i2p-sam 127.0.0.1:7656.*" execStart != null; assert builtins.match ".*--i2p-accept-incoming.*" execStart != null; assert builtins.match ".*--listen-onion.*" execStart != null; assert builtins.match ".*--cjdns-reachable.*" execStart != null; -assert builtins.match ".*--prune-inwit.*" execStart != null; +assert builtins.match ".*--prune-seqsigwit.*" execStart != null; assert builtins.elem "tor.service" service.after; assert builtins.elem "tor.service" service.wants; assert builtins.elem "i2pd.service" service.after; diff --git a/scripts/core-functional/inventory.toml b/scripts/core-functional/inventory.toml index 0cc550454..c46df9ed9 100644 --- a/scripts/core-functional/inventory.toml +++ b/scripts/core-functional/inventory.toml @@ -341,7 +341,7 @@ status = "run" name = "feature_unsupported_utxo_db.py" status = "skip" reason = "no-utxo-set" -analog = "schema refuse / inwit.reloc mismatch" +analog = "schema refuse / seqsigwit.reloc mismatch" [[test]] name = "feature_utxo_set_hash.py"