diff --git a/Cargo.lock b/Cargo.lock index 22dc2428f5..2056831568 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4117,6 +4117,7 @@ dependencies = [ "log", "moonbeam-rpc-primitives-txpool", "pallet-aura", + "pallet-author-filter", "pallet-balances", "pallet-democracy", "pallet-ethereum", @@ -4520,6 +4521,20 @@ dependencies = [ "sp-timestamp", ] +[[package]] +name = "pallet-author-filter" +version = "0.1.0" +dependencies = [ + "author-inherent", + "cumulus-parachain-upgrade", + "frame-support", + "frame-system", + "parity-scale-codec", + "sp-core", + "sp-runtime", + "stake", +] + [[package]] name = "pallet-authority-discovery" version = "2.0.1" diff --git a/pallets/author-filter/Cargo.toml b/pallets/author-filter/Cargo.toml new file mode 100644 index 0000000000..3afe065657 --- /dev/null +++ b/pallets/author-filter/Cargo.toml @@ -0,0 +1,29 @@ +[package] +authors = ["PureStake"] +edition = "2018" +name = "pallet-author-filter" +version = "0.1.0" + +[dependencies] +parity-scale-codec = { version = "1.3.0", default-features = false, features = ["derive"] } + +frame-support = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +frame-system = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +sp-core = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +sp-runtime = { git = "https://github.com/paritytech/substrate", default-features = false, branch = "master" } +author-inherent = { path = "../author-inherent", default-features = false } +stake = { path = "../stake", default-features = false } +cumulus-parachain-upgrade = { git = "https://github.com/paritytech/cumulus", default-features = false, branch = "master" } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "frame-support/std", + "frame-system/std", + "author-inherent/std", + "stake/std", + "sp-core/std", + "sp-runtime/std", + "cumulus-parachain-upgrade/std", +] diff --git a/pallets/author-filter/src/lib.rs b/pallets/author-filter/src/lib.rs new file mode 100644 index 0000000000..8ca0830dc5 --- /dev/null +++ b/pallets/author-filter/src/lib.rs @@ -0,0 +1,151 @@ +// Copyright 2019-2020 PureStake Inc. +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +//! Small pallet responsible determining which accounts are eligible to author at the current +//! block height. +//! +//! Currently this pallet is tightly coupled to our stake pallet, but this design +//! should be generalized in the future. +//! +//! Using a randomness beacon supplied by the `Randomness` trait, this pallet takes the set of +//! currently staked accounts from pallet stake, and filters them down to a pseudorandom subset. +//! The current technique gives no preference to any particular author. In the future, we could +//! disfavor authors who are authoring a disproportionate amount of the time in an attempt to +//! "even the playing field". + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::pallet; + +pub use pallet::*; + +#[pallet] +pub mod pallet { + + use frame_support::pallet_prelude::*; + use frame_support::traits::Randomness; + use frame_support::traits::Vec; + use frame_system::pallet_prelude::*; + use sp_core::H256; + use sp_runtime::Percent; + + /// The Author Filter pallet + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// Configuration trait of this pallet. + #[pallet::config] + pub trait Config: + frame_system::Config + stake::Config + cumulus_parachain_upgrade::Config + { + /// The overarching event type + type Event: From> + IsType<::Event>; + /// Deterministic on-chain pseudo-randomness used to do the filtering + type RandomnessSource: Randomness; + } + + // This code will be called by the author-inherent pallet to check whether the reported author + // of this block is eligible at this height. We calculate that result on demand and do not + // record it instorage (although we do emit a debugging event for now). + // This implementation relies on the relay parent's block number from the validation data + // inherent. Therefore the validation data inherent **must** be included before this check is + // performed. Concretely the validation data inherent must be included before the author + // inherent. + impl author_inherent::CanAuthor for Pallet { + fn can_author(account: &T::AccountId) -> bool { + let mut staked: Vec = stake::Module::::validators(); + + let num_eligible = EligibleRatio::::get().mul_ceil(staked.len()); + let mut eligible = Vec::with_capacity(num_eligible); + + // Grab the relay parent height as a temporary source of relay-based entropy + let validation_data = cumulus_parachain_upgrade::Module::::validation_data() + .expect("validation data was set in parachain system inherent"); + let relay_height = validation_data.persisted.block_number; + + for i in 0..num_eligible { + // A context identifier for grabbing the randomness. Consists of three parts + // - The constant string *b"filter" - to identify this pallet + // - The index `i` when we're selecting the ith eligible author + // - The relay parent block number so that the eligible authors at the next height + // change. Avoids liveness attacks from colluding minorities of active authors. + // Third one will not be necessary once we dleverage the relay chain's randomness. + let subject: [u8; 8] = [ + b'f', + b'i', + b'l', + b't', + b'e', + b'r', + i as u8, + relay_height as u8, + ]; + let index = T::RandomnessSource::random(&subject).to_low_u64_be() as usize; + + // Move the selected author from the original vector into the eligible vector + // TODO we could short-circuit this check by returning early when the claimed + // author is selected. For now I'll leave it like this because: + // 1. it is easier to understand what our core filtering logic is + // 2. we currently show the entire filtered set in the debug event + eligible.push(staked.remove(index % staked.len())); + } + + // Emit an event for debugging purposes + // let our_height = frame_system::Module::::block_number(); + // >::deposit_event(Event::Filtered(our_height, relay_height, eligible.clone())); + + eligible.contains(account) + } + } + + // No hooks + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::call] + impl Pallet { + /// Update the eligible ratio. Intended to be called by governance. + #[pallet::weight(0)] + pub fn set_eligible(origin: OriginFor, new: Percent) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + EligibleRatio::::put(&new); + >::deposit_event(Event::EligibleUpdated(new)); + + Ok(Default::default()) + } + } + + /// The percentage of active staked authors that will be eligible at each height. + #[pallet::storage] + pub type EligibleRatio = StorageValue<_, Percent, ValueQuery, Half>; + + // Default value for the `EligibleRatio` is one half. + #[pallet::type_value] + pub fn Half() -> Percent { + Percent::from_percent(50) + } + + #[pallet::event] + #[pallet::generate_deposit(fn deposit_event)] + pub enum Event { + /// The amount of eligible authors for the filter to select has been changed. + EligibleUpdated(Percent), + /// The staked authors have been filtered to these eligible authors in this block. + /// This is a debugging and development event and should be removed eventually. + /// Fields are: para block height, relay block height, eligible authors + Filtered(T::BlockNumber, u32, Vec), + } +} diff --git a/pallets/author-inherent/src/lib.rs b/pallets/author-inherent/src/lib.rs index 17d3afc078..9c77584bf8 100644 --- a/pallets/author-inherent/src/lib.rs +++ b/pallets/author-inherent/src/lib.rs @@ -41,7 +41,8 @@ pub trait EventHandler { pub trait CanAuthor { fn can_author(account: &AccountId) -> bool; } -/// Default permissions is none, see `stake` pallet for different impl used in runtime +/// Default implementation where anyone can author, see `stake` and `author-filter` pallets for +/// additional implementations. impl CanAuthor for () { fn can_author(_: &T) -> bool { true @@ -52,7 +53,9 @@ pub trait Config: System { /// Other pallets that want to be informed about block authorship type EventHandler: EventHandler; - /// Checks if account can be set as block author + /// Checks if account can be set as block author. + /// If the pallet that implements this trait depends on an inherent, that inherent **must** + /// be included before this one. type CanAuthor: CanAuthor; } diff --git a/pallets/stake/src/lib.rs b/pallets/stake/src/lib.rs index f4220b9322..c632efb46c 100644 --- a/pallets/stake/src/lib.rs +++ b/pallets/stake/src/lib.rs @@ -522,7 +522,7 @@ decl_storage! { /// Current candidates with associated state Candidates: map hasher(blake2_128_concat) T::AccountId => Option>; /// Current validator set - Validators: Vec; + Validators get(fn validators): Vec; /// Total Locked Total: BalanceOf; /// Pool of candidates, ordered by account id diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index a80106a075..050d3e8efe 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -17,6 +17,7 @@ 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 } +pallet-author-filter = { path = "../pallets/author-filter", default-features = false } # Substrate dependencies pallet-aura = { git = "https://github.com/paritytech/substrate.git", default-features = false, branch = "master", optional = true } @@ -113,6 +114,7 @@ std = [ "cumulus-primitives/std", "account/std", "stake/std", + "pallet-author-filter/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 3c5f573bfc..f6826b75ca 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -110,7 +110,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("moonbase-alphanet"), impl_name: create_runtime_str!("moonbase-alphanet"), authoring_version: 3, - spec_version: 18, + spec_version: 19, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 2, @@ -399,7 +399,12 @@ impl stake::Config for Runtime { } impl author_inherent::Config for Runtime { type EventHandler = Stake; - type CanAuthor = Stake; + type CanAuthor = AuthorFilter; +} + +impl pallet_author_filter::Config for Runtime { + type Event = Event; + type RandomnessSource = RandomnessCollectiveFlip; } construct_runtime! { @@ -420,9 +425,12 @@ construct_runtime! { EVM: pallet_evm::{Module, Config, Call, Storage, Event}, Ethereum: pallet_ethereum::{Module, Call, Storage, Event, Config, ValidateUnsigned}, Stake: stake::{Module, Call, Storage, Event, Config}, - AuthorInherent: author_inherent::{Module, Call, Storage, Inherent}, Scheduler: pallet_scheduler::{Module, Storage, Config, Event, Call}, Democracy: pallet_democracy::{Module, Storage, Config, Event, Call}, + // The order matters here. Inherents will be included in the order specified here. + // Concretely wee need the author inherent to come after the parachain_upgrade inherent. + AuthorInherent: author_inherent::{Module, Call, Storage, Inherent}, + AuthorFilter: pallet_author_filter::{Module, Storage, Event,} } }