Skip to content

store: parent edges for pruned explorer links - #683

Open
rearden-grok[bot] wants to merge 9 commits into
store/txfixedfrom
store/inputs-edge
Open

rearden-grok[bot] wants to merge 9 commits into
store/txfixedfrom
store/inputs-edge

Conversation

@rearden-grok

@rearden-grok rearden-grok Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #659 (store/txfixed). Amends unreleased schema 25. Do not merge #659 until this series is in the same ship, so master never ships n_in inside txstat.body. No bump to 26.

  • inputs.loc is n_in as u16 per create (0 = unstamped). n_in > u16::MAX is Corrupt.
  • inputs.body is 8 bytes per input in vin order: parent create_fk (u40, 0 = coinbase) and parent vout (u24). The spending vin is the record index (spent.body already stores it).
  • inputs.off checkpoints every 1024 creates. Confirm appends the edge with Class A. Skew truncate drops it with the other stems.
  • txstat.body stays 8 bytes: three ULEBs (fee_sat, base, wit_extra). Ordinary large-fee or large-witness rows no longer overflow. A still-too-wide fee plus size still tails (txstat.ovf / txstat.blk). All-zero is unstamped.
  • A four-ULEB cell that started with n_in (the earlier unreleased 25 experiment) is not detected and is not rewritten. Resync that datadir.
  • getblockstats ins and the range-load coinbase hint read n_in from inputs.loc when the txout meta count is 0.
  • Pruned Esplora vin is the parent txid, vout, and prevout value/script. scriptSig, witness, and sequence stay omitted. Child links stay on spent.body.
  • inwit is renamed to seqsigwit (sequence, scriptSig, witness). Table kind id stays 16. Open renames existing inwit.* files in place (same bytes).
  • txstat.* and inputs.* live next to seqsigwit, on the cold datadir when --datadir-cold is set. A split store that still has those stems only on the hot dir is a layout error. inputs and txstat are not pruned. The 288-height window applies only to seqsigwit.
  • Open with creates, no inputs.loc, and a matching seqsigwit count backfills the parent edges from seqsigwit prevouts (decode_prevout_at). A null parent becomes the coinbase edge (vout 0).

Test plan

  • cargo test -p rbitcoin-store --lib reopen_backfills_inputs_from_seqsigwit
  • cargo test -p rbitcoin-store --lib inputs_n_in_scans_both_prevouts
  • cargo clippy -p rbitcoin-store --lib -- -D warnings
  • required CI

Do not merge unless asked.

let Some(edges) = query.store().input_edges(tx_fk)? else {
return Ok(None);
};
let mut vin = Vec::with_capacity(edges.len());
q.set_pruneheight(Some(Height(0))).unwrap();
let mut spend_txid = [0u8; 32];
spend_txid[31] = 0xee;
let h1_hash = rbitcoin_store::block_header_hash(1, &merkle, &spend_txid, 2, 0x207fffff, 0);
assert_eq!(v["pruned"], true);
assert!(v.get("vin").is_none(), "pruned JSON omits vin: {v}");
assert_eq!(v["vin"][0]["is_coinbase"], true);
assert!(v["vin"][0].get("witness").is_none(), "{v}");
assert!(v.get("vin").is_none(), "pruned JSON omits vin: {v}");
assert_eq!(v["vin"][0]["is_coinbase"], true);
assert!(v["vin"][0].get("witness").is_none(), "{v}");
assert!(v["vin"][0].get("scriptsig").is_none(), "{v}");
assert!(v["vin"][0].get("witness").is_none(), "{v}");
assert!(v["vin"][0].get("scriptsig").is_none(), "{v}");
let sv = build_tx_json(&q, spend_fk, Network::Regtest).unwrap();
assert_eq!(sv["pruned"], true, "{sv}");
assert_eq!(sv["vin"][0]["txid"], block_hash_hex(&txid));
assert_eq!(sv["vin"][0]["vout"], 0);
assert_eq!(sv["vin"][0]["is_coinbase"], false);
assert!(sv["vin"][0].get("witness").is_none(), "{sv}");
let raw = build_tx_json(&q, fk, Network::Regtest).unwrap();
assert_eq!(raw["pruned"], true);
assert!(raw.get("vin").is_none(), "{raw}");
assert_eq!(raw["vin"][0]["is_coinbase"], true, "{raw}");
inputs.loc stores n_in and inputs.body stores one 8-byte parent
create_fk plus vout per input, in vin order. Class A append writes
that edge with txstat. A coinbase is a null parent. n_in remains in
the txstat cell until the cell is narrowed.
The 8-byte econ cell is fee, base size, and witness extra. Input count
for getblockstats, txout overlay, and the multi-input coinbase hint
comes from the parent-edge locator. A row that used to overflow because
of the n_in byte now fits; a wide fee and size still tails.
A pruned transaction JSON lists each input's parent txid and vout, and
the parent output value and script from txout. scriptSig, witness, and
sequence stay absent.
Sequence, scriptSig, and witness stay in seqsigwit. Open renames a
leftover inwit stem in place. txstat and inputs live in the same
directory as that stem, including under --datadir-cold.
A datadir that already has creates but no inputs.loc recovers parent
links by walking seqsigwit prevouts once. The locator probe runs after
that fill, so a missing stem is not NotFound. Four-field txstat cells
from the unreleased experiment stay a resync.
Inputs::append_unstamped scored CRAP 132 with no caller in tests, which
failed the coverage job above 30. The test walks a zero count, a window
checkpoint, and a later real edge.
New records are sequence, scriptSig, and witness. Flag bit 4 marks that
the parent edge lives on inputs.body, and loads stamp it back onto the
in-memory input. A record without that bit is still the legacy inline
prevout, so a 0.7 datadir can backfill.
The other Class A stems are singular. inputs.loc, inputs.off, and
inputs.body become input.*. Kind ids 22 and 23 stay. Open renames a
leftover inputs.* stem.
decode_prevout_at scored CRAP 38.5 after the parent edge left the
record. The sequence, script, and witness skip is its own functions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant