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 ({lower}) must be <= upper ({upper})"
198 );
199
200 (lower + upper) / 2
202 } else {
203 sorted_tips[sorted_tips.len() / 2]
205 };
206
207 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 if median_tip > self.config.safety_margin {
217 let checkpoint = median_tip - self.config.safety_margin;
218
219 debug_assert!(
221 checkpoint <= median_tip,
222 "Checkpoint ({checkpoint}) must be <= median ({median_tip})"
223 );
224
225 checkpoint
226 } else {
227 0 }
229 }
230
231 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 continue;
260 }
261 }
262 }
263
264 results
265 }
266
267 #[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 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 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 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 let required_agreement_count =
334 ((total_peers as f64) * self.config.consensus_threshold).ceil() as usize;
335
336 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 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 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 #[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 verify_header_chain(header_chain)?;
410
411 verify_supply(&consensus.commitment)?;
413
414 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 #[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 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 #[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 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 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 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 self.verify_utxo_proofs(consensus, utxos_to_verify)
552 }
553}
554
555fn compute_block_hash(header: &BlockHeader) -> Hash {
557 use sha2::{Digest, Sha256};
558
559 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 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#[doc(hidden)]
592const _PEER_CONSENSUS_SPEC: () = ();
593
594