proof of wager

This commit is contained in:
Hardhat Chad
2025-05-23 16:29:31 -07:00
parent 379ebae250
commit b5a50622a2
23 changed files with 312 additions and 805 deletions

10
Cargo.lock generated
View File

@@ -1246,9 +1246,9 @@ dependencies = [
[[package]] [[package]]
name = "ore-api" name = "ore-api"
version = "3.6.0-beta" version = "3.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eff73b3394583f3df7ce0f58e9ec66d890777c6cc6dd739de63f8ec826fd4baf" checksum = "90933d4deaa73a1c73201d8a1aed50ce88211718370ceb0408ead479cb77e227"
dependencies = [ dependencies = [
"array-const-fn-init", "array-const-fn-init",
"bytemuck", "bytemuck",
@@ -1283,16 +1283,16 @@ dependencies = [
[[package]] [[package]]
name = "ore-boost-api" name = "ore-boost-api"
version = "4.0.0-beta" version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d09065544f61448a879ee9cc04e1c45a13ba76c38988fce83faa3f7aa31b1f" checksum = "0b6c68dc61ee91b269520653f607038bb7f6ebf053385b0a896d4e272bc9f485"
dependencies = [ dependencies = [
"array-const-fn-init", "array-const-fn-init",
"bytemuck", "bytemuck",
"const-crypto", "const-crypto",
"fixed", "fixed",
"num_enum", "num_enum",
"ore-api 3.6.0-beta", "ore-api 3.6.0",
"solana-program", "solana-program",
"spl-associated-token-account", "spl-associated-token-account",
"spl-token 4.0.2", "spl-token 4.0.2",

View File

@@ -24,11 +24,14 @@ num_enum = "0.7.2"
ore-api = { path = "api" } ore-api = { path = "api" }
ore-boost-api = "4.0.0-alpha" ore-boost-api = "4.0.0-alpha"
solana-program = "^2.1" solana-program = "^2.1"
solana-client = "^2.1"
solana-sdk = "^2.1"
spl-token = { version = "^4", features = ["no-entrypoint"] } spl-token = { version = "^4", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "^6", features = [ "no-entrypoint" ] } spl-associated-token-account = { version = "^6", features = [ "no-entrypoint" ] }
static_assertions = "1.1.0" static_assertions = "1.1.0"
steel = { features = ["spl"], version = "4.0" } steel = { features = ["spl"], version = "4.0" }
thiserror = "1.0.57" thiserror = "1.0.57"
tokio = { version = "1.37.0", features = ["full"] }
[patch.crates-io] [patch.crates-io]

View File

@@ -52,6 +52,12 @@ pub const SMOOTHING_FACTOR: u64 = 2;
/// The seed of the bus account PDA. /// The seed of the bus account PDA.
pub const BUS: &[u8] = b"bus"; pub const BUS: &[u8] = b"bus";
/// The seed of the block account PDA.
pub const BLOCK: &[u8] = b"block";
/// The seed of the wager account PDA.
pub const WAGER: &[u8] = b"wager";
/// The seed of the config account PDA. /// The seed of the config account PDA.
pub const CONFIG: &[u8] = b"config"; pub const CONFIG: &[u8] = b"config";

View File

@@ -4,59 +4,40 @@ use steel::*;
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)] #[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum OreInstruction { pub enum OreInstruction {
// User // User
Claim = 0, Bet = 0,
Close = 1, Close = 1,
Mine = 2, Payout = 2,
Open = 3, Reset = 3,
Reset = 4,
Update = 5,
// Admin // Admin
Initialize = 100, Initialize = 100,
Migrate = 101,
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)] #[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Claim { pub struct Bet {
pub amount: [u8; 8], pub amount: [u8; 8],
pub seed: [u8; 32],
} }
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)] #[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Close {} pub struct Close {}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Mine {
pub nonce: [u8; 8],
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Open {}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)] #[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Reset {} pub struct Reset {}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)] #[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Update {} pub struct Payout {}
#[repr(C)] #[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)] #[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {} pub struct Initialize {}
#[repr(C)] instruction!(OreInstruction, Bet);
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Migrate {}
instruction!(OreInstruction, Claim);
instruction!(OreInstruction, Close); instruction!(OreInstruction, Close);
instruction!(OreInstruction, Mine); instruction!(OreInstruction, Payout);
instruction!(OreInstruction, Open);
instruction!(OreInstruction, Reset); instruction!(OreInstruction, Reset);
instruction!(OreInstruction, Update);
instruction!(OreInstruction, Initialize); instruction!(OreInstruction, Initialize);
instruction!(OreInstruction, Migrate);

View File

@@ -2,7 +2,6 @@ pub mod consts;
pub mod error; pub mod error;
pub mod event; pub mod event;
pub mod instruction; pub mod instruction;
pub mod loaders;
pub mod sdk; pub mod sdk;
pub mod state; pub mod state;
@@ -11,8 +10,6 @@ pub mod prelude {
pub use crate::error::*; pub use crate::error::*;
pub use crate::event::*; pub use crate::event::*;
pub use crate::instruction::*; pub use crate::instruction::*;
pub use crate::loaders::*;
pub use crate::sdk::*;
pub use crate::state::*; pub use crate::state::*;
} }

