Skip to main content

blvm_protocol/utxo_commitments/
peer_consensus.rs

1//! Peer Consensus Protocol
2//!
3//! Implements the N-of-M peer consensus model for UTXO set verification.
4//! Discovers diverse peers and finds consensus among them to verify UTXO commitments
5//! without trusting any single peer.
6
7#[cfg(feature = "utxo-commitments")]
8use crate::utxo_commitments::data_structures::{
9    UtxoCommitment, UtxoCommitmentError, UtxoCommitmentResult,
10};
11#[cfg(feature = "utxo-commitments")]
12use crate::utxo_commitments::network_integration::UtxoCommitmentsNetworkClient;
13#[cfg(feature = "utxo-commitments")]
14use crate::utxo_commitments::verification::{verify_header_chain, verify_supply};
15#[cfg(feature = "utxo-commitments")]
16use blvm_consensus::types::{BlockHeader, Hash, Natural};
17#[cfg(feature = "utxo-commitments")]
18#[cfg(feature = "spec-lock")]
19use blvm_spec_lock::spec_locked;
20#[cfg(feature = "utxo-commitments")]
21use sparse_merkle_tree::MerkleProof;
22#[cfg(feature = "utxo-commitments")]
23use std::collections::{HashMap, HashSet};
24#[cfg(feature = "utxo-commitments")]
25use std::net::IpAddr;
26
27/// Peer information for diversity tracking
28#[derive(Debug, Clone)]
29pub struct PeerInfo {
30    pub address: IpAddr,
31    pub asn: Option<u32>,               // Autonomous System Number
32    pub country: Option<String>,        // Country code (ISO 3166-1 alpha-2)
33    pub implementation: Option<String>, // Node implementation
34    pub subnet: u32,                    // /16 subnet for diversity checks
35}
36
37impl PeerInfo {
38    /// Extract /16 subnet from IP address
39    pub fn extract_subnet(ip: IpAddr) -> u32 {
40        match ip {
41            IpAddr::V4(ipv4) => {
42                let octets = ipv4.octets();
43                ((octets[0] as u32) << 24) | ((octets[1] as u32) << 16)
44            }
45            IpAddr::V6(ipv6) => {
46                // For IPv6, use first 32 bits for subnet
47                let segments = ipv6.segments();
48                ((segments[0] as u32) << 16) | (segments[1] as u32)
49            }
50        }
51    }
52}
53
54/// Peer with UTXO commitment response
55#[derive(Debug, Clone)]
56pub struct PeerCommitment {
57    pub peer_info: PeerInfo,
58    pub commitment: UtxoCommitment,
59}
60
61/// Consensus result from peer queries
62#[derive(Debug, Clone)]
63pub struct ConsensusResult {
64    /// The consensus UTXO commitment (agreed upon by majority)
65    pub commitment: UtxoCommitment,
66    /// Number of peers that agreed (out of total queried)
67    pub agreement_count: usize,
68    pub total_peers: usize,
69    /// Agreement percentage (0.0 to 1.0)
70    pub agreement_ratio: f64,
71    /// Per-UTXO proofs carried with the commitment. Empty means none were supplied.
72    pub proofs: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
73}
74
75/// Peer consensus configuration
76#[derive(Debug, Clone)]
77pub struct ConsensusConfig {
78    /// Minimum number of diverse peers required
79    pub min_peers: usize,
80    /// Target number of peers to query
81    pub target_peers: usize,
82    /// Consensus threshold (0.0 to 1.0, e.g., 0.8 = 80%)
83    pub consensus_threshold: f64,
84    /// Maximum peers per ASN
85    pub max_peers_per_asn: usize,
86    /// Block safety margin (blocks back from tip)
87    pub safety_margin: Natural,
88    /// Shuffle peers before diversity selection (eclipse resistance). Default: true.
89    /// Set to false for deterministic tests or formal verification.
90    pub shuffle_peers: bool,
91}
92
93impl Default for ConsensusConfig {
94    fn default() -> Self {
95        Self {
96            min_peers: 5,
97            target_peers: 10,
98            consensus_threshold: 0.8, // 80% agreement required
99            max_peers_per_asn: 2,
100            safety_margin: 2016, // ~2 weeks of blocks
101            shuffle_peers: true,
102        }
103    }
104}
105
106/// Peer consensus manager
107pub struct PeerConsensus {
108    pub config: ConsensusConfig,
109}
110
111impl PeerConsensus {
112    /// Create a new peer consensus manager
113    pub fn new(config: ConsensusConfig) -> Self {
114        Self { config }
115    }
116
117    /// Discover diverse peers
118    ///
119    /// Filters peers to ensure diversity across:
120    /// - ASNs (max N per ASN)
121    /// - Subnets (/16 for IPv4, /32 for IPv6)
122    /// - Geographic regions
123    /// - Bitcoin implementations
124    #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
125    pub fn discover_diverse_peers(&self, all_peers: Vec<PeerInfo>) -> Vec<PeerInfo> {
126        use rand::seq::SliceRandom;
127
128        // Shuffle to prevent predictable peer selection (eclipse resistance)
129        let mut peers = all_peers;
130        if self.config.shuffle_peers {
131            peers.shuffle(&mut rand::thread_rng());
132        }
133
134        let mut diverse_peers = Vec::new();
135        let mut seen_asn: HashMap<u32, usize> = HashMap::new();
136        let mut seen_subnets: HashSet<u32> = HashSet::new();
137        let _seen_countries: HashSet<String> = HashSet::new();
138
139        for peer in peers {
140            // Check ASN limit
141            if let Some(asn) = peer.asn {
142                let asn_count = seen_asn.entry(asn).or_insert(0);
143                if *asn_count >= self.config.max_peers_per_asn {
144                    continue; // Skip - too many peers from this ASN
145                }
146                *asn_count += 1;
147            }
148
149            // Check subnet (no peers from same /16)
150            if seen_subnets.contains(&peer.subnet) {
151                continue; // Skip - duplicate subnet
152            }
153            seen_subnets.insert(peer.subnet);
154
155            // Add diverse peer
156            diverse_peers.push(peer);
157
158            // Stop when we have enough
159            if diverse_peers.len() >= self.config.target_peers {
160                break;
161            }
162        }
163
164        diverse_peers
165    }
166
167    /// Determine checkpoint height based on peer chain tips
168    ///
169    /// Uses median of peer tips minus safety margin to prevent deep reorgs.
170    ///
171    /// Mathematical invariants:
172    /// - Median is always between min(tips) and max(tips)
173    /// - Checkpoint height is always >= 0
174    /// - Checkpoint height <= median_tip
175    #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
176    pub fn determine_checkpoint_height(&self, peer_tips: Vec<Natural>) -> Natural {
177        if peer_tips.is_empty() {
178            return 0;
179        }
180
181        // Sort to find median
182        let mut sorted_tips = peer_tips;
183        sorted_tips.sort();
184
185        // Runtime assertion: Verify sorted order
186        debug_assert!(
187            sorted_tips.windows(2).all(|w| w[0] <= w[1]),
188            "Tips must be sorted in ascending order"
189        );
190
191        let median_tip = if sorted_tips.len() % 2 == 0 {
192            // Even number: average of middle two
193            let mid = sorted_tips.len() / 2;
194            let lower = sorted_tips[mid - 1];
195            let upper = sorted_tips[mid];
196
197            // Runtime assertion: Verify median bounds
198            debug_assert!(
199                lower <= upper,
200                "Lower median value ({lower}) must be <= upper ({upper})"
201            );
202
203            // Use checked arithmetic to prevent overflow
204            (lower + upper) / 2
205        } else {
206            // Odd number: middle value
207            sorted_tips[sorted_tips.len() / 2]
208        };
209
210        // Runtime assertion: Median is within bounds
211        if let (Some(&min_tip), Some(&max_tip)) = (sorted_tips.first(), sorted_tips.last()) {
212            debug_assert!(
213                median_tip >= min_tip && median_tip <= max_tip,
214                "Median ({median_tip}) must be between min ({min_tip}) and max ({max_tip})"
215            );
216        }
217
218        // Apply safety margin with checked arithmetic
219        if median_tip > self.config.safety_margin {
220            let checkpoint = median_tip - self.config.safety_margin;
221
222            // Runtime assertion: Checkpoint is non-negative and <= median
223            debug_assert!(
224                checkpoint <= median_tip,
225                "Checkpoint ({checkpoint}) must be <= median ({median_tip})"
226            );
227
228            checkpoint
229        } else {
230            0 // Genesis block
231        }
232    }
233
234    /// Request UTXO sets from multiple peers
235    ///
236    /// Sends GetUTXOSet messages via the provided network client and collects responses.
237    /// Returns list of peer commitments (peer + commitment pairs).
238    ///
239    /// The caller provides a list of `(PeerInfo, peer_id)` tuples where `peer_id` is an
240    /// opaque identifier understood only by the node's networking layer. This keeps the
241    /// consensus layer agnostic of concrete transport types or peer address formats.
242    pub async fn request_utxo_sets<C: UtxoCommitmentsNetworkClient>(
243        &self,
244        network_client: &C,
245        peers: &[(PeerInfo, String)],
246        checkpoint_height: Natural,
247        checkpoint_hash: Hash,
248    ) -> Vec<PeerCommitment> {
249        let mut results = Vec::new();
250
251        for (peer_info, peer_id) in peers {
252            match network_client
253                .request_utxo_set(peer_id, checkpoint_height, checkpoint_hash)
254                .await
255            {
256                Ok(commitment) => results.push(PeerCommitment {
257                    peer_info: peer_info.clone(),
258                    commitment,
259                }),
260                Err(_) => {
261                    // Per-peer failure; consensus threshold handles partial failures.
262                    continue;
263                }
264            }
265        }
266
267        results
268    }
269
270    /// Find consensus among peer responses
271    ///
272    /// Groups commitments by their values and finds the majority consensus.
273    /// Returns the consensus commitment if threshold is met.
274    #[cfg_attr(feature = "spec-lock", spec_locked("11.4"))]
275    pub fn find_consensus(
276        &self,
277        peer_commitments: Vec<PeerCommitment>,
278    ) -> UtxoCommitmentResult<ConsensusResult> {
279        let total_peers = peer_commitments.len();
280        if total_peers < self.config.min_peers {
281            return Err(UtxoCommitmentError::VerificationFailed(format!(
282                "Insufficient peers: got {}, need at least {}",
283                total_peers, self.config.min_peers
284            )));
285        }
286
287        // Group commitments by their values (merkle root + supply + count + height)
288        let mut commitment_groups: HashMap<(Hash, u64, u64, Natural), Vec<PeerCommitment>> =
289            HashMap::new();
290
291        for peer_commitment in peer_commitments {
292            let key = (
293                peer_commitment.commitment.merkle_root,
294                peer_commitment.commitment.total_supply,
295                peer_commitment.commitment.utxo_count,
296                peer_commitment.commitment.block_height,
297            );
298            commitment_groups
299                .entry(key)
300                .or_default()
301                .push(peer_commitment);
302        }
303
304        // Find group with highest agreement
305        type CommitmentKey = (Hash, u64, u64, Natural);
306        let mut best_group: Option<(&CommitmentKey, Vec<PeerCommitment>)> = None;
307        let mut best_agreement_count = 0;
308
309        for (key, group) in commitment_groups.iter() {
310            let agreement_count = group.len();
311
312            if agreement_count > best_agreement_count {
313                best_agreement_count = agreement_count;
314                best_group = Some((key, group.clone()));
315            }
316        }
317
318        // Check if we found any consensus group
319        let (_, group) = match best_group {
320            Some(g) => g,
321            None => {
322                return Err(UtxoCommitmentError::VerificationFailed(
323                    "No consensus groups found".to_string(),
324                ));
325            }
326        };
327
328        // Check if consensus threshold is met using integer arithmetic to avoid floating-point precision issues
329        // Threshold check: agreement_count / total_peers >= consensus_threshold
330        // Equivalent to: agreement_count >= (total_peers * consensus_threshold).ceil()
331        //
332        // Mathematical invariant: required_agreement_count must satisfy:
333        // - required_agreement_count >= ceil(total_peers * threshold)
334        // - required_agreement_count <= total_peers
335        // - If agreement_count >= required_agreement_count, then agreement_count/total_peers >= threshold
336        let required_agreement_count =
337            ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
338
339        // Runtime assertion: Verify mathematical invariants
340        debug_assert!(
341            required_agreement_count <= total_peers,
342            "Required agreement count ({required_agreement_count}) cannot exceed total peers ({total_peers})"
343        );
344        debug_assert!(
345            required_agreement_count >= 1,
346            "Required agreement count must be at least 1"
347        );
348        debug_assert!(
349            best_agreement_count <= total_peers,
350            "Best agreement count ({best_agreement_count}) cannot exceed total peers ({total_peers})"
351        );
352
353        if best_agreement_count < required_agreement_count {
354            let best_ratio = best_agreement_count as f64 / total_peers as f64;
355            return Err(UtxoCommitmentError::VerificationFailed(format!(
356                "No consensus: best agreement is {:.1}% ({} of {} peers), need {:.1}% (at least {} peers)",
357                best_ratio * 100.0,
358                best_agreement_count,
359                total_peers,
360                self.config.consensus_threshold * 100.0,
361                required_agreement_count
362            )));
363        }
364
365        // Return consensus result
366        let commitment = group[0].commitment.clone();
367        let agreement_count = group.len();
368        let agreement_ratio = agreement_count as f64 / total_peers as f64;
369
370        // Runtime assertion: Verify consensus result invariants
371        debug_assert!(
372            agreement_count >= required_agreement_count,
373            "Agreement count ({agreement_count}) must meet threshold ({required_agreement_count})"
374        );
375        debug_assert!(
376            agreement_ratio >= self.config.consensus_threshold,
377            "Agreement ratio ({:.4}) must be >= threshold ({:.4})",
378            agreement_ratio,
379            self.config.consensus_threshold
380        );
381        debug_assert!(
382            agreement_count <= total_peers,
383            "Agreement count ({agreement_count}) cannot exceed total peers ({total_peers})"
384        );
385        debug_assert!(
386            (0.0..=1.0).contains(&agreement_ratio),
387            "Agreement ratio ({agreement_ratio:.4}) must be in [0, 1]"
388        );
389
390        Ok(ConsensusResult {
391            commitment,
392            agreement_count,
393            total_peers,
394            agreement_ratio,
395            proofs: Vec::new(),
396        })
397    }
398
399    /// Verify consensus commitment against block headers
400    ///
401    /// Verifies that:
402    /// 1. Block header chain is valid (PoW verification)
403    /// 2. Commitment supply matches expected supply at height
404    /// 3. Commitment block hash matches actual block hash
405    #[cfg_attr(
406        feature = "spec-lock",
407        spec_locked("11.4", "VerifyConsensusCommitment")
408    )]
409    #[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(header_chain.len() > 0 || result.is_err()))]
410    pub fn verify_consensus_commitment(
411        &self,
412        consensus: &ConsensusResult,
413        header_chain: &[BlockHeader],
414    ) -> UtxoCommitmentResult<bool> {
415        // 1. Verify header chain (PoW)
416        verify_header_chain(header_chain)?;
417
418        // 2. Verify supply matches expected
419        verify_supply(&consensus.commitment)?;
420
421        // 3. Verify commitment block hash matches header chain
422        if consensus.commitment.block_height as usize >= header_chain.len() {
423            return Err(UtxoCommitmentError::VerificationFailed(format!(
424                "Commitment height {} exceeds header chain length {}",
425                consensus.commitment.block_height,
426                header_chain.len()
427            )));
428        }
429
430        let expected_header = &header_chain[consensus.commitment.block_height as usize];
431        let expected_hash = compute_block_hash(expected_header);
432
433        if consensus.commitment.block_hash != expected_hash {
434            return Err(UtxoCommitmentError::VerificationFailed(format!(
435                "Block hash mismatch: commitment has {:?}, header chain has {:?}",
436                consensus.commitment.block_hash, expected_hash
437            )));
438        }
439
440        Ok(true)
441    }
442
443    /// Verify UTXO proofs for critical UTXOs after consensus verification
444    ///
445    /// This function should be called after `verify_consensus_commitment()` succeeds
446    /// to cryptographically verify that specific UTXOs exist in the consensus commitment.
447    ///
448    /// This prevents coin freezing attacks where malicious peers provide commitments
449    /// with correct total supply but missing/modified individual UTXOs.
450    ///
451    /// # Arguments
452    /// * `consensus` - The verified consensus commitment
453    /// * `utxos_to_verify` - Vector of (outpoint, expected_utxo, proof) tuples to verify
454    ///
455    /// # Returns
456    /// `Ok(true)` if all proofs are valid, `Err` if any verification fails
457    ///
458    /// # Example
459    /// ```rust,no_run
460    /// use blvm_protocol::utxo_commitments::{UtxoMerkleTree, peer_consensus::PeerConsensus};
461    ///
462    /// // After verify_consensus_commitment succeeds:
463    /// let utxos_to_verify = vec![
464    ///     (outpoint1, utxo1, proof1),
465    ///     (outpoint2, utxo2, proof2),
466    /// ];
467    ///
468    /// peer_consensus.verify_utxo_proofs(&consensus, utxos_to_verify)?;
469    /// ```
470    #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
471    pub fn verify_utxo_proofs(
472        &self,
473        consensus: &ConsensusResult,
474        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
475    ) -> UtxoCommitmentResult<bool> {
476        use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
477
478        // Sequential verification (baseline)
479        for (outpoint, expected_utxo, proof) in utxos_to_verify {
480            let is_valid = UtxoMerkleTree::verify_utxo_proof(
481                &consensus.commitment,
482                &outpoint,
483                &expected_utxo,
484                proof,
485            )?;
486
487            if !is_valid {
488                return Err(UtxoCommitmentError::VerificationFailed(format!(
489                    "UTXO proof verification failed for outpoint {outpoint:?} - possible attack"
490                )));
491            }
492        }
493
494        Ok(true)
495    }
496
497    /// Verify UTXO proofs in parallel (optimized for batch verification)
498    ///
499    /// This function performs parallel verification of multiple UTXO proofs,
500    /// which is more efficient when verifying many UTXOs at once.
501    ///
502    /// # Arguments
503    /// * `consensus` - The verified consensus commitment
504    /// * `utxos_to_verify` - Vector of (outpoint, expected_utxo, proof) tuples to verify
505    ///
506    /// # Returns
507    /// `Ok(true)` if all proofs are valid, `Err` if any verification fails
508    ///
509    /// # Performance
510    /// Uses parallel processing for better performance when verifying many proofs.
511    /// For small batches (< 10), sequential verification may be faster due to overhead.
512    #[cfg(feature = "parallel-verification")]
513    pub fn verify_utxo_proofs_parallel(
514        &self,
515        consensus: &ConsensusResult,
516        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
517    ) -> UtxoCommitmentResult<bool> {
518        use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
519        use rayon::prelude::*;
520
521        // Parallel verification using rayon
522        let results: Vec<UtxoCommitmentResult<bool>> = utxos_to_verify
523            .into_par_iter()
524            .map(|(outpoint, expected_utxo, proof)| {
525                UtxoMerkleTree::verify_utxo_proof(
526                    &consensus.commitment,
527                    &outpoint,
528                    &expected_utxo,
529                    proof,
530                )
531            })
532            .collect();
533
534        // Check all results
535        for (i, result) in results.iter().enumerate() {
536            match result {
537                Ok(true) => continue,
538                Ok(false) | Err(_) => {
539                    return Err(UtxoCommitmentError::VerificationFailed(format!(
540                        "UTXO proof verification failed at index {i} - possible attack"
541                    )));
542                }
543            }
544        }
545
546        Ok(true)
547    }
548
549    /// Verify UTXO proofs in parallel (fallback for when rayon is not available)
550    ///
551    /// Falls back to sequential verification if parallel feature is not enabled.
552    pub fn verify_utxo_proofs_parallel_fallback(
553        &self,
554        consensus: &ConsensusResult,
555        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
556    ) -> UtxoCommitmentResult<bool> {
557        // Use sequential verification as fallback
558        self.verify_utxo_proofs(consensus, utxos_to_verify)
559    }
560}
561
562/// Compute block header hash (double SHA256)
563fn compute_block_hash(header: &BlockHeader) -> Hash {
564    use sha2::{Digest, Sha256};
565
566    // Serialize block header
567    let mut bytes = Vec::with_capacity(80);
568    bytes.extend_from_slice(&header.version.to_le_bytes());
569    bytes.extend_from_slice(&header.prev_block_hash);
570    bytes.extend_from_slice(&header.merkle_root);
571    bytes.extend_from_slice(&header.timestamp.to_le_bytes());
572    bytes.extend_from_slice(&header.bits.to_le_bytes());
573    bytes.extend_from_slice(&header.nonce.to_le_bytes());
574
575    // Double SHA256
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}
583
584/// Accept a commitment only when its per-UTXO proofs match. An empty list fails.
585pub fn accept_commitment_proofs(
586    consensus: &ConsensusResult,
587) -> UtxoCommitmentResult<UtxoCommitment> {
588    if consensus.proofs.is_empty() {
589        return Err(UtxoCommitmentError::VerificationFailed(
590            "per-UTXO proof is required".into(),
591        ));
592    }
593    let checker = PeerConsensus::new(ConsensusConfig::default());
594    checker.verify_utxo_proofs(consensus, consensus.proofs.clone())?;
595    Ok(consensus.commitment.clone())
596}
597
598// ============================================================================
599// FORMAL VERIFICATION
600// ============================================================================
601
602/// Mathematical Specification for Peer Consensus:
603/// ∀ peers ∈ [PeerInfo], commitments ∈ [UtxoCommitment], threshold ∈ [0,1]:
604/// - find_consensus(commitments, threshold) = consensus ⟺
605///     |{c ∈ commitments | c = consensus}| / |commitments| ≥ threshold
606/// - discover_diverse_peers(peers) ⊆ peers (no new peers created)
607/// - verify_consensus_commitment(consensus, headers) verifies PoW + supply
608///
609/// Invariants:
610/// - Consensus requires threshold percentage agreement
611/// - Diverse peer discovery filters for diversity
612#[doc(hidden)]
613const _PEER_CONSENSUS_SPEC: () = ();
614
615// End of module