blvm_protocol/utxo_commitments/
network_integration.rs1#[cfg(feature = "utxo-commitments")]
7use crate::spam_filter::{SpamFilter, SpamSummary};
8#[cfg(feature = "utxo-commitments")]
9use crate::utxo_commitments::data_structures::{
10 UtxoCommitment, UtxoCommitmentError, UtxoCommitmentResult,
11};
12#[cfg(feature = "utxo-commitments")]
13use blvm_consensus::types::{BlockHeader, Hash as HashType, Natural, Transaction};
14#[cfg(feature = "utxo-commitments")]
15#[derive(Debug, Clone)]
17pub struct FilteredBlock {
18 pub header: BlockHeader,
19 pub commitment: UtxoCommitment,
20 pub transactions: Vec<Transaction>,
21 pub transaction_indices: Vec<u32>,
22 pub spam_summary: SpamSummary,
23}
24
25pub trait UtxoCommitmentsNetworkClient: Send + Sync {
33 fn request_utxo_set(
38 &self,
39 peer_id: &str,
40 height: Natural,
41 block_hash: HashType,
42 ) -> std::pin::Pin<
43 Box<dyn std::future::Future<Output = UtxoCommitmentResult<UtxoCommitment>> + Send + '_>,
44 >;
45
46 fn request_filtered_block(
48 &self,
49 peer_id: &str,
50 block_hash: HashType,
51 ) -> std::pin::Pin<
52 Box<dyn std::future::Future<Output = UtxoCommitmentResult<FilteredBlock>> + Send + '_>,
53 >;
54
55 fn request_full_block(
60 &self,
61 peer_id: &str,
62 block_hash: HashType,
63 ) -> std::pin::Pin<
64 Box<dyn std::future::Future<Output = UtxoCommitmentResult<FullBlock>> + Send + '_>,
65 >;
66
67 fn get_peer_ids(&self) -> Vec<String>;
69}
70
71#[derive(Debug, Clone)]
76pub struct FullBlock {
77 pub block: blvm_consensus::types::Block,
78 pub witnesses: Vec<Vec<blvm_consensus::segwit::Witness>>,
79}
80
81pub async fn request_utxo_sets_from_peers_fn<F, Fut>(
85 request_fn: F,
86 peers: &[String],
87 height: Natural,
88 block_hash: HashType,
89) -> Vec<(String, UtxoCommitmentResult<UtxoCommitment>)>
90where
91 F: Fn(&str, Natural, HashType) -> Fut,
92 Fut: std::future::Future<Output = UtxoCommitmentResult<UtxoCommitment>>,
93{
94 let mut results = Vec::new();
95
96 for peer_id in peers {
97 let result = request_fn(peer_id, height, block_hash).await;
98 results.push((peer_id.clone(), result));
99 }
100
101 results
102}
103
104pub fn process_and_verify_filtered_block(
106 filtered_block: &FilteredBlock,
107 expected_height: Natural,
108 spam_filter: &SpamFilter,
109) -> UtxoCommitmentResult<bool> {
110 if filtered_block.commitment.block_height != expected_height {
115 return Err(UtxoCommitmentError::VerificationFailed(format!(
116 "Commitment height mismatch: expected {}, got {}",
117 expected_height, filtered_block.commitment.block_height
118 )));
119 }
120
121 verify_filtered_block_spam_filtering(filtered_block, spam_filter)?;
123
124 let computed_hash = compute_block_hash(&filtered_block.header);
126 if filtered_block.commitment.block_hash != computed_hash {
127 return Err(UtxoCommitmentError::VerificationFailed(format!(
128 "Block hash mismatch: expected {:?}, got {:?}",
129 computed_hash, filtered_block.commitment.block_hash
130 )));
131 }
132
133 Ok(true)
134}
135
136fn verify_filtered_block_spam_filtering(
142 filtered_block: &FilteredBlock,
143 spam_filter: &SpamFilter,
144) -> UtxoCommitmentResult<()> {
145 for (index, tx) in filtered_block.transactions.iter().enumerate() {
146 if spam_filter.is_spam(tx).is_spam {
147 return Err(UtxoCommitmentError::VerificationFailed(format!(
148 "Filtered block transaction {index} failed local spam filter"
149 )));
150 }
151 }
152
153 let (retained, recomputed_summary) = spam_filter.filter_block(&filtered_block.transactions);
154 if retained.len() != filtered_block.transactions.len() {
155 return Err(UtxoCommitmentError::VerificationFailed(
156 "Filtered block transactions fail local spam re-filter".to_string(),
157 ));
158 }
159 if recomputed_summary.filtered_count != 0 {
160 return Err(UtxoCommitmentError::VerificationFailed(
161 "Filtered block included transactions classified as spam locally".to_string(),
162 ));
163 }
164
165 let _ = &filtered_block.spam_summary;
167
168 Ok(())
169}
170
171fn compute_block_hash(header: &BlockHeader) -> HashType {
173 use sha2::{Digest, Sha256};
174
175 let mut bytes = Vec::with_capacity(80);
176 bytes.extend_from_slice(&header.version.to_le_bytes());
177 bytes.extend_from_slice(&header.prev_block_hash);
178 bytes.extend_from_slice(&header.merkle_root);
179 bytes.extend_from_slice(&header.timestamp.to_le_bytes());
180 bytes.extend_from_slice(&header.bits.to_le_bytes());
181 bytes.extend_from_slice(&header.nonce.to_le_bytes());
182
183 let first_hash = Sha256::digest(&bytes);
184 let second_hash = Sha256::digest(first_hash);
185
186 let mut hash = [0u8; 32];
187 hash.copy_from_slice(&second_hash);
188 hash
189}
190
191#[cfg(all(test, feature = "utxo-commitments"))]
192mod verify_tests {
193 use super::*;
194 use blvm_consensus::types::{OutPoint, Transaction, TransactionInput, TransactionOutput};
195
196 fn sample_header() -> BlockHeader {
197 BlockHeader {
198 version: 1,
199 prev_block_hash: [0u8; 32],
200 merkle_root: [1u8; 32],
201 timestamp: 1_700_000_000,
202 bits: 0x207fffff,
203 nonce: 0,
204 }
205 }
206
207 fn sample_tx() -> Transaction {
208 Transaction {
209 version: 1,
210 inputs: blvm_consensus::tx_inputs![TransactionInput {
211 prevout: OutPoint {
212 hash: [2u8; 32],
213 index: 0,
214 },
215 script_sig: vec![0x51],
216 sequence: 0xffff_ffff,
217 }],
218 outputs: blvm_consensus::tx_outputs![TransactionOutput {
219 value: 50_000,
220 script_pubkey: vec![0x76, 0xa9, 0x14].repeat(20),
221 }],
222 lock_time: 0,
223 }
224 }
225
226 #[test]
227 fn process_and_verify_filtered_block_reapplies_spam_filter() {
228 let header = sample_header();
229 let hash = compute_block_hash(&header);
230 let spam_filter = SpamFilter::new();
231 let filtered_block = FilteredBlock {
232 header: header.clone(),
233 commitment: UtxoCommitment {
234 merkle_root: [3u8; 32],
235 total_supply: 21_000_000,
236 utxo_count: 1,
237 block_height: 100,
238 block_hash: hash,
239 },
240 transactions: vec![sample_tx()],
241 transaction_indices: vec![0],
242 spam_summary: SpamSummary::default(),
243 };
244
245 assert!(process_and_verify_filtered_block(&filtered_block, 100, &spam_filter).unwrap());
246 }
247}