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