diff --git a/Cargo.lock b/Cargo.lock index b634c3c..a5a35c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1134,6 +1134,17 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +[[package]] +name = "core_affinity" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622892f5635ce1fc38c8f16dfc938553ed64af482edb5e150bf4caedbfcb2304" +dependencies = [ + "libc", + "num_cpus", + "winapi", +] + [[package]] name = "coreaudio-rs" version = "0.11.3" @@ -2643,7 +2654,7 @@ dependencies = [ [[package]] name = "more-cli" -version = "1.1.2" +version = "2.0.0" dependencies = [ "bincode", "bs58 0.5.1", @@ -2652,6 +2663,7 @@ dependencies = [ "chrono", "clap 4.4.12", "colored", + "core_affinity", "drillx", "futures", "num_cpus", @@ -2670,6 +2682,7 @@ dependencies = [ "spl-associated-token-account", "spl-token", "tokio", + "url", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c6f6f32..52be73c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "more-cli" -version = "1.1.2" +version = "2.0.0" edition = "2021" license = "Apache-2.0" description = "A command line interface for ORE cryptocurrency mining. Tailored by Miraland Labs." @@ -26,6 +26,7 @@ cached = "0.46.1" chrono = "0.4.38" clap = { version = "4.4.12", features = ["derive"] } colored = "2.0" +core_affinity = "0.8.1" drillx = "2.0.0" # drillx = { git = "https://github.com/regolith-labs/drillx", branch = "master", features = ["solana"] } futures = "0.3.30" @@ -51,6 +52,7 @@ spl-associated-token-account = { version = "^2.3", features = [ "no-entrypoint", ] } tokio = "1.35.1" +url = "2.5" # [patch.crates-io] # drillx = { path = "../drillx/drillx" } diff --git a/README.md b/README.md index 0cda94a..3cafca3 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,30 @@ To install the CLI, use [cargo](https://doc.rust-lang.org/cargo/getting-started/ cargo install more-cli ``` + +### Dependencies +If you run into issues during installation, please install the following dependencies for your operating system and try again: + +#### Linux +``` +sudo apt-get install openssl pkg-config libssl-dev +``` + +#### MacOS (using [Homebrew](https://brew.sh/)) +``` +brew install openssl pkg-config + +# If you encounter issues with OpenSSL, you might need to set the following environment variables: +export PATH="/usr/local/opt/openssl/bin:$PATH" +export LDFLAGS="-L/usr/local/opt/openssl/lib" +export CPPFLAGS="-I/usr/local/opt/openssl/include" +``` + +#### Windows (using [Chocolatey](https://chocolatey.org/)) +``` +choco install openssl pkgconfiglite +``` + ## Build To build the codebase from scratch, checkout the repo and use cargo to build: diff --git a/src/args.rs b/src/args.rs index 81f9d88..c518834 100644 --- a/src/args.rs +++ b/src/args.rs @@ -16,10 +16,10 @@ pub struct BenchmarkArgs { long, short, value_name = "THREAD_COUNT", - help = "The number of threads to use during the benchmark", + help = "The number of cores to use during the benchmark", default_value = "1" )] - pub threads: u64, + pub cores: u64, } #[derive(Parser, Debug)] @@ -54,7 +54,15 @@ pub struct InitializeArgs {} #[derive(Parser, Debug)] pub struct MineArgs { - // #[cfg(not(feature = "gpu"))] + #[arg( + long, + short, + value_name = "CORES_COUNT", + help = "The number of CPU cores to allocate to mining", + default_value = "1" + )] + pub cores: Option, + #[arg( long, short, @@ -62,7 +70,7 @@ pub struct MineArgs { help = "The number of CPU threads to allocate to mining", default_value = "1" )] - pub threads: u64, + pub threads: Option, #[arg( long, @@ -74,6 +82,16 @@ pub struct MineArgs { pub buffer_time: u64, } +#[derive(Parser, Debug)] +pub struct ProofArgs { + #[arg( + index = 0, + value_name = "ADDRESS", + help = "The address of the proof to fetch" + )] + pub address: Option, +} + #[derive(Parser, Debug)] pub struct RewardsArgs {} diff --git a/src/benchmark.rs b/src/benchmark.rs index f8aa4a0..22bfffa 100644 --- a/src/benchmark.rs +++ b/src/benchmark.rs @@ -1,5 +1,6 @@ use std::{sync::Arc, time::Instant}; +use drillx::equix; use solana_rpc_client::spinner; use crate::{args::BenchmarkArgs, Miner}; @@ -8,8 +9,8 @@ const TEST_DURATION: i64 = 30; impl Miner { pub async fn benchmark(&self, args: BenchmarkArgs) { - // Check num threads - self.check_num_cores(args.threads); + // Check num cores + self.check_num_cores(args.cores); // Dispatch job to each thread let challenge = [0; 32]; @@ -18,16 +19,33 @@ impl Miner { "Benchmarking. This will take {} sec...", TEST_DURATION )); - let handles: Vec<_> = (0..args.threads) + let core_ids = core_affinity::get_core_ids().unwrap(); + let handles: Vec<_> = core_ids + .into_iter() .map(|i| { std::thread::spawn({ move || { let timer = Instant::now(); - let first_nonce = u64::MAX.saturating_div(args.threads).saturating_mul(i); + let first_nonce = u64::MAX + .saturating_div(args.cores) + .saturating_mul(i.id as u64); let mut nonce = first_nonce; + let mut memory = equix::SolverMemory::new(); loop { + // Return if core should not be used + if (i.id as u64).ge(&args.cores) { + return 0; + } + + // Pin to core + let _ = core_affinity::set_for_current(i); + // Create hash - let _hx = drillx::hash(&challenge, &nonce.to_le_bytes()); + let _hx = drillx::hash_with_memory( + &mut memory, + &challenge, + &nonce.to_le_bytes(), + ); // Increment nonce nonce += 1; diff --git a/src/busses.rs b/src/busses.rs index 56bed8b..369d5c3 100644 --- a/src/busses.rs +++ b/src/busses.rs @@ -4,7 +4,7 @@ use ore_api::{ state::Bus, }; use ore_utils::AccountDeserialize; -use solana_program::pubkey::Pubkey; +// use solana_program::pubkey::Pubkey; impl Miner { // // MI: vanilla version @@ -38,47 +38,26 @@ impl Miner { } } - // // MI - // pub async fn _find_max_ore_bus(&self) -> Pubkey { + // // MI: inspired by DanielChrobak + // pub async fn find_bus(&self) -> Pubkey { // let client = self.rpc_client.clone(); // let mut max_rewards: f64 = 0.; - // let mut max_ore_bus: Pubkey = Pubkey::default(); - // for address in BUS_ADDRESSES.iter() { - // let data = client.get_account_data(address).await.unwrap(); - // match Bus::try_from_bytes(&data) { - // Ok(bus) => { + // let mut max_ore_bus: Pubkey = BUS_ADDRESSES[0]; + // let data = client.get_multiple_accounts(&BUS_ADDRESSES).await.unwrap(); + + // for (address, account) in BUS_ADDRESSES.iter().zip(data.iter()) { + // if let Some(account) = account { + // let data_bytes = &account.data[..]; // Extract data bytes + // if let Ok(bus) = Bus::try_from_bytes(data_bytes) { // let rewards = (bus.rewards as f64) / 10f64.powf(TOKEN_DECIMALS as f64); // if rewards > max_rewards { // max_rewards = rewards; // max_ore_bus = *address; // } // } - // Err(_) => {} // } // } + // max_ore_bus // } - - // MI: inspired by DanielChrobak - pub async fn find_bus(&self) -> Pubkey { - let client = self.rpc_client.clone(); - let mut max_rewards: f64 = 0.; - let mut max_ore_bus: Pubkey = BUS_ADDRESSES[0]; - let data = client.get_multiple_accounts(&BUS_ADDRESSES).await.unwrap(); - - for (address, account) in BUS_ADDRESSES.iter().zip(data.iter()) { - if let Some(account) = account { - let data_bytes = &account.data[..]; // Extract data bytes - if let Ok(bus) = Bus::try_from_bytes(data_bytes) { - let rewards = (bus.rewards as f64) / 10f64.powf(TOKEN_DECIMALS as f64); - if rewards > max_rewards { - max_rewards = rewards; - max_ore_bus = *address; - } - } - } - } - - max_ore_bus - } } diff --git a/src/dynamic_fee.rs b/src/dynamic_fee.rs index ad028f1..687dedd 100644 --- a/src/dynamic_fee.rs +++ b/src/dynamic_fee.rs @@ -3,113 +3,127 @@ use crate::Miner; use ore_api::consts::BUS_ADDRESSES; use reqwest::Client; use serde_json::{json, Value}; +use url::Url; + +enum FeeStrategy { + Alchemy, + Helius, + Triton, +} impl Miner { - pub async fn dynamic_fee(&self) -> u64 { - let ore_addresses: Vec = - std::iter::once("oreV2ZymfyeXgNgBdqMkumTqqAprVqgBWQfoYkrtKWQ".to_string()) - .chain(BUS_ADDRESSES.iter().map(|pubkey| pubkey.to_string())) - .collect(); + pub async fn dynamic_fee(&self) -> Option { + // Get url + let rpc_url = self + .dynamic_fee_url + .clone() + .unwrap_or(self.rpc_client.url()); - match &self.dynamic_fee_strategy { - None => self.priority_fee.unwrap_or(0), - Some(strategy) => { - let client = Client::new(); + // Select fee estiamte strategy + let host = Url::parse(&rpc_url) + .unwrap() + .host_str() + .unwrap() + .to_string(); + let strategy = if host.contains("helius-rpc.com") { + FeeStrategy::Helius + } else if host.contains("rpcpool.com") { + FeeStrategy::Triton + } else if host.contains("alchemy.com") { + FeeStrategy::Alchemy + } else { + return None; + }; - let body = match strategy.as_str() { - "helius" => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getPriorityFeeEstimate", - "params": [{ - "accountKeys": ore_addresses, - "options": { - "recommended": true - } - }] - }) - } - "triton" => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getRecentPrioritizationFees", - "params": [ - ore_addresses, - { - "percentile": 5000, - } - ] - }) - } - "alchemy" => { - json!({ - "jsonrpc": "2.0", - "id": "priority-fee-estimate", - "method": "getRecentPrioritizationFees", - "params": [ - ore_addresses, - ] - }) - } - _ => return self.priority_fee.unwrap_or(0), - }; + // Build fee estimate request + let client = Client::new(); + let ore_addresses: Vec = std::iter::once(ore_api::ID.to_string()) + .chain(BUS_ADDRESSES.iter().map(|pubkey| pubkey.to_string())) + .collect(); + let body = match strategy { + FeeStrategy::Helius => { + json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getPriorityFeeEstimate", + "params": [{ + "accountKeys": ore_addresses, + "options": { + "recommended": true + } + }] + }) + } + FeeStrategy::Triton => { + json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getRecentPrioritizationFees", + "params": [ + ore_addresses, + { + "percentile": 5000, + } + ] + }) + } + FeeStrategy::Alchemy => { + json!({ + "jsonrpc": "2.0", + "id": "priority-fee-estimate", + "method": "getRecentPrioritizationFees", + "params": [ + ore_addresses, + ] + }) + } + }; - let response: Value = client - .post(self.dynamic_fee_url.as_ref().unwrap()) - .json(&body) - .send() - .await - .unwrap() - .json() - .await - .unwrap(); + // Send request + let response: Value = client + .post(rpc_url) + .json(&body) + .send() + .await + .unwrap() + .json() + .await + .unwrap(); - let calculated_fee = - match strategy.as_str() { - "helius" => response["result"]["priorityFeeEstimate"] - .as_f64() - .map(|fee| fee as u64) - .ok_or_else(|| { - format!("Failed to parse priority fee. Response: {:?}", response) - }) - .unwrap(), - "triton" => response["result"] - .as_array() - .and_then(|arr| arr.last()) - .and_then(|last| last["prioritizationFee"].as_u64()) - .ok_or_else(|| { - format!("Failed to parse priority fee. Response: {:?}", response) - }) - .unwrap(), - "alchemy" => response["result"] - .as_array() - .and_then(|arr| { - Some( - arr.into_iter() - .map(|v| v["prioritizationFee"].as_u64().unwrap()) - .collect::>(), - ) - }) - .and_then(|fees| { - Some((fees.iter().sum::() as f32 / fees.len() as f32).ceil() - as u64) - }) - .ok_or_else(|| { - format!("Failed to parse priority fee. Response: {:?}", response) - }) - .unwrap(), - _ => return self.priority_fee.unwrap_or(0), - }; + // Parse response + let calculated_fee = match strategy { + FeeStrategy::Helius => response["result"]["priorityFeeEstimate"] + .as_f64() + .map(|fee| fee as u64) + .ok_or_else(|| format!("Failed to parse priority fee. Response: {:?}", response)) + .unwrap(), + FeeStrategy::Triton => response["result"] + .as_array() + .and_then(|arr| arr.last()) + .and_then(|last| last["prioritizationFee"].as_u64()) + .ok_or_else(|| format!("Failed to parse priority fee. Response: {:?}", response)) + .unwrap(), + FeeStrategy::Alchemy => response["result"] + .as_array() + .and_then(|arr| { + Some( + arr.into_iter() + .map(|v| v["prioritizationFee"].as_u64().unwrap()) + .collect::>(), + ) + }) + .and_then(|fees| { + Some((fees.iter().sum::() as f32 / fees.len() as f32).ceil() as u64) + }) + .ok_or_else(|| format!("Failed to parse priority fee. Response: {:?}", response)) + .unwrap(), + }; - // Check if the calculated fee is higher than self.dynamic_fee_max - if let Some(max_fee) = self.dynamic_fee_max { - calculated_fee.min(max_fee) - } else { - calculated_fee - } - } + // Check if the calculated fee is higher than max + if let Some(max_fee) = self.priority_fee { + Some(calculated_fee.min(max_fee)) + } else { + Some(calculated_fee) } } } diff --git a/src/main.rs b/src/main.rs index eeb6062..9df3cc6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,16 +6,17 @@ mod claim; mod close; mod config; mod cu_limits; +mod dynamic_fee; #[cfg(feature = "admin")] mod initialize; mod mine; mod open; +mod proof; mod rewards; mod send_and_confirm; mod stake; mod upgrade; mod utils; -mod dynamic_fee; use std::sync::Arc; @@ -31,8 +32,7 @@ struct Miner { pub keypair_filepath: Option, pub priority_fee: Option, pub dynamic_fee_url: Option, - pub dynamic_fee_strategy: Option, - pub dynamic_fee_max: Option, + pub dynamic_fee: bool, pub rpc_client: Arc, pub fee_payer_filepath: Option, pub no_sound_notification: bool, @@ -61,6 +61,9 @@ enum Commands { #[command(about = "Start mining")] Mine(MineArgs), + #[command(about = "Fetch a proof account by address")] + Proof(ProofArgs), + #[command(about = "Fetch the current reward rate for each difficulty level")] Rewards(RewardsArgs), @@ -98,7 +101,7 @@ struct Args { #[arg( long, value_name = "KEYPAIR_FILEPATH", - help = "Filepath to keypair to use", + help = "Filepath to keypair to use.", global = true )] keypair: Option, @@ -106,16 +109,16 @@ struct Args { #[arg( long, value_name = "FEE_PAYER_FILEPATH", - help = "Filepath to keypair to use for fee payer", + help = "Filepath to keypair to use as transaction fee payer.", global = true )] - fee_payer_filepath: Option, + fee_payer: Option, #[arg( long, value_name = "MICROLAMPORTS", - help = "Number of microlamports to pay as priority fee per transaction", - default_value = "0", + help = "Price to pay for compute units. If dynamic fees are being used, this value will be the max.", + default_value = "500000", global = true )] priority_fee: Option, @@ -123,28 +126,13 @@ struct Args { #[arg( long, value_name = "DYNAMIC_FEE_URL", - help = "RPC URL to use for dynamic fee estimation. If set will enable dynamic fee pricing instead of static priority fee pricing.", + help = "RPC URL to use for dynamic fee estimation.", global = true )] dynamic_fee_url: Option, - #[arg( - long, - value_name = "DYNAMIC_FEE_STRATEGY", - help = "Strategy to use for dynamic fee estimation. Must be one of 'helius', or 'triton'.", - default_value = "helius", - global = true - )] - dynamic_fee_strategy: Option, - - #[arg( - long, - value_name = "DYNAMIC_FEE_MAX", - help = "Maximum priority fee to use for dynamic fee estimation.", - default_value = "500000", - global = true - )] - dynamic_fee_max: Option, + #[arg(long, help = "Use dynamic priority fees", global = true)] + dynamic_fee: bool, /// Mine with sound notification on/off #[arg( @@ -179,7 +167,7 @@ async fn main() { // Initialize miner. let cluster = args.rpc.unwrap_or(cli_config.json_rpc_url); let default_keypair = args.keypair.unwrap_or(cli_config.keypair_path.clone()); - let fee_payer_filepath = args.fee_payer_filepath.unwrap_or(cli_config.keypair_path.clone()); + let fee_payer_filepath = args.fee_payer.unwrap_or(default_keypair.clone()); let rpc_client = RpcClient::new_with_commitment(cluster, CommitmentConfig::confirmed()); let miner = Arc::new(Miner::new( @@ -187,8 +175,7 @@ async fn main() { args.priority_fee, Some(default_keypair), args.dynamic_fee_url, - args.dynamic_fee_strategy, - args.dynamic_fee_max, + args.dynamic_fee, Some(fee_payer_filepath), args.no_sound_notification, )); @@ -216,6 +203,9 @@ async fn main() { Commands::Mine(args) => { miner.mine(args).await; } + Commands::Proof(args) => { + miner.proof(args).await; + } Commands::Rewards(_) => { miner.rewards().await; } @@ -238,8 +228,7 @@ impl Miner { priority_fee: Option, keypair_filepath: Option, dynamic_fee_url: Option, - dynamic_fee_strategy: Option, - dynamic_fee_max: Option, + dynamic_fee: bool, fee_payer_filepath: Option, no_sound_notification: bool, ) -> Self { @@ -248,8 +237,7 @@ impl Miner { keypair_filepath, priority_fee, dynamic_fee_url, - dynamic_fee_strategy, - dynamic_fee_max, + dynamic_fee, fee_payer_filepath, no_sound_notification, } diff --git a/src/mine.rs b/src/mine.rs index 456ce1e..119a995 100644 --- a/src/mine.rs +++ b/src/mine.rs @@ -8,8 +8,9 @@ use drillx::{ }; use ore_api::{ consts::{BUS_ADDRESSES, BUS_COUNT, EPOCH_DURATION}, - state::{Config, Proof}, + state::{Bus, Config, Proof}, }; +use ore_utils::AccountDeserialize; use rand::Rng; use solana_program::pubkey::Pubkey; use solana_rpc_client::spinner; @@ -18,55 +19,93 @@ use solana_sdk::signer::Signer; use crate::{ args::MineArgs, send_and_confirm::ComputeBudget, - utils::{amount_u64_to_string, get_clock, get_config, get_proof_with_authority, proof_pubkey}, + utils::{ + amount_u64_to_string, get_clock, get_config, get_updated_proof_with_authority, proof_pubkey, + }, Miner, }; +enum ParallelStrategy { + Cores(u64), + Threads(u64), +} + impl Miner { pub async fn mine(&self, args: MineArgs) { - // Register, if needed. + // Open account, if needed. let signer = self.signer(); self.open().await; + let mut parallel_strategy = ParallelStrategy::Cores(1); + // Check num threads - self.check_num_cores(args.threads); + // self.check_num_cores(args.threads); + if let Some(cores) = args.cores { + self.check_num_cores(cores); + parallel_strategy = ParallelStrategy::Cores(cores); + } else if let Some(threads) = args.threads { + self.check_num_threads(threads); + parallel_strategy = ParallelStrategy::Threads(threads); + } else { + println!( + "{} No parallel strategy provided. Default to 1 core", + "WARNING".bold().yellow(), + ); + }; // Start mining loop + let mut last_hash_at = 0; loop { // Fetch proof let config = get_config(&self.rpc_client).await; - let proof = get_proof_with_authority(&self.rpc_client, signer.pubkey()).await; + let proof = + get_updated_proof_with_authority(&self.rpc_client, signer.pubkey(), last_hash_at) + .await; + last_hash_at = proof.last_hash_at; println!( "\nStake: {} ORE\n Multiplier: {:12}x", amount_u64_to_string(proof.balance), calculate_multiplier(proof.balance, config.top_balance) ); - // Calc cutoff time + // Calculate cutoff time let cutoff_time = self.get_cutoff(proof, args.buffer_time).await; // Run drillx - let solution = Self::find_hash_par( - proof, - cutoff_time, - args.threads, - config.min_difficulty as u32, - ) - .await; - - // Submit most difficult hash - let mut compute_budget = 500_000; + let solution = match parallel_strategy { + ParallelStrategy::Cores(cores) => { + Self::find_hash_par_cores( + proof, + cutoff_time, + cores, + config.min_difficulty as u32, + ) + .await + } + ParallelStrategy::Threads(threads) => { + Self::find_hash_par_threads( + proof, + cutoff_time, + threads, + config.min_difficulty as u32, + ) + .await + } + }; + + // Build instruction set let mut ixs = vec![ore_api::instruction::auth(proof_pubkey(signer.pubkey()))]; + let mut compute_budget = 500_000; // if self.should_reset(config).await { if self.should_reset(config).await && rand::thread_rng().gen_range(0..100).eq(&0) { compute_budget += 100_000; ixs.push(ore_api::instruction::reset(signer.pubkey())); } + + // Build mine ix ixs.push(ore_api::instruction::mine( signer.pubkey(), signer.pubkey(), - // MI - // find_bus(), self.find_bus().await, solution, )); @@ -75,17 +114,118 @@ impl Miner { // self.send_and_confirm(&ixs, ComputeBudget::Fixed(compute_budget), false) // .await // .ok(); - if self.send_and_confirm(&ixs, ComputeBudget::Fixed(compute_budget), false) + if self + .send_and_confirm(&ixs, ComputeBudget::Fixed(compute_budget), false) .await - .is_ok() { - if ! self.no_sound_notification { + .is_ok() + { + if !self.no_sound_notification { utils::play_sound(); } } } } - async fn find_hash_par( + // MI: since 2.0 + async fn find_hash_par_cores( + proof: Proof, + cutoff_time: u64, + cores: u64, + min_difficulty: u32, + ) -> Solution { + // Dispatch job to each thread + let progress_bar = Arc::new(spinner::new_progress_bar()); + progress_bar.set_message("Mining..."); + let core_ids = core_affinity::get_core_ids().unwrap(); + let handles: Vec<_> = core_ids + .into_iter() + .map(|i| { + std::thread::spawn({ + let proof = proof.clone(); + let progress_bar = progress_bar.clone(); + let mut memory = equix::SolverMemory::new(); + move || { + // Return if core should not be used + if (i.id as u64).ge(&cores) { + return (0, 0, Hash::default()); + } + + // Pin to core + let _ = core_affinity::set_for_current(i); + + // Start hashing + let timer = Instant::now(); + let mut nonce = u64::MAX.saturating_div(cores).saturating_mul(i.id as u64); + let mut best_nonce = nonce; + let mut best_difficulty = 0; + let mut best_hash = Hash::default(); + loop { + // Create hash + if let Ok(hx) = drillx::hash_with_memory( + &mut memory, + &proof.challenge, + &nonce.to_le_bytes(), + ) { + let difficulty = hx.difficulty(); + if difficulty.gt(&best_difficulty) { + best_nonce = nonce; + best_difficulty = difficulty; + best_hash = hx; + } + } + + // Exit if time has elapsed + if nonce % 100 == 0 { + if timer.elapsed().as_secs().ge(&cutoff_time) { + if best_difficulty.ge(&min_difficulty) { + // Mine until min difficulty has been met + break; + } + } else if i.id == 0 { + progress_bar.set_message(format!( + "Mining... ({} sec remaining)", + cutoff_time.saturating_sub(timer.elapsed().as_secs()), + )); + } + } + + // Increment nonce + nonce += 1; + } + + // Return the best nonce + (best_nonce, best_difficulty, best_hash) + } + }) + }) + .collect(); + + // Join handles and return best nonce + let mut best_nonce = 0; + let mut best_difficulty = 0; + let mut best_hash = Hash::default(); + for h in handles { + if let Ok((nonce, difficulty, hash)) = h.join() { + if difficulty > best_difficulty { + best_difficulty = difficulty; + best_nonce = nonce; + best_hash = hash; + } + } + } + + // Update log + progress_bar.finish_with_message(format!( + "Best hash: {} (difficulty: {})", + bs58::encode(best_hash.h).into_string(), + best_difficulty + )); + + Solution::new(best_hash.d, best_nonce.to_le_bytes()) + } + + // MI: reserve threads approach + async fn find_hash_par_threads( proof: Proof, cutoff_time: u64, threads: u64, @@ -171,7 +311,19 @@ impl Miner { Solution::new(best_hash.d, best_nonce.to_le_bytes()) } - pub fn check_num_cores(&self, threads: u64) { + // MI: since 2.0 + pub fn check_num_cores(&self, cores: u64) { + let num_cores = num_cpus::get() as u64; + if cores.gt(&num_cores) { + println!( + "{} Cannot exceeds available cores ({})", + "WARNING".bold().yellow(), + num_cores + ); + } + } + + pub fn check_num_threads(&self, threads: u64) { // Check num threads let num_cores = num_cpus::get() as u64; if threads.gt(&num_cores) { @@ -202,14 +354,31 @@ impl Miner { .saturating_sub(clock.unix_timestamp) .max(0) as u64 } + + async fn find_bus(&self) -> Pubkey { + // Fetch the bus with the largest balance + if let Ok(accounts) = self.rpc_client.get_multiple_accounts(&BUS_ADDRESSES).await { + let mut top_bus_balance: u64 = 0; + let mut top_bus = BUS_ADDRESSES[0]; + for account in accounts { + if let Some(account) = account { + if let Ok(bus) = Bus::try_from_bytes(&account.data) { + if bus.rewards.gt(&top_bus_balance) { + top_bus_balance = bus.rewards; + top_bus = BUS_ADDRESSES[bus.id as usize]; + } + } + } + } + return top_bus; + } + + // Otherwise return a random bus + let i = rand::thread_rng().gen_range(0..BUS_COUNT); + BUS_ADDRESSES[i] + } } fn calculate_multiplier(balance: u64, top_balance: u64) -> f64 { 1.0 + (balance as f64 / top_balance as f64).min(1.0f64) } - -// TODO Pick a better strategy (avoid draining bus) -fn _find_bus() -> Pubkey { - let i = rand::thread_rng().gen_range(0..BUS_COUNT); - BUS_ADDRESSES[i] -} diff --git a/src/proof.rs b/src/proof.rs new file mode 100644 index 0000000..4e9d8f3 --- /dev/null +++ b/src/proof.rs @@ -0,0 +1,42 @@ +use std::str::FromStr; + +use ore_api::consts::TOKEN_DECIMALS; +use solana_program::pubkey::Pubkey; +use solana_sdk::signature::Signer; +use spl_token::amount_to_ui_amount; + +use crate::{ + args::ProofArgs, + utils::{get_proof, proof_pubkey}, + Miner, +}; + +impl Miner { + pub async fn proof(&self, args: ProofArgs) { + let signer = self.signer(); + let address = if let Some(address) = args.address { + Pubkey::from_str(&address).unwrap() + } else { + proof_pubkey(signer.pubkey()) + }; + let proof = get_proof(&self.rpc_client, address).await; + println!("Address: {:?}", address); + println!("Authority: {:?}", proof.authority); + println!( + "Balance: {:?} ORE", + amount_to_ui_amount(proof.balance, TOKEN_DECIMALS) + ); + println!( + "Last hash: {}", + solana_sdk::hash::Hash::new_from_array(proof.last_hash).to_string() + ); + println!("Last hash at: {:?}", proof.last_hash_at); + println!("Last stake at: {:?}", proof.last_stake_at); + println!("Miner: {:?}", proof.miner); + println!("Total hashes: {:?}", proof.total_hashes); + println!( + "Total rewards: {:?} ORE", + amount_to_ui_amount(proof.total_rewards, TOKEN_DECIMALS) + ); + } +} diff --git a/src/send_and_confirm.rs b/src/send_and_confirm.rs index fb81df4..44ef30c 100644 --- a/src/send_and_confirm.rs +++ b/src/send_and_confirm.rs @@ -24,11 +24,11 @@ const MIN_SOL_BALANCE: f64 = 0.005; const RPC_RETRIES: usize = 0; const _SIMULATION_RETRIES: usize = 4; -const GATEWAY_RETRIES: usize = 150; // MI, vanilla: 150, keep retries * delay same -const CONFIRM_RETRIES: usize = 1; +const GATEWAY_RETRIES: usize = 150; +const CONFIRM_RETRIES: usize = 8; // MI, 1 in version -const CONFIRM_DELAY: u64 = 0; -const GATEWAY_DELAY: u64 = 300; // MI, vanilla: 300, keep retries * delay same +const CONFIRM_DELAY: u64 = 500; // MI, 0 in version 1 +const GATEWAY_DELAY: u64 = 0; // MI, 300 in version 1 pub enum ComputeBudget { Dynamic, @@ -42,24 +42,14 @@ impl Miner { compute_budget: ComputeBudget, skip_confirm: bool, ) -> ClientResult { - let progress_bar = spinner::new_progress_bar(); let signer = self.signer(); let client = self.rpc_client.clone(); let fee_payer = self.fee_payer(); // Return error, if balance is zero - if let Ok(balance) = client.get_balance(&fee_payer.pubkey()).await { - if balance <= sol_to_lamports(MIN_SOL_BALANCE) { - panic!( - "{} Insufficient balance: {} SOL\nPlease top up with at least {} SOL", - "ERROR".bold().red(), - lamports_to_sol(balance), - MIN_SOL_BALANCE - ); - } - } + self.check_balance().await; - // Set compute units + // Set compute budget let mut final_ixs = vec![]; match compute_budget { ComputeBudget::Dynamic => { @@ -71,14 +61,12 @@ impl Miner { } } - let priority_fee = match &self.dynamic_fee_url { - Some(_) => self.dynamic_fee().await, - None => self.priority_fee.unwrap_or(0), - }; - + // Set compute unit price final_ixs.push(ComputeBudgetInstruction::set_compute_unit_price( - priority_fee, + self.priority_fee.unwrap_or(0), )); + + // Add in user instructions final_ixs.extend_from_slice(ixs); // Build tx @@ -91,48 +79,46 @@ impl Miner { }; let mut tx = Transaction::new_with_payer(&final_ixs, Some(&fee_payer.pubkey())); - // MI vanilla - // // Sign tx - // let (hash, _slot) = client - // .get_latest_blockhash_with_commitment(self.rpc_client.commitment()) - // .await - // .unwrap(); - - // Sign tx - let (hash, _slot) = loop { - match client - .get_latest_blockhash_with_commitment(self.rpc_client.commitment()) - .await - { - Ok((hash, _slot)) => break (hash, _slot), - Err(_) => {} - } - }; - - if signer.pubkey() == fee_payer.pubkey() { - tx.sign(&[&signer], hash); - } else { - tx.sign(&[&signer, &fee_payer], hash); - } - // Submit tx + let progress_bar = spinner::new_progress_bar(); let mut attempts = 0; loop { - let message = match &self.dynamic_fee_url { - Some(_) => format!( - "Submitting transaction... (attempt {} with dynamic priority fee of {} via {})", - attempts, - priority_fee, - self.dynamic_fee_strategy.as_ref().unwrap() - ), - None => format!( - "Submitting transaction... (attempt {} with static priority fee of {})", - attempts, priority_fee - ), - }; + progress_bar.set_message(format!("Submitting transaction... (attempt {})", attempts,)); - progress_bar.set_message(message); + // Sign tx with a new blockhash (after approximately ~45 sec) + if attempts % 10 == 0 { + // Reset the compute unit price + if self.dynamic_fee { + let fee = if let Some(fee) = self.dynamic_fee().await { + progress_bar.println(format!(" Priority fee: {} microlamports", fee)); + fee + } else { + let fee = self.priority_fee.unwrap_or(0); + progress_bar.println(format!(" {} Dynamic fees not supported by this RPC. Falling back to static value: {} microlamports", "WARNING".bold().yellow(), fee)); + fee + }; + final_ixs.remove(1); + final_ixs.insert(1, ComputeBudgetInstruction::set_compute_unit_price(fee)); + } + // Resign tx + let (hash, _slot) = loop { + match client + .get_latest_blockhash_with_commitment(self.rpc_client.commitment()) + .await + { + Ok((hash, _slot)) => break (hash, _slot), + Err(_) => {} + } + }; + if signer.pubkey() == fee_payer.pubkey() { + tx.sign(&[&signer], hash); + } else { + tx.sign(&[&signer, &fee_payer], hash); + } + } + + // Send transaction match client.send_transaction_with_config(&tx, send_cfg).await { Ok(sig) => { // Skip confirmation @@ -141,7 +127,7 @@ impl Miner { return Ok(sig); } - // Confirm the tx landed + // Confirm transaction for _ in 0..CONFIRM_RETRIES { std::thread::sleep(Duration::from_millis(CONFIRM_DELAY)); match client.get_signature_statuses(&[sig]).await { @@ -215,6 +201,24 @@ impl Miner { } } + pub async fn check_balance(&self) { + // Throw error if balance is less than min + if let Ok(balance) = self + .rpc_client + .get_balance(&self.fee_payer().pubkey()) + .await + { + if balance <= sol_to_lamports(MIN_SOL_BALANCE) { + panic!( + "{} Insufficient balance: {} SOL\nPlease top up with at least {} SOL", + "ERROR".bold().red(), + lamports_to_sol(balance), + MIN_SOL_BALANCE + ); + } + } + } + // TODO fn _simulate(&self) { diff --git a/src/utils.rs b/src/utils.rs index 469c8f1..13fb565 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,7 @@ -use std::io::{Cursor, Read}; +use std::{ + io::{Cursor, Read}, + time::Duration, +}; use cached::proc_macro::cached; use ore_api::{ @@ -34,6 +37,20 @@ pub async fn get_proof_with_authority(client: &RpcClient, authority: Pubkey) -> get_proof(client, proof_address).await } +pub async fn get_updated_proof_with_authority( + client: &RpcClient, + authority: Pubkey, + lash_hash_at: i64, +) -> Proof { + loop { + let proof = get_proof_with_authority(client, authority).await; + if proof.last_hash_at.gt(&lash_hash_at) { + return proof; + } + std::thread::sleep(Duration::from_millis(1000)); + } +} + pub async fn get_proof(client: &RpcClient, address: Pubkey) -> Proof { let data = client .get_account_data(&address) @@ -51,9 +68,7 @@ pub async fn get_clock(client: &RpcClient) -> Clock { let data: Vec; loop { - match client - .get_account_data(&sysvar::clock::ID) - .await { + match client.get_account_data(&sysvar::clock::ID).await { Ok(d) => { data = d; break; @@ -106,7 +121,7 @@ pub fn play_sound() { let cursor = Cursor::new(bytes); sink.append(rodio::Decoder::new(cursor).unwrap()); sink.sleep_until_end(); - }, + } Err(_) => print!("\x07"), } }