mirror of
https://github.com/d0zingcat/ore.git
synced 2026-05-14 07:26:51 +00:00
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
This commit is contained in:
31
Cargo.lock
generated
31
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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" ] }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -56,10 +56,10 @@ pub fn mine(
|
||||
authority: Pubkey,
|
||||
bus: Pubkey,
|
||||
solution: Solution,
|
||||
boost_accounts: Vec<Pubkey>,
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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::<Treasury>(
|
||||
|
||||
@@ -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::<Boost>(&ore_boost_api::ID)?;
|
||||
let stake = stake_info
|
||||
.as_account::<Stake>(&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::<Boost>(&ore_boost_api::ID)?;
|
||||
reservation_info
|
||||
.as_account::<Reservation>(&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::<Proof>(&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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user