#[cfg(feature = "utxo-commitments")]
use crate::spam_filter::{SpamFilter, SpamSummary};
#[cfg(feature = "utxo-commitments")]
use crate::utxo_commitments::data_structures::{
UtxoCommitment, UtxoCommitmentError, UtxoCommitmentResult,
};
#[cfg(feature = "utxo-commitments")]
use blvm_consensus::types::{BlockHeader, Hash as HashType, Natural, Transaction};
#[cfg(feature = "utxo-commitments")]
#[derive(Debug, Clone)]
pub struct FilteredBlock {
pub header: BlockHeader,
pub commitment: UtxoCommitment,
pub transactions: Vec<Transaction>,
pub transaction_indices: Vec<u32>,
pub spam_summary: SpamSummary,
}
pub trait UtxoCommitmentsNetworkClient: Send + Sync {
fn request_utxo_set(
&self,
peer_id: &str,
height: Natural,
block_hash: HashType,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = UtxoCommitmentResult<UtxoCommitment>> + Send + '_>,
>;
fn request_filtered_block(
&self,
peer_id: &str,
block_hash: HashType,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = UtxoCommitmentResult<FilteredBlock>> + Send + '_>,
>;
fn request_full_block(
&self,
peer_id: &str,
block_hash: HashType,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = UtxoCommitmentResult<FullBlock>> + Send + '_>,
>;
fn get_peer_ids(&self) -> Vec<String>;
}
#[derive(Debug, Clone)]
pub struct FullBlock {
pub block: blvm_consensus::types::Block,
pub witnesses: Vec<Vec<blvm_consensus::segwit::Witness>>,
}
pub async fn request_utxo_sets_from_peers_fn<F, Fut>(
request_fn: F,
peers: &[String],
height: Natural,
block_hash: HashType,
) -> Vec<(String, UtxoCommitmentResult<UtxoCommitment>)>
where
F: Fn(&str, Natural, HashType) -> Fut,
Fut: std::future::Future<Output = UtxoCommitmentResult<UtxoCommitment>>,
{
let mut results = Vec::new();
for peer_id in peers {
let result = request_fn(peer_id, height, block_hash).await;
results.push((peer_id.clone(), result));
}
results
}
pub fn process_and_verify_filtered_block(
filtered_block: &FilteredBlock,
expected_height: Natural,
spam_filter: &SpamFilter,
) -> UtxoCommitmentResult<bool> {
if filtered_block.commitment.block_height != expected_height {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Commitment height mismatch: expected {}, got {}",
expected_height, filtered_block.commitment.block_height
)));
}
verify_filtered_block_spam_filtering(filtered_block, spam_filter)?;
let computed_hash = compute_block_hash(&filtered_block.header);
if filtered_block.commitment.block_hash != computed_hash {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Block hash mismatch: expected {:?}, got {:?}",
computed_hash, filtered_block.commitment.block_hash
)));
}
Ok(true)
}
fn verify_filtered_block_spam_filtering(
filtered_block: &FilteredBlock,
spam_filter: &SpamFilter,
) -> UtxoCommitmentResult<()> {
for (index, tx) in filtered_block.transactions.iter().enumerate() {
if spam_filter.is_spam(tx).is_spam {
return Err(UtxoCommitmentError::VerificationFailed(format!(
"Filtered block transaction {index} failed local spam filter"
)));
}
}
let (retained, recomputed_summary) = spam_filter.filter_block(&filtered_block.transactions);
if retained.len() != filtered_block.transactions.len() {
return Err(UtxoCommitmentError::VerificationFailed(
"Filtered block transactions fail local spam re-filter".to_string(),
));
}
if recomputed_summary.filtered_count != 0 {
return Err(UtxoCommitmentError::VerificationFailed(
"Filtered block included transactions classified as spam locally".to_string(),
));
}
let _ = &filtered_block.spam_summary;
Ok(())
}
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
}
#[cfg(all(test, feature = "utxo-commitments"))]
mod verify_tests {
use super::*;
use blvm_consensus::types::{OutPoint, Transaction, TransactionInput, TransactionOutput};
fn sample_header() -> BlockHeader {
BlockHeader {
version: 1,
prev_block_hash: [0u8; 32],
merkle_root: [1u8; 32],
timestamp: 1_700_000_000,
bits: 0x207fffff,
nonce: 0,
}
}
fn sample_tx() -> Transaction {
Transaction {
version: 1,
inputs: blvm_consensus::tx_inputs![TransactionInput {
prevout: OutPoint {
hash: [2u8; 32],
index: 0,
},
script_sig: vec![0x51],
sequence: 0xffff_ffff,
}],
outputs: blvm_consensus::tx_outputs![TransactionOutput {
value: 50_000,
script_pubkey: vec![0x76, 0xa9, 0x14].repeat(20),
}],
lock_time: 0,
}
}
#[test]
fn process_and_verify_filtered_block_reapplies_spam_filter() {
let header = sample_header();
let hash = compute_block_hash(&header);
let spam_filter = SpamFilter::new();
let filtered_block = FilteredBlock {
header: header.clone(),
commitment: UtxoCommitment {
merkle_root: [3u8; 32],
total_supply: 21_000_000,
utxo_count: 1,
block_height: 100,
block_hash: hash,
},
transactions: vec![sample_tx()],
transaction_indices: vec![0],
spam_summary: SpamSummary::default(),
};
assert!(process_and_verify_filtered_block(&filtered_block, 100, &spam_filter).unwrap());
}
}