mirror of
https://git.ghostchain.io/proxmio/ghost-node.git
synced 2025-12-27 11:19:57 +00:00
rustfmt staking-miner and fix typos
Signed-off-by: Uncle Stretch <uncle.stretch@ghostchain.io>
This commit is contained in:
122
utils/staking-miner/src/commands/dry_run.rs
Normal file
122
utils/staking-miner/src/commands/dry_run.rs
Normal file
@@ -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<Hash>,
|
||||
|
||||
/// 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<u32>,
|
||||
|
||||
/// 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<String>,
|
||||
}
|
||||
|
||||
pub async fn dry_run_cmd<T>(client: Client, config: DryRunConfig) -> Result<(), Error>
|
||||
where
|
||||
T: MinerConfig<AccountId = AccountId, MaxVotesPerVoter = static_types::MaxVotesPerVoter>
|
||||
+ 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::<T>(
|
||||
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(())
|
||||
}
|
||||
90
utils/staking-miner/src/commands/emergency_solution.rs
Normal file
90
utils/staking-miner/src/commands/emergency_solution.rs
Normal file
@@ -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<Hash>,
|
||||
|
||||
/// 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<u32>,
|
||||
}
|
||||
|
||||
pub async fn emergency_solution_cmd<T>(
|
||||
client: Client,
|
||||
config: EmergencySolutionConfig,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: MinerConfig<AccountId = AccountId, MaxVotesPerVoter = static_types::MaxVotesPerVoter>
|
||||
+ 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::<T>(
|
||||
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(())
|
||||
}
|
||||
7
utils/staking-miner/src/commands/mod.rs
Normal file
7
utils/staking-miner/src/commands/mod.rs
Normal file
@@ -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};
|
||||
858
utils/staking-miner/src/commands/monitor.rs
Normal file
858
utils/staking-miner/src/commands/monitor.rs
Normal file
@@ -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 <percent>"`: submit if the submission is `n` percent better.
|
||||
///
|
||||
/// `--submission-strategy "no-worse-than <percent>"`: 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 <percent>": submit if submission is `n` percent better.
|
||||
///
|
||||
impl FromStr for SubmissionStrategy {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<T>(client: Client, config: MonitorConfig) -> Result<(), Error>
|
||||
where
|
||||
T: MinerConfig<AccountId = AccountId, MaxVotesPerVoter = static_types::MaxVotesPerVoter>
|
||||
+ 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::<Error>();
|
||||
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::<T>(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<T>(
|
||||
at: Header,
|
||||
client: Client,
|
||||
signer: Signer,
|
||||
config: MonitorConfig,
|
||||
submit_lock: Arc<Mutex<()>>,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
T: MinerConfig<AccountId = AccountId, MaxVotesPerVoter = static_types::MaxVotesPerVoter>
|
||||
+ 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::<T::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::<T>(
|
||||
&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::<T::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::<T>(
|
||||
&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<T>(
|
||||
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::<T>(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<T: MinerConfig + Send + Sync + 'static>(
|
||||
client: &Client,
|
||||
signer: Signer,
|
||||
(solution, score, round): (SolutionOf<T>, 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::<runtime::election_provider_multi_phase::events::SolutionStored>(
|
||||
);
|
||||
|
||||
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::<runtime::election_provider_multi_phase::events::SolutionStored>(
|
||||
);
|
||||
|
||||
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<RpcSubscription<Header>, 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<Hash, Error> {
|
||||
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::<JsonRpseeError>() {
|
||||
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<T, C>(mut tx: TxProgress<T, C>) -> Result<TxInBlock<T, C>, subxt::Error>
|
||||
where
|
||||
T: subxt::Config,
|
||||
C: subxt::client::OnlineClientT<T>,
|
||||
{
|
||||
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
|
||||
)))
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user