Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/node/block_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;
Expand Down
1 change: 1 addition & 0 deletions src/node/parallel_ibd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/node/reorg_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
};

Expand Down
47 changes: 47 additions & 0 deletions src/storage/blockstore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<Arc<Vec<Hash>>> {
type Cached = Option<(Hash, u64, Arc<Vec<Hash>>)>;
static CACHE: std::sync::Mutex<Cached> = 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::*;
Expand Down
2 changes: 2 additions & 0 deletions src/storage/pruning.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)?;

Expand Down
66 changes: 66 additions & 0 deletions tests/assume_valid_ancestor_lookup.rs
Original file line number Diff line number Diff line change
@@ -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);
}