Skip to main content

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::UnexecIf
344                        | SpamType::NullDataOp13
345                        | SpamType::NullDataMagic
346                        | SpamType::DataLikeMs => {
347                            spam_summary.by_type.ordinals += 1;
348                        }
349                        SpamType::NotSpam => {}
350                    }
351                }
352            }
353
354            // Compute transaction ID for proper outpoint creation
355            let tx_id = compute_tx_id(tx);
356
357            // CRITICAL: Remove spent inputs from ALL transactions (including spam)
358            // This ensures UTXO set consistency even when spam transactions spend non-spam inputs
359            if !is_coinbase(tx) {
360                for input in &tx.inputs {
361                    // Get the UTXO first (needed for remove to update tracking)
362                    match utxo_tree.get(&input.prevout) {
363                        Ok(Some(utxo)) => {
364                            // Remove the UTXO (even if transaction is spam)
365                            if let Err(e) = utxo_tree.remove(&input.prevout, &utxo) {
366                                return Err(UtxoCommitmentError::TransactionApplication(format!(
367                                    "Failed to remove spent input: {e:?}"
368                                )));
369                            }
370                        }
371                        Ok(None) => {
372                            // UTXO doesn't exist - this might be valid if it was already spent
373                            // or invalid if the transaction wasn't properly validated
374                            // Continue but log - this should be validated before calling
375                        }
376                        Err(e) => {
377                            return Err(UtxoCommitmentError::TransactionApplication(format!(
378                                "Failed to get UTXO for removal: {e:?}"
379                            )));
380                        }
381                    }
382                }
383            }
384
385            // Only add outputs from non-spam transactions
386            // This provides bandwidth savings while maintaining UTXO set consistency
387            if !is_spam {
388                for (i, output) in tx.outputs.iter().enumerate() {
389                    let outpoint = OutPoint {
390                        hash: tx_id,
391                        index: i as u32,
392                    };
393
394                    let utxo = UTXO {
395                        value: output.value,
396                        script_pubkey: output.script_pubkey.as_slice().into(),
397                        height: block_height,
398                        is_coinbase: is_coinbase(tx),
399                    };
400
401                    if let Err(e) = utxo_tree.insert(outpoint, utxo) {
402                        return Err(UtxoCommitmentError::TransactionApplication(format!(
403                            "Failed to add output: {e:?}"
404                        )));
405                    }
406                }
407            }
408            // Spam transaction outputs are skipped (not added to tree)
409        }
410
411        // Return summary and new root
412        let root = utxo_tree.root();
413
414        Ok((spam_summary, root))
415    }
416}
417
418/// Update UTXO commitments after block connection
419///
420/// This function should be called after successfully connecting a block
421/// to keep UTXO commitments synchronized with the blockchain state.
422///
423/// # Arguments
424///
425/// * `utxo_tree` - Mutable reference to the UTXO Merkle tree
426/// * `block` - The block that was just connected
427/// * `block_height` - Height of the connected block
428/// * `spam_filter` - Optional spam filter (if None, all transactions are included)
429///
430/// # Returns
431///
432/// Returns the new Merkle root hash of the UTXO tree.
433///
434/// # Example
435///
436/// ```rust
437/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
438/// use blvm_consensus::block::connect_block;
439/// use blvm_protocol::utxo_commitments::{UtxoMerkleTree, update_commitments_after_block};
440/// use blvm_consensus::spam_filter::SpamFilter;
441/// use blvm_consensus::types::{Network, ValidationResult};
442///
443/// # let block = blvm_consensus::types::Block {
444/// #     header: blvm_consensus::types::BlockHeader {
445/// #         version: 1, prev_block_hash: [0; 32], merkle_root: [0; 32],
446/// #         timestamp: 1234567890, bits: 0x1d00ffff, nonce: 0,
447/// #     },
448/// #     transactions: vec![].into(),
449/// # };
450/// # let witnesses = vec![];
451/// # let utxo_set = blvm_consensus::types::UtxoSet::new();
452/// # let height = 0;
453/// # let mut utxo_tree = UtxoMerkleTree::new()?;
454/// let (result, new_utxo_set, _) = connect_block(&block, &witnesses, utxo_set, height, None, Network::Regtest)?;
455/// if matches!(result, ValidationResult::Valid) {
456///     let spam_filter = SpamFilter::new();
457///     let root = update_commitments_after_block(
458///         &mut utxo_tree,
459///         &block,
460///         height,
461///         Some(&spam_filter),
462///     )?;
463///     println!("New UTXO commitment root: {:?}", root);
464/// }
465/// # Ok(())
466/// # }
467/// ```
468#[cfg(feature = "utxo-commitments")]
469pub fn update_commitments_after_block(
470    utxo_tree: &mut UtxoMerkleTree,
471    block: &crate::types::Block,
472    block_height: Natural,
473    spam_filter: Option<&SpamFilter>,
474) -> UtxoCommitmentResult<HashType> {
475    use blvm_consensus::block::calculate_tx_id;
476    use blvm_consensus::transaction::is_coinbase;
477
478    // If spam filter is provided, use filtered processing
479    if let Some(filter) = spam_filter {
480        let initial_sync = InitialSync {
481            peer_consensus: crate::utxo_commitments::peer_consensus::PeerConsensus::new(
482                crate::utxo_commitments::peer_consensus::ConsensusConfig::default(),
483            ),
484            spam_filter: filter.clone(),
485        };
486        let (_, root) =
487            initial_sync.process_filtered_block(utxo_tree, block_height, &block.transactions)?;
488        Ok(root)
489    } else {
490        // No spam filter: process all transactions normally
491        for tx in &block.transactions {
492            let tx_id = calculate_tx_id(tx);
493
494            // Remove spent inputs (except coinbase)
495            if !is_coinbase(tx) {
496                for input in &tx.inputs {
497                    // Get the UTXO first (needed for remove)
498                    match utxo_tree.get(&input.prevout) {
499                        Ok(Some(utxo)) => {
500                            utxo_tree.remove(&input.prevout, &utxo)?;
501                        }
502                        Ok(None) => {
503                            // UTXO doesn't exist - might be invalid or already spent
504                            // Continue but this should have been caught during validation
505                        }
506                        Err(e) => {
507                            return Err(UtxoCommitmentError::TransactionApplication(format!(
508                                "Failed to get UTXO for removal: {e:?}"
509                            )));
510                        }
511                    }
512                }
513            }
514
515            // Add new outputs
516            for (i, output) in tx.outputs.iter().enumerate() {
517                let outpoint = blvm_consensus::types::OutPoint {
518                    hash: tx_id,
519                    index: i as u32,
520                };
521
522                let utxo = blvm_consensus::types::UTXO {
523                    value: output.value,
524                    script_pubkey: output.script_pubkey.as_slice().into(),
525                    height: block_height,
526                    is_coinbase: is_coinbase(tx),
527                };
528
529                utxo_tree.insert(outpoint, utxo)?;
530            }
531        }
532
533        Ok(utxo_tree.root())
534    }
535}
536
537/// Compute transaction ID (txid) using Bitcoin's standard double SHA256
538///
539/// Transaction ID is computed as: SHA256(SHA256(serialized_tx))
540/// where serialized_tx is the transaction in Bitcoin wire format (non-SegWit format).
541///
542/// Note: For SegWit transactions, the txid still uses the non-witness serialization
543/// (witness data is excluded from txid calculation).
544///
545/// This matches consensus transaction ID computation exactly.
546fn compute_tx_id(tx: &Transaction) -> HashType {
547    use crate::serialization::transaction::serialize_transaction;
548    use sha2::{Digest, Sha256};
549
550    // Serialize transaction to Bitcoin wire format (non-SegWit format for txid)
551    let serialized = serialize_transaction(tx);
552
553    // Double SHA256 (Bitcoin standard for transaction IDs)
554    let first_hash = Sha256::digest(&serialized);
555    let second_hash = Sha256::digest(first_hash);
556
557    // Convert to HashType [u8; 32]
558    let mut txid = [0u8; 32];
559    txid.copy_from_slice(&second_hash);
560
561    txid
562}
563
564/// Compute block header hash (double SHA256)
565fn compute_block_hash(header: &BlockHeader) -> HashType {
566    use sha2::{Digest, Sha256};
567
568    let mut bytes = Vec::with_capacity(80);
569    bytes.extend_from_slice(&header.version.to_le_bytes());
570    bytes.extend_from_slice(&header.prev_block_hash);
571    bytes.extend_from_slice(&header.merkle_root);
572    bytes.extend_from_slice(&header.timestamp.to_le_bytes());
573    bytes.extend_from_slice(&header.bits.to_le_bytes());
574    bytes.extend_from_slice(&header.nonce.to_le_bytes());
575
576    let first_hash = Sha256::digest(&bytes);
577    let second_hash = Sha256::digest(first_hash);
578
579    let mut hash = [0u8; 32];
580    hash.copy_from_slice(&second_hash);
581    hash
582}