View File

@@ -1,36 +0,0 @@
use steel::*;
use crate::{
consts::*,
state::{Config, Treasury},
};
pub trait OreAccountInfoValidation {
fn is_bus(&self) -> Result<&Self, ProgramError>;
fn is_config(&self) -> Result<&Self, ProgramError>;
fn is_treasury(&self) -> Result<&Self, ProgramError>;
fn is_treasury_tokens(&self) -> Result<&Self, ProgramError>;
}
impl OreAccountInfoValidation for AccountInfo<'_> {
fn is_bus(&self) -> Result<&Self, ProgramError> {
if !BUS_ADDRESSES.contains(self.key) {
return Err(ProgramError::InvalidSeeds);
}
Ok(self)
}
fn is_config(&self) -> Result<&Self, ProgramError> {
self.has_address(&CONFIG_ADDRESS)?
.is_type::<Config>(&crate::ID)
}
fn is_treasury(&self) -> Result<&Self, ProgramError> {
self.has_address(&TREASURY_ADDRESS)?
.is_type::<Treasury>(&crate::ID)
}
fn is_treasury_tokens(&self) -> Result<&Self, ProgramError> {
self.has_address(&TREASURY_TOKENS_ADDRESS)
}
}

View File

@@ -1,136 +1,3 @@
use steel::*; use steel::*;
use crate::{consts::*, instruction::*, state::proof_pda}; use crate::{consts::*, instruction::*, state::*};
/// Builds a claim instruction.
pub fn claim(signer: Pubkey, beneficiary: Pubkey, amount: u64) -> Instruction {
let proof = proof_pda(signer).0;
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new(beneficiary, false),
AccountMeta::new(proof, false),
AccountMeta::new_readonly(TREASURY_ADDRESS, false),
AccountMeta::new(TREASURY_TOKENS_ADDRESS, false),
AccountMeta::new_readonly(spl_token::ID, false),
],
data: Claim {
amount: amount.to_le_bytes(),
}
.to_bytes(),
}
}
/// Builds a close instruction.
pub fn close(signer: Pubkey) -> Instruction {
let proof = proof_pda(signer).0;
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new(proof, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
],
data: Close {}.to_bytes(),
}
}
/// Builds a mine instruction.
pub fn mine(
signer: Pubkey,
authority: Pubkey,
bus: Pubkey,
nonce: u64,
boost_config: Pubkey,
) -> Instruction {
let proof = proof_pda(authority).0;
let accounts = vec![
AccountMeta::new(signer, true),
AccountMeta::new(bus, false),
AccountMeta::new_readonly(CONFIG_ADDRESS, false),
AccountMeta::new(proof, false),
AccountMeta::new_readonly(sysvar::instructions::ID, false),
AccountMeta::new_readonly(sysvar::slot_hashes::ID, false),
AccountMeta::new_readonly(boost_config, false),
AccountMeta::new(proof_pda(boost_config).0, false),
];
Instruction {
program_id: crate::ID,
accounts,
data: Mine {
// digest: solution.d,
nonce: nonce.to_le_bytes(),
}
.to_bytes(),
}
}
/// Builds an open instruction.
pub fn open(signer: Pubkey, miner: Pubkey, payer: Pubkey) -> Instruction {
let proof_pda = proof_pda(signer);
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new_readonly(miner, false),
AccountMeta::new(payer, true),
AccountMeta::new(proof_pda.0, false),
AccountMeta::new_readonly(solana_program::system_program::ID, false),
AccountMeta::new_readonly(sysvar::slot_hashes::ID, false),
],
data: Open {}.to_bytes(),
}
}
/// Builds a reset instruction.
pub fn reset(signer: Pubkey, best_proof: Pubkey) -> Instruction {
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
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),
],
data: Reset {}.to_bytes(),
}
}
// Build an update instruction.
pub fn update(signer: Pubkey, miner: Pubkey) -> Instruction {
let proof = proof_pda(signer).0;
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new_readonly(miner, false),
AccountMeta::new(proof, false),
],
data: Update {}.to_bytes(),
}
}
// Builds an initialize instruction.
pub fn initialize(signer: Pubkey) -> Instruction {
Instruction {
program_id: crate::ID,
accounts: vec![
AccountMeta::new(signer, true),
AccountMeta::new(CONFIG_ADDRESS, false),
AccountMeta::new(METADATA_ADDRESS, false),
AccountMeta::new(MINT_ADDRESS, false),
AccountMeta::new(TREASURY_ADDRESS, 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(),
}
}

36
api/src/state/block.rs Normal file
View File

@@ -0,0 +1,36 @@
use steel::*;
use super::OreAccount;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Block {
/// The current round.
pub current_round: u64,
/// The cumulative amount of SOL risked in the current round, up to and including this bet.
pub total_bets: u64,
/// The number of bets made in the current round.
pub bet_count: u64,
/// The time time the current round started.
pub started_at: u64,
/// The slot at which the current round ends.
pub ends_at: u64,
/// Whether or not the current round has ended.
pub payed_out: u64,
/// The mint used to track wagers of the current round.
pub mint: Pubkey,
/// The amount of ORE to distribute to the winner.
pub reward: u64,
/// The noise used for the current round.
pub noise: [u8; 32],
}
account!(OreAccount, Block);

View File

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

View File

@@ -1,43 +0,0 @@
use steel::*;
use super::{OldOreAccount, OreAccount};
/// Config is a singleton account which manages program global variables.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Config {
/// The timestamp of the last reset.
pub last_reset_at: i64,
/// The best difficulty score of this epoch.
pub best_hash: [u8; 32],
/// 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 block_reward: u64,
}
/// Config is a singleton account which manages program global variables.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct OldConfig {
/// 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 target emissions rate in ORE/min.
pub target_emmissions_rate: u64,
}
account!(OreAccount, Config);
account!(OldOreAccount, OldConfig);

