snarkvm_synthesizer/vm/helpers/
rewards.rsuse console::{account::Address, network::prelude::*};
use ledger_committee::{Committee, MIN_DELEGATOR_STAKE};
use indexmap::IndexMap;
#[cfg(not(feature = "serial"))]
use rayon::prelude::*;
const MAX_COINBASE_REWARD: u64 = ledger_block::MAX_COINBASE_REWARD; pub fn staking_rewards<N: Network>(
stakers: &IndexMap<Address<N>, (Address<N>, u64)>,
committee: &Committee<N>,
block_reward: u64,
) -> IndexMap<Address<N>, (Address<N>, u64)> {
if stakers.is_empty() || committee.total_stake() == 0 || block_reward == 0 {
return stakers.clone();
}
cfg_iter!(stakers)
.map(|(staker, (validator, stake))| {
let Some((validator_stake, _is_open, commission_rate)) = committee.members().get(validator) else {
trace!("Validator {validator} is not in the committee - skipping {staker}");
return (*staker, (*validator, *stake));
};
if *commission_rate > 100 {
error!("Commission rate ({commission_rate}) is greater than 100 - skipping {staker}");
return (*staker, (*validator, *stake));
}
if *validator_stake > committee.total_stake().saturating_div(4) {
trace!("Validator {validator} has more than 25% of the total stake - skipping {staker}");
return (*staker, (*validator, *stake));
}
if *stake < MIN_DELEGATOR_STAKE && *staker != *validator {
trace!("Staker has less than {MIN_DELEGATOR_STAKE} microcredits - skipping {staker}");
return (*staker, (*validator, *stake));
}
let numerator = (block_reward as u128).saturating_mul(*stake as u128);
let denominator = committee.total_stake() as u128;
let quotient = numerator.saturating_div(denominator);
if quotient > MAX_COINBASE_REWARD as u128 {
error!("Staking reward ({quotient}) is too large - skipping {staker}");
return (*staker, (*validator, *stake));
}
let staking_reward = u64::try_from(quotient).expect("Staking reward is too large");
let staking_reward_after_commission = match staker == validator {
true => {
let total_delegated_stake = validator_stake.saturating_sub(*stake);
let numerator = (block_reward as u128).saturating_mul(total_delegated_stake as u128);
let quotient = numerator.saturating_div(denominator);
let total_commission_to_receive =
quotient.saturating_mul(*commission_rate as u128).saturating_div(100u128);
let total_commission_to_receive =
u64::try_from(total_commission_to_receive).expect("Commission is too large");
staking_reward.saturating_add(total_commission_to_receive)
}
false => {
let commission = quotient.saturating_mul(*commission_rate as u128).saturating_div(100u128);
let commission_to_pay = u64::try_from(commission).expect("Commission is too large");
staking_reward.saturating_sub(commission_to_pay)
}
};
(*staker, (*validator, stake.saturating_add(staking_reward_after_commission)))
})
.collect()
}
pub fn proving_rewards<N: Network>(
proof_targets: Vec<(Address<N>, u64)>,
puzzle_reward: u64,
) -> IndexMap<Address<N>, u64> {
let combined_proof_target = proof_targets.iter().map(|(_, t)| *t as u128).sum::<u128>();
if proof_targets.is_empty() || combined_proof_target == 0 || puzzle_reward == 0 {
return Default::default();
}
let mut rewards = IndexMap::<_, u64>::with_capacity(proof_targets.len());
for (address, proof_target) in proof_targets {
let numerator = (puzzle_reward as u128).saturating_mul(proof_target as u128);
let denominator = combined_proof_target.max(1);
let quotient = numerator.saturating_div(denominator);
if quotient > MAX_COINBASE_REWARD as u128 {
error!("Prover reward ({quotient}) is too large - skipping solution from {address}");
continue;
}
let prover_reward = u64::try_from(quotient).expect("Prover reward is too large");
if prover_reward > 0 {
let entry = rewards.entry(address).or_default();
*entry = entry.saturating_add(prover_reward);
}
}
rewards
}
#[cfg(test)]
mod tests {
use super::*;
use console::prelude::TestRng;
use indexmap::indexmap;
type CurrentNetwork = console::network::MainnetV0;
const ITERATIONS: usize = 1000;
#[test]
fn test_staking_rewards() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_with_commissions(rng);
let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let address = *committee.members().iter().next().unwrap().0;
for _ in 0..ITERATIONS {
let stake = rng.gen_range(MIN_DELEGATOR_STAKE..committee.total_stake());
let stakers = indexmap! {address => (address, stake)};
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
assert_eq!(next_stakers.len(), 1);
let (candidate_address, (candidate_validator, candidate_stake)) = next_stakers.into_iter().next().unwrap();
assert_eq!(candidate_address, address);
assert_eq!(candidate_validator, address);
let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
assert_eq!(candidate_stake, stake + u64::try_from(reward).unwrap(), "stake: {stake}, reward: {reward}");
}
}
#[test]
fn test_staking_rewards_to_validator_not_in_committee() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(rng);
let fake_committee = ledger_committee::test_helpers::sample_committee(rng);
let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let stakers_fake = crate::committee::test_helpers::to_stakers(fake_committee.members(), rng);
let all_stakers: IndexMap<Address<CurrentNetwork>, (Address<CurrentNetwork>, u64)> =
stakers.clone().into_iter().chain(stakers_fake.clone()).collect();
let timer = std::time::Instant::now();
let next_stakers = staking_rewards::<CurrentNetwork>(&all_stakers, &committee, block_reward);
println!("staking_rewards: {}ms", timer.elapsed().as_millis());
assert_eq!(next_stakers.len(), all_stakers.len());
for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
all_stakers.into_iter().zip(next_stakers.into_iter())
{
assert_eq!(staker, next_staker);
assert_eq!(validator, next_validator);
if !committee.members().contains_key(&validator) {
assert_eq!(stake, next_stake, "stake: {stake}, reward should be 0");
} else {
let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
assert_eq!(stake + u64::try_from(reward).unwrap(), next_stake, "stake: {stake}, reward: {reward}");
}
}
}
#[test]
fn test_staking_rewards_commission() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_with_commissions(rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let commissions: IndexMap<Address<CurrentNetwork>, u8> =
committee.members().iter().map(|(address, (_, _, commission))| (*address, *commission)).collect();
println!("commissions: {:?}", commissions);
let mut total_commissions: IndexMap<Address<CurrentNetwork>, u64> = Default::default();
let timer = std::time::Instant::now();
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
println!("staking_rewards: {}ms", timer.elapsed().as_millis());
assert_eq!(next_stakers.len(), stakers.len());
for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
stakers.clone().into_iter().zip(next_stakers.clone().into_iter())
{
assert_eq!(staker, next_staker);
assert_eq!(validator, next_validator);
let commission_rate = commissions.get(&validator).copied().unwrap_or(0);
if staker == validator {
let total_stake = committee.get_stake(validator);
let total_delegated_stake = total_stake.saturating_sub(stake);
let reward = block_reward as u128 * total_delegated_stake as u128 / committee.total_stake() as u128;
let commission_to_receive = reward * commission_rate as u128 / 100;
total_commissions.insert(validator, u64::try_from(commission_to_receive).unwrap());
assert_eq!(
stake + u64::try_from(reward + commission_to_receive).unwrap(),
next_stake,
"stake: {stake}, reward: {reward}, commission_to_receive: {commission_to_receive}, commission_rate: {commission_rate}"
);
} else {
let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
let commission_to_pay = reward * commission_rate as u128 / 100;
assert_eq!(
stake + u64::try_from(reward - commission_to_pay).unwrap(),
next_stake,
"stake: {stake}, reward: {reward}, commission_to_pay: {commission_to_pay}, commission_rate: {commission_rate}"
);
}
}
assert_eq!(
total_commissions.len(),
committee.members().len(),
"total_commissions.len() != committee.members().len()"
);
for (validator, commission) in total_commissions {
let (_, stake) = stakers.get(&validator).unwrap();
let (_, next_stake) = next_stakers.get(&validator).unwrap();
let reward = block_reward as u128 * *stake as u128 / committee.total_stake() as u128;
let expected_stake = stake + commission + u64::try_from(reward).unwrap();
assert_eq!(*next_stake, expected_stake, "stake: {stake}, commission: {commission}");
}
}
#[test]
fn test_staking_rewards_large() {
let rng = &mut TestRng::default();
let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let committee = ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 100, rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let timer = std::time::Instant::now();
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
println!("staking_rewards: {}ms", timer.elapsed().as_millis());
assert_eq!(next_stakers.len(), stakers.len());
for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
stakers.into_iter().zip(next_stakers.into_iter())
{
assert_eq!(staker, next_staker);
assert_eq!(validator, next_validator);
let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
assert_eq!(stake + u64::try_from(reward).unwrap(), next_stake, "stake: {stake}, reward: {reward}");
}
}
#[test]
fn test_staking_rewards_when_delegator_is_under_min_yields_no_reward() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let address = *stakers.iter().find(|(address, _)| !committee.is_committee_member(**address)).unwrap().0;
for _ in 0..ITERATIONS {
let stake = rng.gen_range(0..MIN_DELEGATOR_STAKE);
let stakers = indexmap! {address => (address, stake)};
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
assert_eq!(next_stakers.len(), 1);
let (candidate_address, (candidate_validator, candidate_stake)) = next_stakers.into_iter().next().unwrap();
assert_eq!(candidate_address, address);
assert_eq!(candidate_validator, address);
assert_eq!(candidate_stake, stake);
}
}
#[test]
fn test_staking_rewards_cannot_exceed_coinbase_reward() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(rng);
let address = *committee.members().iter().next().unwrap().0;
let stakers = indexmap![address => (address, MIN_DELEGATOR_STAKE)];
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, u64::MAX);
assert_eq!(stakers, next_stakers);
for _ in 0..ITERATIONS {
let block_reward = rng.gen_range(MAX_COINBASE_REWARD..u64::MAX);
let stake = rng.gen_range(MIN_DELEGATOR_STAKE..u64::MAX);
let stakers = indexmap![address => (address, stake)];
let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
assert_eq!(stakers, next_stakers);
}
}
#[test]
fn test_staking_rewards_is_empty() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(rng);
let rewards = staking_rewards::<CurrentNetwork>(&indexmap![], &committee, rng.gen());
assert!(rewards.is_empty());
}
#[test]
fn test_proving_rewards() {
let rng = &mut TestRng::default();
for _ in 0..ITERATIONS {
let address = Address::rand(rng);
let puzzle_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
let rewards = proving_rewards::<CurrentNetwork>(vec![(address, u64::MAX)], puzzle_reward);
assert_eq!(rewards.len(), 1);
let (candidate_address, candidate_amount) = rewards.into_iter().next().unwrap();
assert_eq!(candidate_address, address);
assert!(candidate_amount <= puzzle_reward);
}
}
#[test]
fn test_proving_rewards_cannot_exceed_coinbase_reward() {
let rng = &mut TestRng::default();
for _ in 0..ITERATIONS {
let address = Address::rand(rng);
let puzzle_reward = rng.gen_range(MAX_COINBASE_REWARD..u64::MAX);
let proof_target = rng.gen_range(0..u64::MAX);
let rewards = proving_rewards::<CurrentNetwork>(vec![(address, proof_target)], puzzle_reward);
assert!(rewards.is_empty());
}
}
#[test]
fn test_proving_rewards_is_empty() {
let rng = &mut TestRng::default();
let address = Address::rand(rng);
let rewards = proving_rewards::<CurrentNetwork>(vec![], rng.gen());
assert!(rewards.is_empty());
let rewards = proving_rewards::<CurrentNetwork>(vec![(address, 2)], u64::MAX);
assert!(rewards.is_empty());
let rewards = proving_rewards::<CurrentNetwork>(vec![(address, 2)], 0);
assert!(rewards.is_empty());
}
}