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:
Hardhat Chad
2025-01-19 19:07:16 -06:00
committed by GitHub
parent ebec3ca237
commit 70a9d38e4d
10 changed files with 112 additions and 91 deletions

31
Cargo.lock generated
View File

@@ -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",

View File

@@ -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" ] }

View File

@@ -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);

View File

@@ -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,

View File

@@ -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,
}

View File

@@ -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.

View File

@@ -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>(

View File

@@ -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();

View File

@@ -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;

View File

@@ -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);