Skip to content

Commit

Permalink
Merge pull request AleoNet#2453 from demox-labs/update-credits
Browse files Browse the repository at this point in the history
ARC-0041: Update credits program to support delegation before validator bonding and native commission.
  • Loading branch information
alzger authored May 30, 2024
2 parents 9c3bd7f + 5547410 commit 74a4378
Show file tree
Hide file tree
Showing 37 changed files with 2,097 additions and 1,145 deletions.
2 changes: 1 addition & 1 deletion ledger/block/src/ratify/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub(crate) mod test_helpers {
let bonded_balances = committee
.members()
.iter()
.map(|(address, (amount, _))| (*address, (*address, *address, *amount)))
.map(|(address, (amount, _, _))| (*address, (*address, *address, *amount)))
.collect();

vec![
Expand Down
11 changes: 10 additions & 1 deletion ledger/block/src/transition/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,10 +317,19 @@ impl<N: Network> Transition<N> {
&& self.function_name.to_string() == "bond_public"
}

/// Returns `true` if this is a `bond_validator` transition.
#[inline]
pub fn is_bond_validator(&self) -> bool {
self.inputs.len() == 3
&& self.outputs.len() == 1
&& self.program_id.to_string() == "credits.aleo"
&& self.function_name.to_string() == "bond_validator"
}

/// Returns `true` if this is an `unbond_public` transition.
#[inline]
pub fn is_unbond_public(&self) -> bool {
self.inputs.len() == 1
self.inputs.len() == 2
&& self.outputs.len() == 1
&& self.program_id.to_string() == "credits.aleo"
&& self.function_name.to_string() == "unbond_public"
Expand Down
12 changes: 8 additions & 4 deletions ledger/committee/src/bytes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ impl<N: Network> FromBytes for Committee<N> {
)));
}

// Calculate the number of bytes per member. Each member is a (address, stake, is_open) tuple.
let member_byte_size = Address::<N>::size_in_bytes() + 8 + 1;
// Calculate the number of bytes per member. Each member is a (address, stake, is_open, commission) tuple.
let member_byte_size = Address::<N>::size_in_bytes() + 8 + 1 + 1;
// Read the member bytes.
let mut member_bytes = vec![0u8; num_members as usize * member_byte_size];
reader.read_exact(&mut member_bytes)?;
Expand All @@ -52,8 +52,10 @@ impl<N: Network> FromBytes for Committee<N> {
let stake = u64::read_le(&mut bytes)?;
// Read the is_open flag.
let is_open = bool::read_le(&mut bytes)?;
// Read the commission.
let commission = u8::read_le(&mut bytes)?;
// Insert the member and (stake, is_open).
Ok((member, (stake, is_open)))
Ok((member, (stake, is_open, commission)))
})
.collect::<Result<IndexMap<_, _>, std::io::Error>>()?;

