// 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 jsonrpsee_ws_client::{WsClient, WsClientBuilder}; use remote_externalities::{Builder, Mode, OnlineConfig}; 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, 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 {}", sp_core::hexdisplay::HexDisplay::from(&extrinsic.encode()) ); 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 => { use $crate::polkadot_runtime_exports::*; $($code)* }, $crate::AnyRuntime::Kusama => { use $crate::kusama_runtime_exports::*; $($code)* }, $crate::AnyRuntime::Westend => { use $crate::westend_runtime_exports::*; $($code)* } } } } } #[derive(Debug, thiserror::Error)] enum Error { Io(#[from] std::io::Error), Jsonrpsee(#[from] jsonrpsee_ws_client::types::Error), 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, } 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 { ::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)] 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(long, short, default_value = "10")] iterations: usize, } #[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(long, short, default_value = "10")] iterations: usize, } #[derive(Debug, Clone, StructOpt)] struct SharedConfig { /// The `ws` node to connect to. #[structopt(long, default_value = DEFAULT_URI)] uri: String, /// The file from which we read the account seed. /// /// WARNING: don't use an account with a large stash for this. Based on how the bot is /// configured, it might re-try lose funds through transaction fees/deposits. #[structopt(long)] account_seed: std::path::PathBuf, } #[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 `Staking` /// stored. async fn create_election_ext( uri: String, at: Option, with_staking: bool, ) -> Result { use frame_support::{storage::generator::StorageMap, traits::PalletInfo}; use sp_core::hashing::twox_128; Builder::::new() .mode(Mode::Online(OnlineConfig { transport: uri.into(), at, modules: if with_staking { vec![ ::PalletInfo::name::>() .expect("Pallet always has name; qed.") .to_string(), ::PalletInfo::name::>() .expect("Pallet always has name; qed.") .to_string(), ] } else { vec![::PalletInfo::name::>() .expect("Pallet always has name; qed.") .to_string() ] }, ..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, iterations: usize, do_feasibility: bool, ) -> Result<(EPM::RawSolution>, u32), Error> { ext.execute_with(|| { let (solution, _) = >::mine_solution(iterations)?; 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.len() == 0 { 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(()) }) } #[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::Ss58AddressFormat::PolkadotAccount, ); // 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::Ss58AddressFormat::KusamaAccount, ); // 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::Ss58AddressFormat::PolkadotAccount, ); // 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); let signer_account = any_runtime! { signer::read_signer_uri::<_, Runtime>(&shared.account_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, // --------------------^^ comes from the macro prelude, needs no generic. Command::DryRun(c) => dry_run_cmd(&client, shared, c, signer_account).await, Command::EmergencySolution => emergency_solution_cmd(shared.clone()).await, } }; log::info!(target: LOG_TARGET, "round of execution finished. outcome = {:?}", outcome); } #[cfg(test)] mod tests { use super::*; fn get_version() -> sp_version::RuntimeVersion { use frame_support::traits::Get; 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()); } }