synor/crates/synor-consensus/src/rewards.rs
2026-01-08 05:22:24 +05:30

346 lines
10 KiB
Rust

//! Block reward calculations for Synor.
//!
//! Synor uses a chromatic halving schedule similar to Kaspa,
//! with smooth reduction rather than sudden halvings.
//!
//! Initial reward: 500 SYNOR per block
//! Total supply: 70,000,000 SYNOR (fixed)
use synor_types::Amount;
use thiserror::Error;
/// Initial block reward in sompi (500 SYNOR).
pub const INITIAL_REWARD: u64 = 500 * Amount::ONE_SYNOR;
/// Halving interval in blocks (~1 year at 10 bps).
pub const HALVING_INTERVAL: u64 = 315_360_000; // ~1 year
/// Number of halvings until reward becomes zero.
pub const MAX_HALVINGS: u32 = 8;
/// Chromatic halving period (smooth reduction).
pub const CHROMATIC_PERIOD: u64 = 1_000_000; // ~27 hours
/// Block reward information.
#[derive(Clone, Debug)]
pub struct BlockReward {
/// Base subsidy (before fees).
pub subsidy: Amount,
/// Total fees collected.
pub fees: Amount,
/// Total reward (subsidy + fees).
pub total: Amount,
/// Current emission epoch.
pub epoch: u32,
}
impl BlockReward {
/// Creates a new block reward.
pub fn new(subsidy: Amount, fees: Amount) -> Self {
BlockReward {
subsidy,
fees,
total: subsidy.saturating_add(fees),
epoch: 0,
}
}
/// Creates a genesis reward.
pub fn genesis() -> Self {
BlockReward {
subsidy: Amount::from_sompi(INITIAL_REWARD),
fees: Amount::ZERO,
total: Amount::from_sompi(INITIAL_REWARD),
epoch: 0,
}
}
}
/// Reward calculator.
pub struct RewardCalculator {
/// Initial reward in sompi.
initial_reward: u64,
/// Halving interval.
halving_interval: u64,
/// Use chromatic (smooth) halving.
chromatic: bool,
}
impl RewardCalculator {
/// Creates a new reward calculator.
pub fn new() -> Self {
RewardCalculator {
initial_reward: INITIAL_REWARD,
halving_interval: HALVING_INTERVAL,
chromatic: true,
}
}
/// Creates a calculator for testing.
pub fn for_testing() -> Self {
RewardCalculator {
initial_reward: 500 * 100_000_000, // 500 SYNOR
halving_interval: 1000, // Quick halvings
chromatic: true,
}
}
/// Calculates the block subsidy for a given DAA score.
pub fn calculate_subsidy(&self, daa_score: u64) -> Amount {
if self.chromatic {
self.calculate_chromatic_subsidy(daa_score)
} else {
self.calculate_standard_subsidy(daa_score)
}
}
/// Standard halving (like Bitcoin).
fn calculate_standard_subsidy(&self, daa_score: u64) -> Amount {
let halvings = (daa_score / self.halving_interval) as u32;
if halvings >= MAX_HALVINGS {
return Amount::ZERO;
}
let reward = self.initial_reward >> halvings;
Amount::from_sompi(reward)
}
/// Chromatic halving (smooth reduction like Kaspa).
fn calculate_chromatic_subsidy(&self, daa_score: u64) -> Amount {
// Chromatic halving uses a smoother curve
// Reward = initial * (1/2)^(daa_score / halving_interval)
// For efficiency, we approximate this
let halvings = daa_score / self.halving_interval;
let position_in_epoch = daa_score % self.halving_interval;
if halvings as u32 >= MAX_HALVINGS {
return Amount::ZERO;
}
// Base reward at this epoch
let base_reward = self.initial_reward >> halvings;
// Chromatic adjustment within epoch
// Linearly decrease toward next halving
let next_reward = base_reward / 2;
let epoch_progress = position_in_epoch as f64 / self.halving_interval as f64;
// Smooth interpolation
let adjusted = base_reward as f64 - (base_reward - next_reward) as f64 * epoch_progress;
Amount::from_sompi(adjusted as u64)
}
/// Calculates total reward (subsidy + fees).
pub fn calculate_reward(&self, daa_score: u64, fees: Amount) -> BlockReward {
let subsidy = self.calculate_subsidy(daa_score);
let halvings = (daa_score / self.halving_interval) as u32;
BlockReward {
subsidy,
fees,
total: subsidy.saturating_add(fees),
epoch: halvings.min(MAX_HALVINGS),
}
}
/// Estimates total supply at a given DAA score.
pub fn estimate_supply_at(&self, daa_score: u64) -> Amount {
// This is an approximation
let mut total = 0u64;
let mut current_score = 0u64;
while current_score < daa_score {
let subsidy = self.calculate_subsidy(current_score);
if subsidy == Amount::ZERO {
break;
}
// Add rewards for a chunk of blocks
let chunk_size = (daa_score - current_score).min(10000);
total = total.saturating_add(subsidy.as_sompi() * chunk_size);
current_score += chunk_size;
}
Amount::from_sompi(total)
}
/// Returns the current epoch (halving count).
pub fn get_epoch(&self, daa_score: u64) -> u32 {
((daa_score / self.halving_interval) as u32).min(MAX_HALVINGS)
}
/// Returns the DAA score at which rewards end.
pub fn emission_end_score(&self) -> u64 {
self.halving_interval * MAX_HALVINGS as u64
}
/// Returns the initial reward.
pub fn initial_reward(&self) -> Amount {
Amount::from_sompi(self.initial_reward)
}
}
impl Default for RewardCalculator {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for RewardCalculator {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RewardCalculator")
.field("initial_reward", &Amount::from_sompi(self.initial_reward))
.field("halving_interval", &self.halving_interval)
.field("chromatic", &self.chromatic)
.finish()
}
}
/// Fee distribution for block rewards.
#[derive(Clone, Debug)]
pub struct FeeDistribution {
/// Amount to burn (deflationary).
pub burn: Amount,
/// Amount to stakers.
pub stakers: Amount,
/// Amount to community pool.
pub community: Amount,
/// Amount to miner/validator.
pub miner: Amount,
}
impl FeeDistribution {
/// Distributes fees according to Synor's tokenomics.
pub fn distribute(fees: Amount) -> Self {
// 10% burn, 60% stakers, 20% community, 10% miner
let fee_sompi = fees.as_sompi();
let burn = Amount::from_sompi(fee_sompi * 10 / 100);
let stakers = Amount::from_sompi(fee_sompi * 60 / 100);
let community = Amount::from_sompi(fee_sompi * 20 / 100);
let miner = fees
.saturating_sub(burn)
.saturating_sub(stakers)
.saturating_sub(community);
FeeDistribution {
burn,
stakers,
community,
miner,
}
}
}
/// Errors related to rewards.
#[derive(Debug, Error)]
pub enum RewardError {
#[error("Invalid DAA score")]
InvalidDaaScore,
#[error("Reward overflow")]
Overflow,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_initial_reward() {
let calc = RewardCalculator::new();
let reward = calc.calculate_subsidy(0);
assert_eq!(reward, Amount::from_synor(500));
}
#[test]
fn test_reward_decreases() {
let calc = RewardCalculator::for_testing();
let reward_0 = calc.calculate_subsidy(0);
let reward_1000 = calc.calculate_subsidy(1000);
// After one halving, reward should be about half
assert!(reward_1000 < reward_0);
assert!(reward_1000 >= Amount::from_synor(200)); // Roughly half of 500
}
#[test]
fn test_chromatic_smooth() {
let calc = RewardCalculator::for_testing();
// Rewards should decrease smoothly within an epoch
let reward_0 = calc.calculate_subsidy(0);
let reward_500 = calc.calculate_subsidy(500); // Halfway through epoch
let reward_999 = calc.calculate_subsidy(999); // End of epoch
assert!(reward_0 > reward_500);
assert!(reward_500 > reward_999);
}
#[test]
fn test_max_halvings() {
let calc = RewardCalculator::for_testing();
// After max halvings, reward should be zero
let end_score = calc.emission_end_score();
let reward = calc.calculate_subsidy(end_score + 1000);
assert_eq!(reward, Amount::ZERO);
}
#[test]
fn test_block_reward() {
let calc = RewardCalculator::new();
let fees = Amount::from_synor(1);
let reward = calc.calculate_reward(0, fees);
assert_eq!(reward.subsidy, Amount::from_synor(500));
assert_eq!(reward.fees, Amount::from_synor(1));
assert_eq!(reward.total, Amount::from_synor(501));
assert_eq!(reward.epoch, 0);
}
#[test]
fn test_fee_distribution() {
let fees = Amount::from_synor(100);
let dist = FeeDistribution::distribute(fees);
// Verify distribution adds up
let total = dist
.burn
.saturating_add(dist.stakers)
.saturating_add(dist.community)
.saturating_add(dist.miner);
assert_eq!(total, fees);
// Verify percentages
assert_eq!(dist.burn, Amount::from_synor(10));
assert_eq!(dist.stakers, Amount::from_synor(60));
assert_eq!(dist.community, Amount::from_synor(20));
assert_eq!(dist.miner, Amount::from_synor(10));
}
#[test]
fn test_get_epoch() {
let calc = RewardCalculator::for_testing();
assert_eq!(calc.get_epoch(0), 0);
assert_eq!(calc.get_epoch(500), 0);
assert_eq!(calc.get_epoch(1000), 1);
assert_eq!(calc.get_epoch(2000), 2);
}
#[test]
fn test_genesis_reward() {
let reward = BlockReward::genesis();
assert_eq!(reward.subsidy, Amount::from_synor(500));
assert_eq!(reward.fees, Amount::ZERO);
assert_eq!(reward.epoch, 0);
}
}