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}
71
72#[derive(Debug, Clone)]
74pub struct ConsensusConfig {
75 pub min_peers: usize,
77 pub target_peers: usize,
79 pub consensus_threshold: f64,
81 pub max_peers_per_asn: usize,
83 pub safety_margin: Natural,
85 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, max_peers_per_asn: 2,
97 safety_margin: 2016, shuffle_peers: true,
99 }
100 }
101}
102
103pub struct PeerConsensus {
105 pub config: ConsensusConfig,
106}
107
108impl PeerConsensus {
109 pub fn new(config: ConsensusConfig) -> Self {
111 Self { config }
112 }
113
114 #[spec_locked("13.4")]
122 pub fn discover_diverse_peers(&self, all_peers: Vec<PeerInfo>) -> Vec<PeerInfo> {
123 use rand::seq::SliceRandom;
124
125 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 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; }
143 *asn_count += 1;
144 }
145
146 if seen_subnets.contains(&peer.subnet) {
148 continue; }
150 seen_subnets.insert(peer.subnet);
151
152 diverse_peers.push(peer);
154
155 if diverse_peers.len() >= self.config.target_peers {
157 break;
158 }
159 }
160
161 diverse_peers
162 }
163
164 #[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 let mut sorted_tips = peer_tips;
180 sorted_tips.sort();
181
182 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 let mid = sorted_tips.len() / 2;
191 let lower = sorted_tips[mid - 1];
192 let upper = sorted_tips[mid];
193
194 debug_assert!(
196 lower <= upper,
197 "Lower median value ({}) must be <= upper ({})",
198 lower,
199 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 ({}) must be between min ({}) and max ({})",
214 median_tip,
215 min_tip,
216 max_tip
217 );
218 }
219
220 if median_tip > self.config.safety_margin {
222 let checkpoint = median_tip - self.config.safety_margin;
223
224 debug_assert!(
226 checkpoint <= median_tip,
227 "Checkpoint ({}) must be <= median ({})",
228 checkpoint,
229 median_tip
230 );
231
232 checkpoint
233 } else {
234 0 }
236 }
237
238 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 continue;
267 }
268 }
269 }
270
271 results
272 }
273
274 #[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 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 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 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 let required_agreement_count =
340 ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
341
342 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 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 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 #[spec_locked("11.4", "VerifyConsensusCommitment")]
417 #[blvm_spec_lock::ensures(header_chain.len() > 0 || result.is_err())]
418 pub fn verify_consensus_commitment(
419 &self,
420 consensus: &ConsensusResult,
421 header_chain: &[BlockHeader],
422 ) -> UtxoCommitmentResult<bool> {
423 verify_header_chain(header_chain)?;
425
426 verify_supply(&consensus.commitment)?;
428
429 if consensus.commitment.block_height as usize >= header_chain.len() {
431 return Err(UtxoCommitmentError::VerificationFailed(format!(
432 "Commitment height {} exceeds header chain length {}",
433 consensus.commitment.block_height,
434 header_chain.len()
435 )));
436 }
437
438 let expected_header = &header_chain[consensus.commitment.block_height as usize];
439 let expected_hash = compute_block_hash(expected_header);
440
441 if consensus.commitment.block_hash != expected_hash {
442 return Err(UtxoCommitmentError::VerificationFailed(format!(
443 "Block hash mismatch: commitment has {:?}, header chain has {:?}",
444 consensus.commitment.block_hash, expected_hash
445 )));
446 }
447
448 Ok(true)
449 }
450
451 #[spec_locked("13.4")]
479 pub fn verify_utxo_proofs(
480 &self,
481 consensus: &ConsensusResult,
482 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
483 ) -> UtxoCommitmentResult<bool> {
484 use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
485
486 for (outpoint, expected_utxo, proof) in utxos_to_verify {
488 let is_valid = UtxoMerkleTree::verify_utxo_proof(
489 &consensus.commitment,
490 &outpoint,
491 &expected_utxo,
492 proof,
493 )?;
494
495 if !is_valid {
496 return Err(UtxoCommitmentError::VerificationFailed(format!(
497 "UTXO proof verification failed for outpoint {:?} - possible attack",
498 outpoint
499 )));
500 }
501 }
502
503 Ok(true)
504 }
505
506 #[cfg(feature = "parallel-verification")]
522 pub fn verify_utxo_proofs_parallel(
523 &self,
524 consensus: &ConsensusResult,
525 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
526 ) -> UtxoCommitmentResult<bool> {
527 use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
528 use rayon::prelude::*;
529
530 let results: Vec<UtxoCommitmentResult<bool>> = utxos_to_verify
532 .into_par_iter()
533 .map(|(outpoint, expected_utxo, proof)| {
534 UtxoMerkleTree::verify_utxo_proof(
535 &consensus.commitment,
536 &outpoint,
537 &expected_utxo,
538 proof,
539 )
540 })
541 .collect();
542
543 for (i, result) in results.iter().enumerate() {
545 match result {
546 Ok(true) => continue,
547 Ok(false) | Err(_) => {
548 return Err(UtxoCommitmentError::VerificationFailed(format!(
549 "UTXO proof verification failed at index {} - possible attack",
550 i
551 )));
552 }
553 }
554 }
555
556 Ok(true)
557 }
558
559 pub fn verify_utxo_proofs_parallel_fallback(
563 &self,
564 consensus: &ConsensusResult,
565 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
566 ) -> UtxoCommitmentResult<bool> {
567 self.verify_utxo_proofs(consensus, utxos_to_verify)
569 }
570}
571
572fn compute_block_hash(header: &BlockHeader) -> Hash {
574 use sha2::{Digest, Sha256};
575
576 let mut bytes = Vec::with_capacity(80);
578 bytes.extend_from_slice(&header.version.to_le_bytes());
579 bytes.extend_from_slice(&header.prev_block_hash);
580 bytes.extend_from_slice(&header.merkle_root);
581 bytes.extend_from_slice(&header.timestamp.to_le_bytes());
582 bytes.extend_from_slice(&header.bits.to_le_bytes());
583 bytes.extend_from_slice(&header.nonce.to_le_bytes());
584
585 let first_hash = Sha256::digest(&bytes);
587 let second_hash = Sha256::digest(&first_hash);
588
589 let mut hash = [0u8; 32];
590 hash.copy_from_slice(&second_hash);
591 hash
592}
593
594#[doc(hidden)]
609const _PEER_CONSENSUS_SPEC: () = ();
610
611