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