From 22c4742d4823cbc2b426cd0afb6e020a2cc48896 Mon Sep 17 00:00:00 2001 From: Amar Singh Date: Fri, 15 Jan 2021 08:47:05 -0500 Subject: [PATCH] test staking (#173) * WIP draft compiles * Add authorship pallet. * bump spec version * cargo fmt * remove commented code * add license * wrap commetn * Add some notes * another idea * rename pallet * remove pallet authorship. That was a false lead. * Make it compile * Event and event handler * Ditch FindAuthor implementation. * Inherent is required * Add the digest. Is it really that easy? * Wire into standalone node. Lots of duplicate code. * cargo fmt * cleanup and fix * Add to both runtimes * init * add check-inherent impl and fix runtime config * add Call to stake config and fix standalone Cargo lock * add stakers in genesis and remove repeated timestamp inherent in parachain node * wired parachain cli with account id param * move to account_id to RunCmd like in utxo workshop * parse H160 from string * clean * wire standalone node * make account_id optional * fix parse h160 impl and return error early if cli run account id is not set * fix inherent provider registration in standalone, add tests, now an issue generating the chain spec * split match statement in nodes so account is only required in some branches * push attempt * pass tests * pass * make author permissions more generic, unit test payout distribution, bump versions * init stake polkadot js integration tests * update runtime parameters * update node genesis with new validator min stake requirements * and integration tests * test rewards sent to sole validator for block authoring * add code docs * green * fmt * Update tests/tests/test-balance.ts Co-authored-by: Joshy Orndorff * Update pallets/author-inherent/src/lib.rs Co-authored-by: Joshy Orndorff * Update pallets/author-inherent/src/lib.rs Co-authored-by: Joshy Orndorff * Update pallets/stake/src/lib.rs Co-authored-by: Joshy Orndorff * Update pallets/stake/src/lib.rs Co-authored-by: Joshy Orndorff * fmt and add back parachain import * address some comments * comment use of account as H160 default in when not needed for call * only register author inherent when running node and otherwise pass in None * rm outdated comment Co-authored-by: Joshy Orndorff Co-authored-by: Joshy Orndorff --- Cargo.lock | 11 +- Cargo.toml | 1 - moonbeam-types-bundle/package-lock.json | 2 +- node/parachain/Cargo.toml | 2 + node/parachain/src/chain_spec.rs | 8 +- node/parachain/src/cli.rs | 12 +- node/parachain/src/command.rs | 18 +-- node/parachain/src/service.rs | 36 ++++- node/standalone/Cargo.lock | 10 +- node/standalone/Cargo.toml | 3 + node/standalone/src/chain_spec.rs | 8 +- node/standalone/src/cli.rs | 13 +- node/standalone/src/command.rs | 16 +- node/standalone/src/service.rs | 45 +++++- .../{author => author-inherent}/Cargo.toml | 15 +- .../{author => author-inherent}/src/lib.rs | 110 +++++++++---- pallets/stake/Cargo.toml | 6 +- pallets/stake/src/lib.rs | 97 +++++++---- pallets/stake/src/mock.rs | 8 +- pallets/stake/src/tests.rs | 150 ++++++++++++++++++ runtime/Cargo.toml | 4 +- runtime/src/lib.rs | 27 ++-- runtime/src/parachain.rs | 6 +- runtime/src/standalone.rs | 6 +- .../templates/simple-specs-template.json | 7 +- tests/tests/test-balance.ts | 8 +- tests/tests/test-stake.ts | 38 +++++ tests/tests/util/fillBlockWithTx.ts | 2 +- tests/tests/util/testWithMoonbeam.ts | 2 + 29 files changed, 523 insertions(+), 148 deletions(-) rename pallets/{author => author-inherent}/Cargo.toml (66%) rename pallets/{author => author-inherent}/src/lib.rs (52%) create mode 100644 tests/tests/test-stake.ts diff --git a/Cargo.lock b/Cargo.lock index 00274114e85c0..81658cbf43485 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,16 +385,14 @@ dependencies = [ ] [[package]] -name = "author" +name = "author-inherent" version = "0.1.0" dependencies = [ "frame-support", "frame-system", "parity-scale-codec", "sp-authorship", - "sp-core", "sp-inherents", - "sp-io", "sp-runtime", "sp-std", ] @@ -4009,6 +4007,7 @@ version = "0.1.0" dependencies = [ "ansi_term 0.12.1", "assert_cmd", + "author-inherent", "cumulus-collator", "cumulus-consensus", "cumulus-network", @@ -4117,7 +4116,7 @@ name = "moonbeam-runtime" version = "0.1.0" dependencies = [ "account", - "author", + "author-inherent", "cumulus-parachain-upgrade", "cumulus-primitives", "cumulus-runtime", @@ -9403,9 +9402,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stake" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "author", + "author-inherent", "frame-support", "frame-system", "pallet-balances", diff --git a/Cargo.toml b/Cargo.toml index ca33d37df6ea0..bdae2ce59050b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ members = [ 'runtime', 'node/parachain', - 'node/rpc',#Temporary # We do NOT include the standalone node in this main workspace because it builds the # runtime with the `standalone` feature, which the parachain does not support. ] diff --git a/moonbeam-types-bundle/package-lock.json b/moonbeam-types-bundle/package-lock.json index 2986a5636ef03..8fd6d12700988 100644 --- a/moonbeam-types-bundle/package-lock.json +++ b/moonbeam-types-bundle/package-lock.json @@ -1,6 +1,6 @@ { "name": "moonbeam-types-bundle", - "version": "1.0.3", + "version": "1.0.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/node/parachain/Cargo.toml b/node/parachain/Cargo.toml index 53f0d904eee87..5f37879d0d259 100644 --- a/node/parachain/Cargo.toml +++ b/node/parachain/Cargo.toml @@ -66,6 +66,8 @@ fc-rpc = { git = "https://github.com/purestake/frontier", branch = "v0.4-hotfixe fp-rpc = { git = "https://github.com/purestake/frontier", branch = "v0.4-hotfixes" } fc-consensus = { git = "https://github.com/purestake/frontier", branch = "v0.4-hotfixes" } +author-inherent = { path = "../../pallets/author-inherent"} + # Cumulus dependencies cumulus-consensus = { git = "https://github.com/paritytech/cumulus", branch = "master" } cumulus-collator = { git = "https://github.com/paritytech/cumulus", branch = "master" } diff --git a/node/parachain/src/chain_spec.rs b/node/parachain/src/chain_spec.rs index 61b4375224f3a..3dc742ce521cd 100644 --- a/node/parachain/src/chain_spec.rs +++ b/node/parachain/src/chain_spec.rs @@ -98,6 +98,12 @@ fn testnet_genesis( accounts: BTreeMap::new(), }), pallet_ethereum: Some(EthereumConfig {}), - stake: Some(StakeConfig { stakers: vec![] }), + stake: Some(StakeConfig { + stakers: endowed_accounts + .iter() + .cloned() + .map(|k| (k, None, 100_000)) + .collect(), + }), } } diff --git a/node/parachain/src/cli.rs b/node/parachain/src/cli.rs index 0cafcd2ed4056..767ddb833f058 100644 --- a/node/parachain/src/cli.rs +++ b/node/parachain/src/cli.rs @@ -14,8 +14,8 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . +use sp_core::H160; use std::path::PathBuf; - use structopt::StructOpt; /// Sub-commands supported by the collator. @@ -95,6 +95,16 @@ pub struct RunCmd { /// Id of the parachain this collator collates for. #[structopt(long)] pub parachain_id: Option, + + /// Public identity for participating in staking and receiving rewards + #[structopt(long, parse(try_from_str = parse_h160))] + pub account_id: Option, +} + +fn parse_h160(input: &str) -> Result { + input + .parse::() + .map_err(|_| "Failed to parse H160".to_string()) } impl std::ops::Deref for RunCmd { diff --git a/node/parachain/src/command.rs b/node/parachain/src/command.rs index f593888f367aa..2437f26e391c6 100644 --- a/node/parachain/src/command.rs +++ b/node/parachain/src/command.rs @@ -138,7 +138,6 @@ fn extract_genesis_wasm(chain_spec: &Box) -> Result Result<()> { let cli = Cli::from_args(); - match &cli.subcommand { Some(Subcommand::BuildSpec(cmd)) => { let runner = cli.create_runner(cmd)?; @@ -152,7 +151,7 @@ pub fn run() -> Result<()> { task_manager, import_queue, .. - } = crate::service::new_partial(&config)?; + } = crate::service::new_partial(&config, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) } @@ -163,7 +162,7 @@ pub fn run() -> Result<()> { client, task_manager, .. - } = crate::service::new_partial(&config)?; + } = crate::service::new_partial(&config, None)?; Ok((cmd.run(client, config.database), task_manager)) }) } @@ -174,7 +173,7 @@ pub fn run() -> Result<()> { client, task_manager, .. - } = crate::service::new_partial(&config)?; + } = crate::service::new_partial(&config, None)?; Ok((cmd.run(client, config.chain_spec), task_manager)) }) } @@ -186,7 +185,7 @@ pub fn run() -> Result<()> { task_manager, import_queue, .. - } = crate::service::new_partial(&config)?; + } = crate::service::new_partial(&config, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) } @@ -202,7 +201,7 @@ pub fn run() -> Result<()> { task_manager, backend, .. - } = crate::service::new_partial(&config)?; + } = crate::service::new_partial(&config, None)?; Ok((cmd.run(client, backend), task_manager)) }) } @@ -249,9 +248,10 @@ pub fn run() -> Result<()> { } None => { let runner = cli.create_runner(&*cli.run)?; - + let account = cli.run.account_id.ok_or(sc_cli::Error::Input( + "Account ID required but not set".to_string(), + ))?; runner.run_node_until_exit(|config| async move { - // TODO let key = sp_core::Pair::generate().0; let extension = chain_spec::Extensions::try_get(&*config.chain_spec); @@ -286,7 +286,7 @@ pub fn run() -> Result<()> { info!("Parachain genesis state: {}", genesis_state); info!("Is collating: {}", if collator { "yes" } else { "no" }); - crate::service::start_node(config, key, polkadot_config, id, collator) + crate::service::start_node(config, key, account, polkadot_config, id, collator) .await .map(|r| r.0) }) diff --git a/node/parachain/src/service.rs b/node/parachain/src/service.rs index 992383de57eaf..9b252819c9246 100644 --- a/node/parachain/src/service.rs +++ b/node/parachain/src/service.rs @@ -20,11 +20,13 @@ use cumulus_service::{ }; use fc_consensus::FrontierBlockImport; use moonbeam_runtime::{opaque::Block, RuntimeApi}; +use parity_scale_codec::Encode; use polkadot_primitives::v0::CollatorPair; use sc_executor::native_executor_instance; pub use sc_executor::NativeExecutor; use sc_service::{Configuration, PartialComponents, Role, TFullBackend, TFullClient, TaskManager}; -use sp_core::Pair; +use sp_core::{Pair, H160}; +use sp_inherents::InherentDataProviders; use sp_runtime::traits::BlakeTwo256; use sp_trie::PrefixedMemoryDB; use std::sync::Arc; @@ -35,6 +37,26 @@ native_executor_instance!( moonbeam_runtime::native_version, ); +/// Build the inherent data providers (timestamp and authorship) for the node. +pub fn build_inherent_data_providers( + author: Option, +) -> Result { + let providers = InherentDataProviders::new(); + + providers + .register_provider(sp_timestamp::InherentDataProvider) + .map_err(Into::into) + .map_err(sp_consensus::error::Error::InherentData)?; + if let Some(account) = author { + providers + .register_provider(author_inherent::InherentDataProvider(account.encode())) + .map_err(Into::into) + .map_err(sp_consensus::error::Error::InherentData)?; + } + + Ok(providers) +} + type FullClient = TFullClient; type FullBackend = TFullBackend; @@ -45,6 +67,7 @@ type FullBackend = TFullBackend; #[allow(clippy::type_complexity)] pub fn new_partial( config: &Configuration, + author: Option, ) -> Result< PartialComponents< FullClient, @@ -56,7 +79,7 @@ pub fn new_partial( >, sc_service::Error, > { - let inherent_data_providers = sp_inherents::InherentDataProviders::new(); + let inherent_data_providers = build_inherent_data_providers(author)?; let (client, backend, keystore_container, task_manager) = sc_service::new_full_parts::(&config)?; @@ -102,6 +125,7 @@ pub fn new_partial( async fn start_node_impl( parachain_config: Configuration, collator_key: CollatorPair, + account_id: H160, polkadot_config: Configuration, id: polkadot_primitives::v0::Id, validator: bool, @@ -128,11 +152,7 @@ where }, )?; - let params = new_partial(¶chain_config)?; - params - .inherent_data_providers - .register_provider(sp_timestamp::InherentDataProvider) - .unwrap(); + let params = new_partial(¶chain_config, Some(account_id))?; let client = params.client.clone(); let backend = params.backend.clone(); @@ -252,6 +272,7 @@ where pub async fn start_node( parachain_config: Configuration, collator_key: CollatorPair, + account_id: H160, polkadot_config: Configuration, id: polkadot_primitives::v0::Id, validator: bool, @@ -259,6 +280,7 @@ pub async fn start_node( start_node_impl( parachain_config, collator_key, + account_id, polkadot_config, id, validator, diff --git a/node/standalone/Cargo.lock b/node/standalone/Cargo.lock index ed1ffdfefea68..f0e0b0a73f652 100644 --- a/node/standalone/Cargo.lock +++ b/node/standalone/Cargo.lock @@ -353,7 +353,7 @@ dependencies = [ ] [[package]] -name = "author" +name = "author-inherent" version = "0.1.0" dependencies = [ "frame-support", @@ -3611,6 +3611,7 @@ dependencies = [ name = "moonbase-standalone" version = "0.1.1" dependencies = [ + "author-inherent", "fc-consensus", "futures 0.3.8", "jsonrpc-core 15.1.0", @@ -3619,6 +3620,7 @@ dependencies = [ "moonbeam-rpc", "moonbeam-runtime", "pallet-transaction-payment-rpc", + "parity-scale-codec", "sc-basic-authorship", "sc-cli", "sc-client-api", @@ -3690,7 +3692,7 @@ name = "moonbeam-runtime" version = "0.1.0" dependencies = [ "account", - "author", + "author-inherent", "cumulus-parachain-upgrade", "cumulus-primitives", "cumulus-runtime", @@ -7517,9 +7519,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "stake" -version = "0.1.0" +version = "0.1.1" dependencies = [ - "author", + "author-inherent", "frame-support", "frame-system", "pallet-balances", diff --git a/node/standalone/Cargo.toml b/node/standalone/Cargo.toml index 5bd1d613034fa..d45a55403c246 100644 --- a/node/standalone/Cargo.toml +++ b/node/standalone/Cargo.toml @@ -27,6 +27,7 @@ structopt = "0.3" jsonrpc-core = "15.0.0" jsonrpc-pubsub = "15.0.0" serde_json = "1.0" +parity-scale-codec = "1.3.0" sp-api = { git = "https://github.com/paritytech/substrate", branch = "master" } sp-blockchain = { git = "https://github.com/paritytech/substrate", branch = "master" } @@ -59,5 +60,7 @@ moonbeam-runtime = {path = "../../runtime", default-features = false, features = moonbeam-rpc = { path = "../rpc" } fc-consensus = { git = "https://github.com/purestake/frontier", branch = "v0.4-hotfixes" } +author-inherent = { path = "../../pallets/author-inherent"} + [build-dependencies] substrate-build-script-utils = { git = "https://github.com/paritytech/substrate", branch = "master" } diff --git a/node/standalone/src/chain_spec.rs b/node/standalone/src/chain_spec.rs index bd366fdfeb549..89a0762104929 100644 --- a/node/standalone/src/chain_spec.rs +++ b/node/standalone/src/chain_spec.rs @@ -157,6 +157,12 @@ fn testnet_genesis( accounts: BTreeMap::new(), }), pallet_ethereum: Some(EthereumConfig {}), - stake: Some(StakeConfig { stakers: vec![] }), + stake: Some(StakeConfig { + stakers: endowed_accounts + .iter() + .cloned() + .map(|k| (k, None, 100_000)) + .collect(), + }), } } diff --git a/node/standalone/src/cli.rs b/node/standalone/src/cli.rs index a2a2ecaaf3f47..9b0c0e9f19843 100644 --- a/node/standalone/src/cli.rs +++ b/node/standalone/src/cli.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . +use sp_core::H160; use structopt::StructOpt; #[allow(missing_docs)] @@ -24,8 +25,18 @@ pub struct RunCmd { pub base: sc_cli::RunCmd, /// Force using Kusama native runtime. - #[structopt(long = "manual-seal")] + #[structopt(long)] pub manual_seal: bool, + + /// Public identity for participating in staking and receiving rewards + #[structopt(long, parse(try_from_str = parse_h160))] + pub account_id: Option, +} + +fn parse_h160(input: &str) -> Result { + input + .parse::() + .map_err(|_| "Failed to parse H160".to_string()) } #[derive(Debug, StructOpt)] diff --git a/node/standalone/src/command.rs b/node/standalone/src/command.rs index 11db052b5d40e..9b2c9f38f4415 100644 --- a/node/standalone/src/command.rs +++ b/node/standalone/src/command.rs @@ -64,7 +64,6 @@ impl SubstrateCli for Cli { /// Parse and run command line arguments pub fn run() -> sc_cli::Result<()> { let cli = Cli::from_args(); - match &cli.subcommand { Some(Subcommand::BuildSpec(cmd)) => { let runner = cli.create_runner(cmd)?; @@ -78,7 +77,7 @@ pub fn run() -> sc_cli::Result<()> { task_manager, import_queue, .. - } = new_partial(&config, cli.run.manual_seal)?; + } = new_partial(&config, cli.run.manual_seal, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) } @@ -89,7 +88,7 @@ pub fn run() -> sc_cli::Result<()> { client, task_manager, .. - } = new_partial(&config, cli.run.manual_seal)?; + } = new_partial(&config, cli.run.manual_seal, None)?; Ok((cmd.run(client, config.database), task_manager)) }) } @@ -100,7 +99,7 @@ pub fn run() -> sc_cli::Result<()> { client, task_manager, .. - } = new_partial(&config, cli.run.manual_seal)?; + } = new_partial(&config, cli.run.manual_seal, None)?; Ok((cmd.run(client, config.chain_spec), task_manager)) }) } @@ -112,7 +111,7 @@ pub fn run() -> sc_cli::Result<()> { task_manager, import_queue, .. - } = new_partial(&config, cli.run.manual_seal)?; + } = new_partial(&config, cli.run.manual_seal, None)?; Ok((cmd.run(client, import_queue), task_manager)) }) } @@ -128,16 +127,19 @@ pub fn run() -> sc_cli::Result<()> { task_manager, backend, .. - } = new_partial(&config, cli.run.manual_seal)?; + } = new_partial(&config, cli.run.manual_seal, None)?; Ok((cmd.run(client, backend), task_manager)) }) } None => { let runner = cli.create_runner(&cli.run.base)?; + let account = cli.run.account_id.ok_or(sc_cli::Error::Input( + "Account ID required but not set".to_string(), + ))?; runner.run_node_until_exit(|config| async move { match config.role { Role::Light => service::new_light(config), - _ => service::new_full(config, cli.run.manual_seal), + _ => service::new_full(config, cli.run.manual_seal, account), } }) } diff --git a/node/standalone/src/service.rs b/node/standalone/src/service.rs index 16d698e07c696..3077a614edcbe 100644 --- a/node/standalone/src/service.rs +++ b/node/standalone/src/service.rs @@ -19,6 +19,7 @@ use crate::mock_timestamp::MockTimestampInherentDataProvider; use fc_consensus::FrontierBlockImport; use moonbeam_runtime::{self, opaque::Block, RuntimeApi}; +use parity_scale_codec::Encode; use sc_client_api::{ExecutorProvider, RemoteBackend}; use sc_consensus_manual_seal::{self as manual_seal}; use sc_executor::native_executor_instance; @@ -26,6 +27,7 @@ pub use sc_executor::NativeExecutor; use sc_finality_grandpa::{GrandpaBlockImport, SharedVoterState}; use sc_service::{error::Error as ServiceError, Configuration, TaskManager}; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; +use sp_core::H160; use sp_inherents::InherentDataProviders; use std::sync::Arc; use std::time::Duration; @@ -37,6 +39,33 @@ native_executor_instance!( moonbeam_runtime::native_version, ); +/// Build the inherent data providers (timestamp and authorship) for the node. +pub fn build_inherent_data_providers( + manual_seal: bool, + author: Option, +) -> Result { + let providers = InherentDataProviders::new(); + if let Some(account) = author { + providers + .register_provider(author_inherent::InherentDataProvider(account.encode())) + .map_err(Into::into) + .map_err(sp_consensus::error::Error::InherentData)?; + } + if manual_seal { + providers + .register_provider(MockTimestampInherentDataProvider) + .map_err(Into::into) + .map_err(sp_consensus::error::Error::InherentData)?; + } else { + providers + .register_provider(sp_timestamp::InherentDataProvider) + .map_err(Into::into) + .map_err(sp_consensus::error::Error::InherentData)?; + } + + Ok(providers) +} + type FullClient = sc_service::TFullClient; type FullBackend = sc_service::TFullBackend; type FullSelectChain = sc_consensus::LongestChain; @@ -61,6 +90,7 @@ pub enum ConsensusResult { pub fn new_partial( config: &Configuration, manual_seal: bool, + author: Option, ) -> Result< sc_service::PartialComponents< FullClient, @@ -72,7 +102,7 @@ pub fn new_partial( >, ServiceError, > { - let inherent_data_providers = sp_inherents::InherentDataProviders::new(); + let inherent_data_providers = build_inherent_data_providers(manual_seal, author)?; let (client, backend, keystore_container, task_manager) = sc_service::new_full_parts::(&config)?; @@ -88,11 +118,6 @@ pub fn new_partial( ); if manual_seal { - inherent_data_providers - .register_provider(MockTimestampInherentDataProvider) - .map_err(Into::into) - .map_err(sp_consensus::error::Error::InherentData)?; - let frontier_block_import = FrontierBlockImport::new(client.clone(), client.clone(), true); let import_queue = sc_consensus_manual_seal::import_queue( @@ -153,7 +178,11 @@ pub fn new_partial( } /// Builds a new service for a full client. -pub fn new_full(config: Configuration, manual_seal: bool) -> Result { +pub fn new_full( + config: Configuration, + manual_seal: bool, + author: H160, +) -> Result { let sc_service::PartialComponents { client, backend, @@ -164,7 +193,7 @@ pub fn new_full(config: Configuration, manual_seal: bool) -> Result { diff --git a/pallets/author/Cargo.toml b/pallets/author-inherent/Cargo.toml similarity index 66% rename from pallets/author/Cargo.toml rename to pallets/author-inherent/Cargo.toml index a5062d8cb27cb..0faf91195b514 100644 --- a/pallets/author/Cargo.toml +++ b/pallets/author-inherent/Cargo.toml @@ -1,27 +1,20 @@ [package] -name = "author" +name = "author-inherent" version = "0.1.0" -description = "Block Author tracking" +description = "Inject the block author via an inherent, and persist it via a Consensus digest" authors = ["PureStake"] edition = "2018" -license = "Apache-2.0" -homepage = "https://substrate.dev" -repository = "https://github.com/paritytech/substrate/" -readme = "README.md" +license = 'GPL-3.0-only' [dependencies] frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } -parity-scale-codec = { version = "1.0.0", default-features = false, features = ["derive"] } +parity-scale-codec = { version = "1.3.4", default-features = false, features = ["derive"] } sp-authorship = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-inherents = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } -[dev-dependencies] -sp-io = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } - [features] default = ["std"] std = [ diff --git a/pallets/author/src/lib.rs b/pallets/author-inherent/src/lib.rs similarity index 52% rename from pallets/author/src/lib.rs rename to pallets/author-inherent/src/lib.rs index 28330b08da961..0a3b9617fc6d8 100644 --- a/pallets/author/src/lib.rs +++ b/pallets/author-inherent/src/lib.rs @@ -14,39 +14,62 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . -//! Block author tracking by inherents +//! Pallet that allows block authors to include their identity in a block via an inherent. +//! Currently the author does not _prove_ their identity, just states it. So it should not be used, +//! for things like equivocation slashing that require authenticated authorship information. + #![cfg_attr(not(feature = "std"), no_std)] -use frame_support::{decl_error, decl_module, decl_storage, ensure, weights::Weight}; +use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure}; use frame_system::{ensure_none, Config as System}; use parity_scale_codec::{Decode, Encode}; #[cfg(feature = "std")] use sp_inherents::ProvideInherentData; use sp_inherents::{InherentData, InherentIdentifier, IsFatalError, ProvideInherent}; -use sp_runtime::RuntimeString; +use sp_runtime::{ConsensusEngineId, DigestItem, RuntimeString}; use sp_std::vec::Vec; +/// The given account ID is the author of the current block. pub trait EventHandler { - /// Note that the given account ID is the author of the current block. fn note_author(author: Author); } - -pub trait IsValidator { - fn is_validator(account: &AccountId) -> bool; +/// Permissions for what block author can be set in this pallet +pub trait CanAuthor { + fn can_author(account: &AccountId) -> bool; +} +/// Default permissions is none, see `stake` pallet for different impl used in runtime +impl CanAuthor for () { + fn can_author(_: &T) -> bool { + true + } } pub trait Config: System { - /// An event handler for authored blocks. + /// Event type used by the runtime. + type Event: From> + Into<::Event>; + + /// Other pallets that want to be informed about block authorship type EventHandler: EventHandler; - /// Checks if account is a validator - type IsAuthority: IsValidator; + + /// Checks if account can be set as block author + type CanAuthor: CanAuthor; +} + +decl_event! { + pub enum Event where + AccountId = ::AccountId, + BlockNumber = ::BlockNumber, + { + /// Author, Block Height + AuthorSet(AccountId, BlockNumber), + } } decl_error! { pub enum Error for Module { /// Author already set in block. AuthorAlreadySet, - NotValidator, + CannotBeAuthor, } } @@ -60,33 +83,43 @@ decl_storage! { decl_module! { pub struct Module for enum Call where origin: T::Origin { type Error = Error; + fn deposit_event() = default; /// Inherent to set the author of a block #[weight = 0] fn set_author(origin, author: T::AccountId) { ensure_none(origin)?; ensure!(>::get().is_none(), Error::::AuthorAlreadySet); - ensure!(T::IsAuthority::is_validator(&author), Error::::NotValidator); - ::Author::put(author); - } + ensure!(T::CanAuthor::can_author(&author), Error::::CannotBeAuthor); + + let current_block = frame_system::Module::::block_number(); + + // Update storage + Author::::put(&author); - fn on_initialize() -> Weight { - // Reset the author to None at the beginning of the block - ::Author::kill(); - // Return zero weight because we are not using weight-based - // transaction fees. - 0 + // Add a digest item so Apps can detect the block author + // For now we use the Consensus digest item. + // Maybe this will change later. + frame_system::Module::::deposit_log(DigestItem::::Consensus( + ENGINE_ID, + author.encode(), + )); + + // Notify any other pallets that are listening (eg rewards) about the author + T::EventHandler::note_author(author.clone()); + + Self::deposit_event(Event::::AuthorSet(author, current_block)); } fn on_finalize() { - // TODO: panic if author is not set (not done bc integration tests must set author) - if let Some(author) = >::get() { - T::EventHandler::note_author(author); - } + assert!(>::take().is_some(), "Author inherent must be in the block"); } } } +// Can I express this as `*b"auth"` like we do for the inherent id? +pub const ENGINE_ID: ConsensusEngineId = [b'a', b'u', b't', b'h']; + pub const INHERENT_IDENTIFIER: InherentIdentifier = *b"author__"; #[derive(Encode)] @@ -116,9 +149,10 @@ impl InherentError { } /// The type of data that the inherent will contain. -/// Just a byte array. It will be decoded to an actual pubkey later +/// Just a byte array. It will be decoded to an actual account id later. pub type InherentType = Vec; +/// The thing that the outer node will use to actually inject the inherent data #[cfg(feature = "std")] pub struct InherentDataProvider(pub InherentType); @@ -146,14 +180,36 @@ impl ProvideInherent for Module { const INHERENT_IDENTIFIER: InherentIdentifier = INHERENT_IDENTIFIER; fn create_inherent(data: &InherentData) -> Option { - // Grab the Vec labelled with "author_" from the map of all inherent data + // Grab the Vec labelled with "author__" from the map of all inherent data let author_raw = data .get_data::(&INHERENT_IDENTIFIER) .expect("Gets and decodes authorship inherent data")?; - // Decode the Vec into an actual author + //TODO we need to make the author _prove_ their identity, not just claim it. + // we should have them sign something here. Best idea so far: parent block hash. + + // Decode the Vec into an account Id let author = T::AccountId::decode(&mut &author_raw[..]).expect("Decodes author raw inherent data"); + Some(Call::set_author(author)) } + + fn check_inherent(_call: &Self::Call, data: &InherentData) -> Result<(), Self::Error> { + let author_raw = data + .get_data::(&INHERENT_IDENTIFIER) + .expect("Gets and decodes authorship inherent data") + .ok_or_else(|| { + InherentError::Other(sp_runtime::RuntimeString::Borrowed( + "Decode authorship inherent data failed", + )) + })?; + let author = + T::AccountId::decode(&mut &author_raw[..]).expect("Decodes author raw inherent data"); + ensure!( + T::CanAuthor::can_author(&author), + InherentError::Other(sp_runtime::RuntimeString::Borrowed("Cannot Be Author")) + ); + Ok(()) + } } diff --git a/pallets/stake/Cargo.toml b/pallets/stake/Cargo.toml index be759219d0382..4dc61ab212707 100644 --- a/pallets/stake/Cargo.toml +++ b/pallets/stake/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "stake" -version = "0.1.0" +version = "0.1.1" authors = ["PureStake"] edition = "2018" description = "staking pallet for validator selection and rewards" [dependencies] -author = { path = "../author", default-features = false } +author-inherent = { path = "../author-inherent", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } frame-system = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -23,7 +23,7 @@ sp-core = { git = "https://github.com/paritytech/substrate", branch = "master", [features] default = ["std"] std = [ - "author/std", + "author-inherent/std", "frame-support/std", "frame-system/std", "pallet-balances/std", diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index ea296bc59ea3d..de2e8372ca6b7 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -14,7 +14,34 @@ // You should have received a copy of the GNU General Public License // along with Moonbeam. If not, see . -//! Minimal staking module with ordered validator selection +//! # Stake +//! Minimal staking pallet that implements ordered validator selection by total amount at stake +//! +//! ### Rules +//! There is a new round every `BlocksPerRound` blocks. +//! +//! At the start of every round, +//! * `IssuancePerRound` is distributed to validators for `BondDuration` rounds ago +//! in proportion to the points they received in that round (for authoring blocks) +//! * queued validator exits are executed +//! * a new set of validators is chosen from the candidates +//! +//! To join the set of candidates, an account must call `join_candidates` with +//! stake >= `MinValidatorStk` and fee <= `MaxFee`. The fee is taken off the top +//! of any rewards for the validator before the remaining rewards are distributed +//! in proportion to stake to all nominators (including the validator, who always +//! self-nominates). +//! +//! To leave the set of candidates, the validator calls `leave_candidates`. If the call succeeds, +//! the validator is removed from the pool of candidates so they cannot be selected for future +//! validator sets, but they are not unstaked until `BondDuration` rounds later. The exit request is +//! stored in the `ExitQueue` and processed `BondDuration` rounds later to unstake the validator +//! and all of its nominators. +//! +//! To join the set of nominators, an account must not be a validator candidate nor an existing +//! nominator. To join the set of nominators, an account must call `join_nominators` with +//! stake >= `MinNominatorStk`. + #![recursion_limit = "256"] #![cfg_attr(not(feature = "std"), no_std)] @@ -187,7 +214,7 @@ pub trait Config: System { /// Maximum nominators per validator type MaxNominatorsPerValidator: Get; /// Balance issued as rewards per round (constant issuance) - type Issuance: Get>; + type IssuancePerRound: Get>; /// Maximum fee for any validator type MaxFee: Get; /// Minimum stake for any registered on-chain account to become a validator @@ -256,7 +283,7 @@ decl_storage! { /// Total Locked Total: BalanceOf; /// Pool of candidates, ordered by account id - CandidateQueue: OrderedSet>>; + CandidatePool: OrderedSet>>; /// Queue of validator exit requests, ordered by account id ExitQueue: OrderedSet>; /// Exposure at stake per round, per validator @@ -289,7 +316,7 @@ decl_storage! { } else { >::join_candidates( T::Origin::from(Some(actor.clone()).into()), - Perbill::from_percent(2), + Perbill::from_percent(2),// default fee for validators set at genesis is 2% balance, ) }; @@ -320,7 +347,7 @@ decl_module! { ensure!(!Self::is_nominator(&acc),Error::::NominatorExists); ensure!(fee <= T::MaxFee::get(),Error::::FeeOverMax); ensure!(bond >= T::MinValidatorStk::get(),Error::::ValBondBelowMin); - let mut candidates = >::get(); + let mut candidates = >::get(); ensure!( candidates.insert(Bond{owner: acc.clone(), amount: bond}), Error::::CandidateExists @@ -330,7 +357,7 @@ decl_module! { let new_total = >::get() + bond; >::put(new_total); >::insert(&acc,candidate); - >::put(candidates); + >::put(candidates); Self::deposit_event(RawEvent::JoinedValidatorCandidates(acc,bond,new_total)); Ok(()) } @@ -340,9 +367,9 @@ decl_module! { let mut state = >::get(&validator).ok_or(Error::::CandidateDNE)?; ensure!(state.is_active(),Error::::AlreadyOffline); state.go_offline(); - let mut candidates = >::get(); + let mut candidates = >::get(); if candidates.remove(&Bond::from_owner(validator.clone())) { - >::put(candidates); + >::put(candidates); } >::insert(&validator,state); Self::deposit_event(RawEvent::ValidatorWentOffline(::get(),validator)); @@ -355,12 +382,12 @@ decl_module! { ensure!(!state.is_active(),Error::::AlreadyActive); ensure!(!state.is_leaving(),Error::::CannotActivateIfLeaving); state.go_online(); - let mut candidates = >::get(); + let mut candidates = >::get(); ensure!( candidates.insert(Bond{owner:validator.clone(),amount:state.total}), Error::::AlreadyActive ); - >::put(candidates); + >::put(candidates); >::insert(&validator,state); Self::deposit_event(RawEvent::ValidatorBackOnline(::get(),validator)); Ok(()) @@ -378,9 +405,9 @@ decl_module! { Error::::AlreadyLeaving ); state.leave_candidates(when); - let mut candidates = >::get(); + let mut candidates = >::get(); if candidates.remove(&Bond::from_owner(validator.clone())) { - >::put(candidates); + >::put(candidates); } >::put(exits); >::insert(&validator,state); @@ -481,13 +508,13 @@ impl Module { } // ensure candidate is active before calling fn update_active_candidate(candidate: T::AccountId, new_total: BalanceOf) { - let mut candidates = >::get(); + let mut candidates = >::get(); candidates.remove(&Bond::from_owner(candidate.clone())); candidates.insert(Bond { owner: candidate.clone(), amount: new_total, }); - >::put(candidates); + >::put(candidates); } fn pay_stakers(next: RoundIndex) { let duration = T::BondDuration::get(); @@ -497,7 +524,7 @@ impl Module { if total == 0u32 { return; } - let issuance = T::Issuance::get(); + let issuance = T::IssuancePerRound::get(); for (val, pts) in >::drain_prefix(round_to_payout) { let pct_due = Perbill::from_rational_approximation(pts, total); let mut amt_due = pct_due * issuance; @@ -505,16 +532,24 @@ impl Module { continue; } if let Some(state) = >::get(&val) { - let fee_off_top = state.fee * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&val, fee_off_top).ok() { - Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); - } - amt_due -= fee_off_top; - for Bond { owner, amount } in state.nominators.0 { - let percent = Perbill::from_rational_approximation(amount, state.total); - let due = percent * amt_due; - if let Some(imb) = T::Currency::deposit_into_existing(&owner, due).ok() { - Self::deposit_event(RawEvent::Rewarded(owner.clone(), imb.peek())); + if state.nominators.0.len() == 1usize { + // solo validator with no nominators + if let Some(imb) = T::Currency::deposit_into_existing(&val, amt_due).ok() { + Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); + } + } else { + let fee = state.fee * amt_due; + if let Some(imb) = T::Currency::deposit_into_existing(&val, fee).ok() { + Self::deposit_event(RawEvent::Rewarded(val.clone(), imb.peek())); + } + amt_due -= fee; + for Bond { owner, amount } in state.nominators.0 { + let percent = Perbill::from_rational_approximation(amount, state.total); + let due = percent * amt_due; + if let Some(imb) = T::Currency::deposit_into_existing(&owner, due).ok() + { + Self::deposit_event(RawEvent::Rewarded(owner.clone(), imb.peek())); + } } } } @@ -552,7 +587,7 @@ impl Module { /// Best as in most cumulatively supported in terms of stake fn best_candidates_become_validators(next: RoundIndex) -> (u32, BalanceOf) { let (mut all_validators, mut total) = (0u32, BalanceOf::::zero()); - let mut candidates = >::get().0; + let mut candidates = >::get().0; // order candidates by stake (least to greatest so requires `rev()`) candidates.sort_unstable_by(|a, b| a.amount.partial_cmp(&b.amount).unwrap()); let max_validators = T::MaxValidators::get() as usize; @@ -583,9 +618,9 @@ impl Module { /// Add reward points to block authors: /// * 20 points to the block producer for producing a block in the chain -impl author::EventHandler for Module +impl author_inherent::EventHandler for Module where - T: Config + author::Config, + T: Config + author_inherent::Config, { fn note_author(author: T::AccountId) { let now = ::get(); @@ -595,11 +630,11 @@ where } } -impl author::IsValidator for Module +impl author_inherent::CanAuthor for Module where - T: Config + author::Config, + T: Config + author_inherent::Config, { - fn is_validator(account: &T::AccountId) -> bool { + fn can_author(account: &T::AccountId) -> bool { Self::is_validator(account) } } diff --git a/pallets/stake/src/mock.rs b/pallets/stake/src/mock.rs index 1301cd2196284..db377c220f059 100644 --- a/pallets/stake/src/mock.rs +++ b/pallets/stake/src/mock.rs @@ -92,16 +92,12 @@ impl pallet_balances::Config for Test { type AccountStore = frame_system::Module; type WeightInfo = (); } -impl author::Config for Test { - type EventHandler = Module; - type IsAuthority = Module; -} parameter_types! { pub const BlocksPerRound: u32 = 5; pub const BondDuration: u32 = 2; pub const MaxValidators: u32 = 5; pub const MaxNominatorsPerValidator: usize = 10; - pub const Issuance: u128 = 10; + pub const IssuancePerRound: u128 = 10; pub const MaxFee: Perbill = Perbill::from_percent(50); pub const MinValidatorStk: u128 = 10; pub const MinNominatorStk: u128 = 5; @@ -113,7 +109,7 @@ impl Config for Test { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; - type Issuance = Issuance; + type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; type MinNominatorStk = MinNominatorStk; diff --git a/pallets/stake/src/tests.rs b/pallets/stake/src/tests.rs index eb44a058be22b..931e351809a82 100644 --- a/pallets/stake/src/tests.rs +++ b/pallets/stake/src/tests.rs @@ -390,3 +390,153 @@ fn exit_queue_works() { assert_eq!(events, expected); }); } + +#[test] +fn payout_distribution_works() { + genesis2().execute_with(|| { + // same storage changes as EventHandler::note_author impl + fn set_pts(round: u32, acc: u64, pts: u32) { + ::Points::mutate(round, |p| *p += pts); + ::AwardedPts::insert(round, acc, pts); + } + roll_to(4); + roll_to(8); + let events = Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>(); + // should choose top MaxValidators (5), in order + let mut expected = vec![ + RawEvent::ValidatorChosen(2, 1, 100), + RawEvent::ValidatorChosen(2, 2, 90), + RawEvent::ValidatorChosen(2, 3, 80), + RawEvent::ValidatorChosen(2, 4, 70), + RawEvent::ValidatorChosen(2, 5, 60), + RawEvent::NewRound(5, 2, 5, 400), + ]; + assert_eq!(events, expected); + // ~ set block author as 1 for all blocks this round + set_pts(2, 1, 100); + roll_to(16); + let events = Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>(); + // pay total issuance (=10) to 1 + let mut new = vec![ + RawEvent::ValidatorChosen(3, 1, 100), + RawEvent::ValidatorChosen(3, 2, 90), + RawEvent::ValidatorChosen(3, 3, 80), + RawEvent::ValidatorChosen(3, 4, 70), + RawEvent::ValidatorChosen(3, 5, 60), + RawEvent::NewRound(10, 3, 5, 400), + RawEvent::Rewarded(1, 10), + RawEvent::ValidatorChosen(4, 1, 100), + RawEvent::ValidatorChosen(4, 2, 90), + RawEvent::ValidatorChosen(4, 3, 80), + RawEvent::ValidatorChosen(4, 4, 70), + RawEvent::ValidatorChosen(4, 5, 60), + RawEvent::NewRound(15, 4, 5, 400), + ]; + expected.append(&mut new); + assert_eq!(events, expected); + // ~ set block author as 1 for 3 blocks this round + set_pts(4, 1, 60); + // ~ set block author as 2 for 2 blocks this round + set_pts(4, 2, 40); + roll_to(26); + let events = Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>(); + // pay 60% total issuance to 1 and 40% total issuance to 2 + let mut new1 = vec![ + RawEvent::ValidatorChosen(5, 1, 100), + RawEvent::ValidatorChosen(5, 2, 90), + RawEvent::ValidatorChosen(5, 3, 80), + RawEvent::ValidatorChosen(5, 4, 70), + RawEvent::ValidatorChosen(5, 5, 60), + RawEvent::NewRound(20, 5, 5, 400), + RawEvent::Rewarded(1, 6), + RawEvent::Rewarded(2, 4), + RawEvent::ValidatorChosen(6, 1, 100), + RawEvent::ValidatorChosen(6, 2, 90), + RawEvent::ValidatorChosen(6, 3, 80), + RawEvent::ValidatorChosen(6, 4, 70), + RawEvent::ValidatorChosen(6, 5, 60), + RawEvent::NewRound(25, 6, 5, 400), + ]; + expected.append(&mut new1); + assert_eq!(events, expected); + // ~ each validator produces 1 block this round + set_pts(6, 1, 20); + set_pts(6, 2, 20); + set_pts(6, 3, 20); + set_pts(6, 4, 20); + set_pts(6, 5, 20); + roll_to(36); + let events = Sys::events() + .into_iter() + .map(|r| r.event) + .filter_map(|e| { + if let MetaEvent::stake(inner) = e { + Some(inner) + } else { + None + } + }) + .collect::>(); + // pay 20% issuance for all validators + let mut new2 = vec![ + RawEvent::ValidatorChosen(7, 1, 100), + RawEvent::ValidatorChosen(7, 2, 90), + RawEvent::ValidatorChosen(7, 3, 80), + RawEvent::ValidatorChosen(7, 4, 70), + RawEvent::ValidatorChosen(7, 5, 60), + RawEvent::NewRound(30, 7, 5, 400), + RawEvent::Rewarded(5, 2), + RawEvent::Rewarded(3, 2), + RawEvent::Rewarded(1, 2), + RawEvent::Rewarded(4, 2), + RawEvent::Rewarded(2, 2), + RawEvent::ValidatorChosen(8, 1, 100), + RawEvent::ValidatorChosen(8, 2, 90), + RawEvent::ValidatorChosen(8, 3, 80), + RawEvent::ValidatorChosen(8, 4, 70), + RawEvent::ValidatorChosen(8, 5, 60), + RawEvent::NewRound(35, 8, 5, 400), + ]; + expected.append(&mut new2); + assert_eq!(events, expected); + // check that distributing rewards clears awarded pts + assert!(::AwardedPts::get(1, 1).is_zero()); + assert!(::AwardedPts::get(4, 1).is_zero()); + assert!(::AwardedPts::get(4, 2).is_zero()); + assert!(::AwardedPts::get(6, 1).is_zero()); + assert!(::AwardedPts::get(6, 2).is_zero()); + assert!(::AwardedPts::get(6, 3).is_zero()); + assert!(::AwardedPts::get(6, 4).is_zero()); + assert!(::AwardedPts::get(6, 5).is_zero()); + }); +} diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 032b00fb185ba..9ae6db8b52692 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -20,7 +20,6 @@ precompiles = { path = "precompiles/", default-features = false } account = { path = "account/", default-features = false } pallet-ethereum-chain-id = { path = "../pallets/ethereum-chain-id", default-features = false } stake = { path = "../pallets/stake", default-features = false } -author = { path = "../pallets/author", default-features = false } # Substrate dependencies pallet-aura = { git = "https://github.com/paritytech/substrate.git", default-features = false, branch = "master", optional = true } @@ -63,6 +62,7 @@ cumulus-primitives = { git = "https://github.com/paritytech/cumulus", default-f # TODO Why can't this come directly from cumulus parachain-info = { path = "../pallets/parachain-info", default-features = false } +author-inherent = { path = "../pallets/author-inherent", default-features = false } [build-dependencies] substrate-wasm-builder = { version = "3.0.0", git = "https://github.com/paritytech/substrate.git", branch = "master" } @@ -108,6 +108,7 @@ std = [ "frame-system-rpc-runtime-api/std", "pallet-transaction-payment-rpc-runtime-api/std", "pallet-ethereum-chain-id/std", + "author-inherent/std", # TODO These dependencies are only necessary when building without the standalone feature. I don't # see a way to express that. Everything is correct, like this, but we unnecessarily build these @@ -127,7 +128,6 @@ std = [ "pallet-grandpa/std", "account/std", "stake/std", - "author/std", ] # Will be enabled by the `wasm-builder` when building the runtime for WASM. diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 78beace70ea60..efba0f2660a5e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -38,10 +38,10 @@ mod parachain; #[cfg(feature = "standalone")] mod standalone; +#[cfg(not(feature = "standalone"))] +use parachain::*; #[cfg(feature = "standalone")] use standalone::*; -// #[cfg(not(feature = "standalone"))] -// use parachain::*; use fp_rpc::TransactionStatus; use parity_scale_codec::{Decode, Encode}; @@ -309,13 +309,21 @@ impl pallet_ethereum::Config for Runtime { } parameter_types! { - pub const BlocksPerRound: u32 = 5; + /// Moonbeam starts a new round every 2 minutes (20 * block_time) + pub const BlocksPerRound: u32 = 20; + /// Reward payments and validator exit requests are delayed by 4 minutes (2 * 20 * block_time) pub const BondDuration: u32 = 2; - pub const MaxValidators: u32 = 5; + /// Maximum 8 valid block authors at any given time + pub const MaxValidators: u32 = 8; + /// Maximum 10 nominators per validator pub const MaxNominatorsPerValidator: usize = 10; - pub const Issuance: u128 = 100; + /// Issue 49 new tokens as rewards to validators every 2 minutes (round) + pub const IssuancePerRound: u128 = 49; + /// The maximum percent a validator can take off the top of its rewards is 50% pub const MaxFee: Perbill = Perbill::from_percent(50); - pub const MinValidatorStk: u128 = 10; + /// Minimum stake required to be reserved to be a validator is 5 + pub const MinValidatorStk: u128 = 100_000; + /// Minimum stake required to be reserved to be a nominator is 5 pub const MinNominatorStk: u128 = 5; } impl stake::Config for Runtime { @@ -325,14 +333,15 @@ impl stake::Config for Runtime { type BondDuration = BondDuration; type MaxValidators = MaxValidators; type MaxNominatorsPerValidator = MaxNominatorsPerValidator; - type Issuance = Issuance; + type IssuancePerRound = IssuancePerRound; type MaxFee = MaxFee; type MinValidatorStk = MinValidatorStk; type MinNominatorStk = MinNominatorStk; } -impl author::Config for Runtime { +impl author_inherent::Config for Runtime { + type Event = Event; type EventHandler = Stake; - type IsAuthority = Stake; + type CanAuthor = Stake; } #[cfg(feature = "standalone")] diff --git a/runtime/src/parachain.rs b/runtime/src/parachain.rs index 3abc2b5b39203..a3ec1eb804635 100644 --- a/runtime/src/parachain.rs +++ b/runtime/src/parachain.rs @@ -22,7 +22,7 @@ macro_rules! runtime_parachain { spec_name: create_runtime_str!("moonbase-alphanet"), impl_name: create_runtime_str!("moonbase-alphanet"), authoring_version: 3, - spec_version: 8, + spec_version: 9, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 2, @@ -72,8 +72,8 @@ macro_rules! runtime_parachain { EthereumChainId: pallet_ethereum_chain_id::{Module, Storage, Config}, EVM: pallet_evm::{Module, Config, Call, Storage, Event}, Ethereum: pallet_ethereum::{Module, Call, Storage, Event, Config, ValidateUnsigned}, - Stake: stake::{Module, Storage, Event, Config}, - Author: author::{Module, Storage}, + Stake: stake::{Module, Call, Storage, Event, Config}, + AuthorInherent: author_inherent::{Module, Call, Storage, Inherent, Event}, } } }; diff --git a/runtime/src/standalone.rs b/runtime/src/standalone.rs index 11eeb9d9e8210..129af9223673a 100644 --- a/runtime/src/standalone.rs +++ b/runtime/src/standalone.rs @@ -31,7 +31,7 @@ macro_rules! runtime_standalone { spec_name: create_runtime_str!("moonbeam-standalone"), impl_name: create_runtime_str!("moonbeam-standalone"), authoring_version: 3, - spec_version: 8, + spec_version: 9, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 2, @@ -89,8 +89,8 @@ macro_rules! runtime_standalone { EthereumChainId: pallet_ethereum_chain_id::{Module, Storage, Config}, Ethereum: pallet_ethereum::{Module, Call, Storage, Event, Config, ValidateUnsigned}, EVM: pallet_evm::{Module, Config, Call, Storage, Event}, - Stake: stake::{Module, Storage, Event, Config}, - Author: author::{Module, Storage}, + Stake: stake::{Module, Call, Storage, Event, Config}, + AuthorInherent: author_inherent::{Module, Call, Storage, Inherent, Event}, } ); }; diff --git a/tests/moonbeam-test-specs/templates/simple-specs-template.json b/tests/moonbeam-test-specs/templates/simple-specs-template.json index 79529b9437d7a..cd0e217527cae 100644 --- a/tests/moonbeam-test-specs/templates/simple-specs-template.json +++ b/tests/moonbeam-test-specs/templates/simple-specs-template.json @@ -34,7 +34,12 @@ } } }, - "palletEthereum": {} + "palletEthereum": {}, + "stake": { + "stakers": [ + ["0x6be02d1d3665660d22ff9624b7be0551ee1ac91b", null, 100000] + ] + } } } } diff --git a/tests/tests/test-balance.ts b/tests/tests/test-balance.ts index 42ae68b54e493..0d5071ebf5975 100644 --- a/tests/tests/test-balance.ts +++ b/tests/tests/test-balance.ts @@ -6,7 +6,7 @@ import { createAndFinalizeBlock, describeWithMoonbeam, customRequest } from "./u describeWithMoonbeam("Moonbeam RPC (Balance)", `simple-specs.json`, (context) => { const GENESIS_ACCOUNT = "0x6be02d1d3665660d22ff9624b7be0551ee1ac91b"; - const GENESIS_ACCOUNT_BALANCE = "340282366920938463463374607431768211455"; + const GENESIS_ACCOUNT_BALANCE = "340282366920938463463374607431768111455"; const GENESIS_ACCOUNT_PRIVATE_KEY = "0x99B3C12287537E38C90A9219D4CB074A89A16E9CDB20BF85728EBD97C343E342"; const TEST_ACCOUNT = "0x1111111111111111111111111111111111111111"; @@ -26,7 +26,7 @@ describeWithMoonbeam("Moonbeam RPC (Balance)", `simple-specs.json`, (context) => { from: GENESIS_ACCOUNT, to: TEST_ACCOUNT, - value: "0x200", // Must me higher than ExistentialDeposit (500) + value: "0x200", // Must be higher than ExistentialDeposit (0) gasPrice: "0x01", gas: "0x100000", }, @@ -35,7 +35,7 @@ describeWithMoonbeam("Moonbeam RPC (Balance)", `simple-specs.json`, (context) => await customRequest(context.web3, "eth_sendRawTransaction", [tx.rawTransaction]); await createAndFinalizeBlock(context.polkadotApi); expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( - "340282366920938463463374607431768189943" + "340282366920938463463374607431768089943" ); expect(await context.web3.eth.getBalance(TEST_ACCOUNT)).to.equal("512"); }); @@ -47,7 +47,7 @@ describeWithMoonbeam("Moonbeam RPC (Balance)", `simple-specs.json`, (context) => { from: GENESIS_ACCOUNT, to: TEST_ACCOUNT, - value: "0x200", // Must me higher than ExistentialDeposit (500) + value: "0x200", // Must be higher than ExistentialDeposit (currently 0) gasPrice: "0x01", gas: "0x100000", }, diff --git a/tests/tests/test-stake.ts b/tests/tests/test-stake.ts new file mode 100644 index 0000000000000..3fc28ab91c34a --- /dev/null +++ b/tests/tests/test-stake.ts @@ -0,0 +1,38 @@ +import { expect } from "chai"; +import { step } from "mocha-steps"; + +import { describeWithMoonbeam, createAndFinalizeBlock } from "./util"; + +describeWithMoonbeam("Moonbeam RPC (Stake)", `simple-specs.json`, (context) => { + const GENESIS_ACCOUNT = "0x6be02d1d3665660d22ff9624b7be0551ee1ac91b"; + const GENESIS_STAKED = "100000"; + const ACCOUNT_BALANCE = "340282366920938463463374607431768111455"; + step("validator bond reserved in genesis", async function () { + const account = await context.polkadotApi.query.system.account(GENESIS_ACCOUNT); + expect(account.data.reserved.toString()).to.equal(GENESIS_STAKED); + }); + + step("validator set in genesis", async function () { + const validators = await context.polkadotApi.query.stake.validators(); + expect((validators[0] as Buffer).toString("hex").toLowerCase()).equal(GENESIS_ACCOUNT); + }); + + step("issuance minted to the sole validator for authoring blocks", async function () { + const expectedBalance = BigInt(ACCOUNT_BALANCE) + BigInt(49); + const expectedBalance2 = expectedBalance + BigInt(49); + + var block = await context.web3.eth.getBlockNumber(); + while (block < 40) { + await createAndFinalizeBlock(context.polkadotApi); + block = await context.web3.eth.getBlockNumber(); + } + expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal(expectedBalance.toString()); + while (block < 60) { + await createAndFinalizeBlock(context.polkadotApi); + block = await context.web3.eth.getBlockNumber(); + } + expect(await context.web3.eth.getBalance(GENESIS_ACCOUNT)).to.equal( + expectedBalance2.toString() + ); + }); +}); diff --git a/tests/tests/util/fillBlockWithTx.ts b/tests/tests/util/fillBlockWithTx.ts index 8822ba3159f99..ccdc207f06ba2 100644 --- a/tests/tests/util/fillBlockWithTx.ts +++ b/tests/tests/util/fillBlockWithTx.ts @@ -88,7 +88,7 @@ export interface ErrorReport { }; } -// This functiom sends a batch of signed transactions to the pool and records both +// This function sends a batch of signed transactions to the pool and records both // how many tx were included in the first block and the total number of tx that were // included in a block // By default, the tx is a simple transfer, but a TransactionConfig can be specified as an option diff --git a/tests/tests/util/testWithMoonbeam.ts b/tests/tests/util/testWithMoonbeam.ts index a0c76038c155e..906fa44180570 100644 --- a/tests/tests/util/testWithMoonbeam.ts +++ b/tests/tests/util/testWithMoonbeam.ts @@ -6,6 +6,7 @@ import { spawn, ChildProcess } from "child_process"; import { BINARY_PATH, DISPLAY_LOG, + GENESIS_ACCOUNT, MOONBEAM_LOG, PORT, RPC_PORT, @@ -46,6 +47,7 @@ export async function startMoonbeamNode( `--no-telemetry`, `--no-prometheus`, `--manual-seal`, + `--account-id=${GENESIS_ACCOUNT.substring(2)}`, // Required by author inherent `--no-grandpa`, `--force-authoring`, `-l${MOONBEAM_LOG}`,