// Copyright 2020 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 . #![deny(unused_extern_crates, missing_docs)] //! Utilities for End to end runtime tests use codec::Encode; use democracy::{AccountVote, Conviction, Vote}; use grandpa::GrandpaBlockImport; use polkadot_runtime::{ CouncilCollective, Event, FastTrackVotingPeriod, Runtime, RuntimeApi, TechnicalCollective, }; use polkadot_runtime_common::claims; use sc_consensus_babe::BabeBlockImport; use sc_consensus_manual_seal::consensus::babe::SlotTimestampProvider; use sc_executor::NativeElseWasmExecutor; use sc_service::{TFullBackend, TFullClient}; use sp_runtime::{app_crypto::sp_core::H256, generic::Era, AccountId32}; use std::{error::Error, future::Future, str::FromStr}; use support::weights::Weight; use test_runner::{ build_runtime, client_parts, ChainInfo, ConfigOrChainSpec, Node, SignatureVerificationOverride, }; type BlockImport = BabeBlockImport>; type Block = polkadot_primitives::v1::Block; type SelectChain = sc_consensus::LongestChain, Block>; /// Declare an instance of the native executor named `ExecutorDispatch`. Include the wasm binary as the /// equivalent wasm code. pub struct ExecutorDispatch; impl sc_executor::NativeExecutionDispatch for ExecutorDispatch { type ExtendHostFunctions = (benchmarking::benchmarking::HostFunctions, SignatureVerificationOverride); fn dispatch(method: &str, data: &[u8]) -> Option> { polkadot_runtime::api::dispatch(method, data) } fn native_version() -> sc_executor::NativeVersion { polkadot_runtime::native_version() } } /// `ChainInfo` implementation. pub struct PolkadotChainInfo; impl ChainInfo for PolkadotChainInfo { type Block = Block; type ExecutorDispatch = ExecutorDispatch; type Runtime = Runtime; type RuntimeApi = RuntimeApi; type SelectChain = SelectChain; type BlockImport = BlockImport< Self::Block, TFullBackend, TFullClient>, Self::SelectChain, >; type SignedExtras = polkadot_runtime::SignedExtra; type InherentDataProviders = (SlotTimestampProvider, sp_consensus_babe::inherents::InherentDataProvider); fn signed_extras(from: ::AccountId) -> Self::SignedExtras { ( system::CheckNonZeroSender::::new(), system::CheckSpecVersion::::new(), system::CheckTxVersion::::new(), system::CheckGenesis::::new(), system::CheckMortality::::from(Era::Immortal), system::CheckNonce::::from(system::Pallet::::account_nonce(from)), system::CheckWeight::::new(), transaction_payment::ChargeTransactionPayment::::from(0), claims::PrevalidateAttests::::new(), ) } } /// Dispatch with root origin, via pallet-democracy pub async fn dispatch_with_root( call: impl Into<::Call>, node: &Node, ) -> Result<(), Box> where T: ChainInfo< Block = Block, ExecutorDispatch = ExecutorDispatch, Runtime = Runtime, RuntimeApi = RuntimeApi, SelectChain = SelectChain, BlockImport = BlockImport< Block, TFullBackend, TFullClient>, SelectChain, >, SignedExtras = polkadot_runtime::SignedExtra, >, { type DemocracyCall = democracy::Call; type CouncilCollectiveEvent = collective::Event; type CouncilCollectiveCall = collective::Call; type TechnicalCollectiveCall = collective::Call; type TechnicalCollectiveEvent = collective::Event; // here lies a black mirror esque copy of on chain whales. let whales = vec![ "1rvXMZpAj9nKLQkPFCymyH7Fg3ZyKJhJbrc7UtHbTVhJm1A", "15j4dg5GzsL1bw2U2AWgeyAk6QTxq43V7ZPbXdAmbVLjvDCK", ] .into_iter() .map(|account| AccountId32::from_str(account).unwrap()) .collect::>(); // and these let (technical_collective, council_collective) = node.with_state(|| { ( collective::Members::::get(), collective::Members::::get(), ) }); // hash of the proposal in democracy let proposal_hash = { // note the call (pre-image?) of the call. node.submit_extrinsic( DemocracyCall::note_preimage { encoded_proposal: call.into().encode() }, Some(whales[0].clone()), ) .await?; node.seal_blocks(1).await; // fetch proposal hash from event emitted by the runtime let events = node.events(); events .iter() .filter_map(|event| match event.event { Event::Democracy(democracy::Event::PreimageNoted { ref proposal_hash, .. }) => Some(proposal_hash.clone()), _ => None, }) .next() .ok_or_else(|| { format!("democracy::Event::PreimageNoted not found in events: {:#?}", events) })? }; // submit `external_propose` call through council collective { let external_propose = DemocracyCall::external_propose_majority { proposal_hash: proposal_hash.clone().into(), }; let length = external_propose.using_encoded(|x| x.len()) as u32 + 1; let weight = Weight::MAX / 100_000_000; let proposal = CouncilCollectiveCall::propose { threshold: council_collective.len() as u32, proposal: Box::new(external_propose.clone().into()), length_bound: length, }; node.submit_extrinsic(proposal.clone(), Some(council_collective[0].clone())) .await?; node.seal_blocks(1).await; // fetch proposal index from event emitted by the runtime let events = node.events(); let (index, hash): (u32, H256) = events .iter() .filter_map(|event| match event.event { Event::Council(CouncilCollectiveEvent::Proposed { account: _, proposal_index, ref proposal_hash, threshold: _, }) => Some((proposal_index, proposal_hash.clone())), _ => None, }) .next() .ok_or_else(|| { format!("CouncilCollectiveEvent::Proposed not found in events: {:#?}", events) })?; // vote for member in &council_collective[1..] { let call = CouncilCollectiveCall::vote { proposal: hash.clone(), index, approve: true }; node.submit_extrinsic(call, Some(member.clone())).await?; } node.seal_blocks(1).await; // close vote let call = CouncilCollectiveCall::close { proposal_hash: hash, index, proposal_weight_bound: weight, length_bound: length, }; node.submit_extrinsic(call, Some(council_collective[0].clone())).await?; node.seal_blocks(1).await; // assert that proposal has been passed on chain let events = node .events() .into_iter() .filter(|event| match event.event { Event::Council(CouncilCollectiveEvent::Closed { proposal_hash, yes: _, no: _ }) if hash == proposal_hash => true, Event::Council(CouncilCollectiveEvent::Approved { proposal_hash }) if hash == proposal_hash => true, Event::Council(CouncilCollectiveEvent::Executed { proposal_hash, result: Ok(()), }) if hash == proposal_hash => true, _ => false, }) .collect::>(); // make sure all 3 events are in state assert_eq!( events.len(), 3, "CouncilCollectiveEvent::{{Closed, Approved, Executed}} not found in events: {:#?}", node.events(), ); } // next technical collective must fast track the proposal. { let fast_track = DemocracyCall::fast_track { proposal_hash: proposal_hash.into(), voting_period: FastTrackVotingPeriod::get(), delay: 0, }; let weight = Weight::MAX / 100_000_000; let length = fast_track.using_encoded(|x| x.len()) as u32 + 1; let proposal = TechnicalCollectiveCall::propose { threshold: technical_collective.len() as u32, proposal: Box::new(fast_track.into()), length_bound: length, }; node.submit_extrinsic(proposal, Some(technical_collective[0].clone())).await?; node.seal_blocks(1).await; let events = node.events(); let (index, hash) = events .iter() .filter_map(|event| match event.event { Event::TechnicalCommittee(TechnicalCollectiveEvent::Proposed { account: _, proposal_index, ref proposal_hash, threshold: _, }) => Some((proposal_index, proposal_hash.clone())), _ => None, }) .next() .ok_or_else(|| { format!("TechnicalCollectiveEvent::Proposed not found in events: {:#?}", events) })?; // vote for member in &technical_collective[1..] { let call = TechnicalCollectiveCall::vote { proposal: hash.clone(), index, approve: true }; node.submit_extrinsic(call, Some(member.clone())).await?; } node.seal_blocks(1).await; // close vote let call = CouncilCollectiveCall::close { proposal_hash: hash, index, proposal_weight_bound: weight, length_bound: length, }; node.submit_extrinsic(call, Some(technical_collective[0].clone())).await?; node.seal_blocks(1).await; // assert that fast-track proposal has been passed on chain let events = node .events() .into_iter() .filter(|event| match event.event { Event::TechnicalCommittee(TechnicalCollectiveEvent::Closed { proposal_hash: _hash, .. }) if hash == _hash => true, Event::TechnicalCommittee(TechnicalCollectiveEvent::Approved { proposal_hash: _hash, }) if hash == _hash => true, Event::TechnicalCommittee(TechnicalCollectiveEvent::Executed { proposal_hash: _hash, result: Ok(()), }) if hash == _hash => true, _ => false, }) .collect::>(); // make sure all 3 events are in state assert_eq!( events.len(), 3, "TechnicalCollectiveEvent::{{Closed, Approved, Executed}} not found in events: {:#?}", node.events(), ); } // now runtime upgrade proposal is a fast-tracked referendum we can vote for. let ref_index = node .events() .into_iter() .filter_map(|event| match event.event { Event::Democracy(democracy::Event::Started { ref_index: index, .. }) => Some(index), _ => None, }) .next() .ok_or_else(|| { format!("democracy::Event::Started not found in events: {:#?}", node.events()) })?; let call = DemocracyCall::vote { ref_index, vote: AccountVote::Standard { vote: Vote { aye: true, conviction: Conviction::Locked1x }, // 10 DOTS balance: 10_000_000_000_000, }, }; for whale in whales { node.submit_extrinsic(call.clone(), Some(whale)).await?; } // wait for fast track period. node.seal_blocks(FastTrackVotingPeriod::get() as usize).await; // assert that the proposal is passed by looking at events let events = node .events() .into_iter() .filter(|event| match event.event { Event::Democracy(democracy::Event::Passed { ref_index: _index }) if _index == ref_index => true, Event::Democracy(democracy::Event::PreimageUsed { proposal_hash: _hash, .. }) if _hash == proposal_hash => true, Event::Democracy(democracy::Event::Executed { ref_index: _index, result: Ok(()) }) if _index == ref_index => true, _ => false, }) .collect::>(); // make sure all events were emitted assert_eq!( events.len(), 3, "democracy::Event::{{Passed, PreimageUsed, Executed}} not found in events: {:#?}", node.events(), ); Ok(()) } /// Runs the test-runner as a binary. pub fn run(callback: F) -> Result<(), Box> where F: FnOnce(Node) -> Fut, Fut: Future>>, { use sc_cli::{CliConfiguration, SubstrateCli}; use structopt::StructOpt; let tokio_runtime = build_runtime()?; // parse CLI args let cmd = ::from_args(); // set up logging let filters = cmd.run.base.log_filters()?; let logger = sc_tracing::logging::LoggerBuilder::new(filters); logger.init()?; // set up the test-runner let config = cmd.create_configuration(&cmd.run.base, tokio_runtime.handle().clone())?; sc_cli::print_node_infos::(&config); let (rpc, task_manager, client, pool, command_sink, backend) = client_parts::(ConfigOrChainSpec::Config(config))?; let node = Node::::new(rpc, task_manager, client, pool, command_sink, backend); // hand off node. tokio_runtime.block_on(callback(node))?; Ok(()) } #[cfg(test)] mod tests { use super::*; use polkadot_service::chain_spec::polkadot_development_config; use sp_keyring::sr25519::Keyring::Alice; use sp_runtime::{traits::IdentifyAccount, MultiSigner}; #[test] fn test_runner() { let runtime = build_runtime().unwrap(); let (rpc, task_manager, client, pool, command_sink, backend) = client_parts::(ConfigOrChainSpec::ChainSpec( Box::new(polkadot_development_config().unwrap()), runtime.handle().clone(), )) .unwrap(); let node = Node::::new(rpc, task_manager, client, pool, command_sink, backend); runtime.block_on(async { // seals blocks node.seal_blocks(1).await; // submit extrinsics let alice = MultiSigner::from(Alice.public()).into_account(); node.submit_extrinsic( system::Call::remark { remark: (b"hello world").to_vec() }, Some(alice), ) .await .unwrap(); // look ma, I can read state. let _events = node.with_state(|| system::Pallet::::events()); // get access to the underlying client. let _client = node.client(); }); } }