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