From 70a9d38e4d4ee64104972303210ba7c7650ee417 Mon Sep 17 00:00:00 2001 From: Hardhat Chad <155858888+HardhatChad@users.noreply.github.com> Date: Sun, 19 Jan 2025 19:07:16 -0600 Subject: [PATCH] Global Boost (#112) * update accounting logic for global boosts * rename vars * update event * const * deprecate top balance * handle div by zero * cast to u128 * debug logs * fix proof parser * debug logs * update boost sdk * remove debug logs * import * cleanup sdk * silent error * debug logs * more logs * boost math * debug log * log timing * debug logs * alt model * alt model * refine sdk * fix optional account parser * debug logs * boost keys * update sdk * remove debug logs * mainnet program id * cleanup * update metadata * update deps --- Cargo.lock | 31 +++++++-- Cargo.toml | 6 +- api/src/event.rs | 8 +-- api/src/sdk.rs | 15 +++-- api/src/state/config.rs | 1 + api/src/state/proof.rs | 1 + program/src/initialize.rs | 1 - program/src/mine.rs | 136 +++++++++++++++++++------------------- program/src/open.rs | 1 - program/src/reset.rs | 3 - 10 files changed, 112 insertions(+), 91 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b74ee41..17edd33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1321,7 +1321,27 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "ore-api" -version = "2.7.0" +version = "3.0.0" +dependencies = [ + "array-const-fn-init", + "bytemuck", + "const-crypto", + "drillx", + "mpl-token-metadata", + "num_enum", + "solana-program", + "spl-associated-token-account", + "spl-token", + "static_assertions", + "steel", + "thiserror", +] + +[[package]] +name = "ore-api" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fe81730f32cc3e75a1dda6ed6aa824d6bab2622867a135d4173851fda40f14a" dependencies = [ "array-const-fn-init", "bytemuck", @@ -1339,14 +1359,15 @@ dependencies = [ [[package]] name = "ore-boost-api" -version = "0.3.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce6c36c89d829a4b36debc127625513aaf668ed8f0eb4982493fdc3b6b004e9" +checksum = "0bc00f3a2ccc8504446d9170854e8e040e4292fe7b74a470b9b6c169968c70f9" dependencies = [ "array-const-fn-init", "bytemuck", "const-crypto", "num_enum", + "ore-api 3.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "solana-program", "spl-associated-token-account", "spl-token", @@ -1357,11 +1378,11 @@ dependencies = [ [[package]] name = "ore-program" -version = "2.7.0" +version = "3.0.0" dependencies = [ "drillx", "mpl-token-metadata", - "ore-api", + "ore-api 3.0.0", "ore-boost-api", "rand 0.8.5", "solana-include-idl", diff --git a/Cargo.toml b/Cargo.toml index dd48f32..3413e64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["api", "program"] [workspace.package] -version = "2.7.0" +version = "3.0.0" edition = "2021" license = "Apache-2.0" homepage = "https://ore.supply" @@ -19,8 +19,8 @@ const-crypto = "0.1.0" drillx = { version = "2.0.0", features = ["solana"] } mpl-token-metadata = "4.1.2" num_enum = "0.7.2" -ore-api = { path = "api", version = "2.1.9" } -ore-boost-api = "0.3" +ore-api = { path = "api" } +ore-boost-api = "1.1" solana-program = "^1.18" spl-token = { version = "^4", features = ["no-entrypoint"] } spl-associated-token-account = { version = "^2.3", features = [ "no-entrypoint" ] } diff --git a/api/src/event.rs b/api/src/event.rs index b79abe5..8e39bca 100644 --- a/api/src/event.rs +++ b/api/src/event.rs @@ -7,10 +7,10 @@ pub struct MineEvent { pub difficulty: u64, pub last_hash_at: i64, pub timing: i64, - pub reward: u64, - pub boost_1: u64, - pub boost_2: u64, - pub boost_3: u64, + pub net_reward: u64, + pub net_base_reward: u64, + pub net_miner_boost_reward: u64, + pub net_staker_boost_reward: u64, } event!(MineEvent); diff --git a/api/src/sdk.rs b/api/src/sdk.rs index fc9a85d..2c634dc 100644 --- a/api/src/sdk.rs +++ b/api/src/sdk.rs @@ -56,10 +56,10 @@ pub fn mine( authority: Pubkey, bus: Pubkey, solution: Solution, - boost_accounts: Vec, + boost_keys: Option<(Pubkey, Pubkey)>, ) -> Instruction { let proof = proof_pda(authority).0; - let required_accounts = vec![ + let mut accounts = vec![ AccountMeta::new(signer, true), AccountMeta::new(bus, false), AccountMeta::new_readonly(CONFIG_ADDRESS, false), @@ -67,13 +67,14 @@ pub fn mine( AccountMeta::new_readonly(sysvar::instructions::ID, false), AccountMeta::new_readonly(sysvar::slot_hashes::ID, false), ]; - let optional_accounts = boost_accounts - .into_iter() - .map(|pk| AccountMeta::new_readonly(pk, false)) - .collect(); + if let Some((boost_address, reservation_address)) = boost_keys { + accounts.push(AccountMeta::new_readonly(boost_address, false)); + accounts.push(AccountMeta::new(proof_pda(boost_address).0, false)); + accounts.push(AccountMeta::new_readonly(reservation_address, false)); + } Instruction { program_id: crate::ID, - accounts: [required_accounts, optional_accounts].concat(), + accounts, data: Mine { digest: solution.d, nonce: solution.n, diff --git a/api/src/state/config.rs b/api/src/state/config.rs index 52b21cc..03793ac 100644 --- a/api/src/state/config.rs +++ b/api/src/state/config.rs @@ -16,6 +16,7 @@ pub struct Config { pub min_difficulty: u64, /// The largest known stake balance on the network from the last epoch. + #[deprecated(since = "2.4.0", note = "Please stake with the boost program")] pub top_balance: u64, } diff --git a/api/src/state/proof.rs b/api/src/state/proof.rs index 733e115..15bd42d 100644 --- a/api/src/state/proof.rs +++ b/api/src/state/proof.rs @@ -23,6 +23,7 @@ pub struct Proof { pub last_hash_at: i64, /// The last time stake was deposited into this account. + #[deprecated(since = "2.4.0", note = "Please stake with the boost program")] pub last_stake_at: i64, /// The keypair which has permission to submit hashes for mining. diff --git a/program/src/initialize.rs b/program/src/initialize.rs index 364a666..f28f04b 100644 --- a/program/src/initialize.rs +++ b/program/src/initialize.rs @@ -103,7 +103,6 @@ pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> Program config.base_reward_rate = INITIAL_BASE_REWARD_RATE; config.last_reset_at = 0; config.min_difficulty = INITIAL_MIN_DIFFICULTY as u64; - config.top_balance = 0; // Initialize treasury. create_account::( diff --git a/program/src/mine.rs b/program/src/mine.rs index aa87c37..ec001fb 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -2,7 +2,7 @@ use std::mem::size_of; use drillx::Solution; use ore_api::prelude::*; -use ore_boost_api::state::{Boost, Stake}; +use ore_boost_api::{consts::BOOST_DENOMINATOR, state::{Boost, Reservation}}; #[allow(deprecated)] use solana_program::{ keccak::hashv, @@ -86,55 +86,33 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let normalized_difficulty = difficulty .checked_sub(config.min_difficulty as u32) .unwrap(); - let mut reward = config + let base_reward = config .base_reward_rate .checked_mul(2u64.checked_pow(normalized_difficulty).unwrap()) .unwrap(); // Apply boosts. // - // Boosts are staking incentives that can multiply a miner's rewards. Up to 3 boosts can be applied - // on any given mine operation. - let base_reward = reward; - let mut boost_rewards = [0u64; 3]; - let mut applied_boosts = [Pubkey::new_from_array([0; 32]); 3]; - for i in 0..3 { - if optional_accounts.len().gt(&(i * 2)) { - // Load optional accounts. - let boost_info = optional_accounts[i * 2].clone(); - let stake_info = optional_accounts[i * 2 + 1].clone(); - let boost = boost_info.as_account::(&ore_boost_api::ID)?; - let stake = stake_info - .as_account::(&ore_boost_api::ID)? - .assert(|s| s.authority == proof.authority)? - .assert(|s| s.boost == *boost_info.key)?; + // Boosts are staking incentives that can multiply a miner's rewards. The boost rewards are + // split between the miner and staker. + let mut boost_reward = 0; + if let [boost_info, _boost_proof_info, reservation_info] = optional_accounts { + // Load boost accounts. + let boost = boost_info.as_account::(&ore_boost_api::ID)?; + reservation_info + .as_account::(&ore_boost_api::ID)? + .assert(|r| r.authority == *proof_info.key)? + .assert(|r| r.boost == *boost_info.key)? + .assert(|r| r.ts == proof.last_hash_at)?; - // Skip if boost is applied twice. - if applied_boosts.contains(boost_info.key) { - continue; - } - - // Record this boost has been used. - applied_boosts[i] = *boost_info.key; - - // Apply multiplier if boost is not expired and last stake at was more than one minute ago. - if boost.expires_at.gt(&t) - && boost.total_stake.gt(&0) - && stake.last_stake_at.saturating_add(ONE_MINUTE).le(&t) - { - let multiplier = boost.multiplier.checked_sub(1).unwrap(); - let boost_reward = (base_reward as u128) - .checked_mul(multiplier as u128) - .unwrap() - .checked_mul(stake.balance as u128) - .unwrap() - .checked_div(boost.total_stake as u128) - .unwrap() as u64; - reward = reward.checked_add(boost_reward).unwrap(); - - // Push boost event - boost_rewards[i] = boost_reward; - } + // Apply multiplier if boost is unlocked and not expired. + if boost.expires_at > t && boost.locked == 0 + { + boost_reward = (base_reward as u128) + .checked_mul(boost.multiplier as u128) + .unwrap() + .checked_div(BOOST_DENOMINATOR as u128) + .unwrap() as u64; } } @@ -146,24 +124,25 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { // // The penalty works by halving the reward amount for every minute late the solution has been submitted. // This ultimately drives the reward to zero given enough time (10-20 minutes). - let reward_pre_penalty = reward; + let gross_reward = base_reward.checked_add(boost_reward).unwrap(); + let mut gross_penalized_reward = gross_reward; let t_liveness = t_target.saturating_add(TOLERANCE); - if t.gt(&t_liveness) { + 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.gt(&0) { - reward = reward.saturating_div(2u64.saturating_pow(mins_late as u32)); + 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.gt(&0) && reward.gt(&0) { - let penalty = reward + 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); - reward = reward.saturating_sub(penalty); + gross_penalized_reward = gross_penalized_reward.saturating_sub(penalty); } } @@ -171,15 +150,45 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { // // Busses are limited to distributing 1 ORE 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 1 ORE. - let reward_actual = reward.min(bus.rewards).min(ONE_ORE); + let net_reward = gross_penalized_reward.min(bus.rewards).min(ONE_ORE); - // Update balances. + // Scale the base and boost rewards to account for penalties. + let net_base_reward = if gross_reward > 0 { + (net_reward as u128) + .checked_mul(base_reward as u128) + .unwrap() + .checked_div(gross_reward as u128) + .unwrap() as u64 + } else { + 0 + }; + let net_boost_reward = net_reward.checked_sub(net_base_reward).unwrap(); + + // Split the boost rewards between miner and staker. + let net_staker_boost_reward = net_boost_reward.checked_div(2).unwrap(); + let net_miner_boost_reward = net_boost_reward.checked_sub(net_staker_boost_reward).unwrap(); + let net_miner_reward = net_base_reward.checked_add(net_miner_boost_reward).unwrap(); + + // 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 = bus.theoretical_rewards.checked_add(reward).unwrap(); - bus.rewards = bus.rewards.checked_sub(reward_actual).unwrap(); - proof.balance = proof.balance.checked_add(reward_actual).unwrap(); + bus.theoretical_rewards = bus.theoretical_rewards.checked_add(gross_penalized_reward).unwrap(); + bus.rewards = bus.rewards.checked_sub(net_reward).unwrap(); + + // Update miner balances. + proof.balance = proof.balance.checked_add(net_miner_reward).unwrap(); + + // Update staker balances. + if net_staker_boost_reward > 0 { + if let [boost_info, boost_proof_info, _reservation_info] = optional_accounts { + let boost_proof = boost_proof_info + .as_account_mut::(&ore_api::ID)? + .assert_mut(|p| p.authority == *boost_info.key)?; + boost_proof.balance = boost_proof.balance.checked_add(net_staker_boost_reward).unwrap(); + boost_proof.total_rewards = boost_proof.total_rewards.checked_add(net_staker_boost_reward).unwrap(); + } + } // Hash a recent slot hash into the next challenge to prevent pre-mining attacks. // @@ -196,28 +205,21 @@ pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { let prev_last_hash_at = proof.last_hash_at; proof.last_hash_at = t.max(t_target); proof.total_hashes = proof.total_hashes.saturating_add(1); - proof.total_rewards = proof.total_rewards.saturating_add(reward_actual); + proof.total_rewards = proof.total_rewards.saturating_add(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. - for i in 0..3 { - boost_rewards[i] = (boost_rewards[i] as u128) - .checked_mul(reward_actual as u128) - .unwrap() - .checked_div(reward_pre_penalty as u128) - .unwrap() as u64; - } MineEvent { balance: proof.balance, difficulty: difficulty as u64, last_hash_at: prev_last_hash_at, timing: t.saturating_sub(t_liveness), - reward: reward_actual, - boost_1: boost_rewards[0], - boost_2: boost_rewards[1], - boost_3: boost_rewards[2], + net_reward, + net_base_reward, + net_miner_boost_reward, + net_staker_boost_reward, } .log_return(); diff --git a/program/src/open.rs b/program/src/open.rs index 4eec85a..1824f77 100644 --- a/program/src/open.rs +++ b/program/src/open.rs @@ -40,7 +40,6 @@ pub fn process_open(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult .0; proof.last_hash = [0; 32]; proof.last_hash_at = clock.unix_timestamp; - proof.last_stake_at = clock.unix_timestamp; proof.miner = *miner_info.key; proof.total_hashes = 0; proof.total_rewards = 0; diff --git a/program/src/reset.rs b/program/src/reset.rs index 5bcc317..5028532 100644 --- a/program/src/reset.rs +++ b/program/src/reset.rs @@ -81,9 +81,6 @@ pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResul } let total_epoch_rewards = MAX_EPOCH_REWARDS.saturating_sub(total_remaining_rewards); - // Update global top balance. - config.top_balance = top_balance; - // Update base reward rate for next epoch. config.base_reward_rate = calculate_new_reward_rate(config.base_reward_rate, total_theoretical_rewards);