use crate::spam_filter::{SpamBreakdown, SpamFilter, SpamFilterConfig, SpamSummary, SpamType};
#[cfg(feature = "utxo-commitments")]
use crate::utxo_commitments::data_structures::{
UtxoCommitment, UtxoCommitmentError, UtxoCommitmentResult,
};
#[cfg(feature = "utxo-commitments")]
use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
#[cfg(feature = "utxo-commitments")]
use crate::utxo_commitments::network_integration::UtxoCommitmentsNetworkClient;
#[cfg(feature = "utxo-commitments")]
use crate::utxo_commitments::peer_consensus::{ConsensusConfig, PeerConsensus, PeerInfo};
#[cfg(feature = "utxo-commitments")]
use blvm_consensus::types::{
BlockHeader, Hash as HashType, Natural, OutPoint, Transaction, UtxoSet, UTXO,
};
#[cfg(feature = "utxo-commitments")]
pub struct InitialSync {
peer_consensus: PeerConsensus,
spam_filter: SpamFilter,
}
impl InitialSync {
pub fn new(config: ConsensusConfig) -> Self {
Self {
peer_consensus: PeerConsensus::new(config),
spam_filter: SpamFilter::new(),
}
}
pub fn with_spam_filter(config: ConsensusConfig, spam_filter_config: SpamFilterConfig) -> Self {
Self {
peer_consensus: PeerConsensus::new(config),
spam_filter: SpamFilter::with_config(spam_filter_config),
}
}
pub async fn execute_initial_sync<C: UtxoCommitmentsNetworkClient>(
&self,
peers: &[(PeerInfo, String)],
header_chain: &[BlockHeader],
network_client: &C,
) -> UtxoCommitmentResult<UtxoCommitment> {
let all_infos: Vec<PeerInfo> = peers.iter().map(|(info, _)| info.clone()).collect();
let diverse_infos = self.peer_consensus.discover_diverse_peers(all_infos);
if diverse_infos.len() < self.peer_consensus.config.min_peers {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Insufficient diverse peers: got {}, need {}",
diverse_infos.len(),
self.peer_consensus.config.min_peers
)));
}
let diverse_with_ids: Vec<(PeerInfo, String)> = peers
.iter()
.filter(|(info, _)| {
diverse_infos
.iter()
.any(|d| d.address == info.address && d.subnet == info.subnet)
})
.cloned()
.collect();
let peer_tips: Vec<Natural> = vec![];
let checkpoint_height = if !peer_tips.is_empty() {
self.peer_consensus.determine_checkpoint_height(peer_tips)
} else if !header_chain.is_empty() {
let tip = header_chain.len() as Natural - 1;
if tip > self.peer_consensus.config.safety_margin {
tip - self.peer_consensus.config.safety_margin
} else {
0
}
} else {
return Err(UtxoCommitmentError::VerificationFailed(
"No header chain or peer tips available".to_string(),
));
};
if checkpoint_height as usize >= header_chain.len() {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Checkpoint height {} exceeds header chain length {}",
checkpoint_height,
header_chain.len()
)));
}
let checkpoint_header = &header_chain[checkpoint_height as usize];
let checkpoint_hash = compute_block_hash(checkpoint_header);
let peer_commitments = self
.peer_consensus
.request_utxo_sets(
network_client,
&diverse_with_ids,
checkpoint_height,
checkpoint_hash,
)
.await;
let consensus = self.peer_consensus.find_consensus(peer_commitments)?;
self.peer_consensus
.verify_consensus_commitment(&consensus, header_chain)?;
#[cfg(feature = "utxo-proof-verification")]
{
}
Ok(consensus.commitment)
}
pub async fn complete_sync_from_checkpoint<C, F, Fut>(
&self,
utxo_tree: &mut UtxoMerkleTree,
checkpoint_height: Natural,
current_tip: Natural,
network_client: &C,
get_block_hash: F,
peer_id: &str,
network: crate::types::Network,
network_time: u64,
recent_headers: Option<&[BlockHeader]>,
checkpoint_utxo_set: Option<UtxoSet>,
) -> UtxoCommitmentResult<()>
where
C: UtxoCommitmentsNetworkClient,
F: Fn(Natural) -> Fut,
Fut: std::future::Future<Output = UtxoCommitmentResult<HashType>>,
{
use crate::block::connect_block;
let mut utxo_set: UtxoSet = checkpoint_utxo_set.unwrap_or_default();
for height in checkpoint_height + 1..=current_tip {
let block_hash = get_block_hash(height).await?;
let full_block = network_client
.request_full_block(peer_id, block_hash)
.await?;
if full_block.block.header.timestamp == 0 {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Invalid block header at height {}",
height
)));
}
let context = crate::block::block_validation_context_for_connect_ibd(
recent_headers,
network_time,
network,
);
let (validation_result, new_utxo_set, _undo_log) = connect_block(
&full_block.block,
&full_block.witnesses,
utxo_set.clone(),
height,
&context,
)
.map_err(|e| {
UtxoCommitmentError::VerificationFailed(format!(
"connect_block failed at height {}: {}",
height, e
))
})?;
if !matches!(
validation_result,
blvm_consensus::types::ValidationResult::Valid
) {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Block validation failed at height {}: {:?}",
height, validation_result
)));
}
let old_utxo_set = utxo_set.clone();
utxo_tree.update_from_utxo_set(&new_utxo_set, &old_utxo_set)?;
let computed_block_hash = compute_block_hash(&full_block.block.header);
let computed_commitment = utxo_tree.generate_commitment(computed_block_hash, height);
use blvm_consensus::economic::total_supply;
let expected_supply = total_supply(height) as u64;
if computed_commitment.total_supply != expected_supply {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Supply mismatch at height {}: computed {}, expected {}",
height, computed_commitment.total_supply, expected_supply
)));
}
utxo_set = new_utxo_set;
}
Ok(())
}
pub fn process_filtered_block(
&self,
utxo_tree: &mut UtxoMerkleTree,
block_height: Natural,
block_transactions: &[Transaction],
) -> UtxoCommitmentResult<(SpamSummary, HashType)> {
use blvm_consensus::transaction::is_coinbase;
let mut spam_summary = SpamSummary {
filtered_count: 0,
filtered_size: 0,
by_type: SpamBreakdown::default(),
};
for tx in block_transactions {
let spam_result = self.spam_filter.is_spam(tx);
let is_spam = spam_result.is_spam;
if is_spam {
spam_summary.filtered_count += 1;
let tx_size = 4 + 1 + 1 + 4 + (tx.inputs.len() as u64 * 150) + tx.outputs.iter().map(|out| 8 + out.script_pubkey.len() as u64).sum::<u64>(); spam_summary.filtered_size += tx_size;
for spam_type in &spam_result.detected_types {
match spam_type {
SpamType::Ordinals => {
spam_summary.by_type.ordinals += 1;
}
SpamType::Dust => {
spam_summary.by_type.dust += 1;
}
SpamType::BRC20 => {
spam_summary.by_type.brc20 += 1;
}
SpamType::LargeWitness => {
spam_summary.by_type.ordinals += 1; }
SpamType::LowFeeRate => {
spam_summary.by_type.dust += 1; }
SpamType::HighSizeValueRatio => {
spam_summary.by_type.ordinals += 1; }
SpamType::ManySmallOutputs => {
spam_summary.by_type.dust += 1; }
SpamType::NotSpam => {}
}
}
}
let tx_id = compute_tx_id(tx);
if !is_coinbase(tx) {
for input in &tx.inputs {
match utxo_tree.get(&input.prevout) {
Ok(Some(utxo)) => {
if let Err(e) = utxo_tree.remove(&input.prevout, &utxo) {
return Err(UtxoCommitmentError::TransactionApplication(format!(
"Failed to remove spent input: {:?}",
e
)));
}
}
Ok(None) => {
}
Err(e) => {
return Err(UtxoCommitmentError::TransactionApplication(format!(
"Failed to get UTXO for removal: {:?}",
e
)));
}
}
}
}
if !is_spam {
for (i, output) in tx.outputs.iter().enumerate() {
let outpoint = OutPoint {
hash: tx_id,
index: i as u32,
};
let utxo = UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height: block_height,
is_coinbase: is_coinbase(tx),
};
if let Err(e) = utxo_tree.insert(outpoint, utxo) {
return Err(UtxoCommitmentError::TransactionApplication(format!(
"Failed to add output: {:?}",
e
)));
}
}
}
}
let root = utxo_tree.root();
Ok((spam_summary, root))
}
}
#[cfg(feature = "utxo-commitments")]
pub fn update_commitments_after_block(
utxo_tree: &mut UtxoMerkleTree,
block: &crate::types::Block,
block_height: Natural,
spam_filter: Option<&SpamFilter>,
) -> UtxoCommitmentResult<HashType> {
use blvm_consensus::block::calculate_tx_id;
use blvm_consensus::transaction::is_coinbase;
if let Some(filter) = spam_filter {
let initial_sync = InitialSync {
peer_consensus: crate::utxo_commitments::peer_consensus::PeerConsensus::new(
crate::utxo_commitments::peer_consensus::ConsensusConfig::default(),
),
spam_filter: filter.clone(),
};
let (_, root) =
initial_sync.process_filtered_block(utxo_tree, block_height, &block.transactions)?;
Ok(root)
} else {
for tx in &block.transactions {
let tx_id = calculate_tx_id(tx);
if !is_coinbase(tx) {
for input in &tx.inputs {
match utxo_tree.get(&input.prevout) {
Ok(Some(utxo)) => {
utxo_tree.remove(&input.prevout, &utxo)?;
}
Ok(None) => {
}
Err(e) => {
return Err(UtxoCommitmentError::TransactionApplication(format!(
"Failed to get UTXO for removal: {:?}",
e
)));
}
}
}
}
for (i, output) in tx.outputs.iter().enumerate() {
let outpoint = blvm_consensus::types::OutPoint {
hash: tx_id,
index: i as u32,
};
let utxo = blvm_consensus::types::UTXO {
value: output.value,
script_pubkey: output.script_pubkey.as_slice().into(),
height: block_height,
is_coinbase: is_coinbase(tx),
};
utxo_tree.insert(outpoint, utxo)?;
}
}
Ok(utxo_tree.root())
}
}
fn compute_tx_id(tx: &Transaction) -> HashType {
use crate::serialization::transaction::serialize_transaction;
use sha2::{Digest, Sha256};
let serialized = serialize_transaction(tx);
let first_hash = Sha256::digest(&serialized);
let second_hash = Sha256::digest(first_hash);
let mut txid = [0u8; 32];
txid.copy_from_slice(&second_hash);
txid
}
fn compute_block_hash(header: &BlockHeader) -> HashType {
use sha2::{Digest, Sha256};
let mut bytes = Vec::with_capacity(80);
bytes.extend_from_slice(&header.version.to_le_bytes());
bytes.extend_from_slice(&header.prev_block_hash);
bytes.extend_from_slice(&header.merkle_root);
bytes.extend_from_slice(&header.timestamp.to_le_bytes());
bytes.extend_from_slice(&header.bits.to_le_bytes());
bytes.extend_from_slice(&header.nonce.to_le_bytes());
let first_hash = Sha256::digest(&bytes);
let second_hash = Sha256::digest(&first_hash);
let mut hash = [0u8; 32];
hash.copy_from_slice(&second_hash);
hash
}