View File

@@ -1,12 +1,12 @@
mod bus; mod block;
mod config;
mod proof; mod proof;
mod treasury; mod treasury;
mod wager;
pub use bus::*; pub use block::*;
pub use config::*;
pub use proof::*; pub use proof::*;
pub use treasury::*; pub use treasury::*;
pub use wager::*;
use steel::*; use steel::*;
@@ -15,29 +15,27 @@ use crate::consts::*;
#[repr(u8)] #[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] #[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)]
pub enum OreAccount { pub enum OreAccount {
Bus = 100,
Config = 101,
Proof = 102, Proof = 102,
Treasury = 103, Treasury = 103,
Block = 104,
Wager = 105,
} }
/// Derive the PDA of the config account. pub fn block_pda() -> (Pubkey, u8) {
pub fn config_pda() -> (Pubkey, u8) { Pubkey::find_program_address(&[BLOCK], &crate::ID)
Pubkey::find_program_address(&[CONFIG], &crate::id())
} }
/// Derive the PDA of a proof account.
pub fn proof_pda(authority: Pubkey) -> (Pubkey, u8) { pub fn proof_pda(authority: Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[PROOF, authority.as_ref()], &crate::id()) Pubkey::find_program_address(&[PROOF, authority.as_ref()], &crate::id())
} }
/// Derive the PDA of the treasury account. pub fn wager_pda(round: u64, id: u64) -> (Pubkey, u8) {
pub fn treasury_pda() -> (Pubkey, u8) { Pubkey::find_program_address(
Pubkey::find_program_address(&[TREASURY], &crate::id()) &[WAGER, &round.to_le_bytes(), &id.to_le_bytes()],
&crate::ID,
)
} }
#[repr(u8)] pub fn treasury_pda() -> (Pubkey, u8) {
#[derive(Clone, Copy, Debug, Eq, PartialEq, IntoPrimitive, TryFromPrimitive)] Pubkey::find_program_address(&[TREASURY], &crate::ID)
pub enum OldOreAccount {
OldConfig = 101,
} }

27
api/src/state/wager.rs Normal file
View File

@@ -0,0 +1,27 @@
use steel::*;
use super::OreAccount;
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Pod, Zeroable)]
pub struct Wager {
/// The signer authorized to use this wager.
pub authority: Pubkey,
/// The current round this miner is betting in.
pub round: u64,
/// The ID of the bet.
pub id: u64,
/// The quantity of SOL this miner has bet in the current round.
pub amount: u64,
/// The cumulative amount of SOL bet in the current round, up to and including this wager.
pub cumulative_bets: u64,
/// The timestamp of the wager.
pub timestamp: u64,
}
account!(OreAccount, Wager);

81
program/src/bet.rs Normal file
View File

