checkpoint

This commit is contained in:
Hardhat Chad
2025-10-01 11:33:17 -07:00
parent bef1c56ee3
commit cf248d7a28
9 changed files with 280 additions and 228 deletions

View File

@@ -41,6 +41,9 @@ pub const SQUARE: &[u8] = b"square";
/// The seed of the stake account PDA.
pub const STAKE: &[u8] = b"stake";
/// The seed of the round account PDA.
pub const ROUND: &[u8] = b"round";
/// The seed of the treasury account PDA.
pub const TREASURY: &[u8] = b"treasury";

View File

@@ -6,26 +6,27 @@ pub enum OreInstruction {
// Miner
Automate = 0,
Boost = 1,
ClaimSOL = 2,
ClaimORE = 3,
Deploy = 4,
Initialize = 5,
Log = 6,
Reset = 7,
Checkpoint = 2,
ClaimSOL = 3,
ClaimORE = 4,
Deploy = 5,
Initialize = 6,
Log = 7,
Reset = 8,
// Staker
Deposit = 8,
Withdraw = 9,
ClaimYield = 10,
Deposit = 9,
Withdraw = 10,
ClaimYield = 11,
// Admin
Bury = 11,
Wrap = 12,
SetAdmin = 13,
SetFeeCollector = 14,
Bury = 12,
Wrap = 13,
SetAdmin = 14,
SetFeeCollector = 15,
// Seeker
ClaimSeeker = 15,
ClaimSeeker = 16,
}
#[repr(C)]
@@ -144,8 +145,13 @@ pub struct ClaimYield {
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct ClaimSeeker {}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Checkpoint {}
instruction!(OreInstruction, Automate);
instruction!(OreInstruction, Boost);
instruction!(OreInstruction, Checkpoint);
instruction!(OreInstruction, ClaimSOL);
instruction!(OreInstruction, ClaimORE);
instruction!(OreInstruction, Deploy);

View File

@@ -160,7 +160,7 @@ pub fn deploy(
let automation_address = automation_pda(authority).0;
let board_address = board_pda().0;
let miner_address = miner_pda(authority).0;
let square_address = square_pda().0;
// let square_address = square_pda().0;
// Convert array of 25 booleans into a 32-bit mask where each bit represents whether
// that square index is selected (1) or not (0)
@@ -177,7 +177,7 @@ pub fn deploy(
AccountMeta::new(automation_address, false),
AccountMeta::new(board_address, false),
AccountMeta::new(miner_address, false),
AccountMeta::new(square_address, false),
// AccountMeta::new(square_address, false),
AccountMeta::new_readonly(system_program::ID, false),
];
@@ -285,7 +285,7 @@ pub fn reset(signer: Pubkey, fee_collector: Pubkey, miners: Vec<Pubkey>) -> Inst
let board_address = board_pda().0;
let config_address = config_pda().0;
let mint_address = MINT_ADDRESS;
let square_address = square_pda().0;
// let square_address = square_pda().0;
let treasury_address = TREASURY_ADDRESS;
let treasury_tokens_address = treasury_tokens_address();
let mut accounts = vec![
@@ -294,7 +294,7 @@ pub fn reset(signer: Pubkey, fee_collector: Pubkey, miners: Vec<Pubkey>) -> Inst
AccountMeta::new(config_address, false),
AccountMeta::new(fee_collector, false),
AccountMeta::new(mint_address, false),
AccountMeta::new(square_address, false),
// AccountMeta::new(square_address, false),
AccountMeta::new(treasury_address, false),
AccountMeta::new(treasury_tokens_address, false),
AccountMeta::new_readonly(system_program::ID, false),

View File

@@ -6,7 +6,7 @@ use super::OreAccount;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Game {
pub struct Board {
/// The current round number.
pub round_id: u64,
@@ -35,9 +35,18 @@ pub struct Round {
/// The slot at which claims for this round account end.
pub expires_at: i64,
/// The amount of ORE in the motherlode.
pub motherlode: u64,
/// The account to which rent should be returned when this account is closed.
pub rent_payer: Pubkey,
/// The top miner of the round.
pub top_miner: Pubkey,
/// The amount of ORE to distribute to the top miner.
pub top_miner_reward: u64,
/// The total amount of SOL deployed in the round.
pub total_deployed: u64,
@@ -48,23 +57,66 @@ pub struct Round {
pub total_winnings: u64,
}
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Move {
/// The authority of the move.
pub authority: Pubkey,
impl Round {
pub fn rng(&self) -> u64 {
if self.slot_hash == [0; 32] {
return 0;
}
let r1 = u64::from_le_bytes(self.slot_hash[0..8].try_into().unwrap());
let r2 = u64::from_le_bytes(self.slot_hash[8..16].try_into().unwrap());
let r3 = u64::from_le_bytes(self.slot_hash[16..24].try_into().unwrap());
let r4 = u64::from_le_bytes(self.slot_hash[24..32].try_into().unwrap());
let r = r1 ^ r2 ^ r3 ^ r4;
r
}
/// The amount of SOL deployed in each square.
pub deployed: [u64; 25],
pub fn winning_square(&self, rng: u64) -> usize {
(rng % 25) as usize
}
/// The round number.
pub round_id: u64,
pub fn top_miner_sample(&self, rng: u64, winning_square: usize) -> u64 {
if self.deployed[winning_square] == 0 {
return 0;
}
rng.reverse_bits() % self.deployed[winning_square]
}
pub fn calculate_total_winnings(&self, winning_square: usize) -> u64 {
let mut total_winnings = 0;
for (i, &deployed) in self.deployed.iter().enumerate() {
if i != winning_square {
total_winnings += deployed;
}
}
total_winnings
}
pub fn did_motherlode_hit(&self, rng: u64) -> bool {
rng.reverse_bits() % 625 == 0
}
}
// impl Board {
// pub fn pda(&self) -> (Pubkey, u8) {
// board_pda()
// }
// }
impl Board {
pub fn pda(&self) -> (Pubkey, u8) {
board_pda()
}
}
// account!(OreAccount, Board);
account!(OreAccount, Board);
account!(OreAccount, Round);
#[cfg(test)]
mod tests {
use solana_program::rent::Rent;
use super::*;
#[test]
fn test_rent() {
let size_of_round = 8 + std::mem::size_of::<Round>();
let required_rent = Rent::default().minimum_balance(size_of_round);
println!("required_rent: {}", required_rent);
assert!(false);
}
}

View File

@@ -13,9 +13,14 @@ pub struct Miner {
/// The miner's prospects in the current round.
pub deployed: [u64; 25],
/// Unused buffer.
#[deprecated(note = "No longer used")]
pub buffer: [u8; 32],
/// The cumulative amount of SOL deployed on each square prior to this miner's move.
pub cumulative: [u64; 25],
/// SOL witheld in reserve to pay for checkpointing.
pub checkpoint_fee: u64,
/// The last round that this miner checkpointed.
pub checkpoint_id: u64,
/// The amount of SOL this miner has had refunded and may claim.
pub refund_sol: u64,

View File

@@ -3,7 +3,7 @@ mod board;
mod config;
mod miner;
mod seeker;
mod square;
// mod square;
mod stake;
mod treasury;
@@ -12,7 +12,7 @@ pub use board::*;
pub use config::*;
pub use miner::*;
pub use seeker::*;
pub use square::*;
// pub use square::*;
pub use stake::*;
pub use treasury::*;
@@ -30,9 +30,10 @@ pub enum OreAccount {
//
Board = 105,
Square = 106,
// Square = 106,
Seeker = 107,
Stake = 108,
Round = 109,
}
pub fn automation_pda(authority: Pubkey) -> (Pubkey, u8) {
@@ -55,8 +56,12 @@ pub fn seeker_pda(mint: Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[SEEKER, &mint.to_bytes()], &crate::ID)
}
pub fn square_pda() -> (Pubkey, u8) {
Pubkey::find_program_address(&[SQUARE], &crate::ID)
// pub fn square_pda() -> (Pubkey, u8) {
// Pubkey::find_program_address(&[SQUARE], &crate::ID)
// }
pub fn round_pda(id: u64) -> (Pubkey, u8) {
Pubkey::find_program_address(&[ROUND, &id.to_le_bytes()], &crate::ID)
}
pub fn stake_pda(authority: Pubkey) -> (Pubkey, u8) {

94
program/src/checkpoint.rs Normal file
View File

@@ -0,0 +1,94 @@
use ore_api::prelude::*;
use solana_program::rent::Rent;
use steel::*;
// TODO Bot fees
/// Checkpoints a miner's rewards.
pub fn process_checkpoint(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let clock = Clock::get()?;
let [signer_info, board_info, miner_info, round_info, treasury_info, system_program] = accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let board = board_info.as_account::<Board>(&ore_api::ID)?;
let miner = miner_info
.as_account_mut::<Miner>(&ore_api::ID)?
.assert_mut(|m| m.checkpoint_id < m.round_id)?; // Ensure miner has not already checkpointed this round.
if round_info.data_is_empty() {
// If round account is empty, ensure the correct account was provided.
// This can happen if the miner attempted to checkpoint after the round expired and the account was closed.
// In this case, the miner forfeits any potential rewards and their checkpoint is recorded.
round_info.has_seeds(&[ROUND, &miner.round_id.to_le_bytes()], &ore_api::ID)?;
miner.checkpoint_id = miner.round_id;
return Ok(());
}
let round = round_info
.as_account_mut::<Round>(&ore_api::ID)?
.assert_mut(|r| r.id < board.round_id)? // Ensure round has ended.
.assert_mut(|r| r.id == miner.round_id)?; // Ensure miner round ID matches the provided round.
treasury_info.as_account::<Treasury>(&ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// Ensure round is not expired.
if clock.unix_timestamp >= round.expires_at {
// In this case, the miner forfeits any potential rewards and their checkpoint is recorded.
miner.checkpoint_id = round.id;
return Ok(());
}
// Calculate miner rewards.
let mut rewards_sol = 0;
let mut rewards_ore = 0;
let r = round.rng();
let winning_square = round.winning_square(r) as usize;
if miner.deployed[winning_square] > 0 {
// Sanity check.
assert!(
round.deployed[winning_square] >= miner.deployed[winning_square],
"Invalid round deployed amount"
);
// Calculate SOL rewards.
rewards_sol = ((round.total_winnings as u128 * miner.deployed[winning_square] as u128)
/ round.deployed[winning_square] as u128) as u64;
// Calculate ORE rewards.
let top_miner_sample = round.top_miner_sample(r, winning_square);
if top_miner_sample >= miner.cumulative[winning_square]
&& top_miner_sample < miner.cumulative[winning_square] + miner.deployed[winning_square]
{
rewards_ore = round.top_miner_reward;
}
// Calculate motherlode rewards.
if round.motherlode > 0 {
rewards_ore += ((round.motherlode as u128 * miner.deployed[winning_square] as u128)
/ round.deployed[winning_square] as u128) as u64;
}
}
// Checkpoint miner.
miner.checkpoint_id = round.id;
miner.rewards_ore += rewards_ore;
miner.lifetime_rewards_ore += rewards_ore;
miner.rewards_sol += rewards_sol;
miner.lifetime_rewards_sol += rewards_sol;
// Do SOL transfers.
if rewards_sol > 0 {
round_info.send(rewards_sol, &miner_info);
}
// Assert round has sufficient funds for rent.
let account_size = 8 + std::mem::size_of::<Round>();
let required_rent = Rent::get()?.minimum_balance(account_size);
assert!(
round_info.lamports() >= required_rent,
"Round does not have sufficient funds for rent"
);
Ok(())
}

View File

@@ -11,9 +11,8 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
// Load accounts.
let clock = Clock::get()?;
let (required_accounts, miner_accounts) = accounts.split_at(7);
let [signer_info, authority_info, automation_info, board_info, miner_info, square_info, system_program] =
required_accounts
let [signer_info, authority_info, automation_info, board_info, miner_info, round_info, round_prev_info, system_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
@@ -22,12 +21,11 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
automation_info.is_writable()?;
let board = board_info
.as_account_mut::<Board>(&ore_api::ID)?
.assert_mut(|b| {
(clock.slot >= b.start_slot && clock.slot < b.end_slot && b.slot_hash == [0; 32])
|| (clock.slot >= b.end_slot + INTERMISSION_SLOTS && b.slot_hash != [0; 32])
})?;
.assert_mut(|b| clock.slot >= b.start_slot && clock.slot < b.end_slot)?;
let round = round_info
.as_account_mut::<Round>(&ore_api::ID)?
.assert_mut(|r| r.id == board.round_id)?;
miner_info.is_writable()?;
let square = square_info.as_account_mut::<Square>(&ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
// Check if signer is the automation executor.
@@ -58,7 +56,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
AutomationStrategy::Random => {
// Random automation strategy. Generate a random mask based on number of squares user wants to deploy to.
let num_squares = ((automation.mask & 0xFF) as u64).min(25);
let r = hashv(&[&automation.authority.to_bytes(), &board.id.to_le_bytes()]).0;
let r = hashv(&[&automation.authority.to_bytes(), &round.id.to_le_bytes()]).0;
squares = generate_random_mask(num_squares, &r);
}
}
@@ -74,7 +72,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
sol_log(
&format!(
"Round {}. Deploying {} SOL to {} squares",
board.id,
round.id,
lamports_to_sol(amount),
squares.iter().filter(|&&s| s).count(),
)
@@ -93,10 +91,12 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
let miner = miner_info.as_account_mut::<Miner>(&ore_api::ID)?;
miner.authority = *signer_info.key;
miner.deployed = [0; 25];
miner.cumulative = [0; 25];
miner.refund_sol = 0;
miner.rewards_sol = 0;
miner.rewards_ore = 0;
miner.round_id = board.id;
miner.round_id = round.id;
miner.checkpoint_id = round.id;
miner.lifetime_rewards_sol = 0;
miner.lifetime_rewards_ore = 0;
miner
@@ -105,56 +105,30 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
.as_account_mut::<Miner>(&ore_api::ID)?
.assert_mut(|m| {
if let Some(automation) = &automation {
// only run automation once per round
m.authority == automation.authority
&& (m.round_id < board.id || board.slot_hash != [0; 32])
} else {
m.authority == *signer_info.key
}
})?
};
// Reset board.
if board.slot_hash != [0; 32] {
// Reset board
board.deployed = [0; 25];
board.id += 1;
board.slot_hash = [0; 32];
board.start_slot = clock.slot;
board.end_slot = clock.slot + 150; // one minute
board.top_miner = Pubkey::default();
board.total_deployed = 0;
board.total_vaulted = 0;
board.total_winnings = 0;
// Reset squares
square.count = [0; 25];
square.deployed = [[0; 16]; 25];
square.miners = [[Pubkey::default(); 16]; 25];
}
// Reset miner
if miner.round_id != board.id {
miner.deployed = [0; 25];
miner.round_id = board.id;
}
if miner.round_id != round.id {
// Assert miner has checkpointed prior round.
assert!(
miner.checkpoint_id == miner.round_id,
"Miner has not checkpointed"
);
// Close early if estimated automation balance is less than required.
if let Some(automation) = &automation {
let num_squares = squares.iter().filter(|&&s| s).count();
let estimated_cost = (num_squares as u64 * amount) + automation.fee;
if estimated_cost > automation.balance {
sol_log("Automation balance too low.");
automation_info.close(authority_info)?;
return Ok(());
}
// Reset miner for new round.
miner.deployed = [0; 25];
miner.cumulative = round.deployed;
miner.round_id = round.id;
}
// Calculate all deployments.
let mut refund_amounts = [0; 25];
let mut refund_miner_infos = [None; 25];
let mut total_amount = 0;
'deploy: for (square_id, &should_deploy) in squares.iter().enumerate() {
for (square_id, &should_deploy) in squares.iter().enumerate() {
// Skip if square index is out of bounds.
if square_id > 24 {
break;
@@ -165,118 +139,36 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
continue;
}
// Get deployment metadata.
let is_first_move = miner.deployed[square_id] == 0;
let mut idx = if is_first_move {
// Insert at end of the list.
square.count[square_id] as usize
} else {
// Find the miner's index in the list.
let mut found = false;
let mut idx = 0;
for i in 0..16 {
if square.miners[square_id][i] == miner.authority {
idx = i;
found = true;
break;
}
}
// Safety check.
// This should never happen.
assert!(found);
idx
};
// If the square is full, refund the miner with the smallest deployment and kick them off the square.
if idx == 16 {
// Find miner with the smallest deployment.
let mut smallest_miner = Pubkey::default();
let mut smallest_deployment = u64::MAX;
for i in 0..16 {
if square.deployed[square_id][i] < smallest_deployment {
smallest_deployment = square.deployed[square_id][i];
smallest_miner = square.miners[square_id][i];
idx = i;
}
}
// Safety check.
// This should never happen.
assert!(smallest_miner != Pubkey::default());
// If deploy amount is less than smallest deployment, skip this square.
if amount < smallest_deployment {
continue 'deploy;
}
// Refund the smallest miner and kick them off the square.
'refund: for miner_info in miner_accounts {
if *miner_info.key == miner_pda(smallest_miner).0 {
// Refund the smallest miner.
let smallest_miner = miner_info
.as_account_mut::<Miner>(&ore_api::ID)?
.assert_mut(|m| m.authority == smallest_miner)?;
smallest_miner.refund_sol += smallest_deployment;
smallest_miner.deployed[square_id] -= smallest_deployment;
refund_amounts[square_id] = smallest_deployment;
refund_miner_infos[square_id] = Some(miner_info);
// Kick the smallest miner from the square.
board.deployed[square_id] -= smallest_deployment;
board.total_deployed -= smallest_deployment;
square.deployed[square_id][idx] -= smallest_deployment;
square.miners[square_id][idx] = Pubkey::default();
square.count[square_id] -= 1;
sol_log(
&format!(
"Kicking miner {} from square {}. Refunding: {}",
smallest_miner.authority, square_id, smallest_deployment
)
.as_str(),
);
break 'refund;
}
}
// Skip if miner already deployed to this square.
if miner.deployed[square_id] > 0 {
continue;
}
// Safety check.
// This should never happen.
assert!(idx < 16);
// Safety check.
// Skip if square count is still >= 16. This can only happen if the signer didn't provide a miner account to refund.
if square.count[square_id] >= 16 {
sol_log(&format!("Square {} is full. Skipping deployment.", square_id).as_str());
continue 'deploy;
}
// Record cumulative amount.
miner.cumulative[square_id] = round.deployed[square_id];
// Update miner
miner.deployed[square_id] += amount;
// Update square
if is_first_move {
square.miners[square_id][idx] = miner.authority;
square.count[square_id] += 1;
}
miner.deployed[square_id] = amount;
// Update board
board.deployed[square_id] += amount;
board.total_deployed += amount;
// Update square deployed
square.deployed[square_id][idx] += amount;
round.deployed[square_id] += amount;
round.total_deployed += amount;
// Update total amount
total_amount += amount;
// Exit early if automation does not have enough balance for another square.
if let Some(automation) = &automation {
if total_amount + automation.fee + amount > automation.balance {
break;
}
}
}
// Transfer SOL.
if let Some(automation) = automation {
automation.balance -= total_amount + automation.fee;
automation_info.send(total_amount, &board_info);
automation_info.send(total_amount, &round_info);
automation_info.send(automation.fee, &signer_info);
// Close automation if balance is 0.
@@ -284,14 +176,7 @@ pub fn process_deploy(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResul
automation_info.close(authority_info)?;
}
} else {
board_info.collect(total_amount, &signer_info)?;
}
// Transfer SOL refunds.
for (square_id, refund_amount) in refund_amounts.iter().enumerate() {
if let Some(refund_miner_info) = refund_miner_infos[square_id] {
board_info.send(*refund_amount, &refund_miner_info);
}
round_info.collect(total_amount, &signer_info)?;
}
Ok(())

View File

@@ -1,38 +1,40 @@
// mod automate;
// mod boost;
// mod bury;
// mod claim_ore;
// mod claim_seeker;
// mod claim_sol;
// mod claim_yield;
// mod deploy;
// mod deposit;
mod automate;
mod boost;
mod bury;
mod checkpoint;
mod claim_ore;
mod claim_seeker;
mod claim_sol;
mod claim_yield;
mod deploy;
mod deposit;
// mod initialize;
// mod log;
// mod reset;
// mod set_admin;
// mod set_fee_collector;
// mod whitelist;
// mod withdraw;
// mod wrap;
mod log;
mod reset;
mod set_admin;
mod set_fee_collector;
mod whitelist;
mod withdraw;
mod wrap;
// use automate::*;
// use boost::*;
// use bury::*;
// use claim_ore::*;
// use claim_seeker::*;
// use claim_sol::*;
// use claim_yield::*;
// use deploy::*;
// use deposit::*;
use automate::*;
use boost::*;
use bury::*;
use checkpoint::*;
use claim_ore::*;
use claim_seeker::*;
use claim_sol::*;
use claim_yield::*;
use deploy::*;
use deposit::*;
// use initialize::*;
// use log::*;
// use reset::*;
// use set_admin::*;
// use set_fee_collector::*;
// use whitelist::*;
// use withdraw::*;
// use wrap::*;
use log::*;
use reset::*;
use set_admin::*;
use set_fee_collector::*;
use whitelist::*;
use withdraw::*;
use wrap::*;
use ore_api::instruction::*;
use steel::*;