From 003fd9e05670c04940d4924d8bb19aef91fcae2d Mon Sep 17 00:00:00 2001 From: MeABzZz <73040345+MeABzZz@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:31:39 -0300 Subject: [PATCH] feat(node): tell consensus which blocks descend to the assume-valid block Companion to blvm-consensus's Core-semantics assume-valid: build the assume-valid block's ancestry once by walking prev hashes on the stored header tree, and pass it as BlockValidationContext::assume_valid_ancestor on the IBD, connect, reorg and pruning paths. --- src/node/block_processor.rs | 1 + src/node/parallel_ibd/mod.rs | 1 + src/node/reorg_executor.rs | 1 + src/storage/blockstore.rs | 47 +++++++++++++++++++ src/storage/pruning.rs | 2 + tests/assume_valid_ancestor_lookup.rs | 66 +++++++++++++++++++++++++++ 6 files changed, 118 insertions(+) create mode 100644 tests/assume_valid_ancestor_lookup.rs diff --git a/src/node/block_processor.rs b/src/node/block_processor.rs index bc23009..cfcd3bf 100644 --- a/src/node/block_processor.rs +++ b/src/node/block_processor.rs @@ -364,6 +364,7 @@ pub fn validate_block_with_context_and_undo( protocol.connect_block_validation_context(recent_headers.as_deref(), network_time); consensus_ctx.sequence_prev_mtp = Some(blockstore.sequence_prev_mtp_lookup()); consensus_ctx.difficulty_ancestor = Some(blockstore.difficulty_ancestor_lookup()); + consensus_ctx.assume_valid_ancestor = Some(blockstore.assume_valid_ancestor_lookup()); let owned_utxo = std::mem::take(utxo_set); let (result, new_utxo_set, undo_log) = blvm_consensus::block::connect_block(block, witnesses, owned_utxo, height, &consensus_ctx)?; diff --git a/src/node/parallel_ibd/mod.rs b/src/node/parallel_ibd/mod.rs index 84ba22c..518edf5 100644 --- a/src/node/parallel_ibd/mod.rs +++ b/src/node/parallel_ibd/mod.rs @@ -10148,6 +10148,7 @@ impl ParallelIBD { context.ibd_block_outputs = ibd_block_outputs; context.sequence_prev_mtp = Some(blockstore.sequence_prev_mtp_lookup()); context.difficulty_ancestor = Some(blockstore.difficulty_ancestor_lookup()); + context.assume_valid_ancestor = Some(blockstore.assume_valid_ancestor_lookup()); let owned_utxo = if ibd_utxo_lookup.is_some() { UtxoSet::default() } else { diff --git a/src/node/reorg_executor.rs b/src/node/reorg_executor.rs index 5d3f940..7ff978b 100644 --- a/src/node/reorg_executor.rs +++ b/src/node/reorg_executor.rs @@ -280,6 +280,7 @@ pub fn try_activate_heavier_fork( ); ctx.sequence_prev_mtp = Some(mtp_store.sequence_prev_mtp_lookup()); ctx.difficulty_ancestor = Some(difficulty_store.difficulty_ancestor_lookup()); + ctx.assume_valid_ancestor = Some(difficulty_store.assume_valid_ancestor_lookup()); ctx }; diff --git a/src/storage/blockstore.rs b/src/storage/blockstore.rs index 407df4d..5ea08a4 100644 --- a/src/storage/blockstore.rs +++ b/src/storage/blockstore.rs @@ -826,6 +826,24 @@ impl BlockStore { }) } + /// Ancestry of the configured assume-valid block on the stored header tree (Core: + /// `assumevalid->GetAncestor(height)`), for the assume-valid script skip. + /// + /// The ancestry is built once by walking `prev_block_hash` back from the assume-valid + /// header, so it is exactly that block's chain, whatever else the height index holds. + /// Until the assume-valid header is stored the lookup returns `None` (scripts are + /// verified) and retries on the next call. + pub fn assume_valid_ancestor_lookup(&self) -> blvm_consensus::block::AssumeValidAncestor { + let store = self.clone(); + std::sync::Arc::new(move |height| { + let av_hash = blvm_consensus::config::get_assume_valid_hash()?; + let av_height = blvm_consensus::block::get_assume_valid_height(); + assume_valid_ancestry(&store, av_hash, av_height)? + .get(usize::try_from(height).ok()?) + .copied() + }) + } + /// Oldest-to-newest headers ending at `end_height`, at most `count` blocks. /// /// Stops at the first missing header so the slice stays a contiguous suffix @@ -1422,6 +1440,35 @@ impl BlockStore { } } +/// Hashes of the assume-valid block's chain indexed by height, cached per process. +fn assume_valid_ancestry( + store: &BlockStore, + av_hash: Hash, + av_height: u64, +) -> Option>> { + type Cached = Option<(Hash, u64, Arc>)>; + static CACHE: std::sync::Mutex = std::sync::Mutex::new(None); + let mut cache = CACHE.lock().ok()?; + if let Some((hash, height, chain)) = cache.as_ref() { + if *hash == av_hash && *height == av_height { + return Some(Arc::clone(chain)); + } + } + // Not synced to the assume-valid header yet: cheap miss, no allocation. + store.get_header(&av_hash).ok()??; + let mut chain = vec![[0u8; 32]; usize::try_from(av_height).ok()? + 1]; + let mut hash = av_hash; + for h in (0..=av_height).rev() { + chain[h as usize] = hash; + if h > 0 { + hash = store.get_header(&hash).ok()??.prev_block_hash; + } + } + let chain = Arc::new(chain); + *cache = Some((av_hash, av_height, Arc::clone(&chain))); + Some(chain) +} + #[cfg(test)] mod header_height_tests { use super::*; diff --git a/src/storage/pruning.rs b/src/storage/pruning.rs index 9f70759..dab6f0d 100644 --- a/src/storage/pruning.rs +++ b/src/storage/pruning.rs @@ -762,6 +762,8 @@ impl PruningManager { blvm_protocol::block::BlockValidationContext::for_network(self.network); ctx.sequence_prev_mtp = Some(self.blockstore.sequence_prev_mtp_lookup()); ctx.difficulty_ancestor = Some(self.blockstore.difficulty_ancestor_lookup()); + ctx.assume_valid_ancestor = + Some(self.blockstore.assume_valid_ancestor_lookup()); let (validation_result, new_utxo_set, _undo_log) = connect_block(&block, &witnesses, utxo_set, height, &ctx)?; diff --git a/tests/assume_valid_ancestor_lookup.rs b/tests/assume_valid_ancestor_lookup.rs new file mode 100644 index 0000000..a66b82b --- /dev/null +++ b/tests/assume_valid_ancestor_lookup.rs @@ -0,0 +1,66 @@ +//! The node tells consensus which blocks are ancestors of the assume-valid block by +//! walking the stored header tree back from that block (Core: `GetAncestor`), not by +//! trusting whatever hash the height index holds. + +use blvm_consensus::config::{ConsensusConfig, init_consensus_config}; +use blvm_node::storage::Storage; +use blvm_protocol::{BlockHeader, Hash}; +use tempfile::TempDir; + +const AV_HEIGHT: u64 = 6; + +fn header(prev: Hash, salt: u32) -> BlockHeader { + BlockHeader { + version: 4, + prev_block_hash: prev, + merkle_root: [salt as u8; 32], + timestamp: 1_300_000_000, + bits: 0x207fffff, + nonce: salt as u64, + } +} + +/// Headers 0..=len-1 chained by prev hash; returns (hash, header, height). +fn chain(len: u64, salt: u32) -> Vec<(Hash, BlockHeader, u64)> { + let mut out = Vec::new(); + let mut prev = [0u8; 32]; + for h in 0..len { + let hd = header(prev, salt + h as u32); + let hash = blvm_consensus::block::block_header_hash(&hd); + out.push((hash, hd, h)); + prev = hash; + } + out +} + +#[test] +fn ancestry_follows_the_assume_valid_block_not_the_height_index() { + // Competing headers for heights 0..=3 are stored first, so the height index keeps them + // (first seen wins) even after the real chain arrives. + let real = chain(10, 1_000); + let mut fork = chain(4, 5_000); + fork[0] = real[0].clone(); // same genesis + + let mut cfg = ConsensusConfig::default(); + cfg.block_validation.assume_valid_height = AV_HEIGHT; + cfg.block_validation.assume_valid_hash = Some(real[AV_HEIGHT as usize].0); + init_consensus_config(cfg); + + let dir = TempDir::new().unwrap(); + let storage = Storage::new(dir.path()).unwrap(); + let blocks = storage.blocks(); + let lookup = blocks.assume_valid_ancestor_lookup(); + + // Assume-valid header unknown yet: nothing is an ancestor, scripts get verified. + blocks.store_headers_batch(&fork).unwrap(); + assert_eq!(lookup(2), None); + + blocks.store_headers_batch(&real).unwrap(); + assert_eq!(blocks.get_hash_by_height(2).unwrap(), Some(fork[2].0)); + for h in 0..=AV_HEIGHT { + assert_eq!(lookup(h), Some(real[h as usize].0), "height {h}"); + } + assert_ne!(lookup(2), Some(fork[2].0)); + // Above the assume-valid block there is no ancestor. + assert_eq!(lookup(AV_HEIGHT + 1), None); +}