// Copyright 2021 Parity Technologies (UK) Ltd. // This file is part of Polkadot. // Polkadot is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Polkadot is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . //! # Polkadot Staking Miner. //! //! Simple bot capable of monitoring a polkadot (and cousins) chain and submitting solutions to the //! `pallet-election-provider-multi-phase`. See `--help` for more details. //! //! # Implementation Notes: //! //! - First draft: Be aware that this is the first draft and there might be bugs, or undefined //! behaviors. Don't attach this bot to an account with lots of funds. //! - Quick to crash: The bot is written so that it only continues to work if everything goes well. //! In case of any failure (RPC, logic, IO), it will crash. This was a decision to simplify the //! development. It is intended to run this bot with a `restart = true` way, so that it reports it //! crash, but resumes work thereafter. mod dry_run; mod emergency_solution; mod monitor; mod prelude; mod rpc_helpers; mod signer; pub(crate) use prelude::*; pub(crate) use signer::get_account_info; use frame_election_provider_support::NposSolver; use frame_support::traits::Get; use jsonrpsee_ws_client::{WsClient, WsClientBuilder}; use remote_externalities::{Builder, Mode, OnlineConfig}; use sp_npos_elections::ExtendedBalance; use sp_runtime::traits::Block as BlockT; use structopt::StructOpt; pub(crate) enum AnyRuntime { Polkadot, Kusama, Westend, } pub(crate) static mut RUNTIME: AnyRuntime = AnyRuntime::Polkadot; macro_rules! construct_runtime_prelude { ($runtime:ident) => { paste::paste! { #[allow(unused_import)] pub(crate) mod [<$runtime _runtime_exports>] { pub(crate) use crate::prelude::EPM; pub(crate) use [<$runtime _runtime>]::*; pub(crate) use crate::monitor::[] as monitor_cmd; pub(crate) use crate::dry_run::[] as dry_run_cmd; pub(crate) use crate::emergency_solution::[] as emergency_solution_cmd; pub(crate) use private::{[] as create_uxt}; mod private { use super::*; pub(crate) fn []( raw_solution: EPM::RawSolution>, witness: u32, signer: crate::signer::Signer, nonce: crate::prelude::Index, tip: crate::prelude::Balance, era: sp_runtime::generic::Era, ) -> UncheckedExtrinsic { use codec::Encode as _; use sp_core::Pair as _; use sp_runtime::traits::StaticLookup as _; let crate::signer::Signer { account, pair, .. } = signer; let local_call = EPMCall::::submit { raw_solution: Box::new(raw_solution), num_signed_submissions: witness }; let call: Call = as std::convert::TryInto>::try_into(local_call) .expect("election provider pallet must exist in the runtime, thus \ inner call can be converted, qed." ); let extra: SignedExtra = crate::[](nonce, tip, era); let raw_payload = SignedPayload::new(call, extra).expect("creating signed payload infallible; qed."); let signature = raw_payload.using_encoded(|payload| { pair.clone().sign(payload) }); let (call, extra, _) = raw_payload.deconstruct(); let address = ::Lookup::unlookup(account.clone()); let extrinsic = UncheckedExtrinsic::new_signed(call, address, signature.into(), extra); log::debug!( target: crate::LOG_TARGET, "constructed extrinsic {} with length {}", sp_core::hexdisplay::HexDisplay::from(&extrinsic.encode()), extrinsic.encode().len(), ); extrinsic } } }} }; } // NOTE: we might be able to use some code from the bridges repo here. fn signed_ext_builder_polkadot( nonce: Index, tip: Balance, era: sp_runtime::generic::Era, ) -> polkadot_runtime_exports::SignedExtra { use polkadot_runtime_exports::Runtime; ( frame_system::CheckSpecVersion::::new(), frame_system::CheckTxVersion::::new(), frame_system::CheckGenesis::::new(), frame_system::CheckMortality::::from(era), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), runtime_common::claims::PrevalidateAttests::::new(), ) } fn signed_ext_builder_kusama( nonce: Index, tip: Balance, era: sp_runtime::generic::Era, ) -> kusama_runtime_exports::SignedExtra { use kusama_runtime_exports::Runtime; ( frame_system::CheckSpecVersion::::new(), frame_system::CheckTxVersion::::new(), frame_system::CheckGenesis::::new(), frame_system::CheckMortality::::from(era), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), ) } fn signed_ext_builder_westend( nonce: Index, tip: Balance, era: sp_runtime::generic::Era, ) -> westend_runtime_exports::SignedExtra { use westend_runtime_exports::Runtime; ( frame_system::CheckSpecVersion::::new(), frame_system::CheckTxVersion::::new(), frame_system::CheckGenesis::::new(), frame_system::CheckMortality::::from(era), frame_system::CheckNonce::::from(nonce), frame_system::CheckWeight::::new(), pallet_transaction_payment::ChargeTransactionPayment::::from(tip), ) } construct_runtime_prelude!(polkadot); construct_runtime_prelude!(kusama); construct_runtime_prelude!(westend); // NOTE: this is no longer used extensively, most of the per-runtime stuff us delegated to // `construct_runtime_prelude` and macro's the import directly from it. A part of the code is also // still generic over `T`. My hope is to still make everything generic over a `Runtime`, but sadly // that is not currently possible as each runtime has its unique `Call`, and all Calls are not // sharing any generic trait. In other words, to create the `UncheckedExtrinsic` of each chain, you // need the concrete `Call` of that chain as well. #[macro_export] macro_rules! any_runtime { ($($code:tt)*) => { unsafe { match $crate::RUNTIME { $crate::AnyRuntime::Polkadot => { #[allow(unused)] use $crate::polkadot_runtime_exports::*; $($code)* }, $crate::AnyRuntime::Kusama => { #[allow(unused)] use $crate::kusama_runtime_exports::*; $($code)* }, $crate::AnyRuntime::Westend => { #[allow(unused)] use $crate::westend_runtime_exports::*; $($code)* } } } } } /// Same as [`any_runtime`], but instead of returning a `Result`, this simply returns `()`. Useful /// for situations where the result is not useful and un-ergonomic to handle. #[macro_export] macro_rules! any_runtime_unit { ($($code:tt)*) => { unsafe { match $crate::RUNTIME { $crate::AnyRuntime::Polkadot => { #[allow(unused)] use $crate::polkadot_runtime_exports::*; let _ = $($code)*; }, $crate::AnyRuntime::Kusama => { #[allow(unused)] use $crate::kusama_runtime_exports::*; let _ = $($code)*; }, $crate::AnyRuntime::Westend => { #[allow(unused)] use $crate::westend_runtime_exports::*; let _ = $($code)*; } } } } } #[derive(frame_support::DebugNoBound, thiserror::Error)] enum Error { Io(#[from] std::io::Error), JsonRpsee(#[from] jsonrpsee_ws_client::types::Error), RpcHelperError(#[from] rpc_helpers::RpcHelperError), Codec(#[from] codec::Error), Crypto(sp_core::crypto::SecretStringError), RemoteExternalities(&'static str), PalletMiner(EPM::unsigned::MinerError), PalletElection(EPM::ElectionError), PalletFeasibility(EPM::FeasibilityError), AccountDoesNotExists, IncorrectPhase, AlreadySubmitted, VersionMismatch, } impl From for Error { fn from(e: sp_core::crypto::SecretStringError) -> Error { Error::Crypto(e) } } impl From> for Error { fn from(e: EPM::unsigned::MinerError) -> Error { Error::PalletMiner(e) } } impl From> for Error { fn from(e: EPM::ElectionError) -> Error { Error::PalletElection(e) } } impl From for Error { fn from(e: EPM::FeasibilityError) -> Error { Error::PalletFeasibility(e) } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { as std::fmt::Debug>::fmt(self, f) } } #[derive(Debug, Clone, StructOpt)] enum Command { /// Monitor for the phase being signed, then compute. Monitor(MonitorConfig), /// Just compute a solution now, and don't submit it. DryRun(DryRunConfig), /// Provide a solution that can be submitted to the chain as an emergency response. EmergencySolution, } #[derive(Debug, Clone, StructOpt)] enum Solvers { SeqPhragmen { #[structopt(long, default_value = "10")] iterations: usize, }, PhragMMS { #[structopt(long, default_value = "10")] iterations: usize, }, } /// Mine a solution with the given `solver`. fn mine_with( solver: &Solvers, ext: &mut Ext, ) -> Result<(EPM::RawSolution>, u32), Error> where T: EPM::Config, T::Solver: NposSolver, { use frame_election_provider_support::{PhragMMS, SequentialPhragmen}; match solver { Solvers::SeqPhragmen { iterations } => { BalanceIterations::set(*iterations); mine_unchecked::< T, SequentialPhragmen< ::AccountId, sp_runtime::Perbill, Balancing, >, >(ext, false) }, Solvers::PhragMMS { iterations } => { BalanceIterations::set(*iterations); mine_unchecked::< T, PhragMMS<::AccountId, sp_runtime::Perbill, Balancing>, >(ext, false) }, } } frame_support::parameter_types! { /// Number of balancing iterations for a solution algorithm. Set based on the [`Solvers`] CLI /// config. pub static BalanceIterations: usize = 10; pub static Balancing: Option<(usize, ExtendedBalance)> = Some((BalanceIterations::get(), 0)); } #[derive(Debug, Clone, StructOpt)] 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. #[structopt(long, default_value = "head", possible_values = &["head", "finalized"])] listen: String, #[structopt(subcommand)] solver: Solvers, } #[derive(Debug, Clone, StructOpt)] struct DryRunConfig { /// The block hash at which scraping happens. If none is provided, the latest head is used. #[structopt(long)] at: Option, #[structopt(subcommand)] solver: Solvers, } #[derive(Debug, Clone, StructOpt)] struct SharedConfig { /// The `ws` node to connect to. #[structopt(long, short, default_value = DEFAULT_URI, env = "URI")] uri: String, /// The seed of a funded account in hex. /// /// 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. #[structopt(long, short, env = "SEED")] seed: String, } #[derive(Debug, Clone, StructOpt)] struct Opt { /// The `ws` node to connect to. #[structopt(flatten)] shared: SharedConfig, #[structopt(subcommand)] command: Command, } /// Build the Ext at hash with all the data of `ElectionProviderMultiPhase` and any additional /// pallets. async fn create_election_ext( uri: String, at: Option, additional: Vec, ) -> Result> { use frame_support::{storage::generator::StorageMap, traits::PalletInfo}; use sp_core::hashing::twox_128; let mut pallets = vec![::PalletInfo::name::>() .expect("Pallet always has name; qed.") .to_string()]; pallets.extend(additional); Builder::::new() .mode(Mode::Online(OnlineConfig { transport: uri.into(), at, pallets, ..Default::default() })) .inject_hashed_prefix(&>::prefix_hash()) .inject_hashed_key(&[twox_128(b"System"), twox_128(b"Number")].concat()) .build() .await .map_err(|why| Error::RemoteExternalities(why)) } /// Compute the election at the given block number. It expects to NOT be `Phase::Off`. In other /// words, the snapshot must exists on the given externalities. fn mine_unchecked( ext: &mut Ext, do_feasibility: bool, ) -> Result<(EPM::RawSolution>, u32), Error> where T: EPM::Config, S: NposSolver< Error = <::Solver as NposSolver>::Error, AccountId = <::Solver as NposSolver>::AccountId, >, { ext.execute_with(|| { let (solution, _) = >::mine_solution::().map_err::, _>(Into::into)?; if do_feasibility { let _ = >::feasibility_check( solution.clone(), EPM::ElectionCompute::Signed, )?; } let witness = >::decode_len().unwrap_or_default(); Ok((solution, witness as u32)) }) } #[allow(unused)] fn mine_dpos(ext: &mut Ext) -> Result<(), Error> { ext.execute_with(|| { use std::collections::BTreeMap; use EPM::RoundSnapshot; let RoundSnapshot { voters, .. } = EPM::Snapshot::::get().unwrap(); let desired_targets = EPM::DesiredTargets::::get().unwrap(); let mut candidates_and_backing = BTreeMap::::new(); voters.into_iter().for_each(|(who, stake, targets)| { if targets.is_empty() { println!("target = {:?}", (who, stake, targets)); return } let share: u128 = (stake as u128) / (targets.len() as u128); for target in targets { *candidates_and_backing.entry(target.clone()).or_default() += share } }); let mut candidates_and_backing = candidates_and_backing.into_iter().collect::>(); candidates_and_backing.sort_by_key(|(_, total_stake)| *total_stake); let winners = candidates_and_backing .into_iter() .rev() .take(desired_targets as usize) .collect::>(); let score = { let min_staker = *winners.last().map(|(_, stake)| stake).unwrap(); let sum_stake = winners.iter().fold(0u128, |acc, (_, stake)| acc + stake); let sum_squared = winners.iter().fold(0u128, |acc, (_, stake)| acc + stake); [min_staker, sum_stake, sum_squared] }; println!("mined a dpos-like solution with score = {:?}", score); Ok(()) }) } pub(crate) async fn check_versions( client: &WsClient, print: bool, ) -> Result<(), Error> { let linked_version = T::Version::get(); let on_chain_version = rpc_helpers::rpc::( client, "state_getRuntimeVersion", params! {}, ) .await .expect("runtime version RPC should always work; qed"); if print { log::info!(target: LOG_TARGET, "linked version {:?}", linked_version); log::info!(target: LOG_TARGET, "on-chain version {:?}", on_chain_version); } if linked_version != on_chain_version { log::error!( target: LOG_TARGET, "VERSION MISMATCH: any transaction will fail with bad-proof" ); Err(Error::VersionMismatch) } else { Ok(()) } } #[tokio::main] async fn main() { env_logger::Builder::from_default_env() .format_module_path(true) .format_level(true) .init(); let Opt { shared, command } = Opt::from_args(); log::debug!(target: LOG_TARGET, "attempting to connect to {:?}", shared.uri); let client = loop { let maybe_client = WsClientBuilder::default() .connection_timeout(std::time::Duration::new(20, 0)) .max_request_body_size(u32::MAX) .build(&shared.uri) .await; match maybe_client { Ok(client) => break client, Err(why) => { log::warn!( target: LOG_TARGET, "failed to connect to client due to {:?}, retrying soon..", why ); std::thread::sleep(std::time::Duration::from_millis(2500)); }, } }; let chain = rpc_helpers::rpc::(&client, "system_chain", params! {}) .await .expect("system_chain infallible; qed."); match chain.to_lowercase().as_str() { "polkadot" | "development" => { sp_core::crypto::set_default_ss58_version( sp_core::crypto::Ss58AddressFormatRegistry::PolkadotAccount.into(), ); sub_tokens::dynamic::set_name("DOT"); sub_tokens::dynamic::set_decimal_points(10_000_000_000); // safety: this program will always be single threaded, thus accessing global static is // safe. unsafe { RUNTIME = AnyRuntime::Polkadot; } }, "kusama" | "kusama-dev" => { sp_core::crypto::set_default_ss58_version( sp_core::crypto::Ss58AddressFormatRegistry::KusamaAccount.into(), ); sub_tokens::dynamic::set_name("KSM"); sub_tokens::dynamic::set_decimal_points(1_000_000_000_000); // safety: this program will always be single threaded, thus accessing global static is // safe. unsafe { RUNTIME = AnyRuntime::Kusama; } }, "westend" => { sp_core::crypto::set_default_ss58_version( sp_core::crypto::Ss58AddressFormatRegistry::PolkadotAccount.into(), ); sub_tokens::dynamic::set_name("WND"); sub_tokens::dynamic::set_decimal_points(1_000_000_000_000); // safety: this program will always be single threaded, thus accessing global static is // safe. unsafe { RUNTIME = AnyRuntime::Westend; } }, _ => { eprintln!("unexpected chain: {:?}", chain); return }, } log::info!(target: LOG_TARGET, "connected to chain {:?}", chain); any_runtime_unit! { check_versions::(&client, true).await }; let signer_account = any_runtime! { signer::signer_uri_from_string::(&shared.seed, &client) .await .expect("Provided account is invalid, terminating.") }; let outcome = any_runtime! { match command.clone() { Command::Monitor(c) => monitor_cmd(&client, shared, c, signer_account).await .map_err(|e| { log::error!(target: LOG_TARGET, "Monitor error: {:?}", e); }), Command::DryRun(c) => dry_run_cmd(&client, shared, c, signer_account).await .map_err(|e| { log::error!(target: LOG_TARGET, "DryRun error: {:?}", e); }), Command::EmergencySolution => emergency_solution_cmd(shared.clone()).await .map_err(|e| { log::error!(target: LOG_TARGET, "EmergencySolution error: {:?}", e); }), } }; log::info!(target: LOG_TARGET, "round of execution finished. outcome = {:?}", outcome); } #[cfg(test)] mod tests { use super::*; fn get_version() -> sp_version::RuntimeVersion { T::Version::get() } #[test] fn any_runtime_works() { unsafe { RUNTIME = AnyRuntime::Polkadot; } let polkadot_version = any_runtime! { get_version::() }; unsafe { RUNTIME = AnyRuntime::Kusama; } let kusama_version = any_runtime! { get_version::() }; assert_eq!(polkadot_version.spec_name, "polkadot".into()); assert_eq!(kusama_version.spec_name, "kusama".into()); } }