@@ -0,0 +1,81 @@
use std::mem::size_of;
use ore_api::prelude::*;
use solana_program::{keccak::hashv, slot_hashes::SlotHash};
use steel::*;
pub fn process_bet(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
// Parse data.
let args = Bet::try_from_bytes(data)?;
let amount = u64::from_le_bytes(args.amount);
// Load accounts.
let clock = Clock::get()?;
let [signer_info, block_info, wager_info, block_bets_info, sender_info, system_program, token_program, slot_hashes_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let block = block_info
.as_account_mut::<Block>(&ore_api::ID)?
.assert_mut(|b| b.ends_at > clock.slot)?
.assert_mut(|b| b.payed_out != 0)?;
block_bets_info.as_associated_token_account(block_info.key, &block.mint)?;
sender_info.as_associated_token_account(signer_info.key, &block.mint)?;
wager_info.is_writable()?.is_empty()?.has_seeds(
&[
WAGER,
&block.current_round.to_le_bytes(),
&block.bet_count.to_le_bytes(),
],
&ore_api::ID,
)?;
system_program.is_program(&system_program::ID)?;
token_program.is_program(&spl_token::ID)?;
slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?;
// Create wager account.
create_program_account::<Wager>(
&wager_info,
&system_program,
&signer_info,
&ore_api::ID,
&[
WAGER,
&block.current_round.to_le_bytes(),
&block.bet_count.to_le_bytes(),
],
)?;
let wager = wager_info.as_account_mut::<Wager>(&ore_api::ID)?;
wager.amount = amount;
wager.authority = *signer_info.key;
wager.id = block.bet_count;
wager.round = block.current_round;
wager.timestamp = clock.unix_timestamp as u64;
wager.cumulative_bets = block.total_bets;
// Update block.
block.total_bets += amount;
block.bet_count += 1;
// Hash client seed into block noise. Use a recent slot hash if no seed is provided.
// This follows the scheme for provable randomness.
let seed: &[u8] = if args.seed == [0; 32] {
&slot_hashes_sysvar.data.borrow()[0..size_of::<SlotHash>()]
} else {
args.seed.as_slice()
};
block.noise = hashv(&[&block.noise, seed]).to_bytes();
// Transfer wagers.
transfer(
&signer_info,
&sender_info,
&block_bets_info,
&token_program,
amount,
)?;
Ok(())
}

View File

@@ -1,52 +0,0 @@
use ore_api::prelude::*;
use steel::*;
/// Claim distributes claimable ORE from the treasury to a miner.
pub fn process_claim(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
// Parse args.
let args = Claim::try_from_bytes(data)?;
let amount = u64::from_le_bytes(args.amount);
// Load accounts.
let clock = Clock::get()?;
let [signer_info, beneficiary_info, proof_info, treasury_info, treasury_tokens_info, token_program] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
beneficiary_info
.is_writable()?
.as_token_account()?
.assert(|t| t.mint() == MINT_ADDRESS)?;
let proof = proof_info
.as_account_mut::<Proof>(&ore_api::ID)?
.assert_mut_err(
|p| p.authority == *signer_info.key,
ProgramError::MissingRequiredSignature,
)?;
treasury_info.is_treasury()?;
treasury_tokens_info.is_writable()?.is_treasury_tokens()?;
token_program.is_program(&spl_token::ID)?;
// Update miner balance.
proof.balance = proof
.balance
.checked_sub(amount)
.ok_or(OreError::ClaimTooLarge)?;
// Update last claim timestamp.
proof.last_claim_at = clock.unix_timestamp;
// Transfer tokens from treasury to beneficiary.
transfer_signed(
treasury_info,
treasury_tokens_info,
beneficiary_info,
token_program,
amount,
&[TREASURY],
)?;
Ok(())
}

View File

@@ -1,25 +1,21 @@
use ore_api::prelude::*; use ore_api::prelude::*;
use steel::*; use steel::*;
/// Close closes a proof account and returns the rent to the owner.
pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { pub fn process_close(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts. // Load accounts.
let [signer_info, proof_info, system_program] = accounts else { let [signer_info, block_info, wager_info, system_program] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys); return Err(ProgramError::NotEnoughAccountKeys);
}; };
signer_info.is_signer()?; signer_info.is_signer()?;
proof_info let block = block_info.as_account::<Block>(&ore_api::ID)?;
.is_writable()? wager_info
.as_account::<Proof>(&ore_api::ID)? .as_account_mut::<Wager>(&ore_api::ID)?
.assert_err( .assert_mut(|w| w.authority == *signer_info.key)?
|p| p.authority == *signer_info.key, .assert_mut(|w| w.round < block.current_round)?;
ProgramError::MissingRequiredSignature,
)?
.assert(|p| p.balance == 0)?;
system_program.is_program(&system_program::ID)?; system_program.is_program(&system_program::ID)?;
// Return rent to signer. // Close the wager account
proof_info.close(signer_info)?; wager_info.close(&signer_info)?;
Ok(()) Ok(())
} }

View File

