//! 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); } }