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 ({lower}) must be <= upper ({upper})"
198            );
199
200            // Use checked arithmetic to prevent overflow
201            (lower + upper) / 2
202        } else {
203            // Odd number: middle value
204            sorted_tips[sorted_tips.len() / 2]
205        };
206
207        // Runtime assertion: Median is within bounds
208        if let (Some(&min_tip), Some(&max_tip)) = (sorted_tips.first(), sorted_tips.last()) {
209            debug_assert!(
210                median_tip >= min_tip && median_tip <= max_tip,
211                "Median ({median_tip}) must be between min ({min_tip}) and max ({max_tip})"
212            );
213        }
214
215        // Apply safety margin with checked arithmetic
216        if median_tip > self.config.safety_margin {
217            let checkpoint = median_tip - self.config.safety_margin;
218
219            // Runtime assertion: Checkpoint is non-negative and <= median
220            debug_assert!(
221                checkpoint <= median_tip,
222                "Checkpoint ({checkpoint}) must be <= median ({median_tip})"
223            );
224
225            checkpoint
226        } else {
227            0 // Genesis block
228        }
229    }
230
231    /// Request UTXO sets from multiple peers
232    ///
233    /// Sends GetUTXOSet messages via the provided network client and collects responses.
234    /// Returns list of peer commitments (peer + commitment pairs).
235    ///
236    /// The caller provides a list of `(PeerInfo, peer_id)` tuples where `peer_id` is an
237    /// opaque identifier understood only by the node's networking layer. This keeps the
238    /// consensus layer agnostic of concrete transport types or peer address formats.
239    pub async fn request_utxo_sets<C: UtxoCommitmentsNetworkClient>(
240        &self,
241        network_client: &C,
242        peers: &[(PeerInfo, String)],
243        checkpoint_height: Natural,
244        checkpoint_hash: Hash,
245    ) -> Vec<PeerCommitment> {
246        let mut results = Vec::new();
247
248        for (peer_info, peer_id) in peers {
249            match network_client
250                .request_utxo_set(peer_id, checkpoint_height, checkpoint_hash)
251                .await
252            {
253                Ok(commitment) => results.push(PeerCommitment {
254                    peer_info: peer_info.clone(),
255                    commitment,
256                }),
257                Err(_) => {
258                    // Per-peer failure; consensus threshold handles partial failures.
259                    continue;
260                }
261            }
262        }
263
264        results
265    }
266
267    /// Find consensus among peer responses
268    ///
269    /// Groups commitments by their values and finds the majority consensus.
270    /// Returns the consensus commitment if threshold is met.
271    #[spec_locked("11.4")]
272    pub fn find_consensus(
273        &self,
274        peer_commitments: Vec<PeerCommitment>,
275    ) -> UtxoCommitmentResult<ConsensusResult> {
276        let total_peers = peer_commitments.len();
277        if total_peers < self.config.min_peers {
278            return Err(UtxoCommitmentError::VerificationFailed(format!(
279                "Insufficient peers: got {}, need at least {}",
280                total_peers, self.config.min_peers
281            )));
282        }
283
284        // Group commitments by their values (merkle root + supply + count + height)
285        let mut commitment_groups: HashMap<(Hash, u64, u64, Natural), Vec<PeerCommitment>> =
286            HashMap::new();
287
288        for peer_commitment in peer_commitments {
289            let key = (
290                peer_commitment.commitment.merkle_root,
291                peer_commitment.commitment.total_supply,
292                peer_commitment.commitment.utxo_count,
293                peer_commitment.commitment.block_height,
294            );
295            commitment_groups
296                .entry(key)
297                .or_default()
298                .push(peer_commitment);
299        }
300
301        // Find group with highest agreement
302        type CommitmentKey = (Hash, u64, u64, Natural);
303        let mut best_group: Option<(&CommitmentKey, Vec<PeerCommitment>)> = None;
304        let mut best_agreement_count = 0;
305
306        for (key, group) in commitment_groups.iter() {
307            let agreement_count = group.len();
308
309            if agreement_count > best_agreement_count {
310                best_agreement_count = agreement_count;
311                best_group = Some((key, group.clone()));
312            }
313        }
314
315        // Check if we found any consensus group
316        let (_, group) = match best_group {
317            Some(g) => g,
318            None => {
319                return Err(UtxoCommitmentError::VerificationFailed(
320                    "No consensus groups found".to_string(),
321                ));
322            }
323        };
324
325        // Check if consensus threshold is met using integer arithmetic to avoid floating-point precision issues
326        // Threshold check: agreement_count / total_peers >= consensus_threshold
327        // Equivalent to: agreement_count >= (total_peers * consensus_threshold).ceil()
328        //
329        // Mathematical invariant: required_agreement_count must satisfy:
330        // - required_agreement_count >= ceil(total_peers * threshold)
331        // - required_agreement_count <= total_peers
332        // - If agreement_count >= required_agreement_count, then agreement_count/total_peers >= threshold
333        let required_agreement_count =
334            ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
335
336        // Runtime assertion: Verify mathematical invariants
337        debug_assert!(
338            required_agreement_count <= total_peers,
339            "Required agreement count ({required_agreement_count}) cannot exceed total peers ({total_peers})"
340        );
341        debug_assert!(
342            required_agreement_count >= 1,
343            "Required agreement count must be at least 1"
344        );
345        debug_assert!(
346            best_agreement_count <= total_peers,
347            "Best agreement count ({best_agreement_count}) cannot exceed total peers ({total_peers})"
348        );
349
350        if best_agreement_count < required_agreement_count {
351            let best_ratio = best_agreement_count as f64 / total_peers as f64;
352            return Err(UtxoCommitmentError::VerificationFailed(format!(
353                "No consensus: best agreement is {:.1}% ({} of {} peers), need {:.1}% (at least {} peers)",
354                best_ratio * 100.0,
355                best_agreement_count,
356                total_peers,
357                self.config.consensus_threshold * 100.0,
358                required_agreement_count
359            )));
360        }
361
362        // Return consensus result
363        let commitment = group[0].commitment.clone();
364        let agreement_count = group.len();
365        let agreement_ratio = agreement_count as f64 / total_peers as f64;
366
367        // Runtime assertion: Verify consensus result invariants
368        debug_assert!(
369            agreement_count >= required_agreement_count,
370            "Agreement count ({agreement_count}) must meet threshold ({required_agreement_count})"
371        );
372        debug_assert!(
373            agreement_ratio >= self.config.consensus_threshold,
374            "Agreement ratio ({:.4}) must be >= threshold ({:.4})",
375            agreement_ratio,
376            self.config.consensus_threshold
377        );
378        debug_assert!(
379            agreement_count <= total_peers,
380            "Agreement count ({agreement_count}) cannot exceed total peers ({total_peers})"
381        );
382        debug_assert!(
383            (0.0..=1.0).contains(&agreement_ratio),
384            "Agreement ratio ({agreement_ratio:.4}) must be in [0, 1]"
385        );
386
387        Ok(ConsensusResult {
388            commitment,
389            agreement_count,
390            total_peers,
391            agreement_ratio,
392        })
393    }
394
395    /// Verify consensus commitment against block headers
396    ///
397    /// Verifies that:
398    /// 1. Block header chain is valid (PoW verification)
399    /// 2. Commitment supply matches expected supply at height
400    /// 3. Commitment block hash matches actual block hash
401    #[spec_locked("11.4", "VerifyConsensusCommitment")]
402    #[blvm_spec_lock::ensures(header_chain.len() > 0 || result.is_err())]
403    pub fn verify_consensus_commitment(
404        &self,
405        consensus: &ConsensusResult,
406        header_chain: &[BlockHeader],
407    ) -> UtxoCommitmentResult<bool> {
408        // 1. Verify header chain (PoW)
409        verify_header_chain(header_chain)?;
410
411        // 2. Verify supply matches expected
412        verify_supply(&consensus.commitment)?;
413
414        // 3. Verify commitment block hash matches header chain
415        if consensus.commitment.block_height as usize >= header_chain.len() {
416            return Err(UtxoCommitmentError::VerificationFailed(format!(
417                "Commitment height {} exceeds header chain length {}",
418                consensus.commitment.block_height,
419                header_chain.len()
420            )));
421        }
422
423        let expected_header = &header_chain[consensus.commitment.block_height as usize];
424        let expected_hash = compute_block_hash(expected_header);
425
426        if consensus.commitment.block_hash != expected_hash {
427            return Err(UtxoCommitmentError::VerificationFailed(format!(
428                "Block hash mismatch: commitment has {:?}, header chain has {:?}",
429                consensus.commitment.block_hash, expected_hash
430            )));
431        }
432
433        Ok(true)
434    }
435
436    /// Verify UTXO proofs for critical UTXOs after consensus verification
437    ///
438    /// This function should be called after `verify_consensus_commitment()` succeeds
439    /// to cryptographically verify that specific UTXOs exist in the consensus commitment.
440    ///
441    /// This prevents coin freezing attacks where malicious peers provide commitments
442    /// with correct total supply but missing/modified individual UTXOs.
443    ///
444    /// # Arguments
445    /// * `consensus` - The verified consensus commitment
446    /// * `utxos_to_verify` - Vector of (outpoint, expected_utxo, proof) tuples to verify
447    ///
448    /// # Returns
449    /// `Ok(true)` if all proofs are valid, `Err` if any verification fails
450    ///
451    /// # Example
452    /// ```rust,no_run
453    /// use blvm_protocol::utxo_commitments::{UtxoMerkleTree, peer_consensus::PeerConsensus};
454    ///
455    /// // After verify_consensus_commitment succeeds:
456    /// let utxos_to_verify = vec![
457    ///     (outpoint1, utxo1, proof1),
458    ///     (outpoint2, utxo2, proof2),
459    /// ];
460    ///
461    /// peer_consensus.verify_utxo_proofs(&consensus, utxos_to_verify)?;
462    /// ```
463    #[spec_locked("13.4")]
464    pub fn verify_utxo_proofs(
465        &self,
466        consensus: &ConsensusResult,
467        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
468    ) -> UtxoCommitmentResult<bool> {
469        use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
470
471        // Sequential verification (baseline)
472        for (outpoint, expected_utxo, proof) in utxos_to_verify {
473            let is_valid = UtxoMerkleTree::verify_utxo_proof(
474                &consensus.commitment,
475                &outpoint,
476                &expected_utxo,
477                proof,
478            )?;
479
480            if !is_valid {
481                return Err(UtxoCommitmentError::VerificationFailed(format!(
482                    "UTXO proof verification failed for outpoint {outpoint:?} - possible attack"
483                )));
484            }
485        }
486
487        Ok(true)
488    }
489
490    /// Verify UTXO proofs in parallel (optimized for batch verification)
491    ///
492    /// This function performs parallel verification of multiple UTXO proofs,
493    /// which is more efficient when verifying many UTXOs at once.
494    ///
495    /// # Arguments
496    /// * `consensus` - The verified consensus commitment
497    /// * `utxos_to_verify` - Vector of (outpoint, expected_utxo, proof) tuples to verify
498    ///
499    /// # Returns
500    /// `Ok(true)` if all proofs are valid, `Err` if any verification fails
501    ///
502    /// # Performance
503    /// Uses parallel processing for better performance when verifying many proofs.
504    /// For small batches (< 10), sequential verification may be faster due to overhead.
505    #[cfg(feature = "parallel-verification")]
506    pub fn verify_utxo_proofs_parallel(
507        &self,
508        consensus: &ConsensusResult,
509        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
510    ) -> UtxoCommitmentResult<bool> {
511        use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
512        use rayon::prelude::*;
513
514        // Parallel verification using rayon
515        let results: Vec<UtxoCommitmentResult<bool>> = utxos_to_verify
516            .into_par_iter()
517            .map(|(outpoint, expected_utxo, proof)| {
518                UtxoMerkleTree::verify_utxo_proof(
519                    &consensus.commitment,
520                    &outpoint,
521                    &expected_utxo,
522                    proof,
523                )
524            })
525            .collect();
526
527        // Check all results
528        for (i, result) in results.iter().enumerate() {
529            match result {
530                Ok(true) => continue,
531                Ok(false) | Err(_) => {
532                    return Err(UtxoCommitmentError::VerificationFailed(format!(
533                        "UTXO proof verification failed at index {i} - possible attack"
534                    )));
535                }
536            }
537        }
538
539        Ok(true)
540    }
541
542    /// Verify UTXO proofs in parallel (fallback for when rayon is not available)
543    ///
544    /// Falls back to sequential verification if parallel feature is not enabled.
545    pub fn verify_utxo_proofs_parallel_fallback(
546        &self,
547        consensus: &ConsensusResult,
548        utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
549    ) -> UtxoCommitmentResult<bool> {
550        // Use sequential verification as fallback
551        self.verify_utxo_proofs(consensus, utxos_to_verify)
552    }
553}
554
555/// Compute block header hash (double SHA256)
556fn compute_block_hash(header: &BlockHeader) -> Hash {
557    use sha2::{Digest, Sha256};
558
559    // Serialize block header
560    let mut bytes = Vec::with_capacity(80);
561    bytes.extend_from_slice(&header.version.to_le_bytes());
562    bytes.extend_from_slice(&header.prev_block_hash);
563    bytes.extend_from_slice(&header.merkle_root);
564    bytes.extend_from_slice(&header.timestamp.to_le_bytes());
565    bytes.extend_from_slice(&header.bits.to_le_bytes());
566    bytes.extend_from_slice(&header.nonce.to_le_bytes());
567
568    // Double SHA256
569    let first_hash = Sha256::digest(&bytes);
570    let second_hash = Sha256::digest(first_hash);
571
572    let mut hash = [0u8; 32];
573    hash.copy_from_slice(&second_hash);
574    hash
575}
576
577// ============================================================================
578// FORMAL VERIFICATION
579// ============================================================================
580
581/// Mathematical Specification for Peer Consensus:
582/// ∀ peers ∈ [PeerInfo], commitments ∈ [UtxoCommitment], threshold ∈ [0,1]:
583/// - find_consensus(commitments, threshold) = consensus ⟺
584///     |{c ∈ commitments | c = consensus}| / |commitments| ≥ threshold
585/// - discover_diverse_peers(peers) ⊆ peers (no new peers created)
586/// - verify_consensus_commitment(consensus, headers) verifies PoW + supply
587///
588/// Invariants:
589/// - Consensus requires threshold percentage agreement
590/// - Diverse peer discovery filters for diversity
591#[doc(hidden)]
592const _PEER_CONSENSUS_SPEC: () = ();
593
594// End of module