diff --git a/README.md b/README.md index 9159df8..938c80e 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [`Claim`](program/src/claim.rs) – Distributes claimable rewards as tokens from the treasury to a miner. - [`Close`](program/src/close.rs) – Closes a proof account returns the rent to the owner. - [`Open`](program/src/open.rs) – Creates a new proof account for a prospective miner. +- [`DeclareProof`](program/src/declare_proof.rs) – Declares a single proof pubkey for processing. - [`Mine`](program/src/mine.rs) – Verifies a hash provided by a miner and issues claimable rewards. - [`Stake`](program/src/stake.rs) – Stakes ORE with a miner to increase their multiplier. - [`Reset`](program/src/reset.rs) – Resets the program for a new epoch. diff --git a/api/src/error.rs b/api/src/error.rs index e3d8ee0..f334a77 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -27,6 +27,10 @@ pub enum OreError { CannotClose = 9, #[error("This account cannot be crowned because its last stake was too recent")] CannotCrown = 10, + #[error("Only the declared proof can be processed in this transaction")] + DeclaredProofMissmatch = 11, + #[error("Failed to find and parse the declared proof from the transaction")] + FindAndParseDeclaredProofFailed = 12, } impl From for ProgramError { diff --git a/api/src/instruction.rs b/api/src/instruction.rs index fdf0469..f461785 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -89,6 +89,10 @@ pub enum OreInstruction { #[account(6, name = "mint_v1", desc = "Ore v1 token mint account", writable)] #[account(7, name = "token_program", desc = "SPL token program")] Upgrade = 7, + + #[account(0, name = "ore_program", desc = "Ore program")] + #[account(1, name = "proof", desc = "Ore proof account")] + DeclareProof = 8, #[account(0, name = "ore_program", desc = "Ore program")] #[account(1, name = "signer", desc = "Admin signer", signer)] @@ -224,6 +228,19 @@ pub fn close(signer: Pubkey) -> Instruction { } } +/// Builds a declare proof instruction. +pub fn declare_proof( + proof: Pubkey, +) -> Instruction { + Instruction { + program_id: crate::id(), + accounts: vec![ + AccountMeta::new_readonly(proof, false), + ], + data: OreInstruction::DeclareProof.to_vec(), + } +} + /// Builds a mine instruction. pub fn mine( signer: Pubkey, diff --git a/program/src/declare_proof.rs b/program/src/declare_proof.rs new file mode 100644 index 0000000..9f4e458 --- /dev/null +++ b/program/src/declare_proof.rs @@ -0,0 +1,85 @@ +use solana_program::{ + account_info::AccountInfo, + entrypoint::ProgramResult, + program_error::ProgramError, + pubkey::Pubkey, + sanitize::SanitizeError, + serialize_utils::{read_pubkey, read_u16}, +}; + + +/// DeclareProof is used by other instructions in the same transaction. +/// - Other instructions will use transaction introspection to ensure they +/// only process the declared proof. +/// - Other instructions will use find_and_parse_declared_proof with the +/// introspection data +/// +/// Safety requirements: +/// - No safety requirements are required in this instruction to keep cu's as +/// low as possible. Other instructions that use the declared proof handle +/// validation via the loader. +/// - Only one account should be provided. +pub fn process_declare_proof<'a, 'info>( + _program_id: &Pubkey, + accounts: &'a [AccountInfo<'info>], + _data: &[u8], +) -> ProgramResult { + let [_proof_info] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + Ok(()) +} + +/// Require that only the declared proof can be processed in this transaction. +/// +/// The intent here is to disincentivize sybil. As long as a user can fit multiple hashes in a single +/// transaction, there is a financial incentive to sybil multiple keypairs and pack as many hashes +/// as possible into each transaction to minimize fee / hash. +/// +/// If each transaction is limited to one hash only, then a user will minimize their fee / hash +/// by allocating all their hashpower to finding the single most difficult hash they can. +/// +/// Errors if: +/// - Fails to find and parse the declared proof pubkey in the second instruction +/// of the transaction +pub fn find_and_parse_declared_proof(data: &[u8]) -> Result { + // start the current byte index at 0 + let mut current = 0; + let num_instructions = read_u16(&mut current, data)?; + + if num_instructions < 2 { + // Not enough instructions in this transaction. + // The declare_proof instruction must be included with + // the other instruction in the same transaction. + return Err(SanitizeError::IndexOutOfBounds) + } + + // The first ix should be a set compute budget ix + // The second ix should be a declare_proof ix + // The starting index is the second ix + let index = 1; + + current += index * 2; + let start = read_u16(&mut current, data)?; + + current = start as usize; + + current += 2; // skip the accounts length variable + current += 1; // skip the meta_byte + + // The only account provided should be the proof pubkey + let proof_pubkey = read_pubkey(&mut current, data)?; + + // There shouldn't be any more accounts, + // this should be the Ore program ID + let program_pubkey = read_pubkey(&mut current, data)?; + if program_pubkey.eq(&ore_api::ID) { + return Ok(proof_pubkey); + } else { + return Err(SanitizeError::InvalidValue); + } +} + diff --git a/program/src/lib.rs b/program/src/lib.rs index 909a87a..1846e63 100644 --- a/program/src/lib.rs +++ b/program/src/lib.rs @@ -1,6 +1,7 @@ mod claim; mod close; mod initialize; +mod declare_proof; mod mine; mod open; mod reset; @@ -11,6 +12,7 @@ mod upgrade; use claim::*; use close::*; use initialize::*; +use declare_proof::*; use mine::*; use open::*; use reset::*; @@ -45,6 +47,7 @@ pub fn process_instruction( match OreInstruction::try_from(*tag).or(Err(ProgramError::InvalidInstructionData))? { OreInstruction::Claim => process_claim(program_id, accounts, data)?, OreInstruction::Close => process_close(program_id, accounts, data)?, + OreInstruction::DeclareProof => process_declare_proof(program_id, accounts, data)?, OreInstruction::Mine => process_mine(program_id, accounts, data)?, OreInstruction::Open => process_open(program_id, accounts, data)?, OreInstruction::Reset => process_reset(program_id, accounts, data)?, diff --git a/program/src/mine.rs b/program/src/mine.rs index 8c03516..8749399 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -25,7 +25,7 @@ use solana_program::{ sysvar::{self, instructions::load_current_index, Sysvar}, }; -use crate::utils::AccountDeserialize; +use crate::{find_and_parse_declared_proof, utils::AccountDeserialize}; /// Mine is the primary workhorse instruction of the Ore program. Its responsibilities include: /// 1. Calculate the hash from the provided nonce. @@ -38,6 +38,7 @@ use crate::utils::AccountDeserialize; /// - Can only succeed if mining is not paused. /// - Can only succeed if the last reset was less than 60 seconds ago. /// - Can only succeed if the provided hash satisfies the minimum difficulty requirement. +/// - Can only succeed if the miners proof pubkey matches the declared proof pubkey. /// - The provided proof account must be associated with the signer. /// - The provided bus, config, noise, stake, and slot hash sysvar must be valid. pub fn process_mine<'a, 'info>( @@ -61,10 +62,13 @@ pub fn process_mine<'a, 'info>( load_sysvar(instructions_sysvar, sysvar::instructions::id())?; load_sysvar(slot_hashes_sysvar, sysvar::slot_hashes::id())?; - // Validate this is the only mine ix in the transaction. - if !introspect_transaction(&instructions_sysvar.data.borrow()).unwrap_or(false) { - return Err(OreError::TransactionInvalid.into()); - } + if let Ok(pubkey) = find_and_parse_declared_proof(&instructions_sysvar.data.borrow()) { + if !pubkey.eq(proof_info.key) { + return Err(OreError::DeclaredProofMissmatch.into()); + } + } else { + return Err(OreError::FindAndParseDeclaredProofFailed.into()); + }; // Validate epoch is active. let config_data = config_info.data.borrow(); @@ -182,50 +186,3 @@ pub fn process_mine<'a, 'info>( Ok(()) } -/// Require that there is only one `mine` instruction per transaction and it is called from the -/// top level of the transaction. -/// -/// The intent here is to disincentivize sybil. As long as a user can fit multiple hashes in a single -/// transaction, there is a financial incentive to sybil multiple keypairs and pack as many hashes -/// as possible into each transaction to minimize fee / hash. -/// -/// If each transaction is limited to one hash only, then a user will minimize their fee / hash -/// by allocating all their hashpower to finding the single most difficult hash they can. -fn introspect_transaction(msg: &[u8]) -> Result { - #[allow(deprecated)] - let idx = load_current_index(msg); - let mut c = 0; - let num_instructions = read_u16(&mut c, msg)?; - let pc = c; - for i in 0..num_instructions as usize { - c = pc + i * 2; - c = read_u16(&mut c, msg)? as usize; - let num_accounts = read_u16(&mut c, msg)? as usize; - c += num_accounts * 33; - let program_id = read_pubkey(&mut c, msg)?; - if i.eq(&(idx as usize)) { - // Require top-level instruction at current index is a `mine` - if program_id.ne(&ore_api::ID) { - return Ok(false); - } - c += 2; - if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) { - if ix.ne(&OreInstruction::Mine) { - return Ok(false); - } - } - } else { - // Require no other instructions in the transaction are a `mine` - if program_id.eq(&ore_api::ID) { - c += 2; - if let Ok(ix) = OreInstruction::try_from(read_u8(&mut c, msg)?) { - if ix.eq(&OreInstruction::Mine) { - return Ok(false); - } - } - } - } - } - - Ok(true) -}