@@ -1,122 +1,52 @@
use ore_api::prelude::*; use ore_api::prelude::*;
use solana_program::program_pack::Pack;
use spl_token::state::Mint;
use steel::*; use steel::*;
/// Initialize sets up the ORE program to begin mining. /// Initialize sets up the ORE program to begin mining.
pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { pub fn process_initialize(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts. // Load accounts.
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] = let [signer_info, block_info, block_bets_info, sol_mint_info, system_program, token_program, associated_token_program] =
accounts accounts
else { else {
return Err(ProgramError::NotEnoughAccountKeys); return Err(ProgramError::NotEnoughAccountKeys);
}; };
signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS)?; signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS)?;
config_info block_info
.is_empty()? .is_empty()?
.is_writable()? .is_writable()?
.has_seeds(&[CONFIG], &ore_api::ID)?; .has_seeds(&[BLOCK], &ore_api::ID)?;
metadata_info.is_empty()?.is_writable()?.has_seeds( block_bets_info.is_empty()?.is_writable()?;
&[ sol_mint_info
METADATA, .has_address(&spl_token::native_mint::ID)?
mpl_token_metadata::ID.as_ref(), .as_mint()?;
MINT_ADDRESS.as_ref(),
],
&mpl_token_metadata::ID,
)?;
mint_info
.is_empty()?
.is_writable()?
.has_seeds(&[MINT, MINT_NOISE.as_slice()], &ore_api::ID)?;
treasury_info
.is_empty()?
.is_writable()?
.has_seeds(&[TREASURY], &ore_api::ID)?;
treasury_tokens_info.is_empty()?.is_writable()?;
system_program.is_program(&system_program::ID)?; system_program.is_program(&system_program::ID)?;
token_program.is_program(&spl_token::ID)?; token_program.is_program(&spl_token::ID)?;
associated_token_program.is_program(&spl_associated_token_account::ID)?; associated_token_program.is_program(&spl_associated_token_account::ID)?;
metadata_program.is_program(&mpl_token_metadata::ID)?;
rent_sysvar.is_sysvar(&sysvar::rent::ID)?;
// Initialize config. // Initialize config.
create_program_account::<Config>( create_program_account::<Block>(
config_info, block_info,
system_program, system_program,
signer_info, signer_info,
&ore_api::ID, &ore_api::ID,
&[CONFIG], &[BLOCK],
)?; )?;
let config = config_info.as_account_mut::<Config>(&ore_api::ID)?; let block = block_info.as_account_mut::<Block>(&ore_api::ID)?;
// config.base_reward_rate = INITIAL_BASE_REWARD_RATE; block.current_round = 0;
config.last_reset_at = 0; block.total_bets = 0;
config.best_hash = [u8::MAX; 32]; block.bet_count = 0;
config.best_proof = Pubkey::default(); block.started_at = 0;
config.challenge = [0; 32]; block.ends_at = 0;
config.block_reward = 0; block.payed_out = 0;
block.mint = spl_token::native_mint::ID;
// Initialize treasury. block.reward = 0;
create_program_account::<Treasury>( block.noise = [0; 32];
treasury_info,
system_program,
signer_info,
&ore_api::ID,
&[TREASURY],
)?;
// Initialize mint.
allocate_account_with_bump(
mint_info,
system_program,
signer_info,
Mint::LEN,
&spl_token::ID,
&[MINT, MINT_NOISE.as_slice()],
MINT_BUMP,
)?;
initialize_mint_signed_with_bump(
mint_info,
treasury_info,
None,
token_program,
rent_sysvar,
TOKEN_DECIMALS,
&[MINT, MINT_NOISE.as_slice()],
MINT_BUMP,
)?;
// Initialize mint metadata.
mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi {
__program: metadata_program,
metadata: metadata_info,
mint: mint_info,
mint_authority: treasury_info,
payer: signer_info,
update_authority: (signer_info, true),
system_program,
rent: Some(rent_sysvar),
__args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
data: mpl_token_metadata::types::DataV2 {
name: METADATA_NAME.to_string(),
symbol: METADATA_SYMBOL.to_string(),
uri: METADATA_URI.to_string(),
seller_fee_basis_points: 0,
creators: None,
collection: None,
uses: None,
},
is_mutable: true,
collection_details: None,
},
}
.invoke_signed(&[&[TREASURY, &[TREASURY_BUMP]]])?;
// Initialize treasury token account. // Initialize treasury token account.
create_associated_token_account( create_associated_token_account(
signer_info, signer_info,
treasury_info, block_info,
treasury_tokens_info, block_bets_info,
mint_info, sol_mint_info,
system_program, system_program,
token_program, token_program,
associated_token_program, associated_token_program,

View File

@@ -1,22 +1,17 @@
mod claim; mod bet;
mod close; mod close;
mod initialize; mod initialize;
mod migrate; mod payout;
mod mine;
mod open;
mod reset; mod reset;
mod update;
use claim::*; use bet::*;
use close::*; use close::*;
use initialize::*; use initialize::*;
use migrate::*; use payout::*;
use mine::*;
use open::*;
use ore_api::instruction::*;
use reset::*; use reset::*;
use ore_api::instruction::*;
use steel::*; use steel::*;
use update::*;
pub fn process_instruction( pub fn process_instruction(
program_id: &Pubkey, program_id: &Pubkey,
@@ -26,14 +21,11 @@ pub fn process_instruction(
let (ix, data) = parse_instruction(&ore_api::ID, program_id, data)?; let (ix, data) = parse_instruction(&ore_api::ID, program_id, data)?;
match ix { match ix {
OreInstruction::Claim => process_claim(accounts, data)?, OreInstruction::Bet => process_bet(accounts, data)?,
OreInstruction::Close => process_close(accounts, data)?, OreInstruction::Close => process_close(accounts, data)?,
OreInstruction::Mine => process_mine(accounts, data)?,
OreInstruction::Open => process_open(accounts, data)?,
OreInstruction::Reset => process_reset(accounts, data)?, OreInstruction::Reset => process_reset(accounts, data)?,
OreInstruction::Update => process_update(accounts, data)?,
OreInstruction::Initialize => process_initialize(accounts, data)?, OreInstruction::Initialize => process_initialize(accounts, data)?,
OreInstruction::Migrate => process_migrate(accounts, data)?, OreInstruction::Payout => process_payout(accounts, data)?,
} }
Ok(()) Ok(())

View File

@@ -1,124 +0,0 @@
use ore_api::prelude::*;
use solana_program::hash;
use steel::*;
/// Mine validates hashes and increments a miner's claimable balance.
pub fn process_migrate(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
// Parse args.
let args = Migrate::try_from_bytes(data)?;
// Load accounts.
let clock = Clock::get()?;
let t: i64 = clock.unix_timestamp;
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, system_program, slot_hashes_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?.has_address(&INITIALIZER_ADDRESS);
let config = config_info
.as_account_mut::<OldConfig>(&ore_api::ID)?
.assert_mut_err(
|c| t < c.last_reset_at + EPOCH_DURATION,
OreError::NeedsReset.into(),
)?;
let bus_0 = bus_0_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 0)?;
let bus_1 = bus_1_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 1)?;
let bus_2 = bus_2_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 2)?;
let bus_3 = bus_3_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 3)?;
let bus_4 = bus_4_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 4)?;
let bus_5 = bus_5_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 5)?;
let bus_6 = bus_6_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 6)?;
let bus_7 = bus_7_info
.as_account_mut::<Bus>(&ore_api::ID)?
.assert_mut(|b| b.id == 7)?;
mint_info
.is_writable()?
.has_address(&MINT_ADDRESS)?
.as_mint()?;
treasury_info
.has_address(&TREASURY_ADDRESS)?
.is_writable()?;
treasury_tokens_info
.has_address(&TREASURY_TOKENS_ADDRESS)?
.as_associated_token_account(&TREASURY_ADDRESS, &MINT_ADDRESS)?;
token_program.is_program(&spl_token::ID)?;
system_program.is_program(&system_program::ID)?;
slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?;
let mut total_bus_balance = 0;
total_bus_balance += bus_0.rewards;
total_bus_balance += bus_1.rewards;
total_bus_balance += bus_2.rewards;
total_bus_balance += bus_3.rewards;
total_bus_balance += bus_4.rewards;
total_bus_balance += bus_5.rewards;
total_bus_balance += bus_6.rewards;
total_bus_balance += bus_7.rewards;
// Reset bus balances
bus_0.rewards = 0;
bus_1.rewards = 0;
bus_2.rewards = 0;
bus_3.rewards = 0;
bus_4.rewards = 0;
bus_5.rewards = 0;
bus_6.rewards = 0;
bus_7.rewards = 0;
// Delete bus accounts
bus_0_info.close(signer_info)?;
bus_1_info.close(signer_info)?;
bus_2_info.close(signer_info)?;
bus_3_info.close(signer_info)?;
bus_4_info.close(signer_info)?;
bus_5_info.close(signer_info)?;
bus_6_info.close(signer_info)?;
bus_7_info.close(signer_info)?;
// Burn all tokens in the bus balances
burn_signed(
treasury_tokens_info,
mint_info,
treasury_info,
token_program,
total_bus_balance,
&[TREASURY],
)?;
// let proof = proof_info
// .as_account_mut::<Proof>(&ore_api::ID)?
// .assert_mut_err(
// |p| p.miner == *signer_info.key,
// ProgramError::MissingRequiredSignature,
// )?;
// Compute the hash.
// let solution = hash::hashv(&[
// args.nonce.as_slice(),
// config.challenge.as_slice(),
// proof.authority.to_bytes().as_slice(),
// ]);
// // Update the best solution.
// if solution.to_bytes() < config.best_hash {
// config.best_hash = solution.to_bytes();
// config.best_proof = *proof_info.key;
// }
Ok(())
}

