use 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))| {
if committee.get_stake(*validator) > 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 {
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");
(*staker, (*validator, stake.saturating_add(staking_reward)))
})
.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::Testnet3;
const ITERATIONS: usize = 1000;
#[test]
fn test_staking_rewards() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(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_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_staker_is_under_min_yields_no_reward() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(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(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());
}
}