From ce26787a110bb04fe2b7e0bce6428bc1aff6a489 Mon Sep 17 00:00:00 2001 From: Uncle Stretch Date: Tue, 29 Jul 2025 14:38:49 +0300 Subject: [PATCH] rustfmt staking-miner and fix typos Signed-off-by: Uncle Stretch --- utils/staking-miner/Cargo.toml | 2 +- utils/staking-miner/src/client.rs | 9 +- utils/staking-miner/src/commands/dry_run.rs | 122 +++ .../src/commands/emergency_solution.rs | 90 ++ utils/staking-miner/src/commands/mod.rs | 7 + utils/staking-miner/src/commands/monitor.rs | 858 ++++++++++++++++++ utils/staking-miner/src/epm.rs | 164 ++-- utils/staking-miner/src/helpers.rs | 46 +- utils/staking-miner/src/main.rs | 37 +- utils/staking-miner/src/opt.rs | 13 +- utils/staking-miner/src/prelude.rs | 5 +- utils/staking-miner/src/prometheus.rs | 48 +- utils/staking-miner/src/signer.rs | 5 +- utils/staking-miner/src/static_types.rs | 41 +- 14 files changed, 1317 insertions(+), 130 deletions(-) create mode 100644 utils/staking-miner/src/commands/dry_run.rs create mode 100644 utils/staking-miner/src/commands/emergency_solution.rs create mode 100644 utils/staking-miner/src/commands/mod.rs create mode 100644 utils/staking-miner/src/commands/monitor.rs diff --git a/utils/staking-miner/Cargo.toml b/utils/staking-miner/Cargo.toml index d9f9fe3..fd82fee 100644 --- a/utils/staking-miner/Cargo.toml +++ b/utils/staking-miner/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ghost-miner" -version = "1.5.0" +version = "1.5.1" description = "A tool to submit NPoS election solutions for Ghost and Casper Network" license.workspace = true authors.workspace = true diff --git a/utils/staking-miner/src/client.rs b/utils/staking-miner/src/client.rs index de71bfb..4632ee0 100644 --- a/utils/staking-miner/src/client.rs +++ b/utils/staking-miner/src/client.rs @@ -6,7 +6,7 @@ use subxt::backend::rpc::RpcClient as RawRpcClient; #[derive(Clone, Debug)] pub struct Client { /// Access to typed rpc calls from subxt. - rpc:: RpcClient, + rpc: RpcClient, /// Access to chain APIs such as storage, events etc. chain_api: ChainClient, } @@ -30,13 +30,16 @@ impl Client { "failed to connect to client due to {:?}, retrying soon...", e ); - }, + } }; tokio::time::sleep(std::time::Duration::from_millis(2_500)).await; }; let chain_api = ChainClient::from_rpc_client(rpc.clone()).await?; - Ok(Self { rpc: RpcClient::new(rpc), chain_api }) + Ok(Self { + rpc: RpcClient::new(rpc), + chain_api, + }) } /// Get a reference to the RPC interface exposed by subxt. diff --git a/utils/staking-miner/src/commands/dry_run.rs b/utils/staking-miner/src/commands/dry_run.rs new file mode 100644 index 0000000..c274eb7 --- /dev/null +++ b/utils/staking-miner/src/commands/dry_run.rs @@ -0,0 +1,122 @@ +use crate::{ + client::Client, + epm, + error::Error, + helpers::{signer_from_seed_or_path, storage_at}, + opt::Solver, + prelude::*, + static_types, +}; +use clap::Parser; +use codec::Encode; +use pallet_election_provider_multi_phase::RawSolution; + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub struct DryRunConfig { + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[clap(long)] + pub at: Option, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// Force create a new snapshot, else expect one to exist onchain. + #[clap(long)] + pub force_snapshot: bool, + + /// The number of winners to take, instead of the `desired_targets` in snapshot. + // Doing this would cause the dry-run to typically fail, but that's fine, the program should + // still print out some score, and that should be it. + #[clap(long)] + pub force_winner_count: Option, + + /// The path to a file containing the seed of the account. If the file is not found, the seed is + /// used as-is. If this is not provided, we won't attempt to submit anything. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[clap(long, short, env = "SEED")] + pub seed_or_path: Option, +} + +pub async fn dry_run_cmd(client: Client, config: DryRunConfig) -> Result<(), Error> +where + T: MinerConfig + + Send + + Sync + + 'static, + T::Solution: Send, +{ + let storage = storage_at(config.at, client.chain_api()).await?; + let round = storage + .fetch_or_default(&runtime::storage().election_provider_multi_phase().round()) + .await?; + + let miner_solution = epm::fetch_snapshot_and_mine_solution::( + client.chain_api(), + config.at, + config.solver, + round, + config.force_winner_count, + ) + .await?; + + let solution = miner_solution.solution(); + let score = miner_solution.score(); + let raw_solution = RawSolution { + solution, + score, + round, + }; + + log::info!( + target: LOG_TARGET, + "solution score {:?} / length {:?}", + score, + raw_solution.encode().len(), + ); + + // Now we've logged the score, check whether the solution makes sense. No point doing this + // if force_winner_count is selected since it'll definitely fail in that case. + if config.force_winner_count.is_none() { + miner_solution.feasibility_check()?; + } + + // If an account seed or path is provided, then do a dry run to the node. Otherwise, + // we've logged the solution above and we do nothing else. + if let Some(seed_or_path) = &config.seed_or_path { + let signer = signer_from_seed_or_path(seed_or_path)?; + let account_info = storage + .fetch( + &runtime::storage() + .system() + .account(signer.public_key().to_account_id()), + ) + .await? + .ok_or(Error::AccountDoesNotExists)?; + + log::info!(target: LOG_TARGET, "Loaded account {}, {:?}", signer.public_key().to_account_id(), account_info); + + let nonce = client + .rpc() + .system_account_next_index(&signer.public_key().to_account_id()) + .await?; + let tx = epm::signed_solution(raw_solution)?; + let xt = client.chain_api().tx().create_signed_with_nonce( + &tx, + &signer, + nonce, + Default::default(), + )?; + let dry_run_bytes = client.rpc().dry_run(xt.encoded(), config.at).await?; + let dry_run_result = dry_run_bytes.into_dry_run_result(&client.chain_api().metadata())?; + + log::info!(target: LOG_TARGET, "dry-run outcome is {:?}", dry_run_result); + } + + Ok(()) +} diff --git a/utils/staking-miner/src/commands/emergency_solution.rs b/utils/staking-miner/src/commands/emergency_solution.rs new file mode 100644 index 0000000..1fb6a36 --- /dev/null +++ b/utils/staking-miner/src/commands/emergency_solution.rs @@ -0,0 +1,90 @@ +use crate::{ + client::Client, epm, error::Error, helpers::storage_at, opt::Solver, prelude::*, static_types, +}; +use clap::Parser; +use codec::Encode; +use sp_core::hexdisplay::HexDisplay; +use std::io::Write; +use subxt::tx::TxPayload; + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub struct EmergencySolutionConfig { + /// The block hash at which scraping happens. If none is provided, the latest head is used. + #[clap(long)] + pub at: Option, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// The number of top backed winners to take instead. All are taken, if not provided. + pub force_winner_count: Option, +} + +pub async fn emergency_solution_cmd( + client: Client, + config: EmergencySolutionConfig, +) -> Result<(), Error> +where + T: MinerConfig + + Send + + Sync + + 'static, + T::Solution: Send, +{ + if let Some(max_winners) = config.force_winner_count { + static_types::MaxWinners::set(max_winners); + } + + let storage = storage_at(config.at, client.chain_api()).await?; + + let round = storage + .fetch_or_default(&runtime::storage().election_provider_multi_phase().round()) + .await?; + + let miner_solution = epm::fetch_snapshot_and_mine_solution::( + client.chain_api(), + config.at, + config.solver, + round, + config.force_winner_count, + ) + .await?; + + let ready_solution = miner_solution.feasibility_check()?; + let encoded_size = ready_solution.encoded_size(); + let score = ready_solution.score; + let mut supports: Vec<_> = ready_solution.supports.into_inner(); + + // maybe truncate. + if let Some(force_winner_count) = config.force_winner_count { + log::info!( + target: LOG_TARGET, + "truncating {} winners to {}", + supports.len(), + force_winner_count + ); + supports.sort_unstable_by_key(|(_, s)| s.total); + supports.truncate(force_winner_count as usize); + } + + let call = epm::set_emergency_result(supports.clone())?; + let encoded_call = call.encode_call_data(&client.chain_api().metadata())?; + let encoded_supports = supports.encode(); + + // write results to files. + let mut supports_file = std::fs::File::create("solution.supports.bin")?; + let mut encoded_call_file = std::fs::File::create("encoded.call")?; + supports_file.write_all(&encoded_supports)?; + encoded_call_file.write_all(&encoded_call)?; + + let hex = HexDisplay::from(&encoded_call); + log::info!(target: LOG_TARGET, "Hex call:\n {:?}", hex); + + log::info!(target: LOG_TARGET, "Use the hex encoded call above to construct the governance proposal or the extrinsic to submit."); + log::info!(target: LOG_TARGET, "ReadySolution: size {:?} / score = {:?}", encoded_size, score); + log::info!(target: LOG_TARGET, "`set_emergency_result` encoded call written to ./encoded.call"); + + Ok(()) +} diff --git a/utils/staking-miner/src/commands/mod.rs b/utils/staking-miner/src/commands/mod.rs new file mode 100644 index 0000000..7281558 --- /dev/null +++ b/utils/staking-miner/src/commands/mod.rs @@ -0,0 +1,7 @@ +pub mod dry_run; +pub mod emergency_solution; +pub mod monitor; + +pub use dry_run::{dry_run_cmd, DryRunConfig}; +pub use emergency_solution::{emergency_solution_cmd, EmergencySolutionConfig}; +pub use monitor::{monitor_cmd, MonitorConfig}; diff --git a/utils/staking-miner/src/commands/monitor.rs b/utils/staking-miner/src/commands/monitor.rs new file mode 100644 index 0000000..7c576e1 --- /dev/null +++ b/utils/staking-miner/src/commands/monitor.rs @@ -0,0 +1,858 @@ +use crate::{ + client::Client, + epm, + error::Error, + helpers::{kill_main_task_if_critical_err, signer_from_seed_or_path, TimedFuture}, + opt::Solver, + prelude::*, + prometheus, static_types, +}; +use clap::Parser; +use codec::{Decode, Encode}; +use frame_election_provider_support::NposSolution; +use futures::future::TryFutureExt; +use jsonrpsee::core::Error as JsonRpseeError; +use pallet_election_provider_multi_phase::{RawSolution, SolutionOf}; +use sp_runtime::Perbill; +use std::{str::FromStr, sync::Arc}; +use subxt::{ + backend::{legacy::rpc_methods::DryRunResult, rpc::RpcSubscription}, + config::{DefaultExtrinsicParamsBuilder, Header as _}, + error::RpcError, + tx::{TxInBlock, TxProgress}, + Error as SubxtError, +}; +use tokio::sync::Mutex; + +#[derive(Debug, Clone, Parser)] +#[cfg_attr(test, derive(PartialEq))] +pub struct MonitorConfig { + /// They type of event to listen to. + /// + /// Typically, finalized is safer and there is no chance of anything going wrong, but it can be + /// slower. It is recommended to use finalized, if the duration of the signed phase is longer + /// than the the finality delay. + #[clap(long, value_enum, default_value_t = Listen::Finalized)] + pub listen: Listen, + + /// The solver algorithm to use. + #[clap(subcommand)] + pub solver: Solver, + + /// Submission strategy to use. + /// + /// Possible options: + /// + /// `--submission-strategy if-leading`: only submit if leading. + /// + /// `--submission-strategy always`: always submit. + /// + /// `--submission-strategy "percent-better "`: submit if the submission is `n` percent better. + /// + /// `--submission-strategy "no-worse-than "`: submit if submission is no more than `n` percent worse. + #[clap(long, value_parser, default_value = "if-leading")] + pub submission_strategy: SubmissionStrategy, + + /// The path to a file containing the seed of the account. If the file is not found, the seed is + /// used as-is. + /// + /// Can also be provided via the `SEED` environment variable. + /// + /// WARNING: Don't use an account with a large stash for this. Based on how the bot is + /// configured, it might re-try and lose funds through transaction fees/deposits. + #[clap(long, short, env = "SEED")] + pub seed_or_path: String, + + /// Delay in number seconds to wait until starting mining a solution. + /// + /// At every block when a solution is attempted + /// a delay can be enforced to avoid submitting at + /// "same time" and risk potential races with other miners. + /// + /// When this is enabled and there are competing solutions, your solution might not be submitted + /// if the scores are equal. + #[clap(long, default_value_t = 0)] + pub delay: usize, + + /// Verify the submission by `dry-run` the extrinsic to check the validity. + /// If the extrinsic is invalid then the submission is ignored and the next block will attempted again. + /// + /// This requires a RPC endpoint that exposes unsafe RPC methods, if the RPC endpoint doesn't expose unsafe RPC methods + /// then the miner will be terminated. + #[clap(long)] + pub dry_run: bool, +} + +/// The type of event to listen to. +/// +/// +/// Typically, finalized is safer and there is no chance of anything going wrong, but it can be +/// slower. It is recommended to use finalized, if the duration of the signed phase is longer +/// than the the finality delay. +#[cfg_attr(test, derive(PartialEq))] +#[derive(clap::ValueEnum, Debug, Copy, Clone)] +pub enum Listen { + /// Latest finalized head of the canonical chain. + Finalized, + /// Latest head of the canonical chain. + Head, +} + +/// Submission strategy to use. +#[derive(Debug, Copy, Clone)] +#[cfg_attr(test, derive(PartialEq))] +pub enum SubmissionStrategy { + /// Always submit. + Always, + // Submit if we are leading, or if the solution that's leading is more that the given `Perbill` + // better than us. This helps detect obviously fake solutions and still combat them. + /// Only submit if at the time, we are the best (or equal to it). + IfLeading, + /// Submit if we are no worse than `Perbill` worse than the best. + ClaimNoWorseThan(Perbill), + /// Submit if we are leading, or if the solution that's leading is more that the given `Perbill` + /// better than us. This helps detect obviously fake solutions and still combat them. + ClaimBetterThan(Perbill), +} + +/// Custom `impl` to parse `SubmissionStrategy` from CLI. +/// +/// Possible options: +/// * --submission-strategy if-leading: only submit if leading +/// * --submission-strategy always: always submit +/// * --submission-strategy "percent-better ": submit if submission is `n` percent better. +/// +impl FromStr for SubmissionStrategy { + type Err = String; + + fn from_str(s: &str) -> Result { + let s = s.trim(); + + let res = if s == "if-leading" { + Self::IfLeading + } else if s == "always" { + Self::Always + } else if let Some(percent) = s.strip_prefix("no-worse-than ") { + let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?; + Self::ClaimNoWorseThan(Perbill::from_percent(percent)) + } else if let Some(percent) = s.strip_prefix("percent-better ") { + let percent: u32 = percent.parse().map_err(|e| format!("{:?}", e))?; + Self::ClaimBetterThan(Perbill::from_percent(percent)) + } else { + return Err(s.into()); + }; + Ok(res) + } +} + +pub async fn monitor_cmd(client: Client, config: MonitorConfig) -> Result<(), Error> +where + T: MinerConfig + + Send + + Sync + + 'static, + T::Solution: Send, +{ + let signer = signer_from_seed_or_path(&config.seed_or_path)?; + + let account_info = { + let addr = runtime::storage() + .system() + .account(signer.public_key().to_account_id()); + client + .chain_api() + .storage() + .at_latest() + .await? + .fetch(&addr) + .await? + .ok_or(Error::AccountDoesNotExists)? + }; + + log::info!(target: LOG_TARGET, "Loaded account {}, {:?}", signer.public_key().to_account_id(), account_info); + + if config.dry_run { + // if we want to try-run, ensure the node supports it. + dry_run_works(client.rpc()).await?; + } + + let mut subscription = heads_subscription(client.rpc(), config.listen).await?; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let submit_lock = Arc::new(Mutex::new(())); + + loop { + let at = tokio::select! { + maybe_rp = subscription.next() => { + match maybe_rp { + Some(Ok(r)) => r, + Some(Err(e)) => { + log::error!(target: LOG_TARGET, "subscription failed to decode Header {:?}, this is bug please file an issue", e); + return Err(e.into()); + } + // The subscription was dropped, should only happen if: + // - the connection was closed. + // - the subscription could not keep up with the server. + None => { + log::warn!(target: LOG_TARGET, "subscription to `{:?}` terminated. Retrying..", config.listen); + subscription = heads_subscription(client.rpc(), config.listen).await?; + continue + } + } + }, + maybe_err = rx.recv() => { + match maybe_err { + Some(err) => return Err(err), + None => unreachable!("at least one sender kept in the main loop should always return Some; qed"), + } + } + }; + + // Spawn task and non-recoverable errors are sent back to the main task + // such as if the connection has been closed. + let tx2 = tx.clone(); + let client2 = client.clone(); + let signer2 = signer.clone(); + let config2 = config.clone(); + let submit_lock2 = submit_lock.clone(); + tokio::spawn(async move { + if let Err(err) = + mine_and_submit_solution::(at, client2, signer2, config2, submit_lock2).await + { + kill_main_task_if_critical_err(&tx2, err) + } + }); + + let account_info = client + .chain_api() + .storage() + .at_latest() + .await? + .fetch( + &runtime::storage() + .system() + .account(signer.public_key().to_account_id()), + ) + .await? + .ok_or(Error::AccountDoesNotExists)?; + // this is lossy but fine for now. + prometheus::set_balance(account_info.data.free as f64); + } +} + +/// Construct extrinsic at given block and watch it. +async fn mine_and_submit_solution( + at: Header, + client: Client, + signer: Signer, + config: MonitorConfig, + submit_lock: Arc>, +) -> Result<(), Error> +where + T: MinerConfig + + Send + + Sync + + 'static, + T::Solution: Send, +{ + let block_hash = at.hash(); + log::trace!(target: LOG_TARGET, "new event at #{:?} ({:?})", at.number, block_hash); + + // NOTE: as we try to send at each block then the nonce is used guard against + // submitting twice. Because once a solution has been accepted on chain + // the "next transaction" at a later block but with the same nonce will be rejected + let nonce = client + .rpc() + .system_account_next_index(&signer.public_key().to_account_id()) + .await?; + + ensure_signed_phase(client.chain_api(), block_hash) + .inspect_err(|e| { + log::debug!( + target: LOG_TARGET, + "ensure_signed_phase failed: {:?}; skipping block: {}", + e, + at.number + ) + }) + .await?; + + let round_fut = async { + client + .chain_api() + .storage() + .at(block_hash) + .fetch_or_default(&runtime::storage().election_provider_multi_phase().round()) + .await + }; + + let round = round_fut + .inspect_err(|e| log::error!(target: LOG_TARGET, "Mining solution failed: {:?}", e)) + .await?; + + ensure_no_previous_solution::( + client.chain_api(), + block_hash, + &signer.public_key().0.into(), + ) + .inspect_err(|e| { + log::debug!( + target: LOG_TARGET, + "ensure_no_previous_solution failed: {:?}; skipping block: {}", + e, + at.number + ) + }) + .await?; + + tokio::time::sleep(std::time::Duration::from_secs(config.delay as u64)).await; + let _lock = submit_lock.lock().await; + + let (solution, score) = match epm::fetch_snapshot_and_mine_solution::( + &client.chain_api(), + Some(block_hash), + config.solver, + round, + None, + ) + .timed() + .await + { + (Ok(mined_solution), elapsed) => { + // check that the solution looks valid: + mined_solution.feasibility_check()?; + + // and then get the values we need from it: + let solution = mined_solution.solution(); + let score = mined_solution.score(); + let size = mined_solution.size(); + + let elapsed_ms = elapsed.as_millis(); + let encoded_len = solution.encoded_size(); + let active_voters = solution.voter_count() as u32; + let desired_targets = solution.unique_targets().len() as u32; + + let final_weight = tokio::task::spawn_blocking(move || { + T::solution_weight(size.voters, size.targets, active_voters, desired_targets) + }) + .await?; + + log::info!( + target: LOG_TARGET, + "Mined solution with {:?} size: {:?} round: {:?} at: {}, took: {} ms, len: {:?}, weight = {:?}", + score, + size, + round, + at.number(), + elapsed_ms, + encoded_len, + final_weight, + ); + + prometheus::set_length(encoded_len); + prometheus::set_weight(final_weight); + prometheus::observe_mined_solution_duration(elapsed_ms as f64); + prometheus::set_score(score); + + (solution, score) + } + (Err(e), _) => return Err(Error::Other(e.to_string())), + }; + + let best_head = get_latest_head(client.rpc(), config.listen).await?; + + ensure_signed_phase(client.chain_api(), best_head) + .inspect_err(|e| { + log::debug!( + target: LOG_TARGET, + "ensure_signed_phase failed: {:?}; skipping block: {}", + e, + at.number + ) + }) + .await?; + + ensure_no_previous_solution::( + client.chain_api(), + best_head, + &signer.public_key().0.into(), + ) + .inspect_err(|e| { + log::debug!( + target: LOG_TARGET, + "ensure_no_previous_solution failed: {:?}; skipping block: {:?}", + e, + best_head, + ) + }) + .await?; + + match ensure_solution_passes_strategy( + client.chain_api(), + best_head, + score, + config.submission_strategy, + ) + .timed() + .await + { + (Ok(_), now) => { + log::trace!( + target: LOG_TARGET, + "Solution validity verification took: {} ms", + now.as_millis() + ); + } + (Err(e), _) => { + log::debug!( + target: LOG_TARGET, + "ensure_no_better_solution failed: {:?}; skipping block: {}", + e, + at.number + ); + return Err(e); + } + }; + + prometheus::on_submission_attempt(); + match submit_and_watch_solution::( + &client, + signer, + (solution, score, round), + nonce, + config.listen, + config.dry_run, + &at, + ) + .timed() + .await + { + (Ok(_), now) => { + prometheus::on_submission_success(); + prometheus::observe_submit_and_watch_duration(now.as_millis() as f64); + } + (Err(e), _) => { + log::warn!( + target: LOG_TARGET, + "submit_and_watch_solution failed: {e}; skipping block: {}", + at.number + ); + } + }; + Ok(()) +} + +/// Ensure that now is the signed phase. +async fn ensure_signed_phase(api: &ChainClient, block_hash: Hash) -> Result<(), Error> { + use pallet_election_provider_multi_phase::Phase; + + let addr = runtime::storage() + .election_provider_multi_phase() + .current_phase(); + let phase = api.storage().at(block_hash).fetch(&addr).await?; + + if let Some(Phase::Signed) = phase.map(|p| p.0) { + Ok(()) + } else { + Err(Error::IncorrectPhase) + } +} + +/// Ensure that our current `us` have not submitted anything previously. +async fn ensure_no_previous_solution( + api: &ChainClient, + block_hash: Hash, + us: &AccountId, +) -> Result<(), Error> +where + T: NposSolution + scale_info::TypeInfo + Decode + 'static, +{ + let addr = runtime::storage() + .election_provider_multi_phase() + .signed_submission_indices(); + let indices = api.storage().at(block_hash).fetch_or_default(&addr).await?; + + for (_score, _, idx) in indices.0 { + let submission = epm::signed_submission_at::(idx, Some(block_hash), api).await?; + + if let Some(submission) = submission { + if &submission.who == us { + return Err(Error::AlreadySubmitted); + } + } + } + + Ok(()) +} + +async fn ensure_solution_passes_strategy( + api: &ChainClient, + block_hash: Hash, + score: sp_npos_elections::ElectionScore, + strategy: SubmissionStrategy, +) -> Result<(), Error> { + // don't care about current scores. + if matches!(strategy, SubmissionStrategy::Always) { + return Ok(()); + } + + let addr = runtime::storage() + .election_provider_multi_phase() + .signed_submission_indices(); + let indices = api.storage().at(block_hash).fetch_or_default(&addr).await?; + + log::debug!(target: LOG_TARGET, "submitted solutions: {:?}", indices.0); + + if indices.0.last().map_or(true, |(best_score, _, _)| { + score_passes_strategy(score, best_score.0, strategy) + }) { + Ok(()) + } else { + Err(Error::BetterScoreExist) + } +} + +async fn submit_and_watch_solution( + client: &Client, + signer: Signer, + (solution, score, round): (SolutionOf, sp_npos_elections::ElectionScore, u32), + nonce: u64, + listen: Listen, + dry_run: bool, + at: &Header, +) -> Result<(), Error> { + let tx = epm::signed_solution(RawSolution { + solution, + score, + round, + })?; + + // TODO: https://github.com/paritytech/polkadot-staking-miner/issues/730 + // + // The extrinsic mortality length is static and doesn't know when the + // signed phase ends. + let signed_phase_len = client.chain_api().constants().at(&runtime::constants() + .election_provider_multi_phase() + .signed_phase())?; + let xt_cfg = DefaultExtrinsicParamsBuilder::default() + .mortal(at, signed_phase_len as u64) + .build(); + + let xt = + client + .chain_api() + .tx() + .create_signed_with_nonce(&tx, &signer, nonce as u64, xt_cfg)?; + + if dry_run { + let dry_run_bytes = client.rpc().dry_run(xt.encoded(), None).await?; + + match dry_run_bytes.into_dry_run_result(&client.chain_api().metadata())? { + DryRunResult::Success => (), + DryRunResult::DispatchError(e) => { + return Err(Error::TransactionRejected(e.to_string())) + } + DryRunResult::TransactionValidityError => { + return Err(Error::TransactionRejected( + "TransactionValidityError".into(), + )) + } + } + } + + let tx_progress = xt.submit_and_watch().await.map_err(|e| { + log::warn!(target: LOG_TARGET, "submit solution failed: {:?}", e); + e + })?; + + match listen { + Listen::Head => { + let in_block = wait_for_in_block(tx_progress).await?; + let events = in_block.fetch_events().await.expect("events should exist"); + + let solution_stored = events + .find_first::( + ); + + if let Ok(Some(_)) = solution_stored { + log::info!("Included at {:?}", in_block.block_hash()); + } else { + return Err(Error::Other(format!( + "No SolutionStored event found at {:?}", + in_block.block_hash() + ))); + } + } + Listen::Finalized => { + let finalized = tx_progress.wait_for_finalized_success().await?; + + let solution_stored = finalized + .find_first::( + ); + + if let Ok(Some(_)) = solution_stored { + log::info!("Finalized at {:?}", finalized.block_hash()); + } else { + return Err(Error::Other(format!( + "No SolutionStored event found at {:?}", + finalized.block_hash() + ))); + } + } + }; + + Ok(()) +} + +async fn heads_subscription( + rpc: &RpcClient, + listen: Listen, +) -> Result, Error> { + match listen { + Listen::Head => rpc.chain_subscribe_new_heads().await, + Listen::Finalized => rpc.chain_subscribe_finalized_heads().await, + } + .map_err(Into::into) +} + +async fn get_latest_head(rpc: &RpcClient, listen: Listen) -> Result { + match listen { + Listen::Head => match rpc.chain_get_block_hash(None).await { + Ok(Some(hash)) => Ok(hash), + Ok(None) => Err(Error::Other("Latest block not found".into())), + Err(e) => Err(e.into()), + }, + Listen::Finalized => rpc.chain_get_finalized_head().await.map_err(Into::into), + } +} + +/// Returns `true` if `our_score` better the onchain `best_score` according the given strategy. +pub(crate) fn score_passes_strategy( + our_score: sp_npos_elections::ElectionScore, + best_score: sp_npos_elections::ElectionScore, + strategy: SubmissionStrategy, +) -> bool { + match strategy { + SubmissionStrategy::Always => true, + SubmissionStrategy::IfLeading => { + our_score.strict_threshold_better(best_score, Perbill::zero()) + } + SubmissionStrategy::ClaimBetterThan(epsilon) => { + our_score.strict_threshold_better(best_score, epsilon) + } + SubmissionStrategy::ClaimNoWorseThan(epsilon) => { + !best_score.strict_threshold_better(our_score, epsilon) + } + } +} + +async fn dry_run_works(rpc: &RpcClient) -> Result<(), Error> { + if let Err(SubxtError::Rpc(RpcError::ClientError(e))) = rpc.dry_run(&[], None).await { + let rpc_err = match e.downcast::() { + Ok(e) => *e, + Err(_) => { + return Err(Error::Other( + "Failed to downcast RPC error; this is a bug please file an issue".to_string(), + )) + } + }; + + if let JsonRpseeError::Call(e) = rpc_err { + if e.message() == "RPC call is unsafe to be called externally" { + return Err(Error::Other( + "dry-run requires a RPC endpoint with `--rpc-methods unsafe`; \ + either connect to another RPC endpoint or disable dry-run" + .to_string(), + )); + } + } + } + Ok(()) +} + +/// Wait for the transaction to be in a block. +/// +/// **Note:** transaction statuses like `Invalid`/`Usurped`/`Dropped` indicate with some +/// probability that the transaction will not make it into a block but there is no guarantee +/// that this is true. In those cases the stream is closed however, so you currently have no way to find +/// out if they finally made it into a block or not. +async fn wait_for_in_block(mut tx: TxProgress) -> Result, subxt::Error> +where + T: subxt::Config, + C: subxt::client::OnlineClientT, +{ + use subxt::{error::TransactionError, tx::TxStatus}; + + while let Some(status) = tx.next().await { + match status? { + // Finalized or otherwise in a block! Return. + TxStatus::InBestBlock(s) | TxStatus::InFinalizedBlock(s) => return Ok(s), + // Error scenarios; return the error. + TxStatus::Error { message } => return Err(TransactionError::Error(message).into()), + TxStatus::Invalid { message } => return Err(TransactionError::Invalid(message).into()), + TxStatus::Dropped { message } => return Err(TransactionError::Dropped(message).into()), + // Ignore anything else and wait for next status event: + _ => continue, + } + } + Err(RpcError::SubscriptionDropped.into()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn score_passes_strategy_works() { + let s = |x| sp_npos_elections::ElectionScore { + minimal_stake: x, + ..Default::default() + }; + let two = Perbill::from_percent(2); + + // anything passes Always + assert!(score_passes_strategy( + s(0), + s(0), + SubmissionStrategy::Always + )); + assert!(score_passes_strategy( + s(5), + s(0), + SubmissionStrategy::Always + )); + assert!(score_passes_strategy( + s(5), + s(10), + SubmissionStrategy::Always + )); + + // if leading + assert!(!score_passes_strategy( + s(0), + s(0), + SubmissionStrategy::IfLeading + )); + assert!(score_passes_strategy( + s(1), + s(0), + SubmissionStrategy::IfLeading + )); + assert!(score_passes_strategy( + s(2), + s(0), + SubmissionStrategy::IfLeading + )); + assert!(!score_passes_strategy( + s(5), + s(10), + SubmissionStrategy::IfLeading + )); + assert!(!score_passes_strategy( + s(9), + s(10), + SubmissionStrategy::IfLeading + )); + assert!(!score_passes_strategy( + s(10), + s(10), + SubmissionStrategy::IfLeading + )); + + // if better by 2% + assert!(!score_passes_strategy( + s(50), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + assert!(!score_passes_strategy( + s(100), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + assert!(!score_passes_strategy( + s(101), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + assert!(!score_passes_strategy( + s(102), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + assert!(score_passes_strategy( + s(103), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + assert!(score_passes_strategy( + s(150), + s(100), + SubmissionStrategy::ClaimBetterThan(two) + )); + + // if no less than 2% worse + assert!(!score_passes_strategy( + s(50), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(!score_passes_strategy( + s(97), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(98), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(99), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(100), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(101), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(102), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(103), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + assert!(score_passes_strategy( + s(150), + s(100), + SubmissionStrategy::ClaimNoWorseThan(two) + )); + } + + #[test] + fn submission_strategy_from_str_works() { + assert_eq!( + SubmissionStrategy::from_str("if-leading"), + Ok(SubmissionStrategy::IfLeading) + ); + assert_eq!( + SubmissionStrategy::from_str("always"), + Ok(SubmissionStrategy::Always) + ); + assert_eq!( + SubmissionStrategy::from_str(" percent-better 99 "), + Ok(SubmissionStrategy::ClaimBetterThan(Accuracy::from_percent( + 99 + ))) + ); + } +} diff --git a/utils/staking-miner/src/epm.rs b/utils/staking-miner/src/epm.rs index ce7236e..9390c7f 100644 --- a/utils/staking-miner/src/epm.rs +++ b/utils/staking-miner/src/epm.rs @@ -13,13 +13,10 @@ use std::{ }; use codec::{Decode, Encode}; -use frame_election_provider_support::{ - Get, NposSolution, PhragMMS, SequentialPhragmen, -}; +use frame_election_provider_support::{Get, NposSolution, PhragMMS, SequentialPhragmen}; use frame_support::{weights::Weight, BoundedVec}; use pallet_election_provider_multi_phase::{ - usigned::TrimmingStatus, RawSolution, ReadySolution, SolutionOf, - SolutionOrSnapshotSize, + usigned::TrimmingStatus, RawSolution, ReadySolution, SolutionOf, SolutionOrSnapshotSize, }; use scale_info::{PortableRegistry, TypeInfo}; use scale_value::scale::decode_as_type; @@ -29,12 +26,16 @@ use subxt::{dynamic::Value, tx::DynamicPayload}; const EPM_PALLET_NAME: &str = "ElectionProviderMultiPhase"; type TypeId = u32; -type MinerVoterOf = frame_election_provider_support::Voter; +type MinerVoterOf = + frame_election_provider_support::Voter; type RoundSnapshot = pallet_election_provider_multi_phase::RoundSnapshot; -type Voters = Vec<(AccountId, VoteWeight, BoundedVec)>; +type Voters = Vec<( + AccountId, + VoteWeight, + BoundedVec, +)>; -#[derive(Copy, Clone, Debug)] -#[derive(Debug)] +#[derive(Copy, Clone, Debug, Debug)] struct EpmConstant { epm: &'static str, constant: &'static str, @@ -42,7 +43,10 @@ struct EpmConstant { impl EpmConstant { const fn new(constant: &'static str) -> Self { - Self { epm: EPM_PALLET_NAME, constant } + Self { + epm: EPM_PALLET_NAME, + constant, + } } const fn to_parts(self) -> (&'static str, &'static str) { @@ -101,18 +105,24 @@ where let est_weight: Weight = tokio::task::spawn_blocking(move || { T::solution_weight(active_voters, targets_len, active_voters, desired_targets) - }).await?; + }) + .await?; let max_weight: Weight = T::MaxWeight::get(); if est_weight.all_lt(max_weight) { - return Ok(Self { - state: State { voters, voters_by_stake }, - _marker: PhantomData + return Ok(Self { + state: State { + voters, + voters_by_stake, + }, + _marker: PhantomData, }); } - let Some((_, idx)) = voters_by_stake.pop_first() else { break }; + let Some((_, idx)) = voters_by_stake.pop_first() else { + break; + }; let rm = voters[idx].0.clone(); for (_voter, _stake, supports) in &mut voters { @@ -122,7 +132,9 @@ where targets.remove(&rm); } - return Err(Error::Feasibility("Failed to pre-trim weight < T::MaxLength".to_string())); + return Err(Error::Feasibility( + "Failed to pre-trim weight < T::MaxLength".to_string(), + )); } pub fn trim(&mut self, n: usize) -> Result { @@ -139,7 +151,10 @@ where supports.retain(|a| a != &rm); } } - Ok(State { voters, voters_by_stake }) + Ok(State { + voters, + voters_by_stake, + }) } pub fn to_voters(&self) -> Voters { @@ -197,28 +212,37 @@ fn read_constant<'a, T: serde::Deserialize<'a>>( .map_err(|e| Error::Subxt(e.into()))?; scale_value::serde::from_value::<_, T>(val).map_err(|e| { - Error::InvalidMetadata( - format!("Decoding `{}` failed {}", std::any::type_name::(), e) - ) + Error::InvalidMetadata(format!( + "Decoding `{}` failed {}", + std::any::type_name::(), + e + )) }) } pub(crate) fn set_emergency_result( supports: frame_election_provider_support::Supports, ) -> Result { - let scale_result = to_scale_value(supports).map_err(|e| { - Error::DynamicTransaction(format!("Failed to encode `Supports`: {:?}", e)) - })?; - Ok(subxt::dynamic::tx(EPM_PALLET_NAME, "set_emergency_election_result", vec![scale_result])) + let scale_result = to_scale_value(supports) + .map_err(|e| Error::DynamicTransaction(format!("Failed to encode `Supports`: {:?}", e)))?; + Ok(subxt::dynamic::tx( + EPM_PALLET_NAME, + "set_emergency_election_result", + vec![scale_result], + )) } pub fn signed_solution( solution: RawSolution, ) -> Result { - let scale_solution = to_scale_value(solution).map_err(|e| { + let scale_solution = to_scale_value(solution).map_err(|e| { Error::DynamicTransaction(format!("Failed to encode `RawSolution`: {:?}", e)) })?; - Ok(subxt::dynamic::tx(EPM_PALLET_NAME, "submit", vec![scale_solution])) + Ok(subxt::dynamic::tx( + EPM_PALLET_NAME, + "submit", + vec![scale_solution], + )) } pub fn unsigned_solution( @@ -227,7 +251,11 @@ pub fn unsigned_solution( ) -> Result { let scale_solution = to_scale_value(solution)?; let scale_witness = to_scale_value(witness)?; - Ok(subxt::dynamic::tx(EPM_PALLET_NAME, "submit_unsigned", vec![scale_solution, scale_witness])) + Ok(subxt::dynamic::tx( + EPM_PALLET_NAME, + "submit_unsigned", + vec![scale_solution, scale_witness], + )) } pub async fn signed_submission( @@ -244,7 +272,7 @@ pub async fn signed_submission( Ok(Some(val)) => { let submissions = Decode::decode(&mut val.encode())?; Ok(Some(submissions)) - }, + } Ok(None) => Ok(None), Err(err) => Err(err.into()), } @@ -263,7 +291,7 @@ pub async fn snapshot_at( Ok(Some(val)) => { let snapshot = Decode::decode(&mut val.encode())?; Ok(Some(snapshot)) - }, + } Ok(None) => Err(Error::EmptySnapshot), Err(err) => Err(err.into()), } @@ -274,7 +302,15 @@ pub async fn mine_solution( targets: Vec, voters: Voters, desired_targets: u32, -) -> Result<(SolutionOf, ElectionScore, SolutionOrSnapshotSize, TrimmingStatus), Error> +) -> Result< + ( + SolutionOf, + ElectionScore, + SolutionOrSnapshotSize, + TrimmingStatus, + ), + Error, +> where T: MinerConfig + Send @@ -288,14 +324,18 @@ where Miner::::mine_solution_with_snapshot::< SequentialPhragmen, >(voters, targets, desired_targets) - }, + } Solver::PhragMMS { iterations } => { BalanceIterations::set(iterations); - Miner::::mine_solution_with_snapshot::< - PhragMMS, - >(voters, targets, desired_targets) - }, - }).await { + Miner::::mine_solution_with_snapshot::>( + voters, + targets, + desired_targets, + ) + } + }) + .await + { Ok(Ok(s)) => Ok(s), Err(e) => Err(e.into()), Ok(Err(e)) => Err(Error::Other(format!("{:?}", e))), @@ -322,13 +362,21 @@ where let desired_targets = match forced_desired_targets { Some(x) => x, None => storage - .fetch(&runtime::storage()::election_provider_multi_phase().desired_targets()) + .fetch( + &runtime::storage() + .election_provider_multi_phase() + .desired_targets(), + ) .await? .expect("Snapshot is non-empty; `desired_target` should exist; qed"), }; let minimum_untrusted_score = storage - .fetch(&runtime::storage().election_provider_multi_phase().minimum_untrusted_score()) + .fetch( + &runtime::storage() + .election_provider_multi_phase() + .minimum_untrusted_score(), + ) .await? .map(|score| score.0); @@ -339,16 +387,17 @@ where snapshot.targets.clone(), voters.to_voters(), desired_targets, - ).await?; + ) + .await?; if !trim_status.is_trimmed() { return Ok(MinedSolution { - round, - desired_targets, - snapshot, - minimum_untrusted_score, - solution, - score, + round, + desired_targets, + snapshot, + minimum_untrusted_score, + solution, + score, solution_or_snapshot_size, }); } @@ -368,14 +417,15 @@ where snapshot.targets.clone(), next_state.to_voters(), desired_targets, - ).await?; + ) + .await?; if !trim_status.is_trimmed() { best_solution = Some((solution, score, solution_or_snapshot_size)); h = mid - 1; } else { l = mid + 1; - } , + } } if let Some((solution, score, solution_or_snapshot_size)) = best_solution { @@ -425,9 +475,13 @@ where self.solution_or_snapshot_size } - pub fn feasibility_check(&self) => Result, Error> { + pub fn feasibility_check(&self) -> Result, Error> { match Miner::::feasibility_check( - RawSolution { solution: self.solution.clone(), score: self.score, round: self.round }, + RawSolution { + solution: self.solution.clone(), + score: self.score, + round: self.round, + }, pallet_election_provider_multi_phase::ElectionCompute::Signed, self.desired_targets, self.snapshot.clone(), @@ -438,7 +492,7 @@ where Err(e) => { log::error!(target: LOG_TARGET, "Solution feasibility error {:?}", e); Err(Error::Feasibility(format!("{:?}", e))) - }, + } } } } @@ -483,7 +537,9 @@ pub async fn runtime_api_solution_weight Result { let tx = unsigned_solution(raw_solution, witness)?; - let client = SHARED_CLIENT.get().expect("shared client is configured as start; qed"); + let client = SHARED_CLIENT + .get() + .expect("shared client is configured as start; qed"); let call_data = { let mut buffer = Vec::new(); @@ -499,7 +555,11 @@ pub async fn runtime_api_solution_weight { let elapsed = start.elapsed(); Poll::Ready((v, elapsed)) - }, + } } } } pub trait TimedFuture: Sized + Future { fn timed(self) -> Timed { - Timed { inner: self, start: None } + Timed { + inner: self, + start: None, + } } } @@ -59,15 +62,15 @@ pub struct RuntimeDispatchInfo { pub weight: Weight, } -pub fn kill_main_task_if_critical_err(tx: &tokio::sync::mpsc::UnboundedSender, err::Error) { +pub fn kill_main_task_if_critical_err(tx: &tokio::sync::mpsc::UnboundedSender, err: Error) { match err { - Error::AlreadySubmitted | - Error::BetterScoreExist | - Error::IncorrectPhase | - Error::TransactionRejected | - Error::JoinError | - Error::Feasibility | - Error::EmptySnapshot => {}, + Error::AlreadySubmitted + | Error::BetterScoreExist + | Error::IncorrectPhase + | Error::TransactionRejected + | Error::JoinError + | Error::Feasibility + | Error::EmptySnapshot => {} Error::Subxt(SubxtError::Rpc(rpc_err)) => { log::debug!(target: LOG_TARGET, "rpc error: {:?}", rpc_err); @@ -77,10 +80,11 @@ pub fn kill_main_task_if_critical_err(tx: &tokio::sync::mpsc::UnboundedSender *e, Err(_) => { let _ = tx.send(Error::Other( - "Failed to downcast RPC error; this is a bug please file an issue".to_string() + "Failed to downcast RPC error; this is a bug please file an issue" + .to_string(), )); return; - }, + } }; match jsonrpsee_err { @@ -89,30 +93,30 @@ pub fn kill_main_task_if_critical_err(tx: &tokio::sync::mpsc::UnboundedSender {}, + } + JsonRpseeError::RequestTimeout => {} err => { let _ = tx.send(Error::Subxt(SubxtError::Rpc(RpcError::ClientError( Box::new(err), )))); - }, + } } - }, + } RpcError::SubscriptionDropped => (), _ => (), } - }, + } err => { let _ = tx.send(err); - }, + } } } diff --git a/utils/staking-miner/src/main.rs b/utils/staking-miner/src/main.rs index c010fa5..16ea038 100644 --- a/utils/staking-miner/src/main.rs +++ b/utils/staking-miner/src/main.rs @@ -34,10 +34,7 @@ use std::str::FromStr; use tokio::sync::oneshot; use tracing_subscriber::EnvFilter; -use crate::{ - client::Client, - opt::RuntimeVersion, -}; +use crate::{client::Client, opt::RuntimeVersion}; #[derive(Debig, Clone, Parser)] #[cfg_attr(test, derive(PartialEq))] @@ -96,12 +93,17 @@ macro_rules! any_runtime { #[tokio::main] async fn main() -> Result<(), Error> { - let Opt { uri, command, prometheus_port, log } = Opt::parse(); + let Opt { + uri, + command, + prometheus_port, + log, + } = Opt::parse(); let filter = EnvFilter::from_default_env().add_directive(log.parse()?); tracing_subscriber::fmt().with_env_filter(filter).init(); let client = Client::new(&uri).await?; - let runtime_version: RuntimeVersion = + let runtime_version: RuntimeVersion = client.rpc().state_get_runtime_version(None).await?.into(); let chain = opt::Chain::from_str(&runtime_version.spec_name)?; let _prometheus_handle = prometheus::run(prometheus_port) @@ -109,7 +111,9 @@ async fn main() -> Result<(), Error> { log::info!(target: LOG_TARGET, "Connected to chain: {}", chain); epm::update_metadata_constants(client.chain_api())?; - SHARED_CLIENT.set(client.clone()).expect("shared client only set once; qed"); + SHARED_CLIENT + .set(client.clone()) + .expect("shared client only set once; qed"); // Start a new tokio tasl to perform the runtime updates in the backgound. // If this fails then the miner will be stopped and has to be re-started. @@ -203,7 +207,7 @@ async fn runtime_upgrade_task(client: ChainClient, tx: oneshot::Sender) { Err(e) => { let _ = tx.send(e.into()); return; - }, + } }; loop { @@ -218,10 +222,10 @@ async fn runtime_upgrade_task(client: ChainClient, tx: oneshot::Sender) { Err(e) => { let _ = tx.send(e.into()); return; - }, + } }; continue; - }, + } }; let version = update.runtime_version().spec_version; @@ -233,10 +237,10 @@ async fn runtime_upgrade_task(client: ChainClient, tx: oneshot::Sender) { } prometheus::on_runtime_upgrade(); log::info!(target: LOG_TARGET, "upgrade to version: {} successful", version); - }, + } Err(e) => { log::debug!(target: LOG_TARGET, "upgrade to version: {} failed: {:?}", version, e); - }, + } } } } @@ -262,7 +266,8 @@ mod tests { "--delay", "12", "seq-phragmen", - ]).unwrap(); + ]) + .unwrap(); assert_eq!( opt, @@ -293,7 +298,8 @@ mod tests { "--seed-or-path", "//Alice", "prag-mms", - ]).unwrap(); + ]) + .unwrap(); assert_eq!( opt, @@ -323,7 +329,8 @@ mod tests { "prag-mms", "--iterations", "1337", - ]).unwrap(); + ]) + .unwrap(); assert_eq!( opt, diff --git a/utils/staking-miner/src/opt.rs b/utils/staking-miner/src/opt.rs index cff1c69..b7eb3e6 100644 --- a/utils/staking-miner/src/opt.rs +++ b/utils/staking-miner/src/opt.rs @@ -6,7 +6,7 @@ use sp_npos_elections::BalancingConfig; use sp_runtime::DeserializeOwned; use std::{collections::HashMap, fmt, str::FromStr}; -use subxt::backend::legacy::rpc_methods:: as subxt_rpc; +use subxt::backend::legacy::rpc_methods as subxt_rpc; #[derive(Debug, Clone, Parser)] #[cfg_attr(test, derive(PartialEq))] @@ -18,7 +18,7 @@ pub enum Solver { PhragMMS { #[clap(long, default_value = "10")] iterations: usize, - } + }, } frame_support::parameter_types! { @@ -30,7 +30,7 @@ frame_support::parameter_types! { #[derive(Debug, Copy, Clone)] pub enum Chain { Ghost, - Casper + Casper, } impl fmt::Display for Chain { @@ -64,8 +64,8 @@ impl TryFrom for Chain { .get("specName") .expect("RuntimeVersion must have specName; qed") .clone(); - let mut chain = serde_json::from_value::(json) - .expect("specName must be String; qed"); + let mut chain = + serde_json::from_value::(json).expect("specName must be String; qed"); chain.make_ascii_lowercase(); Chain::from_str(&chain) } @@ -96,8 +96,7 @@ impl From for RuntimeVersion { } } -#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)] -#[derive(Debug)] +#[derive(Deserialize, Serialize, PartialEq, Debug, Clone, Debug)] pub struct RuntimeVersion { pub spec_name: String, pub impl_name: String, diff --git a/utils/staking-miner/src/prelude.rs b/utils/staking-miner/src/prelude.rs index 0053c30..d669aaf 100644 --- a/utils/staking-miner/src/prelude.rs +++ b/utils/staking-miner/src/prelude.rs @@ -1,7 +1,7 @@ pub use pallet_election_provider_multi_phase::{Miner, MinerConfig}; pub use subxt::ext::sp_core; -pub use primitives::{AccountId, Header, Hash, Balance}; +pub use primitives::{AccountId, Balance, Hash, Header}; // pub type AccountId = sp_runtime::AccountId32; // pub type Header = subxt::config::substrate::SubstrateHeader; @@ -21,7 +21,8 @@ pub type RpcClient = subxt::backend::legacy::LegacyPrcMethods; pub type Config = subxt::SubstrateConfig; -pub type SignedSubmission = pallet_election_provider_multi_phase::SignedSubmission; +pub type SignedSubmission = + pallet_election_provider_multi_phase::SignedSubmission; #[subxt::subxt( runtime_metadata_path = "artifacts/metadata.scale", diff --git a/utils/staking-miner/src/prometheus.rs b/utils/staking-miner/src/prometheus.rs index e3339f5..799a0d8 100644 --- a/utils/staking-miner/src/prometheus.rs +++ b/utils/staking-miner/src/prometheus.rs @@ -21,9 +21,15 @@ async fn serve_req(req: Request) -> Result, hyper::Error> { .header(CONTENT_TYPE, encoder.format_type()) .body(Body::from(buffer)) .unwrap() - }, - (&Method::GET, "/") => Response::builder().status(200).body(Body::from("")).unwrap(), - _ => Response::builder().status(404).body(Body::from("")).unwrap(), + } + (&Method::GET, "/") => Response::builder() + .status(200) + .body(Body::from("")) + .unwrap(), + _ => Response::builder() + .status(404) + .body(Body::from("")) + .unwrap(), }; Ok(response) @@ -75,35 +81,40 @@ mod hidden { register_counter!(opts!( "staking_miner_trim_started", "Number of started trimmed solutions", - )).unwrap() + )) + .unwrap() }); static TRIMMED_SOLUTION_SUCCESS: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_trim_success", "Number of successful trimmed solutions", - )).unwrap() + )) + .unwrap() }); static SUBMISSIONS_STARTED: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_submissions_started", "Number of submissions started", - )).unwrap() + )) + .unwrap() }); static SUBMISSIONS_SUCCESS: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_submissions_success", "Number of submissions finished successfully", - )).unwrap() + )) + .unwrap() }); static MINED_SOLUTION_DURATION: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_mining_duration_ms", "The mined solution time in milliseconds.", - )).unwrap() + )) + .unwrap() }); static SUBMIT_SOLUTION_AND_WATCH_DURATION: Lazy = Lazy::new(|| { @@ -117,49 +128,56 @@ mod hidden { register_counter!(opts!( "staking_miner_balance", "The balance of the staking miner account", - )).unwrap() + )) + .unwrap() }); static SCORE_MINIMAL_STAKE: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_score_minimal_stake", "The minimal winner, in terms of total backing stake", - )).unwrap() + )) + .unwrap() }); static SCORE_SUM_STAKE: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_score_sum_stake", "The sum of the total backing of all winners", - )).unwrap() + )) + .unwrap() }); static SCORE_SUM_STAKE_SQUARED: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_score_sum_stake_squared", "The sum of the total backing of all winners, aka. the variance.", - )).unwrap() + )) + .unwrap() }); static RUNTIME_UPGRADES: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_runtime", "Number of runtime upgrades performed", - )).unwrap() + )) + .unwrap() }); static SUBMISSION_LENGTH: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_solution_length_bytes", "Number of bytes in the solution submitted", - )).unwrap() + )) + .unwrap() }); static SUBMISSION_WEIGHT: Lazy = Lazy::new(|| { register_counter!(opts!( "staking_miner_solution_weight", "Weight of the solution submitted", - )).unwrap() + )) + .unwrap() }); pub fn on_runtime_upgrade() { diff --git a/utils/staking-miner/src/signer.rs b/utils/staking-miner/src/signer.rs index ca62d28..5f2ea7f 100644 --- a/utils/staking-miner/src/signer.rs +++ b/utils/staking-miner/src/signer.rs @@ -16,7 +16,10 @@ impl std::fmt::Display for Signer { impl Clone for Signer { fn clone(&self) -> Self { - Self { pair: self.pair.clone(), signer: PairSigner::new(self.pair.clone()) } + Self { + pair: self.pair.clone(), + signer: PairSigner::new(self.pair.clone()), + } } } diff --git a/utils/staking-miner/src/static_types.rs b/utils/staking-miner/src/static_types.rs index 9fd80ba..485377e 100644 --- a/utils/staking-miner/src/static_types.rs +++ b/utils/staking-miner/src/static_types.rs @@ -46,7 +46,10 @@ mod max_weight { impl MaxWeight { pub fn get() -> Weight { - Weight::from_parts(REF_TIME.load(Ordering::SeqCst), PROOF_SIZE.load(Ordering::SeqCst)) + Weight::from_parts( + REF_TIME.load(Ordering::SeqCst), + PROOF_SIZE.load(Ordering::SeqCst), + ) } } @@ -84,7 +87,7 @@ pub mod ghost { #[derive(Debug)] pub struct MinerConfig; - impla pallet_election_provider_multi_phase::unsigned::MinerConfig for MinerConfig { + impl pallet_election_provider_multi_phase::unsigned::MinerConfig for MinerConfig { type AccountId = AccountId; type MaxLength = MaxLength; type MaxWeight = MaxWeight; @@ -100,18 +103,23 @@ pub mod ghost { ) -> Weight { let Some(votes) = epm::mock_votes( active_voters, - desired_targets.try_into().expect("Desired targets < u16::MAX"), + desired_targets + .try_into() + .expect("Desired targets < u16::MAX"), ) else { return Weight::MAX; }; let raw = RawSolution { - solution: NposSolution16 { votes1: votes, ..Default::default() }, + solution: NposSolution16 { + votes1: votes, + ..Default::default() + }, ..Default::default() }; - if raw.solution.voter_count() != active_voters as usize || - raw.solution.unique_targets().len() != desired_targets as usize + if raw.solution.voter_count() != active_voters as usize + || raw.solution.unique_targets().len() != desired_targets as usize { return Weight::MAX; } @@ -119,7 +127,8 @@ pub mod ghost { futures::executor::block_on(epm::runtime_api_solution_weight( raw, SolutionOrSnapshotSize { voters, targets }, - )).expect("solution_weight should work") + )) + .expect("solution_weight should work") } } } @@ -139,7 +148,7 @@ pub mod casper { #[derive(Debug)] pub struct MinerConfig; - impla pallet_election_provider_multi_phase::unsigned::MinerConfig for MinerConfig { + impl pallet_election_provider_multi_phase::unsigned::MinerConfig for MinerConfig { type AccountId = AccountId; type MaxLength = MaxLength; type MaxWeight = MaxWeight; @@ -155,18 +164,23 @@ pub mod casper { ) -> Weight { let Some(votes) = epm::mock_votes( active_voters, - desired_targets.try_into().expect("Desired targets < u16::MAX"), + desired_targets + .try_into() + .expect("Desired targets < u16::MAX"), ) else { return Weight::MAX; }; let raw = RawSolution { - solution: NposSolution16 { votes1: votes, ..Default::default() }, + solution: NposSolution16 { + votes1: votes, + ..Default::default() + }, ..Default::default() }; - if raw.solution.voter_count() != active_voters as usize || - raw.solution.unique_targets().len() != desired_targets as usize + if raw.solution.voter_count() != active_voters as usize + || raw.solution.unique_targets().len() != desired_targets as usize { return Weight::MAX; } @@ -174,7 +188,8 @@ pub mod casper { futures::executor::block_on(epm::runtime_api_solution_weight( raw, SolutionOrSnapshotSize { voters, targets }, - )).expect("solution_weight should work") + )) + .expect("solution_weight should work") } } }