View File

@@ -1,44 +0,0 @@
use ore_api::prelude::*;
use solana_program::hash;
use steel::*;
/// Mine validates hashes and increments a miner's claimable balance.
pub fn process_mine(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
// Parse args.
let args = Mine::try_from_bytes(data)?;
// Load accounts.
let clock = Clock::get()?;
let t: i64 = clock.unix_timestamp;
let [signer_info, config_info, proof_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let config = config_info.as_account_mut::<Config>(&ore_api::ID)?;
let proof = proof_info
.as_account_mut::<Proof>(&ore_api::ID)?
.assert_mut_err(
|p| p.miner == *signer_info.key,
ProgramError::MissingRequiredSignature,
)?;
// Compute the hash.
let solution = hash::hashv(&[
args.nonce.as_slice(),
config.challenge.as_slice(),
proof.authority.to_bytes().as_slice(),
]);
// Update the best solution.
if solution.to_bytes() < config.best_hash {
config.best_hash = solution.to_bytes();
config.best_proof = *proof_info.key;
}
// Update the proof.
proof.last_hash = solution.to_bytes();
proof.last_hash_at = t;
proof.total_hashes += 1;
Ok(())
}

View File

@@ -1,48 +0,0 @@
use std::mem::size_of;
use ore_api::prelude::*;
use solana_program::{keccak::hashv, slot_hashes::SlotHash};
use steel::*;
/// Open creates a new proof account to track a miner's state.
pub fn process_open(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let [signer_info, miner_info, payer_info, proof_info, system_program, slot_hashes_info] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
payer_info.is_signer()?;
proof_info
.is_empty()?
.is_writable()?
.has_seeds(&[PROOF, signer_info.key.as_ref()], &ore_api::ID)?;
system_program.is_program(&system_program::ID)?;
slot_hashes_info.is_sysvar(&sysvar::slot_hashes::ID)?;
// Initialize proof.
create_program_account::<Proof>(
proof_info,
system_program,
payer_info,
&ore_api::ID,
&[PROOF, signer_info.key.as_ref()],
)?;
let clock = Clock::get()?;
let proof = proof_info.as_account_mut::<Proof>(&ore_api::ID)?;
proof.authority = *signer_info.key;
proof.balance = 0;
proof.challenge = hashv(&[
signer_info.key.as_ref(),
&slot_hashes_info.data.borrow()[0..size_of::<SlotHash>()],
])
.0;
proof.last_hash = [0; 32];
proof.last_hash_at = clock.unix_timestamp;
proof.miner = *miner_info.key;
proof.total_hashes = 0;
proof.total_rewards = 0;
Ok(())
}

62
program/src/payout.rs Normal file
View File

@@ -0,0 +1,62 @@
use std::mem::size_of;
use ore_api::prelude::*;
use solana_program::{keccak::hashv, slot_hashes::SlotHash};
use steel::*;
pub fn process_payout(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let clock = Clock::get()?;
let [signer_info, block_info, wager_info, recipient_info, treasury_info, treasury_tokens_info, system_program, token_program, slot_hashes_sysvar] =
accounts
else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let block = block_info
.as_account_mut::<Block>(&ore_api::ID)?
.assert_mut(|b| b.ends_at < clock.slot)?
.assert_mut(|b| b.payed_out == 0)?;
let wager = wager_info.as_account::<Wager>(&ore_api::ID)?;
recipient_info.as_associated_token_account(&wager.authority, &MINT_ADDRESS)?;
treasury_info.has_address(&TREASURY_ADDRESS)?;
treasury_tokens_info
.has_address(&TREASURY_TOKENS_ADDRESS)?
.is_writable()?;
system_program.is_program(&system_program::ID)?;
token_program.is_program(&spl_token::ID)?;
slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?;
// Select the slothash from the slot at when the round ended.
// The represents the server seed for the provably fair random number.
let offset = clock.slot - block.ends_at;
let size = size_of::<SlotHash>();
let i = offset as usize * size;
let slot_hash = &slot_hashes_sysvar.data.borrow()[i..i + size];
block.noise = hashv(&[&block.noise, slot_hash]).to_bytes();
// Calculate the random number.
let x = u64::from_le_bytes(block.noise[0..8].try_into().unwrap());
let y = u64::from_le_bytes(block.noise[8..16].try_into().unwrap());
let z = u64::from_le_bytes(block.noise[16..24].try_into().unwrap());
let w = u64::from_le_bytes(block.noise[24..32].try_into().unwrap());
let roll = (x ^ y ^ z ^ w) % block.total_bets;
// Assert that the wager account passed in is the winner.
assert!(roll >= wager.cumulative_bets && roll < wager.cumulative_bets + wager.amount);
// Mark the block as paid out.
block.payed_out = 1;
// Transfer the winnings to the recipient.
transfer_signed(
&treasury_info,
&treasury_tokens_info,
&recipient_info,
&token_program,
ONE_ORE / 2,
&[TREASURY],
)?;
Ok(())
}

View File

@@ -1,102 +1,72 @@
use drillx::difficulty;
use ore_api::prelude::*; use ore_api::prelude::*;
use ore_boost_api::state::Config as BoostConfig; use ore_boost_api::{consts::DENOMINATOR_BPS, prelude::Config as BoostConfig};
use solana_program::{hash::hashv, slot_hashes::SlotHash};
use steel::*; use steel::*;
/// Reset tops up the bus balances and updates the emissions and reward rates. /// Reset tops up the bus balances and updates the emissions and reward rates.
pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult { pub fn process_reset(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts. // Load accounts.
let clock = Clock::get()?; let clock = Clock::get()?;
let (required_accounts, boost_accounts) = accounts.split_at(7); let (required_accounts, boost_accounts) = accounts.split_at(6);
let [signer_info, config_info, mint_info, proof_info, treasury_info, treasury_tokens_info, token_program, slot_hashes_sysvar] = let [signer_info, block_info, mint_info, treasury_info, treasury_tokens_info, token_program, slot_hashes_sysvar] =
required_accounts required_accounts
else { else {
return Err(ProgramError::NotEnoughAccountKeys); return Err(ProgramError::NotEnoughAccountKeys);
}; };
signer_info.is_signer()?; signer_info.is_signer()?;
let config = config_info let block = block_info
.is_config()? .as_account_mut::<Block>(&ore_api::ID)?
.as_account_mut::<Config>(&ore_api::ID)?; .assert_mut(|b| b.ends_at < clock.slot)?
.assert_mut(|b| b.payed_out != 0)?;
let mint = mint_info let mint = mint_info
.has_address(&MINT_ADDRESS)? .has_address(&MINT_ADDRESS)?
.is_writable()? .is_writable()?
.as_mint()?; .as_mint()?;
let proof = proof_info treasury_info.has_address(&TREASURY_ADDRESS)?;
.as_account_mut::<Proof>(&ore_api::ID)? treasury_tokens_info.has_address(&TREASURY_TOKENS_ADDRESS)?;
.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)?; token_program.is_program(&spl_token::ID)?;
slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?; slot_hashes_sysvar.is_sysvar(&sysvar::slot_hashes::ID)?;
// Parse boost accounts.
let [boost_config_info, boost_proof_info] = boost_accounts else { let [boost_config_info, boost_proof_info] = boost_accounts else {
return Err(ProgramError::NotEnoughAccountKeys); return Err(ProgramError::NotEnoughAccountKeys);
}; };
let boost_config = boost_config_info.as_account::<BoostConfig>(&ore_api::ID)?; let boost_config = boost_config_info.as_account::<BoostConfig>(&ore_boost_api::ID)?;
let boost_proof = boost_proof_info let boost_proof = boost_proof_info
.as_account_mut::<Proof>(&ore_api::ID)? .as_account_mut::<Proof>(&ore_api::ID)?
.assert_mut(|p| p.authority == *boost_config_info.key)?; .assert_mut(|p| p.authority == *boost_config_info.key)?;
// Validate enough time has passed since the last reset. // Payout to boosts.
if clock.unix_timestamp < config.last_reset_at + EPOCH_DURATION { let net_emissions = get_target_emissions_rate(mint.supply());
return Ok(()); let boost_reward =
} (net_emissions as u128 * boost_config.take_rate as u128 / DENOMINATOR_BPS as u128) as u64;
// Record difficulty.
let score = difficulty(config.best_hash) as u64;
// Reset the challenge.
config.challenge = hashv(&[
config.challenge.as_slice(),
&slot_hashes_sysvar.data.borrow()[0..size_of::<SlotHash>()],
])
.to_bytes();
// Reset the config.
let block_reward = get_block_reward(mint.supply());
config.block_reward = block_reward;
config.best_proof = Pubkey::default();
config.best_hash = [u8::MAX; 32];
config.last_reset_at = clock.unix_timestamp;
// Calculate boost reward.
let take_rate = boost_config.take_rate.min(9900); // Cap at 99%
let boost_reward = block_reward * take_rate / ore_boost_api::consts::DENOMINATOR_BPS;
let miner_reward = block_reward - boost_reward;
// Update proof balances.
proof.balance += miner_reward;
proof.total_rewards += miner_reward;
boost_proof.balance += boost_reward; boost_proof.balance += boost_reward;
boost_proof.total_rewards += boost_reward; boost_proof.total_rewards += boost_reward;
// Reset the block.
block.reward = net_emissions - boost_reward;
block.started_at = clock.slot;
block.ends_at = clock.slot + 150; // 60 seconds
block.payed_out = 0;
block.total_bets = 0;
block.bet_count = 0;
block.noise = [0; 32];
block.current_round += 1;
// Fund the treasury. // Fund the treasury.
mint_to_signed( mint_to_signed(
mint_info, mint_info,
treasury_tokens_info, treasury_tokens_info,
treasury_info, treasury_info,
token_program, token_program,
block_reward, net_emissions,
&[TREASURY], &[TREASURY],
)?; )?;
// Emit event.
BlockEvent {
score,
block_reward,
boost_reward,
ts: clock.unix_timestamp as u64,
}
.log_return();
Ok(()) Ok(())
} }
/// This function calculates the block reward (ORE / min) based on the current supply. /// 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 hard stop at 5 million ORE. /// It is designed to reduce emissions by 10% approximately every 12 months with a hardcap at 5 million ORE.
pub(crate) fn get_block_reward(current_supply: u64) -> u64 { pub(crate) fn get_target_emissions_rate(current_supply: u64) -> u64 {
match current_supply { match current_supply {
n if n < ONE_ORE * 525_600 => 100_000_000_000, // Year ~1 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 n if n < ONE_ORE * 998_640 => 90_000_000_000, // Year ~2
@@ -126,52 +96,7 @@ pub(crate) fn get_block_reward(current_supply: u64) -> u64 {
n if n < ONE_ORE * 4_916_405 => 7_178_979_874, // Year ~26 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_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 * 4_980_928 => 5_814_973_607, // Year ~28
n if n < MAX_SUPPLY => 5_233_476_327.min(MAX_SUPPLY - current_supply), // Year ~29 n if n < ONE_ORE * 5_000_000 => 5_233_476_327, // Year ~29
_ => 0, _ => 0,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
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_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_block_reward_zero_supply() {
assert_eq!(get_block_reward(0), 100_000_000_000);
}
}

View File

@@ -1,22 +0,0 @@
use ore_api::prelude::*;
use steel::*;
/// Update changes the miner authority on a proof account.
pub fn process_update(accounts: &[AccountInfo<'_>], _data: &[u8]) -> ProgramResult {
// Load accounts.
let [signer_info, miner_info, proof_info] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
signer_info.is_signer()?;
let proof = proof_info
.as_account_mut::<Proof>(&ore_api::ID)?
.assert_mut_err(
|p| p.authority == *signer_info.key,
ProgramError::MissingRequiredSignature,
)?;
// Update the proof's miner authority.
proof.miner = *miner_info.key;
Ok(())
}