diff --git a/api/src/sdk.rs b/api/src/sdk.rs index de99c2c..c47e287 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -3,7 +3,7 @@ use steel::*; use crate::{ consts::*, instruction::*, - state::{bus_pda, config_pda, proof_pda, treasury_pda}, + state::{config_pda, proof_pda, treasury_pda}, }; /// Builds an auth instruction. @@ -97,21 +97,14 @@ pub fn open(signer: Pubkey, miner: Pubkey, payer: Pubkey) -> Instruction { } /// Builds a reset instruction. -pub fn reset(signer: Pubkey) -> Instruction { +pub fn reset(signer: Pubkey, best_proof: Pubkey) -> Instruction { Instruction { program_id: crate::ID, accounts: vec![ AccountMeta::new(signer, true), - AccountMeta::new(BUS_ADDRESSES[0], false), - AccountMeta::new(BUS_ADDRESSES[1], false), - AccountMeta::new(BUS_ADDRESSES[2], false), - AccountMeta::new(BUS_ADDRESSES[3], false), - AccountMeta::new(BUS_ADDRESSES[4], false), - AccountMeta::new(BUS_ADDRESSES[5], false), - AccountMeta::new(BUS_ADDRESSES[6], false), - AccountMeta::new(BUS_ADDRESSES[7], false), AccountMeta::new(CONFIG_ADDRESS, false), AccountMeta::new(MINT_ADDRESS, false), + AccountMeta::new(best_proof, false), AccountMeta::new(TREASURY_ADDRESS, false), AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), AccountMeta::new_readonly(spl_token::ID, false), @@ -134,52 +127,52 @@ pub fn update(signer: Pubkey, miner: Pubkey) -> Instruction { } } -/// Builds an initialize instruction. -pub fn initialize(signer: Pubkey) -> Instruction { - let bus_pdas = [ - bus_pda(0), - bus_pda(1), - bus_pda(2), - bus_pda(3), - bus_pda(4), - bus_pda(5), - bus_pda(6), - bus_pda(7), - ]; - let config_pda = config_pda(); - let mint_pda = Pubkey::find_program_address(&[MINT, MINT_NOISE.as_slice()], &crate::ID); - let treasury_pda = treasury_pda(); - let metadata_pda = Pubkey::find_program_address( - &[ - METADATA, - mpl_token_metadata::ID.as_ref(), - mint_pda.0.as_ref(), - ], - &mpl_token_metadata::ID, - ); - Instruction { - program_id: crate::ID, - accounts: vec![ - AccountMeta::new(signer, true), - AccountMeta::new(bus_pdas[0].0, false), - AccountMeta::new(bus_pdas[1].0, false), - AccountMeta::new(bus_pdas[2].0, false), - AccountMeta::new(bus_pdas[3].0, false), - AccountMeta::new(bus_pdas[4].0, false), - AccountMeta::new(bus_pdas[5].0, false), - AccountMeta::new(bus_pdas[6].0, false), - AccountMeta::new(bus_pdas[7].0, false), - AccountMeta::new(config_pda.0, false), - AccountMeta::new(metadata_pda.0, false), - AccountMeta::new(mint_pda.0, false), - AccountMeta::new(treasury_pda.0, false), - AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), - AccountMeta::new_readonly(system_program::ID, false), - AccountMeta::new_readonly(spl_token::ID, false), - AccountMeta::new_readonly(spl_associated_token_account::ID, false), - AccountMeta::new_readonly(mpl_token_metadata::ID, false), - AccountMeta::new_readonly(sysvar::rent::ID, false), - ], - data: Initialize {}.to_bytes(), - } -} +// Builds an initialize instruction. +// pub fn initialize(signer: Pubkey) -> Instruction { +// let bus_pdas = [ +// bus_pda(0), +// bus_pda(1), +// bus_pda(2), +// bus_pda(3), +// bus_pda(4), +// bus_pda(5), +// bus_pda(6), +// bus_pda(7), +// ]; +// let config_pda = config_pda(); +// let mint_pda = Pubkey::find_program_address(&[MINT, MINT_NOISE.as_slice()], &crate::ID); +// let treasury_pda = treasury_pda(); +// let metadata_pda = Pubkey::find_program_address( +// &[ +// METADATA, +// mpl_token_metadata::ID.as_ref(), +// mint_pda.0.as_ref(), +// ], +// &mpl_token_metadata::ID, +// ); +// Instruction { +// program_id: crate::ID, +// accounts: vec![ +// AccountMeta::new(signer, true), +// AccountMeta::new(bus_pdas[0].0, false), +// AccountMeta::new(bus_pdas[1].0, false), +// AccountMeta::new(bus_pdas[2].0, false), +// AccountMeta::new(bus_pdas[3].0, false), +// AccountMeta::new(bus_pdas[4].0, false), +// AccountMeta::new(bus_pdas[5].0, false), +// AccountMeta::new(bus_pdas[6].0, false), +// AccountMeta::new(bus_pdas[7].0, false), +// AccountMeta::new(config_pda.0, false), +// AccountMeta::new(metadata_pda.0, false), +// AccountMeta::new(mint_pda.0, false), +// AccountMeta::new(treasury_pda.0, false), +// AccountMeta::new(TREASURY_TOKENS_ADDRESS, false), +// AccountMeta::new_readonly(system_program::ID, false), +// AccountMeta::new_readonly(spl_token::ID, false), +// AccountMeta::new_readonly(spl_associated_token_account::ID, false), +// AccountMeta::new_readonly(mpl_token_metadata::ID, false), +// AccountMeta::new_readonly(sysvar::rent::ID, false), +// ], +// data: Initialize {}.to_bytes(), +// } +// } diff --git a/api/src/state/bus.rs b/api/src/state/bus.rs deleted file mode 100644 index 71504b3..0000000 --- a/api/src/state/bus.rs +++ /dev/null @@ -1,25 +0,0 @@ -use steel::*; - -use super::OreAccount; - -/// Bus accounts are responsible for distributing mining rewards. There are 8 busses total -/// to minimize write-lock contention and allow Solana to process mine instructions in parallel. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] -pub struct Bus { - /// The ID of the bus account. - pub id: u64, - - /// The remaining rewards this bus has left to payout in the current epoch. - pub rewards: u64, - - /// The rewards this bus would have paid out in the current epoch if there no limit. - /// This is used to calculate the updated reward rate. - pub theoretical_rewards: u64, - - /// The largest known stake balance seen by the bus this epoch. - #[deprecated(since = "2.8.0", note = "Top balance is no longer tracked or used")] - pub top_balance: u64, -} - -account!(OreAccount, Bus); diff --git a/api/src/state/config.rs b/api/src/state/config.rs index c60dede..035e6f9 100644 --- a/api/src/state/config.rs +++ b/api/src/state/config.rs @@ -6,17 +6,20 @@ use super::OreAccount; #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)] pub struct Config { - /// The base reward rate paid out for a hash of minimum difficulty. - pub base_reward_rate: u64, - /// The timestamp of the last reset. pub last_reset_at: i64, - /// The minimum accepted difficulty. - pub min_difficulty: u64, + /// The best difficulty score of this epoch. + pub best_difficulty: u64, + + /// The proof of the best submitted hash of this epoch. + pub best_proof: Pubkey, + + /// The challenge of this epoch. + pub challenge: [u8; 32], /// The target emissions rate in ORE/min. - pub target_emmissions_rate: u64, + pub block_reward: u64, } account!(OreAccount, Config); diff --git a/api/src/state/mod.rs b/api/src/state/mod.rs index 20baa13..1017310 100644 --- a/api/src/state/mod.rs +++ b/api/src/state/mod.rs @@ -1,9 +1,7 @@ -mod bus; mod config; mod proof; mod treasury; -pub use bus::*; pub use config::*; pub use proof::*; pub use treasury::*; @@ -15,17 +13,11 @@ use crate::consts::*; #[repr(u8)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] pub enum OreAccount { - Bus = 100, Config = 101, Proof = 102, Treasury = 103, } -/// Fetch the PDA of a bus account. -pub fn bus_pda(id: u8) -> (Pubkey, u8) { - Pubkey::find_program_address(&[BUS, &[id]], &crate::id()) -} - /// Derive the PDA of the config account. pub fn config_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[CONFIG], &crate::id()) diff --git a/program/src/initialize.rs b/program/src/initialize.rs index 32fe733..c01579e 100644 --- a/program/src/initialize.rs +++ b/program/src/initialize.rs @@ -6,44 +6,12 @@ use steel::*; /// Initialize sets up the ORE program to begin mining. pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, bus_7_info, config_info, metadata_info, mint_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, metadata_program, rent_sysvar] = + let [signer_info, config_info, metadata_info, mint_info, treasury_info, treasury_tokens_info, system_program, token_program, associated_token_program, metadata_program, rent_sysvar] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS)?; - bus_0_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[0]], &ore_api::ID)?; - bus_1_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[1]], &ore_api::ID)?; - bus_2_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[2]], &ore_api::ID)?; - bus_3_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[3]], &ore_api::ID)?; - bus_4_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[4]], &ore_api::ID)?; - bus_5_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[5]], &ore_api::ID)?; - bus_6_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[6]], &ore_api::ID)?; - bus_7_info - .is_empty()? - .is_writable()? - .has_seeds(&[BUS, &[7]], &ore_api::ID)?; config_info .is_empty()? .is_writable()? @@ -71,25 +39,6 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program metadata_program.is_program(&mpl_token_metadata::ID)?; rent_sysvar.is_sysvar(&sysvar::rent::ID)?; - // Initialize bus accounts. - let bus_infos = [ - bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, - bus_7_info, - ]; - for i in 0..BUS_COUNT { - create_program_account::( - bus_infos[i], - system_program, - signer_info, - &ore_api::ID, - &[BUS, &[i as u8]], - )?; - let bus = bus_infos[i].as_account_mut::(&ore_api::ID)?; - bus.id = i as u64; - bus.rewards = 0; - bus.theoretical_rewards = 0; - } - // Initialize config. create_program_account::( config_info, @@ -99,9 +48,9 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program &[CONFIG], )?; let config = config_info.as_account_mut::(&ore_api::ID)?; - config.base_reward_rate = INITIAL_BASE_REWARD_RATE; + // config.base_reward_rate = INITIAL_BASE_REWARD_RATE; config.last_reset_at = 0; - config.min_difficulty = INITIAL_MIN_DIFFICULTY as u64; + // config.min_difficulty = INITIAL_MIN_DIFFICULTY as u64; // Initialize treasury. create_program_account::( diff --git a/program/src/mine.rs b/program/src/mine.rs index aa337c8..28ed483 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -1,14 +1,6 @@ -use std::mem::size_of; - use drillx::difficulty; use ore_api::prelude::*; -use ore_boost_api::{consts::DENOMINATOR_BPS, state::Config as BoostConfig}; -use solana_program::{ - keccak::{self, hashv}, - sanitize::SanitizeError, - serialize_utils::{read_pubkey, read_u16}, - slot_hashes::SlotHash, -}; +use solana_program::hash; use steel::*; /// Mine validates hashes and increments a miner's claimable balance. @@ -19,18 +11,13 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { // Load accounts. let clock = Clock::get()?; let t: i64 = clock.unix_timestamp; - let (required_accounts, boost_accounts) = accounts.split_at(6); - let [signer_info, bus_info, config_info, proof_info, instructions_sysvar, slot_hashes_sysvar] = - required_accounts - else { + let [signer_info, config_info, proof_info] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - let bus = bus_info.is_bus()?.as_account_mut::(&ore_api::ID)?; let config = config_info - .is_config()? - .as_account::(&ore_api::ID)? - .assert_err( + .as_account_mut::(&ore_api::ID)? + .assert_mut_err( |c| t < c.last_reset_at + EPOCH_DURATION, OreError::NeedsReset.into(), )?; @@ -40,201 +27,22 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { |p| p.miner == *signer_info.key, ProgramError::MissingRequiredSignature, )?; - instructions_sysvar.is_sysvar(&sysvar::instructions::ID)?; - slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; - - // Load boost accounts. - let [boost_config_info, boost_proof_info] = boost_accounts else { - return Err(ProgramError::NotEnoughAccountKeys); - }; - let boost_config = boost_config_info.as_account::(&ore_boost_api::ID)?; - let boost_proof = boost_proof_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|p| p.authority == *boost_config_info.key)?; - - // Authenticate the proof account. - // - // Only one proof account can be used for any given transaction. All `mine` instructions - // in the transaction must use the same proof account. - authenticate(&instructions_sysvar.data.borrow(), proof_info.key)?; - - // Reject spam transactions. - // - // Miners are rate limited to approximately 1 hash per minute. If a miner attempts to submit - // solutions more frequently than this, reject with an error. - let t_target = proof.last_hash_at + ONE_MINUTE; - let t_spam = t_target - TOLERANCE; - if t < t_spam { - return Err(OreError::Spam.into()); - } // Compute the hash. - // - // Here we use simple keccak. - let solution = keccak::hashv(&[proof.challenge.as_slice(), args.nonce.as_slice()]); + let solution = hash::hashv(&[ + args.nonce.as_slice(), + config.challenge.as_slice(), + proof.authority.to_bytes().as_slice(), + ]); - // Validate the hash satisfies the minimum difficulty. - // - // We use drillx to get the difficulty (leading zeros) of the hash. If the hash does not have the - // minimum required difficulty, we reject it with an error. - let difficulty = difficulty(solution.0); - if difficulty < config.min_difficulty as u32 { - return Err(OreError::HashTooEasy.into()); + // Get the difficulty score. + let difficulty = difficulty(solution.to_bytes()); + + // Update the best solution. + if difficulty as u64 > config.best_difficulty { + config.best_difficulty = difficulty as u64; + config.best_proof = *proof_info.key; } - // Normalize the difficulty and calculate the gross reward amount. - // - // The reward doubles for every bit of difficulty (leading zeros) on the hash. We use the normalized - // difficulty so the minimum accepted difficulty pays out at the base reward rate. - let normalized_difficulty = difficulty - config.min_difficulty as u32; - let gross_reward = config.base_reward_rate * 2u64.checked_pow(normalized_difficulty).unwrap(); - - // Apply liveness penalty. - // - // The liveness penalty exists to ensure there is no "dark" hashpower on the network. It - // should not be possible to spend an excessively long time on a given challenge and submit a hash - // with a large difficulty score to earn an outsized reward. - // - // The liveness penalty works by halving the reward amount for every minute a solution has been submitted late. - // This ultimately drives the reward to zero given enough time (10-20 minutes). - let mut gross_penalized_reward = gross_reward; - let t_liveness = t_target + TOLERANCE; - if t > t_liveness { - // Halve the reward for every minute late. - let secs_late = t.saturating_sub(t_target) as u64; - let mins_late = secs_late.saturating_div(ONE_MINUTE as u64); - if mins_late > 0 { - gross_penalized_reward = - gross_reward.saturating_div(2u64.saturating_pow(mins_late as u32)); - } - - // Linear decay with remainder seconds. - let remainder_secs = secs_late.saturating_sub(mins_late.saturating_mul(ONE_MINUTE as u64)); - if remainder_secs > 0 && gross_penalized_reward > 0 { - let penalty = gross_penalized_reward - .saturating_div(2) - .saturating_mul(remainder_secs) - .saturating_div(ONE_MINUTE as u64); - gross_penalized_reward = gross_penalized_reward.saturating_sub(penalty); - } - } - - // Apply bus limit. - // - // Busses are limited to distributing the target emissions rate per epoch. The payout amount must be capped to whatever is - // left in the selected bus. This limits the maximum amount that will be paid out for any given hash to the target emissions rate. - let net_reward = gross_penalized_reward - .min(bus.rewards) - .min(config.target_emmissions_rate); - - // Split the net reward between the miner and stakers. - let net_boost_reward = - (net_reward as u128 * boost_config.take_rate as u128 / DENOMINATOR_BPS as u128) as u64; - let net_miner_reward = net_reward - net_boost_reward; - - // Sanity check the rewards. - assert_eq!(net_reward, net_miner_reward + net_boost_reward); - - // Update bus balances. - // - // We track the theoretical rewards that would have been paid out ignoring the bus limit, so the - // base reward rate will be updated to account for the real hashpower on the network. - bus.theoretical_rewards += gross_penalized_reward; - bus.rewards -= net_reward; - - // Update staker balances. - boost_proof.balance += net_boost_reward; - boost_proof.total_rewards += net_boost_reward; - - // Update miner balances. - proof.balance += net_miner_reward; - - // Hash a recent slot hash into the next challenge to prevent pre-mining attacks. - // - // The slot hashes are unpredictable values. By seeding the next challenge with the most recent slot hash, - // miners are forced to submit their current solution before they can begin mining for the next. - proof.last_hash = solution.0; - proof.challenge = hashv(&[ - solution.0.as_slice(), - &slot_hashes_sysvar.data.borrow()[0..size_of::()], - ]) - .0; - - // Update stats. - let prev_last_hash_at = proof.last_hash_at; - proof.last_hash_at = t.max(t_target); - proof.total_hashes += 1; - proof.total_rewards += net_miner_reward; - - // Log data. - // - // The boost rewards are scaled down before logging to account for penalties and bus limits. - // This return data can be used by pool operators to calculate miner and staker rewards. - MineEvent { - balance: proof.balance, - difficulty: difficulty as u64, - last_hash_at: prev_last_hash_at, - timing: t - t_liveness, - net_reward, - net_base_reward: net_miner_reward, - net_miner_boost_reward: 0, - net_staker_boost_reward: net_boost_reward, - } - .log_return(); - Ok(()) } - -/// Authenticate the proof account. -/// -/// This process is necessary to prevent sybil attacks. If a user can pack multiple hashes into a single -/// transaction, then there is a financial incentive to mine across multiple keypairs and submit as many hashes -/// as possible in the same transaction to minimize fee / hash. -/// -/// We prevent this by forcing every transaction to declare upfront the proof account that will be used for mining. -/// The authentication process includes passing the 32 byte pubkey address as instruction data to a CU-optimized noop -/// program. We parse this address through transaction introspection and use it to ensure the same proof account is -/// used for every `mine` instruction in a given transaction. -fn authenticate(data: &[u8], proof_address: &Pubkey) -> ProgramResult { - if let Ok(Some(auth_address)) = parse_auth_address(data) { - if proof_address.ne(&auth_address) { - return Err(OreError::AuthFailed.into()); - } - } else { - return Err(OreError::AuthFailed.into()); - } - Ok(()) -} - -/// Use transaction introspection to parse the authenticated pubkey. -fn parse_auth_address(data: &[u8]) -> Result, SanitizeError> { - // Start the current byte index at 0 - let mut curr = 0; - let num_instructions = read_u16(&mut curr, data)?; - let pc = curr; - - // Iterate through the transaction instructions - for i in 0..num_instructions as usize { - // Shift pointer to correct positition - curr = pc + i * 2; - curr = read_u16(&mut curr, data)? as usize; - - // Skip accounts - let num_accounts = read_u16(&mut curr, data)? as usize; - curr += num_accounts * 33; - - // Read the instruction program id - let program_id = read_pubkey(&mut curr, data)?; - - // Introspect on the first noop instruction - if program_id.eq(&NOOP_PROGRAM_ID) { - // Return address read from instruction data - curr += 2; - let address = read_pubkey(&mut curr, data)?; - return Ok(Some(address)); - } - } - - // Default return none - Ok(None) -} diff --git a/program/src/reset.rs b/program/src/reset.rs index b34df7e..bb196f7 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -1,39 +1,16 @@ use ore_api::prelude::*; +use solana_program::{hash::hashv, slot_hashes::SlotHash}; use steel::*; /// Reset tops up the bus balances and updates the emissions and reward rates. pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { // Load accounts. - let [signer_info, bus_0_info, bus_1_info, bus_2_info, bus_3_info, bus_4_info, bus_5_info, bus_6_info, bus_7_info, config_info, mint_info, treasury_info, treasury_tokens_info, token_program] = + let [signer_info, config_info, mint_info, proof_info, treasury_info, treasury_tokens_info, token_program, slot_hashes_sysvar] = accounts else { return Err(ProgramError::NotEnoughAccountKeys); }; signer_info.is_signer()?; - let bus_0 = bus_0_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 0)?; - let bus_1 = bus_1_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 1)?; - let bus_2 = bus_2_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 2)?; - let bus_3 = bus_3_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 3)?; - let bus_4 = bus_4_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 4)?; - let bus_5 = bus_5_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 5)?; - let bus_6 = bus_6_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 6)?; - let bus_7 = bus_7_info - .as_account_mut::(&ore_api::ID)? - .assert_mut(|b| b.id == 7)?; let config = config_info .is_config()? .as_account_mut::(&ore_api::ID)?; @@ -41,9 +18,13 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul .has_address(&MINT_ADDRESS)? .is_writable()? .as_mint()?; + let proof = proof_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|p| p.authority == config.best_proof)?; treasury_info.is_treasury()?.is_writable()?; treasury_tokens_info.is_treasury_tokens()?.is_writable()?; token_program.is_program(&spl_token::ID)?; + slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; // Validate enough time has passed since the last reset. let clock = Clock::get()?; @@ -56,8 +37,18 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul } // Process epoch. - let busses = [bus_0, bus_1, bus_2, bus_3, bus_4, bus_5, bus_6, bus_7]; - let amount_to_mint = config.process_epoch(busses, &clock, &mint)?; + config.block_reward = get_block_reward(mint.supply()); + config.best_proof = Pubkey::default(); + config.best_difficulty = 0; + config.last_reset_at = clock.unix_timestamp; + config.challenge = hashv(&[ + config.challenge.as_slice(), + &slot_hashes_sysvar.data.borrow()[0..size_of::()], + ]) + .to_bytes(); + + // Update proof balance. + proof.balance += config.block_reward; // Fund the treasury token account. mint_to_signed( @@ -65,120 +56,17 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul treasury_tokens_info, treasury_info, token_program, - amount_to_mint, + config.block_reward, &[TREASURY], )?; Ok(()) } -trait EpochProcessor { - fn process_epoch( - &mut self, - busses: [&mut Bus; 8], - clock: &Clock, - mint: &Mint, - ) -> Result; -} - -impl EpochProcessor for Config { - fn process_epoch( - &mut self, - busses: [&mut Bus; 8], - clock: &Clock, - mint: &Mint, - ) -> Result { - // Max supply check. - if mint.supply() >= MAX_SUPPLY { - return Err(OreError::MaxSupply.into()); - } - - // Update timestamp. - self.last_reset_at = clock.unix_timestamp; - - // Adjust emissions curve based on current supply. - self.target_emmissions_rate = get_target_emissions_rate(mint.supply()); - - // Calculate target rewards to distribute in coming epoch (emissions rate multiplied by epoch duration). - let target_epoch_rewards = self.target_emmissions_rate * EPOCH_MINUTES as u64; - - // Reset bus counters and calculate theoretical rewards mined in the last epoch. - let mut amount_to_mint = 0u64; - let mut remaining_supply = MAX_SUPPLY.saturating_sub(mint.supply()); - let mut theoretical_epoch_rewards = 0u64; - for bus in busses { - // Reset theoretical rewards. - theoretical_epoch_rewards += bus.theoretical_rewards; - bus.theoretical_rewards = 0; - - // Reset bus rewards. - let topup_amount = target_epoch_rewards - .saturating_sub(bus.rewards) - .min(remaining_supply); - remaining_supply -= topup_amount; - amount_to_mint += topup_amount; - bus.rewards += topup_amount; - } - - // Update base reward rate for next epoch. - self.base_reward_rate = calculate_new_reward_rate( - self.base_reward_rate, - theoretical_epoch_rewards, - target_epoch_rewards, - ); - - // If base reward rate is too low, increment min difficulty by 1 and double base reward rate. - if self.base_reward_rate < BASE_REWARD_RATE_MIN_THRESHOLD { - self.min_difficulty += 1; - self.base_reward_rate *= 2; - } - - // If base reward rate is too high, decrement min difficulty by 1 and halve base reward rate. - if self.base_reward_rate >= BASE_REWARD_RATE_MAX_THRESHOLD && self.min_difficulty > 1 { - self.min_difficulty -= 1; - self.base_reward_rate /= 2; - } - - Ok(amount_to_mint) - } -} - -/// This function calculates what the new reward rate should be based on how many total rewards -/// were mined in the prior epoch. The math is largely identitical to function used by the Bitcoin -/// network to update the difficulty between each epoch. -/// -/// new_rate = current_rate * (target_rewards / actual_rewards) -/// -/// The new rate is then smoothed by a constant factor to avoid large fluctuations. In Ore's case, -/// the epochs are short (60 seconds) so a smoothing factor of 2 has been chosen. That is, the reward rate -/// can at most double or halve from one epoch to the next. -pub(crate) fn calculate_new_reward_rate( - current_rate: u64, - epoch_rewards: u64, - target_epoch_rewards: u64, -) -> u64 { - // Avoid division by zero. Leave the reward rate unchanged, if detected. - if epoch_rewards.eq(&0) { - return current_rate; - } - - // Calculate new reward rate. - let new_rate = (current_rate as u128) - .saturating_mul(target_epoch_rewards as u128) - .saturating_div(epoch_rewards as u128) as u64; - - // Smooth reward rate so it cannot change by more than a constant factor from one epoch to the next. - let new_rate_min = current_rate.saturating_div(SMOOTHING_FACTOR); - let new_rate_max = current_rate.saturating_mul(SMOOTHING_FACTOR); - let new_rate_smoothed = new_rate.min(new_rate_max).max(new_rate_min); - - // Prevent reward rate from dropping below 1 or exceeding BUS_EPOCH_REWARDS and return. - new_rate_smoothed.max(1).min(target_epoch_rewards) -} - -/// This function calculates the target emissions rate (ORE / min) based on the current supply. -/// It is designed to reduce emissions by 10% approximately every 12 months with a hardcap at 5 million ORE. -pub(crate) fn get_target_emissions_rate(current_supply: u64) -> u64 { +/// This function calculates the block reward (ORE / min) based on the current supply. +/// It is designed to reduce emissions by 10% approximately every 12 months with a hard stop at 5 million ORE. +pub(crate) fn get_block_reward(current_supply: u64) -> u64 { + let max_supply = ONE_ORE * 5_000_000; match current_supply { n if n < ONE_ORE * 525_600 => 100_000_000_000, // Year ~1 n if n < ONE_ORE * 998_640 => 90_000_000_000, // Year ~2 @@ -208,420 +96,52 @@ pub(crate) fn get_target_emissions_rate(current_supply: u64) -> u64 { n if n < ONE_ORE * 4_916_405 => 7_178_979_874, // Year ~26 n if n < ONE_ORE * 4_950_365 => 6_461_081_886, // Year ~27 n if n < ONE_ORE * 4_980_928 => 5_814_973_607, // Year ~28 - n if n < ONE_ORE * 5_000_000 => 5_233_476_327, // Year ~29 + n if n < max_supply => 5_233_476_327.min(max_supply - current_supply), // Year ~29 _ => 0, } } #[cfg(test)] mod tests { - use rand::{distributions::Uniform, Rng}; - use solana_program::program_option::COption; - use steel::{Clock, Mint}; - - use crate::{calculate_new_reward_rate, reset::EpochProcessor}; - use ore_api::{ - consts::{ - BASE_REWARD_RATE_MIN_THRESHOLD, BUS_COUNT, EPOCH_MINUTES, ONE_ORE, SMOOTHING_FACTOR, - TOKEN_DECIMALS, - }, - state::{Bus, Config}, - }; - - const FUZZ_SIZE: u64 = 10_000; - const TARGET_EPOCH_REWARDS: u64 = ONE_ORE * EPOCH_MINUTES as u64; - const MAX_EPOCH_REWARDS: u64 = TARGET_EPOCH_REWARDS * BUS_COUNT as u64; + use super::*; #[test] - fn test_calculate_new_reward_rate_target() { - let current_rate = 1000; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate)); + fn test_block_reward_max_supply() { + let max_supply = ONE_ORE * 5_000_000; + + // Test at max supply + assert_eq!(get_block_reward(max_supply), 0); + + // Test slightly below max supply + let near_max = max_supply - 1; + assert_eq!(get_block_reward(near_max), 1); + + // Test at max supply - 1000 + let below_max = max_supply - 1000; + assert_eq!(get_block_reward(below_max), 1000); + + // Test that reward never exceeds remaining supply + let supply_4_999_990 = ONE_ORE * 4_999_990; + assert!(get_block_reward(supply_4_999_990) <= max_supply - supply_4_999_990); } #[test] - fn test_calculate_new_reward_rate_div_by_zero() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 0, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate)); + fn test_block_reward_boundaries() { + // Test first tier boundary + let year1_supply = ONE_ORE * 525_599; + assert_eq!(get_block_reward(year1_supply), 100_000_000_000); + + // Test middle tier boundary + let year15_supply = ONE_ORE * 4_173_835; + assert_eq!(get_block_reward(year15_supply), 22_876_792_454); + + // Test last tier boundary before max supply logic + let last_tier_supply = ONE_ORE * 4_980_927; + assert_eq!(get_block_reward(last_tier_supply), 5_814_973_607); } #[test] - fn test_calculate_new_reward_rate_lower() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate( - current_rate, - TARGET_EPOCH_REWARDS.saturating_add(10_000_000_000), - TARGET_EPOCH_REWARDS, - ); - assert!(new_rate.lt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_lower_edge() { - let current_rate = BASE_REWARD_RATE_MIN_THRESHOLD; - let new_rate = - calculate_new_reward_rate(current_rate, TARGET_EPOCH_REWARDS + 1, TARGET_EPOCH_REWARDS); - assert!(new_rate.lt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_lower_fuzz() { - let mut rng = rand::thread_rng(); - for _ in 0..FUZZ_SIZE { - let current_rate: u64 = rng.sample(Uniform::new(1, TARGET_EPOCH_REWARDS)); - let actual_rewards: u64 = - rng.sample(Uniform::new(TARGET_EPOCH_REWARDS, MAX_EPOCH_REWARDS)); - let new_rate = - calculate_new_reward_rate(current_rate, actual_rewards, TARGET_EPOCH_REWARDS); - assert!(new_rate.lt(¤t_rate)); - } - } - - #[test] - fn test_calculate_new_reward_rate_higher() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate( - current_rate, - TARGET_EPOCH_REWARDS.saturating_sub(10_000_000_000), - TARGET_EPOCH_REWARDS, - ); - assert!(new_rate.gt(¤t_rate)); - } - - #[test] - fn test_calculate_new_reward_rate_higher_fuzz() { - let mut rng = rand::thread_rng(); - for _ in 0..FUZZ_SIZE { - let current_rate: u64 = rng.sample(Uniform::new(1, TARGET_EPOCH_REWARDS)); - let actual_rewards: u64 = rng.sample(Uniform::new(1, TARGET_EPOCH_REWARDS)); - let new_rate = - calculate_new_reward_rate(current_rate, actual_rewards, TARGET_EPOCH_REWARDS); - assert!(new_rate.gt(¤t_rate)); - } - } - - #[test] - fn test_calculate_new_reward_rate_max_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, 1, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate.saturating_mul(SMOOTHING_FACTOR))); - } - - #[test] - fn test_calculate_new_reward_rate_min_smooth() { - let current_rate = 1000; - let new_rate = calculate_new_reward_rate(current_rate, u64::MAX, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(¤t_rate.saturating_div(SMOOTHING_FACTOR))); - } - - #[test] - fn test_calculate_new_reward_rate_max_inputs() { - let new_rate = calculate_new_reward_rate( - TARGET_EPOCH_REWARDS, - MAX_EPOCH_REWARDS, - TARGET_EPOCH_REWARDS, - ); - assert!(new_rate.eq(&TARGET_EPOCH_REWARDS.saturating_div(SMOOTHING_FACTOR))); - } - - #[test] - fn test_calculate_new_reward_rate_min_inputs() { - let new_rate = calculate_new_reward_rate(1, 1, TARGET_EPOCH_REWARDS); - assert!(new_rate.eq(&1u64.saturating_mul(SMOOTHING_FACTOR))); - } - - #[allow(deprecated)] - #[test] - fn test_process_epoch_simple() { - let mut config = Config { - base_reward_rate: 1024, - last_reset_at: 0, - min_difficulty: 1, - target_emmissions_rate: ONE_ORE, - }; - let bus_0 = &mut Bus { - id: 0, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_1 = &mut Bus { - id: 1, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_2 = &mut Bus { - id: 2, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_3 = &mut Bus { - id: 3, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_4 = &mut Bus { - id: 4, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_5 = &mut Bus { - id: 5, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_6 = &mut Bus { - id: 6, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_7 = &mut Bus { - id: 7, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let busses = [bus_0, bus_1, bus_2, bus_3, bus_4, bus_5, bus_6, bus_7]; - let clock = Clock::default(); - let mint = Mint::V0(spl_token::state::Mint { - mint_authority: COption::None, - supply: ONE_ORE * 100, - decimals: TOKEN_DECIMALS, - is_initialized: true, - freeze_authority: COption::None, - }); - - let amount_to_mint = config.process_epoch(busses, &clock, &mint).unwrap(); - assert_eq!(config.target_emmissions_rate, ONE_ORE); - assert_eq!( - ONE_ORE * EPOCH_MINUTES as u64 * BUS_COUNT as u64, - amount_to_mint - ); - } - - #[allow(deprecated)] - #[test] - fn test_process_epoch_emissions_boundary() { - let mut config = Config { - base_reward_rate: 1024, - last_reset_at: 0, - min_difficulty: 1, - target_emmissions_rate: ONE_ORE, - }; - let bus_0 = &mut Bus { - id: 0, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_1 = &mut Bus { - id: 1, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_2 = &mut Bus { - id: 2, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_3 = &mut Bus { - id: 3, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_4 = &mut Bus { - id: 4, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_5 = &mut Bus { - id: 5, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_6 = &mut Bus { - id: 6, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_7 = &mut Bus { - id: 7, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let busses = [bus_0, bus_1, bus_2, bus_3, bus_4, bus_5, bus_6, bus_7]; - let clock = Clock::default(); - let mint = Mint::V0(spl_token::state::Mint { - mint_authority: COption::None, - supply: ONE_ORE * 525_600, - decimals: TOKEN_DECIMALS, - is_initialized: true, - freeze_authority: COption::None, - }); - - let amount_to_mint = config.process_epoch(busses, &clock, &mint).unwrap(); - assert_eq!(config.target_emmissions_rate, 90_000_000_000); - assert_eq!( - 90_000_000_000 * EPOCH_MINUTES as u64 * BUS_COUNT as u64, - amount_to_mint - ); - } - - #[allow(deprecated)] - #[test] - fn test_process_epoch_max_supply() { - let mut config = Config { - base_reward_rate: 1024, - last_reset_at: 0, - min_difficulty: 1, - target_emmissions_rate: 5_233_476_327, - }; - let bus_0 = &mut Bus { - id: 0, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_1 = &mut Bus { - id: 1, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_2 = &mut Bus { - id: 2, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_3 = &mut Bus { - id: 3, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_4 = &mut Bus { - id: 4, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_5 = &mut Bus { - id: 5, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_6 = &mut Bus { - id: 6, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_7 = &mut Bus { - id: 7, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let busses = [bus_0, bus_1, bus_2, bus_3, bus_4, bus_5, bus_6, bus_7]; - let clock = Clock::default(); - let mint = Mint::V0(spl_token::state::Mint { - mint_authority: COption::None, - supply: ONE_ORE * 4_999_999, - decimals: TOKEN_DECIMALS, - is_initialized: true, - freeze_authority: COption::None, - }); - - let amount_to_mint = config.process_epoch(busses, &clock, &mint).unwrap(); - assert_eq!(config.target_emmissions_rate, 5_233_476_327); - assert_eq!(ONE_ORE, amount_to_mint); - } - - #[allow(deprecated)] - #[test] - fn test_process_epoch_zero_emissions() { - let mut config = Config { - base_reward_rate: 1024, - last_reset_at: 0, - min_difficulty: 1, - target_emmissions_rate: 5_233_476_327, - }; - let bus_0 = &mut Bus { - id: 0, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_1 = &mut Bus { - id: 1, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_2 = &mut Bus { - id: 2, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_3 = &mut Bus { - id: 3, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_4 = &mut Bus { - id: 4, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_5 = &mut Bus { - id: 5, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_6 = &mut Bus { - id: 6, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let bus_7 = &mut Bus { - id: 7, - rewards: 0, - theoretical_rewards: 0, - top_balance: 0, - }; - let busses = [bus_0, bus_1, bus_2, bus_3, bus_4, bus_5, bus_6, bus_7]; - let clock = Clock::default(); - let mint = Mint::V0(spl_token::state::Mint { - mint_authority: COption::None, - supply: ONE_ORE * 5_000_000, - decimals: TOKEN_DECIMALS, - is_initialized: true, - freeze_authority: COption::None, - }); - - let amount_to_mint = config.process_epoch(busses, &clock, &mint); - assert!(amount_to_mint.is_err()); + fn test_block_reward_zero_supply() { + assert_eq!(get_block_reward(0), 100_000_000_000); } }