blvm_protocol/utxo_commitments/
peer_consensus.rs1#[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#[derive(Debug, Clone)]
28pub struct PeerInfo {
29 pub address: IpAddr,
30 pub asn: Option<u32>, pub country: Option<String>, pub implementation: Option<String>, pub subnet: u32, }
35
36impl PeerInfo {
37 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 let segments = ipv6.segments();
47 ((segments[0] as u32) << 16) | (segments[1] as u32)
48 }
49 }
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct PeerCommitment {
56 pub peer_info: PeerInfo,
57 pub commitment: UtxoCommitment,
58}
59
60#[derive(Debug, Clone)]
62pub struct ConsensusResult {
63 pub commitment: UtxoCommitment,
65 pub agreement_count: usize,
67 pub total_peers: usize,
68 pub agreement_ratio: f64,
70 pub proofs: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
72}
73
74#[derive(Debug, Clone)]
76pub struct ConsensusConfig {
77 pub min_peers: usize,
79 pub target_peers: usize,
81 pub consensus_threshold: f64,
83 pub max_peers_per_asn: usize,
85 pub safety_margin: Natural,
87 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, max_peers_per_asn: 2,
99 safety_margin: 2016, shuffle_peers: true,
101 }
102 }
103}
104
105pub struct PeerConsensus {
107 pub config: ConsensusConfig,
108}
109
110impl PeerConsensus {
111 pub fn new(config: ConsensusConfig) -> Self {
113 Self { config }
114 }
115
116 #[spec_locked("13.4")]
124 pub fn discover_diverse_peers(&self, all_peers: Vec<PeerInfo>) -> Vec<PeerInfo> {
125 use rand::seq::SliceRandom;
126
127 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 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; }
145 *asn_count += 1;
146 }
147
148 if seen_subnets.contains(&peer.subnet) {
150 continue; }
152 seen_subnets.insert(peer.subnet);
153
154 diverse_peers.push(peer);
156
157 if diverse_peers.len() >= self.config.target_peers {
159 break;
160 }
161 }
162
163 diverse_peers
164 }
165
166 #[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 let mut sorted_tips = peer_tips;
182 sorted_tips.sort();
183
184 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 let mid = sorted_tips.len() / 2;
193 let lower = sorted_tips[mid - 1];
194 let upper = sorted_tips[mid];
195
196 debug_assert!(
198 lower <= upper,
199 "Lower median value ({lower}) must be <= upper ({upper})"
200 );
201
202 (lower + upper) / 2
204 } else {
205 sorted_tips[sorted_tips.len() / 2]
207 };
208
209 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 if median_tip > self.config.safety_margin {
219 let checkpoint = median_tip - self.config.safety_margin;
220
221 debug_assert!(
223 checkpoint <= median_tip,
224 "Checkpoint ({checkpoint}) must be <= median ({median_tip})"
225 );
226
227 checkpoint
228 } else {
229 0 }
231 }
232
233 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 continue;
262 }
263 }
264 }
265
266 results
267 }
268
269 #[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 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 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 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 let required_agreement_count =
336 ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
337
338 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 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 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 #[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 verify_header_chain(header_chain)?;
413
414 verify_supply(&consensus.commitment)?;
416
417 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 #[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 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 #[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 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 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 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 self.verify_utxo_proofs(consensus, utxos_to_verify)
555 }
556}
557
558fn compute_block_hash(header: &BlockHeader) -> Hash {
560 use sha2::{Digest, Sha256};
561
562 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 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
580pub 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#[doc(hidden)]
609const _PEER_CONSENSUS_SPEC: () = ();
610
611