diff --git a/Cargo.lock b/Cargo.lock index ab24045..f4d3deb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1287,7 +1287,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "ore-api" -version = "2.0.0-beta.4" +version = "2.0.0-beta.5" dependencies = [ "array-const-fn-init", "bytemuck", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "ore-program" -version = "2.0.0-beta.4" +version = "2.0.0-beta.5" dependencies = [ "drillx", "mpl-token-metadata", @@ -1320,7 +1320,7 @@ dependencies = [ [[package]] name = "ore-utils" -version = "2.0.0-beta.4" +version = "2.0.0-beta.5" dependencies = [ "bytemuck", "solana-program", diff --git a/Cargo.toml b/Cargo.toml index e78e68e..95b762e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ resolver = "2" members = ["api", "program", "utils"] [workspace.package] -version = "2.0.0-beta.4" +version = "2.0.0-beta.5" edition = "2021" license = "Apache-2.0" homepage = "https://ore.supply" diff --git a/api/src/consts.rs b/api/src/consts.rs index 5ae12b9..d7d0470 100644 --- a/api/src/consts.rs +++ b/api/src/consts.rs @@ -150,3 +150,11 @@ pub const TREASURY_TOKENS_ADDRESS: Pubkey = Pubkey::new_from_array( ) .0, ); + +/// The address of the Solana compute budget program. +pub const COMPUTE_BUDGET_PROGRAM_ID: Pubkey = + pubkey!("ComputeBudget111111111111111111111111111111"); + +/// The address of the CU-optimized Solana noop program. +// pub const NOOP_PROGRAM_ID: Pubkey = pubkey!("noop8ytexvkpCuqbf6FB89BSuNemHtPRqaNC31GWivW"); +pub const NOOP_PROGRAM_ID: Pubkey = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"); diff --git a/api/src/error.rs b/api/src/error.rs index b7e96e2..159fc01 100644 --- a/api/src/error.rs +++ b/api/src/error.rs @@ -19,8 +19,8 @@ pub enum OreError { Spam = 5, #[error("The maximum supply has been reached")] MaxSupply = 6, - #[error("Only one hash may be validated per transaction")] - TransactionInvalid = 7, + #[error("The proof does not match the expected account")] + AuthFailed = 7, } impl From for ProgramError { diff --git a/api/src/instruction.rs b/api/src/instruction.rs index fdf0469..ff4f5fa 100644 --- a/api/src/instruction.rs +++ b/api/src/instruction.rs @@ -224,6 +224,15 @@ pub fn close(signer: Pubkey) -> Instruction { } } +/// Builds an auth instruction. +pub fn auth(proof: Pubkey) -> Instruction { + Instruction { + program_id: NOOP_PROGRAM_ID, + accounts: vec![], + data: proof.to_bytes().to_vec(), + } +} + /// Builds a mine instruction. pub fn mine( signer: Pubkey, diff --git a/program/src/mine.rs b/program/src/mine.rs index f5d1076..bc8f50a 100644 --- a/program/src/mine.rs +++ b/program/src/mine.rs @@ -5,7 +5,7 @@ use ore_api::{ consts::*, error::OreError, event::MineEvent, - instruction::{MineArgs, OreInstruction}, + instruction::MineArgs, loaders::*, state::{Bus, Config, Proof}, }; @@ -18,10 +18,11 @@ use solana_program::{ entrypoint::ProgramResult, log::sol_log, program_error::ProgramError, + pubkey::Pubkey, sanitize::SanitizeError, - serialize_utils::{read_pubkey, read_u16, read_u8}, + serialize_utils::{read_pubkey, read_u16}, slot_hashes::SlotHash, - sysvar::{self, instructions::load_current_index, Sysvar}, + sysvar::{self, Sysvar}, }; use crate::utils::AccountDeserialize; @@ -37,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>(accounts: &'a [AccountInfo<'info>], data: &[u8]) -> ProgramResult { @@ -56,9 +58,13 @@ pub fn process_mine<'a, 'info>(accounts: &'a [AccountInfo<'info>], data: &[u8]) 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()); + // Authenticate the proof account + if let Ok(Some(auth_address)) = authenticate(&instructions_sysvar.data.borrow()) { + if auth_address.ne(proof_info.key) { + return Err(OreError::AuthFailed.into()); + } + } else { + return Err(OreError::AuthFailed.into()); } // Validate epoch is active. @@ -177,50 +183,48 @@ pub fn process_mine<'a, 'info>(accounts: &'a [AccountInfo<'info>], data: &[u8]) Ok(()) } -/// Require that there is only one `mine` instruction per transaction and it is called from the -/// top level of the transaction. +/// Get the authenticated pubkey. /// -/// The intent here is to disincentivize sybil. As long as a user can fit multiple hashes in a single +/// The intent here is to disincentivize sybil. If a user can fit multiple hashes into 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; +/// +/// We solve this by "authenticating" the proof account on every mine instruction. That is, +/// every transaction with a `mine` instruction needs to include an `auth` instruction that +/// specifies the proof account that will be used. The `auth` instruction must be first ORE +/// instruction in the transaction. The `mine` instruction should error out if the provided proof +/// account doesn't match the authenticated address. +/// +/// Errors if: +/// - Fails to find and parse an authentication address. +fn authenticate(data: &[u8]) -> Result, SanitizeError> { + // Start the current byte index at 0 + let mut curr = 0; + let num_instructions = read_u16(&mut curr, data)?; + let pc = curr; + + // Iterate through the transaction instructions 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); - } - } - } + // Get byte counter + curr = pc + i * 2; + curr = read_u16(&mut curr, data)? as usize; + + // Read the instruction program id + let num_accounts = read_u16(&mut curr, data)? as usize; + curr += num_accounts * 33; + let program_id = read_pubkey(&mut curr, data)?; + + // Introspect on the first non compute budget instruction + if program_id.ne(&COMPUTE_BUDGET_PROGRAM_ID) { + // Read address from ix data + curr += 2; + let address = read_pubkey(&mut curr, data)?; + return Ok(Some(address)); } } - Ok(true) + Ok(None) }