diff --git a/Cargo.lock b/Cargo.lock index 07a847a4e5e6..05d9b0f5f810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3181,6 +3181,12 @@ dependencies = [ "slab", ] +[[package]] +name = "jsonrpsee" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e48ecdd757b22fae87e87aad2dbadf11c56499c6509763c8ef20db16ffb0e9" + [[package]] name = "jsonrpsee-proc-macros" version = "0.2.0" @@ -9984,6 +9990,44 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dba1a27d3efae4351c8051072d619e3ade2820635c3958d826bfea39d59b54c8" +[[package]] +name = "staking-miner" +version = "0.9.0" +dependencies = [ + "env_logger 0.8.4", + "frame-election-provider-support", + "frame-support", + "frame-system", + "hex", + "jsonrpsee", + "jsonrpsee-types", + "jsonrpsee-ws-client", + "kusama-runtime", + "lazy_static", + "log", + "pallet-election-provider-multi-phase", + "pallet-staking", + "pallet-transaction-payment", + "parity-scale-codec", + "paste 1.0.5", + "polkadot-core-primitives", + "polkadot-runtime", + "polkadot-runtime-common", + "remote-externalities", + "serde", + "serde_json", + "sp-core", + "sp-io", + "sp-npos-elections", + "sp-runtime", + "sp-transaction-pool", + "sp-version", + "structopt", + "thiserror", + "tokio 0.2.21", + "westend-runtime", +] + [[package]] name = "static_assertions" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 54681c52ebe1..a9cd823c8d25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ members = [ "parachain/test-parachains", "parachain/test-parachains/adder", "parachain/test-parachains/adder/collator", + "utils/staking-miner", ] # We want to be able to build the bridge relayer without pulling it (and all of its diff --git a/node/service/src/chain_spec.rs b/node/service/src/chain_spec.rs index 676762b7e536..2eee6b5f4cea 100644 --- a/node/service/src/chain_spec.rs +++ b/node/service/src/chain_spec.rs @@ -1213,7 +1213,7 @@ pub fn polkadot_testnet_genesis( }, staking: polkadot::StakingConfig { minimum_validator_count: 1, - validator_count: 2, + validator_count: initial_authorities.len() as u32, stakers: initial_authorities .iter() .map(|x| { @@ -1312,7 +1312,7 @@ pub fn kusama_testnet_genesis( }, staking: kusama::StakingConfig { minimum_validator_count: 1, - validator_count: 2, + validator_count: initial_authorities.len() as u32, stakers: initial_authorities .iter() .map(|x| { @@ -1416,7 +1416,7 @@ pub fn westend_testnet_genesis( }, staking: westend::StakingConfig { minimum_validator_count: 1, - validator_count: 2, + validator_count: initial_authorities.len() as u32, stakers: initial_authorities .iter() .map(|x| { diff --git a/runtime/common/src/elections.rs b/runtime/common/src/elections.rs index 3b7086fcfd5d..b2109b598ebd 100644 --- a/runtime/common/src/elections.rs +++ b/runtime/common/src/elections.rs @@ -18,10 +18,14 @@ use frame_support::{ parameter_types, - traits::Get, - weights::{DispatchClass, Weight, WeightToFeePolynomial}, + weights::{DispatchClass, Weight}, }; -use sp_runtime::Perbill; +use sp_runtime::{ + traits::{Zero, Dispatchable}, + FixedU128, FixedPointNumber, Perbill, +}; +use pallet_transaction_payment::OnChargeTransaction; +use frame_support::weights::{DispatchInfo, Pays}; use super::{BlockExecutionWeight, BlockLength, BlockWeights}; parameter_types! { @@ -44,18 +48,21 @@ parameter_types! { .get(DispatchClass::Normal); } -/// Compute the expected fee for submitting an election solution. -/// -/// This is `multiplier` multiplied by the fee for the expected submission weight according to the -/// weight info. -/// -/// Assumes that the signed submission queue is full. -pub fn fee_for_submit_call(multiplier: Perbill) -> WeightToFee::Balance +pub fn fee_for_submit_call( + multiplier: FixedU128, + weight: Weight, + length: u32, +) -> primitives::v1::Balance where - T: pallet_election_provider_multi_phase::Config, - WeightToFee: WeightToFeePolynomial, - WeightInfo: pallet_election_provider_multi_phase::WeightInfo, + T: pallet_transaction_payment::Config, + ::OnChargeTransaction: + OnChargeTransaction, + ::Call: Dispatchable, { - let expected_weight = WeightInfo::submit(T::SignedMaxSubmissions::get()); - multiplier * WeightToFee::calc(&expected_weight) + let info = DispatchInfo { weight, class: DispatchClass::Normal, pays_fee: Pays::Yes }; + multiplier.saturating_mul_int(pallet_transaction_payment::Pallet::::compute_fee( + length, + &info, + Zero::zero(), + )) } diff --git a/runtime/kusama/src/lib.rs b/runtime/kusama/src/lib.rs index 77cbcfcde33a..33e7b3697fe8 100644 --- a/runtime/kusama/src/lib.rs +++ b/runtime/kusama/src/lib.rs @@ -65,8 +65,8 @@ use xcm_builder::{ use xcm_executor::XcmExecutor; use sp_arithmetic::Perquintill; use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, - ApplyExtrinsicResult, KeyTypeId, Percent, Permill, Perbill, + create_runtime_str, generic, impl_opaque_keys, ApplyExtrinsicResult, KeyTypeId, Percent, + Permill, Perbill, FixedPointNumber, transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority}, traits::{ BlakeTwo256, Block as BlockT, OpaqueKeys, ConvertInto, AccountIdLookup, @@ -101,6 +101,7 @@ pub use pallet_staking::StakerStatus; pub use sp_runtime::BuildStorage; pub use pallet_timestamp::Call as TimestampCall; pub use pallet_balances::Call as BalancesCall; +pub use pallet_election_provider_multi_phase::Call as EPMCall; /// Constant values used within the runtime. pub mod constants; @@ -348,6 +349,7 @@ impl pallet_session::historical::Config for Runtime { type FullIdentificationOf = pallet_staking::ExposureOf; } +use pallet_election_provider_multi_phase::WeightInfo; parameter_types! { // phase durations. 1/4 of the last session for each. pub const SignedPhase: u32 = EPOCH_DURATION_IN_SLOTS / 4; @@ -360,11 +362,14 @@ parameter_types! { // This formula is currently adjusted such that a typical solution will spend an amount equal // to the base deposit for every 50 kb. pub const SignedDepositByte: Balance = deposit(1, 0) / (50 * 1024); - pub SignedRewardBase: Balance = fee_for_submit_call::< - Runtime, - crate::constants::fee::WeightToFee, - crate::weights::pallet_election_provider_multi_phase::WeightInfo, - >(Perbill::from_perthousand(1500)); + pub SignedRewardBase: Balance = fee_for_submit_call::( + // give 20% threshold. + sp_runtime::FixedU128::saturating_from_rational(12, 10), + // maximum weight possible. + weights::pallet_election_provider_multi_phase::WeightInfo::::submit(SignedMaxSubmissions::get()), + // assume a solution of 100kb length. + 100 * 1024 + ); // fallback: emergency phase. pub const Fallback: pallet_election_provider_multi_phase::FallbackStrategy = diff --git a/runtime/polkadot/src/lib.rs b/runtime/polkadot/src/lib.rs index 518c24fb3754..a0bb4f1dc485 100644 --- a/runtime/polkadot/src/lib.rs +++ b/runtime/polkadot/src/lib.rs @@ -41,7 +41,7 @@ use primitives::v1::{ ValidatorIndex, InboundDownwardMessage, InboundHrmpMessage, SessionInfo, }; use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, ApplyExtrinsicResult, + create_runtime_str, generic, impl_opaque_keys, ApplyExtrinsicResult, FixedPointNumber, KeyTypeId, Percent, Permill, Perbill, curve::PiecewiseLinear, transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority}, traits::{ @@ -77,6 +77,7 @@ pub use pallet_staking::StakerStatus; pub use sp_runtime::BuildStorage; pub use pallet_timestamp::Call as TimestampCall; pub use pallet_balances::Call as BalancesCall; +pub use pallet_election_provider_multi_phase::Call as EPMCall; /// Constant values used within the runtime. pub mod constants; @@ -328,6 +329,7 @@ impl pallet_session::historical::Config for Runtime { type FullIdentificationOf = pallet_staking::ExposureOf; } +use pallet_election_provider_multi_phase::WeightInfo; parameter_types! { // phase durations. 1/4 of the last session for each. pub const SignedPhase: u32 = EPOCH_DURATION_IN_SLOTS / 4; @@ -340,11 +342,14 @@ parameter_types! { // This formula is currently adjusted such that a typical solution will spend an amount equal // to the base deposit for every 50 kb. pub const SignedDepositByte: Balance = deposit(1, 0) / (50 * 1024); - pub SignedRewardBase: Balance = fee_for_submit_call::< - Runtime, - crate::constants::fee::WeightToFee, - crate::weights::pallet_election_provider_multi_phase::WeightInfo, - >(Perbill::from_perthousand(1500)); + pub SignedRewardBase: Balance = fee_for_submit_call::( + // give 20% threshold. + sp_runtime::FixedU128::saturating_from_rational(12, 10), + // maximum weight possible. + weights::pallet_election_provider_multi_phase::WeightInfo::::submit(SignedMaxSubmissions::get()), + // assume a solution of 200kb length. + 200 * 1024 + ); // fallback: emergency phase. pub const Fallback: pallet_election_provider_multi_phase::FallbackStrategy = diff --git a/runtime/westend/src/lib.rs b/runtime/westend/src/lib.rs index 4a312cec5d7f..499825520ddf 100644 --- a/runtime/westend/src/lib.rs +++ b/runtime/westend/src/lib.rs @@ -65,7 +65,7 @@ use xcm_builder::{ }; use sp_runtime::{ - create_runtime_str, generic, impl_opaque_keys, + create_runtime_str, generic, impl_opaque_keys, FixedPointNumber, ApplyExtrinsicResult, KeyTypeId, Perbill, curve::PiecewiseLinear, transaction_validity::{TransactionValidity, TransactionSource, TransactionPriority}, traits::{ @@ -100,6 +100,7 @@ pub use pallet_staking::StakerStatus; pub use sp_runtime::BuildStorage; pub use pallet_timestamp::Call as TimestampCall; pub use pallet_balances::Call as BalancesCall; +pub use pallet_election_provider_multi_phase::Call as EPMCall; /// Constant values used within the runtime. pub mod constants; @@ -333,6 +334,7 @@ impl pallet_session::historical::Config for Runtime { type FullIdentificationOf = pallet_staking::ExposureOf; } +use pallet_election_provider_multi_phase::WeightInfo; parameter_types! { // phase durations. 1/4 of the last session for each. pub const SignedPhase: u32 = EPOCH_DURATION_IN_SLOTS / 4; @@ -345,11 +347,14 @@ parameter_types! { // This formula is currently adjusted such that a typical solution will spend an amount equal // to the base deposit for every 50 kb. pub const SignedDepositByte: Balance = deposit(1, 0) / (50 * 1024); - pub SignedRewardBase: Balance = fee_for_submit_call::< - Runtime, - crate::constants::fee::WeightToFee, - crate::weights::pallet_election_provider_multi_phase::WeightInfo, - >(Perbill::from_perthousand(1500)); + pub SignedRewardBase: Balance = fee_for_submit_call::( + // give 20% threshold. + sp_runtime::FixedU128::saturating_from_rational(12, 10), + // maximum weight possible. + weights::pallet_election_provider_multi_phase::WeightInfo::::submit(SignedMaxSubmissions::get()), + // assume a solution of 100kb length. + 100 * 1024 + ); // fallback: emergency phase. pub const Fallback: pallet_election_provider_multi_phase::FallbackStrategy = diff --git a/utils/staking-miner/.gitignore b/utils/staking-miner/.gitignore new file mode 100644 index 000000000000..db7cff848330 --- /dev/null +++ b/utils/staking-miner/.gitignore @@ -0,0 +1,2 @@ +*.key +*.bin diff --git a/utils/staking-miner/Cargo.toml b/utils/staking-miner/Cargo.toml new file mode 100644 index 000000000000..e8aae026bf72 --- /dev/null +++ b/utils/staking-miner/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "staking-miner" +version = "0.9.0" +authors = ["Parity Technologies "] +edition = "2018" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0" } +tokio = { version = "0.2", features = ["macros"] } +log = "0.4.11" +env_logger = "0.8.3" +structopt = "0.3.0" +jsonrpsee-ws-client = { version = "0.2.0", default-features = false, features = ["tokio02"] } +jsonrpsee-types = { version = "0.2.0" } +jsonrpsee = "=0.2.0-alpha.6" +serde_json = "1.0" +serde = "1.0.0" +hex = "0.4.0" +lazy_static = "1.4.0" +paste = "1.0.5" +thiserror = "1.0.0" + +remote-externalities = { git = "https://github.com/paritytech/substrate", branch = "master" } + +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-npos-elections = { git = "https://github.com/paritytech/substrate", branch = "master" } +sp-transaction-pool = { git = "https://github.com/paritytech/substrate", branch = "master" } + + +frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" } +frame-election-provider-support = { git = "https://github.com/paritytech/substrate", branch = "master" } +pallet-election-provider-multi-phase = { git = "https://github.com/paritytech/substrate", branch = "master" } +pallet-staking = { git = "https://github.com/paritytech/substrate", branch = "master" } +pallet-transaction-payment = { git = "https://github.com/paritytech/substrate", branch = "master" } + +core-primitives = { package = "polkadot-core-primitives", path = "../../core-primitives" } + +runtime-common = { package = "polkadot-runtime-common", path = "../../runtime/common" } +polkadot-runtime = { path = "../../runtime/polkadot" } +kusama-runtime = { path = "../../runtime/kusama" } +westend-runtime = { path = "../../runtime/westend" } + +[dev-dependencies] +sp-version = { git = "https://github.com/paritytech/substrate", branch = "master" } diff --git a/utils/staking-miner/src/dry_run.rs b/utils/staking-miner/src/dry_run.rs new file mode 100644 index 000000000000..853e8da588b8 --- /dev/null +++ b/utils/staking-miner/src/dry_run.rs @@ -0,0 +1,105 @@ +// 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 . + +//! The dry-run command. + +use crate::{ + params, prelude::*, rpc_helpers::*, signer::Signer, DryRunConfig, Error, SharedConfig, WsClient, +}; +use codec::Encode; + +/// Forcefully create the snapshot. This can be used to compute the election at anytime. +fn force_create_snapshot(ext: &mut Ext) -> Result<(), Error> { + ext.execute_with(|| { + if >::exists() { + log::info!(target: LOG_TARGET, "snapshot already exists."); + Ok(()) + } else { + log::info!(target: LOG_TARGET, "creating a fake snapshot now."); + >::create_snapshot().map(|_| ()).map_err(Into::into) + } + }) +} + +/// Helper method to print the encoded size of the snapshot. +fn measure_snapshot_size(ext: &mut Ext) { + ext.execute_with(|| { + log::info!(target: LOG_TARGET, "Metadata: {:?}", >::snapshot_metadata()); + log::info!( + target: LOG_TARGET, + "Encoded Length: {:?}", + >::snapshot() + .expect("snapshot must exist before calling `measure_snapshot_size`") + .encode() + .len() + ); + }) +} + +/// Find the stake threshold in order to have at most `count` voters. +#[allow(unused)] +fn find_threshold(ext: &mut Ext, count: usize) { + ext.execute_with(|| { + let mut voters = >::snapshot() + .expect("snapshot must exist before calling `measure_snapshot_size`") + .voters; + voters.sort_by_key(|(_voter, weight, _targets)| std::cmp::Reverse(*weight)); + match voters.get(count) { + Some(threshold_voter) => println!("smallest allowed voter is {:?}", threshold_voter), + None => { + println!("requested truncation to {} voters but had only {}", count, voters.len()); + println!("smallest current voter: {:?}", voters.last()); + } + } + }) +} + +macro_rules! dry_run_cmd_for { ($runtime:ident) => { paste::paste! { + /// Execute the dry-run command. + pub(crate) async fn []( + client: &WsClient, + shared: SharedConfig, + config: DryRunConfig, + signer: Signer, + ) -> Result<(), Error> { + use $crate::[<$runtime _runtime_exports>]::*; + let mut ext = crate::create_election_ext::(shared.uri.clone(), config.at, true).await?; + force_create_snapshot::(&mut ext)?; + measure_snapshot_size::(&mut ext); + let (raw_solution, witness) = crate::mine_unchecked::(&mut ext, config.iterations, false)?; + log::info!(target: LOG_TARGET, "mined solution with {:?}", &raw_solution.score); + + let nonce = crate::get_account_info::(client, &signer.account, config.at) + .await? + .map(|i| i.nonce) + .expect("signer account is checked to exist upon startup; it can only die if it \ + transfers funds out of it, or get slashed. If it does not exist at this point, \ + it is likely due to a bug, or the signer got slashed. Terminating." + ); + let tip = 0 as Balance; + let era = sp_runtime::generic::Era::Immortal; + let extrinsic = ext.execute_with(|| create_uxt(raw_solution, witness, signer.clone(), nonce, tip, era)); + + let bytes = sp_core::Bytes(extrinsic.encode().to_vec()); + let outcome = rpc_decode::(client, "system_dryRun", params!{ bytes }).await?; + log::info!(target: LOG_TARGET, "dry-run outcome is {:?}", outcome); + Ok(()) + } +}}} + +dry_run_cmd_for!(polkadot); +dry_run_cmd_for!(kusama); +dry_run_cmd_for!(westend); diff --git a/utils/staking-miner/src/emergency_solution.rs b/utils/staking-miner/src/emergency_solution.rs new file mode 100644 index 000000000000..b41c69e1e0e6 --- /dev/null +++ b/utils/staking-miner/src/emergency_solution.rs @@ -0,0 +1,52 @@ +// 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 . + +//! The emergency-solution command. + +use crate::{prelude::*, SharedConfig, Error}; +use std::io::Write; +use codec::Encode; + +macro_rules! emergency_solution_cmd_for { ($runtime:ident) => { paste::paste! { + /// Execute the emergency-solution command. + pub(crate) async fn []( + shared: SharedConfig, + ) -> Result<(), Error> { + use $crate::[<$runtime _runtime_exports>]::*; + let mut ext = crate::create_election_ext::(shared.uri.clone(), None, false).await?; + ext.execute_with(|| { + assert!(EPM::Pallet::::current_phase().is_emergency()); + // NOTE: this internally calls feasibility_check, but we just re-do it here as an easy way + // to get a `ReadySolution`. + let (raw_solution, _) = >::mine_solution(50)?; + log::info!(target: LOG_TARGET, "mined solution with {:?}", &raw_solution.score); + let ready_solution = EPM::Pallet::::feasibility_check(raw_solution, EPM::ElectionCompute::Signed)?; + let encoded_ready = ready_solution.encode(); + let encoded_support = ready_solution.supports.encode(); + let mut solution_file = std::fs::File::create("solution.bin")?; + let mut supports_file = std::fs::File::create("solution.supports.bin")?; + solution_file.write_all(&encoded_ready)?; + supports_file.write_all(&encoded_support)?; + log::info!(target: LOG_TARGET, "ReadySolution: size {:?} / score = {:?}", encoded_ready.len(), ready_solution.score); + log::trace!(target: LOG_TARGET, "Supports: {}", sp_core::hexdisplay::HexDisplay::from(&encoded_support)); + Ok(()) + }) + } +}}} + +emergency_solution_cmd_for!(polkadot); +emergency_solution_cmd_for!(kusama); +emergency_solution_cmd_for!(westend); diff --git a/utils/staking-miner/src/main.rs b/utils/staking-miner/src/main.rs new file mode 100644 index 000000000000..0bc83866f977 --- /dev/null +++ b/utils/staking-miner/src/main.rs @@ -0,0 +1,493 @@ +// 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::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 chian 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}; + let system_block_hash_key = >::prefix_hash(); + + 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(&system_block_hash_key) + .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()); + } +} diff --git a/utils/staking-miner/src/monitor.rs b/utils/staking-miner/src/monitor.rs new file mode 100644 index 000000000000..502b5a0bc55e --- /dev/null +++ b/utils/staking-miner/src/monitor.rs @@ -0,0 +1,163 @@ +// 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 . + +//! The monitor command. + +use crate::{ + params, prelude::*, rpc_helpers::*, signer::Signer, Error, MonitorConfig, SharedConfig, +}; +use codec::Encode; +use jsonrpsee_ws_client::{ + traits::SubscriptionClient, v2::params::JsonRpcParams, Subscription, WsClient, +}; +use sp_transaction_pool::TransactionStatus; + +/// Ensure that now is the signed phase. +async fn ensure_signed_phase( + client: &WsClient, + at: B::Hash, +) -> Result<(), Error> { + let key = sp_core::storage::StorageKey(EPM::CurrentPhase::::hashed_key().to_vec()); + let phase = get_storage::>(client, params! {key, at}) + .await? + .unwrap_or_default(); + + if phase.is_signed() { + Ok(()) + } else { + Err(Error::IncorrectPhase) + } +} + +/// Ensure that our current `us` have not submitted anything previously. +async fn ensure_no_previous_solution< + T: EPM::Config + frame_system::Config, + B: BlockT, +>( + ext: &mut Ext, + us: &AccountId, +) -> Result<(), Error> { + use EPM::signed::SignedSubmissions; + ext.execute_with(|| { + if >::get().iter().any(|ss| &ss.who == us) { + Err(Error::AlreadySubmitted) + } else { + Ok(()) + } + }) +} + +macro_rules! monitor_cmd_for { ($runtime:tt) => { paste::paste! { + /// The monitor command. + pub(crate) async fn []( + client: &WsClient, + shared: SharedConfig, + config: MonitorConfig, + signer: Signer, + ) -> Result<(), Error> { + use $crate::[<$runtime _runtime_exports>]::*; + let (sub, unsub) = if config.listen == "head" { + ("chain_subscribeNewHeads", "chain_unsubscribeNewHeads") + } else { + ("chain_subscribeFinalizedHeads", "chain_unsubscribeFinalizedHeads") + }; + + log::info!(target: LOG_TARGET, "subscribing to {:?} / {:?}", sub, unsub); + let mut subscription: Subscription
= client + .subscribe(&sub, JsonRpcParams::NoParams, &unsub) + .await + .unwrap(); + + while let Some(now) = subscription.next().await? { + let hash = now.hash(); + log::debug!(target: LOG_TARGET, "new event at #{:?} ({:?})", now.number, hash); + + // we prefer doing this check before fetching anything into a remote-ext. + if ensure_signed_phase::(client, hash).await.is_err() { + log::debug!(target: LOG_TARGET, "phase closed, not interested in this block at all."); + continue; + }; + + // NOTE: we don't check the score of any of the submitted solutions. If we submit a weak + // one, as long as we are valid, we will end up getting our deposit back, so not a big + // deal for now. Note that to avoid an unfeasible solution, we should make sure that we + // only start the process on a finalized snapshot. If the signed phase is long enough, + // this will not be a solution. + + // grab an externalities without staking, just the election snapshot. + let mut ext = crate::create_election_ext::(shared.uri.clone(), Some(hash), false).await?; + + if ensure_no_previous_solution::(&mut ext, &signer.account).await.is_err() { + log::debug!(target: LOG_TARGET, "We already have a solution in this phase, skipping."); + continue; + } + + let (raw_solution, witness) = crate::mine_unchecked::(&mut ext, config.iterations, true)?; + log::info!(target: LOG_TARGET, "mined solution with {:?}", &raw_solution.score); + + let nonce = crate::get_account_info::(client, &signer.account, Some(hash)) + .await? + .map(|i| i.nonce) + .expect(crate::signer::SIGNER_ACCOUNT_WILL_EXIST); + let tip = 0 as Balance; + let period = ::BlockHashCount::get() / 2; + let current_block = now.number.saturating_sub(1); + let era = sp_runtime::generic::Era::mortal(period.into(), current_block.into()); + log::trace!(target: LOG_TARGET, "transaction mortality: {:?} -> {:?}", era.birth(current_block.into()), era.death(current_block.into())); + let extrinsic = ext.execute_with(|| create_uxt(raw_solution, witness, signer.clone(), nonce, tip, era)); + let bytes = sp_core::Bytes(extrinsic.encode()); + + let mut tx_subscription: Subscription< + TransactionStatus<::Hash, ::Hash> + > = client + .subscribe(&"author_submitAndWatchExtrinsic", params! { bytes }, "author_unwatchExtrinsic") + .await + .unwrap(); + + let _success = while let Some(status_update) = tx_subscription.next().await? { + log::trace!(target: LOG_TARGET, "status update {:?}", status_update); + match status_update { + TransactionStatus::Ready | TransactionStatus::Broadcast(_) | TransactionStatus::Future => continue, + TransactionStatus::InBlock(hash) => { + log::info!(target: LOG_TARGET, "included at {:?}", hash); + let key = sp_core::storage::StorageKey(frame_system::Events::::hashed_key().to_vec()); + let events =get_storage::< + Vec::Hash>> + >(client, params!{ key, hash }).await?.unwrap_or_default(); + log::info!(target: LOG_TARGET, "events at inclusion {:?}", events); + } + TransactionStatus::Retracted(hash) => { + log::info!(target: LOG_TARGET, "Retracted at {:?}", hash); + } + TransactionStatus::Finalized(hash) => { + log::info!(target: LOG_TARGET, "Finalized at {:?}", hash); + break + } + _ => { + log::warn!(target: LOG_TARGET, "Stopping listen due to other status {:?}", status_update); + break + } + } + }; + } + + Ok(()) + } +}}} + +monitor_cmd_for!(polkadot); +monitor_cmd_for!(kusama); +monitor_cmd_for!(westend); diff --git a/utils/staking-miner/src/prelude.rs b/utils/staking-miner/src/prelude.rs new file mode 100644 index 000000000000..7989c2e3c66b --- /dev/null +++ b/utils/staking-miner/src/prelude.rs @@ -0,0 +1,48 @@ +// 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 . + +//! Types that we don't fetch from a particular runtime and just assume that they are constant all +//! of the place. +//! +//! It is actually easy to convert the rest as well, but it'll be a lot of noise in our codebase, +//! needing to sprinkle `any_runtime` in a few extra places. + +/// The account id type. +pub type AccountId = core_primitives::AccountId; +/// The block number type. +pub type BlockNumber = core_primitives::BlockNumber; +/// The balance type. +pub type Balance = core_primitives::Balance; +/// The index of an account. +pub type Index = core_primitives::AccountIndex; +/// The hash type. We re-export it here, but we can easily get it from block as well. +pub type Hash = core_primitives::Hash; + +pub use sp_runtime::traits::{Block as BlockT, Header as HeaderT}; + +/// Default URI to connect to. +pub const DEFAULT_URI: &'static str = "wss://rpc.polkadot.io"; +/// The logging target. +pub const LOG_TARGET: &'static str = "staking-miner"; + +/// The election provider pallet. +pub use pallet_election_provider_multi_phase as EPM; + +/// The externalities type. +pub type Ext = sp_io::TestExternalities; + +/// The key pair type being used. We 'strongly' assume sr25519 for simplicity. +pub type Pair = sp_core::sr25519::Pair; diff --git a/utils/staking-miner/src/rpc_helpers.rs b/utils/staking-miner/src/rpc_helpers.rs new file mode 100644 index 000000000000..c2590d1f8406 --- /dev/null +++ b/utils/staking-miner/src/rpc_helpers.rs @@ -0,0 +1,107 @@ +// 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 . + +//! Helper method for RPC. + +use super::*; +use jsonrpsee_ws_client::traits::Client; +pub(crate) use jsonrpsee_ws_client::v2::params::JsonRpcParams; + +#[macro_export] +macro_rules! params { + ($($param:expr),*) => { + { + let mut __params = vec![]; + $( + __params.push(serde_json::to_value($param).expect("json serialization infallible; qed.")); + )* + $crate::rpc_helpers::JsonRpcParams::Array(__params) + } + }; + () => { + $crate::rpc::JsonRpcParams::NoParams, + } +} + +/// Make the rpc request, returning `Ret`. +pub(crate) async fn rpc<'a, Ret: serde::de::DeserializeOwned>( + client: &WsClient, + method: &'a str, + params: JsonRpcParams<'a>, +) -> Result { + client.request::(method, params).await.map_err(Into::into) +} + +/// Make the rpc request, decode the outcome into `Dec`. Don't use for storage, it will fail for +/// non-existent storage items. +pub(crate) async fn rpc_decode<'a, Dec: codec::Decode>( + client: &WsClient, + method: &'a str, + params: JsonRpcParams<'a>, +) -> Result { + let bytes = rpc::(client, method, params).await?; + ::decode(&mut &*bytes.0).map_err(Into::into) +} + +/// Get the storage item. +pub(crate) async fn get_storage<'a, T: codec::Decode>( + client: &WsClient, + params: JsonRpcParams<'a>, +) -> Result, Error> { + let maybe_bytes = rpc::>(client, "state_getStorage", params).await?; + if let Some(bytes) = maybe_bytes { + let decoded = ::decode(&mut &*bytes.0)?; + Ok(Some(decoded)) + } else { + Ok(None) + } +} + +use codec::{EncodeLike, FullCodec}; +use frame_support::storage::{StorageMap, StorageValue}; +#[allow(unused)] +pub(crate) async fn get_storage_value_frame_v2<'a, V: StorageValue, T: FullCodec, Hash>( + client: &WsClient, + maybe_at: Option, +) -> Result, Error> +where + V::Query: codec::Decode, + Hash: serde::Serialize, +{ + let key = >::hashed_key(); + get_storage::(&client, params! { key, maybe_at }).await +} + +#[allow(unused)] +pub(crate) async fn get_storage_map_frame_v2< + 'a, + Hash, + KeyArg: EncodeLike, + K: FullCodec, + T: FullCodec, + M: StorageMap, +>( + client: &WsClient, + key: KeyArg, + maybe_at: Option, +) -> Result, Error> +where + M::Query: codec::Decode, + Hash: serde::Serialize, +{ + let key = >::hashed_key_for(key); + get_storage::(&client, params! { key, maybe_at }).await +} diff --git a/utils/staking-miner/src/signer.rs b/utils/staking-miner/src/signer.rs new file mode 100644 index 000000000000..409d7befab8a --- /dev/null +++ b/utils/staking-miner/src/signer.rs @@ -0,0 +1,73 @@ +// 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 . + +//! Wrappers around creating a signer account. + +use crate::{rpc_helpers, AccountId, Error, Index, Pair, WsClient, LOG_TARGET}; +use sp_core::crypto::Pair as _; +use std::path::Path; + +pub(crate) const SIGNER_ACCOUNT_WILL_EXIST: &'static str = + "signer account is checked to exist upon startup; it can only die if it transfers funds out \ + of it, or get slashed. If it does not exist at this point, it is likely due to a bug, or the \ + signer got slashed. Terminating."; + +/// Some information about the signer. Redundant at this point, but makes life easier. +#[derive(Clone)] +pub(crate) struct Signer { + /// The account id. + pub(crate) account: AccountId, + /// The full crypto key-pair. + pub(crate) pair: Pair, + /// The raw uri read from file. + pub(crate) uri: String, +} + +pub(crate) async fn get_account_info( + client: &WsClient, + who: &T::AccountId, + maybe_at: Option, +) -> Result>, Error> { + rpc_helpers::get_storage::>( + client, + crate::params! { + sp_core::storage::StorageKey(>::hashed_key_for(&who)), + maybe_at + }, + ) + .await +} + +/// Read the signer account's uri from the given `path`. +pub(crate) async fn read_signer_uri< + P: AsRef, + T: frame_system::Config, +>( + path: P, + client: &WsClient, +) -> Result { + let uri = std::fs::read_to_string(path)?; + + // trim any trailing garbage. + let uri = uri.trim_end(); + + let pair = Pair::from_string(&uri, None)?; + let account = T::AccountId::from(pair.public()); + let _info = + get_account_info::(&client, &account, None).await?.ok_or(Error::AccountDoesNotExists)?; + log::info!(target: LOG_TARGET, "loaded account {:?}, info: {:?}", &account, _info); + Ok(Signer { account, pair, uri: uri.to_string() }) +}