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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ before 1.0).

### Fixed

- **Spent-range never reads `txout.body`:** `tx_spent_range*` prefix-sums RAM
`n_out` from Class A append (open hydrates that vec from LAYOUT17 meta).
Missing `n_out` is `Corrupt("invariant: spent n_out missing")`.

- **Compact reconstruct merkle-checks before `Ok`:** a unique short-id (or
`blocktxn`) fill is not a block until the txs match the compact header
merkle (BIP152 `FinishBlock`). Empty missing → `getdata`, not
Expand Down
4 changes: 2 additions & 2 deletions SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Version:** `SCHEMA_VERSION = 21` (`rbitcoin_primitives`).
**Status:** 21 drops `spent.idx`. Spent `(off,len)` is `8 × max(n_out,1)` from
txout meta; sparse `spent.off` (u64/1024 creates). A 21 binary unlinks leftover
RAM `n_out` (sparse `spent.off`, u64/1024 creates). A 21 binary unlinks leftover
`spent.idx` (dir and flat `spent.idx.meta`) and rewrites `store/meta` 20→21.
A 20 binary refuses 21 `meta`. Occupied schema 18/19 `tx.head` or
`scripthash*` is **refused** (wipe those index dirs, keep Class A). Empty
Expand Down Expand Up @@ -361,7 +361,7 @@ i = fk - first_fk

Hard span per segment: `2^32 × 8` ≈ 32 GiB. Soft rollover earlier (default 16 GiB; `RBITCOIN_TX_IDX_SOFT_SPAN`). **Each stem rolls independently** when that stem’s next start would exceed the soft span (`inwit` no longer forces `txout` idx splits). Length: `start(fk+1) − start(fk)` (may cross segments); last record uses published body end. ~**4 B/tx** vs prior 8 B absolute u64 index (~50% smaller).

**`spent.body` has no `spent.idx`.** Record length is `8 × max(n_out, 1)` (zero-out still pays one stride so starts stay monotone). `spent_abs(off, vout) = off + 8×vout`. `n_out` is txout LAYOUT17 meta. Sparse `spent.off` stores absolute starts every 1024 creates (`ArrayLink` u64 LE). Leftover `spent.idx/` (and flat `spent.idx.meta`) is unlinked on open; `store/meta` is rewritten to 21.
**`spent.body` has no `spent.idx`.** Record length is `8 × max(n_out, 1)` (zero-out still pays one stride so starts stay monotone). `spent_abs(off, vout) = off + 8×vout`. `n_out` is Class A append RAM. Sparse `spent.off` stores absolute starts every 1024 creates (`ArrayLink` u64 LE). Spent-range APIs prefix-sum that vec and never read `txout.body`. Open hydrates the vec from LAYOUT17 meta once. Leftover `spent.idx/` (and flat `spent.idx.meta`) is unlinked on open; `store/meta` is rewritten to 21.

### Input encoding (embedded)

Expand Down
19 changes: 13 additions & 6 deletions crates/rbitcoin-store/src/tx_table/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ impl TxTable {
}
let n_bodies = body.count();
spent_off.ensure_covering(&body, n_bodies)?;
let spent_end = spent_off.end_for(&body, n_bodies)?;
let spent_end = spent_off.end_for(n_bodies)?;
if spent.body_logical_len() < spent_end {
return Err(StoreError::Corrupt("spent.body short for n_out prefix"));
}
Expand Down Expand Up @@ -1050,15 +1050,14 @@ impl TxTable {
self.body.record_range_batch(fks)
}

/// `spent.body` range for one create.
/// `spent.body` range for one create (RAM `n_out` + `spent.off`; no `txout.body`).
pub fn spent_range(&self, fk: Fk) -> Result<(u64, u64), StoreError> {
self.spent_off.range_for(&self.body, fk, self.spent.count())
self.spent_off.range_for(fk, self.spent.count())
}

/// `spent.body` ranges (same fk order as [`Self::body_range_batch`]).
pub fn spent_range_batch(&self, fks: &[Fk]) -> Result<Vec<Option<(u64, u64)>>, StoreError> {
self.spent_off
.ranges_batch(&self.body, fks, self.spent.count())
self.spent_off.ranges_batch(fks, self.spent.count())
}

/// Annotate spends at known absolute spender-meta offsets (confirm write).
Expand Down Expand Up @@ -1516,6 +1515,7 @@ impl TxTable {
if self.inwit.count() != base || self.spent.count() != base {
return Err(StoreError::Corrupt("Class A stem count mismatch on append"));
}
let n_outs: Vec<u32> = items.iter().map(|(_, _, outs)| outs.len() as u32).collect();
let fks = self.append_stems_one_wave(
items.len(),
est_out,
Expand All @@ -1527,6 +1527,7 @@ impl TxTable {
},
|i, buf| encode_inwit_with_secret(&items[i].1, buf, Some(&self.secret)),
|i, buf| encode_spent_zeros(items[i].2.len() as u32, buf),
&n_outs,
)?;
let ids: Vec<[u8; 32]> = items.iter().map(|(tx, _, _)| tx.txid).collect();
self.txids.append_batch(base, &ids)?;
Expand Down Expand Up @@ -1591,6 +1592,10 @@ impl TxTable {
encode_spent_slot_v17(0, fk)?;
}
}
let n_outs: Vec<u32> = items
.iter()
.map(|(pin, _)| pin.as_ref().1.len() as u32)
.collect();
let fks = self.append_stems_one_wave(
items.len(),
est_out,
Expand All @@ -1608,6 +1613,7 @@ impl TxTable {
encode_spent_slots(outs.len() as u32, pairs, buf)
.expect("spent overlay prechecked");
},
&n_outs,
)?;
let ids: Vec<[u8; 32]> = items.iter().map(|(pin, _)| pin.0.txid).collect();
self.txids.append_batch(base, &ids)?;
Expand Down Expand Up @@ -1635,6 +1641,7 @@ impl TxTable {
encode_out: impl FnMut(usize, &mut Vec<u8>),
encode_in: impl FnMut(usize, &mut Vec<u8>),
encode_sp: impl FnMut(usize, &mut Vec<u8>),
n_outs: &[u32],
) -> Result<Vec<Fk>, StoreError> {
let Some(p_out) = self.body.prepare_batch_encode(n, est_out, encode_out)? else {
return Ok(Vec::new());
Expand All @@ -1660,7 +1667,7 @@ impl TxTable {
"Class A append fk mismatch across stems",
));
}
self.spent_off.note_starts(sp_base, &sp_starts);
self.spent_off.note_starts(sp_base, &sp_starts, n_outs);
Ok(fks)
}

Expand Down
Loading