snarkvm_ledger_committee/
lib.rs

1// Copyright (c) 2019-2025 Provable Inc.
2// This file is part of the snarkVM library.
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at:
7
8// http://www.apache.org/licenses/LICENSE-2.0
9
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16#![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
42/// The minimum self bond for a validator to join the committee
43pub const MIN_VALIDATOR_SELF_STAKE: u64 = 100_000_000u64; // microcredits
44/// The minimum amount of stake required for a validator to bond.
45pub const MIN_VALIDATOR_STAKE: u64 = 10_000_000_000_000u64; // microcredits
46/// The minimum amount of stake required for a delegator to bond.
47pub const MIN_DELEGATOR_STAKE: u64 = 10_000_000_000u64; // microcredits
48/// The maximum number of delegators.
49pub const MAX_DELEGATORS: u32 = 100_000u32;
50
51#[derive(Clone, PartialEq, Eq)]
52pub struct Committee<N: Network> {
53    /// The committee ID, defined as the hash of the starting round, members, and total stake.
54    id: Field<N>,
55    /// The round number (of the block) at which this committee was created (always even).
56    starting_round: u64,
57    /// A map of `address` to `(stake, is_open, commission)` state.
58    members: IndexMap<Address<N>, (u64, bool, u8)>,
59    /// The total stake of all `members`.
60    total_stake: u64,
61}
62
63impl<N: Network> Committee<N> {
64    /// The committee lookback range.
65    pub const COMMITTEE_LOOKBACK_RANGE: u64 = BatchHeader::<N>::MAX_GC_ROUNDS as u64;
66
67    /// The maximum number of members that may be in a committee.
68    pub fn max_committee_size() -> Result<u16> {
69        N::LATEST_MAX_CERTIFICATES()
70    }
71
72    /// Initializes a new `Committee` instance.
73    pub fn new_genesis(members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
74        // Return the new committee.
75        Self::new(0u64, members)
76    }
77
78    /// Initializes a new `Committee` instance.
79    pub fn new(starting_round: u64, members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
80        // Ensure there are at least 3 members.
81        ensure!(members.len() >= 3, "Committee must have at least 3 members");
82        // Ensure there are no more than the maximum number of members.
83        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 all members have the minimum required stake.
89        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 all members have a commission percentage within 100%.
94        ensure!(
95            members.values().all(|(_, _, commission)| *commission <= 100),
96            "All members must have a commission percentage less than or equal to 100"
97        );
98        // Compute the total stake of the committee for this round.
99        let total_stake = Self::compute_total_stake(&members)?;
100        // Compute the committee ID.
101        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        // Return the new committee.
105        Ok(Self { id, starting_round, members, total_stake })
106    }
107}
108
109impl<N: Network> Committee<N> {
110    /// Returns the committee ID.
111    pub const fn id(&self) -> Field<N> {
112        self.id
113    }
114
115    /// Returns the round number (of the block) at which this committee was created (always even).
116    pub const fn starting_round(&self) -> u64 {
117        self.starting_round
118    }
119
120    /// Returns the committee members alongside their stake.
121    pub const fn members(&self) -> &IndexMap<Address<N>, (u64, bool, u8)> {
122        &self.members
123    }
124
125    /// Returns the number of validators in the committee.
126    pub fn num_members(&self) -> usize {
127        self.members.len()
128    }
129
130    /// Returns `true` if the given address is in the committee.
131    pub fn is_committee_member(&self, address: Address<N>) -> bool {
132        self.members.contains_key(&address)
133    }
134
135    /// Returns `true` if the given address is in the committee and is open.
136    pub fn is_committee_member_open(&self, address: Address<N>) -> bool {
137        self.members.get(&address).copied().unwrap_or_default().1
138    }
139
140    /// Returns the amount of stake for the given address.
141    pub fn get_stake(&self, address: Address<N>) -> u64 {
142        self.members.get(&address).copied().unwrap_or_default().0
143    }
144
145    /// Returns `true` if the combined stake for the given addresses reaches the availability threshold.
146    /// This method takes in a `HashSet` to guarantee that the given addresses are unique.
147    pub fn is_availability_threshold_reached(&self, addresses: &HashSet<Address<N>>) -> bool {
148        let mut stake = 0u64;
149        // Compute the combined stake for the given addresses.
150        for address in addresses {
151            // Accumulate the stake, checking for overflow.
152            stake = stake.saturating_add(self.get_stake(*address));
153        }
154        // Return whether the combined stake reaches the availability threshold.
155        stake >= self.availability_threshold()
156    }
157
158    /// Returns `true` if the combined stake for the given addresses reaches the quorum threshold.
159    /// This method takes in a `HashSet` to guarantee that the given addresses are unique.
160    pub fn is_quorum_threshold_reached(&self, addresses: &HashSet<Address<N>>) -> bool {
161        let mut stake = 0u64;
162        // Compute the combined stake for the given addresses.
163        for address in addresses {
164            // Accumulate the stake, checking for overflow.
165            stake = stake.saturating_add(self.get_stake(*address));
166        }
167        // Return whether the combined stake reaches the quorum threshold.
168        stake >= self.quorum_threshold()
169    }
170
171    /// Returns the amount of stake required to reach the availability threshold `(f + 1)`.
172    pub fn availability_threshold(&self) -> u64 {
173        // Assuming `N = 3f + 1 + k`, where `0 <= k < 3`,
174        // then `(N + 2) / 3 = f + 1 + k/3 = f + 1`.
175        self.total_stake().saturating_add(2).saturating_div(3)
176    }
177
178    /// Returns the amount of stake required to reach a quorum threshold `(N - f)`.
179    pub fn quorum_threshold(&self) -> u64 {
180        // Assuming `N = 3f + 1 + k`, where `0 <= k < 3`,
181        // then `2N/3 + 1 = 2f + 1 + (2k + 2)/3 = 2f + 1 + k = N - f`.
182        // In the line above, `/` means integer division.
183        self.total_stake().saturating_mul(2).saturating_div(3).saturating_add(1)
184    }
185
186    /// Returns the total amount of stake in the committee.
187    pub const fn total_stake(&self) -> u64 {
188        self.total_stake
189    }
190}
191
192impl<N: Network> Committee<N> {
193    /// Returns the leader address for the current round.
194    /// Note: This method returns a deterministic result that is SNARK-friendly.
195    pub fn get_leader(&self, current_round: u64) -> Result<Address<N>> {
196        // Ensure the current round is at least the starting round.
197        ensure!(current_round >= self.starting_round, "Current round must be at least the starting round");
198        // Retrieve the total stake of the committee.
199        let total_stake = self.total_stake();
200        // Construct the round seed.
201        let seed = [current_round].map(Field::from_u64);
202        // Hash the round seed.
203        let hash = Literal::Field(N::hash_to_group_psd4(&seed)?.to_x_coordinate());
204        // Compute the stake index from the hash output.
205        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        // Initialize a tracker for the leader.
211        let mut leader = None;
212        // Initialize a tracker for the current stake index.
213        let mut current_stake_index = 0u64;
214        // Sort the committee members.
215        let candidates = self.sorted_members();
216        // Determine the leader of the previous round.
217        for (candidate, (stake, _, _)) in candidates {
218            // Increment the current stake index by the candidate's stake.
219            current_stake_index = current_stake_index.saturating_add(stake);
220            // If the current stake index is greater than or equal to the stake index,
221            // set the leader to the candidate, and break.
222            if current_stake_index >= stake_index {
223                leader = Some(candidate);
224                break;
225            }
226        }
227        // Note: This is guaranteed to be safe.
228        Ok(leader.unwrap())
229    }
230
231    /// Returns the committee members sorted by their address' x-coordinate in decreasing order.
232    /// Note: This ensures the method returns a deterministic result that is SNARK-friendly.
233    fn sorted_members(&self) -> indexmap::map::IntoIter<Address<N>, (u64, bool, u8)> {
234        let members = self.members.clone();
235        // Note: The use of 'sorted_unstable_by' is safe here because the addresses are guaranteed to be unique.
236        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    /// Compute the total stake of the given members.
244    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            // Accumulate the stake, checking for overflow.
248            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    /// Samples a list of random committees.
271    pub fn sample_committees(rng: &mut TestRng) -> Vec<Committee<CurrentNetwork>> {
272        // Sample the number of committees.
273        let num_committees = rng.gen_range(10..=100);
274        // Sample the committees.
275        (0..num_committees).map(|_| sample_committee(rng)).collect()
276    }
277
278    /// Samples a random committee.
279    pub fn sample_committee(rng: &mut TestRng) -> Committee<CurrentNetwork> {
280        sample_committee_for_round(1, rng)
281    }
282
283    /// Samples a random committee with random commissions.
284    pub fn sample_committee_with_commissions(rng: &mut TestRng) -> Committee<CurrentNetwork> {
285        // Sample the members.
286        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        // Return the committee.
297        Committee::<CurrentNetwork>::new(1, members).unwrap()
298    }
299
300    /// Samples a random committee for a given round.
301    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    /// Samples a random committee for a given round and number of members.
306    pub fn sample_committee_for_round_and_size(
307        round: u64,
308        num_members: u16,
309        rng: &mut TestRng,
310    ) -> Committee<CurrentNetwork> {
311        // Sample the members.
312        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        // Return the committee.
318        Committee::<CurrentNetwork>::new(round, members).unwrap()
319    }
320
321    /// Generates a committee for the specified round, and private keys for its members.
322    ///
323    /// This is slower than `sample_committee_for_round` or `sample_committee_for_round_and_size` due to the additional key generation.
324    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        // Sample the members.
330        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        // Return the committee and keys.
340        (Committee::<CurrentNetwork>::new(round, members).unwrap(), private_keys)
341    }
342
343    /// Samples a random committee for a given round and members.
344    pub fn sample_committee_for_round_and_members(
345        round: u64,
346        members: Vec<Address<CurrentNetwork>>,
347        rng: &mut TestRng,
348    ) -> Committee<CurrentNetwork> {
349        // Sample the members.
350        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        // Return the committee.
356        Committee::<CurrentNetwork>::new(round, committee_members).unwrap()
357    }
358
359    /// Samples a committee where all validators have the same stake.
360    pub fn sample_committee_equal_stake_committee(num_members: u16, rng: &mut TestRng) -> Committee<CurrentNetwork> {
361        assert!(num_members >= 4);
362        // Sample the members.
363        let mut members = IndexMap::new();
364        // Add in the minimum and maximum staked nodes.
365        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        // Return the committee.
372        Committee::<CurrentNetwork>::new(1, members).unwrap()
373    }
374
375    /// Samples a random committee.
376    #[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        // Set the maximum amount staked in the node.
380        const MAX_STAKE: u64 = 100_000_000_000_000;
381        // Initialize the Exponential distribution.
382        let distribution = Exp::new(2.0).unwrap();
383        // Initialize maximum stake range.
384        let range = (MAX_STAKE - MIN_VALIDATOR_STAKE) as f64;
385        // Sample the members.
386        let mut members = IndexMap::new();
387        // Add in the minimum and maximum staked nodes.
388        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        // Return the committee.
401        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    /// Checks the leader distribution.
416    fn check_leader_distribution(committee: Committee<CurrentNetwork>, num_rounds: u64, tolerance_percent: f64) {
417        // Initialize a tracker for the leaders.
418        let leaders = Arc::new(RwLock::new(IndexMap::<Address<CurrentNetwork>, i64>::new()));
419        // Iterate through the rounds.
420        cfg_into_iter!(1..=num_rounds).for_each(|round| {
421            // Compute the leader.
422            let leader = committee.get_leader(round).unwrap();
423            // Increment the leader count for the current leader.
424            leaders.write().entry(leader).or_default().add_assign(1);
425        });
426        let leaders = leaders.read();
427        // Ensure the leader distribution is uniform.
428        for (i, (address, (stake, _, _))) in committee.members.iter().enumerate() {
429            // Get the leader count for the validator.
430            let Some(leader_count) = leaders.get(address) else {
431                println!("{i}: 0 rounds");
432                continue;
433            };
434            // Compute the target leader percentage.
435            let target_percent = *stake as f64 / committee.total_stake() as f64 * 100f64;
436            // Compute the actual leader percentage for the validator.
437            let leader_percent = (*leader_count as f64 / num_rounds as f64) * 100f64;
438            // Compute the error percentage from the target.
439            let error_percent = (leader_percent - target_percent) / target_percent * 100f64;
440
441            // Print the results.
442            let stake = stake / 1_000_000; // credits
443            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        // Initialize the RNG.
453        let rng = &mut TestRng::default();
454        // Set the number of rounds.
455        const NUM_ROUNDS: u64 = 256 * 100;
456        // Sample a committee.
457        let committee = crate::test_helpers::sample_committee(rng);
458        // Check the leader distribution.
459        check_leader_distribution(committee, NUM_ROUNDS, 2.5);
460    }
461
462    #[test]
463    fn test_get_leader_distribution() {
464        // Initialize the RNG.
465        let rng = &mut TestRng::default();
466        // Set the number of rounds.
467        const NUM_ROUNDS: u64 = 256 * 2_000;
468        // Sample the number of members.
469        let num_members = rng.gen_range(3..=Committee::<CurrentNetwork>::max_committee_size().unwrap());
470        // Sample a committee.
471        let committee = crate::test_helpers::sample_committee_custom(num_members, rng);
472        // Check the leader distribution.
473        check_leader_distribution(committee, NUM_ROUNDS, 5.0);
474    }
475
476    #[test]
477    fn test_sorted_members() {
478        // Initialize the RNG.
479        let rng = &mut TestRng::default();
480        // Sample a committee.
481        let committee = crate::test_helpers::sample_committee_custom(
482            Committee::<CurrentNetwork>::max_committee_size().unwrap(),
483            rng,
484        );
485
486        // Start a timer.
487        let timer = std::time::Instant::now();
488        // Sort the members.
489        let sorted_members = committee.sorted_members().collect::<Vec<_>>();
490        println!("sorted_members: {}ms", timer.elapsed().as_millis());
491        // Check that the members are sorted based on our sorting criteria.
492        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}