snarkvm_ledger_committee/
lib.rs1#![forbid(unsafe_code)]
17#![warn(clippy::cast_possible_truncation)]
18
19extern crate snarkvm_console as console;
20
21mod bytes;
22mod serialize;
23mod string;
24mod to_id;
25
26#[cfg(any(test, feature = "prop-tests"))]
27pub mod prop_tests;
28
29use console::{
30 prelude::*,
31 program::{Literal, LiteralType},
32 types::{Address, Field},
33};
34use snarkvm_ledger_narwhal_batch_header::BatchHeader;
35
36use indexmap::IndexMap;
37use std::collections::HashSet;
38
39#[cfg(not(feature = "serial"))]
40use rayon::prelude::*;
41
42pub const MIN_VALIDATOR_SELF_STAKE: u64 = 100_000_000u64; pub const MIN_VALIDATOR_STAKE: u64 = 10_000_000_000_000u64; pub const MIN_DELEGATOR_STAKE: u64 = 10_000_000_000u64; pub const MAX_DELEGATORS: u32 = 100_000u32;
50
51#[derive(Clone, PartialEq, Eq)]
52pub struct Committee<N: Network> {
53 id: Field<N>,
55 starting_round: u64,
57 members: IndexMap<Address<N>, (u64, bool, u8)>,
59 total_stake: u64,
61}
62
63impl<N: Network> Committee<N> {
64 pub const COMMITTEE_LOOKBACK_RANGE: u64 = BatchHeader::<N>::MAX_GC_ROUNDS as u64;
66
67 pub fn max_committee_size() -> Result<u16> {
69 N::LATEST_MAX_CERTIFICATES()
70 }
71
72 pub fn new_genesis(members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
74 Self::new(0u64, members)
76 }
77
78 pub fn new(starting_round: u64, members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
80 ensure!(members.len() >= 3, "Committee must have at least 3 members");
82 ensure!(
84 members.len() <= Self::max_committee_size()? as usize,
85 "Committee must have no more than {} members",
86 Self::max_committee_size()?
87 );
88 ensure!(
90 members.values().all(|(stake, _, _)| *stake >= MIN_VALIDATOR_STAKE),
91 "All members must have at least {MIN_VALIDATOR_STAKE} microcredits in stake"
92 );
93 ensure!(
95 members.values().all(|(_, _, commission)| *commission <= 100),
96 "All members must have a commission percentage less than or equal to 100"
97 );
98 let total_stake = Self::compute_total_stake(&members)?;
100 let id = Self::compute_committee_id(starting_round, &members, total_stake)?;
102 #[cfg(feature = "metrics")]
103 snarkvm_metrics::gauge(snarkvm_metrics::committee::TOTAL_STAKE, total_stake as f64);
104 Ok(Self { id, starting_round, members, total_stake })
106 }
107}
108
109impl<N: Network> Committee<N> {
110 pub const fn id(&self) -> Field<N> {
112 self.id
113 }
114
115 pub const fn starting_round(&self) -> u64 {
117 self.starting_round
118 }
119
120 pub const fn members(&self) -> &IndexMap<Address<N>, (u64, bool, u8)> {
122 &self.members
123 }
124
125 pub fn num_members(&self) -> usize {
127 self.members.len()
128 }
129
130 pub fn is_committee_member(&self, address: Address<N>) -> bool {
132 self.members.contains_key(&address)
133 }
134
135 pub fn is_committee_member_open(&self, address: Address<N>) -> bool {
137 self.members.get(&address).copied().unwrap_or_default().1
138 }
139
140 pub fn get_stake(&self, address: Address<N>) -> u64 {
142 self.members.get(&address).copied().unwrap_or_default().0
143 }
144
145 pub fn is_availability_threshold_reached(&self, addresses: &HashSet<Address<N>>) -> bool {
148 let mut stake = 0u64;
149 for address in addresses {
151 stake = stake.saturating_add(self.get_stake(*address));
153 }
154 stake >= self.availability_threshold()
156 }
157
158 pub fn is_quorum_threshold_reached(&self, addresses: &HashSet<Address<N>>) -> bool {
161 let mut stake = 0u64;
162 for address in addresses {
164 stake = stake.saturating_add(self.get_stake(*address));
166 }
167 stake >= self.quorum_threshold()
169 }
170
171 pub fn availability_threshold(&self) -> u64 {
173 self.total_stake().saturating_add(2).saturating_div(3)
176 }
177
178 pub fn quorum_threshold(&self) -> u64 {
180 self.total_stake().saturating_mul(2).saturating_div(3).saturating_add(1)
184 }
185
186 pub const fn total_stake(&self) -> u64 {
188 self.total_stake
189 }
190}
191
192impl<N: Network> Committee<N> {
193 pub fn get_leader(&self, current_round: u64) -> Result<Address<N>> {
196 ensure!(current_round >= self.starting_round, "Current round must be at least the starting round");
198 let total_stake = self.total_stake();
200 let seed = [current_round].map(Field::from_u64);
202 let hash = Literal::Field(N::hash_to_group_psd4(&seed)?.to_x_coordinate());
204 let stake_index = match hash.cast_lossy(LiteralType::U64)? {
206 Literal::U64(output) => (*output) % total_stake,
207 _ => bail!("BFT failed to downcast the hash output to a U64 literal"),
208 };
209
210 let mut leader = None;
212 let mut current_stake_index = 0u64;
214 let candidates = self.sorted_members();
216 for (candidate, (stake, _, _)) in candidates {
218 current_stake_index = current_stake_index.saturating_add(stake);
220 if current_stake_index >= stake_index {
223 leader = Some(candidate);
224 break;
225 }
226 }
227 Ok(leader.unwrap())
229 }
230
231 fn sorted_members(&self) -> indexmap::map::IntoIter<Address<N>, (u64, bool, u8)> {
234 let members = self.members.clone();
235 members.sorted_unstable_by(|address1, (_, _, _), address2, (_, _, _)| {
237 address2.to_x_coordinate().cmp(&address1.to_x_coordinate())
238 })
239 }
240}
241
242impl<N: Network> Committee<N> {
243 fn compute_total_stake(members: &IndexMap<Address<N>, (u64, bool, u8)>) -> Result<u64> {
245 let mut power = 0u64;
246 for (stake, _, _) in members.values() {
247 power = match power.checked_add(*stake) {
249 Some(power) => power,
250 None => bail!("Failed to calculate total stake - overflow detected"),
251 };
252 }
253 Ok(power)
254 }
255}
256
257#[cfg(any(test, feature = "test-helpers"))]
258pub mod test_helpers {
259 use super::*;
260 use console::{
261 account::{Address, PrivateKey},
262 prelude::TestRng,
263 };
264
265 use indexmap::IndexMap;
266 use rand_distr::{Distribution, Exp};
267
268 type CurrentNetwork = console::network::MainnetV0;
269
270 pub fn sample_committees(rng: &mut TestRng) -> Vec<Committee<CurrentNetwork>> {
272 let num_committees = rng.gen_range(10..=100);
274 (0..num_committees).map(|_| sample_committee(rng)).collect()
276 }
277
278 pub fn sample_committee(rng: &mut TestRng) -> Committee<CurrentNetwork> {
280 sample_committee_for_round(1, rng)
281 }
282
283 pub fn sample_committee_with_commissions(rng: &mut TestRng) -> Committee<CurrentNetwork> {
285 let mut members = IndexMap::new();
287 for index in 0..4 {
288 let is_open = rng.r#gen();
289 let commission = match index {
290 0 => 0,
291 1 => 100,
292 _ => rng.gen_range(0..=100),
293 };
294 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (2 * MIN_VALIDATOR_STAKE, is_open, commission));
295 }
296 Committee::<CurrentNetwork>::new(1, members).unwrap()
298 }
299
300 pub fn sample_committee_for_round(round: u64, rng: &mut TestRng) -> Committee<CurrentNetwork> {
302 sample_committee_for_round_and_size(round, 4, rng)
303 }
304
305 pub fn sample_committee_for_round_and_size(
307 round: u64,
308 num_members: u16,
309 rng: &mut TestRng,
310 ) -> Committee<CurrentNetwork> {
311 let mut members = IndexMap::new();
313 for _ in 0..num_members {
314 let is_open = rng.r#gen();
315 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (2 * MIN_VALIDATOR_STAKE, is_open, 0));
316 }
317 Committee::<CurrentNetwork>::new(round, members).unwrap()
319 }
320
321 pub fn sample_committee_and_keys_for_round(
325 round: u64,
326 num_members: usize,
327 rng: &mut TestRng,
328 ) -> (Committee<CurrentNetwork>, Vec<PrivateKey<CurrentNetwork>>) {
329 let mut members = IndexMap::with_capacity(num_members);
331 let mut private_keys = Vec::with_capacity(num_members);
332 for _ in 0..num_members {
333 let private_key = PrivateKey::new(rng).unwrap();
334 let address = Address::try_from(private_key).unwrap();
335 let is_open = rng.r#gen();
336 private_keys.push(private_key);
337 members.insert(address, (2 * MIN_VALIDATOR_STAKE, is_open, 0));
338 }
339 (Committee::<CurrentNetwork>::new(round, members).unwrap(), private_keys)
341 }
342
343 pub fn sample_committee_for_round_and_members(
345 round: u64,
346 members: Vec<Address<CurrentNetwork>>,
347 rng: &mut TestRng,
348 ) -> Committee<CurrentNetwork> {
349 let mut committee_members = IndexMap::new();
351 for member in members {
352 let is_open = rng.r#gen();
353 committee_members.insert(member, (2 * MIN_VALIDATOR_STAKE, is_open, 0));
354 }
355 Committee::<CurrentNetwork>::new(round, committee_members).unwrap()
357 }
358
359 pub fn sample_committee_equal_stake_committee(num_members: u16, rng: &mut TestRng) -> Committee<CurrentNetwork> {
361 assert!(num_members >= 4);
362 let mut members = IndexMap::new();
364 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (MIN_VALIDATOR_STAKE, false, 0));
366 while members.len() < num_members as usize - 1 {
367 let stake = MIN_VALIDATOR_STAKE;
368 let is_open = rng.r#gen();
369 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (stake, is_open, 0));
370 }
371 Committee::<CurrentNetwork>::new(1, members).unwrap()
373 }
374
375 #[allow(clippy::cast_possible_truncation)]
377 pub fn sample_committee_custom(num_members: u16, rng: &mut TestRng) -> Committee<CurrentNetwork> {
378 assert!(num_members >= 3);
379 const MAX_STAKE: u64 = 100_000_000_000_000;
381 let distribution = Exp::new(2.0).unwrap();
383 let range = (MAX_STAKE - MIN_VALIDATOR_STAKE) as f64;
385 let mut members = IndexMap::new();
387 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (MIN_VALIDATOR_STAKE, false, 0));
389 while members.len() < num_members as usize - 1 {
390 loop {
391 let stake = MIN_VALIDATOR_STAKE as f64 + range * distribution.sample(rng);
392 if stake >= MIN_VALIDATOR_STAKE as f64 && stake <= MAX_STAKE as f64 {
393 let is_open = rng.r#gen();
394 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (stake as u64, is_open, 0));
395 break;
396 }
397 }
398 }
399 members.insert(Address::<CurrentNetwork>::new(rng.r#gen()), (MAX_STAKE, false, 0));
400 Committee::<CurrentNetwork>::new(1, members).unwrap()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::*;
408 use console::prelude::TestRng;
409
410 use parking_lot::RwLock;
411 use std::sync::Arc;
412
413 type CurrentNetwork = console::network::MainnetV0;
414
415 fn check_leader_distribution(committee: Committee<CurrentNetwork>, num_rounds: u64, tolerance_percent: f64) {
417 let leaders = Arc::new(RwLock::new(IndexMap::<Address<CurrentNetwork>, i64>::new()));
419 cfg_into_iter!(1..=num_rounds).for_each(|round| {
421 let leader = committee.get_leader(round).unwrap();
423 leaders.write().entry(leader).or_default().add_assign(1);
425 });
426 let leaders = leaders.read();
427 for (i, (address, (stake, _, _))) in committee.members.iter().enumerate() {
429 let Some(leader_count) = leaders.get(address) else {
431 println!("{i}: 0 rounds");
432 continue;
433 };
434 let target_percent = *stake as f64 / committee.total_stake() as f64 * 100f64;
436 let leader_percent = (*leader_count as f64 / num_rounds as f64) * 100f64;
438 let error_percent = (leader_percent - target_percent) / target_percent * 100f64;
440
441 let stake = stake / 1_000_000; println!("{i}: {stake}, {leader_count}, {target_percent:.3}%, {leader_percent:.3}%, {error_percent:.2}%");
444 if target_percent > 0.5 {
445 assert!(error_percent.abs() < tolerance_percent);
446 }
447 }
448 }
449
450 #[test]
451 fn test_get_leader_distribution_simple() {
452 let rng = &mut TestRng::default();
454 const NUM_ROUNDS: u64 = 256 * 100;
456 let committee = crate::test_helpers::sample_committee(rng);
458 check_leader_distribution(committee, NUM_ROUNDS, 2.5);
460 }
461
462 #[test]
463 fn test_get_leader_distribution() {
464 let rng = &mut TestRng::default();
466 const NUM_ROUNDS: u64 = 256 * 2_000;
468 let num_members = rng.gen_range(3..=Committee::<CurrentNetwork>::max_committee_size().unwrap());
470 let committee = crate::test_helpers::sample_committee_custom(num_members, rng);
472 check_leader_distribution(committee, NUM_ROUNDS, 5.0);
474 }
475
476 #[test]
477 fn test_sorted_members() {
478 let rng = &mut TestRng::default();
480 let committee = crate::test_helpers::sample_committee_custom(
482 Committee::<CurrentNetwork>::max_committee_size().unwrap(),
483 rng,
484 );
485
486 let timer = std::time::Instant::now();
488 let sorted_members = committee.sorted_members().collect::<Vec<_>>();
490 println!("sorted_members: {}ms", timer.elapsed().as_millis());
491 for i in 0..sorted_members.len() - 1 {
493 let (address1, (_, _, _)) = sorted_members[i];
494 let (address2, (_, _, _)) = sorted_members[i + 1];
495 assert!(address1.to_x_coordinate() > address2.to_x_coordinate());
496 }
497 }
498}