diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8fd296e8..390bc0c1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,7 +61,7 @@ jobs: - arch: aarch64 target-tupl: aarch64-unknown-linux-gnu dependencies: gcc-aarch64-linux-gnu protobuf-compiler libclang-dev g++-aarch64-linux-gnu - bindgenExtraClangArgs: "--sysroot=/usr/aarch64-linux-gnu -mfloat-abi=hard" + bindgenExtraClangArgs: "--sysroot=/usr/aarch64-linux-gnu" steps: - uses: actions/checkout@v4 diff --git a/Cargo.lock b/Cargo.lock index 3faf3fa1..78614eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5461,6 +5461,27 @@ dependencies = [ "sqnc-pallet-traits", ] +[[package]] +name = "pallet-validator-set" +version = "12.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-session", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-weights", +] + [[package]] name = "parity-bip39" version = "2.0.1" @@ -9260,12 +9281,14 @@ dependencies = [ "pallet-preimage", "pallet-process-validation", "pallet-scheduler", + "pallet-session", "pallet-sudo", "pallet-symmetric-key", "pallet-timestamp", "pallet-transaction-payment-free", "pallet-transaction-payment-rpc-runtime-api", "pallet-utxo-nft", + "pallet-validator-set", "parity-scale-codec", "scale-info", "serde", diff --git a/Cargo.toml b/Cargo.toml index 7f633ed6..0da6f03c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ pallet-scheduler = { version = "39.0.0", default-features = false } pallet-balances = { version = "39.0.0", default-features = false } pallet-babe = { version = "38.0.0", default-features = false } pallet-grandpa = { version = "38.0.0", default-features = false } +pallet-session = { version = "38.0.0", default-features = false } pallet-sudo = { version = "38.0.0", default-features = false } pallet-timestamp = { version = "37.0.0", default-features = false } pallet-transaction-payment-rpc-runtime-api = { version = "38.0.0", default-features = false } @@ -61,10 +62,13 @@ sp-genesis-builder = { version = "0.15.1", default-features = false } sp-inherents = { version = "34.0.0", default-features = false } sp-offchain = { version = "34.0.0", default-features = false } sp-session = { version = "36.0.0", default-features = false } +sp-staking = { version = "36.0.0", default-features = false } +sp-state-machine = { version = "0.43.0", default-features = false } sp-statement-store = { version = "18.0.0", default-features = false } sp-storage = { version = "21.0.0", default-features = false } sp-transaction-pool = { version = "34.0.0", default-features = false } sp-version = { version = "37.0.0", default-features = false } +sp-weights = { version = "31.0.0", default-features = false } frame-try-runtime = { version = "0.44.0", default-features = false } pallet-collective = { version = "38.0.0", default-features = false } pallet-membership = { version = "38.0.0", default-features = false } diff --git a/node/src/chain_spec.rs b/node/src/chain_spec.rs index 10965578..7e323a2f 100644 --- a/node/src/chain_spec.rs +++ b/node/src/chain_spec.rs @@ -3,7 +3,7 @@ use sp_consensus_babe::AuthorityId as BabeId; use sp_consensus_grandpa::AuthorityId as GrandpaId; use sp_core::{sr25519, Pair, Public}; use sp_runtime::traits::{IdentifyAccount, Verify}; -use sqnc_runtime::WASM_BINARY; +use sqnc_runtime::{opaque::SessionKeys, SessionConfig, ValidatorSetConfig, WASM_BINARY}; use sqnc_runtime_types::{AccountId, RuntimeExpressionSymbol, RuntimeRestriction, Signature}; const DEFAULT_PROTOCOL_ID: &str = "sqnc"; @@ -29,8 +29,12 @@ where } /// Generate an authority key. -pub fn authority_keys_from_seed(s: &str) -> (BabeId, GrandpaId) { - (get_from_seed::(s), get_from_seed::(s)) +pub fn authority_keys_from_seed(s: &str) -> (AccountId, BabeId, GrandpaId) { + ( + get_account_id_from_seed::(s), + get_from_seed::(s), + get_from_seed::(s), + ) } pub fn development_config() -> Result { @@ -141,7 +145,7 @@ pub fn local_testnet_config() -> Result { /// Configure initial storage state for FRAME modules. fn testnet_genesis( - initial_authorities: Vec<(BabeId, GrandpaId)>, + initial_authorities: Vec<(AccountId, BabeId, GrandpaId)>, root_key: AccountId, endowed_accounts: Vec, technical_committee_accounts: Vec, @@ -152,11 +156,20 @@ fn testnet_genesis( "balances": endowed_accounts.iter().cloned().map(|k| (k, 1i64 << 60)).collect::>(), }, "babe": { - "authorities": initial_authorities.iter().map(|x| (x.0.clone(), 1)).collect::>(), + "authorities": Vec::::new(), "epochConfig": Some(sqnc_runtime::BABE_GENESIS_EPOCH_CONFIG), }, "grandpa": { - "authorities": initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect::>(), + "authorities": Vec::::new(), + }, + "validatorSet": ValidatorSetConfig { + initial_validators: initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + }, + "session": SessionConfig { + keys: initial_authorities.iter().map(|x| { + (x.0.clone(), x.0.clone(), SessionKeys { babe: x.1.clone(), grandpa: x.2.clone() }) + }).collect::>(), + non_authority_keys: Vec::new() }, "sudo": { "key": Some(root_key), diff --git a/pallets/validator-set/Cargo.toml b/pallets/validator-set/Cargo.toml new file mode 100644 index 00000000..dce67fce --- /dev/null +++ b/pallets/validator-set/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = 'pallet-validator-set' +authors = ['Digital Catapult '] +description = 'SessionManager implementation that allows a configured origin to manager the validators for future sessions' +edition = '2021' +license = 'Apache-2.0' +repository = 'https://github.com/digicatapult/sqnc-node/' +version = { workspace = true } + +[dependencies] +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-staking = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-session = { workspace = true, features = ['historical'] } +sp-weights = { workspace = true } +scale-info = { workspace = true, features = ['derive', 'serde'] } +log = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } + +[dev-dependencies] +sp-state-machine = { workspace = true } +serde = { workspace = true, features = ['derive'] } + +[features] +default = ['std'] +runtime-benchmarks = ['frame-benchmarking/runtime-benchmarks'] +std = [ + 'parity-scale-codec/std', + 'frame-benchmarking/std', + 'frame-support/std', + 'frame-system/std', + 'scale-info/std', + 'sp-core/std', + 'sp-io/std', + 'sp-std/std', + 'sp-runtime/std', + 'pallet-session/std', +] +try-runtime = ['frame-support/try-runtime'] diff --git a/pallets/validator-set/README.md b/pallets/validator-set/README.md new file mode 100644 index 00000000..1e7391ad --- /dev/null +++ b/pallets/validator-set/README.md @@ -0,0 +1,204 @@ +# Validator Set Pallet + +A [Substrate](https://github.com/paritytech/polkadot-sdk/tree/master/substrate#substrate) pallet to add/remove authorities/validators in PoA networks. + +## Attribution + +`pallet-validator-set` has been adapted from commit `3bd041f43df5911b7d95c947737aa0a0e45588f8` of the +[`substrate-validator-set`](https://github.com/gautamdhameja/substrate-validator-set) pallet originally +licensed under the Apache-2.0 license. A copy of this license can +be found at [./licenses/substrate-validator-set.LICENSE](./licenses/substrate-validator-set.LICENSE). +This module is then relicensed by Digital Catapult under the same Apache-2.0 +license a copy of which is [in the root of this repository](../../LICENSE) + +License: Apache-2.0 + +## Setup with Substrate Node Template + +### Dependencies - runtime/cargo.toml + +- Add the module's dependency in the `Cargo.toml` of your runtime directory. Make sure to enter the correct path or git url of the pallet as per your setup. + +- Make sure that you also have the Substrate [session pallet](https://github.com/paritytech/polkadot-sdk/tree/master/substrate/frame/session) as part of your runtime. This is because the validator-set pallet is dependent on the session pallet. + +```toml +[dependencies.validator-set] +default-features = false +package = 'substrate-validator-set' +git = 'https://github.com/gautamdhameja/substrate-validator-set.git' +version = '1.1.0' + +[dependencies.pallet-session] +default-features = false +git = 'https://github.com/paritytech/polkadot-sdk.git' +tag = 'polkadot-v1.13.0' +``` + +```toml +std = [ + ... + 'validator-set/std', + 'pallet-session/std', +] +``` + +### Pallet Initialization - runtime/src/lib.rs + +- Import `OpaqueKeys` in your `runtime/src/lib.rs`. + +```rust +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, Verify, IdentifyAccount, NumberFor, OpaqueKeys, +}; +``` + +- Also in `runtime/src/lib.rs` import the `EnsureRoot` trait. This would change if you want to configure a custom origin (see below). + +```rust + use frame_system::EnsureRoot; +``` + +- Declare the pallet in your `runtime/src/lib.rs`. The pallet supports configurable origin and you can either set it to use one of the governance pallets (Collective, Democracy, etc.), or just use root as shown below. But **do not use a normal origin here** because the addition and removal of validators should be done using elevated privileges. + +```rust +parameter_types! { + pub const MinAuthorities: u32 = 2; +} + +impl validator_set::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddRemoveOrigin = EnsureRoot; + type MinAuthorities = MinAuthorities; + type WeightInfo = validator_set::weights::SubstrateWeight; +} +``` + +- Also, declare the session pallet in your `runtime/src/lib.rs`. Some of the type configuration of session pallet would depend on the ValidatorSet pallet as shown below. + +```rust +parameter_types! { + pub const Period: u32 = 2 * MINUTES; + pub const Offset: u32 = 0; +} + +impl pallet_session::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ValidatorId = ::AccountId; + type ValidatorIdOf = validator_set::ValidatorOf; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionManager = ValidatorSet; + type SessionHandler = ::KeyTypeIdProviders; + type Keys = opaque::SessionKeys; + type WeightInfo = (); +} +``` + +- Add `validator_set`, and `session` pallets in `construct_runtime` macro. **Make sure to add them before `Aura` and `Grandpa` pallets and after `Balances`. Also make sure that the `validator_set` pallet is added _before_ the `session` pallet, because it provides the initial validators at genesis, and must initialize first.** + +```rust +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + ... + Balances: pallet_balances, + ValidatorSet: validator_set, + Session: pallet_session, + Aura: pallet_aura, + Grandpa: pallet_grandpa, + ... + ... + } +); +``` + +### Genesis config - chain_spec.rs + +- Import `opaque::SessionKeys, ValidatorSetConfig, SessionConfig` from the runtime in `node/src/chain_spec.rs`. + +```rust +use node_template_runtime::{ + AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, + SudoConfig, SystemConfig, WASM_BINARY, Signature, + opaque::SessionKeys, ValidatorSetConfig, SessionConfig +}; +``` + +- And then in `node/src/chain_spec.rs` update the key generation functions. + +```rust +fn session_keys(aura: AuraId, grandpa: GrandpaId) -> SessionKeys { + SessionKeys { aura, grandpa } +} + +pub fn authority_keys_from_seed(s: &str) -> (AccountId, AuraId, GrandpaId) { + ( + get_account_id_from_seed::(s), + get_from_seed::(s), + get_from_seed::(s) + ) +} +``` + +- Add genesis config in the `chain_spec.rs` file for `session` and `validatorset` pallets, and update it for `Aura` and `Grandpa` pallets. Because the validators are provided by the `session` pallet, we do not initialize them explicitly for `Aura` and `Grandpa` pallets. Order is important, notice that `pallet_session` is declared after `pallet_balances` since it depends on it (session accounts should have some balance). + +```rust +fn testnet_genesis( + wasm_binary: &[u8], + initial_authorities: Vec<(AccountId, AuraId, GrandpaId)>, + root_key: AccountId, + endowed_accounts: Vec, + _enable_println: bool, +) -> GenesisConfig { + GenesisConfig { + system: SystemConfig { + // Add Wasm runtime to storage. + code: wasm_binary.to_vec(), + }, + balances: BalancesConfig { + // Configure endowed accounts with initial balance of 1 << 60. + balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(), + }, + validator_set: ValidatorSetConfig { + initial_validators: initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + }, + session: SessionConfig { + keys: initial_authorities.iter().map(|x| { + (x.0.clone(), x.0.clone(), session_keys(x.1.clone(), x.2.clone())) + }).collect::>(), + }, + aura: AuraConfig { + authorities: vec![], + }, + grandpa: GrandpaConfig { + authorities: vec![], + }, + sudo: SudoConfig { + // Assign network admin rights. + key: Some(root_key), + }, + transaction_payment: Default::default(), + } +} +``` + +## Run + +Once you have set up the pallet in your node/node-template and everything compiles, follow the steps in [docs/local-network-setup.md](./docs/local-network-setup.md) to run a local network and add validators. + +## Extensions + +### Council-based Governance + +Instead of using `sudo`, for a council-based governance, use the pallet with the `Collective` pallet. Follow the steps in [docs/council-integration.md](./docs/council-integration.md). + +### Auto-removal Of Offline Validators + +When a validator goes offline, it skips its block production slot and that causes increased block times. Sometimes, we want to remove these offline validators so that the block time can recover to normal. The `ImOnline` pallet, when added to a runtime, can report offline validators. The `ValidatorSet` pallet implements the required types to integrate with `ImOnline` pallet for automatic removal of offline validators. To use the `ValidatorSet` pallet with the `ImOnline` pallet, follow the steps in [docs/im-online-integration.md](./docs/im-online-integration.md). + +## Disclaimer + +This code is **not audited** for production use cases. You can expect security vulnerabilities. Do not use it without proper testing and audit in a production applications. diff --git a/pallets/validator-set/docs/council-integration.md b/pallets/validator-set/docs/council-integration.md new file mode 100644 index 00000000..59d7241d --- /dev/null +++ b/pallets/validator-set/docs/council-integration.md @@ -0,0 +1,76 @@ +# How to use the Validator Set pallet with Collective pallet as origin + +## Setup + +1. Import and declare the `Collective` pallet in your runtime. Make sure to import the required traits. + +```rust +parameter_types! { + pub const CouncilMotionDuration: BlockNumber = 3 * MINUTES; + pub const CouncilMaxProposals: u32 = 100; + pub const CouncilMaxMembers: u32 = 100; + + // From system config trait impl. + pub MaxCollectivesProposalWeight: Weight = Perbill::from_percent(50) * BlockWeights::get().max_block; +} + +impl pallet_collective::Config for Runtime { + type RuntimeOrigin = RuntimeOrigin; + type Proposal = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type MotionDuration = CouncilMotionDuration; + type MaxProposals = CouncilMaxProposals; + type MaxMembers = CouncilMaxMembers; + type DefaultVote = pallet_collective::PrimeDefaultVote; + type WeightInfo = pallet_collective::weights::SubstrateWeight; + type SetMembersOrigin = EnsureRoot; + type MaxProposalWeight = MaxCollectivesProposalWeight; +} +``` + +2. Setup a custom origin using the `Collective` pallet. In the example below, we have a custom origin that ensures either root or at least half of the collective's approval. + +```rust +type EnsureRootOrHalfCouncil = EitherOfDiverse< + EnsureRoot, + pallet_collective::EnsureProportionMoreThan, +>; +``` + +3. Configure the custom origin for the Validator Set pallet. + +```rust +impl validator_set::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MinAuthorities = MinAuthorities; + type AddRemoveOrigin = EnsureRootOrHalfCouncil; // Note +} +``` + +4. Add genesis config for the `Collective` pallet to initialize some members in the collective when the chain starts. In the example below, the initial authorities are also the collective members. (The `Collective` pallet's default instance is named as council here.) + +```rust +council: CouncilConfig { + members: initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + phantom: Default::default(), +}, +``` + +For rest of the setup steps, see [readme.md](../readme.md). + +## Run + +Before running - Double check `Alice` and `Bob` are set as initial authorities and also as council members. + +- Start the chain in the `local_testnet` mode with the non-validator (--charlie) first (so it take the default port and connects with the Polkadot JS web app). +- Start `--alice` and `--bob` nodes. +- Alice and Bob will produce blocks and Charlie will only import them as it is not a validator yet. +- From Charlie's node connected web app instance, make an RPC call `author - rotate_keys`. This will print session keys for Charlie node. +- Use this printed string in the step 4 and make an extrinsic call from `Charlie` account - `Session - set_keys`. Put 0x in the proof input. +- Now start the council flow. From `Alice` propose a motion to add Charlie as a validator using the validator-set pallet. +- From `Bob` account, vote and approve this proposal. +- Wait for the voting period to end. Close the motion using `Alice` account. +- The motion will execute and `Charlie` will be added as validator. (see screenshot below) + +
+council-motion diff --git a/pallets/validator-set/docs/im-online-integration.md b/pallets/validator-set/docs/im-online-integration.md new file mode 100644 index 00000000..c02a3114 --- /dev/null +++ b/pallets/validator-set/docs/im-online-integration.md @@ -0,0 +1,231 @@ +# How To Use Validator Set Pallet With ImOnline Pallet For Automatic Removal Of Offline Validators + +## Setup + +* Before following the steps below, make sure you have completed all the steps in the [readme.md](../readme.md). + +### Dependencies - runtime/cargo.toml + +* Add the `im-online` pallet in your runtime's `cargo.toml`. + +```toml +[dependencies.pallet-im-online] +default-features = false +git = 'https://github.com/paritytech/polkadot-sdk.git' +tag = 'polkadot-v1.13.0' +``` + +```toml +std = [ + ... + 'pallet-im-online/std', +] +``` + +### Pallet Initialization - runtime/src/lib.rs + +* Import `ImOnlineId` and `Verify` `runtime/src/lib.rs`. + +```rust +use sp_runtime::traits::{ + AccountIdLookup, BlakeTwo256, Block as BlockT, Verify, IdentifyAccount, NumberFor, OpaqueKeys, +}; +use pallet_im_online::sr25519::AuthorityId as ImOnlineId; +``` + +* Add the `ImOnline` key to the session keys for your runtime in `runtime/src/lib.rs`: + +```rust +impl_opaque_keys! { + pub struct SessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, + pub im_online: ImOnline, + } + } +``` + +* Add the `im-online` pallet and it's configuration. This will require more types to be imported. + +```rust +parameter_types! { + pub const ImOnlineUnsignedPriority: TransactionPriority = TransactionPriority::max_value(); + pub const StakingUnsignedPriority: TransactionPriority = TransactionPriority::max_value() / 2; + pub const MaxAuthorities: u32 = 100; + pub const MaxKeys: u32 = 10_000; + pub const MaxPeerInHeartbeats: u32 = 10_000; +} + +impl frame_system::offchain::CreateSignedTransaction for Runtime +where + RuntimeCall: From, +{ + fn create_transaction>( + call: RuntimeCall, + public: ::Signer, + account: AccountId, + nonce: Index, + ) -> Option<(RuntimeCall, ::SignaturePayload)> { + let period = + BlockHashCount::get().checked_next_power_of_two().map(|c| c / 2).unwrap_or(2) as u64; + let current_block = System::block_number().saturated_into::().saturating_sub(1); + let era = Era::mortal(period, current_block); + let extra = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(era), + frame_system::CheckNonce::::from(nonce), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + ); + let raw_payload = SignedPayload::new(call, extra) + .map_err(|e| { + log::warn!("Unable to create signed payload: {:?}", e); + }) + .ok()?; + let signature = raw_payload.using_encoded(|payload| C::sign(payload, public))?; + let address = account; + let (call, extra, _) = raw_payload.deconstruct(); + Some((call, (sp_runtime::MultiAddress::Id(address), signature, extra))) + } +} + +impl frame_system::offchain::SigningTypes for Runtime { + type Public = ::Signer; + type Signature = Signature; +} + +impl frame_system::offchain::SendTransactionTypes for Runtime +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; +} + +impl pallet_im_online::Config for Runtime { + type AuthorityId = ImOnlineId; + type RuntimeEvent = RuntimeEvent; + type NextSessionRotation = pallet_session::PeriodicSessions; + type ValidatorSet = ValidatorSet; + type ReportUnresponsiveness = ValidatorSet; + type UnsignedPriority = ImOnlineUnsignedPriority; + type WeightInfo = pallet_im_online::weights::SubstrateWeight; + type MaxKeys = MaxKeys; + type MaxPeerInHeartbeats = MaxPeerInHeartbeats; +} +``` + +* Add `im-online` pallet in `construct_runtime` macro. + +```rust +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + ... + Balances: pallet_balances, + ValidatorSet: validator_set, + Session: pallet_session, + ImOnline: pallet_im_online, + Aura: pallet_aura, + Grandpa: pallet_grandpa, + ... + ... + } +); +``` + +### Genesis config - chain_spec.rs + +* Add the `im-online` pallet in your node `cargo.toml`. This is needed because we need to import some types in the `chain_spec.rs`. + +```toml +[dependencies.pallet-im-online] +default-features = false +git = 'https://github.com/paritytech/polkadot-sdk.git' +tag = 'polkadot-v1.13.0' +``` + +* Import `ImOnlineId` in the `chain_spec.rs`. + +```rust +use pallet_im_online::sr25519::AuthorityId as ImOnlineId; +``` + +* Also import `ImOnlineConfig` in `chain_spec.rs`. + +```rust +use node_template_runtime::{ + opaque::SessionKeys, AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, + SessionConfig, Signature, SudoConfig, SystemConfig, ValidatorSetConfig, ImOnlineConfig, + WASM_BINARY, +}; +``` + +* Add `ImOnlineId` to the key generation functions in `chain_spec.rs`. + +```rust +fn session_keys(aura: AuraId, grandpa: GrandpaId, im_online: ImOnlineId) -> SessionKeys { + SessionKeys { aura, grandpa, im_online } +} + +pub fn authority_keys_from_seed(s: &str) -> (AccountId, AuraId, GrandpaId, ImOnlineId) { + ( + get_account_id_from_seed::(s), + get_from_seed::(s), + get_from_seed::(s), + get_from_seed::(s), + ) +} +``` + +* Add genesis config in the `chain_spec.rs` file for the `im_online` pallet. Notice that the `ImOnlineId` has also been added to the tuple of keys, and it is also being used in the `keys` config for `session` pallet. + +```rust +fn testnet_genesis( + wasm_binary: &[u8], + initial_authorities: Vec<(AccountId, AuraId, GrandpaId, ImOnlineId)>, + root_key: AccountId, + endowed_accounts: Vec, + _enable_println: bool, +) -> GenesisConfig { + GenesisConfig { + system: SystemConfig { + // Add Wasm runtime to storage. + code: wasm_binary.to_vec(), + }, + balances: BalancesConfig { + // Configure endowed accounts with initial balance of 1 << 60. + balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(), + }, + validator_set: ValidatorSetConfig { + initial_validators: initial_authorities.iter().map(|x| x.0.clone()).collect::>(), + }, + session: SessionConfig { + keys: initial_authorities + .iter() + .map(|x| { + (x.0.clone(), x.0.clone(), session_keys(x.1.clone(), x.2.clone(), x.3.clone())) + }) + .collect::>(), + }, + aura: AuraConfig { authorities: vec![] }, + grandpa: GrandpaConfig { authorities: vec![] }, + im_online: ImOnlineConfig { keys: vec![] }, + sudo: SudoConfig { + // Assign network admin rights. + key: Some(root_key), + }, + transaction_payment: Default::default(), + } +} +``` + +## Run + +To run the node and network, follow the steps in [docs/local-network-setup.md](./local-network-setup.md). diff --git a/pallets/validator-set/docs/img/add-validator.png b/pallets/validator-set/docs/img/add-validator.png new file mode 100644 index 00000000..0916728f Binary files /dev/null and b/pallets/validator-set/docs/img/add-validator.png differ diff --git a/pallets/validator-set/docs/img/council-motion.png b/pallets/validator-set/docs/img/council-motion.png new file mode 100644 index 00000000..4e8e54cf Binary files /dev/null and b/pallets/validator-set/docs/img/council-motion.png differ diff --git a/pallets/validator-set/docs/img/rotate-keys.png b/pallets/validator-set/docs/img/rotate-keys.png new file mode 100644 index 00000000..7be9eba3 Binary files /dev/null and b/pallets/validator-set/docs/img/rotate-keys.png differ diff --git a/pallets/validator-set/docs/img/set-keys.png b/pallets/validator-set/docs/img/set-keys.png new file mode 100644 index 00000000..003d54be Binary files /dev/null and b/pallets/validator-set/docs/img/set-keys.png differ diff --git a/pallets/validator-set/docs/local-network-setup.md b/pallets/validator-set/docs/local-network-setup.md new file mode 100644 index 00000000..abaed7f9 --- /dev/null +++ b/pallets/validator-set/docs/local-network-setup.md @@ -0,0 +1,80 @@ +# Local Network Setup For Testing Validator Set pallet + +If you are using the Substrate node template and want to set up a local network to test/use the validator set pallet, follow the steps below. Make sure you have completed all the steps in the [readme.md](../readme.md) before doing this. + +## Local Network + +Once your node template compiles after adding the validator set pallet, the first thing we need to do is run at least 3 nodes as a local network. +The node template comes with two predefined chain configurations - `dev` and `local`. We will be using the local config that already has two values in the `initial_authorities` genesis config. These are accounts generated using the dev seed for `Alice` and `Bob`. + +### Step 1 + +Run the `Alice` validator: + +```bash +./target/release/node-template --chain=local --alice --base-path ~/tmp/a --port=30334 --ws-port 9944 --ws-external --rpc-cors=all --rpc-methods=Unsafe --rpc-external +``` + +Note that we have provided --chain=local, and custom values for base-path, port, ws-port. These custom value are used to avoid conflicts when running all nodes on local machine. If you are running nodes on separate servers, you can leave these as defaults. + +The remaining parameters `--ws-external --rpc-cors=all --rpc-methods=Unsafe --rpc-external` are used to connect ws and rpc from a remote machine, and to allow rpc calls. For production setup, this should be double-checked, and only allowed if required. Do not use these as-is for production nodes. + +### Step 2 + +Next, once the `Alice` node starts, copy it's node address from the console logs: + +```bash +Local node identity is: 12D3KooWQXBxhGvmbcb8siLZWf7bNzv3KrzEXhi9t2VbDGP57zR9 +``` + +In this case, `12D3KooWQXBxhGvmbcb8siLZWf7bNzv3KrzEXhi9t2VbDGP57zR9` is the node identity. We will need this as the bootnode address for other nodes. + +### Step 3 + +Next, run the `Bob` and `Charlie` nodes: + +```bash +./target/release/node-template --chain=local --bob --base-path ~/tmp/b --port=30335 --ws-port 9945 --ws-external --rpc-cors=all --rpc-methods=Unsafe --rpc-external --bootnodes /ip4/127.0.0.1/tcp/30334/p2p/12D3KooWQXBxhGvmbcb8siLZWf7bNzv3KrzEXhi9t2VbDGP57zR9 +``` + +and, + +```bash +./target/release/node-template --chain=local --charlie --base-path ~/tmp/c --port=30336 --ws-port 9946 --ws-external --rpc-cors=all --rpc-methods=Unsafe --rpc-external --bootnodes /ip4/127.0.0.1/tcp/30334/p2p/12D3KooWQXBxhGvmbcb8siLZWf7bNzv3KrzEXhi9t2VbDGP57zR9 +``` + +As you can see, we have added an additional parameter when running `Bob` and `Charlie` nodes to provide a bootnode. The bootnodes parameter has the `Alice` node's identity. You should replace it with what you get in Step 2 above. + +### Step 4 + +Open Polkadot JS Apps in your browser and connect it to the `Charlie` node using its ws endpoint. In this case, it would be: `ws://127.0.0.1:9946`. Remember we used a custom `ws-port`. +In the Network Explorer you should see that `Alice` and `Bob` are producing blocks, while `Charlie` is not. This is because `Charlie` is not part of initial authorities from genesis config. + +### Step 5 + +From `Charlie` node connected Polkadot JS Apps instance, make an RPC call `author - rotate_keys`. This will print public session keys for `Charlie` node. + +
+rotate-keys + +### Step 6 + +Now make an extrinsic call from `Charlie` account - `Session - set_keys`. +We need to use the substrings of the string obtained in Step 5, in the `keys: NodeTemplateRuntimeOpaqueSessionKeys` input fields. First remove the `0x` from the beginning. Then divide that string into equal parts of 32 bytes each. Prefix `0x` to each of these smaller strings, and then enter them in the input fields for each of the keys (aura, grandpa, imonline). +Put `0x` in the `proof` input. + +
+set-keys + +### Step 7 + +Finally, add `Charlie` as a validator by calling the `add_validator` extrinsic of the Validator Set pallet using `sudo`. Alice should be caller of this extrinsic, because Alice is set as the sudo key in node template. + +
+add-validator + +### Step 8 + +Go to the Network Explorer page in Polkadot JS Apps and note the session number. After two sessions from this, the `Charlie` node should start authoring blocks and you should be able to see under the recent blocks list on the same page. + +Following these steps, you can add more validators. diff --git a/pallets/validator-set/licenses/substrate-validator-set.LICENSE b/pallets/validator-set/licenses/substrate-validator-set.LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/pallets/validator-set/licenses/substrate-validator-set.LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pallets/validator-set/src/benchmarking.rs b/pallets/validator-set/src/benchmarking.rs new file mode 100644 index 00000000..dad0aa67 --- /dev/null +++ b/pallets/validator-set/src/benchmarking.rs @@ -0,0 +1,27 @@ +#![cfg(feature = "runtime-benchmarks")] + +use super::*; +use frame_benchmarking::v1::{account, benchmarks, BenchmarkError}; +use frame_support::traits::EnsureOrigin; + +const SEED: u32 = 0; + +benchmarks! { + add_validator { + let origin = + T::AddRemoveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let validator: T::ValidatorId = account("validator", 0, SEED); + }: _(origin, validator) + + remove_validator { + let origin = + T::AddRemoveOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + let validator: T::ValidatorId = account("validator", 0, SEED); + }: _(origin, validator) + + impl_benchmark_test_suite!( + ValidatorSet, + crate::mock::new_test_ext(), + crate::mock::Test, + ); +} diff --git a/pallets/validator-set/src/lib.rs b/pallets/validator-set/src/lib.rs new file mode 100644 index 00000000..032e435c --- /dev/null +++ b/pallets/validator-set/src/lib.rs @@ -0,0 +1,280 @@ +//! # Validator Set Pallet +//! +//! The Validator Set Pallet allows addition and removal of +//! authorities/validators via extrinsics (transaction calls), in +//! Substrate-based PoA networks. It also integrates with the im-online pallet +//! to automatically remove offline validators. +//! +//! The pallet depends on the Session pallet and implements related traits for session +//! management. Currently it uses periodic session rotation provided by the +//! session pallet to automatically rotate sessions. For this reason, the +//! validator addition and removal becomes effective only after 2 sessions +//! (queuing + applying). + +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod mock; +mod tests; +pub mod weights; + +use frame_support::{ + ensure, + pallet_prelude::*, + traits::{EstimateNextSessionRotation, Get, ValidatorSet, ValidatorSetWithIdentification}, + DefaultNoBound, +}; +use frame_system::pallet_prelude::*; +use log; +pub use pallet::*; +use sp_runtime::traits::{Convert, Zero}; +use sp_staking::offence::{Offence, OffenceError, ReportOffence}; +use sp_std::prelude::*; +pub use weights::*; + +pub const LOG_TARGET: &'static str = "runtime::validator-set"; + +#[frame_support::pallet()] +pub mod pallet { + use super::*; + + /// Configure the pallet by specifying the parameters and types on which it + /// depends. + #[pallet::config] + pub trait Config: frame_system::Config + pallet_session::Config { + /// The Event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// Origin for adding or removing a validator. + type AddRemoveOrigin: EnsureOrigin; + + /// Minimum number of validators to leave in the validator set during + /// auto removal. + /// Initial validator count could be less than this. + type MinAuthorities: Get; + + /// Information on runtime weights. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::storage] + #[pallet::getter(fn validators)] + pub type Validators = StorageValue<_, Vec, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn offline_validators)] + pub type OfflineValidators = StorageValue<_, Vec, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// New validator addition initiated. Effective in ~2 sessions. + ValidatorAdditionInitiated(T::ValidatorId), + + /// Validator removal initiated. Effective in ~2 sessions. + ValidatorRemovalInitiated(T::ValidatorId), + } + + // Errors inform users that something went wrong. + #[pallet::error] + pub enum Error { + /// Target (post-removal) validator count is below the minimum. + TooLowValidatorCount, + /// Validator is already in the validator set. + Duplicate, + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::genesis_config] + #[derive(DefaultNoBound)] + pub struct GenesisConfig { + pub initial_validators: Vec, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + assert!(>::get().is_empty(), "Validators are already initialized!"); + >::put(&self.initial_validators); + } + } + + #[pallet::call] + impl Pallet { + /// Add a new validator. + /// + /// New validator's session keys should be set in Session pallet before + /// calling this. + /// + /// The origin can be configured using the `AddRemoveOrigin` type in the + /// host runtime. Can also be set to sudo/root. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::add_validator())] + pub fn add_validator(origin: OriginFor, validator_id: T::ValidatorId) -> DispatchResult { + T::AddRemoveOrigin::ensure_origin(origin)?; + + Self::do_add_validator(validator_id.clone())?; + + Ok(()) + } + + /// Remove a validator. + /// + /// The origin can be configured using the `AddRemoveOrigin` type in the + /// host runtime. Can also be set to sudo/root. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::remove_validator())] + pub fn remove_validator(origin: OriginFor, validator_id: T::ValidatorId) -> DispatchResult { + T::AddRemoveOrigin::ensure_origin(origin)?; + + Self::do_remove_validator(validator_id.clone())?; + + Ok(()) + } + } +} + +impl Pallet { + fn do_add_validator(validator_id: T::ValidatorId) -> DispatchResult { + ensure!(!>::get().contains(&validator_id), Error::::Duplicate); + >::mutate(|v| v.push(validator_id.clone())); + + Self::deposit_event(Event::ValidatorAdditionInitiated(validator_id.clone())); + log::debug!(target: LOG_TARGET, "Validator addition initiated."); + + Ok(()) + } + + fn do_remove_validator(validator_id: T::ValidatorId) -> DispatchResult { + let mut validators = >::get(); + + // Ensuring that the post removal, target validator count doesn't go + // below the minimum. + ensure!( + validators.len().saturating_sub(1) as u32 >= T::MinAuthorities::get(), + Error::::TooLowValidatorCount + ); + + validators.retain(|v| *v != validator_id); + + >::put(validators); + + Self::deposit_event(Event::ValidatorRemovalInitiated(validator_id.clone())); + log::debug!(target: LOG_TARGET, "Validator removal initiated."); + + Ok(()) + } + + // Adds offline validators to a local cache for removal on new session. + fn mark_for_removal(validator_id: T::ValidatorId) { + >::mutate(|v| v.push(validator_id)); + log::debug!(target: LOG_TARGET, "Offline validator marked for auto removal."); + } + + // Removes offline validators from the validator set and clears the offline + // cache. It is called in the session change hook and removes the validators + // who were reported offline during the session that is ending. We do not + // check for `MinAuthorities` here, because the offline validators will not + // produce blocks and will have the same overall effect on the runtime. + fn remove_offline_validators() { + let validators_to_remove = >::get(); + + // Delete from active validator set. + >::mutate(|vs| vs.retain(|v| !validators_to_remove.contains(v))); + log::debug!( + target: LOG_TARGET, + "Initiated removal of {:?} offline validators.", + validators_to_remove.len() + ); + + // Clear the offline validator list to avoid repeated deletion. + >::put(Vec::::new()); + } +} + +// Provides the new set of validators to the session module when session is +// being rotated. +impl pallet_session::SessionManager for Pallet { + // Plan a new session and provide new validator set. + fn new_session(_new_index: u32) -> Option> { + // Remove any offline validators. This will only work when the runtime + // also has the im-online pallet. + Self::remove_offline_validators(); + + log::debug!(target: LOG_TARGET, "New session called; updated validator set provided."); + + Some(Self::validators()) + } + + fn end_session(_end_index: u32) {} + + fn start_session(_start_index: u32) {} +} + +impl EstimateNextSessionRotation> for Pallet { + fn average_session_length() -> BlockNumberFor { + Zero::zero() + } + + fn estimate_current_session_progress(_now: BlockNumberFor) -> (Option, sp_weights::Weight) { + (None, Zero::zero()) + } + + fn estimate_next_session_rotation(_now: BlockNumberFor) -> (Option>, sp_weights::Weight) { + (None, Zero::zero()) + } +} + +// Implementation of Convert trait to satisfy trait bounds in session pallet. +// Here it just returns the same ValidatorId. +pub struct ValidatorOf(sp_std::marker::PhantomData); + +impl Convert> for ValidatorOf { + fn convert(account: T::ValidatorId) -> Option { + Some(account) + } +} + +impl ValidatorSet for Pallet { + type ValidatorId = T::ValidatorId; + type ValidatorIdOf = ValidatorOf; + + fn session_index() -> sp_staking::SessionIndex { + pallet_session::Pallet::::current_index() + } + + fn validators() -> Vec { + pallet_session::Pallet::::validators() + } +} + +impl ValidatorSetWithIdentification for Pallet { + type Identification = T::ValidatorId; + type IdentificationOf = ValidatorOf; +} + +// Offence reporting and unresponsiveness management. +// This is for the ImOnline pallet integration. +impl> + ReportOffence for Pallet +{ + fn report_offence(_reporters: Vec, offence: O) -> Result<(), OffenceError> { + let offenders = offence.offenders(); + + for (v, _) in offenders.into_iter() { + Self::mark_for_removal(v); + } + + Ok(()) + } + + fn is_known_offence(_offenders: &[(T::ValidatorId, T::ValidatorId)], _time_slot: &O::TimeSlot) -> bool { + false + } +} diff --git a/pallets/validator-set/src/mock.rs b/pallets/validator-set/src/mock.rs new file mode 100644 index 00000000..8f4a1661 --- /dev/null +++ b/pallets/validator-set/src/mock.rs @@ -0,0 +1,176 @@ +//! Mock helpers for Validator Set pallet. + +#![cfg(test)] + +use super::*; +use crate as validator_set; +use frame_support::{derive_impl, parameter_types}; +use frame_system::EnsureRoot; +use pallet_session::*; +use parity_scale_codec as codec; +use sp_core::crypto::key_types::DUMMY; +use sp_runtime::{ + impl_opaque_keys, testing::UintAuthorityId, traits::OpaqueKeys, BuildStorage, KeyTypeId, RuntimeAppPublic, +}; +use sp_state_machine::BasicExternalities; +use std::collections::BTreeMap; + +impl_opaque_keys! { + pub struct MockSessionKeys { + pub dummy: UintAuthorityId, + } +} + +impl From for MockSessionKeys { + fn from(dummy: UintAuthorityId) -> Self { + Self { dummy } + } +} + +pub const KEY_ID_A: KeyTypeId = KeyTypeId([4; 4]); +pub const KEY_ID_B: KeyTypeId = KeyTypeId([9; 4]); + +#[derive(Debug, Clone, codec::Encode, codec::Decode, PartialEq, Eq)] +pub struct PreUpgradeMockSessionKeys { + pub a: [u8; 32], + pub b: [u8; 64], +} + +impl OpaqueKeys for PreUpgradeMockSessionKeys { + type KeyTypeIdProviders = (); + + fn key_ids() -> &'static [KeyTypeId] { + &[KEY_ID_A, KEY_ID_B] + } + + fn get_raw(&self, i: KeyTypeId) -> &[u8] { + match i { + i if i == KEY_ID_A => &self.a[..], + i if i == KEY_ID_B => &self.b[..], + _ => &[], + } + } +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + ValidatorSet: validator_set, + Session: pallet_session, + } +); + +parameter_types! { + pub static Validators: Vec = vec![1, 2, 3]; + pub static NextValidators: Vec = vec![1, 2, 3]; + pub static Authorities: Vec = + vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)]; + pub static ForceSessionEnd: bool = false; + pub static SessionLength: u64 = 2; + pub static SessionChanged: bool = false; + pub static TestSessionChanged: bool = false; + pub static Disabled: bool = false; + pub static BeforeSessionEndCalled: bool = false; + pub static ValidatorAccounts: BTreeMap = BTreeMap::new(); +} + +pub struct TestShouldEndSession; +impl ShouldEndSession for TestShouldEndSession { + fn should_end_session(now: u64) -> bool { + let l = SessionLength::get(); + now % l == 0 + || ForceSessionEnd::mutate(|l| { + let r = *l; + *l = false; + r + }) + } +} + +pub struct TestSessionHandler; +impl SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [sp_runtime::KeyTypeId] = &[UintAuthorityId::ID]; + fn on_genesis_session(_validators: &[(u64, T)]) {} + fn on_new_session(changed: bool, validators: &[(u64, T)], _queued_validators: &[(u64, T)]) { + SessionChanged::mutate(|l| *l = changed); + Authorities::mutate(|l| { + *l = validators + .iter() + .map(|(_, id)| id.get::(DUMMY).unwrap_or_default()) + .collect() + }); + } + fn on_disabled(_validator_index: u32) { + Disabled::mutate(|l| *l = true) + } + fn on_before_session_ending() { + BeforeSessionEndCalled::mutate(|b| *b = true); + } +} + +pub fn authorities() -> Vec { + Authorities::get().to_vec() +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let keys: Vec<_> = NextValidators::get() + .iter() + .cloned() + .map(|i| (i, i, UintAuthorityId(i).into())) + .collect(); + BasicExternalities::execute_with_storage(&mut t, || { + for (ref k, ..) in &keys { + frame_system::Pallet::::inc_providers(k); + } + frame_system::Pallet::::inc_providers(&4); + // An additional identity that we use. + frame_system::Pallet::::inc_providers(&69); + }); + validator_set::GenesisConfig:: { + initial_validators: keys.iter().map(|x| x.0).collect::>(), + } + .assimilate_storage(&mut t) + .unwrap(); + pallet_session::GenesisConfig:: { + keys, + non_authority_keys: Vec::new(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let v = NextValidators::get().iter().map(|&i| (i, i)).collect(); + ValidatorAccounts::mutate(|m| *m = v); + sp_io::TestExternalities::new(t) +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; +} + +parameter_types! { + pub const MinAuthorities: u32 = 2; +} + +impl validator_set::Config for Test { + type AddRemoveOrigin = EnsureRoot; + type RuntimeEvent = RuntimeEvent; + type MinAuthorities = MinAuthorities; + type WeightInfo = (); +} + +impl pallet_session::Config for Test { + type ValidatorId = u64; + type ValidatorIdOf = validator_set::ValidatorOf; + type ShouldEndSession = TestShouldEndSession; + type NextSessionRotation = (); + type SessionManager = ValidatorSet; + type SessionHandler = TestSessionHandler; + type Keys = MockSessionKeys; + type WeightInfo = (); + type RuntimeEvent = RuntimeEvent; +} diff --git a/pallets/validator-set/src/tests.rs b/pallets/validator-set/src/tests.rs new file mode 100644 index 00000000..c9385415 --- /dev/null +++ b/pallets/validator-set/src/tests.rs @@ -0,0 +1,71 @@ +//! Tests for the Validator Set pallet. + +#![cfg(test)] + +use super::*; +use crate::mock::{authorities, new_test_ext, RuntimeOrigin, Session, Test, ValidatorSet}; +use frame_support::{assert_noop, assert_ok, pallet_prelude::*}; +use sp_runtime::testing::UintAuthorityId; + +#[test] +fn simple_setup_should_work() { + new_test_ext().execute_with(|| { + assert_eq!( + authorities(), + vec![UintAuthorityId(1), UintAuthorityId(2), UintAuthorityId(3)] + ); + assert_eq!(ValidatorSet::validators(), vec![1u64, 2u64, 3u64]); + assert_eq!(Session::validators(), vec![1, 2, 3]); + }); +} + +#[test] +fn add_validator_updates_validators_list() { + new_test_ext().execute_with(|| { + assert_ok!(ValidatorSet::add_validator(RuntimeOrigin::root(), 4)); + assert_eq!(ValidatorSet::validators(), vec![1u64, 2u64, 3u64, 4u64]) + }); +} + +#[test] +fn remove_validator_updates_validators_list() { + new_test_ext().execute_with(|| { + assert_ok!(ValidatorSet::remove_validator(RuntimeOrigin::root(), 2)); + assert_eq!(ValidatorSet::validators(), &[1, 3]); + // Add validator again + assert_ok!(ValidatorSet::add_validator(RuntimeOrigin::root(), 2)); + assert_eq!(ValidatorSet::validators(), &[1, 3, 2]); + }); +} + +#[test] +fn add_validator_fails_with_invalid_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + ValidatorSet::add_validator(RuntimeOrigin::signed(1), 4), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn remove_validator_fails_with_invalid_origin() { + new_test_ext().execute_with(|| { + assert_noop!( + ValidatorSet::remove_validator(RuntimeOrigin::signed(1), 4), + DispatchError::BadOrigin + ); + }); +} + +#[test] +fn duplicate_check() { + new_test_ext().execute_with(|| { + assert_ok!(ValidatorSet::add_validator(RuntimeOrigin::root(), 4)); + assert_eq!(ValidatorSet::validators(), vec![1u64, 2u64, 3u64, 4u64]); + assert_noop!( + ValidatorSet::add_validator(RuntimeOrigin::root(), 4), + Error::::Duplicate + ); + }); +} diff --git a/pallets/validator-set/src/weights.rs b/pallets/validator-set/src/weights.rs new file mode 100644 index 00000000..645c8a57 --- /dev/null +++ b/pallets/validator-set/src/weights.rs @@ -0,0 +1,93 @@ + +//! Autogenerated weights for Validator Set +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-05-29, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("local"), DB CACHE: 1024 + +// Executed Command: +// ./target/release/node-template +// benchmark +// pallet +// --chain +// local +// --pallet +// validator_set +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +/// Weight functions needed for validator_set. +pub trait WeightInfo { + fn add_validator() -> Weight; + fn remove_validator() -> Weight; +} + +/// Weights for validator_set using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: ValidatorSet Validators (r:1 w:1) + /// Proof Skipped: ValidatorSet Validators (max_values: Some(1), max_size: None, mode: Measured) + fn add_validator() -> Weight { + // Proof Size summary in bytes: + // Measured: `117` + // Estimated: `1602` + // Minimum execution time: 20_810_000 picoseconds. + Weight::from_parts(21_330_000, 1602) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } + /// Storage: ValidatorSet Validators (r:1 w:1) + /// Proof Skipped: ValidatorSet Validators (max_values: Some(1), max_size: None, mode: Measured) + fn remove_validator() -> Weight { + // Proof Size summary in bytes: + // Measured: `117` + // Estimated: `1602` + // Minimum execution time: 18_700_000 picoseconds. + Weight::from_parts(19_840_000, 1602) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: ValidatorSet Validators (r:1 w:1) + /// Proof Skipped: ValidatorSet Validators (max_values: Some(1), max_size: None, mode: Measured) + fn add_validator() -> Weight { + // Proof Size summary in bytes: + // Measured: `117` + // Estimated: `1602` + // Minimum execution time: 20_810_000 picoseconds. + Weight::from_parts(21_330_000, 1602) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } + /// Storage: ValidatorSet Validators (r:1 w:1) + /// Proof Skipped: ValidatorSet Validators (max_values: Some(1), max_size: None, mode: Measured) + fn remove_validator() -> Weight { + // Proof Size summary in bytes: + // Measured: `117` + // Estimated: `1602` + // Minimum execution time: 18_700_000 picoseconds. + Weight::from_parts(19_840_000, 1602) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } +} + diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index b6c5dc9b..01f61266 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -22,6 +22,7 @@ frame-support = { workspace = true } pallet-grandpa = { workspace = true } pallet-sudo = { workspace = true } frame-system = { workspace = true } +pallet-session = { workspace = true } pallet-timestamp = { workspace = true } pallet-transaction-payment-rpc-runtime-api = { workspace = true } frame-executive = { workspace = true } @@ -62,6 +63,7 @@ pallet-utxo-nft = { default-features = false, path = '../pallets/utxo-nft' } pallet-process-validation = { default-features = false, path = '../pallets/process-validation' } pallet-symmetric-key = { default-features = false, path = '../pallets/symmetric-key' } pallet-transaction-payment-free = { default-features = false, path = '../pallets/transaction-payment-free' } +pallet-validator-set = { default-features = false, path = '../pallets/validator-set' } sqnc-pallet-traits = { default-features = false, path = '../pallets/traits' } sqnc-runtime-types = { default-features = false, path = './types' } @@ -90,10 +92,12 @@ std = [ "pallet-node-authorization/std", "pallet-preimage/std", "pallet-process-validation/std", + "pallet-session/std", "pallet-sudo/std", "pallet-timestamp/std", "pallet-transaction-payment-free/std", "pallet-transaction-payment-rpc-runtime-api/std", + "pallet-validator-set/std", "sp-api/std", "sp-block-builder/std", "sp-consensus-babe/std", @@ -131,6 +135,7 @@ runtime-benchmarks = [ "pallet-symmetric-key/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", "pallet-utxo-nft/runtime-benchmarks", + "pallet-validator-set/runtime-benchmarks", "sp-runtime/runtime-benchmarks", ] try-runtime = [ @@ -148,9 +153,11 @@ try-runtime = [ "pallet-preimage/try-runtime", "pallet-process-validation/try-runtime", "pallet-scheduler/try-runtime", + "pallet-session/try-runtime", "pallet-sudo/try-runtime", "pallet-symmetric-key/try-runtime", "pallet-timestamp/try-runtime", "pallet-utxo-nft/try-runtime", "pallet-transaction-payment-free/try-runtime", + "pallet-validator-set/try-runtime", ] diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 7b1d9456..261c0b13 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -18,6 +18,7 @@ use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::traits::{BlakeTwo256, Block as BlockT, NumberFor}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, + traits::OpaqueKeys, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; @@ -175,8 +176,8 @@ parameter_types! { impl pallet_babe::Config for Runtime { type EpochDuration = EpochDuration; type ExpectedBlockTime = ExpectedBlockTime; - type EpochChangeTrigger = pallet_babe::SameAuthoritiesForever; - type DisabledValidators = (); + type EpochChangeTrigger = pallet_babe::SameAuthoritiesForever; // Enable session rotation by replacing with pallet_babe::ExternalTrigger + type DisabledValidators = Session; type WeightInfo = (); // not using actual as benchmark does not produce valid WeightInfo type MaxAuthorities = ConstU32<32>; type MaxNominators = ConstU32<0>; @@ -366,14 +367,46 @@ impl pallet_symmetric_key::Config for Runtime { type Preimages = Preimage; } +pub struct NeverEndSession; +impl pallet_session::ShouldEndSession for NeverEndSession { + fn should_end_session(_now: BlockNumber) -> bool { + false + } +} + +impl pallet_session::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ValidatorId = ::AccountId; + type ValidatorIdOf = pallet_validator_set::ValidatorOf; + type ShouldEndSession = NeverEndSession; // Enable session rotation by replacing with Babe + type NextSessionRotation = Babe; + type SessionManager = ValidatorSet; + type SessionHandler = ::KeyTypeIdProviders; + type Keys = opaque::SessionKeys; + type WeightInfo = (); +} + +parameter_types! { + pub const MinAuthorities: u32 = 2; +} + +impl pallet_validator_set::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type AddRemoveOrigin = MoreThanHalfMembers; + type MinAuthorities = MinAuthorities; + type WeightInfo = pallet_validator_set::weights::SubstrateWeight; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { System: frame_system, Timestamp: pallet_timestamp, + Balances: pallet_balances, + ValidatorSet: pallet_validator_set, + Session: pallet_session, Babe: pallet_babe, Grandpa: pallet_grandpa, - Balances: pallet_balances, TransactionPaymentFree: pallet_transaction_payment_free, Sudo: pallet_sudo, UtxoNFT: pallet_utxo_nft, diff --git a/tools/lang/src/ast/parse.rs b/tools/lang/src/ast/parse.rs index 64334f7f..fea289b7 100644 --- a/tools/lang/src/ast/parse.rs +++ b/tools/lang/src/ast/parse.rs @@ -211,7 +211,7 @@ fn parse_type_cmp_type(pair: pest::iterators::Pair) -> Result(pair: pest::iterators::Pair<'a, Rule>) -> Result>, CompilationError> { +fn parse_ident_prop<'a>(pair: pest::iterators::Pair<'a, Rule>) -> Result>, CompilationError> { let span = pair.as_span(); match pair.as_rule() { Rule::ident_prop => {