Skip to content
Merged
7 changes: 7 additions & 0 deletions changelog.d/write-thread-cpu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Changed

- Confirm write collects each block's spend absolute offsets once, reuses
the structural scratch and the in-batch double-spend set, and looks up
create heights by foreign-key span when every block in the batch is
contiguous. The tip event carries the wire header already validated on
the write path.
197 changes: 171 additions & 26 deletions crates/rbitcoin-consensus/src/block/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1499,6 +1499,13 @@ impl Default for StructuralScratch {
}

impl StructuralScratch {
/// Drop annotate slots from the previous write batch. Per-block buffers
/// stay for [`Self::begin_block`]; slots accumulate inside one batch.
pub(crate) fn begin_batch(&mut self) {
self.slots.abs_edges.clear();
self.slots.known.clear();
}

fn begin_block(&mut self) {
self.abs_jobs.clear();
self.abs_seen.clear();
Expand Down Expand Up @@ -1539,8 +1546,9 @@ pub(crate) fn structural_validate_spends(
pending_spent: &mut rbitcoin_query::OutPointSet,
batch_parents: &rbitcoin_query::BatchParents,
mtp_cache: &mut U32Map<u32>,
run_create_height: &FkMap<u32>,
run_create_height: &RunCreateHeight,
scratch: &mut StructuralScratch,
precomputed_abs: Option<&[StructuralAbsJob]>,
) -> Result<StructuralPhaseNs, ConsensusError> {
use std::time::Instant;

Expand All @@ -1550,7 +1558,14 @@ pub(crate) fn structural_validate_spends(
let t_spent = Instant::now();
reject_bip30_unspent_overwrite(query, block, ctx)?;
let t_abs = Instant::now();
structural_abs_heights(query, spends, batch_parents, run_create_height, scratch)?;
structural_abs_heights(
query,
spends,
batch_parents,
run_create_height,
scratch,
precomputed_abs,
)?;
let tip = query.tip_height().map(|h| h.0);
let mut spent_strong_ns = 0u64;
let mut multi_list_ns = 0u64;
Expand Down Expand Up @@ -1592,7 +1607,82 @@ pub(crate) fn structural_validate_spends(
})
}

type StructuralAbsJob = (u64, u32, u64, rbitcoin_primitives::Fk, u32);
pub(crate) type StructuralAbsJob = (u64, u32, u64, rbitcoin_primitives::Fk, u32);

/// Create heights for one write batch.
///
/// Contiguous per-block fk spans are the IBD shape (`first` + count). A gap
/// or overlap uses the map so a fk between spans does not inherit a height.
pub(crate) enum RunCreateHeight {
Spans(Vec<(u64, u64, u32)>),
Map(FkMap<u32>),
}

impl RunCreateHeight {
pub(crate) fn from_blocks<'a>(
blocks: impl IntoIterator<Item = (u32, &'a [rbitcoin_primitives::Fk])>,
) -> Self {
let blocks: Vec<(u32, &'a [rbitcoin_primitives::Fk])> = blocks.into_iter().collect();
if let Some(spans) = contiguous_create_spans(&blocks) {
Self::Spans(spans)
} else {
let mut map = FkMap::default();
for (height, fks) in blocks {
for fk in fks {
map.insert(*fk, height);
}
}
Self::Map(map)
}
}

pub(crate) fn get(&self, fk: rbitcoin_primitives::Fk) -> Option<u32> {
match self {
Self::Map(map) => map.get(&fk).copied(),
Self::Spans(spans) => {
let id = fk.get()?;
let i = spans.partition_point(|span| span.0 <= id);
let (_, end, height) = spans.get(i.checked_sub(1)?)?;
(*end > id).then_some(*height)
}
}
}
}

/// `Some` when every non-empty block is `[first, first+n)` and spans do not overlap.
///
/// A null fk, a gap, or an id that would wrap is `None` so the caller uses the map.
/// A wrapped end must not cover fks that are not in the block.
fn contiguous_create_spans(
blocks: &[(u32, &[rbitcoin_primitives::Fk])],
) -> Option<Vec<(u64, u64, u32)>> {
let mut spans = Vec::with_capacity(blocks.len());
for &(height, fks) in blocks {
if fks.is_empty() {
continue;
}
let mut ids = fks.iter().map(|fk| fk.get());
let first = ids.next().flatten()?;
let mut prev = first;
for id in ids {
let id = id?;
let next = prev.checked_add(1)?;
if id != next {
return None;
}
prev = id;
}
let end = prev.checked_add(1)?;
spans.push((first, end, height));
}
spans.sort_unstable_by_key(|span| span.0);
for pair in spans.windows(2) {
if pair[0].1 > pair[1].0 {
return None;
}
}
Some(spans)
}
type DurableSpentSet =
std::collections::HashSet<(u64, u32), BuildHasherDefault<rbitcoin_query::OutPointHasher>>;
type OverlayMetaSkip = std::collections::HashMap<
Expand All @@ -1609,11 +1699,11 @@ fn fill_overlay_skip(
rbitcoin_primitives::Fk,
u32,
)],
run_create_height: &FkMap<u32>,
run_create_height: &RunCreateHeight,
scratch: &mut StructuralScratch,
) {
for &(_, vout, sfk, cfk, vin) in spends {
if !run_create_height.contains_key(&cfk) {
if run_create_height.get(cfk).is_none() {
continue;
}
let Some(id) = cfk.get() else {
Expand Down Expand Up @@ -1649,18 +1739,39 @@ fn structural_abs_heights(
u32,
)],
batch_parents: &rbitcoin_query::BatchParents,
run_create_height: &FkMap<u32>,
run_create_height: &RunCreateHeight,
scratch: &mut StructuralScratch,
precomputed_abs: Option<&[StructuralAbsJob]>,
) -> Result<(), ConsensusError> {
batch_parents
.spend_abs_jobs_into(
spends
.iter()
.map(|&(_, vout, sfk, cfk, vin)| (cfk, vout, sfk, vin)),
&mut scratch.abs_jobs,
&mut scratch.abs_seen,
)
.map_err(ConsensusError::from)?;
if let Some(jobs) = precomputed_abs {
// Tests recompute so a stale list cannot skip a missing abs.
#[cfg(test)]
{
let fresh = batch_parents
.spend_abs_jobs(
spends
.iter()
.map(|&(_, vout, sfk, cfk, vin)| (cfk, vout, sfk, vin)),
)
.map_err(ConsensusError::from)?;
if fresh.as_slice() != jobs {
return Err(ConsensusError::Store(rbitcoin_store::StoreError::Corrupt(
"invariant: precomputed spend abs diverged from pin",
)));
}
}
scratch.abs_jobs.extend_from_slice(jobs);
} else {
batch_parents
.spend_abs_jobs_into(
spends
.iter()
.map(|&(_, vout, sfk, cfk, vin)| (cfk, vout, sfk, vin)),
&mut scratch.abs_jobs,
&mut scratch.abs_seen,
)
.map_err(ConsensusError::from)?;
}
scratch.unique_fks.extend(
scratch
.abs_jobs
Expand All @@ -1677,7 +1788,7 @@ fn structural_abs_heights(
let Some(id) = fk.get() else {
continue;
};
let Some(h) = h.or_else(|| run_create_height.get(fk).copied()) else {
let Some(h) = h.or_else(|| run_create_height.get(*fk)) else {
continue;
};
scratch.height_by_id.insert(id, h);
Expand Down Expand Up @@ -1836,20 +1947,16 @@ fn structural_mark_pending(
) -> Result<(), ConsensusError> {
for &(prev_txid, vout, _spend_fk, create_fk, _vin) in spends {
let key = (prev_txid, vout);
if pending_spent.contains(&key) {
return Err(ConsensusError::PrevoutSpent);
}
let spent = if create_fk.is_null() {
false
} else if let Some(id) = create_fk.get() {
durable_spent.contains(&(id, vout))
} else {
false
};
if spent {
if spent || !pending_spent.insert(key) {
return Err(ConsensusError::PrevoutSpent);
}
pending_spent.insert(key);
}
Ok(())
}
Expand Down Expand Up @@ -2315,8 +2422,9 @@ mod overlay_meta_skip_tests {

#[test]
fn overlay_meta_skip_omits_matching_abs() {
let mut run = FkMap::default();
run.insert(Fk(10), 5);
let mut map = FkMap::default();
map.insert(Fk(10), 5);
let run = RunCreateHeight::Map(map);
let spends = spends(&[(0, Fk(11), Fk(10), 0)]);
let mut scratch = StructuralScratch::default();
fill_overlay_skip(&spends, &run, &mut scratch);
Expand All @@ -2328,8 +2436,9 @@ mod overlay_meta_skip_tests {

#[test]
fn overlay_meta_skip_keeps_conflicting_spender_on_disk_list() {
let mut run = FkMap::default();
run.insert(Fk(10), 5);
let mut map = FkMap::default();
map.insert(Fk(10), 5);
let run = RunCreateHeight::Map(map);
let spends = spends(&[(0, Fk(11), Fk(10), 0), (0, Fk(12), Fk(10), 0)]);
let mut scratch = StructuralScratch::default();
fill_overlay_skip(&spends, &run, &mut scratch);
Expand All @@ -2339,14 +2448,50 @@ mod overlay_meta_skip_tests {

#[test]
fn overlay_meta_skip_historical_create_stays_on_disk() {
let run = FkMap::default();
let run = RunCreateHeight::Spans(Vec::new());
let spends = spends(&[(0, Fk(11), Fk(10), 0)]);
let mut scratch = StructuralScratch::default();
fill_overlay_skip(&spends, &run, &mut scratch);
assert!(!overlay_meta_is_skip(10, 0, Fk(11), 0, &scratch.skip));
}
}

#[cfg(test)]
mod run_create_height_tests {
use super::RunCreateHeight;
use rbitcoin_primitives::Fk;

fn fk(id: u64) -> Fk {
Fk(id)
}

#[test]
fn run_create_height_span_misses_gaps() {
let one = [fk(10), fk(11)];
let idx = RunCreateHeight::from_blocks([(7u32, one.as_slice())]);
assert!(matches!(idx, RunCreateHeight::Spans(_)));
assert_eq!(idx.get(fk(10)), Some(7));
assert_eq!(idx.get(fk(11)), Some(7));
assert_eq!(idx.get(fk(9)), None);
assert_eq!(idx.get(fk(12)), None);

let low = [fk(10), fk(11)];
let high = [fk(20), fk(21)];
let idx = RunCreateHeight::from_blocks([(1u32, low.as_slice()), (2, high.as_slice())]);
assert_eq!(idx.get(fk(15)), None);
assert_eq!(idx.get(fk(10)), Some(1));
assert_eq!(idx.get(fk(21)), Some(2));

let gapped = [fk(1), fk(2), fk(4)];
let idx = RunCreateHeight::from_blocks([(3u32, gapped.as_slice())]);
assert!(matches!(idx, RunCreateHeight::Map(_)));
assert_eq!(idx.get(fk(3)), None);
assert_eq!(idx.get(fk(1)), Some(3));
assert_eq!(idx.get(fk(2)), Some(3));
assert_eq!(idx.get(fk(4)), Some(3));
}
}

#[cfg(test)]
mod bip34_tests;
#[cfg(test)]
Expand Down
24 changes: 15 additions & 9 deletions crates/rbitcoin-consensus/src/block/structure_rule_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,9 +345,9 @@ fn padded_spend(data_len: usize) -> Transaction {
/// 91842/91880 grandfather). Signet activates BIP34 at height 1, and Core's
/// empty BIP34 hash still enforces BIP30 on every signet block.
fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) {
use crate::block::structural_validate_spends;
use crate::block::{structural_validate_spends, RunCreateHeight};
use rbitcoin_primitives::Fk;
use rbitcoin_query::{BatchParents, FkMap, OutPointSet, U32Map};
use rbitcoin_query::{BatchParents, OutPointSet, U32Map};
let dup = block_with(vec![first.clone()]);
let signet = Box::leak(Box::new(ChainParams::signet()));
for (ctx, net) in [
Expand All @@ -357,6 +357,7 @@ fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) {
"signet",
),
] {
let heights = RunCreateHeight::Spans(Vec::new());
let err = structural_validate_spends(
q,
&dup,
Expand All @@ -367,8 +368,9 @@ fn rejects_unspent_overwrite(q: &rbitcoin_query::Query, first: &Transaction) {
&mut OutPointSet::default(),
&BatchParents::new(),
&mut U32Map::default(),
&FkMap::default(),
&heights,
&mut crate::block::StructuralScratch::default(),
None,
)
.expect_err("an unspent overwrite must trip BIP30");
let msg = format!("{err}");
Expand Down Expand Up @@ -438,10 +440,11 @@ fn bip30_message_at_mainnet_above_bip34(
q: &rbitcoin_query::Query,
block: &Block,
) -> Result<(), ConsensusError> {
use crate::block::structural_validate_spends;
use rbitcoin_query::{BatchParents, FkMap, OutPointSet, U32Map};
use crate::block::{structural_validate_spends, RunCreateHeight};
use rbitcoin_query::{BatchParents, OutPointSet, U32Map};
let main = Box::leak(Box::new(ChainParams::mainnet()));
let ctx = ValidationContext::at(main, Height(main.btc.bip34_height + 1), Milestone::NONE);
let heights = RunCreateHeight::Spans(Vec::new());
structural_validate_spends(
q,
block,
Expand All @@ -452,8 +455,9 @@ fn bip30_message_at_mainnet_above_bip34(
&mut OutPointSet::default(),
&BatchParents::new(),
&mut U32Map::default(),
&FkMap::default(),
&heights,
&mut crate::block::StructuralScratch::default(),
None,
)
.map(|_| ())
}
Expand Down Expand Up @@ -2078,15 +2082,16 @@ fn already_archived_schema13_pin_identity_tip_follow() {

// Structural without denserels/abs is invariant — not soft PrevoutSpent recovery.
{
use super::structural_validate_spends;
use super::{structural_validate_spends, RunCreateHeight};
use rbitcoin_primitives::Fk;
use rbitcoin_query::{BatchParents, FkMap, OutPointSet, U32Map};
use rbitcoin_query::{BatchParents, OutPointSet, U32Map};
let c2_fk = q.tx_fk_by_txid(c2_txid.as_byte_array()).unwrap().unwrap();
let spends = vec![(c2_txid.to_byte_array(), 0u32, Fk(9_000_001), c2_fk, 0)];
let parents = BatchParents::new();
let ctx = ValidationContext::at(Box::leak(Box::new(params.clone())), Height(h_n1), ms);
let mut pending = OutPointSet::default();
let mut mtp = U32Map::default();
let heights = RunCreateHeight::Spans(Vec::new());
let err = structural_validate_spends(
&q,
&b_n1,
Expand All @@ -2097,8 +2102,9 @@ fn already_archived_schema13_pin_identity_tip_follow() {
&mut pending,
&parents,
&mut mtp,
&FkMap::default(),
&heights,
&mut crate::block::StructuralScratch::default(),
None,
)
.expect_err("missing denserels abs must hard-fail");
let msg = format!("{err}");
Expand Down
Loading
Loading