blvm_protocol/utxo_commitments/initial_sync.rs
1//! Initial Sync Algorithm
2//!
3//! Implements the peer consensus initial sync algorithm:
4//! 1. Discover diverse peers
5//! 2. Determine checkpoint height
6//! 3. Request UTXO sets from peers
7//! 4. Find consensus
8//! 5. Verify against block headers
9//! 6. Download UTXO set
10
11use crate::spam_filter::{SpamBreakdown, SpamFilter, SpamFilterConfig, SpamSummary, SpamType};
12#[cfg(feature = "utxo-commitments")]
13use crate::utxo_commitments::data_structures::{
14 UtxoCommitment, UtxoCommitmentError, UtxoCommitmentResult,
15};
16#[cfg(feature = "utxo-commitments")]
17use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
18#[cfg(feature = "utxo-commitments")]
19use crate::utxo_commitments::network_integration::UtxoCommitmentsNetworkClient;
20#[cfg(feature = "utxo-commitments")]
21use crate::utxo_commitments::peer_consensus::{ConsensusConfig, PeerConsensus, PeerInfo};
22#[cfg(feature = "utxo-commitments")]
23use blvm_consensus::types::{
24 BlockHeader, Hash as HashType, Natural, OutPoint, Transaction, UTXO, UtxoSet,
25};
26#[cfg(feature = "utxo-commitments")]
27/// Initial sync manager
28pub struct InitialSync {
29 peer_consensus: PeerConsensus,
30 spam_filter: SpamFilter,
31 // In real implementation: network_client: NetworkClient,
32}
33
34impl InitialSync {
35 /// Create a new initial sync manager
36 pub fn new(config: ConsensusConfig) -> Self {
37 Self {
38 peer_consensus: PeerConsensus::new(config),
39 spam_filter: SpamFilter::new(),
40 }
41 }
42
43 /// Create a new initial sync manager with custom spam filter config
44 pub fn with_spam_filter(config: ConsensusConfig, spam_filter_config: SpamFilterConfig) -> Self {
45 Self {
46 peer_consensus: PeerConsensus::new(config),
47 spam_filter: SpamFilter::with_config(spam_filter_config),
48 }
49 }
50
51 /// Execute initial sync algorithm
52 ///
53 /// Performs the complete initial sync process:
54 /// 1. Discover diverse peers
55 /// 2. Determine checkpoint height
56 /// 3. Request UTXO sets
57 /// 4. Find consensus
58 /// 5. Verify against headers
59 /// 6. Return verified UTXO commitment
60 pub async fn execute_initial_sync<C: UtxoCommitmentsNetworkClient>(
61 &self,
62 peers: &[(PeerInfo, String)],
63 header_chain: &[BlockHeader],
64 network_client: &C,
65 ) -> UtxoCommitmentResult<UtxoCommitment> {
66 // Step 1: Discover diverse peers (based on consensus-level PeerInfo)
67 let all_infos: Vec<PeerInfo> = peers.iter().map(|(info, _)| info.clone()).collect();
68 let diverse_infos = self.peer_consensus.discover_diverse_peers(all_infos);
69
70 if diverse_infos.len() < self.peer_consensus.config.min_peers {
71 return Err(UtxoCommitmentError::VerificationFailed(format!(
72 "Insufficient diverse peers: got {}, need {}",
73 diverse_infos.len(),
74 self.peer_consensus.config.min_peers
75 )));
76 }
77
78 // Re-attach peer IDs to the diverse set
79 let diverse_with_ids: Vec<(PeerInfo, String)> = peers
80 .iter()
81 .filter(|(info, _)| {
82 diverse_infos
83 .iter()
84 .any(|d| d.address == info.address && d.subnet == info.subnet)
85 })
86 .cloned()
87 .collect();
88
89 // Step 2: Determine checkpoint height from the verified header chain.
90 // Peer tip queries (version/start_height aggregation) are not wired yet; the
91 // header chain tip minus safety margin is the supported checkpoint path (REV-P-14).
92 let checkpoint_height = if !header_chain.is_empty() {
93 let tip = header_chain.len() as Natural - 1;
94 tip.saturating_sub(self.peer_consensus.config.safety_margin)
95 } else {
96 return Err(UtxoCommitmentError::VerificationFailed(
97 "No header chain available for checkpoint selection".to_string(),
98 ));
99 };
100
101 // Get checkpoint block hash from header chain (unchanged)
102 if checkpoint_height as usize >= header_chain.len() {
103 return Err(UtxoCommitmentError::VerificationFailed(format!(
104 "Checkpoint height {} exceeds header chain length {}",
105 checkpoint_height,
106 header_chain.len()
107 )));
108 }
109
110 let checkpoint_header = &header_chain[checkpoint_height as usize];
111 let checkpoint_hash = compute_block_hash(checkpoint_header);
112
113 // Step 3: Request UTXO sets from diverse peers via the network client
114 let peer_commitments = self
115 .peer_consensus
116 .request_utxo_sets(
117 network_client,
118 &diverse_with_ids,
119 checkpoint_height,
120 checkpoint_hash,
121 )
122 .await;
123
124 // Step 4: Find consensus
125 let consensus = self.peer_consensus.find_consensus(peer_commitments)?;
126
127 // Step 5: Verify consensus commitment against block headers
128 self.peer_consensus
129 .verify_consensus_commitment(&consensus, header_chain)?;
130
131 // Step 6: Optional per-UTXO proof verification (requires network proof requests).
132 #[cfg(feature = "utxo-proof-verification")]
133 {
134 return Err(UtxoCommitmentError::VerificationFailed(
135 "utxo-proof-verification feature is enabled but per-UTXO proof verification is not implemented; \
136 disable the feature or use full-block sync_forward validation"
137 .to_string(),
138 ));
139 }
140
141 // Step 7: Return verified commitment
142 Ok(consensus.commitment)
143 }
144
145 /// Complete sync from checkpoint to current tip
146 ///
147 /// Syncs forward from checkpoint using FULL blocks with complete validation.
148 /// Fully validates all transactions (signatures, scripts) before updating UTXO set.
149 ///
150 /// # Arguments
151 ///
152 /// * `utxo_tree` - UTXO Merkle tree to update incrementally
153 /// * `checkpoint_height` - Starting height (checkpoint)
154 /// * `current_tip` - Ending height (current chain tip)
155 /// * `network_client` - Network client for requesting blocks
156 /// * `get_block_hash` - Function to get block hash for a given height
157 /// * `peer_id` - Peer ID to request blocks from
158 /// * `network` - Network type (Mainnet, Testnet, Regtest)
159 /// * `network_time` - Current network time (Unix timestamp)
160 /// * `recent_headers` - Recent block headers for median time-past calculation
161 /// * `checkpoint_utxo_set` - Full UTXO set at checkpoint (required for validation).
162 /// If None, starts with empty set (cannot verify checkpoint commitment until end)
163 ///
164 /// # Implementation
165 ///
166 /// 1. Requests FULL blocks from checkpoint+1 to tip
167 /// 2. For each full block:
168 /// - Fully validates block with connect_block() (signatures, scripts, all consensus rules)
169 /// - Updates UTXO set from validated result
170 /// - Updates UTXO tree from validated UTXO set
171 /// - Verifies commitment matches computed root
172 /// 3. Updates UTXO tree incrementally after validation
173 ///
174 /// **Security**: All transactions are cryptographically verified before UTXO set update.
175 #[allow(clippy::too_many_arguments)]
176 pub async fn complete_sync_from_checkpoint<C, F, Fut>(
177 &self,
178 utxo_tree: &mut UtxoMerkleTree,
179 checkpoint_height: Natural,
180 current_tip: Natural,
181 network_client: &C,
182 get_block_hash: F,
183 peer_id: &str,
184 network: crate::types::Network,
185 network_time: u64,
186 recent_headers: Option<&[BlockHeader]>,
187 checkpoint_utxo_set: Option<UtxoSet>,
188 ) -> UtxoCommitmentResult<()>
189 where
190 C: UtxoCommitmentsNetworkClient,
191 F: Fn(Natural) -> Fut,
192 Fut: std::future::Future<Output = UtxoCommitmentResult<HashType>>,
193 {
194 use crate::block::connect_block;
195
196 // Start with checkpoint UTXO set, or empty if not provided
197 // Note: If empty, we cannot verify checkpoint commitment until we've built the full set
198 let mut utxo_set: UtxoSet = checkpoint_utxo_set.unwrap_or_default();
199
200 // Process blocks incrementally from checkpoint+1 to current tip
201 for height in checkpoint_height + 1..=current_tip {
202 // Get block hash for this height
203 let block_hash = get_block_hash(height).await?;
204
205 // Request FULL block (not filtered) from network peer
206 // This includes all transactions with witnesses for complete validation
207 let full_block = network_client
208 .request_full_block(peer_id, block_hash)
209 .await?;
210
211 // Verify block header height matches expected height
212 if full_block.block.header.timestamp == 0 {
213 return Err(UtxoCommitmentError::VerificationFailed(format!(
214 "Invalid block header at height {height}"
215 )));
216 }
217
218 let context = crate::block::block_validation_context_for_connect_ibd(
219 recent_headers,
220 network_time,
221 network,
222 );
223 let (validation_result, new_utxo_set, _undo_log) = connect_block(
224 &full_block.block,
225 &full_block.witnesses,
226 utxo_set.clone(),
227 height,
228 &context,
229 )
230 .map_err(|e| {
231 UtxoCommitmentError::VerificationFailed(format!(
232 "connect_block failed at height {height}: {e}"
233 ))
234 })?;
235
236 // Reject if validation failed
237 if !matches!(
238 validation_result,
239 blvm_consensus::types::ValidationResult::Valid
240 ) {
241 return Err(UtxoCommitmentError::VerificationFailed(format!(
242 "Block validation failed at height {height}: {validation_result:?}"
243 )));
244 }
245
246 // Update UTXO tree from validated UTXO set
247 // This ensures the tree matches the cryptographically verified state
248 // Pass old utxo_set to detect removals efficiently
249 let old_utxo_set = utxo_set.clone();
250 utxo_tree.update_from_utxo_set(&new_utxo_set, &old_utxo_set)?;
251
252 // Generate commitment from validated UTXO tree
253 let computed_block_hash = compute_block_hash(&full_block.block.header);
254 let computed_commitment = utxo_tree.generate_commitment(computed_block_hash, height);
255
256 // Verify commitment supply matches expected
257 use blvm_consensus::economic::total_supply;
258 let expected_supply = total_supply(height) as u64;
259 if computed_commitment.total_supply != expected_supply {
260 return Err(UtxoCommitmentError::VerificationFailed(format!(
261 "Supply mismatch at height {}: computed {}, expected {}",
262 height, computed_commitment.total_supply, expected_supply
263 )));
264 }
265
266 // Note: Commitment root, height, and block hash are verified implicitly
267 // by the fact that we computed them from the validated UTXO set
268
269 // Update utxo_set for next iteration
270 utxo_set = new_utxo_set;
271 }
272
273 Ok(())
274 }
275
276 /// Process a filtered block and update UTXO set
277 ///
278 /// Takes a block with transactions (already filtered or to be filtered),
279 /// applies spam filter, updates UTXO set, and verifies commitment.
280 ///
281 /// **Critical**: This function processes ALL transactions to remove spent inputs,
282 /// but only adds non-spam outputs to the UTXO tree. This ensures UTXO set consistency:
283 /// - Spam transactions that spend non-spam inputs will still remove those inputs
284 /// - Only non-spam outputs are added to the tree (bandwidth savings)
285 /// - UTXO set remains consistent with actual blockchain state
286 ///
287 /// Note: This function applies transactions to the UTXO tree for commitment
288 /// purposes. Full signature verification should be done during block validation
289 /// before calling this function. This function assumes transactions are already
290 /// validated.
291 pub fn process_filtered_block(
292 &self,
293 utxo_tree: &mut UtxoMerkleTree,
294 block_height: Natural,
295 block_transactions: &[Transaction],
296 ) -> UtxoCommitmentResult<(SpamSummary, HashType)> {
297 use blvm_consensus::transaction::is_coinbase;
298
299 let mut spam_summary = SpamSummary {
300 filtered_count: 0,
301 filtered_size: 0,
302 by_type: SpamBreakdown::default(),
303 };
304
305 // Process ALL transactions (including spam) to remove spent inputs
306 // This is critical for UTXO set consistency: even spam transactions must
307 // remove their spent inputs from the tree.
308 for tx in block_transactions {
309 // Check if transaction is spam (for output filtering)
310 let spam_result = self.spam_filter.is_spam(tx);
311 let is_spam = spam_result.is_spam;
312
313 // Update spam summary
314 if is_spam {
315 spam_summary.filtered_count += 1;
316 spam_summary.filtered_size +=
317 blvm_consensus::transaction::calculate_transaction_size(tx) as u64;
318
319 // Update breakdown
320 for spam_type in &spam_result.detected_types {
321 match spam_type {
322 SpamType::Ordinals => {
323 spam_summary.by_type.ordinals += 1;
324 }
325 SpamType::Dust => {
326 spam_summary.by_type.dust += 1;
327 }
328 SpamType::BRC20 => {
329 spam_summary.by_type.brc20 += 1;
330 }
331 SpamType::LargeWitness => {
332 spam_summary.by_type.ordinals += 1; // Count as Ordinals
333 }
334 SpamType::LowFeeRate => {
335 spam_summary.by_type.dust += 1; // Count as suspicious
336 }
337 SpamType::HighSizeValueRatio => {
338 spam_summary.by_type.ordinals += 1; // Count as Ordinals
339 }
340 SpamType::ManySmallOutputs => {
341 spam_summary.by_type.dust += 1; // Count as dust-like
342 }
343 SpamType::NotSpam => {}
344 }
345 }
346 }
347
348 // Compute transaction ID for proper outpoint creation
349 let tx_id = compute_tx_id(tx);
350
351 // CRITICAL: Remove spent inputs from ALL transactions (including spam)
352 // This ensures UTXO set consistency even when spam transactions spend non-spam inputs
353 if !is_coinbase(tx) {
354 for input in &tx.inputs {
355 // Get the UTXO first (needed for remove to update tracking)
356 match utxo_tree.get(&input.prevout) {
357 Ok(Some(utxo)) => {
358 // Remove the UTXO (even if transaction is spam)
359 if let Err(e) = utxo_tree.remove(&input.prevout, &utxo) {
360 return Err(UtxoCommitmentError::TransactionApplication(format!(
361 "Failed to remove spent input: {e:?}"
362 )));
363 }
364 }
365 Ok(None) => {
366 // UTXO doesn't exist - this might be valid if it was already spent
367 // or invalid if the transaction wasn't properly validated
368 // Continue but log - this should be validated before calling
369 }
370 Err(e) => {
371 return Err(UtxoCommitmentError::TransactionApplication(format!(
372 "Failed to get UTXO for removal: {e:?}"
373 )));
374 }
375 }
376 }
377 }
378
379 // Only add outputs from non-spam transactions
380 // This provides bandwidth savings while maintaining UTXO set consistency
381 if !is_spam {
382 for (i, output) in tx.outputs.iter().enumerate() {
383 let outpoint = OutPoint {
384 hash: tx_id,
385 index: i as u32,
386 };
387
388 let utxo = UTXO {
389 value: output.value,
390 script_pubkey: output.script_pubkey.as_slice().into(),
391 height: block_height,
392 is_coinbase: is_coinbase(tx),
393 };
394
395 if let Err(e) = utxo_tree.insert(outpoint, utxo) {
396 return Err(UtxoCommitmentError::TransactionApplication(format!(
397 "Failed to add output: {e:?}"
398 )));
399 }
400 }
401 }
402 // Spam transaction outputs are skipped (not added to tree)
403 }
404
405 // Return summary and new root
406 let root = utxo_tree.root();
407
408 Ok((spam_summary, root))
409 }
410}
411
412/// Update UTXO commitments after block connection
413///
414/// This function should be called after successfully connecting a block
415/// to keep UTXO commitments synchronized with the blockchain state.
416///
417/// # Arguments
418///
419/// * `utxo_tree` - Mutable reference to the UTXO Merkle tree
420/// * `block` - The block that was just connected
421/// * `block_height` - Height of the connected block
422/// * `spam_filter` - Optional spam filter (if None, all transactions are included)
423///
424/// # Returns
425///
426/// Returns the new Merkle root hash of the UTXO tree.
427///
428/// # Example
429///
430/// ```rust
431/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
432/// use blvm_consensus::block::connect_block;
433/// use blvm_protocol::utxo_commitments::{UtxoMerkleTree, update_commitments_after_block};
434/// use blvm_consensus::spam_filter::SpamFilter;
435/// use blvm_consensus::types::{Network, ValidationResult};
436///
437/// # let block = blvm_consensus::types::Block {
438/// # header: blvm_consensus::types::BlockHeader {
439/// # version: 1, prev_block_hash: [0; 32], merkle_root: [0; 32],
440/// # timestamp: 1234567890, bits: 0x1d00ffff, nonce: 0,
441/// # },
442/// # transactions: vec![].into(),
443/// # };
444/// # let witnesses = vec![];
445/// # let utxo_set = blvm_consensus::types::UtxoSet::new();
446/// # let height = 0;
447/// # let mut utxo_tree = UtxoMerkleTree::new()?;
448/// let (result, new_utxo_set, _) = connect_block(&block, &witnesses, utxo_set, height, None, Network::Regtest)?;
449/// if matches!(result, ValidationResult::Valid) {
450/// let spam_filter = SpamFilter::new();
451/// let root = update_commitments_after_block(
452/// &mut utxo_tree,
453/// &block,
454/// height,
455/// Some(&spam_filter),
456/// )?;
457/// println!("New UTXO commitment root: {:?}", root);
458/// }
459/// # Ok(())
460/// # }
461/// ```
462#[cfg(feature = "utxo-commitments")]
463pub fn update_commitments_after_block(
464 utxo_tree: &mut UtxoMerkleTree,
465 block: &crate::types::Block,
466 block_height: Natural,
467 spam_filter: Option<&SpamFilter>,
468) -> UtxoCommitmentResult<HashType> {
469 use blvm_consensus::block::calculate_tx_id;
470 use blvm_consensus::transaction::is_coinbase;
471
472 // If spam filter is provided, use filtered processing
473 if let Some(filter) = spam_filter {
474 let initial_sync = InitialSync {
475 peer_consensus: crate::utxo_commitments::peer_consensus::PeerConsensus::new(
476 crate::utxo_commitments::peer_consensus::ConsensusConfig::default(),
477 ),
478 spam_filter: filter.clone(),
479 };
480 let (_, root) =
481 initial_sync.process_filtered_block(utxo_tree, block_height, &block.transactions)?;
482 Ok(root)
483 } else {
484 // No spam filter: process all transactions normally
485 for tx in &block.transactions {
486 let tx_id = calculate_tx_id(tx);
487
488 // Remove spent inputs (except coinbase)
489 if !is_coinbase(tx) {
490 for input in &tx.inputs {
491 // Get the UTXO first (needed for remove)
492 match utxo_tree.get(&input.prevout) {
493 Ok(Some(utxo)) => {
494 utxo_tree.remove(&input.prevout, &utxo)?;
495 }
496 Ok(None) => {
497 // UTXO doesn't exist - might be invalid or already spent
498 // Continue but this should have been caught during validation
499 }
500 Err(e) => {
501 return Err(UtxoCommitmentError::TransactionApplication(format!(
502 "Failed to get UTXO for removal: {e:?}"
503 )));
504 }
505 }
506 }
507 }
508
509 // Add new outputs
510 for (i, output) in tx.outputs.iter().enumerate() {
511 let outpoint = blvm_consensus::types::OutPoint {
512 hash: tx_id,
513 index: i as u32,
514 };
515
516 let utxo = blvm_consensus::types::UTXO {
517 value: output.value,
518 script_pubkey: output.script_pubkey.as_slice().into(),
519 height: block_height,
520 is_coinbase: is_coinbase(tx),
521 };
522
523 utxo_tree.insert(outpoint, utxo)?;
524 }
525 }
526
527 Ok(utxo_tree.root())
528 }
529}
530
531/// Compute transaction ID (txid) using Bitcoin's standard double SHA256
532///
533/// Transaction ID is computed as: SHA256(SHA256(serialized_tx))
534/// where serialized_tx is the transaction in Bitcoin wire format (non-SegWit format).
535///
536/// Note: For SegWit transactions, the txid still uses the non-witness serialization
537/// (witness data is excluded from txid calculation).
538///
539/// This matches consensus transaction ID computation exactly.
540fn compute_tx_id(tx: &Transaction) -> HashType {
541 use crate::serialization::transaction::serialize_transaction;
542 use sha2::{Digest, Sha256};
543
544 // Serialize transaction to Bitcoin wire format (non-SegWit format for txid)
545 let serialized = serialize_transaction(tx);
546
547 // Double SHA256 (Bitcoin standard for transaction IDs)
548 let first_hash = Sha256::digest(&serialized);
549 let second_hash = Sha256::digest(first_hash);
550
551 // Convert to HashType [u8; 32]
552 let mut txid = [0u8; 32];
553 txid.copy_from_slice(&second_hash);
554
555 txid
556}
557
558/// Compute block header hash (double SHA256)
559fn compute_block_hash(header: &BlockHeader) -> HashType {
560 use sha2::{Digest, Sha256};
561
562 let mut bytes = Vec::with_capacity(80);
563 bytes.extend_from_slice(&header.version.to_le_bytes());
564 bytes.extend_from_slice(&header.prev_block_hash);
565 bytes.extend_from_slice(&header.merkle_root);
566 bytes.extend_from_slice(&header.timestamp.to_le_bytes());
567 bytes.extend_from_slice(&header.bits.to_le_bytes());
568 bytes.extend_from_slice(&header.nonce.to_le_bytes());
569
570 let first_hash = Sha256::digest(&bytes);
571 let second_hash = Sha256::digest(first_hash);
572
573 let mut hash = [0u8; 32];
574 hash.copy_from_slice(&second_hash);
575 hash
576}