/ synthesizer / src / vm / helpers / rewards.rs
rewards.rs
  1  // Copyright (c) 2025 ADnet Contributors
  2  // This file is part of the AlphaVM 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  use alphavm_ledger_committee::{Committee, MIN_DELEGATOR_STAKE};
 17  use console::{account::Address, network::prelude::*};
 18  
 19  use indexmap::IndexMap;
 20  use std::sync::atomic::AtomicU64;
 21  
 22  #[cfg(feature = "locktick")]
 23  use locktick::parking_lot::Mutex;
 24  #[cfg(not(feature = "locktick"))]
 25  use parking_lot::Mutex;
 26  #[cfg(not(feature = "serial"))]
 27  use rayon::prelude::*;
 28  
 29  /// A safety bound (sanity-check) for the coinbase reward.
 30  const MAX_COINBASE_REWARD: u64 = alphavm_ledger_block::MAX_COINBASE_REWARD; // Coinbase reward at block 1.
 31  
 32  /// Returns the updated stakers reflecting the staking rewards for the given committee, block reward,
 33  /// and validator commission rates.
 34  ///
 35  /// The staking reward for validators is defined as: `block_reward * stake / total_stake + commission_to_recieve`.
 36  /// The commission to receive for validators is defined as: `block_reward * (total_stake_delegated / total_stake) * (rate / 100)`.
 37  ///
 38  /// The staking reward for delegators is defined as: `block_reward * stake / total_stake - commission_to_pay`.
 39  /// The commission to pay for delegators is defined as: `block_reward * (stake / total_stake) * (rate / 100)`
 40  ///
 41  /// This method ensures that stakers who are bonded to validators with more than **25%**
 42  /// of the total stake will not receive a staking reward. In addition, this method
 43  /// ensures delegators who have less than 10,000 credits are not eligible for a staking reward.
 44  ///
 45  /// The choice of 25% is to ensure at least 4 validators are operational at any given time.
 46  /// Our security model tolerates Byzantines behavior by validators staking up to f stake,
 47  /// where f = max{m: integer | m < N/3}, N being the total amount staked.
 48  /// Therefore, 1 Byzantine validator out of 4 equal-staked validators will be tolerated.
 49  pub fn staking_rewards<N: Network>(
 50      stakers: &IndexMap<Address<N>, (Address<N>, u64)>,
 51      committee: &Committee<N>,
 52      block_reward: u64,
 53  ) -> IndexMap<Address<N>, (Address<N>, u64)> {
 54      // If the list of stakers is empty, there is no stake, or the block reward is 0, return the stakers.
 55      if stakers.is_empty() || committee.total_stake() == 0 || block_reward == 0 {
 56          return stakers.clone();
 57      }
 58  
 59      // First, validate all validators and create a map of valid validators.
 60      let valid_validators: IndexMap<_, _> = committee
 61          .members()
 62          .iter()
 63          .filter_map(|(validator, (validator_stake, _is_open, commission_rate))| {
 64              // Skip validators with invalid commission rates.
 65              if *commission_rate > 100 {
 66                  error!("Commission rate ({commission_rate}) is greater than 100 - skipping validator {validator}");
 67                  return None;
 68              }
 69              // Skip validators with more than 25% of total stake.
 70              if *validator_stake > committee.total_stake().saturating_div(4) {
 71                  trace!("Validator {validator} has more than 25% of the total stake - skipping all stakers");
 72                  return None;
 73              }
 74              Some((*validator, (*validator_stake, *commission_rate)))
 75          })
 76          .collect();
 77  
 78      // Track validators not in committee as well as their stake.
 79      // Pre-allocating with an expected capacity prevents reallocation while the mutex is held.
 80      let hashset_capacity = committee.members().len();
 81      let missing_validators = Mutex::new(std::collections::HashSet::<Address<N>>::with_capacity(hashset_capacity));
 82      let invalid_validator_stake = AtomicU64::new(0);
 83  
 84      // Compute the updated stakers.
 85      let staking_rewards = cfg_iter!(stakers)
 86          .map(|(staker, (validator, stake))| {
 87              // If the validator is not in the valid validators list, skip the staker.
 88              let Some((validator_stake, commission_rate)) = valid_validators.get(validator) else {
 89                  // Log validator not in committee.
 90                  if tracing::enabled!(tracing::Level::TRACE) && !committee.members().contains_key(validator) {
 91                      let mut logged = missing_validators.lock();
 92                      if logged.insert(*validator) {
 93                          trace!("Validator {validator} is not in the committee - skipping all stakers");
 94                      }
 95                      invalid_validator_stake.fetch_add(*stake, std::sync::atomic::Ordering::Relaxed);
 96                  }
 97                  return (*staker, (*validator, *stake));
 98              };
 99  
100              // If the staker has less than the minimum required stake, skip the staker, unless the staker is the validator.
101              if *stake < MIN_DELEGATOR_STAKE && *staker != *validator {
102                  trace!("Staker has less than {MIN_DELEGATOR_STAKE} microcredits - skipping {staker}");
103                  return (*staker, (*validator, *stake));
104              }
105  
106              // Compute the numerator.
107              let numerator = (block_reward as u128).saturating_mul(*stake as u128);
108              // Compute the denominator.
109              // Note: We guarantee this denominator cannot be 0 (as we return early if the total stake is 0).
110              let denominator = committee.total_stake() as u128;
111              // Compute the quotient.
112              let quotient = numerator.saturating_div(denominator);
113              // Ensure the staking reward is within a safe bound.
114              if quotient > MAX_COINBASE_REWARD as u128 {
115                  error!("Staking reward ({quotient}) is too large - skipping {staker}");
116                  return (*staker, (*validator, *stake));
117              }
118              // Cast the staking reward as a u64.
119              // Note: This '.expect' is guaranteed to be safe, as we ensure the quotient is within a safe bound.
120              let staking_reward = u64::try_from(quotient).expect("Staking reward is too large");
121  
122              // Update the staking reward with the commission.
123              //
124              // Note: This approach to computing commissions is far more computationally-efficient,
125              // however it does introduce a small (deterministic) precision error that is accepted for the
126              // sake of performance. There is a negligible difference (at most 100 microcredits per delegator)
127              // between the validators (+) and the delegators (-) in the allocated commission difference.
128              let staking_reward_after_commission = match staker == validator {
129                  // If the staker is the validator, add the total commission to the staking reward.
130                  true => {
131                      // Calculate the total stake delegated to the validator.
132                      let total_delegated_stake = validator_stake.saturating_sub(*stake);
133                      // Compute the numerator.
134                      let numerator = (block_reward as u128).saturating_mul(total_delegated_stake as u128);
135                      // Compute the quotient. This quotient is the total staking reward recieved by delegators.
136                      let quotient = numerator.saturating_div(denominator);
137                      // Compute the commission.
138                      let total_commission_to_receive =
139                          quotient.saturating_mul(*commission_rate as u128).saturating_div(100u128);
140                      // Cast the commission as a u64.
141                      // Note: This '.expect' is guaranteed to be safe, as we ensure the commission is within a safe bound.
142                      let total_commission_to_receive =
143                          u64::try_from(total_commission_to_receive).expect("Commission is too large");
144  
145                      // Add the commission to the validator staking reward.
146                      staking_reward.saturating_add(total_commission_to_receive)
147                  }
148                  // If the staker is a delegator, subtract the commission from the staking reward.
149                  false => {
150                      // Calculate the commission.
151                      let commission = quotient.saturating_mul(*commission_rate as u128).saturating_div(100u128);
152                      // Cast the commission as a u64.
153                      // Note: This '.expect' is guaranteed to be safe, as we ensure the quotient is within a safe bound.
154                      let commission_to_pay = u64::try_from(commission).expect("Commission is too large");
155  
156                      // Subtract the commission from the delegator staking reward.
157                      staking_reward.saturating_sub(commission_to_pay)
158                  }
159              };
160              // Return the staker and the updated stake.
161              (*staker, (*validator, stake.saturating_add(staking_reward_after_commission)))
162          })
163          .collect();
164  
165      if tracing::enabled!(tracing::Level::TRACE) {
166          let invalid_validator_stake = invalid_validator_stake.load(std::sync::atomic::Ordering::Relaxed);
167          trace!("Total stake invalidated due to validator not in committee: {invalid_validator_stake}");
168      }
169  
170      // Return the result.
171      staking_rewards
172  }
173  
174  /// Returns the proving rewards for a given coinbase reward and list of prover solutions.
175  /// The prover reward is defined as: `puzzle_reward * (proof_target / combined_proof_target)`.
176  pub fn proving_rewards<N: Network>(
177      proof_targets: Vec<(Address<N>, u64)>,
178      puzzle_reward: u64,
179  ) -> IndexMap<Address<N>, u64> {
180      // Compute the combined proof target. Using '.sum' here is safe because we sum u64s into a u128.
181      let combined_proof_target = proof_targets.iter().map(|(_, t)| *t as u128).sum::<u128>();
182  
183      // If the list of solutions is empty, the combined proof target is 0, or the puzzle reward is 0, return an empty map.
184      if proof_targets.is_empty() || combined_proof_target == 0 || puzzle_reward == 0 {
185          return Default::default();
186      }
187  
188      // Initialize a vector to store the proving rewards.
189      let mut rewards = IndexMap::<_, u64>::with_capacity(proof_targets.len());
190  
191      // Calculate the rewards for the individual provers.
192      for (address, proof_target) in proof_targets {
193          // Compute the numerator.
194          let numerator = (puzzle_reward as u128).saturating_mul(proof_target as u128);
195          // Compute the denominator.
196          // Note: We guarantee this denominator cannot be 0 (to prevent a div by 0).
197          let denominator = combined_proof_target.max(1);
198          // Compute the quotient.
199          let quotient = numerator.saturating_div(denominator);
200          // Ensure the proving reward is within a safe bound.
201          if quotient > MAX_COINBASE_REWARD as u128 {
202              error!("Prover reward ({quotient}) is too large - skipping solution from {address}");
203              continue;
204          }
205          // Cast the proving reward as a u64.
206          // Note: This '.expect' is guaranteed to be safe, as we ensure the quotient is within a safe bound.
207          let prover_reward = u64::try_from(quotient).expect("Prover reward is too large");
208          // If there is a proving reward, append it to the vector.
209          if prover_reward > 0 {
210              // Add the proving reward to the prover.
211              let entry = rewards.entry(address).or_default();
212              *entry = entry.saturating_add(prover_reward);
213          }
214      }
215  
216      // Return the proving rewards.
217      rewards
218  }
219  
220  #[cfg(test)]
221  mod tests {
222      use super::*;
223      use console::prelude::TestRng;
224  
225      use indexmap::indexmap;
226  
227      type CurrentNetwork = console::network::MainnetV0;
228  
229      const ITERATIONS: usize = 1000;
230  
231      #[test]
232      fn test_staking_rewards() {
233          let rng = &mut TestRng::default();
234          // Sample a random committee.
235          let committee = alphavm_ledger_committee::test_helpers::sample_committee_with_commissions(rng);
236          // Sample a random block reward.
237          let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
238          // Retrieve an address.
239          let address = *committee.members().iter().next().unwrap().0;
240  
241          for _ in 0..ITERATIONS {
242              // Sample a random stake.
243              let stake = rng.gen_range(MIN_DELEGATOR_STAKE..committee.total_stake());
244              // Construct the stakers.
245              let stakers = indexmap! {address => (address, stake)};
246              let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
247              assert_eq!(next_stakers.len(), 1);
248              let (candidate_address, (candidate_validator, candidate_stake)) = next_stakers.into_iter().next().unwrap();
249              assert_eq!(candidate_address, address);
250              assert_eq!(candidate_validator, address);
251              let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
252              assert_eq!(candidate_stake, stake + u64::try_from(reward).unwrap(), "stake: {stake}, reward: {reward}");
253          }
254      }
255  
256      #[test]
257      fn test_staking_rewards_to_validator_not_in_committee() {
258          let rng = &mut TestRng::default();
259          // Sample a random committee.
260          let committee = alphavm_ledger_committee::test_helpers::sample_committee(rng);
261          let fake_committee = alphavm_ledger_committee::test_helpers::sample_committee(rng);
262          // Sample a random block reward.
263          let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
264  
265          // Generate the stakers
266          let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
267          // Generate stakers for a non-existent committee, to ensure they are not rewarded.
268          let stakers_fake = crate::committee::test_helpers::to_stakers(fake_committee.members(), rng);
269          let all_stakers: IndexMap<Address<CurrentNetwork>, (Address<CurrentNetwork>, u64)> =
270              stakers.clone().into_iter().chain(stakers_fake.clone()).collect();
271  
272          // Start a timer.
273          let timer = std::time::Instant::now();
274  
275          let next_stakers = staking_rewards::<CurrentNetwork>(&all_stakers, &committee, block_reward);
276          println!("staking_rewards: {}ms", timer.elapsed().as_millis());
277          assert_eq!(next_stakers.len(), all_stakers.len());
278          for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
279              all_stakers.into_iter().zip(next_stakers.into_iter())
280          {
281              assert_eq!(staker, next_staker);
282              assert_eq!(validator, next_validator);
283              // If the validator is not in the committee, the stake should not change.
284              if !committee.members().contains_key(&validator) {
285                  assert_eq!(stake, next_stake, "stake: {stake}, reward should be 0");
286              } else {
287                  // Otherwise, the stake should increase.
288                  let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
289                  assert_eq!(stake + u64::try_from(reward).unwrap(), next_stake, "stake: {stake}, reward: {reward}");
290              }
291          }
292      }
293  
294      #[test]
295      fn test_staking_rewards_commission() {
296          let rng = &mut TestRng::default();
297          // Sample a random committee.
298          let committee = alphavm_ledger_committee::test_helpers::sample_committee_with_commissions(rng);
299          // Convert the committee into stakers.
300          let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
301          // Sample a random block reward.
302          let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
303          // Create a map of validators to commissions
304          let commissions: IndexMap<Address<CurrentNetwork>, u8> =
305              committee.members().iter().map(|(address, (_, _, commission))| (*address, *commission)).collect();
306          // Print the commissions from the indexmap
307          println!("commissions: {commissions:?}");
308          // Create a map of validators to the sum of their commissions
309          let mut total_commissions: IndexMap<Address<CurrentNetwork>, u64> = Default::default();
310  
311          // Start a timer.
312          let timer = std::time::Instant::now();
313          // Compute the staking rewards.
314          let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
315          println!("staking_rewards: {}ms", timer.elapsed().as_millis());
316          assert_eq!(next_stakers.len(), stakers.len());
317          for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
318              stakers.clone().into_iter().zip(next_stakers.clone().into_iter())
319          {
320              assert_eq!(staker, next_staker);
321              assert_eq!(validator, next_validator);
322  
323              let commission_rate = commissions.get(&validator).copied().unwrap_or(0);
324  
325              if staker == validator {
326                  // Calculate the total stake delegated to the validator.
327                  let total_stake = committee.get_stake(validator);
328                  let total_delegated_stake = total_stake.saturating_sub(stake);
329  
330                  // Calculate the commission to receive.
331                  let reward = block_reward as u128 * total_delegated_stake as u128 / committee.total_stake() as u128;
332                  let commission_to_receive = reward * commission_rate as u128 / 100;
333  
334                  total_commissions.insert(validator, u64::try_from(commission_to_receive).unwrap());
335                  assert_eq!(
336                      stake + u64::try_from(reward + commission_to_receive).unwrap(),
337                      next_stake,
338                      "stake: {stake}, reward: {reward}, commission_to_receive: {commission_to_receive}, commission_rate: {commission_rate}"
339                  );
340              } else {
341                  // Calculate the commission to pay the validator.
342                  let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
343                  let commission_to_pay = reward * commission_rate as u128 / 100;
344  
345                  assert_eq!(
346                      stake + u64::try_from(reward - commission_to_pay).unwrap(),
347                      next_stake,
348                      "stake: {stake}, reward: {reward}, commission_to_pay: {commission_to_pay}, commission_rate: {commission_rate}"
349                  );
350              }
351          }
352  
353          assert_eq!(
354              total_commissions.len(),
355              committee.members().len(),
356              "total_commissions.len() != committee.members().len()"
357          );
358  
359          // For each staker that is a validator, ensure the next staker = reward + commission.
360          for (validator, commission) in total_commissions {
361              let (_, stake) = stakers.get(&validator).unwrap();
362              let (_, next_stake) = next_stakers.get(&validator).unwrap();
363              let reward = block_reward as u128 * *stake as u128 / committee.total_stake() as u128;
364              let expected_stake = stake + commission + u64::try_from(reward).unwrap();
365              assert_eq!(*next_stake, expected_stake, "stake: {stake}, commission: {commission}");
366          }
367      }
368  
369      #[test]
370      fn test_staking_rewards_large() {
371          let rng = &mut TestRng::default();
372  
373          // Sample a random block reward.
374          let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
375          // Sample a committee.
376          let committee = alphavm_ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 25, rng);
377          // Convert the committee into stakers.
378          let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
379  
380          // Start a timer.
381          let timer = std::time::Instant::now();
382          // Compute the staking rewards.
383          let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
384          println!("staking_rewards: {}ms", timer.elapsed().as_millis());
385          assert_eq!(next_stakers.len(), stakers.len());
386          for ((staker, (validator, stake)), (next_staker, (next_validator, next_stake))) in
387              stakers.into_iter().zip(next_stakers.into_iter())
388          {
389              assert_eq!(staker, next_staker);
390              assert_eq!(validator, next_validator);
391              let reward = block_reward as u128 * stake as u128 / committee.total_stake() as u128;
392              assert_eq!(stake + u64::try_from(reward).unwrap(), next_stake, "stake: {stake}, reward: {reward}");
393          }
394      }
395  
396      #[test]
397      fn test_staking_rewards_when_delegator_is_under_min_yields_no_reward() {
398          let rng = &mut TestRng::default();
399          // Sample a random committee.
400          let committee = alphavm_ledger_committee::test_helpers::sample_committee(rng);
401          // Convert the committee into stakers.
402          let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
403          // Sample a random block reward.
404          let block_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
405          // Retrieve an address of a delegator that isn't a validator
406          let address = *stakers.iter().find(|(address, _)| !committee.is_committee_member(**address)).unwrap().0;
407  
408          for _ in 0..ITERATIONS {
409              // Sample a random stake.
410              let stake = rng.gen_range(0..MIN_DELEGATOR_STAKE);
411              // Construct the stakers.
412              let stakers = indexmap! {address => (address, stake)};
413              let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
414              assert_eq!(next_stakers.len(), 1);
415              let (candidate_address, (candidate_validator, candidate_stake)) = next_stakers.into_iter().next().unwrap();
416              assert_eq!(candidate_address, address);
417              assert_eq!(candidate_validator, address);
418              assert_eq!(candidate_stake, stake);
419          }
420      }
421  
422      #[test]
423      fn test_staking_rewards_cannot_exceed_coinbase_reward() {
424          let rng = &mut TestRng::default();
425          // Sample a random committee.
426          let committee = alphavm_ledger_committee::test_helpers::sample_committee(rng);
427          // Retrieve an address.
428          let address = *committee.members().iter().next().unwrap().0;
429  
430          // Construct the stakers.
431          let stakers = indexmap![address => (address, MIN_DELEGATOR_STAKE)];
432          // Check that a maxed out coinbase reward, returns empty.
433          let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, u64::MAX);
434          assert_eq!(stakers, next_stakers);
435  
436          // Ensure a staking reward that is too large, renders no rewards.
437          for _ in 0..ITERATIONS {
438              // Sample a random overly-large block reward.
439              let block_reward = rng.gen_range(MAX_COINBASE_REWARD..u64::MAX);
440              // Sample a random stake.
441              let stake = rng.gen_range(MIN_DELEGATOR_STAKE..u64::MAX);
442              // Construct the stakers.
443              let stakers = indexmap![address => (address, stake)];
444              // Check that an overly large block reward fails.
445              let next_stakers = staking_rewards::<CurrentNetwork>(&stakers, &committee, block_reward);
446              assert_eq!(stakers, next_stakers);
447          }
448      }
449  
450      #[test]
451      fn test_staking_rewards_is_empty() {
452          let rng = &mut TestRng::default();
453          // Sample a random committee.
454          let committee = alphavm_ledger_committee::test_helpers::sample_committee(rng);
455  
456          // Compute the staking rewards (empty).
457          let rewards = staking_rewards::<CurrentNetwork>(&indexmap![], &committee, rng.r#gen());
458          assert!(rewards.is_empty());
459      }
460  
461      #[test]
462      fn test_proving_rewards() {
463          let rng = &mut TestRng::default();
464  
465          for _ in 0..ITERATIONS {
466              // Sample a random address.
467              let address = Address::rand(rng);
468              // Sample a random puzzle reward.
469              let puzzle_reward = rng.gen_range(0..MAX_COINBASE_REWARD);
470  
471              let rewards = proving_rewards::<CurrentNetwork>(vec![(address, u64::MAX)], puzzle_reward);
472              assert_eq!(rewards.len(), 1);
473              let (candidate_address, candidate_amount) = rewards.into_iter().next().unwrap();
474              assert_eq!(candidate_address, address);
475              assert!(candidate_amount <= puzzle_reward);
476          }
477      }
478  
479      #[test]
480      fn test_proving_rewards_cannot_exceed_coinbase_reward() {
481          let rng = &mut TestRng::default();
482  
483          // Ensure a proving reward that is too large, renders no rewards.
484          for _ in 0..ITERATIONS {
485              // Sample a random address.
486              let address = Address::rand(rng);
487              // Sample a random overly-large puzzle reward.
488              let puzzle_reward = rng.gen_range(MAX_COINBASE_REWARD..u64::MAX);
489              // Sample a random proof target.
490              let proof_target = rng.gen_range(0..u64::MAX);
491              // Check that a maxed out proof target fails.
492              let rewards = proving_rewards::<CurrentNetwork>(vec![(address, proof_target)], puzzle_reward);
493              assert!(rewards.is_empty());
494          }
495      }
496  
497      #[test]
498      fn test_proving_rewards_is_empty() {
499          let rng = &mut TestRng::default();
500          // Sample a random address.
501          let address = Address::rand(rng);
502  
503          // Compute the proving rewards (empty).
504          let rewards = proving_rewards::<CurrentNetwork>(vec![], rng.r#gen());
505          assert!(rewards.is_empty());
506  
507          // Check that a maxed out coinbase reward, returns empty.
508          let rewards = proving_rewards::<CurrentNetwork>(vec![(address, 2)], u64::MAX);
509          assert!(rewards.is_empty());
510  
511          // Ensure a 0 coinbase reward case is empty.
512          let rewards = proving_rewards::<CurrentNetwork>(vec![(address, 2)], 0);
513          assert!(rewards.is_empty());
514      }
515  }