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")]
18#[cfg(feature = "spec-lock")]
19use blvm_spec_lock::spec_locked;
20#[cfg(feature = "utxo-commitments")]
21use sparse_merkle_tree::MerkleProof;
22#[cfg(feature = "utxo-commitments")]
23use std::collections::{HashMap, HashSet};
24#[cfg(feature = "utxo-commitments")]
25use std::net::IpAddr;
26
27#[derive(Debug, Clone)]
29pub struct PeerInfo {
30 pub address: IpAddr,
31 pub asn: Option<u32>, pub country: Option<String>, pub implementation: Option<String>, pub subnet: u32, }
36
37impl PeerInfo {
38 pub fn extract_subnet(ip: IpAddr) -> u32 {
40 match ip {
41 IpAddr::V4(ipv4) => {
42 let octets = ipv4.octets();
43 ((octets[0] as u32) << 24) | ((octets[1] as u32) << 16)
44 }
45 IpAddr::V6(ipv6) => {
46 let segments = ipv6.segments();
48 ((segments[0] as u32) << 16) | (segments[1] as u32)
49 }
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct PeerCommitment {
57 pub peer_info: PeerInfo,
58 pub commitment: UtxoCommitment,
59}
60
61#[derive(Debug, Clone)]
63pub struct ConsensusResult {
64 pub commitment: UtxoCommitment,
66 pub agreement_count: usize,
68 pub total_peers: usize,
69 pub agreement_ratio: f64,
71 pub proofs: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
73}
74
75#[derive(Debug, Clone)]
77pub struct ConsensusConfig {
78 pub min_peers: usize,
80 pub target_peers: usize,
82 pub consensus_threshold: f64,
84 pub max_peers_per_asn: usize,
86 pub safety_margin: Natural,
88 pub shuffle_peers: bool,
91}
92
93impl Default for ConsensusConfig {
94 fn default() -> Self {
95 Self {
96 min_peers: 5,
97 target_peers: 10,
98 consensus_threshold: 0.8, max_peers_per_asn: 2,
100 safety_margin: 2016, shuffle_peers: true,
102 }
103 }
104}
105
106pub struct PeerConsensus {
108 pub config: ConsensusConfig,
109}
110
111impl PeerConsensus {
112 pub fn new(config: ConsensusConfig) -> Self {
114 Self { config }
115 }
116
117 #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
125 pub fn discover_diverse_peers(&self, all_peers: Vec<PeerInfo>) -> Vec<PeerInfo> {
126 use rand::seq::SliceRandom;
127
128 let mut peers = all_peers;
130 if self.config.shuffle_peers {
131 peers.shuffle(&mut rand::thread_rng());
132 }
133
134 let mut diverse_peers = Vec::new();
135 let mut seen_asn: HashMap<u32, usize> = HashMap::new();
136 let mut seen_subnets: HashSet<u32> = HashSet::new();
137 let _seen_countries: HashSet<String> = HashSet::new();
138
139 for peer in peers {
140 if let Some(asn) = peer.asn {
142 let asn_count = seen_asn.entry(asn).or_insert(0);
143 if *asn_count >= self.config.max_peers_per_asn {
144 continue; }
146 *asn_count += 1;
147 }
148
149 if seen_subnets.contains(&peer.subnet) {
151 continue; }
153 seen_subnets.insert(peer.subnet);
154
155 diverse_peers.push(peer);
157
158 if diverse_peers.len() >= self.config.target_peers {
160 break;
161 }
162 }
163
164 diverse_peers
165 }
166
167 #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
176 pub fn determine_checkpoint_height(&self, peer_tips: Vec<Natural>) -> Natural {
177 if peer_tips.is_empty() {
178 return 0;
179 }
180
181 let mut sorted_tips = peer_tips;
183 sorted_tips.sort();
184
185 debug_assert!(
187 sorted_tips.windows(2).all(|w| w[0] <= w[1]),
188 "Tips must be sorted in ascending order"
189 );
190
191 let median_tip = if sorted_tips.len() % 2 == 0 {
192 let mid = sorted_tips.len() / 2;
194 let lower = sorted_tips[mid - 1];
195 let upper = sorted_tips[mid];
196
197 debug_assert!(
199 lower <= upper,
200 "Lower median value ({lower}) must be <= upper ({upper})"
201 );
202
203 (lower + upper) / 2
205 } else {
206 sorted_tips[sorted_tips.len() / 2]
208 };
209
210 if let (Some(&min_tip), Some(&max_tip)) = (sorted_tips.first(), sorted_tips.last()) {
212 debug_assert!(
213 median_tip >= min_tip && median_tip <= max_tip,
214 "Median ({median_tip}) must be between min ({min_tip}) and max ({max_tip})"
215 );
216 }
217
218 if median_tip > self.config.safety_margin {
220 let checkpoint = median_tip - self.config.safety_margin;
221
222 debug_assert!(
224 checkpoint <= median_tip,
225 "Checkpoint ({checkpoint}) must be <= median ({median_tip})"
226 );
227
228 checkpoint
229 } else {
230 0 }
232 }
233
234 pub async fn request_utxo_sets<C: UtxoCommitmentsNetworkClient>(
243 &self,
244 network_client: &C,
245 peers: &[(PeerInfo, String)],
246 checkpoint_height: Natural,
247 checkpoint_hash: Hash,
248 ) -> Vec<PeerCommitment> {
249 let mut results = Vec::new();
250
251 for (peer_info, peer_id) in peers {
252 match network_client
253 .request_utxo_set(peer_id, checkpoint_height, checkpoint_hash)
254 .await
255 {
256 Ok(commitment) => results.push(PeerCommitment {
257 peer_info: peer_info.clone(),
258 commitment,
259 }),
260 Err(_) => {
261 continue;
263 }
264 }
265 }
266
267 results
268 }
269
270 #[cfg_attr(feature = "spec-lock", spec_locked("11.4"))]
275 pub fn find_consensus(
276 &self,
277 peer_commitments: Vec<PeerCommitment>,
278 ) -> UtxoCommitmentResult<ConsensusResult> {
279 let total_peers = peer_commitments.len();
280 if total_peers < self.config.min_peers {
281 return Err(UtxoCommitmentError::VerificationFailed(format!(
282 "Insufficient peers: got {}, need at least {}",
283 total_peers, self.config.min_peers
284 )));
285 }
286
287 let mut commitment_groups: HashMap<(Hash, u64, u64, Natural), Vec<PeerCommitment>> =
289 HashMap::new();
290
291 for peer_commitment in peer_commitments {
292 let key = (
293 peer_commitment.commitment.merkle_root,
294 peer_commitment.commitment.total_supply,
295 peer_commitment.commitment.utxo_count,
296 peer_commitment.commitment.block_height,
297 );
298 commitment_groups
299 .entry(key)
300 .or_default()
301 .push(peer_commitment);
302 }
303
304 type CommitmentKey = (Hash, u64, u64, Natural);
306 let mut best_group: Option<(&CommitmentKey, Vec<PeerCommitment>)> = None;
307 let mut best_agreement_count = 0;
308
309 for (key, group) in commitment_groups.iter() {
310 let agreement_count = group.len();
311
312 if agreement_count > best_agreement_count {
313 best_agreement_count = agreement_count;
314 best_group = Some((key, group.clone()));
315 }
316 }
317
318 let (_, group) = match best_group {
320 Some(g) => g,
321 None => {
322 return Err(UtxoCommitmentError::VerificationFailed(
323 "No consensus groups found".to_string(),
324 ));
325 }
326 };
327
328 let required_agreement_count =
337 ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
338
339 debug_assert!(
341 required_agreement_count <= total_peers,
342 "Required agreement count ({required_agreement_count}) cannot exceed total peers ({total_peers})"
343 );
344 debug_assert!(
345 required_agreement_count >= 1,
346 "Required agreement count must be at least 1"
347 );
348 debug_assert!(
349 best_agreement_count <= total_peers,
350 "Best agreement count ({best_agreement_count}) cannot exceed total peers ({total_peers})"
351 );
352
353 if best_agreement_count < required_agreement_count {
354 let best_ratio = best_agreement_count as f64 / total_peers as f64;
355 return Err(UtxoCommitmentError::VerificationFailed(format!(
356 "No consensus: best agreement is {:.1}% ({} of {} peers), need {:.1}% (at least {} peers)",
357 best_ratio * 100.0,
358 best_agreement_count,
359 total_peers,
360 self.config.consensus_threshold * 100.0,
361 required_agreement_count
362 )));
363 }
364
365 let commitment = group[0].commitment.clone();
367 let agreement_count = group.len();
368 let agreement_ratio = agreement_count as f64 / total_peers as f64;
369
370 debug_assert!(
372 agreement_count >= required_agreement_count,
373 "Agreement count ({agreement_count}) must meet threshold ({required_agreement_count})"
374 );
375 debug_assert!(
376 agreement_ratio >= self.config.consensus_threshold,
377 "Agreement ratio ({:.4}) must be >= threshold ({:.4})",
378 agreement_ratio,
379 self.config.consensus_threshold
380 );
381 debug_assert!(
382 agreement_count <= total_peers,
383 "Agreement count ({agreement_count}) cannot exceed total peers ({total_peers})"
384 );
385 debug_assert!(
386 (0.0..=1.0).contains(&agreement_ratio),
387 "Agreement ratio ({agreement_ratio:.4}) must be in [0, 1]"
388 );
389
390 Ok(ConsensusResult {
391 commitment,
392 agreement_count,
393 total_peers,
394 agreement_ratio,
395 proofs: Vec::new(),
396 })
397 }
398
399 #[cfg_attr(
406 feature = "spec-lock",
407 spec_locked("11.4", "VerifyConsensusCommitment")
408 )]
409 #[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(header_chain.len() > 0 || result.is_err()))]
410 pub fn verify_consensus_commitment(
411 &self,
412 consensus: &ConsensusResult,
413 header_chain: &[BlockHeader],
414 ) -> UtxoCommitmentResult<bool> {
415 verify_header_chain(header_chain)?;
417
418 verify_supply(&consensus.commitment)?;
420
421 if consensus.commitment.block_height as usize >= header_chain.len() {
423 return Err(UtxoCommitmentError::VerificationFailed(format!(
424 "Commitment height {} exceeds header chain length {}",
425 consensus.commitment.block_height,
426 header_chain.len()
427 )));
428 }
429
430 let expected_header = &header_chain[consensus.commitment.block_height as usize];
431 let expected_hash = compute_block_hash(expected_header);
432
433 if consensus.commitment.block_hash != expected_hash {
434 return Err(UtxoCommitmentError::VerificationFailed(format!(
435 "Block hash mismatch: commitment has {:?}, header chain has {:?}",
436 consensus.commitment.block_hash, expected_hash
437 )));
438 }
439
440 Ok(true)
441 }
442
443 #[cfg_attr(feature = "spec-lock", spec_locked("13.4"))]
471 pub fn verify_utxo_proofs(
472 &self,
473 consensus: &ConsensusResult,
474 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
475 ) -> UtxoCommitmentResult<bool> {
476 use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
477
478 for (outpoint, expected_utxo, proof) in utxos_to_verify {
480 let is_valid = UtxoMerkleTree::verify_utxo_proof(
481 &consensus.commitment,
482 &outpoint,
483 &expected_utxo,
484 proof,
485 )?;
486
487 if !is_valid {
488 return Err(UtxoCommitmentError::VerificationFailed(format!(
489 "UTXO proof verification failed for outpoint {outpoint:?} - possible attack"
490 )));
491 }
492 }
493
494 Ok(true)
495 }
496
497 #[cfg(feature = "parallel-verification")]
513 pub fn verify_utxo_proofs_parallel(
514 &self,
515 consensus: &ConsensusResult,
516 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
517 ) -> UtxoCommitmentResult<bool> {
518 use crate::utxo_commitments::merkle_tree::UtxoMerkleTree;
519 use rayon::prelude::*;
520
521 let results: Vec<UtxoCommitmentResult<bool>> = utxos_to_verify
523 .into_par_iter()
524 .map(|(outpoint, expected_utxo, proof)| {
525 UtxoMerkleTree::verify_utxo_proof(
526 &consensus.commitment,
527 &outpoint,
528 &expected_utxo,
529 proof,
530 )
531 })
532 .collect();
533
534 for (i, result) in results.iter().enumerate() {
536 match result {
537 Ok(true) => continue,
538 Ok(false) | Err(_) => {
539 return Err(UtxoCommitmentError::VerificationFailed(format!(
540 "UTXO proof verification failed at index {i} - possible attack"
541 )));
542 }
543 }
544 }
545
546 Ok(true)
547 }
548
549 pub fn verify_utxo_proofs_parallel_fallback(
553 &self,
554 consensus: &ConsensusResult,
555 utxos_to_verify: Vec<(crate::types::OutPoint, crate::types::UTXO, MerkleProof)>,
556 ) -> UtxoCommitmentResult<bool> {
557 self.verify_utxo_proofs(consensus, utxos_to_verify)
559 }
560}
561
562fn compute_block_hash(header: &BlockHeader) -> Hash {
564 use sha2::{Digest, Sha256};
565
566 let mut bytes = Vec::with_capacity(80);
568 bytes.extend_from_slice(&header.version.to_le_bytes());
569 bytes.extend_from_slice(&header.prev_block_hash);
570 bytes.extend_from_slice(&header.merkle_root);
571 bytes.extend_from_slice(&header.timestamp.to_le_bytes());
572 bytes.extend_from_slice(&header.bits.to_le_bytes());
573 bytes.extend_from_slice(&header.nonce.to_le_bytes());
574
575 let first_hash = Sha256::digest(&bytes);
577 let second_hash = Sha256::digest(first_hash);
578
579 let mut hash = [0u8; 32];
580 hash.copy_from_slice(&second_hash);
581 hash
582}
583
584pub fn accept_commitment_proofs(
586 consensus: &ConsensusResult,
587) -> UtxoCommitmentResult<UtxoCommitment> {
588 if consensus.proofs.is_empty() {
589 return Err(UtxoCommitmentError::VerificationFailed(
590 "per-UTXO proof is required".into(),
591 ));
592 }
593 let checker = PeerConsensus::new(ConsensusConfig::default());
594 checker.verify_utxo_proofs(consensus, consensus.proofs.clone())?;
595 Ok(consensus.commitment.clone())
596}
597
598#[doc(hidden)]
613const _PEER_CONSENSUS_SPEC: () = ();
614
615