.
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..57c075f8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+![astar-cover](https://user-images.githubusercontent.com/40356749/135799652-175e0d24-1255-4c26-87e8-447b192fd4b2.gif)
+
+
+
+[![Integration Action](https://github.com/PlasmNetwork/Astar/workflows/Integration/badge.svg)](https://github.com/AstarNetwork/astar-frame/actions)
+[![GitHub tag (latest by date)](https://img.shields.io/github/v/tag/PlasmNetwork/Astar)](https://github.com/AstarNetwork/astar-frame/tags)
+[![Substrate version](https://img.shields.io/badge/Substrate-3.0.0-brightgreen?logo=Parity%20Substrate)](https://substrate.dev/)
+[![License](https://img.shields.io/github/license/PlasmNetwork/Astar?color=green)](https://github.com/AstarNetwork/astar-frame/LICENSE)
+
+[![Twitter URL](https://img.shields.io/twitter/follow/AstarNetwork?style=social)](https://twitter.com/AstarNetwork)
+[![Twitter URL](https://img.shields.io/twitter/follow/ShidenNetwork?style=social)](https://twitter.com/ShidenNetwork)
+[![YouTube](https://img.shields.io/youtube/channel/subscribers/UC36JgEF6gqatVSK9xlzzrvQ?style=social)](https://www.youtube.com/channel/UC36JgEF6gqatVSK9xlzzrvQ)
+[![Docker](https://img.shields.io/docker/pulls/staketechnologies/astar-collator?logo=docker)](https://hub.docker.com/r/staketechnologies/astar-collator)
+[![Discord](https://img.shields.io/badge/Discord-gray?logo=discord)](https://discord.gg/Z3nC9U4)
+[![Telegram](https://img.shields.io/badge/Telegram-gray?logo=telegram)](https://t.me/PlasmOfficial)
+[![Medium](https://img.shields.io/badge/Medium-gray?logo=medium)](https://medium.com/astar-network)
+
+
+
+Astar Network is an interoperable blockchain based the Substrate framework and the hub for dApps within the Polkadot Ecosystem.
+With Astar Network and Shiden Network, people can stake their tokens to a Smart Contract for rewarding projects that provide value to the network.
+
+This repository only contains custom frame modules, for runtimes and node code please check [here](https://github.com/AstarNetwork/Astar/).
+
+For contributing to this project, please read our [Contribution Guideline](./CONTRIBUTING.md).
+
+## Versioning Schema
+**TODO**
+
+## Further Reading
+
+* [Official Documentation](https://docs.astar.network/)
+* [Whitepaper](https://github.com/PlasmNetwork/plasmdocs/blob/master/wp/en.pdf)
+* [Whitepaper(JP)](https://github.com/PlasmNetwork/plasmdocs/blob/master/wp/jp.pdf)
+* [Subtrate Developer Hub](https://substrate.dev/docs/en/)
+* [Substrate Glossary](https://substrate.dev/docs/en/knowledgebase/getting-started/glossary)
+* [Substrate Client Library Documentation](https://polkadot.js.org/docs/)
diff --git a/frame/block-reward/Cargo.toml b/frame/block-reward/Cargo.toml
new file mode 100644
index 00000000..27541eeb
--- /dev/null
+++ b/frame/block-reward/Cargo.toml
@@ -0,0 +1,28 @@
+[package]
+name = "pallet-block-reward"
+version = "0.2.0"
+authors = ["Stake Technologies "]
+edition = "2018"
+license = "Apache-2.0"
+homepage = "https://astar.network"
+repository = "https://github.com/PlasmNetwork/Astar"
+description = "FRAME pallet for manage block rewards"
+
+[package.metadata.docs.rs]
+targets = ["x86_64-unknown-linux-gnu"]
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "2.2.0", default-features = false, features = ["derive"] }
+scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+
+[features]
+default = ["std"]
+std = [
+ "codec/std",
+ "scale-info/std",
+ "frame-support/std",
+ "frame-system/std",
+]
+try-runtime = ["frame-support/try-runtime"]
diff --git a/frame/block-reward/src/lib.rs b/frame/block-reward/src/lib.rs
new file mode 100644
index 00000000..b37b775e
--- /dev/null
+++ b/frame/block-reward/src/lib.rs
@@ -0,0 +1,66 @@
+//! # Block Reward Pallet
+//!
+//! - [`Config`]
+//!
+//! ## Overview
+//!
+//! Simple pallet that implements block reward mechanics.
+//!
+//! ## Interface
+//!
+//! This pallet implements the `OnTimestampSet` trait to handle block production.
+//! Note: We assume that it's impossible to set timestamp two times in a block.
+//!
+//! ## Usage
+//!
+//! 1. Pallet should be set as a handler of `OnTimestampSet`.
+//! 2. `OnBlockReward` handler should be defined as an impl of `OnUnbalanced` trait. For example:
+//! ```nocompile
+//! type NegativeImbalance = >::NegativeImbalance;
+//! struct SaveOnTreasury;
+//! impl OnUnbalanced for SaveOnTreasury {
+//! fn on_nonzero_unbalanced(amount: NegativeImbalance) {
+//! Balances::resolve_creating(&Treasury::pallet_id(), amount);
+//! }
+//! }
+//! ```
+//! 3. Set `RewardAmount` to desiced block reward value in native currency.
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+
+#[frame_support::pallet]
+pub mod pallet {
+ use frame_support::pallet_prelude::*;
+ use frame_support::traits::{Currency, OnTimestampSet, OnUnbalanced};
+
+ /// The balance type of this pallet.
+ pub type BalanceOf =
+ <::Currency as Currency<::AccountId>>::Balance;
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The currency trait.
+ type Currency: Currency;
+
+ /// Handle block reward as imbalance.
+ type OnBlockReward: OnUnbalanced<
+ >::NegativeImbalance,
+ >;
+
+ /// The amount of issuance for each block.
+ #[pallet::constant]
+ type RewardAmount: Get>;
+ }
+
+ #[pallet::pallet]
+ pub struct Pallet(PhantomData);
+
+ impl OnTimestampSet for Pallet {
+ fn on_timestamp_set(_moment: Moment) {
+ let inflation = T::Currency::issue(T::RewardAmount::get());
+ T::OnBlockReward::on_unbalanced(inflation);
+ }
+ }
+}
diff --git a/frame/collator-selection/Cargo.toml b/frame/collator-selection/Cargo.toml
new file mode 100644
index 00000000..790e80e0
--- /dev/null
+++ b/frame/collator-selection/Cargo.toml
@@ -0,0 +1,62 @@
+[package]
+authors = ['Anonymous']
+description = 'Simple staking pallet with a fixed stake.'
+edition = "2021"
+homepage = 'https://substrate.io'
+license = 'Apache-2.0'
+name = 'pallet-collator-selection'
+readme = 'README.md'
+repository = 'https://github.com/paritytech/cumulus/'
+version = '3.0.0'
+
+[package.metadata.docs.rs]
+targets = ['x86_64-unknown-linux-gnu']
+
+[dependencies]
+log = { version = "0.4.0", default-features = false }
+codec = { default-features = false, features = ['derive'], package = 'parity-scale-codec', version = '2.3.0' }
+rand = { version = "0.7.2", default-features = false }
+scale-info = { version = "1.0.0", default-features = false, features = ["derive"] }
+serde = { version = "1.0.119", default-features = false }
+
+sp-std = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-runtime = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-staking = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+frame-support = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+frame-system = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+pallet-authorship = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+pallet-session = { default-features = false, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+
+frame-benchmarking = { default-features = false, optional = true, git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+
+[dev-dependencies]
+sp-core = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-io = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-tracing = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-runtime = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+pallet-timestamp = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+sp-consensus-aura = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+pallet-balances = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+pallet-aura = { git = 'https://github.com/paritytech/substrate', branch = "polkadot-v0.9.13" }
+
+[features]
+default = ['std']
+runtime-benchmarks = [
+ 'frame-benchmarking',
+ 'frame-support/runtime-benchmarks',
+ 'frame-system/runtime-benchmarks',
+]
+std = [
+ 'codec/std',
+ 'log/std',
+ 'scale-info/std',
+ 'rand/std',
+ 'sp-runtime/std',
+ 'sp-staking/std',
+ 'sp-std/std',
+ 'frame-support/std',
+ 'frame-system/std',
+ 'frame-benchmarking/std',
+ 'pallet-authorship/std',
+ 'pallet-session/std',
+]
diff --git a/frame/collator-selection/README.md b/frame/collator-selection/README.md
new file mode 100644
index 00000000..9718db58
--- /dev/null
+++ b/frame/collator-selection/README.md
@@ -0,0 +1 @@
+License: Apache-2.0
\ No newline at end of file
diff --git a/frame/collator-selection/src/benchmarking.rs b/frame/collator-selection/src/benchmarking.rs
new file mode 100644
index 00000000..3e1063e3
--- /dev/null
+++ b/frame/collator-selection/src/benchmarking.rs
@@ -0,0 +1,265 @@
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Benchmarking setup for pallet-collator-selection
+
+use super::*;
+
+#[allow(unused)]
+use crate::Pallet as CollatorSelection;
+use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller};
+use frame_support::{
+ assert_ok,
+ codec::Decode,
+ traits::{Currency, EnsureOrigin, Get},
+};
+use frame_system::{EventRecord, RawOrigin};
+use pallet_authorship::EventHandler;
+use pallet_session::{self as session, SessionManager};
+use sp_std::prelude::*;
+
+pub type BalanceOf =
+ <::Currency as Currency<::AccountId>>::Balance;
+
+const SEED: u32 = 0;
+
+// TODO: remove if this is given in substrate commit.
+macro_rules! whitelist {
+ ($acc:ident) => {
+ frame_benchmarking::benchmarking::add_to_whitelist(
+ frame_system::Account::::hashed_key_for(&$acc).into(),
+ );
+ };
+}
+
+fn assert_last_event(generic_event: ::Event) {
+ let events = frame_system::Pallet::::events();
+ let system_event: ::Event = generic_event.into();
+ // compare to the last event record
+ let EventRecord { event, .. } = &events[events.len() - 1];
+ assert_eq!(event, &system_event);
+}
+
+fn create_funded_user(
+ string: &'static str,
+ n: u32,
+ balance_factor: u32,
+) -> T::AccountId {
+ let user = account(string, n, SEED);
+ let balance = T::Currency::minimum_balance() * balance_factor.into();
+ let _ = T::Currency::make_free_balance_be(&user, balance);
+ user
+}
+
+fn keys(c: u32) -> ::Keys {
+ use rand::{RngCore, SeedableRng};
+
+ let keys = {
+ let mut keys = [0u8; 128];
+
+ if c > 0 {
+ let mut rng = rand::rngs::StdRng::seed_from_u64(c as u64);
+ rng.fill_bytes(&mut keys);
+ }
+
+ keys
+ };
+
+ Decode::decode(&mut &keys[..]).unwrap()
+}
+
+fn validator(c: u32) -> (T::AccountId, ::Keys) {
+ (create_funded_user::("candidate", c, 1000), keys::(c))
+}
+
+fn register_validators(count: u32) {
+ let validators = (0..count).map(|c| validator::(c)).collect::>();
+
+ for (who, keys) in validators {
+ >::set_keys(RawOrigin::Signed(who).into(), keys, Vec::new()).unwrap();
+ }
+}
+
+fn register_candidates(count: u32) {
+ let candidates = (0..count)
+ .map(|c| account("candidate", c, SEED))
+ .collect::>();
+ assert!(
+ >::get() > 0u32.into(),
+ "Bond cannot be zero!"
+ );
+
+ for who in candidates {
+ T::Currency::make_free_balance_be(&who, >::get() * 2u32.into());
+ >::register_as_candidate(RawOrigin::Signed(who).into()).unwrap();
+ }
+}
+
+benchmarks! {
+ where_clause { where T: pallet_authorship::Config + session::Config }
+
+ set_invulnerables {
+ let b in 1 .. T::MaxInvulnerables::get();
+ let new_invulnerables = (0..b).map(|c| account("candidate", c, SEED)).collect::>();
+ let origin = T::UpdateOrigin::successful_origin();
+ }: {
+ assert_ok!(
+ >::set_invulnerables(origin, new_invulnerables.clone())
+ );
+ }
+ verify {
+ assert_last_event::(Event::NewInvulnerables(new_invulnerables).into());
+ }
+
+ set_desired_candidates {
+ let max: u32 = 999;
+ let origin = T::UpdateOrigin::successful_origin();
+ }: {
+ assert_ok!(
+ >::set_desired_candidates(origin, max.clone())
+ );
+ }
+ verify {
+ assert_last_event::(Event::NewDesiredCandidates(max).into());
+ }
+
+ set_candidacy_bond {
+ let bond: BalanceOf = T::Currency::minimum_balance() * 10u32.into();
+ let origin = T::UpdateOrigin::successful_origin();
+ }: {
+ assert_ok!(
+ >::set_candidacy_bond(origin, bond.clone())
+ );
+ }
+ verify {
+ assert_last_event::(Event::NewCandidacyBond(bond).into());
+ }
+
+ // worse case is when we have all the max-candidate slots filled except one, and we fill that
+ // one.
+ register_as_candidate {
+ let c in 1 .. T::MaxCandidates::get();
+
+ >::put(T::Currency::minimum_balance());
+ >::put(c + 1);
+
+ register_validators::(c);
+ register_candidates::(c);
+
+ let caller: T::AccountId = whitelisted_caller();
+ let bond: BalanceOf = T::Currency::minimum_balance() * 2u32.into();
+ T::Currency::make_free_balance_be(&caller, bond.clone());
+
+ >::set_keys(
+ RawOrigin::Signed(caller.clone()).into(),
+ keys::(c + 1),
+ Vec::new()
+ ).unwrap();
+
+ }: _(RawOrigin::Signed(caller.clone()))
+ verify {
+ assert_last_event::(Event::CandidateAdded(caller, bond / 2u32.into()).into());
+ }
+
+ // worse case is the last candidate leaving.
+ leave_intent {
+ let c in (T::MinCandidates::get() + 1) .. T::MaxCandidates::get();
+ >::put(T::Currency::minimum_balance());
+ >::put(c);
+
+ register_validators::(c);
+ register_candidates::(c);
+
+ let leaving = >::get().last().unwrap().who.clone();
+ whitelist!(leaving);
+ }: _(RawOrigin::Signed(leaving.clone()))
+ verify {
+ assert_last_event::(Event::CandidateRemoved(leaving).into());
+ }
+
+ // worse case is paying a non-existing candidate account.
+ note_author {
+ >::put(T::Currency::minimum_balance());
+ T::Currency::make_free_balance_be(
+ &>::account_id(),
+ T::Currency::minimum_balance() * 4u32.into(),
+ );
+ let author = account("author", 0, SEED);
+ let new_block: T::BlockNumber = 10u32.into();
+
+ frame_system::Pallet::::set_block_number(new_block);
+ assert!(T::Currency::free_balance(&author) == 0u32.into());
+ }: {
+ as EventHandler<_, _>>::note_author(author.clone())
+ } verify {
+ assert!(T::Currency::free_balance(&author) > 0u32.into());
+ assert_eq!(frame_system::Pallet::::block_number(), new_block);
+ }
+
+ // worst case for new session.
+ new_session {
+ let r in 1 .. T::MaxCandidates::get();
+ let c in 1 .. T::MaxCandidates::get();
+
+ >::put(T::Currency::minimum_balance());
+ >::put(c);
+ frame_system::Pallet::::set_block_number(0u32.into());
+
+ register_validators::(c);
+ register_candidates::(c);
+
+ let new_block: T::BlockNumber = 1800u32.into();
+ let zero_block: T::BlockNumber = 0u32.into();
+ let candidates = >::get();
+
+ let non_removals = c.saturating_sub(r);
+
+ for i in 0..c {
+ >::insert(candidates[i as usize].who.clone(), zero_block);
+ }
+
+ if non_removals > 0 {
+ for i in 0..non_removals {
+ >::insert(candidates[i as usize].who.clone(), new_block);
+ }
+ } else {
+ for i in 0..c {
+ >::insert(candidates[i as usize].who.clone(), new_block);
+ }
+ }
+
+ let pre_length = >::get().len();
+
+ frame_system::Pallet::::set_block_number(new_block);
+
+ assert!(>::get().len() == c as usize);
+ }: {
+ as SessionManager<_>>::new_session(0)
+ } verify {
+ if c > r && non_removals >= T::MinCandidates::get() {
+ assert!(>::get().len() < pre_length);
+ } else if c > r && non_removals < T::MinCandidates::get() {
+ assert!(>::get().len() == T::MinCandidates::get() as usize);
+ } else {
+ assert!(>::get().len() == pre_length);
+ }
+ }
+}
+
+impl_benchmark_test_suite!(
+ CollatorSelection,
+ crate::mock::new_test_ext(),
+ crate::mock::Test,
+);
diff --git a/frame/collator-selection/src/lib.rs b/frame/collator-selection/src/lib.rs
new file mode 100644
index 00000000..4dfbe6ce
--- /dev/null
+++ b/frame/collator-selection/src/lib.rs
@@ -0,0 +1,518 @@
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//! Collator Selection pallet.
+//!
+//! A pallet to manage collators in a parachain.
+//!
+//! ## Overview
+//!
+//! The Collator Selection pallet manages the collators of a parachain. **Collation is _not_ a
+//! secure activity** and this pallet does not implement any game-theoretic mechanisms to meet BFT
+//! safety assumptions of the chosen set.
+//!
+//! ## Terminology
+//!
+//! - Collator: A parachain block producer.
+//! - Bond: An amount of `Balance` _locked_ for candidate registration.
+//! - Invulnerable: An account guaranteed to be in the collator set.
+//!
+//! ## Implementation
+//!
+//! The final [`Collators`] are aggregated from two individual lists:
+//!
+//! 1. [`Invulnerables`]: a set of collators appointed by governance. These accounts will always be
+//! collators.
+//! 2. [`Candidates`]: these are *candidates to the collation task* and may or may not be elected as
+//! a final collator.
+//!
+//! The current implementation resolves congestion of [`Candidates`] in a first-come-first-serve
+//! manner.
+//!
+//! Candidates will not be allowed to get kicked or leave_intent if the total number of candidates
+//! fall below MinCandidates. This is for potential disaster recovery scenarios.
+//!
+//! ### Rewards
+//!
+//! The Collator Selection pallet maintains an on-chain account (the "Pot"). In each block, the
+//! collator who authored it receives:
+//!
+//! - Half the value of the Pot.
+//! - Half the value of the transaction fees within the block. The other half of the transaction
+//! fees are deposited into the Pot.
+//!
+//! To initiate rewards an ED needs to be transferred to the pot address.
+//!
+//! Note: Eventually the Pot distribution may be modified as discussed in
+//! [this issue](https://github.com/paritytech/statemint/issues/21#issuecomment-810481073).
+
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+
+#[cfg(test)]
+mod mock;
+
+#[cfg(test)]
+mod tests;
+
+#[cfg(feature = "runtime-benchmarks")]
+mod benchmarking;
+pub mod weights;
+
+#[frame_support::pallet]
+pub mod pallet {
+ pub use crate::weights::WeightInfo;
+ use core::ops::Div;
+ use frame_support::{
+ dispatch::DispatchResultWithPostInfo,
+ inherent::Vec,
+ pallet_prelude::*,
+ sp_runtime::{
+ traits::{AccountIdConversion, CheckedSub, Saturating, Zero},
+ RuntimeDebug,
+ },
+ traits::{
+ Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, LockIdentifier,
+ LockableCurrency, ValidatorRegistration, WithdrawReasons,
+ },
+ weights::DispatchClass,
+ PalletId,
+ };
+ use frame_system::{pallet_prelude::*, Config as SystemConfig};
+ use pallet_session::SessionManager;
+ use sp_runtime::traits::Convert;
+ use sp_staking::SessionIndex;
+
+ type BalanceOf =
+ <::Currency as Currency<::AccountId>>::Balance;
+
+ const COLLATOR_STAKING_ID: LockIdentifier = *b"colstake";
+
+ /// A convertor from collators id. Since this pallet does not have stash/controller, this is
+ /// just identity.
+ pub struct IdentityCollator;
+ impl sp_runtime::traits::Convert> for IdentityCollator {
+ fn convert(t: T) -> Option {
+ Some(t)
+ }
+ }
+
+ /// Configure the pallet by specifying the parameters and types on which it depends.
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// Overarching event type.
+ type Event: From> + IsType<::Event>;
+
+ /// The currency mechanism.
+ type Currency: LockableCurrency;
+
+ /// Origin that can dictate updating parameters of this pallet.
+ type UpdateOrigin: EnsureOrigin;
+
+ /// Account Identifier from which the internal Pot is generated.
+ type PotId: Get;
+
+ /// Maximum number of candidates that we should have. This is used for benchmarking and is not
+ /// enforced.
+ ///
+ /// This does not take into account the invulnerables.
+ type MaxCandidates: Get;
+
+ /// Minimum number of candidates that we should have. This is used for disaster recovery.
+ ///
+ /// This does not take into account the invulnerables.
+ type MinCandidates: Get;
+
+ /// Maximum number of invulnerables.
+ ///
+ /// Used only for benchmarking.
+ type MaxInvulnerables: Get;
+
+ // Will be kicked if block is not produced in threshold.
+ type KickThreshold: Get;
+
+ /// A stable ID for a validator.
+ type ValidatorId: Member + Parameter;
+
+ /// A conversion from account ID to validator ID.
+ ///
+ /// Its cost must be at most one storage read.
+ type ValidatorIdOf: Convert>;
+
+ /// Validate a user is registered
+ type ValidatorRegistration: ValidatorRegistration;
+
+ /// The weight information of this pallet.
+ type WeightInfo: WeightInfo;
+ }
+
+ /// Basic information about a collation candidate.
+ #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
+ pub struct CandidateInfo {
+ /// Account identifier.
+ pub who: AccountId,
+ /// Reserved deposit.
+ pub deposit: Balance,
+ }
+
+ #[pallet::pallet]
+ #[pallet::generate_store(pub(super) trait Store)]
+ pub struct Pallet(_);
+
+ /// The invulnerable, fixed collators.
+ #[pallet::storage]
+ #[pallet::getter(fn invulnerables)]
+ pub type Invulnerables = StorageValue<_, Vec, ValueQuery>;
+
+ /// The (community, limited) collation candidates.
+ #[pallet::storage]
+ #[pallet::getter(fn candidates)]
+ pub type Candidates =
+ StorageValue<_, Vec>>, ValueQuery>;
+
+ /// Last block authored by collator.
+ #[pallet::storage]
+ #[pallet::getter(fn last_authored_block)]
+ pub type LastAuthoredBlock =
+ StorageMap<_, Twox64Concat, T::AccountId, T::BlockNumber, ValueQuery>;
+
+ /// Desired number of candidates.
+ ///
+ /// This should ideally always be less than [`Config::MaxCandidates`] for weights to be correct.
+ #[pallet::storage]
+ #[pallet::getter(fn desired_candidates)]
+ pub type DesiredCandidates = StorageValue<_, u32, ValueQuery>;
+
+ /// Fixed deposit bond for each candidate.
+ #[pallet::storage]
+ #[pallet::getter(fn candidacy_bond)]
+ pub type CandidacyBond = StorageValue<_, BalanceOf, ValueQuery>;
+
+ #[pallet::genesis_config]
+ pub struct GenesisConfig {
+ pub invulnerables: Vec,
+ pub candidacy_bond: BalanceOf,
+ pub desired_candidates: u32,
+ }
+
+ #[cfg(feature = "std")]
+ impl Default for GenesisConfig {
+ fn default() -> Self {
+ Self {
+ invulnerables: Default::default(),
+ candidacy_bond: Default::default(),
+ desired_candidates: Default::default(),
+ }
+ }
+ }
+
+ #[pallet::genesis_build]
+ impl GenesisBuild for GenesisConfig {
+ fn build(&self) {
+ let duplicate_invulnerables = self
+ .invulnerables
+ .iter()
+ .collect::>();
+ assert!(
+ duplicate_invulnerables.len() == self.invulnerables.len(),
+ "duplicate invulnerables in genesis."
+ );
+
+ assert!(
+ T::MaxInvulnerables::get() >= (self.invulnerables.len() as u32),
+ "genesis invulnerables are more than T::MaxInvulnerables",
+ );
+ assert!(
+ T::MaxCandidates::get() >= self.desired_candidates,
+ "genesis desired_candidates are more than T::MaxCandidates",
+ );
+
+ >::put(&self.desired_candidates);
+ >::put(&self.candidacy_bond);
+ >::put(&self.invulnerables);
+ }
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event {
+ NewInvulnerables(Vec),
+ NewDesiredCandidates(u32),
+ NewCandidacyBond(BalanceOf),
+ CandidateAdded(T::AccountId, BalanceOf),
+ CandidateRemoved(T::AccountId),
+ }
+
+ // Errors inform users that something went wrong.
+ #[pallet::error]
+ pub enum Error {
+ /// Too many candidates
+ TooManyCandidates,
+ /// Too few candidates
+ TooFewCandidates,
+ /// Unknown error
+ Unknown,
+ /// Permission issue
+ Permission,
+ /// User is already a candidate
+ AlreadyCandidate,
+ /// User is not a candidate
+ NotCandidate,
+ /// User is already an Invulnerable
+ AlreadyInvulnerable,
+ /// Account has no associated validator ID
+ NoAssociatedValidatorId,
+ /// Validator ID is not yet registered
+ ValidatorNotRegistered,
+ /// Free balance is too low for onboarding
+ TooLowFreeBalance,
+ }
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {}
+
+ #[pallet::call]
+ impl Pallet {
+ #[pallet::weight(T::WeightInfo::set_invulnerables(new.len() as u32))]
+ pub fn set_invulnerables(
+ origin: OriginFor,
+ new: Vec,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ // we trust origin calls, this is just a for more accurate benchmarking
+ if (new.len() as u32) > T::MaxInvulnerables::get() {
+ log::warn!(
+ "invulnerables > T::MaxInvulnerables; you might need to run benchmarks again"
+ );
+ }
+ >::put(&new);
+ Self::deposit_event(Event::NewInvulnerables(new));
+ Ok(().into())
+ }
+
+ #[pallet::weight(T::WeightInfo::set_desired_candidates())]
+ pub fn set_desired_candidates(
+ origin: OriginFor,
+ max: u32,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ // we trust origin calls, this is just a for more accurate benchmarking
+ if max > T::MaxCandidates::get() {
+ log::warn!("max > T::MaxCandidates; you might need to run benchmarks again");
+ }
+ >::put(&max);
+ Self::deposit_event(Event::NewDesiredCandidates(max));
+ Ok(().into())
+ }
+
+ #[pallet::weight(T::WeightInfo::set_candidacy_bond())]
+ pub fn set_candidacy_bond(
+ origin: OriginFor,
+ bond: BalanceOf,
+ ) -> DispatchResultWithPostInfo {
+ T::UpdateOrigin::ensure_origin(origin)?;
+ >::put(&bond);
+ Self::deposit_event(Event::NewCandidacyBond(bond));
+ Ok(().into())
+ }
+
+ #[pallet::weight(T::WeightInfo::register_as_candidate(T::MaxCandidates::get()))]
+ pub fn register_as_candidate(origin: OriginFor) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+
+ // ensure we are below limit.
+ let length = >::decode_len().unwrap_or_default();
+ ensure!(
+ (length as u32) < Self::desired_candidates(),
+ Error::::TooManyCandidates
+ );
+ ensure!(
+ !Self::invulnerables().contains(&who),
+ Error::::AlreadyInvulnerable
+ );
+
+ let validator_key = T::ValidatorIdOf::convert(who.clone())
+ .ok_or(Error::::NoAssociatedValidatorId)?;
+ ensure!(
+ T::ValidatorRegistration::is_registered(&validator_key),
+ Error::::ValidatorNotRegistered
+ );
+
+ let deposit = Self::candidacy_bond();
+
+ let free_balance = T::Currency::free_balance(&who);
+ ensure!(free_balance > deposit, Error::::TooLowFreeBalance);
+
+ // First authored block is current block plus kick threshold to handle session delay
+ let incoming = CandidateInfo {
+ who: who.clone(),
+ deposit,
+ };
+
+ let current_count =
+ >::try_mutate(|candidates| -> Result {
+ if candidates.into_iter().any(|candidate| candidate.who == who) {
+ Err(Error::::AlreadyCandidate)?
+ } else {
+ T::Currency::set_lock(
+ COLLATOR_STAKING_ID,
+ &who,
+ deposit,
+ WithdrawReasons::all(),
+ );
+ candidates.push(incoming);
+ >::insert(
+ who.clone(),
+ frame_system::Pallet::::block_number() + T::KickThreshold::get(),
+ );
+ Ok(candidates.len())
+ }
+ })?;
+
+ Self::deposit_event(Event::CandidateAdded(who, deposit));
+ Ok(Some(T::WeightInfo::register_as_candidate(current_count as u32)).into())
+ }
+
+ #[pallet::weight(T::WeightInfo::leave_intent(T::MaxCandidates::get()))]
+ pub fn leave_intent(origin: OriginFor) -> DispatchResultWithPostInfo {
+ let who = ensure_signed(origin)?;
+ ensure!(
+ Self::candidates().len() as u32 > T::MinCandidates::get(),
+ Error::::TooFewCandidates
+ );
+ let current_count = Self::try_remove_candidate(&who)?;
+
+ Ok(Some(T::WeightInfo::leave_intent(current_count as u32)).into())
+ }
+ }
+
+ impl Pallet {
+ /// Get a unique, inaccessible account id from the `PotId`.
+ pub fn account_id() -> T::AccountId {
+ T::PotId::get().into_account()
+ }
+ /// Removes a candidate if they exist and sends them back their deposit
+ fn try_remove_candidate(who: &T::AccountId) -> Result {
+ let current_count =
+ >::try_mutate(|candidates| -> Result {
+ let index = candidates
+ .iter()
+ .position(|candidate| candidate.who == *who)
+ .ok_or(Error::::NotCandidate)?;
+ T::Currency::remove_lock(COLLATOR_STAKING_ID, &who);
+ candidates.remove(index);
+ >::remove(who.clone());
+ Ok(candidates.len())
+ })?;
+ Self::deposit_event(Event::CandidateRemoved(who.clone()));
+ Ok(current_count)
+ }
+
+ /// Assemble the current set of candidates and invulnerables into the next collator set.
+ ///
+ /// This is done on the fly, as frequent as we are told to do so, as the session manager.
+ pub fn assemble_collators(candidates: Vec) -> Vec {
+ let mut collators = Self::invulnerables();
+ collators.extend(candidates.into_iter().collect::>());
+ collators
+ }
+ /// Kicks out and candidates that did not produce a block in the kick threshold.
+ pub fn kick_stale_candidates(
+ candidates: Vec>>,
+ ) -> Vec {
+ let now = frame_system::Pallet::::block_number();
+ let kick_threshold = T::KickThreshold::get();
+ let new_candidates = candidates
+ .into_iter()
+ .filter_map(|c| {
+ let last_block = >::get(c.who.clone());
+ let since_last = now.saturating_sub(last_block);
+ if since_last < kick_threshold
+ || Self::candidates().len() as u32 <= T::MinCandidates::get()
+ {
+ Some(c.who)
+ } else {
+ let outcome = Self::try_remove_candidate(&c.who);
+ if let Err(why) = outcome {
+ log::warn!("Failed to remove candidate {:?}", why);
+ debug_assert!(false, "failed to remove candidate {:?}", why);
+ }
+ None
+ }
+ })
+ .collect::>();
+ new_candidates
+ }
+ }
+
+ /// Keep track of number of authored blocks per authority, uncles are counted as well since
+ /// they're a valid proof of being online.
+ impl
+ pallet_authorship::EventHandler for Pallet
+ {
+ fn note_author(author: T::AccountId) {
+ let pot = Self::account_id();
+ // assumes an ED will be sent to pot.
+ let reward = T::Currency::free_balance(&pot)
+ .checked_sub(&T::Currency::minimum_balance())
+ .unwrap_or_else(Zero::zero)
+ .div(2u32.into());
+ // `reward` is half of pot account minus ED, this should never fail.
+ let _success = T::Currency::transfer(&pot, &author, reward, KeepAlive);
+ debug_assert!(_success.is_ok());
+ >::insert(author, frame_system::Pallet::::block_number());
+
+ frame_system::Pallet::::register_extra_weight_unchecked(
+ T::WeightInfo::note_author(),
+ DispatchClass::Mandatory,
+ );
+ }
+
+ fn note_uncle(_author: T::AccountId, _age: T::BlockNumber) {
+ //TODO can we ignore this?
+ }
+ }
+
+ /// Play the role of the session manager.
+ impl SessionManager for Pallet {
+ fn new_session(index: SessionIndex) -> Option> {
+ log::info!(
+ "assembling new collators for new session {} at #{:?}",
+ index,
+ >::block_number(),
+ );
+
+ let candidates = Self::candidates();
+ let candidates_len_before = candidates.len();
+ let active_candidates = Self::kick_stale_candidates(candidates);
+ let active_candidates_len = active_candidates.len();
+ let result = Self::assemble_collators(active_candidates);
+ let removed = candidates_len_before - active_candidates_len;
+
+ frame_system::Pallet::::register_extra_weight_unchecked(
+ T::WeightInfo::new_session(candidates_len_before as u32, removed as u32),
+ DispatchClass::Mandatory,
+ );
+ Some(result)
+ }
+ fn start_session(_: SessionIndex) {
+ // we don't care.
+ }
+ fn end_session(_: SessionIndex) {
+ // we don't care.
+ }
+ }
+}
diff --git a/frame/collator-selection/src/mock.rs b/frame/collator-selection/src/mock.rs
new file mode 100644
index 00000000..c856817b
--- /dev/null
+++ b/frame/collator-selection/src/mock.rs
@@ -0,0 +1,264 @@
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use super::*;
+use crate as collator_selection;
+use frame_support::{
+ ord_parameter_types, parameter_types,
+ traits::{FindAuthor, GenesisBuild, ValidatorRegistration},
+ PalletId,
+};
+use frame_system as system;
+use frame_system::EnsureSignedBy;
+use sp_core::H256;
+use sp_runtime::{
+ testing::{Header, UintAuthorityId},
+ traits::{BlakeTwo256, IdentityLookup, OpaqueKeys},
+ RuntimeAppPublic,
+};
+
+type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic;
+type Block = frame_system::mocking::MockBlock;
+
+// Configure a mock runtime to test the pallet.
+frame_support::construct_runtime!(
+ pub enum Test where
+ Block = Block,
+ NodeBlock = Block,
+ UncheckedExtrinsic = UncheckedExtrinsic,
+ {
+ System: frame_system::{Pallet, Call, Config, Storage, Event},
+ Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent},
+ Session: pallet_session::{Pallet, Call, Storage, Event, Config},
+ Aura: pallet_aura::{Pallet, Storage, Config},
+ Balances: pallet_balances::{Pallet, Call, Storage, Config, Event},
+ CollatorSelection: collator_selection::{Pallet, Call, Storage, Event},
+ Authorship: pallet_authorship::{Pallet, Call, Storage, Inherent},
+ }
+);
+
+parameter_types! {
+ pub const BlockHashCount: u64 = 250;
+ pub const SS58Prefix: u8 = 42;
+}
+
+impl system::Config for Test {
+ type BaseCallFilter = frame_support::traits::Everything;
+ type BlockWeights = ();
+ type BlockLength = ();
+ type DbWeight = ();
+ type Origin = Origin;
+ type Call = Call;
+ type Index = u64;
+ type BlockNumber = u64;
+ type Hash = H256;
+ type Hashing = BlakeTwo256;
+ type AccountId = u64;
+ type Lookup = IdentityLookup;
+ type Header = Header;
+ type Event = Event;
+ type BlockHashCount = BlockHashCount;
+ type Version = ();
+ type PalletInfo = PalletInfo;
+ type AccountData = pallet_balances::AccountData;
+ type OnNewAccount = ();
+ type OnKilledAccount = ();
+ type SystemWeightInfo = ();
+ type SS58Prefix = SS58Prefix;
+ type OnSetCode = ();
+}
+
+parameter_types! {
+ pub const ExistentialDeposit: u64 = 5;
+ pub const MaxReserves: u32 = 50;
+}
+
+impl pallet_balances::Config for Test {
+ type Balance = u64;
+ type Event = Event;
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ type MaxLocks = ();
+ type MaxReserves = MaxReserves;
+ type ReserveIdentifier = [u8; 8];
+}
+
+pub struct Author4;
+impl FindAuthor for Author4 {
+ fn find_author<'a, I>(_digests: I) -> Option
+ where
+ I: 'a + IntoIterator- ,
+ {
+ Some(4)
+ }
+}
+
+impl pallet_authorship::Config for Test {
+ type FindAuthor = Author4;
+ type UncleGenerations = ();
+ type FilterUncle = ();
+ type EventHandler = CollatorSelection;
+}
+
+parameter_types! {
+ pub const MinimumPeriod: u64 = 1;
+}
+
+impl pallet_timestamp::Config for Test {
+ type Moment = u64;
+ type OnTimestampSet = Aura;
+ type MinimumPeriod = MinimumPeriod;
+ type WeightInfo = ();
+}
+
+impl pallet_aura::Config for Test {
+ type AuthorityId = sp_consensus_aura::sr25519::AuthorityId;
+ type MaxAuthorities = MaxAuthorities;
+ type DisabledValidators = ();
+}
+
+sp_runtime::impl_opaque_keys! {
+ pub struct MockSessionKeys {
+ // a key for aura authoring
+ pub aura: UintAuthorityId,
+ }
+}
+
+impl From for MockSessionKeys {
+ fn from(aura: sp_runtime::testing::UintAuthorityId) -> Self {
+ Self { aura }
+ }
+}
+
+parameter_types! {
+ pub static SessionHandlerCollators: Vec = Vec::new();
+ pub static SessionChangeBlock: u64 = 0;
+}
+
+pub struct TestSessionHandler;
+impl pallet_session::SessionHandler for TestSessionHandler {
+ const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID];
+ fn on_genesis_session(keys: &[(u64, Ks)]) {
+ SessionHandlerCollators::set(keys.into_iter().map(|(a, _)| *a).collect::>())
+ }
+ fn on_new_session(_: bool, keys: &[(u64, Ks)], _: &[(u64, Ks)]) {
+ SessionChangeBlock::set(System::block_number());
+ dbg!(keys.len());
+ SessionHandlerCollators::set(keys.into_iter().map(|(a, _)| *a).collect::>())
+ }
+ fn on_before_session_ending() {}
+ fn on_disabled(_: u32) {}
+}
+
+parameter_types! {
+ pub const Offset: u64 = 0;
+ pub const Period: u64 = 10;
+}
+
+impl pallet_session::Config for Test {
+ type Event = Event;
+ type ValidatorId = ::AccountId;
+ // we don't have stash and controller, thus we don't need the convert as well.
+ type ValidatorIdOf = IdentityCollator;
+ type ShouldEndSession = pallet_session::PeriodicSessions;
+ type NextSessionRotation = pallet_session::PeriodicSessions;
+ type SessionManager = CollatorSelection;
+ type SessionHandler = TestSessionHandler;
+ type Keys = MockSessionKeys;
+ type WeightInfo = ();
+}
+
+ord_parameter_types! {
+ pub const RootAccount: u64 = 777;
+}
+
+parameter_types! {
+ pub const PotId: PalletId = PalletId(*b"PotStake");
+ pub const MaxCandidates: u32 = 20;
+ pub const MaxInvulnerables: u32 = 20;
+ pub const MinCandidates: u32 = 1;
+ pub const MaxAuthorities: u32 = 100_000;
+}
+
+pub struct IsRegistered;
+impl ValidatorRegistration for IsRegistered {
+ fn is_registered(id: &u64) -> bool {
+ if *id == 7u64 {
+ false
+ } else {
+ true
+ }
+ }
+}
+
+impl Config for Test {
+ type Event = Event;
+ type Currency = Balances;
+ type UpdateOrigin = EnsureSignedBy;
+ type PotId = PotId;
+ type MaxCandidates = MaxCandidates;
+ type MinCandidates = MinCandidates;
+ type MaxInvulnerables = MaxInvulnerables;
+ type KickThreshold = Period;
+ type ValidatorId = ::AccountId;
+ type ValidatorIdOf = IdentityCollator;
+ type ValidatorRegistration = IsRegistered;
+ type WeightInfo = ();
+}
+
+pub fn new_test_ext() -> sp_io::TestExternalities {
+ sp_tracing::try_init_simple();
+ let mut t = frame_system::GenesisConfig::default()
+ .build_storage::()
+ .unwrap();
+ let invulnerables = vec![1, 2];
+ let keys = invulnerables
+ .iter()
+ .map(|i| {
+ (
+ *i,
+ *i,
+ MockSessionKeys {
+ aura: UintAuthorityId(*i),
+ },
+ )
+ })
+ .collect::>();
+
+ let balances = pallet_balances::GenesisConfig:: {
+ balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)],
+ };
+ let collator_selection = collator_selection::GenesisConfig:: {
+ desired_candidates: 2,
+ candidacy_bond: 10,
+ invulnerables,
+ };
+ let session = pallet_session::GenesisConfig:: { keys };
+ balances.assimilate_storage(&mut t).unwrap();
+ // collator selection must be initialized before session.
+ collator_selection.assimilate_storage(&mut t).unwrap();
+ session.assimilate_storage(&mut t).unwrap();
+
+ t.into()
+}
+
+pub fn initialize_to_block(n: u64) {
+ for i in System::block_number() + 1..=n {
+ System::set_block_number(i);
+ >::on_initialize(i);
+ }
+}
diff --git a/frame/collator-selection/src/tests.rs b/frame/collator-selection/src/tests.rs
new file mode 100644
index 00000000..bbad7e20
--- /dev/null
+++ b/frame/collator-selection/src/tests.rs
@@ -0,0 +1,409 @@
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+use crate as collator_selection;
+use crate::{mock::*, CandidateInfo, Error};
+use frame_support::{
+ assert_noop, assert_ok,
+ traits::{Currency, GenesisBuild, OnInitialize},
+};
+use sp_runtime::traits::BadOrigin;
+
+#[test]
+fn basic_setup_works() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(CollatorSelection::desired_candidates(), 2);
+ assert_eq!(CollatorSelection::candidacy_bond(), 10);
+
+ assert!(CollatorSelection::candidates().is_empty());
+ assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
+ });
+}
+
+#[test]
+fn it_should_set_invulnerables() {
+ new_test_ext().execute_with(|| {
+ let new_set = vec![1, 2, 3, 4];
+ assert_ok!(CollatorSelection::set_invulnerables(
+ Origin::signed(RootAccount::get()),
+ new_set.clone()
+ ));
+ assert_eq!(CollatorSelection::invulnerables(), new_set);
+
+ // cannot set with non-root.
+ assert_noop!(
+ CollatorSelection::set_invulnerables(Origin::signed(1), new_set.clone()),
+ BadOrigin
+ );
+ });
+}
+
+#[test]
+fn set_desired_candidates_works() {
+ new_test_ext().execute_with(|| {
+ // given
+ assert_eq!(CollatorSelection::desired_candidates(), 2);
+
+ // can set
+ assert_ok!(CollatorSelection::set_desired_candidates(
+ Origin::signed(RootAccount::get()),
+ 7
+ ));
+ assert_eq!(CollatorSelection::desired_candidates(), 7);
+
+ // rejects bad origin
+ assert_noop!(
+ CollatorSelection::set_desired_candidates(Origin::signed(1), 8),
+ BadOrigin
+ );
+ });
+}
+
+#[test]
+fn set_candidacy_bond() {
+ new_test_ext().execute_with(|| {
+ // given
+ assert_eq!(CollatorSelection::candidacy_bond(), 10);
+
+ // can set
+ assert_ok!(CollatorSelection::set_candidacy_bond(
+ Origin::signed(RootAccount::get()),
+ 7
+ ));
+ assert_eq!(CollatorSelection::candidacy_bond(), 7);
+
+ // rejects bad origin.
+ assert_noop!(
+ CollatorSelection::set_candidacy_bond(Origin::signed(1), 8),
+ BadOrigin
+ );
+ });
+}
+
+#[test]
+fn cannot_register_candidate_if_too_many() {
+ new_test_ext().execute_with(|| {
+ // reset desired candidates:
+ >::put(0);
+
+ // can't accept anyone anymore.
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(3)),
+ Error::::TooManyCandidates,
+ );
+
+ // reset desired candidates:
+ >::put(1);
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+
+ // but no more
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(5)),
+ Error::::TooManyCandidates,
+ );
+ })
+}
+
+#[test]
+fn cannot_unregister_candidate_if_too_few() {
+ new_test_ext().execute_with(|| {
+ // reset desired candidates:
+ >::put(1);
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+
+ // can not remove too few
+ assert_noop!(
+ CollatorSelection::leave_intent(Origin::signed(4)),
+ Error::::TooFewCandidates,
+ );
+ })
+}
+
+#[test]
+fn cannot_register_as_candidate_if_invulnerable() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
+
+ // can't 1 because it is invulnerable.
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(1)),
+ Error::::AlreadyInvulnerable,
+ );
+ })
+}
+
+#[test]
+fn cannot_register_as_candidate_if_keys_not_registered() {
+ new_test_ext().execute_with(|| {
+ // can't 7 because keys not registered.
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(7)),
+ Error::::ValidatorNotRegistered
+ );
+ })
+}
+
+#[test]
+fn cannot_register_dupe_candidate() {
+ new_test_ext().execute_with(|| {
+ // can add 3 as candidate
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+ let addition = CandidateInfo {
+ who: 3,
+ deposit: 10,
+ };
+ assert_eq!(CollatorSelection::candidates(), vec![addition]);
+ assert_eq!(CollatorSelection::last_authored_block(3), 10);
+ assert_eq!(Balances::usable_balance(3), 90);
+
+ // but no more
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(3)),
+ Error::::AlreadyCandidate,
+ );
+ })
+}
+
+#[test]
+fn cannot_register_as_candidate_if_poor() {
+ new_test_ext().execute_with(|| {
+ assert_eq!(Balances::usable_balance(&3), 100);
+ assert_eq!(Balances::usable_balance(&33), 0);
+
+ // works
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+
+ // poor
+ assert_noop!(
+ CollatorSelection::register_as_candidate(Origin::signed(33)),
+ Error::::TooLowFreeBalance,
+ );
+ });
+}
+
+#[test]
+fn register_as_candidate_works() {
+ new_test_ext().execute_with(|| {
+ // given
+ assert_eq!(CollatorSelection::desired_candidates(), 2);
+ assert_eq!(CollatorSelection::candidacy_bond(), 10);
+ assert_eq!(CollatorSelection::candidates(), Vec::new());
+ assert_eq!(CollatorSelection::invulnerables(), vec![1, 2]);
+
+ // take two endowed, non-invulnerables accounts.
+ assert_eq!(Balances::usable_balance(&3), 100);
+ assert_eq!(Balances::usable_balance(&4), 100);
+
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+
+ assert_eq!(Balances::usable_balance(&3), 90);
+ assert_eq!(Balances::usable_balance(&4), 90);
+
+ assert_eq!(CollatorSelection::candidates().len(), 2);
+ });
+}
+
+#[test]
+fn leave_intent() {
+ new_test_ext().execute_with(|| {
+ // register a candidate.
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+ assert_eq!(Balances::usable_balance(3), 90);
+
+ // register too so can leave above min candidates
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5)));
+ assert_eq!(Balances::usable_balance(5), 90);
+
+ // cannot leave if not candidate.
+ assert_noop!(
+ CollatorSelection::leave_intent(Origin::signed(4)),
+ Error::::NotCandidate
+ );
+
+ // bond is returned
+ assert_ok!(CollatorSelection::leave_intent(Origin::signed(3)));
+ assert_eq!(Balances::usable_balance(3), 100);
+ assert_eq!(CollatorSelection::last_authored_block(3), 0);
+ });
+}
+
+#[test]
+fn authorship_event_handler() {
+ new_test_ext().execute_with(|| {
+ // put 100 in the pot + 5 for ED
+ Balances::make_free_balance_be(&CollatorSelection::account_id(), 105);
+
+ // 4 is the default author.
+ assert_eq!(Balances::usable_balance(4), 100);
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+ // triggers `note_author`
+ Authorship::on_initialize(1);
+
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
+
+ assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::last_authored_block(4), 0);
+
+ // half of the pot goes to the collator who's the author (4 in tests).
+ assert_eq!(Balances::usable_balance(4), 140);
+ // half + ED stays.
+ assert_eq!(
+ Balances::usable_balance(CollatorSelection::account_id()),
+ 55
+ );
+ });
+}
+
+#[test]
+fn fees_edgecases() {
+ new_test_ext().execute_with(|| {
+ // Nothing panics, no reward when no ED in balance
+ Authorship::on_initialize(1);
+ // put some money into the pot at ED
+ Balances::make_free_balance_be(&CollatorSelection::account_id(), 5);
+ // 4 is the default author.
+ assert_eq!(Balances::usable_balance(4), 100);
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+ // triggers `note_author`
+ Authorship::on_initialize(1);
+
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
+
+ assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::last_authored_block(4), 0);
+ // Nothing received
+ assert_eq!(Balances::usable_balance(4), 90);
+ // all fee stays
+ assert_eq!(Balances::usable_balance(CollatorSelection::account_id()), 5);
+ });
+}
+
+#[test]
+fn session_management_works() {
+ new_test_ext().execute_with(|| {
+ initialize_to_block(1);
+
+ assert_eq!(SessionChangeBlock::get(), 0);
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
+
+ initialize_to_block(4);
+
+ assert_eq!(SessionChangeBlock::get(), 0);
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
+
+ // add a new collator
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+
+ // session won't see this.
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
+ // but we have a new candidate.
+ assert_eq!(CollatorSelection::candidates().len(), 1);
+
+ initialize_to_block(10);
+ assert_eq!(SessionChangeBlock::get(), 10);
+ // pallet-session has 1 session delay; current validators are the same.
+ assert_eq!(Session::validators(), vec![1, 2]);
+ // queued ones are changed, and now we have 3.
+ assert_eq!(Session::queued_keys().len(), 3);
+ // session handlers (aura, et. al.) cannot see this yet.
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2]);
+
+ initialize_to_block(20);
+ assert_eq!(SessionChangeBlock::get(), 20);
+ // changed are now reflected to session handlers.
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3]);
+ });
+}
+
+#[test]
+fn kick_mechanism() {
+ new_test_ext().execute_with(|| {
+ // add a new collator
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(4)));
+ initialize_to_block(10);
+ assert_eq!(CollatorSelection::candidates().len(), 2);
+ initialize_to_block(20);
+ assert_eq!(SessionChangeBlock::get(), 20);
+ // 4 authored this block, gets to stay 3 was kicked
+ assert_eq!(CollatorSelection::candidates().len(), 1);
+ // 3 will be kicked after 1 session delay
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 4]);
+ let collator = CandidateInfo {
+ who: 4,
+ deposit: 10,
+ };
+ assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::last_authored_block(4), 20);
+ initialize_to_block(30);
+ // 3 gets kicked after 1 session delay
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 4]);
+ // kicked collator gets funds back
+ assert_eq!(Balances::usable_balance(3), 100);
+ });
+}
+
+#[test]
+fn should_not_kick_mechanism_too_few() {
+ new_test_ext().execute_with(|| {
+ // add a new collator
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(3)));
+ assert_ok!(CollatorSelection::register_as_candidate(Origin::signed(5)));
+ initialize_to_block(10);
+ assert_eq!(CollatorSelection::candidates().len(), 2);
+ initialize_to_block(20);
+ assert_eq!(SessionChangeBlock::get(), 20);
+ // 4 authored this block, 5 gets to stay too few 3 was kicked
+ assert_eq!(CollatorSelection::candidates().len(), 1);
+ // 3 will be kicked after 1 session delay
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 3, 5]);
+ let collator = CandidateInfo {
+ who: 5,
+ deposit: 10,
+ };
+ assert_eq!(CollatorSelection::candidates(), vec![collator]);
+ assert_eq!(CollatorSelection::last_authored_block(4), 20);
+ initialize_to_block(30);
+ // 3 gets kicked after 1 session delay
+ assert_eq!(SessionHandlerCollators::get(), vec![1, 2, 5]);
+ // kicked collator gets funds back
+ assert_eq!(Balances::usable_balance(3), 100);
+ });
+}
+
+#[test]
+#[should_panic = "duplicate invulnerables in genesis."]
+fn cannot_set_genesis_value_twice() {
+ sp_tracing::try_init_simple();
+ let mut t = frame_system::GenesisConfig::default()
+ .build_storage::()
+ .unwrap();
+ let invulnerables = vec![1, 1];
+
+ let collator_selection = collator_selection::GenesisConfig:: {
+ desired_candidates: 2,
+ candidacy_bond: 10,
+ invulnerables,
+ };
+ // collator selection must be initialized before session.
+ collator_selection.assimilate_storage(&mut t).unwrap();
+}
diff --git a/frame/collator-selection/src/weights.rs b/frame/collator-selection/src/weights.rs
new file mode 100644
index 00000000..e2732776
--- /dev/null
+++ b/frame/collator-selection/src/weights.rs
@@ -0,0 +1,129 @@
+// This file is part of Substrate.
+
+// Copyright (C) 2021 Parity Technologies (UK) Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#![allow(unused_parens)]
+#![allow(unused_imports)]
+
+use frame_support::{
+ traits::Get,
+ weights::{constants::RocksDbWeight, Weight},
+};
+use sp_std::marker::PhantomData;
+
+// The weight info trait for `pallet_collator_selection`.
+pub trait WeightInfo {
+ fn set_invulnerables(_b: u32) -> Weight;
+ fn set_desired_candidates() -> Weight;
+ fn set_candidacy_bond() -> Weight;
+ fn register_as_candidate(_c: u32) -> Weight;
+ fn leave_intent(_c: u32) -> Weight;
+ fn note_author() -> Weight;
+ fn new_session(_c: u32, _r: u32) -> Weight;
+}
+
+/// Weights for pallet_collator_selection using the Substrate node and recommended hardware.
+pub struct SubstrateWeight(PhantomData);
+impl WeightInfo for SubstrateWeight {
+ fn set_invulnerables(b: u32) -> Weight {
+ (18_563_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((68_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(T::DbWeight::get().writes(1 as Weight))
+ }
+ fn set_desired_candidates() -> Weight {
+ (16_363_000 as Weight).saturating_add(T::DbWeight::get().writes(1 as Weight))
+ }
+ fn set_candidacy_bond() -> Weight {
+ (16_840_000 as Weight).saturating_add(T::DbWeight::get().writes(1 as Weight))
+ }
+ fn register_as_candidate(c: u32) -> Weight {
+ (71_196_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((198_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(T::DbWeight::get().reads(4 as Weight))
+ .saturating_add(T::DbWeight::get().writes(2 as Weight))
+ }
+ fn leave_intent(c: u32) -> Weight {
+ (55_336_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((151_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(T::DbWeight::get().reads(1 as Weight))
+ .saturating_add(T::DbWeight::get().writes(2 as Weight))
+ }
+ fn note_author() -> Weight {
+ (71_461_000 as Weight)
+ .saturating_add(T::DbWeight::get().reads(3 as Weight))
+ .saturating_add(T::DbWeight::get().writes(4 as Weight))
+ }
+ fn new_session(r: u32, c: u32) -> Weight {
+ (0 as Weight)
+ // Standard Error: 1_010_000
+ .saturating_add((109_961_000 as Weight).saturating_mul(r as Weight))
+ // Standard Error: 1_010_000
+ .saturating_add((151_952_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(T::DbWeight::get().reads((1 as Weight).saturating_mul(r as Weight)))
+ .saturating_add(T::DbWeight::get().reads((2 as Weight).saturating_mul(c as Weight)))
+ .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(r as Weight)))
+ .saturating_add(T::DbWeight::get().writes((2 as Weight).saturating_mul(c as Weight)))
+ }
+}
+
+// For backwards compatibility and tests
+impl WeightInfo for () {
+ fn set_invulnerables(b: u32) -> Weight {
+ (18_563_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((68_000 as Weight).saturating_mul(b as Weight))
+ .saturating_add(RocksDbWeight::get().writes(1 as Weight))
+ }
+ fn set_desired_candidates() -> Weight {
+ (16_363_000 as Weight).saturating_add(RocksDbWeight::get().writes(1 as Weight))
+ }
+ fn set_candidacy_bond() -> Weight {
+ (16_840_000 as Weight).saturating_add(RocksDbWeight::get().writes(1 as Weight))
+ }
+ fn register_as_candidate(c: u32) -> Weight {
+ (71_196_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((198_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(RocksDbWeight::get().reads(4 as Weight))
+ .saturating_add(RocksDbWeight::get().writes(2 as Weight))
+ }
+ fn leave_intent(c: u32) -> Weight {
+ (55_336_000 as Weight)
+ // Standard Error: 0
+ .saturating_add((151_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(RocksDbWeight::get().reads(1 as Weight))
+ .saturating_add(RocksDbWeight::get().writes(2 as Weight))
+ }
+ fn note_author() -> Weight {
+ (71_461_000 as Weight)
+ .saturating_add(RocksDbWeight::get().reads(3 as Weight))
+ .saturating_add(RocksDbWeight::get().writes(4 as Weight))
+ }
+ fn new_session(r: u32, c: u32) -> Weight {
+ (0 as Weight)
+ // Standard Error: 1_010_000
+ .saturating_add((109_961_000 as Weight).saturating_mul(r as Weight))
+ // Standard Error: 1_010_000
+ .saturating_add((151_952_000 as Weight).saturating_mul(c as Weight))
+ .saturating_add(RocksDbWeight::get().reads((1 as Weight).saturating_mul(r as Weight)))
+ .saturating_add(RocksDbWeight::get().reads((2 as Weight).saturating_mul(c as Weight)))
+ .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(r as Weight)))
+ .saturating_add(RocksDbWeight::get().writes((2 as Weight).saturating_mul(c as Weight)))
+ }
+}
diff --git a/frame/custom-signatures/Cargo.toml b/frame/custom-signatures/Cargo.toml
new file mode 100644
index 00000000..6c563073
--- /dev/null
+++ b/frame/custom-signatures/Cargo.toml
@@ -0,0 +1,42 @@
+[package]
+name = "pallet-custom-signatures"
+version = "4.3.0"
+authors = ["Stake Technologies "]
+edition = "2018"
+license = "Apache-2.0"
+homepage = "https://docs.plasmnet.io/"
+repository = "https://github.com/staketechnologies/Plasm/"
+description = "FRAME pallet for user defined extrinsic signatures"
+
+[dependencies]
+codec = { package = "parity-scale-codec", version = "2.2.0", features = ["derive"], default-features = false }
+scale-info = { version = "1.0", default-features = false, features = ["derive"] }
+serde = { version = "1.0.106", features = ["derive"], optional = true }
+sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13", default-features = false }
+
+[dev-dependencies]
+pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" }
+libsecp256k1 = "0.6.0"
+hex-literal = "0.2.1"
+sp-keyring = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" }
+sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.13" }
+
+[features]
+default = ["std"]
+std = [
+ "serde",
+ "codec/std",
+ "scale-info/std",
+ "sp-io/std",
+ "sp-std/std",
+ "sp-core/std",
+ "sp-runtime/std",
+ "frame-support/std",
+ "frame-system/std",
+]
+try-runtime = ["frame-support/try-runtime"]
diff --git a/frame/custom-signatures/src/ethereum.rs b/frame/custom-signatures/src/ethereum.rs
new file mode 100644
index 00000000..21de7e81
--- /dev/null
+++ b/frame/custom-signatures/src/ethereum.rs
@@ -0,0 +1,83 @@
+//! Ethereum prefixed signatures compatibility instances.
+
+use codec::{Decode, Encode};
+use sp_core::ecdsa;
+use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::keccak_256};
+use sp_runtime::traits::{IdentifyAccount, Lazy, Verify};
+use sp_runtime::MultiSignature;
+use sp_std::prelude::*;
+
+/// Ethereum-compatible signature type.
+#[derive(Encode, Decode, PartialEq, Eq, Clone, scale_info::TypeInfo)]
+pub struct EthereumSignature(pub [u8; 65]);
+
+impl sp_std::fmt::Debug for EthereumSignature {
+ fn fmt(&self, f: &mut sp_std::fmt::Formatter<'_>) -> sp_std::fmt::Result {
+ write!(f, "EthereumSignature({:?})", &self.0[..])
+ }
+}
+
+impl From for EthereumSignature {
+ fn from(signature: ecdsa::Signature) -> Self {
+ Self(signature.into())
+ }
+}
+
+impl sp_std::convert::TryFrom> for EthereumSignature {
+ type Error = ();
+
+ fn try_from(data: Vec) -> Result {
+ if data.len() == 65 {
+ let mut inner = [0u8; 65];
+ inner.copy_from_slice(&data[..]);
+ Ok(EthereumSignature(inner))
+ } else {
+ Err(())
+ }
+ }
+}
+
+/// Constructs the message that Ethereum RPC's `personal_sign` and `eth_sign` would sign.
+///
+/// Note: sign message hash to escape of message length estimation.
+pub fn signable_message(what: &[u8]) -> Vec {
+ let hash = keccak_256(what);
+ let mut v = b"\x19Ethereum Signed Message:\n32".to_vec();
+ v.extend_from_slice(&hash[..]);
+ v
+}
+
+/// Attempts to recover the Ethereum public key from a message signature signed by using
+/// the Ethereum RPC's `personal_sign` and `eth_sign`.
+impl Verify for EthereumSignature {
+ type Signer = ::Signer;
+
+ fn verify>(
+ &self,
+ mut msg: L,
+ account: &::AccountId,
+ ) -> bool {
+ let msg = keccak_256(&signable_message(msg.get()));
+ match secp256k1_ecdsa_recover_compressed(&self.0, &msg).ok() {
+ Some(public) => {
+ let signer = Self::Signer::from(ecdsa::Public::from_raw(public));
+ *account == signer.into_account()
+ }
+ None => false,
+ }
+ }
+}
+
+#[test]
+fn verify_should_works() {
+ use hex_literal::hex;
+ use sp_core::{ecdsa, Pair};
+
+ let msg = "test eth signed message";
+ let pair = ecdsa::Pair::from_seed(&hex![
+ "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"
+ ]);
+ let account = ::Signer::from(pair.public()).into_account();
+ let signature = EthereumSignature(hex!["f5d5cc953828e3fb0d81f3176d88fa5c73d3ad3dc4bc7a8061b03a6db2cd73337778df75a1443e8c642f6ceae0db39b90c321ac270ad7836695cae76f703f3031c"]);
+ assert_eq!(signature.verify(msg.as_ref(), &account), true);
+}
diff --git a/frame/custom-signatures/src/lib.rs b/frame/custom-signatures/src/lib.rs
new file mode 100644
index 00000000..3d91f36a
--- /dev/null
+++ b/frame/custom-signatures/src/lib.rs
@@ -0,0 +1,200 @@
+#![cfg_attr(not(feature = "std"), no_std)]
+
+pub use pallet::*;
+
+/// Ethereum-compatible signatures (eth_sign API call).
+pub mod ethereum;
+
+#[cfg(test)]
+mod tests;
+
+#[frame_support::pallet]
+pub mod pallet {
+ use frame_support::{
+ pallet_prelude::*,
+ traits::{
+ Currency, ExistenceRequirement, Get, OnUnbalanced, UnfilteredDispatchable,
+ WithdrawReasons,
+ },
+ weights::GetDispatchInfo,
+ };
+ use frame_system::{ensure_none, pallet_prelude::*};
+ use sp_runtime::traits::{IdentifyAccount, Verify};
+ use sp_std::{convert::TryFrom, prelude::*};
+
+ #[pallet::pallet]
+ pub struct Pallet(_);
+
+ /// The balance type of this pallet.
+ pub type BalanceOf =
+ <::Currency as Currency<::AccountId>>::Balance;
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The overarching event type.
+ type Event: From> + IsType<::Event>;
+
+ /// A signable call.
+ type Call: Parameter + UnfilteredDispatchable + GetDispatchInfo;
+
+ /// User defined signature type.
+ type Signature: Parameter + Verify + TryFrom>;
+
+ /// User defined signer type.
+ type Signer: IdentifyAccount;
+
+ /// The currency trait.
+ type Currency: Currency;
+
+ /// The call fee destination.
+ type OnChargeTransaction: OnUnbalanced<
+ >::NegativeImbalance,
+ >;
+
+ /// The call processing fee amount.
+ #[pallet::constant]
+ type CallFee: Get