Expand Down Expand Up @@ -85,13 +87,15 @@ impl<N: Network> ToBytes for Committee<N> {
// Write the number of members.
u16::try_from(self.members.len()).map_err(|e| error(e.to_string()))?.write_le(&mut writer)?;
// Write the members.
for (address, (stake, is_open)) in &self.members {
for (address, (stake, is_open, commission)) in &self.members {
// Write the address.
address.write_le(&mut writer)?;
// Write the stake.
stake.write_le(&mut writer)?;
// Write the is_open flag.
is_open.write_le(&mut writer)?;
// Write the commission.
commission.write_le(&mut writer)?;
}
// Write the total stake.
self.total_stake.write_le(&mut writer)
Expand Down
66 changes: 45 additions & 21 deletions ledger/committee/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ use std::collections::HashSet;
#[cfg(not(feature = "serial"))]
use rayon::prelude::*;

/// The minimum self bond for a validator to join the committee
pub const MIN_VALIDATOR_SELF_STAKE: u64 = 100_000_000u64; // microcredits
/// The minimum amount of stake required for a validator to bond.
pub const MIN_VALIDATOR_STAKE: u64 = 10_000_000_000_000u64; // microcredits
/// The minimum amount of stake required for a delegator to bond.
Expand All @@ -49,8 +51,8 @@ pub struct Committee<N: Network> {
id: Field<N>,
/// The starting round number for this committee.
starting_round: u64,
/// A map of `address` to `(stake, is_open)` state.
members: IndexMap<Address<N>, (u64, bool)>,
/// A map of `address` to `(stake, is_open, commission)` state.
members: IndexMap<Address<N>, (u64, bool, u8)>,
/// The total stake of all `members`.
total_stake: u64,
}
Expand All @@ -62,13 +64,13 @@ impl<N: Network> Committee<N> {
pub const MAX_COMMITTEE_SIZE: u16 = BatchHeader::<N>::MAX_CERTIFICATES;

/// Initializes a new `Committee` instance.
pub fn new_genesis(members: IndexMap<Address<N>, (u64, bool)>) -> Result<Self> {
pub fn new_genesis(members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
// Return the new committee.
Self::new(0u64, members)
}

/// Initializes a new `Committee` instance.
pub fn new(starting_round: u64, members: IndexMap<Address<N>, (u64, bool)>) -> Result<Self> {
pub fn new(starting_round: u64, members: IndexMap<Address<N>, (u64, bool, u8)>) -> Result<Self> {
// Ensure there are at least 3 members.
ensure!(members.len() >= 3, "Committee must have at least 3 members");
// Ensure there are no more than the maximum number of members.
Expand All @@ -79,9 +81,14 @@ impl<N: Network> Committee<N> {
);
// Ensure all members have the minimum required stake.
ensure!(
members.values().all(|(stake, _)| *stake >= MIN_VALIDATOR_STAKE),
members.values().all(|(stake, _, _)| *stake >= MIN_VALIDATOR_STAKE),
"All members must have at least {MIN_VALIDATOR_STAKE} microcredits in stake"
);
// Ensure all members have a commission percentage within 100%.
ensure!(
members.values().all(|(_, _, commission)| *commission <= 100),
"All members must have a commission percentage less than or equal to 100"
);
// Compute the total stake of the committee for this round.
let total_stake = Self::compute_total_stake(&members)?;
// Compute the committee ID.
Expand All @@ -105,7 +112,7 @@ impl<N: Network> Committee<N> {
}

/// Returns the committee members alongside their stake.
pub const fn members(&self) -> &IndexMap<Address<N>, (u64, bool)> {
pub const fn members(&self) -> &IndexMap<Address<N>, (u64, bool, u8)> {
&self.members
}

Expand Down Expand Up @@ -200,7 +207,7 @@ impl<N: Network> Committee<N> {
// Sort the committee members.
let candidates = self.sorted_members();
// Determine the leader of the previous round.
for (candidate, (stake, _)) in candidates {
for (candidate, (stake, _, _)) in candidates {
// Increment the current stake index by the candidate's stake.
current_stake_index = current_stake_index.saturating_add(stake);
// If the current stake index is greater than or equal to the stake index,
Expand All @@ -216,20 +223,20 @@ impl<N: Network> Committee<N> {

/// Returns the committee members sorted by their address' x-coordinate in decreasing order.
/// Note: This ensures the method returns a deterministic result that is SNARK-friendly.
fn sorted_members(&self) -> indexmap::map::IntoIter<Address<N>, (u64, bool)> {
fn sorted_members(&self) -> indexmap::map::IntoIter<Address<N>, (u64, bool, u8)> {
let members = self.members.clone();
// Note: The use of 'sorted_unstable_by' is safe here because the addresses are guaranteed to be unique.
members.sorted_unstable_by(|address1, (_, _), address2, (_, _)| {
members.sorted_unstable_by(|address1, (_, _, _), address2, (_, _, _)| {
address2.to_x_coordinate().cmp(&address1.to_x_coordinate())
})
}
}

impl<N: Network> Committee<N> {
/// Compute the total stake of the given members.
fn compute_total_stake(members: &IndexMap<Address<N>, (u64, bool)>) -> Result<u64> {
fn compute_total_stake(members: &IndexMap<Address<N>, (u64, bool, u8)>) -> Result<u64> {
let mut power = 0u64;
for (stake, _) in members.values() {
for (stake, _, _) in members.values() {
// Accumulate the stake, checking for overflow.
power = match power.checked_add(*stake) {
Some(power) => power,
Expand Down Expand Up @@ -263,6 +270,23 @@ pub mod test_helpers {
sample_committee_for_round(1, rng)
}

/// Samples a random committee with random commissions.
pub fn sample_committee_with_commissions(rng: &mut TestRng) -> Committee<CurrentNetwork> {
// Sample the members.
let mut members = IndexMap::new();
for index in 0..4 {
let is_open = rng.gen();
let commission = match index {
0 => 0,
1 => 100,
_ => rng.gen_range(0..=100),
};
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (2 * MIN_VALIDATOR_STAKE, is_open, commission));
}
// Return the committee.
Committee::<CurrentNetwork>::new(1, members).unwrap()
}

/// Samples a random committee for a given round.
pub fn sample_committee_for_round(round: u64, rng: &mut TestRng) -> Committee<CurrentNetwork> {
sample_committee_for_round_and_size(round, 4, rng)
Expand All @@ -278,7 +302,7 @@ pub mod test_helpers {
let mut members = IndexMap::new();
for _ in 0..num_members {
let is_open = rng.gen();
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (2 * MIN_VALIDATOR_STAKE, is_open));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (2 * MIN_VALIDATOR_STAKE, is_open, 0));
}
// Return the committee.
Committee::<CurrentNetwork>::new(round, members).unwrap()
Expand All @@ -294,7 +318,7 @@ pub mod test_helpers {
let mut committee_members = IndexMap::new();
for member in members {
let is_open = rng.gen();
committee_members.insert(member, (2 * MIN_VALIDATOR_STAKE, is_open));
committee_members.insert(member, (2 * MIN_VALIDATOR_STAKE, is_open, 0));
}
// Return the committee.
Committee::<CurrentNetwork>::new(round, committee_members).unwrap()
Expand All @@ -306,11 +330,11 @@ pub mod test_helpers {
// Sample the members.
let mut members = IndexMap::new();
// Add in the minimum and maximum staked nodes.
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MIN_VALIDATOR_STAKE, false));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MIN_VALIDATOR_STAKE, false, 0));
while members.len() < num_members as usize - 1 {
let stake = MIN_VALIDATOR_STAKE;
let is_open = rng.gen();
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (stake, is_open));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (stake, is_open, 0));
}
// Return the committee.
Committee::<CurrentNetwork>::new(1, members).unwrap()
Expand All @@ -329,18 +353,18 @@ pub mod test_helpers {
// Sample the members.
let mut members = IndexMap::new();
// Add in the minimum and maximum staked nodes.
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MIN_VALIDATOR_STAKE, false));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MIN_VALIDATOR_STAKE, false, 0));
while members.len() < num_members as usize - 1 {
loop {
let stake = MIN_VALIDATOR_STAKE as f64 + range * distribution.sample(rng);
if stake >= MIN_VALIDATOR_STAKE as f64 && stake <= MAX_STAKE as f64 {
let is_open = rng.gen();
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (stake as u64, is_open));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (stake as u64, is_open, 0));
break;
}
}
}
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MAX_STAKE, false));
members.insert(Address::<CurrentNetwork>::new(rng.gen()), (MAX_STAKE, false, 0));
// Return the committee.
Committee::<CurrentNetwork>::new(1, members).unwrap()
}
Expand Down Expand Up @@ -369,7 +393,7 @@ mod tests {
});
let leaders = leaders.read();
// Ensure the leader distribution is uniform.
for (i, (address, (stake, _))) in committee.members.iter().enumerate() {
for (i, (address, (stake, _, _))) in committee.members.iter().enumerate() {
// Get the leader count for the validator.
let Some(leader_count) = leaders.get(address) else {
println!("{i}: 0 rounds");
Expand Down Expand Up @@ -432,8 +456,8 @@ mod tests {
println!("sorted_members: {}ms", timer.elapsed().as_millis());
// Check that the members are sorted based on our sorting criteria.
for i in 0..sorted_members.len() - 1 {
let (address1, (_, _)) = sorted_members[i];
let (address2, (_, _)) = sorted_members[i + 1];
let (address1, (_, _, _)) = sorted_members[i];
let (address2, (_, _, _)) = sorted_members[i + 1];
assert!(address1.to_x_coordinate() > address2.to_x_coordinate());
}
}
Expand Down
17 changes: 9 additions & 8 deletions ledger/committee/src/prop_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct Validator {
pub address: Address<CurrentNetwork>,
pub stake: u64,
pub is_open: bool,
pub commission: u8,
}

impl Arbitrary for Validator {
Expand All @@ -63,7 +64,7 @@ impl Hash for Validator {
}

fn to_committee((round, ValidatorSet(validators)): (u64, ValidatorSet)) -> Result<Committee<CurrentNetwork>> {
Committee::new(round, validators.iter().map(|v| (v.address, (v.stake, v.is_open))).collect())
Committee::new(round, validators.iter().map(|v| (v.address, (v.stake, v.is_open, v.commission))).collect())
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -112,7 +113,7 @@ impl Default for ValidatorSet {
let rng = &mut rand_chacha::ChaChaRng::seed_from_u64(i);
let private_key = PrivateKey::new(rng).unwrap();
let address = Address::try_from(private_key).unwrap();
Validator { private_key, address, stake: MIN_VALIDATOR_STAKE, is_open: false }
Validator { private_key, address, stake: MIN_VALIDATOR_STAKE, is_open: false, commission: 0 }
})
.collect(),
)
Expand All @@ -130,10 +131,10 @@ impl Arbitrary for ValidatorSet {
}

pub fn any_valid_validator() -> BoxedStrategy<Validator> {
(MIN_VALIDATOR_STAKE..100_000_000_000_000, any_valid_private_key(), any::<bool>())
.prop_map(|(stake, private_key, is_open)| {
(MIN_VALIDATOR_STAKE..100_000_000_000_000, any_valid_private_key(), any::<bool>(), 0..100u8)
.prop_map(|(stake, private_key, is_open, commission)| {
let address = Address::try_from(private_key).unwrap();
Validator { private_key, address, stake, is_open }
Validator { private_key, address, stake, is_open, commission }
})
.boxed()
}
Expand All @@ -159,10 +160,10 @@ fn too_low_stake_committee() -> BoxedStrategy<Result<Committee<CurrentNetwork>>>

#[allow(dead_code)]
fn invalid_stake_validator() -> BoxedStrategy<Validator> {
(0..MIN_VALIDATOR_STAKE, any_valid_private_key(), any::<bool>())
.prop_map(|(stake, private_key, is_open)| {
(0..MIN_VALIDATOR_STAKE, any_valid_private_key(), any::<bool>(), 0..u8::MAX)
.prop_map(|(stake, private_key, is_open, commission)| {
let address = Address::try_from(private_key).unwrap();
Validator { private_key, address, stake, is_open }
Validator { private_key, address, stake, is_open, commission }
})
.boxed()
}
Expand Down
6 changes: 4 additions & 2 deletions ledger/committee/src/to_id.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ impl<N: Network> Committee<N> {
/// Returns the commmitee ID.
pub fn compute_committee_id(
starting_round: u64,
members: &IndexMap<Address<N>, (u64, bool)>,
members: &IndexMap<Address<N>, (u64, bool, u8)>,
total_stake: u64,
) -> Result<Field<N>> {
let mut preimage = Vec::new();
Expand All @@ -34,13 +34,15 @@ impl<N: Network> Committee<N> {
// Write the number of members.
u16::try_from(members.len())?.write_le(&mut preimage)?;
// Write the members.
for (address, (stake, is_open)) in members {
for (address, (stake, is_open, commission)) in members {
// Write the address.
address.write_le(&mut preimage)?;
// Write the stake.
stake.write_le(&mut preimage)?;
// Write the is_open flag.
is_open.write_le(&mut preimage)?;
// Write the commission.
commission.write_le(&mut preimage)?;
}
// Insert the total stake.
total_stake.write_le(&mut preimage)?;
Expand Down
Loading

0 comments on commit 74a4378

Please sign in to comment.