From b070a3d618315555edb6b3de0e4ccfd11170b3ba Mon Sep 17 00:00:00 2001 From: PopcornPaws Date: Thu, 2 Mar 2023 10:35:53 +0100 Subject: [PATCH 01/17] Fixes `NOT compiled with try-runtime feature` error. --- gn-runtime/Cargo.toml | 5 +++-- gn-runtime/src/lib.rs | 2 -- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gn-runtime/Cargo.toml b/gn-runtime/Cargo.toml index da22ccb2..67723a6c 100644 --- a/gn-runtime/Cargo.toml +++ b/gn-runtime/Cargo.toml @@ -30,7 +30,7 @@ std = [ "frame-support/std", "frame-system/std", "frame-system-rpc-runtime-api/std", - "frame-try-runtime/std", + "frame-try-runtime?/std", "pallet-aura/std", "pallet-balances/std", "pallet-grandpa/std", @@ -57,6 +57,7 @@ std = [ ] try-runtime = [ "frame-executive/try-runtime", + "frame-support/try-runtime", "frame-system/try-runtime", "frame-try-runtime", "pallet-aura/try-runtime", @@ -89,7 +90,7 @@ frame-support = { workspace = true, optional = true } frame-system = { workspace = true, optional = true } frame-system-benchmarking = { workspace = true, optional = true } frame-system-rpc-runtime-api = { workspace = true, optional = true } -frame-try-runtime = { workspace = true, optional = true } +frame-try-runtime = { workspace = true, features = ["try-runtime"], optional = true } # substrate pallets pallet-aura = { workspace = true } diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 21ffdc49..673703ed 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -590,8 +590,6 @@ impl_runtime_apis! { } } - - #[cfg(feature = "try-runtime")] impl frame_try_runtime::TryRuntime for Runtime { fn on_runtime_upgrade(checks: frame_try_runtime::UpgradeCheckSelect) -> (Weight, Weight) { From 6030daf7972f02b6fc4b37b76e19018cafb5cb6a Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Mon, 6 Mar 2023 14:11:05 +0100 Subject: [PATCH 02/17] fix: storage migration for pallet-im-online --- gn-runtime/src/lib.rs | 101 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 2 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 673703ed..b14e595e 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -75,6 +75,15 @@ pub mod opaque { /// Opaque block identifier type. pub type BlockId = generic::BlockId; + // TODO Remove after im_online runtime upgrade is done. + impl_opaque_keys! { + pub struct OldSessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, + + } + } + impl_opaque_keys! { pub struct SessionKeys { pub aura: Aura, @@ -203,6 +212,74 @@ impl frame_system::Config for Runtime { impl pallet_randomness_collective_flip::Config for Runtime {} +impl frame_system::offchain::CreateSignedTransaction> for Runtime { + fn create_transaction>( + call: RuntimeCall, + public: ::Signer, + account: AccountId, + nonce: Index, + ) -> Option<( + RuntimeCall, + ::SignaturePayload, + )> { + let tip = 0; + 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 = generic::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(tip), + ); + 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 pallet_im_online::Config for Runtime { + type AuthorityId = ImOnlineId; + type RuntimeEvent = RuntimeEvent; + type NextSessionRotation = pallet_session::PeriodicSessions; + type ValidatorSet = ValidatorManager; + // type ReportUnresponsiveness = (); + type ReportUnresponsiveness = ValidatorManager; + type UnsignedPriority = ImOnlineUnsignedPriority; + type WeightInfo = pallet_im_online::weights::SubstrateWeight; + type MaxKeys = MaxKeys; + type MaxPeerInHeartbeats = MaxPeerInHeartbeats; + type MaxPeerDataEncodingSize = MaxPeerDataEncodingSize; +} + +impl frame_system::offchain::SendTransactionTypes> for Runtime { + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; +} + impl pallet_aura::Config for Runtime { type AuthorityId = AuraId; type DisabledValidators = (); @@ -299,6 +376,27 @@ impl pallet_validator_manager::Config for Runtime { type MinAuthorities = MinAuthorities; } +// should be removed along with UpgradeSessionKeys +fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { + let id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); + + opaque::SessionKeys { + grandpa: old.grandpa, + aura: old.aura, + im_online: id, + } +} + +// When this is removed, should also remove `OldSessionKeys`. +pub struct UpgradeSessionKeys; +impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + Session::upgrade_keys::(transform_session_keys); + Session::rotate_session(); + BlockWeights::get().max_block + } +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -354,8 +452,7 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, - // TODO remove this after migration - vm_upgrade::Upgrade, + UpgradeSessionKeys, >; // TODO remove this after migration (validator manager pallet) From 275a588c2a4cbe5a5ab6bc2b8c597dc361520725 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 7 Mar 2023 18:26:59 +0100 Subject: [PATCH 03/17] broken: Attempt at sending heartbeats --- gn-runtime/src/lib.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index b14e595e..97b8135a 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -24,7 +24,7 @@ use sp_runtime::{ Verify, }, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, MultiSignature, + ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic, SaturatedConversion, }; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -32,8 +32,10 @@ use sp_version::NativeVersion; use sp_version::RuntimeVersion; use frame_support::{ - construct_runtime, parameter_types, - traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem}, + construct_runtime, + pallet_prelude::TransactionPriority, + parameter_types, sp_io, + traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, ValidatorSet}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, IdentityFee, Weight, @@ -392,7 +394,28 @@ pub struct UpgradeSessionKeys; impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { fn on_runtime_upgrade() -> frame_support::weights::Weight { Session::upgrade_keys::(transform_session_keys); - Session::rotate_session(); + + let validators = Session::queued_keys() + .iter() + .map(|x| x.1.im_online.clone()) + .collect::>(); + + ImOnline::initialize_keys(validators.as_slice()); + + for (index, key) in ImOnline::keys().into_iter().enumerate() { + let heartbeat = pallet_im_online::Heartbeat { + block_number: System::block_number(), + network_state: sp_io::offchain::network_state().unwrap().clone(), + session_index: ValidatorManager::session_index(), + authority_index: index as u32, + validators_len: ImOnline::keys().len() as u32, + }; + + let signature = key.sign(&heartbeat.encode()).unwrap(); + ImOnline::heartbeat(RuntimeOrigin::root(), heartbeat, signature).unwrap(); + } + + // Session::rotate_session(); BlockWeights::get().max_block } } From 1ffefb6637f4cdf77a251e7d1735f5648c75eca0 Mon Sep 17 00:00:00 2001 From: PopcornPaws Date: Thu, 9 Mar 2023 12:15:21 +0100 Subject: [PATCH 04/17] Modified trait impls to resemble the `validator_set` docs more. --- gn-runtime/src/lib.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 97b8135a..f09c9c9b 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -214,7 +214,10 @@ impl frame_system::Config for Runtime { impl pallet_randomness_collective_flip::Config for Runtime {} -impl frame_system::offchain::CreateSignedTransaction> for Runtime { +impl frame_system::offchain::CreateSignedTransaction for Runtime +where + RuntimeCall: From, +{ fn create_transaction>( call: RuntimeCall, public: ::Signer, @@ -258,6 +261,14 @@ impl frame_system::offchain::CreateSignedTransaction frame_system::offchain::SendTransactionTypes for Runtime +where + RuntimeCall: From, +{ + type Extrinsic = UncheckedExtrinsic; + type OverarchingCall = RuntimeCall; +} + impl frame_system::offchain::SigningTypes for Runtime { type Public = ::Signer; type Signature = Signature; @@ -277,11 +288,6 @@ impl pallet_im_online::Config for Runtime { type MaxPeerDataEncodingSize = MaxPeerDataEncodingSize; } -impl frame_system::offchain::SendTransactionTypes> for Runtime { - type Extrinsic = UncheckedExtrinsic; - type OverarchingCall = RuntimeCall; -} - impl pallet_aura::Config for Runtime { type AuthorityId = AuraId; type DisabledValidators = (); From b45937e21bbaba87f0b14b72f4a75e476051875b Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Mon, 20 Mar 2023 13:27:05 +0100 Subject: [PATCH 05/17] broken: Added im-online pallet to pallets for debugging --- Cargo.toml | 1 + gn-node/Cargo.toml | 1 + gn-pallets/pallet-im-online/.cargo-ok | 1 + gn-pallets/pallet-im-online/Cargo.toml | 51 + gn-pallets/pallet-im-online/README.md | 59 + .../pallet-im-online/src/benchmarking.rs | 105 ++ gn-pallets/pallet-im-online/src/lib.rs | 1006 +++++++++++++++++ gn-pallets/pallet-im-online/src/mock.rs | 238 ++++ gn-pallets/pallet-im-online/src/tests.rs | 528 +++++++++ gn-pallets/pallet-im-online/src/weights.rs | 115 ++ gn-runtime/Cargo.toml | 1 + gn-runtime/src/lib.rs | 55 +- 12 files changed, 2135 insertions(+), 26 deletions(-) create mode 100644 gn-pallets/pallet-im-online/.cargo-ok create mode 100644 gn-pallets/pallet-im-online/Cargo.toml create mode 100644 gn-pallets/pallet-im-online/README.md create mode 100644 gn-pallets/pallet-im-online/src/benchmarking.rs create mode 100644 gn-pallets/pallet-im-online/src/lib.rs create mode 100644 gn-pallets/pallet-im-online/src/mock.rs create mode 100644 gn-pallets/pallet-im-online/src/tests.rs create mode 100644 gn-pallets/pallet-im-online/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index cd650b86..ef5aca2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ substrate-frame-rpc-system = { version = "15.0.0", default-features = false } pallet-aura = { version = "14.0.0", default-features = false } pallet-balances = { version = "15.0.0", default-features = false } pallet-grandpa = { version = "15.0.0", default-features = false } +# pallet-im-online = { version = "14.0.0", default-features = false } pallet-randomness-collective-flip = { version = "15.0.0", default-features = false } pallet-session = { version = "15.0.0", default-features = false } pallet-sudo = { version = "15.0.0", default-features = false } diff --git a/gn-node/Cargo.toml b/gn-node/Cargo.toml index 1fc2ec15..b1b04631 100644 --- a/gn-node/Cargo.toml +++ b/gn-node/Cargo.toml @@ -41,6 +41,7 @@ frame-system = { workspace = true, optional = true } substrate-frame-rpc-system = { workspace = true } # substrate pallets +pallet-im-online = { version = "14.0.0", path = "../gn-pallets/pallet-im-online", default-features = false } pallet-transaction-payment = { workspace = true, optional = true } pallet-transaction-payment-rpc = { workspace = true } diff --git a/gn-pallets/pallet-im-online/.cargo-ok b/gn-pallets/pallet-im-online/.cargo-ok new file mode 100644 index 00000000..b5754e20 --- /dev/null +++ b/gn-pallets/pallet-im-online/.cargo-ok @@ -0,0 +1 @@ +ok \ No newline at end of file diff --git a/gn-pallets/pallet-im-online/Cargo.toml b/gn-pallets/pallet-im-online/Cargo.toml new file mode 100644 index 00000000..d2f12a70 --- /dev/null +++ b/gn-pallets/pallet-im-online/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-im-online" +version = "14.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/substrate/" +description = "FRAME's I'm online pallet" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } +log = { version = "0.4.17", default-features = false } +scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } +frame-benchmarking = { version = "15.0.0", default-features = false, optional = true} +frame-support = { version = "15.0.0", default-features = false} +frame-system = { version = "15.0.0", default-features = false} +pallet-authorship = { version = "15.0.0", default-features = false} +sp-application-crypto = { version = "16.0.0", default-features = false} +sp-core = { version = "15.0.0", default-features = false} +sp-io = { version = "16.0.0", default-features = false} +sp-runtime = { version = "17.0.0", default-features = false} +sp-staking = { version = "13.0.0", default-features = false} +sp-std = { version = "6.0.0", default-features = false} + +[dev-dependencies] +pallet-session = { workspace = true } + +[features] +default = ["std"] +std = [ + "frame-benchmarking?/std", + "codec/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-authorship/std", + "scale-info/std", + "sp-application-crypto/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-staking/std", + "sp-std/std", +] +runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] +try-runtime = ["frame-support/try-runtime"] diff --git a/gn-pallets/pallet-im-online/README.md b/gn-pallets/pallet-im-online/README.md new file mode 100644 index 00000000..be11e0c4 --- /dev/null +++ b/gn-pallets/pallet-im-online/README.md @@ -0,0 +1,59 @@ +# I'm online Module + +If the local node is a validator (i.e. contains an authority key), this module +gossips a heartbeat transaction with each new session. The heartbeat functions +as a simple mechanism to signal that the node is online in the current era. + +Received heartbeats are tracked for one era and reset with each new era. The +module exposes two public functions to query if a heartbeat has been received +in the current era or session. + +The heartbeat is a signed transaction, which was signed using the session key +and includes the recent best block number of the local validators chain as well +as the `NetworkState`. +It is submitted as an Unsigned Transaction via off-chain workers. + +- [`im_online::Config`](https://docs.rs/pallet-im-online/latest/pallet_im_online/trait.Config.html) +- [`Call`](https://docs.rs/pallet-im-online/latest/pallet_im_online/enum.Call.html) +- [`Module`](https://docs.rs/pallet-im-online/latest/pallet_im_online/struct.Module.html) + +## Interface + +### Public Functions + +- `is_online` - True if the validator sent a heartbeat in the current session. + +## Usage + +```rust +use pallet_im_online::{self as im_online}; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + im_online::Config {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(0)] + pub fn is_online(origin: OriginFor, authority_index: u32) -> DispatchResult { + let _sender = ensure_signed(origin)?; + let _is_online = >::is_online(authority_index); + Ok(()) + } + } +} +``` + +## Dependencies + +This module depends on the [Session module](https://docs.rs/pallet-session/latest/pallet_session/). + +License: Apache-2.0 diff --git a/gn-pallets/pallet-im-online/src/benchmarking.rs b/gn-pallets/pallet-im-online/src/benchmarking.rs new file mode 100644 index 00000000..e4d1a646 --- /dev/null +++ b/gn-pallets/pallet-im-online/src/benchmarking.rs @@ -0,0 +1,105 @@ +// This file is part of Substrate. + +// Copyright (C) 2020-2022 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. + +//! I'm Online pallet benchmarking. + +#![cfg(feature = "runtime-benchmarks")] + +use super::*; + +use frame_benchmarking::v1::benchmarks; +use frame_support::{traits::UnfilteredDispatchable, WeakBoundedVec}; +use frame_system::RawOrigin; +use sp_core::{offchain::OpaqueMultiaddr, OpaquePeerId}; +use sp_runtime::{ + traits::{ValidateUnsigned, Zero}, + transaction_validity::TransactionSource, +}; + +use crate::Pallet as ImOnline; + +const MAX_KEYS: u32 = 1000; +const MAX_EXTERNAL_ADDRESSES: u32 = 100; + +pub fn create_heartbeat( + k: u32, + e: u32, +) -> Result< + (crate::Heartbeat, ::Signature), + &'static str, +> { + let mut keys = Vec::new(); + for _ in 0..k { + keys.push(T::AuthorityId::generate_pair(None)); + } + let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys.clone()) + .map_err(|()| "More than the maximum number of keys provided")?; + Keys::::put(bounded_keys); + + let network_state = OpaqueNetworkState { + peer_id: OpaquePeerId::default(), + external_addresses: vec![OpaqueMultiaddr::new(vec![0; 32]); e as usize], + }; + let input_heartbeat = Heartbeat { + block_number: T::BlockNumber::zero(), + network_state, + session_index: 0, + authority_index: k - 1, + validators_len: keys.len() as u32, + }; + + let encoded_heartbeat = input_heartbeat.encode(); + let authority_id = keys.get((k - 1) as usize).ok_or("out of range")?; + let signature = authority_id.sign(&encoded_heartbeat).ok_or("couldn't make signature")?; + + Ok((input_heartbeat, signature)) +} + +benchmarks! { + #[extra] + heartbeat { + let k in 1 .. MAX_KEYS; + let e in 1 .. MAX_EXTERNAL_ADDRESSES; + let (input_heartbeat, signature) = create_heartbeat::(k, e)?; + }: _(RawOrigin::None, input_heartbeat, signature) + + #[extra] + validate_unsigned { + let k in 1 .. MAX_KEYS; + let e in 1 .. MAX_EXTERNAL_ADDRESSES; + let (input_heartbeat, signature) = create_heartbeat::(k, e)?; + let call = Call::heartbeat { heartbeat: input_heartbeat, signature }; + }: { + ImOnline::::validate_unsigned(TransactionSource::InBlock, &call) + .map_err(<&str>::from)?; + } + + validate_unsigned_and_then_heartbeat { + let k in 1 .. MAX_KEYS; + let e in 1 .. MAX_EXTERNAL_ADDRESSES; + let (input_heartbeat, signature) = create_heartbeat::(k, e)?; + let call = Call::heartbeat { heartbeat: input_heartbeat, signature }; + let call_enc = call.encode(); + }: { + ImOnline::::validate_unsigned(TransactionSource::InBlock, &call).map_err(<&str>::from)?; + as Decode>::decode(&mut &*call_enc) + .expect("call is encoded above, encoding must be correct") + .dispatch_bypass_filter(RawOrigin::None.into())?; + } + + impl_benchmark_test_suite!(ImOnline, crate::mock::new_test_ext(), crate::mock::Runtime); +} diff --git a/gn-pallets/pallet-im-online/src/lib.rs b/gn-pallets/pallet-im-online/src/lib.rs new file mode 100644 index 00000000..41a4b211 --- /dev/null +++ b/gn-pallets/pallet-im-online/src/lib.rs @@ -0,0 +1,1006 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 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. + +//! # I'm online Pallet +//! +//! If the local node is a validator (i.e. contains an authority key), this pallet +//! gossips a heartbeat transaction with each new session. The heartbeat functions +//! as a simple mechanism to signal that the node is online in the current era. +//! +//! Received heartbeats are tracked for one era and reset with each new era. The +//! pallet exposes two public functions to query if a heartbeat has been received +//! in the current era or session. +//! +//! The heartbeat is a signed transaction, which was signed using the session key +//! and includes the recent best block number of the local validators chain as well +//! as the [NetworkState](../../client/offchain/struct.NetworkState.html). +//! It is submitted as an Unsigned Transaction via off-chain workers. +//! +//! - [`Config`] +//! - [`Call`] +//! - [`Pallet`] +//! +//! ## Interface +//! +//! ### Public Functions +//! +//! - `is_online` - True if the validator sent a heartbeat in the current session. +//! +//! ## Usage +//! +//! ``` +//! use pallet_im_online::{self as im_online}; +//! +//! #[frame_support::pallet] +//! pub mod pallet { +//! use super::*; +//! use frame_support::pallet_prelude::*; +//! use frame_system::pallet_prelude::*; +//! +//! #[pallet::pallet] +//! pub struct Pallet(_); +//! +//! #[pallet::config] +//! pub trait Config: frame_system::Config + im_online::Config {} +//! +//! #[pallet::call] +//! impl Pallet { +//! #[pallet::weight(0)] +//! pub fn is_online(origin: OriginFor, authority_index: u32) -> DispatchResult { +//! let _sender = ensure_signed(origin)?; +//! let _is_online = >::is_online(authority_index); +//! Ok(()) +//! } +//! } +//! } +//! # fn main() { } +//! ``` +//! +//! ## Dependencies +//! +//! This pallet depends on the [Session pallet](../pallet_session/index.html). + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +mod benchmarking; +mod mock; +mod tests; +pub mod weights; + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + traits::{ + EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet, + ValidatorSetWithIdentification, WrapperOpaque, + }, + BoundedSlice, WeakBoundedVec, +}; +use frame_system::offchain::{SendTransactionTypes, SubmitTransaction}; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_application_crypto::RuntimeAppPublic; +use sp_core::offchain::OpaqueNetworkState; +use sp_runtime::{ + offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, + traits::{AtLeast32BitUnsigned, Convert, Saturating, TrailingZeroInput}, + PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion, +}; +use sp_staking::{ + offence::{Kind, Offence, ReportOffence}, + SessionIndex, +}; +use sp_std::prelude::*; +pub use weights::WeightInfo; + +pub mod sr25519 { + mod app_sr25519 { + use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, sr25519}; + app_crypto!(sr25519, IM_ONLINE); + } + + sp_application_crypto::with_pair! { + /// An i'm online keypair using sr25519 as its crypto. + pub type AuthorityPair = app_sr25519::Pair; + } + + /// An i'm online signature using sr25519 as its crypto. + pub type AuthoritySignature = app_sr25519::Signature; + + /// An i'm online identifier using sr25519 as its crypto. + pub type AuthorityId = app_sr25519::Public; +} + +pub mod ed25519 { + mod app_ed25519 { + use sp_application_crypto::{app_crypto, ed25519, key_types::IM_ONLINE}; + app_crypto!(ed25519, IM_ONLINE); + } + + sp_application_crypto::with_pair! { + /// An i'm online keypair using ed25519 as its crypto. + pub type AuthorityPair = app_ed25519::Pair; + } + + /// An i'm online signature using ed25519 as its crypto. + pub type AuthoritySignature = app_ed25519::Signature; + + /// An i'm online identifier using ed25519 as its crypto. + pub type AuthorityId = app_ed25519::Public; +} + +const DB_PREFIX: &[u8] = b"parity/im-online-heartbeat/"; +/// How many blocks do we wait for heartbeat transaction to be included +/// before sending another one. +const INCLUDE_THRESHOLD: u32 = 3; + +/// Status of the offchain worker code. +/// +/// This stores the block number at which heartbeat was requested and when the worker +/// has actually managed to produce it. +/// Note we store such status for every `authority_index` separately. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +struct HeartbeatStatus { + /// An index of the session that we are supposed to send heartbeat for. + pub session_index: SessionIndex, + /// A block number at which the heartbeat for that session has been actually sent. + /// + /// It may be 0 in case the sending failed. In such case we should just retry + /// as soon as possible (i.e. in a worker running for the next block). + pub sent_at: BlockNumber, +} + +impl HeartbeatStatus { + /// Returns true if heartbeat has been recently sent. + /// + /// Parameters: + /// `session_index` - index of current session. + /// `now` - block at which the offchain worker is running. + /// + /// This function will return `true` iff: + /// 1. the session index is the same (we don't care if it went up or down) + /// 2. the heartbeat has been sent recently (within the threshold) + /// + /// The reasoning for 1. is that it's better to send an extra heartbeat than + /// to stall or not send one in case of a bug. + fn is_recent(&self, session_index: SessionIndex, now: BlockNumber) -> bool { + self.session_index == session_index && self.sent_at + INCLUDE_THRESHOLD.into() > now + } +} + +/// Error which may occur while executing the off-chain code. +#[cfg_attr(test, derive(PartialEq))] +enum OffchainErr { + TooEarly, + WaitingForInclusion(BlockNumber), + AlreadyOnline(u32), + FailedSigning, + FailedToAcquireLock, + NetworkState, + SubmitTransaction, +} + +impl sp_std::fmt::Debug for OffchainErr { + fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + match *self { + OffchainErr::TooEarly => write!(fmt, "Too early to send heartbeat."), + OffchainErr::WaitingForInclusion(ref block) => { + write!( + fmt, + "Heartbeat already sent at {:?}. Waiting for inclusion.", + block + ) + } + OffchainErr::AlreadyOnline(auth_idx) => { + write!(fmt, "Authority {} is already online", auth_idx) + } + OffchainErr::FailedSigning => write!(fmt, "Failed to sign heartbeat"), + OffchainErr::FailedToAcquireLock => write!(fmt, "Failed to acquire lock"), + OffchainErr::NetworkState => write!(fmt, "Failed to fetch network state"), + OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction"), + } + } +} + +pub type AuthIndex = u32; + +/// Heartbeat which is sent/received. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct Heartbeat +where + BlockNumber: PartialEq + Eq + Decode + Encode, +{ + /// Block number at the time heartbeat is created.. + pub block_number: BlockNumber, + /// A state of local network (peer id and external addresses) + pub network_state: OpaqueNetworkState, + /// Index of the current session. + pub session_index: SessionIndex, + /// An index of the authority on the list of validators. + pub authority_index: AuthIndex, + /// The length of session validator set + pub validators_len: u32, +} + +/// A type that is the same as [`OpaqueNetworkState`] but with [`Vec`] replaced with +/// [`WeakBoundedVec`] where Limit is the respective size limit +/// `PeerIdEncodingLimit` represents the size limit of the encoding of `PeerId` +/// `MultiAddrEncodingLimit` represents the size limit of the encoding of `MultiAddr` +/// `AddressesLimit` represents the size limit of the vector of peers connected +#[derive(Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo)] +#[codec(mel_bound())] +#[scale_info(skip_type_params(PeerIdEncodingLimit, MultiAddrEncodingLimit, AddressesLimit))] +pub struct BoundedOpaqueNetworkState +where + PeerIdEncodingLimit: Get, + MultiAddrEncodingLimit: Get, + AddressesLimit: Get, +{ + /// PeerId of the local node in SCALE encoded. + pub peer_id: WeakBoundedVec, + /// List of addresses the node knows it can be reached as. + pub external_addresses: + WeakBoundedVec, AddressesLimit>, +} + +impl, MultiAddrEncodingLimit: Get, AddressesLimit: Get> + BoundedOpaqueNetworkState +{ + fn force_from(ons: &OpaqueNetworkState) -> Self { + let peer_id = WeakBoundedVec::<_, PeerIdEncodingLimit>::force_from( + ons.peer_id.0.clone(), + Some( + "Warning: The size of the encoding of PeerId \ + is bigger than expected. A runtime configuration \ + adjustment may be needed.", + ), + ); + + let external_addresses = WeakBoundedVec::<_, AddressesLimit>::force_from( + ons.external_addresses + .iter() + .map(|x| { + WeakBoundedVec::<_, MultiAddrEncodingLimit>::force_from( + x.0.clone(), + Some( + "Warning: The size of the encoding of MultiAddr \ + is bigger than expected. A runtime configuration \ + adjustment may be needed.", + ), + ) + }) + .collect(), + Some( + "Warning: The network has more peers than expected \ + A runtime configuration adjustment may be needed.", + ), + ); + + Self { + peer_id, + external_addresses, + } + } +} + +/// A type for representing the validator id in a session. +pub type ValidatorId = <::ValidatorSet as ValidatorSet< + ::AccountId, +>>::ValidatorId; + +/// A tuple of (ValidatorId, Identification) where `Identification` is the full identification of +/// `ValidatorId`. +pub type IdentificationTuple = ( + ValidatorId, + <::ValidatorSet as ValidatorSetWithIdentification< + ::AccountId, + >>::Identification, +); + +type OffchainResult = Result::BlockNumber>>; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: SendTransactionTypes> + frame_system::Config { + /// The identifier type for an authority. + type AuthorityId: Member + + Parameter + + RuntimeAppPublic + + Ord + + MaybeSerializeDeserialize + + MaxEncodedLen; + + /// The maximum number of keys that can be added. + type MaxKeys: Get; + + /// The maximum number of peers to be stored in `ReceivedHeartbeats` + type MaxPeerInHeartbeats: Get; + + /// The maximum size of the encoding of `PeerId` and `MultiAddr` that are coming + /// from the hearbeat + type MaxPeerDataEncodingSize: Get; + + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// A type for retrieving the validators supposed to be online in a session. + type ValidatorSet: ValidatorSetWithIdentification; + + /// A trait that allows us to estimate the current session progress and also the + /// average session length. + /// + /// This parameter is used to determine the longevity of `heartbeat` transaction and a + /// rough time when we should start considering sending heartbeats, since the workers + /// avoids sending them at the very beginning of the session, assuming there is a + /// chance the authority will produce a block and they won't be necessary. + type NextSessionRotation: EstimateNextSessionRotation; + + /// A type that gives us the ability to submit unresponsiveness offence reports. + type ReportUnresponsiveness: ReportOffence< + Self::AccountId, + IdentificationTuple, + UnresponsivenessOffence>, + >; + + /// A configuration for base priority of unsigned transactions. + /// + /// This is exposed so that it can be tuned for particular runtime, when + /// multiple pallets send unsigned transactions. + #[pallet::constant] + type UnsignedPriority: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new heartbeat was received from `AuthorityId`. + HeartbeatReceived { authority_id: T::AuthorityId }, + /// At the end of the session, no offence was committed. + AllGood, + /// At the end of the session, at least one validator was found to be offline. + SomeOffline { + offline: Vec>, + }, + } + + #[pallet::error] + pub enum Error { + /// Non existent public key. + InvalidKey, + /// Duplicated heartbeat. + DuplicatedHeartbeat, + } + + /// The block number after which it's ok to send heartbeats in the current + /// session. + /// + /// At the beginning of each session we set this to a value that should fall + /// roughly in the middle of the session duration. The idea is to first wait for + /// the validators to produce a block in the current session, so that the + /// heartbeat later on will not be necessary. + /// + /// This value will only be used as a fallback if we fail to get a proper session + /// progress estimate from `NextSessionRotation`, as those estimates should be + /// more accurate then the value we calculate for `HeartbeatAfter`. + #[pallet::storage] + #[pallet::getter(fn heartbeat_after)] + pub(crate) type HeartbeatAfter = StorageValue<_, T::BlockNumber, ValueQuery>; + + /// The current set of keys that may issue a heartbeat. + #[pallet::storage] + #[pallet::getter(fn keys)] + pub(crate) type Keys = + StorageValue<_, WeakBoundedVec, ValueQuery>; + + /// For each session index, we keep a mapping of `SessionIndex` and `AuthIndex` to + /// `WrapperOpaque`. + #[pallet::storage] + #[pallet::getter(fn received_heartbeats)] + pub(crate) type ReceivedHeartbeats = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Twox64Concat, + AuthIndex, + WrapperOpaque< + BoundedOpaqueNetworkState< + T::MaxPeerDataEncodingSize, + T::MaxPeerDataEncodingSize, + T::MaxPeerInHeartbeats, + >, + >, + >; + + /// For each session index, we keep a mapping of `ValidatorId` to the + /// number of blocks authored by the given authority. + #[pallet::storage] + #[pallet::getter(fn authored_blocks)] + pub(crate) type AuthoredBlocks = StorageDoubleMap< + _, + Twox64Concat, + SessionIndex, + Twox64Concat, + ValidatorId, + u32, + ValueQuery, + >; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub keys: Vec, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + GenesisConfig { + keys: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + Pallet::::initialize_keys(&self.keys); + } + } + + #[pallet::call] + impl Pallet { + /// # + /// - Complexity: `O(K + E)` where K is length of `Keys` (heartbeat.validators_len) and E is + /// length of `heartbeat.network_state.external_address` + /// - `O(K)`: decoding of length `K` + /// - `O(E)`: decoding/encoding of length `E` + /// - DbReads: pallet_session `Validators`, pallet_session `CurrentIndex`, `Keys`, + /// `ReceivedHeartbeats` + /// - DbWrites: `ReceivedHeartbeats` + /// # + // NOTE: the weight includes the cost of validate_unsigned as it is part of the cost to + // import block with such an extrinsic. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::validate_unsigned_and_then_heartbeat( + heartbeat.validators_len as u32, + heartbeat.network_state.external_addresses.len() as u32, + ))] + pub fn heartbeat( + origin: OriginFor, + heartbeat: Heartbeat, + // since signature verification is done in `validate_unsigned` + // we can skip doing it here again. + _signature: ::Signature, + ) -> DispatchResult { + ensure_none(origin)?; + + let current_session = T::ValidatorSet::session_index(); + let exists = + ReceivedHeartbeats::::contains_key(¤t_session, &heartbeat.authority_index); + let keys = Keys::::get(); + let public = keys.get(heartbeat.authority_index as usize); + if let (false, Some(public)) = (exists, public) { + Self::deposit_event(Event::::HeartbeatReceived { + authority_id: public.clone(), + }); + + let network_state_bounded = BoundedOpaqueNetworkState::< + T::MaxPeerDataEncodingSize, + T::MaxPeerDataEncodingSize, + T::MaxPeerInHeartbeats, + >::force_from(&heartbeat.network_state); + ReceivedHeartbeats::::insert( + ¤t_session, + &heartbeat.authority_index, + WrapperOpaque::from(network_state_bounded), + ); + + Ok(()) + } else if exists { + Err(Error::::DuplicatedHeartbeat.into()) + } else { + Err(Error::::InvalidKey.into()) + } + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn offchain_worker(now: BlockNumberFor) { + // Only send messages if we are a potential validator. + if sp_io::offchain::is_validator() { + for res in Self::send_heartbeats(now).into_iter().flatten() { + if let Err(e) = res { + log::warn!( + target: "runtime::im-online", + "Skipping heartbeat at {:?}: {:?}", + now, + e, + ) + } + } + } else { + log::warn!( + target: "runtime::im-online", + "Skipping heartbeat at {:?}. Not a validator.", + now, + ) + } + } + } + + /// Invalid transaction custom error. Returned when validators_len field in heartbeat is + /// incorrect. + pub(crate) const INVALID_VALIDATORS_LEN: u8 = 10; + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { + if let Call::heartbeat { + heartbeat, + signature, + } = call + { + if >::is_online(heartbeat.authority_index) { + // we already received a heartbeat for this authority + return InvalidTransaction::Stale.into(); + } + + // check if session index from heartbeat is recent + let current_session = T::ValidatorSet::session_index(); + if heartbeat.session_index != current_session { + return InvalidTransaction::Stale.into(); + } + + // verify that the incoming (unverified) pubkey is actually an authority id + let keys = Keys::::get(); + if keys.len() as u32 != heartbeat.validators_len { + return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into(); + } + let authority_id = match keys.get(heartbeat.authority_index as usize) { + Some(id) => id, + None => return InvalidTransaction::BadProof.into(), + }; + + // check signature (this is expensive so we do it last). + let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| { + authority_id.verify(&encoded_heartbeat, signature) + }); + + if !signature_valid { + return InvalidTransaction::BadProof.into(); + } + + ValidTransaction::with_tag_prefix("ImOnline") + .priority(T::UnsignedPriority::get()) + .and_provides((current_session, authority_id)) + .longevity( + TryInto::::try_into( + T::NextSessionRotation::average_session_length() / 2u32.into(), + ) + .unwrap_or(64_u64), + ) + .propagate(true) + .build() + } else { + InvalidTransaction::Call.into() + } + } + } +} + +/// 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, T::BlockNumber> for Pallet +{ + fn note_author(author: ValidatorId) { + Self::note_authorship(author); + } +} + +impl Pallet { + /// Returns `true` if a heartbeat has been received for the authority at + /// `authority_index` in the authorities series or if the authority has + /// authored at least one block, during the current session. Otherwise + /// `false`. + pub fn is_online(authority_index: AuthIndex) -> bool { + let current_validators = T::ValidatorSet::validators(); + + if authority_index >= current_validators.len() as u32 { + return false; + } + + let authority = ¤t_validators[authority_index as usize]; + + Self::is_online_aux(authority_index, authority) + } + + fn is_online_aux(authority_index: AuthIndex, authority: &ValidatorId) -> bool { + let current_session = T::ValidatorSet::session_index(); + + ReceivedHeartbeats::::contains_key(¤t_session, &authority_index) + || AuthoredBlocks::::get(¤t_session, authority) != 0 + } + + /// Returns `true` if a heartbeat has been received for the authority at `authority_index` in + /// the authorities series, during the current session. Otherwise `false`. + pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool { + let current_session = T::ValidatorSet::session_index(); + ReceivedHeartbeats::::contains_key(¤t_session, &authority_index) + } + + /// Note that the given authority has authored a block in the current session. + fn note_authorship(author: ValidatorId) { + let current_session = T::ValidatorSet::session_index(); + + AuthoredBlocks::::mutate(¤t_session, author, |authored| *authored += 1); + } + + pub(crate) fn send_heartbeats( + block_number: T::BlockNumber, + ) -> OffchainResult>> { + const START_HEARTBEAT_RANDOM_PERIOD: Permill = Permill::from_percent(10); + const START_HEARTBEAT_FINAL_PERIOD: Permill = Permill::from_percent(80); + + // this should give us a residual probability of 1/SESSION_LENGTH of sending an heartbeat, + // i.e. all heartbeats spread uniformly, over most of the session. as the session progresses + // the probability of sending an heartbeat starts to increase exponentially. + let random_choice = |progress: Permill| { + // given session progress `p` and session length `l` + // the threshold formula is: p^6 + 1/l + let session_length = T::NextSessionRotation::average_session_length(); + let residual = Permill::from_rational(1u32, session_length.saturated_into()); + let threshold: Permill = progress.saturating_pow(6).saturating_add(residual); + + let seed = sp_io::offchain::random_seed(); + let random = ::decode(&mut TrailingZeroInput::new(seed.as_ref())) + .expect("input is padded with zeroes; qed"); + let random = Permill::from_parts(random % Permill::ACCURACY); + + random <= threshold + }; + + let should_heartbeat = if let (Some(progress), _) = + T::NextSessionRotation::estimate_current_session_progress(block_number) + { + // we try to get an estimate of the current session progress first since it should + // provide more accurate results. we will start an early heartbeat period where we'll + // randomly pick whether to heartbeat. after 80% of the session has elapsed, if we + // haven't sent an heartbeat yet we'll send one unconditionally. the idea is to prevent + // all nodes from sending the heartbeats at the same block and causing a temporary (but + // deterministic) spike in transactions. + progress >= START_HEARTBEAT_FINAL_PERIOD + || progress >= START_HEARTBEAT_RANDOM_PERIOD && random_choice(progress) + } else { + // otherwise we fallback to using the block number calculated at the beginning + // of the session that should roughly correspond to the middle of the session + let heartbeat_after = >::get(); + block_number >= heartbeat_after + }; + + if !should_heartbeat { + return Err(OffchainErr::TooEarly); + } + + let session_index = T::ValidatorSet::session_index(); + let validators_len = Keys::::decode_len().unwrap_or_default() as u32; + + Ok( + Self::local_authority_keys().map(move |(authority_index, key)| { + Self::send_single_heartbeat( + authority_index, + key, + session_index, + block_number, + validators_len, + ) + }), + ) + } + + fn send_single_heartbeat( + authority_index: u32, + key: T::AuthorityId, + session_index: SessionIndex, + block_number: T::BlockNumber, + validators_len: u32, + ) -> OffchainResult { + // A helper function to prepare heartbeat call. + let prepare_heartbeat = || -> OffchainResult> { + let network_state = + sp_io::offchain::network_state().map_err(|_| OffchainErr::NetworkState)?; + let heartbeat = Heartbeat { + block_number, + network_state, + session_index, + authority_index, + validators_len, + }; + + let signature = key + .sign(&heartbeat.encode()) + .ok_or(OffchainErr::FailedSigning)?; + + Ok(Call::heartbeat { + heartbeat, + signature, + }) + }; + + if Self::is_online(authority_index) { + return Err(OffchainErr::AlreadyOnline(authority_index)); + } + + // acquire lock for that authority at current heartbeat to make sure we don't + // send concurrent heartbeats. + Self::with_heartbeat_lock(authority_index, session_index, block_number, || { + let call = prepare_heartbeat()?; + log::warn!( + target: "runtime::im-online", + "[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}", + authority_index, + block_number, + session_index, + call, + ); + + SubmitTransaction::>::submit_unsigned_transaction(call.into()) + .map_err(|_| OffchainErr::SubmitTransaction)?; + + Ok(()) + }) + } + + fn local_authority_keys() -> impl Iterator { + // on-chain storage + // + // At index `idx`: + // 1. A (ImOnline) public key to be used by a validator at index `idx` to send im-online + // heartbeats. + let authorities = Keys::::get(); + + // local keystore + // + // All `ImOnline` public (+private) keys currently in the local keystore. + let mut local_keys = T::AuthorityId::all(); + + local_keys.sort(); + + authorities + .into_iter() + .enumerate() + .filter_map(move |(index, authority)| { + local_keys + .binary_search(&authority) + .ok() + .map(|location| (index as u32, local_keys[location].clone())) + }) + } + + fn with_heartbeat_lock( + authority_index: u32, + session_index: SessionIndex, + now: T::BlockNumber, + f: impl FnOnce() -> OffchainResult, + ) -> OffchainResult { + let key = { + let mut key = DB_PREFIX.to_vec(); + key.extend(authority_index.encode()); + key + }; + let storage = StorageValueRef::persistent(&key); + let res = storage.mutate( + |status: Result>, StorageRetrievalError>| { + // Check if there is already a lock for that particular block. + // This means that the heartbeat has already been sent, and we are just waiting + // for it to be included. However if it doesn't get included for INCLUDE_THRESHOLD + // we will re-send it. + match status { + // we are still waiting for inclusion. + Ok(Some(status)) if status.is_recent(session_index, now) => { + Err(OffchainErr::WaitingForInclusion(status.sent_at)) + } + // attempt to set new status + _ => Ok(HeartbeatStatus { + session_index, + sent_at: now, + }), + } + }, + ); + if let Err(MutateStorageError::ValueFunctionFailed(err)) = res { + return Err(err); + } + + let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?; + + // we got the lock, let's try to send the heartbeat. + let res = f(); + + // clear the lock in case we have failed to send transaction. + if res.is_err() { + new_status.sent_at = 0u32.into(); + storage.set(&new_status); + } + + res + } + + fn initialize_keys(keys: &[T::AuthorityId]) { + if !keys.is_empty() { + assert!(Keys::::get().is_empty(), "Keys are already initialized!"); + let bounded_keys = >::try_from(keys) + .expect("More than the maximum number of keys provided"); + Keys::::put(bounded_keys); + } + } + + #[cfg(test)] + fn set_keys(keys: Vec) { + let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys) + .expect("More than the maximum number of keys provided"); + Keys::::put(bounded_keys); + } +} + +impl sp_runtime::BoundToRuntimeAppPublic for Pallet { + type Public = T::AuthorityId; +} + +impl OneSessionHandler for Pallet { + type Key = T::AuthorityId; + + fn on_genesis_session<'a, I: 'a>(validators: I) + where + I: Iterator, + { + let keys = validators.map(|x| x.1).collect::>(); + Self::initialize_keys(&keys); + } + + fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I) + where + I: Iterator, + { + // Tell the offchain worker to start making the next session's heartbeats. + // Since we consider producing blocks as being online, + // the heartbeat is deferred a bit to prevent spamming. + let block_number = >::block_number(); + let half_session = T::NextSessionRotation::average_session_length() / 2u32.into(); + >::put(block_number + half_session); + + // Remember who the authorities are for the new session. + let keys = validators.map(|x| x.1).collect::>(); + let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::force_from( + keys, + Some( + "Warning: The session has more keys than expected. \ + A runtime configuration adjustment may be needed.", + ), + ); + Keys::::put(bounded_keys); + } + + fn on_before_session_ending() { + let session_index = T::ValidatorSet::session_index(); + let keys = Keys::::get(); + let current_validators = T::ValidatorSet::validators(); + + let offenders = current_validators + .into_iter() + .enumerate() + .filter(|(index, id)| !Self::is_online_aux(*index as u32, id)) + .filter_map(|(_, id)| { + >::IdentificationOf::convert( + id.clone() + ).map(|full_id| (id, full_id)) + }) + .collect::>>(); + + // Remove all received heartbeats and number of authored blocks from the + // current session, they have already been processed and won't be needed + // anymore. + #[allow(deprecated)] + ReceivedHeartbeats::::remove_prefix(&T::ValidatorSet::session_index(), None); + #[allow(deprecated)] + AuthoredBlocks::::remove_prefix(&T::ValidatorSet::session_index(), None); + + if offenders.is_empty() { + Self::deposit_event(Event::::AllGood); + } else { + Self::deposit_event(Event::::SomeOffline { + offline: offenders.clone(), + }); + + let validator_set_count = keys.len() as u32; + let offence = UnresponsivenessOffence { + session_index, + validator_set_count, + offenders, + }; + if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) { + sp_runtime::print(e); + } + } + } + + fn on_disabled(_i: u32) { + // ignore + } +} + +/// An offence that is filed if a validator didn't send a heartbeat message. +#[derive(RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +pub struct UnresponsivenessOffence { + /// The current session index in which we report the unresponsive validators. + /// + /// It acts as a time measure for unresponsiveness reports and effectively will always point + /// at the end of the session. + pub session_index: SessionIndex, + /// The size of the validator set in current session/era. + pub validator_set_count: u32, + /// Authorities that were unresponsive during the current era. + pub offenders: Vec, +} + +impl Offence for UnresponsivenessOffence { + const ID: Kind = *b"im-online:offlin"; + type TimeSlot = SessionIndex; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.session_index + } + + fn slash_fraction(&self, offenders: u32) -> Perbill { + // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 + // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% + // when 13/30 are offline (around 5% when 1/3 are offline). + if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) { + let x = Perbill::from_rational(3 * threshold, self.validator_set_count); + x.saturating_mul(Perbill::from_percent(7)) + } else { + Perbill::default() + } + } +} diff --git a/gn-pallets/pallet-im-online/src/mock.rs b/gn-pallets/pallet-im-online/src/mock.rs new file mode 100644 index 00000000..783e68df --- /dev/null +++ b/gn-pallets/pallet-im-online/src/mock.rs @@ -0,0 +1,238 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 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. + +//! Test utilities + +#![cfg(test)] + +use frame_support::{ + parameter_types, + traits::{ConstU32, ConstU64}, + weights::Weight, +}; +use pallet_session::historical as pallet_session_historical; +use sp_core::H256; +use sp_runtime::{ + testing::{Header, TestXt, UintAuthorityId}, + traits::{BlakeTwo256, ConvertInto, IdentityLookup}, + Permill, +}; +use sp_staking::{ + offence::{OffenceError, ReportOffence}, + SessionIndex, +}; + +use crate as imonline; +use crate::Config; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Session: pallet_session::{Pallet, Call, Storage, Event, Config}, + ImOnline: imonline::{Pallet, Call, Storage, Config, Event}, + Historical: pallet_session_historical::{Pallet}, + } +); + +parameter_types! { + pub static Validators: Option> = Some(vec![ + 1, + 2, + 3, + ]); +} + +pub struct TestSessionManager; +impl pallet_session::SessionManager for TestSessionManager { + fn new_session(_new_index: SessionIndex) -> Option> { + Validators::mutate(|l| l.take()) + } + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} +} + +impl pallet_session::historical::SessionManager for TestSessionManager { + fn new_session(_new_index: SessionIndex) -> Option> { + Validators::mutate(|l| { + l.take().map(|validators| validators.iter().map(|v| (*v, *v)).collect()) + }) + } + fn end_session(_: SessionIndex) {} + fn start_session(_: SessionIndex) {} +} + +/// An extrinsic type used for tests. +pub type Extrinsic = TestXt; +type IdentificationTuple = (u64, u64); +type Offence = crate::UnresponsivenessOffence; + +parameter_types! { + pub static Offences: Vec<(Vec, Offence)> = vec![]; +} + +/// A mock offence report handler. +pub struct OffenceHandler; +impl ReportOffence for OffenceHandler { + fn report_offence(reporters: Vec, offence: Offence) -> Result<(), OffenceError> { + Offences::mutate(|l| l.push((reporters, offence))); + Ok(()) + } + + fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &SessionIndex) -> bool { + false + } +} + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); + let mut result: sp_io::TestExternalities = t.into(); + // Set the default keys, otherwise session will discard the validator. + result.execute_with(|| { + for i in 1..=6 { + System::inc_providers(&i); + assert_eq!(Session::set_keys(RuntimeOrigin::signed(i), (i - 1).into(), vec![]), Ok(())); + } + }); + result +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type BlockNumber = u64; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +parameter_types! { + pub const Period: u64 = 1; + pub const Offset: u64 = 0; +} + +impl pallet_session::Config for Runtime { + type ShouldEndSession = pallet_session::PeriodicSessions; + type SessionManager = + pallet_session::historical::NoteHistoricalRoot; + type SessionHandler = (ImOnline,); + type ValidatorId = u64; + type ValidatorIdOf = ConvertInto; + type Keys = UintAuthorityId; + type RuntimeEvent = RuntimeEvent; + type NextSessionRotation = pallet_session::PeriodicSessions; + type WeightInfo = (); +} + +impl pallet_session::historical::Config for Runtime { + type FullIdentification = u64; + type FullIdentificationOf = ConvertInto; +} + +impl pallet_authorship::Config for Runtime { + type FindAuthor = (); + type EventHandler = ImOnline; +} + +parameter_types! { + pub static MockCurrentSessionProgress: Option> = None; +} + +parameter_types! { + pub static MockAverageSessionLength: Option = None; +} + +pub struct TestNextSessionRotation; + +impl frame_support::traits::EstimateNextSessionRotation for TestNextSessionRotation { + fn average_session_length() -> u64 { + // take the mock result if any and return it + let mock = MockAverageSessionLength::mutate(|p| p.take()); + + mock.unwrap_or(pallet_session::PeriodicSessions::::average_session_length()) + } + + fn estimate_current_session_progress(now: u64) -> (Option, Weight) { + let (estimate, weight) = + pallet_session::PeriodicSessions::::estimate_current_session_progress( + now, + ); + + // take the mock result if any and return it + let mock = MockCurrentSessionProgress::mutate(|p| p.take()); + + (mock.unwrap_or(estimate), weight) + } + + fn estimate_next_session_rotation(now: u64) -> (Option, Weight) { + pallet_session::PeriodicSessions::::estimate_next_session_rotation(now) + } +} + +impl Config for Runtime { + type AuthorityId = UintAuthorityId; + type RuntimeEvent = RuntimeEvent; + type ValidatorSet = Historical; + type NextSessionRotation = TestNextSessionRotation; + type ReportUnresponsiveness = OffenceHandler; + type UnsignedPriority = ConstU64<{ 1 << 20 }>; + type WeightInfo = (); + type MaxKeys = ConstU32<10_000>; + type MaxPeerInHeartbeats = ConstU32<10_000>; + type MaxPeerDataEncodingSize = ConstU32<1_000>; +} + +impl frame_system::offchain::SendTransactionTypes for Runtime +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +pub fn advance_session() { + let now = System::block_number().max(1); + System::set_block_number(now + 1); + Session::rotate_session(); + let keys = Session::validators().into_iter().map(UintAuthorityId).collect(); + ImOnline::set_keys(keys); + assert_eq!(Session::current_index(), (now / Period::get()) as u32); +} diff --git a/gn-pallets/pallet-im-online/src/tests.rs b/gn-pallets/pallet-im-online/src/tests.rs new file mode 100644 index 00000000..2c026f71 --- /dev/null +++ b/gn-pallets/pallet-im-online/src/tests.rs @@ -0,0 +1,528 @@ +// This file is part of Substrate. + +// Copyright (C) 2019-2022 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. + +//! Tests for the im-online module. + +#![cfg(test)] + +use super::*; +use crate::mock::*; +use frame_support::{assert_noop, dispatch}; +use sp_core::{ + offchain::{ + testing::{TestOffchainExt, TestTransactionPoolExt}, + OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, + }, + OpaquePeerId, +}; +use sp_runtime::{ + testing::UintAuthorityId, + transaction_validity::{InvalidTransaction, TransactionValidityError}, +}; + +#[test] +fn test_unresponsiveness_slash_fraction() { + let dummy_offence = + UnresponsivenessOffence { session_index: 0, validator_set_count: 50, offenders: vec![()] }; + // A single case of unresponsiveness is not slashed. + assert_eq!(dummy_offence.slash_fraction(1), Perbill::zero()); + + assert_eq!( + dummy_offence.slash_fraction(5), + Perbill::zero(), // 0% + ); + + assert_eq!( + dummy_offence.slash_fraction(7), + Perbill::from_parts(4200000), // 0.42% + ); + + // One third offline should be punished around 5%. + assert_eq!( + dummy_offence.slash_fraction(17), + Perbill::from_parts(46200000), // 4.62% + ); +} + +#[test] +fn should_report_offline_validators() { + new_test_ext().execute_with(|| { + // given + let block = 1; + System::set_block_number(block); + // buffer new validators + advance_session(); + // enact the change and buffer another one + let validators = vec![1, 2, 3, 4, 5, 6]; + Validators::mutate(|l| *l = Some(validators.clone())); + advance_session(); + + // when + // we end current session and start the next one + advance_session(); + + // then + let offences = Offences::take(); + assert_eq!( + offences, + vec![( + vec![], + UnresponsivenessOffence { + session_index: 2, + validator_set_count: 3, + offenders: vec![(1, 1), (2, 2), (3, 3),], + } + )] + ); + + // should not report when heartbeat is sent + for (idx, v) in validators.into_iter().take(4).enumerate() { + let _ = heartbeat(block, 3, idx as u32, v.into(), Session::validators()).unwrap(); + } + advance_session(); + + // then + let offences = Offences::take(); + assert_eq!( + offences, + vec![( + vec![], + UnresponsivenessOffence { + session_index: 3, + validator_set_count: 6, + offenders: vec![(5, 5), (6, 6),], + } + )] + ); + }); +} + +fn heartbeat( + block_number: u64, + session_index: u32, + authority_index: u32, + id: UintAuthorityId, + validators: Vec, +) -> dispatch::DispatchResult { + use frame_support::unsigned::ValidateUnsigned; + + let heartbeat = Heartbeat { + block_number, + network_state: OpaqueNetworkState { + peer_id: OpaquePeerId(vec![1]), + external_addresses: vec![], + }, + session_index, + authority_index, + validators_len: validators.len() as u32, + }; + let signature = id.sign(&heartbeat.encode()).unwrap(); + + ImOnline::pre_dispatch(&crate::Call::heartbeat { + heartbeat: heartbeat.clone(), + signature: signature.clone(), + }) + .map_err(|e| match e { + TransactionValidityError::Invalid(InvalidTransaction::Custom(INVALID_VALIDATORS_LEN)) => + "invalid validators len", + e @ _ => <&'static str>::from(e), + })?; + ImOnline::heartbeat(RuntimeOrigin::none(), heartbeat, signature) +} + +#[test] +fn should_mark_online_validator_when_heartbeat_is_received() { + new_test_ext().execute_with(|| { + advance_session(); + // given + Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + assert!(!ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + + // when + let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap(); + + // then + assert!(ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + + // and when + let _ = heartbeat(1, 2, 2, 3.into(), Session::validators()).unwrap(); + + // then + assert!(ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(ImOnline::is_online(2)); + }); +} + +#[test] +fn late_heartbeat_and_invalid_keys_len_should_fail() { + new_test_ext().execute_with(|| { + advance_session(); + // given + Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + // when + assert_noop!( + heartbeat(1, 3, 0, 1.into(), Session::validators()), + "Transaction is outdated" + ); + assert_noop!( + heartbeat(1, 1, 0, 1.into(), Session::validators()), + "Transaction is outdated" + ); + + // invalid validators_len + assert_noop!(heartbeat(1, 2, 0, 1.into(), vec![]), "invalid validators len"); + }); +} + +#[test] +fn should_generate_heartbeats() { + use frame_support::traits::OffchainWorker; + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + // given + let block = 1; + System::set_block_number(block); + UintAuthorityId::set_all_keys(vec![0, 1, 2]); + // buffer new validators + Session::rotate_session(); + // enact the change and buffer another one + Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); + Session::rotate_session(); + + // when + ImOnline::offchain_worker(block); + + // then + let transaction = state.write().transactions.pop().unwrap(); + // All validators have `0` as their session key, so we generate 2 transactions. + assert_eq!(state.read().transactions.len(), 2); + + // check stuff about the transaction. + let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); + let heartbeat = match ex.call { + crate::mock::RuntimeCall::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => + heartbeat, + e => panic!("Unexpected call: {:?}", e), + }; + + assert_eq!( + heartbeat, + Heartbeat { + block_number: block, + network_state: sp_io::offchain::network_state().unwrap(), + session_index: 2, + authority_index: 2, + validators_len: 3, + } + ); + }); +} + +#[test] +fn should_cleanup_received_heartbeats_on_session_end() { + new_test_ext().execute_with(|| { + advance_session(); + + Validators::mutate(|l| *l = Some(vec![1, 2, 3])); + assert_eq!(Session::validators(), Vec::::new()); + + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + // send an heartbeat from authority id 0 at session 2 + let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap(); + + // the heartbeat is stored + assert!(!ImOnline::received_heartbeats(&2, &0).is_none()); + + advance_session(); + + // after the session has ended we have already processed the heartbeat + // message, so any messages received on the previous session should have + // been pruned. + assert!(ImOnline::received_heartbeats(&2, &0).is_none()); + }); +} + +#[test] +fn should_mark_online_validator_when_block_is_authored() { + use pallet_authorship::EventHandler; + + new_test_ext().execute_with(|| { + advance_session(); + // given + Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + + for i in 0..3 { + assert!(!ImOnline::is_online(i)); + } + + // when + ImOnline::note_author(1); + + // then + assert!(ImOnline::is_online(0)); + assert!(!ImOnline::is_online(1)); + assert!(!ImOnline::is_online(2)); + }); +} + +#[test] +fn should_not_send_a_report_if_already_online() { + use pallet_authorship::EventHandler; + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, pool_state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + advance_session(); + // given + Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); + assert_eq!(Session::validators(), Vec::::new()); + // enact the change and buffer another one + advance_session(); + assert_eq!(Session::current_index(), 2); + assert_eq!(Session::validators(), vec![1, 2, 3]); + ImOnline::note_author(2); + ImOnline::note_author(3); + + // when + UintAuthorityId::set_all_keys(vec![1, 2, 3]); + // we expect error, since the authority is already online. + let mut res = ImOnline::send_heartbeats(4).unwrap(); + res.next().unwrap().unwrap(); + assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(1)); + assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(2)); + assert_eq!(res.next(), None); + + // then + let transaction = pool_state.write().transactions.pop().unwrap(); + // All validators have `0` as their session key, but we should only produce 1 heartbeat. + assert_eq!(pool_state.read().transactions.len(), 0); + // check stuff about the transaction. + let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); + let heartbeat = match ex.call { + crate::mock::RuntimeCall::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => + heartbeat, + e => panic!("Unexpected call: {:?}", e), + }; + + assert_eq!( + heartbeat, + Heartbeat { + block_number: 4, + network_state: sp_io::offchain::network_state().unwrap(), + session_index: 2, + authority_index: 0, + validators_len: 3, + } + ); + }); +} + +#[test] +fn should_handle_missing_progress_estimates() { + use frame_support::traits::OffchainWorker; + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, state) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + let block = 1; + + System::set_block_number(block); + UintAuthorityId::set_all_keys(vec![0, 1, 2]); + + // buffer new validators + Session::rotate_session(); + + // enact the change and buffer another one + Validators::mutate(|l| *l = Some(vec![0, 1, 2])); + Session::rotate_session(); + + // we will return `None` on the next call to `estimate_current_session_progress` + // and the offchain worker should fallback to checking `HeartbeatAfter` + MockCurrentSessionProgress::mutate(|p| *p = Some(None)); + ImOnline::offchain_worker(block); + + assert_eq!(state.read().transactions.len(), 3); + }); +} + +#[test] +fn should_handle_non_linear_session_progress() { + // NOTE: this is the reason why we started using `EstimateNextSessionRotation` to figure out if + // we should send a heartbeat, it's possible that between successive blocks we progress through + // the session more than just one block increment (in BABE session length is defined in slots, + // not block numbers). + + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, _) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + UintAuthorityId::set_all_keys(vec![0, 1, 2]); + + // buffer new validator + Session::rotate_session(); + + // mock the session length as being 10 blocks long, + // enact the change and buffer another one + Validators::mutate(|l| *l = Some(vec![0, 1, 2])); + + // mock the session length has being 10 which should make us assume the fallback for half + // session will be reached by block 5. + MockAverageSessionLength::mutate(|p| *p = Some(10)); + + Session::rotate_session(); + + // if we don't have valid results for the current session progres then + // we'll fallback to `HeartbeatAfter` and only heartbeat on block 5. + MockCurrentSessionProgress::mutate(|p| *p = Some(None)); + assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); + + MockCurrentSessionProgress::mutate(|p| *p = Some(None)); + assert!(ImOnline::send_heartbeats(5).ok().is_some()); + + // if we have a valid current session progress then we'll heartbeat as soon + // as we're past 80% of the session regardless of the block number + MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_percent(81)))); + + assert!(ImOnline::send_heartbeats(2).ok().is_some()); + }); +} + +#[test] +fn test_does_not_heartbeat_early_in_the_session() { + let mut ext = new_test_ext(); + let (offchain, _state) = TestOffchainExt::new(); + let (pool, _) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + // mock current session progress as being 5%. we only randomly start + // heartbeating after 10% of the session has elapsed. + MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_float(0.05)))); + assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); + }); +} + +#[test] +fn test_probability_of_heartbeating_increases_with_session_progress() { + let mut ext = new_test_ext(); + let (offchain, state) = TestOffchainExt::new(); + let (pool, _) = TestTransactionPoolExt::new(); + ext.register_extension(OffchainDbExt::new(offchain.clone())); + ext.register_extension(OffchainWorkerExt::new(offchain)); + ext.register_extension(TransactionPoolExt::new(pool)); + + ext.execute_with(|| { + let set_test = |progress, random: f64| { + // the average session length is 100 blocks, therefore the residual + // probability of sending a heartbeat is 1% + MockAverageSessionLength::mutate(|p| *p = Some(100)); + MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_float(progress)))); + + let mut seed = [0u8; 32]; + let encoded = ((random * Permill::ACCURACY as f64) as u32).encode(); + seed[0..4].copy_from_slice(&encoded); + state.write().seed = seed; + }; + + let assert_too_early = |progress, random| { + set_test(progress, random); + assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); + }; + + let assert_heartbeat_ok = |progress, random| { + set_test(progress, random); + assert!(ImOnline::send_heartbeats(2).ok().is_some()); + }; + + assert_too_early(0.05, 1.0); + + assert_too_early(0.1, 0.1); + assert_too_early(0.1, 0.011); + assert_heartbeat_ok(0.1, 0.010); + + assert_too_early(0.4, 0.015); + assert_heartbeat_ok(0.4, 0.014); + + assert_too_early(0.5, 0.026); + assert_heartbeat_ok(0.5, 0.025); + + assert_too_early(0.6, 0.057); + assert_heartbeat_ok(0.6, 0.056); + + assert_too_early(0.65, 0.086); + assert_heartbeat_ok(0.65, 0.085); + + assert_too_early(0.7, 0.13); + assert_heartbeat_ok(0.7, 0.12); + + assert_too_early(0.75, 0.19); + assert_heartbeat_ok(0.75, 0.18); + }); +} diff --git a/gn-pallets/pallet-im-online/src/weights.rs b/gn-pallets/pallet-im-online/src/weights.rs new file mode 100644 index 00000000..4ea275a3 --- /dev/null +++ b/gn-pallets/pallet-im-online/src/weights.rs @@ -0,0 +1,115 @@ +// This file is part of Substrate. + +// Copyright (C) 2023 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. + +//! Autogenerated weights for pallet_im_online +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-01-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 + +// Executed Command: +// ./target/production/substrate +// benchmark +// pallet +// --chain=dev +// --steps=50 +// --repeat=20 +// --pallet=pallet_im_online +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --heap-pages=4096 +// --output=./frame/im-online/src/weights.rs +// --header=./HEADER-APACHE2 +// --template=./.maintain/frame-weight-template.hbs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_im_online. +pub trait WeightInfo { + fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight; +} + +/// Weights for pallet_im_online using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: Session Validators (r:1 w:0) + /// Proof Skipped: Session Validators (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Session CurrentIndex (r:1 w:0) + /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ImOnline Keys (r:1 w:0) + /// Proof: ImOnline Keys (max_values: Some(1), max_size: Some(320002), added: 320497, mode: MaxEncodedLen) + /// Storage: ImOnline ReceivedHeartbeats (r:1 w:1) + /// Proof: ImOnline ReceivedHeartbeats (max_values: None, max_size: Some(10021032), added: 10023507, mode: MaxEncodedLen) + /// Storage: ImOnline AuthoredBlocks (r:1 w:0) + /// Proof: ImOnline AuthoredBlocks (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// The range of component `k` is `[1, 1000]`. + /// The range of component `e` is `[1, 100]`. + fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `359 + k * (32 ±0)` + // Estimated: `10345712 + e * (25 ±0) + k * (64 ±0)` + // Minimum execution time: 91_116 nanoseconds. + Weight::from_parts(72_526_877, 10345712) + // Standard Error: 95 + .saturating_add(Weight::from_ref_time(20_461).saturating_mul(k.into())) + // Standard Error: 967 + .saturating_add(Weight::from_ref_time(307_869).saturating_mul(e.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_proof_size(25).saturating_mul(e.into())) + .saturating_add(Weight::from_proof_size(64).saturating_mul(k.into())) + } +} + +// For backwards compatibility and tests +impl WeightInfo for () { + /// Storage: Session Validators (r:1 w:0) + /// Proof Skipped: Session Validators (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: Session CurrentIndex (r:1 w:0) + /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) + /// Storage: ImOnline Keys (r:1 w:0) + /// Proof: ImOnline Keys (max_values: Some(1), max_size: Some(320002), added: 320497, mode: MaxEncodedLen) + /// Storage: ImOnline ReceivedHeartbeats (r:1 w:1) + /// Proof: ImOnline ReceivedHeartbeats (max_values: None, max_size: Some(10021032), added: 10023507, mode: MaxEncodedLen) + /// Storage: ImOnline AuthoredBlocks (r:1 w:0) + /// Proof: ImOnline AuthoredBlocks (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) + /// The range of component `k` is `[1, 1000]`. + /// The range of component `e` is `[1, 100]`. + fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `359 + k * (32 ±0)` + // Estimated: `10345712 + e * (25 ±0) + k * (64 ±0)` + // Minimum execution time: 91_116 nanoseconds. + Weight::from_parts(72_526_877, 10345712) + // Standard Error: 95 + .saturating_add(Weight::from_ref_time(20_461).saturating_mul(k.into())) + // Standard Error: 967 + .saturating_add(Weight::from_ref_time(307_869).saturating_mul(e.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + .saturating_add(Weight::from_proof_size(25).saturating_mul(e.into())) + .saturating_add(Weight::from_proof_size(64).saturating_mul(k.into())) + } +} diff --git a/gn-runtime/Cargo.toml b/gn-runtime/Cargo.toml index 67723a6c..6cdd0506 100644 --- a/gn-runtime/Cargo.toml +++ b/gn-runtime/Cargo.toml @@ -96,6 +96,7 @@ frame-try-runtime = { workspace = true, features = ["try-runtime"], optional = t pallet-aura = { workspace = true } pallet-balances = { workspace = true } pallet-grandpa = { workspace = true } +pallet-im-online = { version = "14.0.0", path = "../gn-pallets/pallet-im-online", default-features = false } pallet-randomness-collective-flip = { workspace = true } pallet-session = { workspace = true } pallet-sudo = { workspace = true } diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index f09c9c9b..55c89ea5 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -24,7 +24,7 @@ use sp_runtime::{ Verify, }, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic, SaturatedConversion, + ApplyExtrinsicResult, MultiSignature, SaturatedConversion, }; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -34,8 +34,8 @@ use sp_version::RuntimeVersion; use frame_support::{ construct_runtime, pallet_prelude::TransactionPriority, - parameter_types, sp_io, - traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, ValidatorSet}, + parameter_types, + traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, IdentityFee, Weight, @@ -279,8 +279,8 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - // type ReportUnresponsiveness = (); - type ReportUnresponsiveness = ValidatorManager; + type ReportUnresponsiveness = (); + // type ReportUnresponsiveness = ValidlatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -401,27 +401,30 @@ impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { fn on_runtime_upgrade() -> frame_support::weights::Weight { Session::upgrade_keys::(transform_session_keys); - let validators = Session::queued_keys() - .iter() - .map(|x| x.1.im_online.clone()) - .collect::>(); - - ImOnline::initialize_keys(validators.as_slice()); - - for (index, key) in ImOnline::keys().into_iter().enumerate() { - let heartbeat = pallet_im_online::Heartbeat { - block_number: System::block_number(), - network_state: sp_io::offchain::network_state().unwrap().clone(), - session_index: ValidatorManager::session_index(), - authority_index: index as u32, - validators_len: ImOnline::keys().len() as u32, - }; - - let signature = key.sign(&heartbeat.encode()).unwrap(); - ImOnline::heartbeat(RuntimeOrigin::root(), heartbeat, signature).unwrap(); - } - - // Session::rotate_session(); + // // pallet_im_online::pallet::deposit_event(); + // let network_state = OpaqueNetworkState { + // peer_id: OpaquePeerId(Vec::::new()), + // external_addresses: Vec::::new(), + // }; + // for (index, key) in ImOnline::keys().into_iter().enumerate() { + // let heartbeat = pallet_im_online::Heartbeat { + // block_number: System::block_number(), + // network_state: network_state.clone(), + // session_index: ValidatorManager::session_index(), + // authority_index: index as u32, + // validators_len: ImOnline::keys().len() as u32, + // }; + + // // let signature = key.sign(&heartbeat.encode()).unwrap(); + // ImOnline::heartbeat( + // RuntimeOrigin::root(), + // heartbeat, + // key.sign(&"".to_owned()).unwrap(), + // ) + // .unwrap(); + // } + + Session::rotate_session(); BlockWeights::get().max_block } } From b3a22b8cf2845e2ca6a2eff82e22c3e4d4ec57ad Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Mon, 20 Mar 2023 16:28:50 +0100 Subject: [PATCH 06/17] Revert "broken: Added im-online pallet to pallets for debugging" This reverts commit 4dd0c8a42b07c8c625abc14309332b6af53533d8. --- Cargo.toml | 2 +- gn-node/Cargo.toml | 2 +- gn-pallets/pallet-im-online/.cargo-ok | 1 - gn-pallets/pallet-im-online/Cargo.toml | 51 - gn-pallets/pallet-im-online/README.md | 59 - .../pallet-im-online/src/benchmarking.rs | 105 -- gn-pallets/pallet-im-online/src/lib.rs | 1006 ----------------- gn-pallets/pallet-im-online/src/mock.rs | 238 ---- gn-pallets/pallet-im-online/src/tests.rs | 528 --------- gn-pallets/pallet-im-online/src/weights.rs | 115 -- gn-runtime/Cargo.toml | 2 +- gn-runtime/src/lib.rs | 55 +- 12 files changed, 29 insertions(+), 2135 deletions(-) delete mode 100644 gn-pallets/pallet-im-online/.cargo-ok delete mode 100644 gn-pallets/pallet-im-online/Cargo.toml delete mode 100644 gn-pallets/pallet-im-online/README.md delete mode 100644 gn-pallets/pallet-im-online/src/benchmarking.rs delete mode 100644 gn-pallets/pallet-im-online/src/lib.rs delete mode 100644 gn-pallets/pallet-im-online/src/mock.rs delete mode 100644 gn-pallets/pallet-im-online/src/tests.rs delete mode 100644 gn-pallets/pallet-im-online/src/weights.rs diff --git a/Cargo.toml b/Cargo.toml index ef5aca2f..74f04609 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ substrate-frame-rpc-system = { version = "15.0.0", default-features = false } pallet-aura = { version = "14.0.0", default-features = false } pallet-balances = { version = "15.0.0", default-features = false } pallet-grandpa = { version = "15.0.0", default-features = false } -# pallet-im-online = { version = "14.0.0", default-features = false } +pallet-im-online = { version = "14.0.0", default-features = false } pallet-randomness-collective-flip = { version = "15.0.0", default-features = false } pallet-session = { version = "15.0.0", default-features = false } pallet-sudo = { version = "15.0.0", default-features = false } diff --git a/gn-node/Cargo.toml b/gn-node/Cargo.toml index b1b04631..e33c4541 100644 --- a/gn-node/Cargo.toml +++ b/gn-node/Cargo.toml @@ -41,7 +41,7 @@ frame-system = { workspace = true, optional = true } substrate-frame-rpc-system = { workspace = true } # substrate pallets -pallet-im-online = { version = "14.0.0", path = "../gn-pallets/pallet-im-online", default-features = false } +pallet-im-online = { workspace = true } pallet-transaction-payment = { workspace = true, optional = true } pallet-transaction-payment-rpc = { workspace = true } diff --git a/gn-pallets/pallet-im-online/.cargo-ok b/gn-pallets/pallet-im-online/.cargo-ok deleted file mode 100644 index b5754e20..00000000 --- a/gn-pallets/pallet-im-online/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -ok \ No newline at end of file diff --git a/gn-pallets/pallet-im-online/Cargo.toml b/gn-pallets/pallet-im-online/Cargo.toml deleted file mode 100644 index d2f12a70..00000000 --- a/gn-pallets/pallet-im-online/Cargo.toml +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "pallet-im-online" -version = "14.0.0" -authors = ["Parity Technologies "] -edition = "2021" -license = "Apache-2.0" -homepage = "https://substrate.io" -repository = "https://github.com/paritytech/substrate/" -description = "FRAME's I'm online pallet" -readme = "README.md" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.2.2", default-features = false, features = ["derive"] } -log = { version = "0.4.17", default-features = false } -scale-info = { version = "2.1.1", default-features = false, features = ["derive"] } -frame-benchmarking = { version = "15.0.0", default-features = false, optional = true} -frame-support = { version = "15.0.0", default-features = false} -frame-system = { version = "15.0.0", default-features = false} -pallet-authorship = { version = "15.0.0", default-features = false} -sp-application-crypto = { version = "16.0.0", default-features = false} -sp-core = { version = "15.0.0", default-features = false} -sp-io = { version = "16.0.0", default-features = false} -sp-runtime = { version = "17.0.0", default-features = false} -sp-staking = { version = "13.0.0", default-features = false} -sp-std = { version = "6.0.0", default-features = false} - -[dev-dependencies] -pallet-session = { workspace = true } - -[features] -default = ["std"] -std = [ - "frame-benchmarking?/std", - "codec/std", - "frame-support/std", - "frame-system/std", - "log/std", - "pallet-authorship/std", - "scale-info/std", - "sp-application-crypto/std", - "sp-core/std", - "sp-io/std", - "sp-runtime/std", - "sp-staking/std", - "sp-std/std", -] -runtime-benchmarks = ["frame-benchmarking/runtime-benchmarks"] -try-runtime = ["frame-support/try-runtime"] diff --git a/gn-pallets/pallet-im-online/README.md b/gn-pallets/pallet-im-online/README.md deleted file mode 100644 index be11e0c4..00000000 --- a/gn-pallets/pallet-im-online/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# I'm online Module - -If the local node is a validator (i.e. contains an authority key), this module -gossips a heartbeat transaction with each new session. The heartbeat functions -as a simple mechanism to signal that the node is online in the current era. - -Received heartbeats are tracked for one era and reset with each new era. The -module exposes two public functions to query if a heartbeat has been received -in the current era or session. - -The heartbeat is a signed transaction, which was signed using the session key -and includes the recent best block number of the local validators chain as well -as the `NetworkState`. -It is submitted as an Unsigned Transaction via off-chain workers. - -- [`im_online::Config`](https://docs.rs/pallet-im-online/latest/pallet_im_online/trait.Config.html) -- [`Call`](https://docs.rs/pallet-im-online/latest/pallet_im_online/enum.Call.html) -- [`Module`](https://docs.rs/pallet-im-online/latest/pallet_im_online/struct.Module.html) - -## Interface - -### Public Functions - -- `is_online` - True if the validator sent a heartbeat in the current session. - -## Usage - -```rust -use pallet_im_online::{self as im_online}; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: frame_system::Config + im_online::Config {} - - #[pallet::call] - impl Pallet { - #[pallet::weight(0)] - pub fn is_online(origin: OriginFor, authority_index: u32) -> DispatchResult { - let _sender = ensure_signed(origin)?; - let _is_online = >::is_online(authority_index); - Ok(()) - } - } -} -``` - -## Dependencies - -This module depends on the [Session module](https://docs.rs/pallet-session/latest/pallet_session/). - -License: Apache-2.0 diff --git a/gn-pallets/pallet-im-online/src/benchmarking.rs b/gn-pallets/pallet-im-online/src/benchmarking.rs deleted file mode 100644 index e4d1a646..00000000 --- a/gn-pallets/pallet-im-online/src/benchmarking.rs +++ /dev/null @@ -1,105 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2020-2022 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. - -//! I'm Online pallet benchmarking. - -#![cfg(feature = "runtime-benchmarks")] - -use super::*; - -use frame_benchmarking::v1::benchmarks; -use frame_support::{traits::UnfilteredDispatchable, WeakBoundedVec}; -use frame_system::RawOrigin; -use sp_core::{offchain::OpaqueMultiaddr, OpaquePeerId}; -use sp_runtime::{ - traits::{ValidateUnsigned, Zero}, - transaction_validity::TransactionSource, -}; - -use crate::Pallet as ImOnline; - -const MAX_KEYS: u32 = 1000; -const MAX_EXTERNAL_ADDRESSES: u32 = 100; - -pub fn create_heartbeat( - k: u32, - e: u32, -) -> Result< - (crate::Heartbeat, ::Signature), - &'static str, -> { - let mut keys = Vec::new(); - for _ in 0..k { - keys.push(T::AuthorityId::generate_pair(None)); - } - let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys.clone()) - .map_err(|()| "More than the maximum number of keys provided")?; - Keys::::put(bounded_keys); - - let network_state = OpaqueNetworkState { - peer_id: OpaquePeerId::default(), - external_addresses: vec![OpaqueMultiaddr::new(vec![0; 32]); e as usize], - }; - let input_heartbeat = Heartbeat { - block_number: T::BlockNumber::zero(), - network_state, - session_index: 0, - authority_index: k - 1, - validators_len: keys.len() as u32, - }; - - let encoded_heartbeat = input_heartbeat.encode(); - let authority_id = keys.get((k - 1) as usize).ok_or("out of range")?; - let signature = authority_id.sign(&encoded_heartbeat).ok_or("couldn't make signature")?; - - Ok((input_heartbeat, signature)) -} - -benchmarks! { - #[extra] - heartbeat { - let k in 1 .. MAX_KEYS; - let e in 1 .. MAX_EXTERNAL_ADDRESSES; - let (input_heartbeat, signature) = create_heartbeat::(k, e)?; - }: _(RawOrigin::None, input_heartbeat, signature) - - #[extra] - validate_unsigned { - let k in 1 .. MAX_KEYS; - let e in 1 .. MAX_EXTERNAL_ADDRESSES; - let (input_heartbeat, signature) = create_heartbeat::(k, e)?; - let call = Call::heartbeat { heartbeat: input_heartbeat, signature }; - }: { - ImOnline::::validate_unsigned(TransactionSource::InBlock, &call) - .map_err(<&str>::from)?; - } - - validate_unsigned_and_then_heartbeat { - let k in 1 .. MAX_KEYS; - let e in 1 .. MAX_EXTERNAL_ADDRESSES; - let (input_heartbeat, signature) = create_heartbeat::(k, e)?; - let call = Call::heartbeat { heartbeat: input_heartbeat, signature }; - let call_enc = call.encode(); - }: { - ImOnline::::validate_unsigned(TransactionSource::InBlock, &call).map_err(<&str>::from)?; - as Decode>::decode(&mut &*call_enc) - .expect("call is encoded above, encoding must be correct") - .dispatch_bypass_filter(RawOrigin::None.into())?; - } - - impl_benchmark_test_suite!(ImOnline, crate::mock::new_test_ext(), crate::mock::Runtime); -} diff --git a/gn-pallets/pallet-im-online/src/lib.rs b/gn-pallets/pallet-im-online/src/lib.rs deleted file mode 100644 index 41a4b211..00000000 --- a/gn-pallets/pallet-im-online/src/lib.rs +++ /dev/null @@ -1,1006 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2019-2022 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. - -//! # I'm online Pallet -//! -//! If the local node is a validator (i.e. contains an authority key), this pallet -//! gossips a heartbeat transaction with each new session. The heartbeat functions -//! as a simple mechanism to signal that the node is online in the current era. -//! -//! Received heartbeats are tracked for one era and reset with each new era. The -//! pallet exposes two public functions to query if a heartbeat has been received -//! in the current era or session. -//! -//! The heartbeat is a signed transaction, which was signed using the session key -//! and includes the recent best block number of the local validators chain as well -//! as the [NetworkState](../../client/offchain/struct.NetworkState.html). -//! It is submitted as an Unsigned Transaction via off-chain workers. -//! -//! - [`Config`] -//! - [`Call`] -//! - [`Pallet`] -//! -//! ## Interface -//! -//! ### Public Functions -//! -//! - `is_online` - True if the validator sent a heartbeat in the current session. -//! -//! ## Usage -//! -//! ``` -//! use pallet_im_online::{self as im_online}; -//! -//! #[frame_support::pallet] -//! pub mod pallet { -//! use super::*; -//! use frame_support::pallet_prelude::*; -//! use frame_system::pallet_prelude::*; -//! -//! #[pallet::pallet] -//! pub struct Pallet(_); -//! -//! #[pallet::config] -//! pub trait Config: frame_system::Config + im_online::Config {} -//! -//! #[pallet::call] -//! impl Pallet { -//! #[pallet::weight(0)] -//! pub fn is_online(origin: OriginFor, authority_index: u32) -> DispatchResult { -//! let _sender = ensure_signed(origin)?; -//! let _is_online = >::is_online(authority_index); -//! Ok(()) -//! } -//! } -//! } -//! # fn main() { } -//! ``` -//! -//! ## Dependencies -//! -//! This pallet depends on the [Session pallet](../pallet_session/index.html). - -// Ensure we're `no_std` when compiling for Wasm. -#![cfg_attr(not(feature = "std"), no_std)] - -mod benchmarking; -mod mock; -mod tests; -pub mod weights; - -use codec::{Decode, Encode, MaxEncodedLen}; -use frame_support::{ - traits::{ - EstimateNextSessionRotation, Get, OneSessionHandler, ValidatorSet, - ValidatorSetWithIdentification, WrapperOpaque, - }, - BoundedSlice, WeakBoundedVec, -}; -use frame_system::offchain::{SendTransactionTypes, SubmitTransaction}; -pub use pallet::*; -use scale_info::TypeInfo; -use sp_application_crypto::RuntimeAppPublic; -use sp_core::offchain::OpaqueNetworkState; -use sp_runtime::{ - offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef}, - traits::{AtLeast32BitUnsigned, Convert, Saturating, TrailingZeroInput}, - PerThing, Perbill, Permill, RuntimeDebug, SaturatedConversion, -}; -use sp_staking::{ - offence::{Kind, Offence, ReportOffence}, - SessionIndex, -}; -use sp_std::prelude::*; -pub use weights::WeightInfo; - -pub mod sr25519 { - mod app_sr25519 { - use sp_application_crypto::{app_crypto, key_types::IM_ONLINE, sr25519}; - app_crypto!(sr25519, IM_ONLINE); - } - - sp_application_crypto::with_pair! { - /// An i'm online keypair using sr25519 as its crypto. - pub type AuthorityPair = app_sr25519::Pair; - } - - /// An i'm online signature using sr25519 as its crypto. - pub type AuthoritySignature = app_sr25519::Signature; - - /// An i'm online identifier using sr25519 as its crypto. - pub type AuthorityId = app_sr25519::Public; -} - -pub mod ed25519 { - mod app_ed25519 { - use sp_application_crypto::{app_crypto, ed25519, key_types::IM_ONLINE}; - app_crypto!(ed25519, IM_ONLINE); - } - - sp_application_crypto::with_pair! { - /// An i'm online keypair using ed25519 as its crypto. - pub type AuthorityPair = app_ed25519::Pair; - } - - /// An i'm online signature using ed25519 as its crypto. - pub type AuthoritySignature = app_ed25519::Signature; - - /// An i'm online identifier using ed25519 as its crypto. - pub type AuthorityId = app_ed25519::Public; -} - -const DB_PREFIX: &[u8] = b"parity/im-online-heartbeat/"; -/// How many blocks do we wait for heartbeat transaction to be included -/// before sending another one. -const INCLUDE_THRESHOLD: u32 = 3; - -/// Status of the offchain worker code. -/// -/// This stores the block number at which heartbeat was requested and when the worker -/// has actually managed to produce it. -/// Note we store such status for every `authority_index` separately. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] -struct HeartbeatStatus { - /// An index of the session that we are supposed to send heartbeat for. - pub session_index: SessionIndex, - /// A block number at which the heartbeat for that session has been actually sent. - /// - /// It may be 0 in case the sending failed. In such case we should just retry - /// as soon as possible (i.e. in a worker running for the next block). - pub sent_at: BlockNumber, -} - -impl HeartbeatStatus { - /// Returns true if heartbeat has been recently sent. - /// - /// Parameters: - /// `session_index` - index of current session. - /// `now` - block at which the offchain worker is running. - /// - /// This function will return `true` iff: - /// 1. the session index is the same (we don't care if it went up or down) - /// 2. the heartbeat has been sent recently (within the threshold) - /// - /// The reasoning for 1. is that it's better to send an extra heartbeat than - /// to stall or not send one in case of a bug. - fn is_recent(&self, session_index: SessionIndex, now: BlockNumber) -> bool { - self.session_index == session_index && self.sent_at + INCLUDE_THRESHOLD.into() > now - } -} - -/// Error which may occur while executing the off-chain code. -#[cfg_attr(test, derive(PartialEq))] -enum OffchainErr { - TooEarly, - WaitingForInclusion(BlockNumber), - AlreadyOnline(u32), - FailedSigning, - FailedToAcquireLock, - NetworkState, - SubmitTransaction, -} - -impl sp_std::fmt::Debug for OffchainErr { - fn fmt(&self, fmt: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { - match *self { - OffchainErr::TooEarly => write!(fmt, "Too early to send heartbeat."), - OffchainErr::WaitingForInclusion(ref block) => { - write!( - fmt, - "Heartbeat already sent at {:?}. Waiting for inclusion.", - block - ) - } - OffchainErr::AlreadyOnline(auth_idx) => { - write!(fmt, "Authority {} is already online", auth_idx) - } - OffchainErr::FailedSigning => write!(fmt, "Failed to sign heartbeat"), - OffchainErr::FailedToAcquireLock => write!(fmt, "Failed to acquire lock"), - OffchainErr::NetworkState => write!(fmt, "Failed to fetch network state"), - OffchainErr::SubmitTransaction => write!(fmt, "Failed to submit transaction"), - } - } -} - -pub type AuthIndex = u32; - -/// Heartbeat which is sent/received. -#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] -pub struct Heartbeat -where - BlockNumber: PartialEq + Eq + Decode + Encode, -{ - /// Block number at the time heartbeat is created.. - pub block_number: BlockNumber, - /// A state of local network (peer id and external addresses) - pub network_state: OpaqueNetworkState, - /// Index of the current session. - pub session_index: SessionIndex, - /// An index of the authority on the list of validators. - pub authority_index: AuthIndex, - /// The length of session validator set - pub validators_len: u32, -} - -/// A type that is the same as [`OpaqueNetworkState`] but with [`Vec`] replaced with -/// [`WeakBoundedVec`] where Limit is the respective size limit -/// `PeerIdEncodingLimit` represents the size limit of the encoding of `PeerId` -/// `MultiAddrEncodingLimit` represents the size limit of the encoding of `MultiAddr` -/// `AddressesLimit` represents the size limit of the vector of peers connected -#[derive(Clone, Eq, PartialEq, Encode, Decode, MaxEncodedLen, TypeInfo)] -#[codec(mel_bound())] -#[scale_info(skip_type_params(PeerIdEncodingLimit, MultiAddrEncodingLimit, AddressesLimit))] -pub struct BoundedOpaqueNetworkState -where - PeerIdEncodingLimit: Get, - MultiAddrEncodingLimit: Get, - AddressesLimit: Get, -{ - /// PeerId of the local node in SCALE encoded. - pub peer_id: WeakBoundedVec, - /// List of addresses the node knows it can be reached as. - pub external_addresses: - WeakBoundedVec, AddressesLimit>, -} - -impl, MultiAddrEncodingLimit: Get, AddressesLimit: Get> - BoundedOpaqueNetworkState -{ - fn force_from(ons: &OpaqueNetworkState) -> Self { - let peer_id = WeakBoundedVec::<_, PeerIdEncodingLimit>::force_from( - ons.peer_id.0.clone(), - Some( - "Warning: The size of the encoding of PeerId \ - is bigger than expected. A runtime configuration \ - adjustment may be needed.", - ), - ); - - let external_addresses = WeakBoundedVec::<_, AddressesLimit>::force_from( - ons.external_addresses - .iter() - .map(|x| { - WeakBoundedVec::<_, MultiAddrEncodingLimit>::force_from( - x.0.clone(), - Some( - "Warning: The size of the encoding of MultiAddr \ - is bigger than expected. A runtime configuration \ - adjustment may be needed.", - ), - ) - }) - .collect(), - Some( - "Warning: The network has more peers than expected \ - A runtime configuration adjustment may be needed.", - ), - ); - - Self { - peer_id, - external_addresses, - } - } -} - -/// A type for representing the validator id in a session. -pub type ValidatorId = <::ValidatorSet as ValidatorSet< - ::AccountId, ->>::ValidatorId; - -/// A tuple of (ValidatorId, Identification) where `Identification` is the full identification of -/// `ValidatorId`. -pub type IdentificationTuple = ( - ValidatorId, - <::ValidatorSet as ValidatorSetWithIdentification< - ::AccountId, - >>::Identification, -); - -type OffchainResult = Result::BlockNumber>>; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - #[pallet::generate_store(pub(super) trait Store)] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: SendTransactionTypes> + frame_system::Config { - /// The identifier type for an authority. - type AuthorityId: Member - + Parameter - + RuntimeAppPublic - + Ord - + MaybeSerializeDeserialize - + MaxEncodedLen; - - /// The maximum number of keys that can be added. - type MaxKeys: Get; - - /// The maximum number of peers to be stored in `ReceivedHeartbeats` - type MaxPeerInHeartbeats: Get; - - /// The maximum size of the encoding of `PeerId` and `MultiAddr` that are coming - /// from the hearbeat - type MaxPeerDataEncodingSize: Get; - - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - - /// A type for retrieving the validators supposed to be online in a session. - type ValidatorSet: ValidatorSetWithIdentification; - - /// A trait that allows us to estimate the current session progress and also the - /// average session length. - /// - /// This parameter is used to determine the longevity of `heartbeat` transaction and a - /// rough time when we should start considering sending heartbeats, since the workers - /// avoids sending them at the very beginning of the session, assuming there is a - /// chance the authority will produce a block and they won't be necessary. - type NextSessionRotation: EstimateNextSessionRotation; - - /// A type that gives us the ability to submit unresponsiveness offence reports. - type ReportUnresponsiveness: ReportOffence< - Self::AccountId, - IdentificationTuple, - UnresponsivenessOffence>, - >; - - /// A configuration for base priority of unsigned transactions. - /// - /// This is exposed so that it can be tuned for particular runtime, when - /// multiple pallets send unsigned transactions. - #[pallet::constant] - type UnsignedPriority: Get; - - /// Weight information for extrinsics in this pallet. - type WeightInfo: WeightInfo; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// A new heartbeat was received from `AuthorityId`. - HeartbeatReceived { authority_id: T::AuthorityId }, - /// At the end of the session, no offence was committed. - AllGood, - /// At the end of the session, at least one validator was found to be offline. - SomeOffline { - offline: Vec>, - }, - } - - #[pallet::error] - pub enum Error { - /// Non existent public key. - InvalidKey, - /// Duplicated heartbeat. - DuplicatedHeartbeat, - } - - /// The block number after which it's ok to send heartbeats in the current - /// session. - /// - /// At the beginning of each session we set this to a value that should fall - /// roughly in the middle of the session duration. The idea is to first wait for - /// the validators to produce a block in the current session, so that the - /// heartbeat later on will not be necessary. - /// - /// This value will only be used as a fallback if we fail to get a proper session - /// progress estimate from `NextSessionRotation`, as those estimates should be - /// more accurate then the value we calculate for `HeartbeatAfter`. - #[pallet::storage] - #[pallet::getter(fn heartbeat_after)] - pub(crate) type HeartbeatAfter = StorageValue<_, T::BlockNumber, ValueQuery>; - - /// The current set of keys that may issue a heartbeat. - #[pallet::storage] - #[pallet::getter(fn keys)] - pub(crate) type Keys = - StorageValue<_, WeakBoundedVec, ValueQuery>; - - /// For each session index, we keep a mapping of `SessionIndex` and `AuthIndex` to - /// `WrapperOpaque`. - #[pallet::storage] - #[pallet::getter(fn received_heartbeats)] - pub(crate) type ReceivedHeartbeats = StorageDoubleMap< - _, - Twox64Concat, - SessionIndex, - Twox64Concat, - AuthIndex, - WrapperOpaque< - BoundedOpaqueNetworkState< - T::MaxPeerDataEncodingSize, - T::MaxPeerDataEncodingSize, - T::MaxPeerInHeartbeats, - >, - >, - >; - - /// For each session index, we keep a mapping of `ValidatorId` to the - /// number of blocks authored by the given authority. - #[pallet::storage] - #[pallet::getter(fn authored_blocks)] - pub(crate) type AuthoredBlocks = StorageDoubleMap< - _, - Twox64Concat, - SessionIndex, - Twox64Concat, - ValidatorId, - u32, - ValueQuery, - >; - - #[pallet::genesis_config] - pub struct GenesisConfig { - pub keys: Vec, - } - - #[cfg(feature = "std")] - impl Default for GenesisConfig { - fn default() -> Self { - GenesisConfig { - keys: Default::default(), - } - } - } - - #[pallet::genesis_build] - impl GenesisBuild for GenesisConfig { - fn build(&self) { - Pallet::::initialize_keys(&self.keys); - } - } - - #[pallet::call] - impl Pallet { - /// # - /// - Complexity: `O(K + E)` where K is length of `Keys` (heartbeat.validators_len) and E is - /// length of `heartbeat.network_state.external_address` - /// - `O(K)`: decoding of length `K` - /// - `O(E)`: decoding/encoding of length `E` - /// - DbReads: pallet_session `Validators`, pallet_session `CurrentIndex`, `Keys`, - /// `ReceivedHeartbeats` - /// - DbWrites: `ReceivedHeartbeats` - /// # - // NOTE: the weight includes the cost of validate_unsigned as it is part of the cost to - // import block with such an extrinsic. - #[pallet::call_index(0)] - #[pallet::weight(::WeightInfo::validate_unsigned_and_then_heartbeat( - heartbeat.validators_len as u32, - heartbeat.network_state.external_addresses.len() as u32, - ))] - pub fn heartbeat( - origin: OriginFor, - heartbeat: Heartbeat, - // since signature verification is done in `validate_unsigned` - // we can skip doing it here again. - _signature: ::Signature, - ) -> DispatchResult { - ensure_none(origin)?; - - let current_session = T::ValidatorSet::session_index(); - let exists = - ReceivedHeartbeats::::contains_key(¤t_session, &heartbeat.authority_index); - let keys = Keys::::get(); - let public = keys.get(heartbeat.authority_index as usize); - if let (false, Some(public)) = (exists, public) { - Self::deposit_event(Event::::HeartbeatReceived { - authority_id: public.clone(), - }); - - let network_state_bounded = BoundedOpaqueNetworkState::< - T::MaxPeerDataEncodingSize, - T::MaxPeerDataEncodingSize, - T::MaxPeerInHeartbeats, - >::force_from(&heartbeat.network_state); - ReceivedHeartbeats::::insert( - ¤t_session, - &heartbeat.authority_index, - WrapperOpaque::from(network_state_bounded), - ); - - Ok(()) - } else if exists { - Err(Error::::DuplicatedHeartbeat.into()) - } else { - Err(Error::::InvalidKey.into()) - } - } - } - - #[pallet::hooks] - impl Hooks> for Pallet { - fn offchain_worker(now: BlockNumberFor) { - // Only send messages if we are a potential validator. - if sp_io::offchain::is_validator() { - for res in Self::send_heartbeats(now).into_iter().flatten() { - if let Err(e) = res { - log::warn!( - target: "runtime::im-online", - "Skipping heartbeat at {:?}: {:?}", - now, - e, - ) - } - } - } else { - log::warn!( - target: "runtime::im-online", - "Skipping heartbeat at {:?}. Not a validator.", - now, - ) - } - } - } - - /// Invalid transaction custom error. Returned when validators_len field in heartbeat is - /// incorrect. - pub(crate) const INVALID_VALIDATORS_LEN: u8 = 10; - - #[pallet::validate_unsigned] - impl ValidateUnsigned for Pallet { - type Call = Call; - - fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity { - if let Call::heartbeat { - heartbeat, - signature, - } = call - { - if >::is_online(heartbeat.authority_index) { - // we already received a heartbeat for this authority - return InvalidTransaction::Stale.into(); - } - - // check if session index from heartbeat is recent - let current_session = T::ValidatorSet::session_index(); - if heartbeat.session_index != current_session { - return InvalidTransaction::Stale.into(); - } - - // verify that the incoming (unverified) pubkey is actually an authority id - let keys = Keys::::get(); - if keys.len() as u32 != heartbeat.validators_len { - return InvalidTransaction::Custom(INVALID_VALIDATORS_LEN).into(); - } - let authority_id = match keys.get(heartbeat.authority_index as usize) { - Some(id) => id, - None => return InvalidTransaction::BadProof.into(), - }; - - // check signature (this is expensive so we do it last). - let signature_valid = heartbeat.using_encoded(|encoded_heartbeat| { - authority_id.verify(&encoded_heartbeat, signature) - }); - - if !signature_valid { - return InvalidTransaction::BadProof.into(); - } - - ValidTransaction::with_tag_prefix("ImOnline") - .priority(T::UnsignedPriority::get()) - .and_provides((current_session, authority_id)) - .longevity( - TryInto::::try_into( - T::NextSessionRotation::average_session_length() / 2u32.into(), - ) - .unwrap_or(64_u64), - ) - .propagate(true) - .build() - } else { - InvalidTransaction::Call.into() - } - } - } -} - -/// 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, T::BlockNumber> for Pallet -{ - fn note_author(author: ValidatorId) { - Self::note_authorship(author); - } -} - -impl Pallet { - /// Returns `true` if a heartbeat has been received for the authority at - /// `authority_index` in the authorities series or if the authority has - /// authored at least one block, during the current session. Otherwise - /// `false`. - pub fn is_online(authority_index: AuthIndex) -> bool { - let current_validators = T::ValidatorSet::validators(); - - if authority_index >= current_validators.len() as u32 { - return false; - } - - let authority = ¤t_validators[authority_index as usize]; - - Self::is_online_aux(authority_index, authority) - } - - fn is_online_aux(authority_index: AuthIndex, authority: &ValidatorId) -> bool { - let current_session = T::ValidatorSet::session_index(); - - ReceivedHeartbeats::::contains_key(¤t_session, &authority_index) - || AuthoredBlocks::::get(¤t_session, authority) != 0 - } - - /// Returns `true` if a heartbeat has been received for the authority at `authority_index` in - /// the authorities series, during the current session. Otherwise `false`. - pub fn received_heartbeat_in_current_session(authority_index: AuthIndex) -> bool { - let current_session = T::ValidatorSet::session_index(); - ReceivedHeartbeats::::contains_key(¤t_session, &authority_index) - } - - /// Note that the given authority has authored a block in the current session. - fn note_authorship(author: ValidatorId) { - let current_session = T::ValidatorSet::session_index(); - - AuthoredBlocks::::mutate(¤t_session, author, |authored| *authored += 1); - } - - pub(crate) fn send_heartbeats( - block_number: T::BlockNumber, - ) -> OffchainResult>> { - const START_HEARTBEAT_RANDOM_PERIOD: Permill = Permill::from_percent(10); - const START_HEARTBEAT_FINAL_PERIOD: Permill = Permill::from_percent(80); - - // this should give us a residual probability of 1/SESSION_LENGTH of sending an heartbeat, - // i.e. all heartbeats spread uniformly, over most of the session. as the session progresses - // the probability of sending an heartbeat starts to increase exponentially. - let random_choice = |progress: Permill| { - // given session progress `p` and session length `l` - // the threshold formula is: p^6 + 1/l - let session_length = T::NextSessionRotation::average_session_length(); - let residual = Permill::from_rational(1u32, session_length.saturated_into()); - let threshold: Permill = progress.saturating_pow(6).saturating_add(residual); - - let seed = sp_io::offchain::random_seed(); - let random = ::decode(&mut TrailingZeroInput::new(seed.as_ref())) - .expect("input is padded with zeroes; qed"); - let random = Permill::from_parts(random % Permill::ACCURACY); - - random <= threshold - }; - - let should_heartbeat = if let (Some(progress), _) = - T::NextSessionRotation::estimate_current_session_progress(block_number) - { - // we try to get an estimate of the current session progress first since it should - // provide more accurate results. we will start an early heartbeat period where we'll - // randomly pick whether to heartbeat. after 80% of the session has elapsed, if we - // haven't sent an heartbeat yet we'll send one unconditionally. the idea is to prevent - // all nodes from sending the heartbeats at the same block and causing a temporary (but - // deterministic) spike in transactions. - progress >= START_HEARTBEAT_FINAL_PERIOD - || progress >= START_HEARTBEAT_RANDOM_PERIOD && random_choice(progress) - } else { - // otherwise we fallback to using the block number calculated at the beginning - // of the session that should roughly correspond to the middle of the session - let heartbeat_after = >::get(); - block_number >= heartbeat_after - }; - - if !should_heartbeat { - return Err(OffchainErr::TooEarly); - } - - let session_index = T::ValidatorSet::session_index(); - let validators_len = Keys::::decode_len().unwrap_or_default() as u32; - - Ok( - Self::local_authority_keys().map(move |(authority_index, key)| { - Self::send_single_heartbeat( - authority_index, - key, - session_index, - block_number, - validators_len, - ) - }), - ) - } - - fn send_single_heartbeat( - authority_index: u32, - key: T::AuthorityId, - session_index: SessionIndex, - block_number: T::BlockNumber, - validators_len: u32, - ) -> OffchainResult { - // A helper function to prepare heartbeat call. - let prepare_heartbeat = || -> OffchainResult> { - let network_state = - sp_io::offchain::network_state().map_err(|_| OffchainErr::NetworkState)?; - let heartbeat = Heartbeat { - block_number, - network_state, - session_index, - authority_index, - validators_len, - }; - - let signature = key - .sign(&heartbeat.encode()) - .ok_or(OffchainErr::FailedSigning)?; - - Ok(Call::heartbeat { - heartbeat, - signature, - }) - }; - - if Self::is_online(authority_index) { - return Err(OffchainErr::AlreadyOnline(authority_index)); - } - - // acquire lock for that authority at current heartbeat to make sure we don't - // send concurrent heartbeats. - Self::with_heartbeat_lock(authority_index, session_index, block_number, || { - let call = prepare_heartbeat()?; - log::warn!( - target: "runtime::im-online", - "[index: {:?}] Reporting im-online at block: {:?} (session: {:?}): {:?}", - authority_index, - block_number, - session_index, - call, - ); - - SubmitTransaction::>::submit_unsigned_transaction(call.into()) - .map_err(|_| OffchainErr::SubmitTransaction)?; - - Ok(()) - }) - } - - fn local_authority_keys() -> impl Iterator { - // on-chain storage - // - // At index `idx`: - // 1. A (ImOnline) public key to be used by a validator at index `idx` to send im-online - // heartbeats. - let authorities = Keys::::get(); - - // local keystore - // - // All `ImOnline` public (+private) keys currently in the local keystore. - let mut local_keys = T::AuthorityId::all(); - - local_keys.sort(); - - authorities - .into_iter() - .enumerate() - .filter_map(move |(index, authority)| { - local_keys - .binary_search(&authority) - .ok() - .map(|location| (index as u32, local_keys[location].clone())) - }) - } - - fn with_heartbeat_lock( - authority_index: u32, - session_index: SessionIndex, - now: T::BlockNumber, - f: impl FnOnce() -> OffchainResult, - ) -> OffchainResult { - let key = { - let mut key = DB_PREFIX.to_vec(); - key.extend(authority_index.encode()); - key - }; - let storage = StorageValueRef::persistent(&key); - let res = storage.mutate( - |status: Result>, StorageRetrievalError>| { - // Check if there is already a lock for that particular block. - // This means that the heartbeat has already been sent, and we are just waiting - // for it to be included. However if it doesn't get included for INCLUDE_THRESHOLD - // we will re-send it. - match status { - // we are still waiting for inclusion. - Ok(Some(status)) if status.is_recent(session_index, now) => { - Err(OffchainErr::WaitingForInclusion(status.sent_at)) - } - // attempt to set new status - _ => Ok(HeartbeatStatus { - session_index, - sent_at: now, - }), - } - }, - ); - if let Err(MutateStorageError::ValueFunctionFailed(err)) = res { - return Err(err); - } - - let mut new_status = res.map_err(|_| OffchainErr::FailedToAcquireLock)?; - - // we got the lock, let's try to send the heartbeat. - let res = f(); - - // clear the lock in case we have failed to send transaction. - if res.is_err() { - new_status.sent_at = 0u32.into(); - storage.set(&new_status); - } - - res - } - - fn initialize_keys(keys: &[T::AuthorityId]) { - if !keys.is_empty() { - assert!(Keys::::get().is_empty(), "Keys are already initialized!"); - let bounded_keys = >::try_from(keys) - .expect("More than the maximum number of keys provided"); - Keys::::put(bounded_keys); - } - } - - #[cfg(test)] - fn set_keys(keys: Vec) { - let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::try_from(keys) - .expect("More than the maximum number of keys provided"); - Keys::::put(bounded_keys); - } -} - -impl sp_runtime::BoundToRuntimeAppPublic for Pallet { - type Public = T::AuthorityId; -} - -impl OneSessionHandler for Pallet { - type Key = T::AuthorityId; - - fn on_genesis_session<'a, I: 'a>(validators: I) - where - I: Iterator, - { - let keys = validators.map(|x| x.1).collect::>(); - Self::initialize_keys(&keys); - } - - fn on_new_session<'a, I: 'a>(_changed: bool, validators: I, _queued_validators: I) - where - I: Iterator, - { - // Tell the offchain worker to start making the next session's heartbeats. - // Since we consider producing blocks as being online, - // the heartbeat is deferred a bit to prevent spamming. - let block_number = >::block_number(); - let half_session = T::NextSessionRotation::average_session_length() / 2u32.into(); - >::put(block_number + half_session); - - // Remember who the authorities are for the new session. - let keys = validators.map(|x| x.1).collect::>(); - let bounded_keys = WeakBoundedVec::<_, T::MaxKeys>::force_from( - keys, - Some( - "Warning: The session has more keys than expected. \ - A runtime configuration adjustment may be needed.", - ), - ); - Keys::::put(bounded_keys); - } - - fn on_before_session_ending() { - let session_index = T::ValidatorSet::session_index(); - let keys = Keys::::get(); - let current_validators = T::ValidatorSet::validators(); - - let offenders = current_validators - .into_iter() - .enumerate() - .filter(|(index, id)| !Self::is_online_aux(*index as u32, id)) - .filter_map(|(_, id)| { - >::IdentificationOf::convert( - id.clone() - ).map(|full_id| (id, full_id)) - }) - .collect::>>(); - - // Remove all received heartbeats and number of authored blocks from the - // current session, they have already been processed and won't be needed - // anymore. - #[allow(deprecated)] - ReceivedHeartbeats::::remove_prefix(&T::ValidatorSet::session_index(), None); - #[allow(deprecated)] - AuthoredBlocks::::remove_prefix(&T::ValidatorSet::session_index(), None); - - if offenders.is_empty() { - Self::deposit_event(Event::::AllGood); - } else { - Self::deposit_event(Event::::SomeOffline { - offline: offenders.clone(), - }); - - let validator_set_count = keys.len() as u32; - let offence = UnresponsivenessOffence { - session_index, - validator_set_count, - offenders, - }; - if let Err(e) = T::ReportUnresponsiveness::report_offence(vec![], offence) { - sp_runtime::print(e); - } - } - } - - fn on_disabled(_i: u32) { - // ignore - } -} - -/// An offence that is filed if a validator didn't send a heartbeat message. -#[derive(RuntimeDebug, TypeInfo)] -#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] -pub struct UnresponsivenessOffence { - /// The current session index in which we report the unresponsive validators. - /// - /// It acts as a time measure for unresponsiveness reports and effectively will always point - /// at the end of the session. - pub session_index: SessionIndex, - /// The size of the validator set in current session/era. - pub validator_set_count: u32, - /// Authorities that were unresponsive during the current era. - pub offenders: Vec, -} - -impl Offence for UnresponsivenessOffence { - const ID: Kind = *b"im-online:offlin"; - type TimeSlot = SessionIndex; - - fn offenders(&self) -> Vec { - self.offenders.clone() - } - - fn session_index(&self) -> SessionIndex { - self.session_index - } - - fn validator_set_count(&self) -> u32 { - self.validator_set_count - } - - fn time_slot(&self) -> Self::TimeSlot { - self.session_index - } - - fn slash_fraction(&self, offenders: u32) -> Perbill { - // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 - // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% - // when 13/30 are offline (around 5% when 1/3 are offline). - if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) { - let x = Perbill::from_rational(3 * threshold, self.validator_set_count); - x.saturating_mul(Perbill::from_percent(7)) - } else { - Perbill::default() - } - } -} diff --git a/gn-pallets/pallet-im-online/src/mock.rs b/gn-pallets/pallet-im-online/src/mock.rs deleted file mode 100644 index 783e68df..00000000 --- a/gn-pallets/pallet-im-online/src/mock.rs +++ /dev/null @@ -1,238 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2019-2022 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. - -//! Test utilities - -#![cfg(test)] - -use frame_support::{ - parameter_types, - traits::{ConstU32, ConstU64}, - weights::Weight, -}; -use pallet_session::historical as pallet_session_historical; -use sp_core::H256; -use sp_runtime::{ - testing::{Header, TestXt, UintAuthorityId}, - traits::{BlakeTwo256, ConvertInto, IdentityLookup}, - Permill, -}; -use sp_staking::{ - offence::{OffenceError, ReportOffence}, - SessionIndex, -}; - -use crate as imonline; -use crate::Config; - -type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; -type Block = frame_system::mocking::MockBlock; - -frame_support::construct_runtime!( - pub enum Runtime where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Session: pallet_session::{Pallet, Call, Storage, Event, Config}, - ImOnline: imonline::{Pallet, Call, Storage, Config, Event}, - Historical: pallet_session_historical::{Pallet}, - } -); - -parameter_types! { - pub static Validators: Option> = Some(vec![ - 1, - 2, - 3, - ]); -} - -pub struct TestSessionManager; -impl pallet_session::SessionManager for TestSessionManager { - fn new_session(_new_index: SessionIndex) -> Option> { - Validators::mutate(|l| l.take()) - } - fn end_session(_: SessionIndex) {} - fn start_session(_: SessionIndex) {} -} - -impl pallet_session::historical::SessionManager for TestSessionManager { - fn new_session(_new_index: SessionIndex) -> Option> { - Validators::mutate(|l| { - l.take().map(|validators| validators.iter().map(|v| (*v, *v)).collect()) - }) - } - fn end_session(_: SessionIndex) {} - fn start_session(_: SessionIndex) {} -} - -/// An extrinsic type used for tests. -pub type Extrinsic = TestXt; -type IdentificationTuple = (u64, u64); -type Offence = crate::UnresponsivenessOffence; - -parameter_types! { - pub static Offences: Vec<(Vec, Offence)> = vec![]; -} - -/// A mock offence report handler. -pub struct OffenceHandler; -impl ReportOffence for OffenceHandler { - fn report_offence(reporters: Vec, offence: Offence) -> Result<(), OffenceError> { - Offences::mutate(|l| l.push((reporters, offence))); - Ok(()) - } - - fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &SessionIndex) -> bool { - false - } -} - -pub fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::default().build_storage::().unwrap(); - let mut result: sp_io::TestExternalities = t.into(); - // Set the default keys, otherwise session will discard the validator. - result.execute_with(|| { - for i in 1..=6 { - System::inc_providers(&i); - assert_eq!(Session::set_keys(RuntimeOrigin::signed(i), (i - 1).into(), vec![]), Ok(())); - } - }); - result -} - -impl frame_system::Config for Runtime { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type BlockNumber = u64; - type RuntimeCall = RuntimeCall; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = ConstU64<250>; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = (); - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = ConstU32<16>; -} - -parameter_types! { - pub const Period: u64 = 1; - pub const Offset: u64 = 0; -} - -impl pallet_session::Config for Runtime { - type ShouldEndSession = pallet_session::PeriodicSessions; - type SessionManager = - pallet_session::historical::NoteHistoricalRoot; - type SessionHandler = (ImOnline,); - type ValidatorId = u64; - type ValidatorIdOf = ConvertInto; - type Keys = UintAuthorityId; - type RuntimeEvent = RuntimeEvent; - type NextSessionRotation = pallet_session::PeriodicSessions; - type WeightInfo = (); -} - -impl pallet_session::historical::Config for Runtime { - type FullIdentification = u64; - type FullIdentificationOf = ConvertInto; -} - -impl pallet_authorship::Config for Runtime { - type FindAuthor = (); - type EventHandler = ImOnline; -} - -parameter_types! { - pub static MockCurrentSessionProgress: Option> = None; -} - -parameter_types! { - pub static MockAverageSessionLength: Option = None; -} - -pub struct TestNextSessionRotation; - -impl frame_support::traits::EstimateNextSessionRotation for TestNextSessionRotation { - fn average_session_length() -> u64 { - // take the mock result if any and return it - let mock = MockAverageSessionLength::mutate(|p| p.take()); - - mock.unwrap_or(pallet_session::PeriodicSessions::::average_session_length()) - } - - fn estimate_current_session_progress(now: u64) -> (Option, Weight) { - let (estimate, weight) = - pallet_session::PeriodicSessions::::estimate_current_session_progress( - now, - ); - - // take the mock result if any and return it - let mock = MockCurrentSessionProgress::mutate(|p| p.take()); - - (mock.unwrap_or(estimate), weight) - } - - fn estimate_next_session_rotation(now: u64) -> (Option, Weight) { - pallet_session::PeriodicSessions::::estimate_next_session_rotation(now) - } -} - -impl Config for Runtime { - type AuthorityId = UintAuthorityId; - type RuntimeEvent = RuntimeEvent; - type ValidatorSet = Historical; - type NextSessionRotation = TestNextSessionRotation; - type ReportUnresponsiveness = OffenceHandler; - type UnsignedPriority = ConstU64<{ 1 << 20 }>; - type WeightInfo = (); - type MaxKeys = ConstU32<10_000>; - type MaxPeerInHeartbeats = ConstU32<10_000>; - type MaxPeerDataEncodingSize = ConstU32<1_000>; -} - -impl frame_system::offchain::SendTransactionTypes for Runtime -where - RuntimeCall: From, -{ - type OverarchingCall = RuntimeCall; - type Extrinsic = Extrinsic; -} - -pub fn advance_session() { - let now = System::block_number().max(1); - System::set_block_number(now + 1); - Session::rotate_session(); - let keys = Session::validators().into_iter().map(UintAuthorityId).collect(); - ImOnline::set_keys(keys); - assert_eq!(Session::current_index(), (now / Period::get()) as u32); -} diff --git a/gn-pallets/pallet-im-online/src/tests.rs b/gn-pallets/pallet-im-online/src/tests.rs deleted file mode 100644 index 2c026f71..00000000 --- a/gn-pallets/pallet-im-online/src/tests.rs +++ /dev/null @@ -1,528 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2019-2022 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. - -//! Tests for the im-online module. - -#![cfg(test)] - -use super::*; -use crate::mock::*; -use frame_support::{assert_noop, dispatch}; -use sp_core::{ - offchain::{ - testing::{TestOffchainExt, TestTransactionPoolExt}, - OffchainDbExt, OffchainWorkerExt, TransactionPoolExt, - }, - OpaquePeerId, -}; -use sp_runtime::{ - testing::UintAuthorityId, - transaction_validity::{InvalidTransaction, TransactionValidityError}, -}; - -#[test] -fn test_unresponsiveness_slash_fraction() { - let dummy_offence = - UnresponsivenessOffence { session_index: 0, validator_set_count: 50, offenders: vec![()] }; - // A single case of unresponsiveness is not slashed. - assert_eq!(dummy_offence.slash_fraction(1), Perbill::zero()); - - assert_eq!( - dummy_offence.slash_fraction(5), - Perbill::zero(), // 0% - ); - - assert_eq!( - dummy_offence.slash_fraction(7), - Perbill::from_parts(4200000), // 0.42% - ); - - // One third offline should be punished around 5%. - assert_eq!( - dummy_offence.slash_fraction(17), - Perbill::from_parts(46200000), // 4.62% - ); -} - -#[test] -fn should_report_offline_validators() { - new_test_ext().execute_with(|| { - // given - let block = 1; - System::set_block_number(block); - // buffer new validators - advance_session(); - // enact the change and buffer another one - let validators = vec![1, 2, 3, 4, 5, 6]; - Validators::mutate(|l| *l = Some(validators.clone())); - advance_session(); - - // when - // we end current session and start the next one - advance_session(); - - // then - let offences = Offences::take(); - assert_eq!( - offences, - vec![( - vec![], - UnresponsivenessOffence { - session_index: 2, - validator_set_count: 3, - offenders: vec![(1, 1), (2, 2), (3, 3),], - } - )] - ); - - // should not report when heartbeat is sent - for (idx, v) in validators.into_iter().take(4).enumerate() { - let _ = heartbeat(block, 3, idx as u32, v.into(), Session::validators()).unwrap(); - } - advance_session(); - - // then - let offences = Offences::take(); - assert_eq!( - offences, - vec![( - vec![], - UnresponsivenessOffence { - session_index: 3, - validator_set_count: 6, - offenders: vec![(5, 5), (6, 6),], - } - )] - ); - }); -} - -fn heartbeat( - block_number: u64, - session_index: u32, - authority_index: u32, - id: UintAuthorityId, - validators: Vec, -) -> dispatch::DispatchResult { - use frame_support::unsigned::ValidateUnsigned; - - let heartbeat = Heartbeat { - block_number, - network_state: OpaqueNetworkState { - peer_id: OpaquePeerId(vec![1]), - external_addresses: vec![], - }, - session_index, - authority_index, - validators_len: validators.len() as u32, - }; - let signature = id.sign(&heartbeat.encode()).unwrap(); - - ImOnline::pre_dispatch(&crate::Call::heartbeat { - heartbeat: heartbeat.clone(), - signature: signature.clone(), - }) - .map_err(|e| match e { - TransactionValidityError::Invalid(InvalidTransaction::Custom(INVALID_VALIDATORS_LEN)) => - "invalid validators len", - e @ _ => <&'static str>::from(e), - })?; - ImOnline::heartbeat(RuntimeOrigin::none(), heartbeat, signature) -} - -#[test] -fn should_mark_online_validator_when_heartbeat_is_received() { - new_test_ext().execute_with(|| { - advance_session(); - // given - Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); - assert_eq!(Session::validators(), Vec::::new()); - // enact the change and buffer another one - advance_session(); - - assert_eq!(Session::current_index(), 2); - assert_eq!(Session::validators(), vec![1, 2, 3]); - - assert!(!ImOnline::is_online(0)); - assert!(!ImOnline::is_online(1)); - assert!(!ImOnline::is_online(2)); - - // when - let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap(); - - // then - assert!(ImOnline::is_online(0)); - assert!(!ImOnline::is_online(1)); - assert!(!ImOnline::is_online(2)); - - // and when - let _ = heartbeat(1, 2, 2, 3.into(), Session::validators()).unwrap(); - - // then - assert!(ImOnline::is_online(0)); - assert!(!ImOnline::is_online(1)); - assert!(ImOnline::is_online(2)); - }); -} - -#[test] -fn late_heartbeat_and_invalid_keys_len_should_fail() { - new_test_ext().execute_with(|| { - advance_session(); - // given - Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); - assert_eq!(Session::validators(), Vec::::new()); - // enact the change and buffer another one - advance_session(); - - assert_eq!(Session::current_index(), 2); - assert_eq!(Session::validators(), vec![1, 2, 3]); - - // when - assert_noop!( - heartbeat(1, 3, 0, 1.into(), Session::validators()), - "Transaction is outdated" - ); - assert_noop!( - heartbeat(1, 1, 0, 1.into(), Session::validators()), - "Transaction is outdated" - ); - - // invalid validators_len - assert_noop!(heartbeat(1, 2, 0, 1.into(), vec![]), "invalid validators len"); - }); -} - -#[test] -fn should_generate_heartbeats() { - use frame_support::traits::OffchainWorker; - - let mut ext = new_test_ext(); - let (offchain, _state) = TestOffchainExt::new(); - let (pool, state) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - // given - let block = 1; - System::set_block_number(block); - UintAuthorityId::set_all_keys(vec![0, 1, 2]); - // buffer new validators - Session::rotate_session(); - // enact the change and buffer another one - Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); - Session::rotate_session(); - - // when - ImOnline::offchain_worker(block); - - // then - let transaction = state.write().transactions.pop().unwrap(); - // All validators have `0` as their session key, so we generate 2 transactions. - assert_eq!(state.read().transactions.len(), 2); - - // check stuff about the transaction. - let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); - let heartbeat = match ex.call { - crate::mock::RuntimeCall::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => - heartbeat, - e => panic!("Unexpected call: {:?}", e), - }; - - assert_eq!( - heartbeat, - Heartbeat { - block_number: block, - network_state: sp_io::offchain::network_state().unwrap(), - session_index: 2, - authority_index: 2, - validators_len: 3, - } - ); - }); -} - -#[test] -fn should_cleanup_received_heartbeats_on_session_end() { - new_test_ext().execute_with(|| { - advance_session(); - - Validators::mutate(|l| *l = Some(vec![1, 2, 3])); - assert_eq!(Session::validators(), Vec::::new()); - - // enact the change and buffer another one - advance_session(); - - assert_eq!(Session::current_index(), 2); - assert_eq!(Session::validators(), vec![1, 2, 3]); - - // send an heartbeat from authority id 0 at session 2 - let _ = heartbeat(1, 2, 0, 1.into(), Session::validators()).unwrap(); - - // the heartbeat is stored - assert!(!ImOnline::received_heartbeats(&2, &0).is_none()); - - advance_session(); - - // after the session has ended we have already processed the heartbeat - // message, so any messages received on the previous session should have - // been pruned. - assert!(ImOnline::received_heartbeats(&2, &0).is_none()); - }); -} - -#[test] -fn should_mark_online_validator_when_block_is_authored() { - use pallet_authorship::EventHandler; - - new_test_ext().execute_with(|| { - advance_session(); - // given - Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); - assert_eq!(Session::validators(), Vec::::new()); - // enact the change and buffer another one - advance_session(); - - assert_eq!(Session::current_index(), 2); - assert_eq!(Session::validators(), vec![1, 2, 3]); - - for i in 0..3 { - assert!(!ImOnline::is_online(i)); - } - - // when - ImOnline::note_author(1); - - // then - assert!(ImOnline::is_online(0)); - assert!(!ImOnline::is_online(1)); - assert!(!ImOnline::is_online(2)); - }); -} - -#[test] -fn should_not_send_a_report_if_already_online() { - use pallet_authorship::EventHandler; - - let mut ext = new_test_ext(); - let (offchain, _state) = TestOffchainExt::new(); - let (pool, pool_state) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - advance_session(); - // given - Validators::mutate(|l| *l = Some(vec![1, 2, 3, 4, 5, 6])); - assert_eq!(Session::validators(), Vec::::new()); - // enact the change and buffer another one - advance_session(); - assert_eq!(Session::current_index(), 2); - assert_eq!(Session::validators(), vec![1, 2, 3]); - ImOnline::note_author(2); - ImOnline::note_author(3); - - // when - UintAuthorityId::set_all_keys(vec![1, 2, 3]); - // we expect error, since the authority is already online. - let mut res = ImOnline::send_heartbeats(4).unwrap(); - res.next().unwrap().unwrap(); - assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(1)); - assert_eq!(res.next().unwrap().unwrap_err(), OffchainErr::AlreadyOnline(2)); - assert_eq!(res.next(), None); - - // then - let transaction = pool_state.write().transactions.pop().unwrap(); - // All validators have `0` as their session key, but we should only produce 1 heartbeat. - assert_eq!(pool_state.read().transactions.len(), 0); - // check stuff about the transaction. - let ex: Extrinsic = Decode::decode(&mut &*transaction).unwrap(); - let heartbeat = match ex.call { - crate::mock::RuntimeCall::ImOnline(crate::Call::heartbeat { heartbeat, .. }) => - heartbeat, - e => panic!("Unexpected call: {:?}", e), - }; - - assert_eq!( - heartbeat, - Heartbeat { - block_number: 4, - network_state: sp_io::offchain::network_state().unwrap(), - session_index: 2, - authority_index: 0, - validators_len: 3, - } - ); - }); -} - -#[test] -fn should_handle_missing_progress_estimates() { - use frame_support::traits::OffchainWorker; - - let mut ext = new_test_ext(); - let (offchain, _state) = TestOffchainExt::new(); - let (pool, state) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - let block = 1; - - System::set_block_number(block); - UintAuthorityId::set_all_keys(vec![0, 1, 2]); - - // buffer new validators - Session::rotate_session(); - - // enact the change and buffer another one - Validators::mutate(|l| *l = Some(vec![0, 1, 2])); - Session::rotate_session(); - - // we will return `None` on the next call to `estimate_current_session_progress` - // and the offchain worker should fallback to checking `HeartbeatAfter` - MockCurrentSessionProgress::mutate(|p| *p = Some(None)); - ImOnline::offchain_worker(block); - - assert_eq!(state.read().transactions.len(), 3); - }); -} - -#[test] -fn should_handle_non_linear_session_progress() { - // NOTE: this is the reason why we started using `EstimateNextSessionRotation` to figure out if - // we should send a heartbeat, it's possible that between successive blocks we progress through - // the session more than just one block increment (in BABE session length is defined in slots, - // not block numbers). - - let mut ext = new_test_ext(); - let (offchain, _state) = TestOffchainExt::new(); - let (pool, _) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - UintAuthorityId::set_all_keys(vec![0, 1, 2]); - - // buffer new validator - Session::rotate_session(); - - // mock the session length as being 10 blocks long, - // enact the change and buffer another one - Validators::mutate(|l| *l = Some(vec![0, 1, 2])); - - // mock the session length has being 10 which should make us assume the fallback for half - // session will be reached by block 5. - MockAverageSessionLength::mutate(|p| *p = Some(10)); - - Session::rotate_session(); - - // if we don't have valid results for the current session progres then - // we'll fallback to `HeartbeatAfter` and only heartbeat on block 5. - MockCurrentSessionProgress::mutate(|p| *p = Some(None)); - assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); - - MockCurrentSessionProgress::mutate(|p| *p = Some(None)); - assert!(ImOnline::send_heartbeats(5).ok().is_some()); - - // if we have a valid current session progress then we'll heartbeat as soon - // as we're past 80% of the session regardless of the block number - MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_percent(81)))); - - assert!(ImOnline::send_heartbeats(2).ok().is_some()); - }); -} - -#[test] -fn test_does_not_heartbeat_early_in_the_session() { - let mut ext = new_test_ext(); - let (offchain, _state) = TestOffchainExt::new(); - let (pool, _) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - // mock current session progress as being 5%. we only randomly start - // heartbeating after 10% of the session has elapsed. - MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_float(0.05)))); - assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); - }); -} - -#[test] -fn test_probability_of_heartbeating_increases_with_session_progress() { - let mut ext = new_test_ext(); - let (offchain, state) = TestOffchainExt::new(); - let (pool, _) = TestTransactionPoolExt::new(); - ext.register_extension(OffchainDbExt::new(offchain.clone())); - ext.register_extension(OffchainWorkerExt::new(offchain)); - ext.register_extension(TransactionPoolExt::new(pool)); - - ext.execute_with(|| { - let set_test = |progress, random: f64| { - // the average session length is 100 blocks, therefore the residual - // probability of sending a heartbeat is 1% - MockAverageSessionLength::mutate(|p| *p = Some(100)); - MockCurrentSessionProgress::mutate(|p| *p = Some(Some(Permill::from_float(progress)))); - - let mut seed = [0u8; 32]; - let encoded = ((random * Permill::ACCURACY as f64) as u32).encode(); - seed[0..4].copy_from_slice(&encoded); - state.write().seed = seed; - }; - - let assert_too_early = |progress, random| { - set_test(progress, random); - assert_eq!(ImOnline::send_heartbeats(2).err(), Some(OffchainErr::TooEarly)); - }; - - let assert_heartbeat_ok = |progress, random| { - set_test(progress, random); - assert!(ImOnline::send_heartbeats(2).ok().is_some()); - }; - - assert_too_early(0.05, 1.0); - - assert_too_early(0.1, 0.1); - assert_too_early(0.1, 0.011); - assert_heartbeat_ok(0.1, 0.010); - - assert_too_early(0.4, 0.015); - assert_heartbeat_ok(0.4, 0.014); - - assert_too_early(0.5, 0.026); - assert_heartbeat_ok(0.5, 0.025); - - assert_too_early(0.6, 0.057); - assert_heartbeat_ok(0.6, 0.056); - - assert_too_early(0.65, 0.086); - assert_heartbeat_ok(0.65, 0.085); - - assert_too_early(0.7, 0.13); - assert_heartbeat_ok(0.7, 0.12); - - assert_too_early(0.75, 0.19); - assert_heartbeat_ok(0.75, 0.18); - }); -} diff --git a/gn-pallets/pallet-im-online/src/weights.rs b/gn-pallets/pallet-im-online/src/weights.rs deleted file mode 100644 index 4ea275a3..00000000 --- a/gn-pallets/pallet-im-online/src/weights.rs +++ /dev/null @@ -1,115 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2023 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. - -//! Autogenerated weights for pallet_im_online -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-01-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `bm2`, CPU: `Intel(R) Core(TM) i7-7700K CPU @ 4.20GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 - -// Executed Command: -// ./target/production/substrate -// benchmark -// pallet -// --chain=dev -// --steps=50 -// --repeat=20 -// --pallet=pallet_im_online -// --extrinsic=* -// --execution=wasm -// --wasm-execution=compiled -// --heap-pages=4096 -// --output=./frame/im-online/src/weights.rs -// --header=./HEADER-APACHE2 -// --template=./.maintain/frame-weight-template.hbs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use sp_std::marker::PhantomData; - -/// Weight functions needed for pallet_im_online. -pub trait WeightInfo { - fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight; -} - -/// Weights for pallet_im_online using the Substrate node and recommended hardware. -pub struct SubstrateWeight(PhantomData); -impl WeightInfo for SubstrateWeight { - /// Storage: Session Validators (r:1 w:0) - /// Proof Skipped: Session Validators (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: ImOnline Keys (r:1 w:0) - /// Proof: ImOnline Keys (max_values: Some(1), max_size: Some(320002), added: 320497, mode: MaxEncodedLen) - /// Storage: ImOnline ReceivedHeartbeats (r:1 w:1) - /// Proof: ImOnline ReceivedHeartbeats (max_values: None, max_size: Some(10021032), added: 10023507, mode: MaxEncodedLen) - /// Storage: ImOnline AuthoredBlocks (r:1 w:0) - /// Proof: ImOnline AuthoredBlocks (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) - /// The range of component `k` is `[1, 1000]`. - /// The range of component `e` is `[1, 100]`. - fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `359 + k * (32 ±0)` - // Estimated: `10345712 + e * (25 ±0) + k * (64 ±0)` - // Minimum execution time: 91_116 nanoseconds. - Weight::from_parts(72_526_877, 10345712) - // Standard Error: 95 - .saturating_add(Weight::from_ref_time(20_461).saturating_mul(k.into())) - // Standard Error: 967 - .saturating_add(Weight::from_ref_time(307_869).saturating_mul(e.into())) - .saturating_add(T::DbWeight::get().reads(4_u64)) - .saturating_add(T::DbWeight::get().writes(1_u64)) - .saturating_add(Weight::from_proof_size(25).saturating_mul(e.into())) - .saturating_add(Weight::from_proof_size(64).saturating_mul(k.into())) - } -} - -// For backwards compatibility and tests -impl WeightInfo for () { - /// Storage: Session Validators (r:1 w:0) - /// Proof Skipped: Session Validators (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Session CurrentIndex (r:1 w:0) - /// Proof Skipped: Session CurrentIndex (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: ImOnline Keys (r:1 w:0) - /// Proof: ImOnline Keys (max_values: Some(1), max_size: Some(320002), added: 320497, mode: MaxEncodedLen) - /// Storage: ImOnline ReceivedHeartbeats (r:1 w:1) - /// Proof: ImOnline ReceivedHeartbeats (max_values: None, max_size: Some(10021032), added: 10023507, mode: MaxEncodedLen) - /// Storage: ImOnline AuthoredBlocks (r:1 w:0) - /// Proof: ImOnline AuthoredBlocks (max_values: None, max_size: Some(56), added: 2531, mode: MaxEncodedLen) - /// The range of component `k` is `[1, 1000]`. - /// The range of component `e` is `[1, 100]`. - fn validate_unsigned_and_then_heartbeat(k: u32, e: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `359 + k * (32 ±0)` - // Estimated: `10345712 + e * (25 ±0) + k * (64 ±0)` - // Minimum execution time: 91_116 nanoseconds. - Weight::from_parts(72_526_877, 10345712) - // Standard Error: 95 - .saturating_add(Weight::from_ref_time(20_461).saturating_mul(k.into())) - // Standard Error: 967 - .saturating_add(Weight::from_ref_time(307_869).saturating_mul(e.into())) - .saturating_add(RocksDbWeight::get().reads(4_u64)) - .saturating_add(RocksDbWeight::get().writes(1_u64)) - .saturating_add(Weight::from_proof_size(25).saturating_mul(e.into())) - .saturating_add(Weight::from_proof_size(64).saturating_mul(k.into())) - } -} diff --git a/gn-runtime/Cargo.toml b/gn-runtime/Cargo.toml index 6cdd0506..ab29589a 100644 --- a/gn-runtime/Cargo.toml +++ b/gn-runtime/Cargo.toml @@ -96,7 +96,7 @@ frame-try-runtime = { workspace = true, features = ["try-runtime"], optional = t pallet-aura = { workspace = true } pallet-balances = { workspace = true } pallet-grandpa = { workspace = true } -pallet-im-online = { version = "14.0.0", path = "../gn-pallets/pallet-im-online", default-features = false } +pallet-im-online = { workspace = true } pallet-randomness-collective-flip = { workspace = true } pallet-session = { workspace = true } pallet-sudo = { workspace = true } diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 55c89ea5..f09c9c9b 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -24,7 +24,7 @@ use sp_runtime::{ Verify, }, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, MultiSignature, SaturatedConversion, + ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic, SaturatedConversion, }; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -34,8 +34,8 @@ use sp_version::RuntimeVersion; use frame_support::{ construct_runtime, pallet_prelude::TransactionPriority, - parameter_types, - traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem}, + parameter_types, sp_io, + traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, ValidatorSet}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, IdentityFee, Weight, @@ -279,8 +279,8 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - type ReportUnresponsiveness = (); - // type ReportUnresponsiveness = ValidlatorManager; + // type ReportUnresponsiveness = (); + type ReportUnresponsiveness = ValidatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -401,30 +401,27 @@ impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { fn on_runtime_upgrade() -> frame_support::weights::Weight { Session::upgrade_keys::(transform_session_keys); - // // pallet_im_online::pallet::deposit_event(); - // let network_state = OpaqueNetworkState { - // peer_id: OpaquePeerId(Vec::::new()), - // external_addresses: Vec::::new(), - // }; - // for (index, key) in ImOnline::keys().into_iter().enumerate() { - // let heartbeat = pallet_im_online::Heartbeat { - // block_number: System::block_number(), - // network_state: network_state.clone(), - // session_index: ValidatorManager::session_index(), - // authority_index: index as u32, - // validators_len: ImOnline::keys().len() as u32, - // }; - - // // let signature = key.sign(&heartbeat.encode()).unwrap(); - // ImOnline::heartbeat( - // RuntimeOrigin::root(), - // heartbeat, - // key.sign(&"".to_owned()).unwrap(), - // ) - // .unwrap(); - // } - - Session::rotate_session(); + let validators = Session::queued_keys() + .iter() + .map(|x| x.1.im_online.clone()) + .collect::>(); + + ImOnline::initialize_keys(validators.as_slice()); + + for (index, key) in ImOnline::keys().into_iter().enumerate() { + let heartbeat = pallet_im_online::Heartbeat { + block_number: System::block_number(), + network_state: sp_io::offchain::network_state().unwrap().clone(), + session_index: ValidatorManager::session_index(), + authority_index: index as u32, + validators_len: ImOnline::keys().len() as u32, + }; + + let signature = key.sign(&heartbeat.encode()).unwrap(); + ImOnline::heartbeat(RuntimeOrigin::root(), heartbeat, signature).unwrap(); + } + + // Session::rotate_session(); BlockWeights::get().max_block } } From 3b6af72f318b39e08f57103e4053303a6b04e3a7 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Mon, 20 Mar 2023 17:36:26 +0100 Subject: [PATCH 07/17] fix: clean up remnants of past tries --- gn-runtime/src/lib.rs | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index f09c9c9b..6e882e8f 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -24,7 +24,7 @@ use sp_runtime::{ Verify, }, transaction_validity::{TransactionSource, TransactionValidity}, - ApplyExtrinsicResult, MultiSignature, RuntimeAppPublic, SaturatedConversion, + ApplyExtrinsicResult, MultiSignature, SaturatedConversion, }; use sp_std::prelude::*; #[cfg(feature = "std")] @@ -34,7 +34,7 @@ use sp_version::RuntimeVersion; use frame_support::{ construct_runtime, pallet_prelude::TransactionPriority, - parameter_types, sp_io, + parameter_types, traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, ValidatorSet}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, @@ -386,12 +386,12 @@ impl pallet_validator_manager::Config for Runtime { // should be removed along with UpgradeSessionKeys fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { - let id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); + let dummy_id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); opaque::SessionKeys { grandpa: old.grandpa, aura: old.aura, - im_online: id, + im_online: dummy_id, } } @@ -400,28 +400,6 @@ pub struct UpgradeSessionKeys; impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { fn on_runtime_upgrade() -> frame_support::weights::Weight { Session::upgrade_keys::(transform_session_keys); - - let validators = Session::queued_keys() - .iter() - .map(|x| x.1.im_online.clone()) - .collect::>(); - - ImOnline::initialize_keys(validators.as_slice()); - - for (index, key) in ImOnline::keys().into_iter().enumerate() { - let heartbeat = pallet_im_online::Heartbeat { - block_number: System::block_number(), - network_state: sp_io::offchain::network_state().unwrap().clone(), - session_index: ValidatorManager::session_index(), - authority_index: index as u32, - validators_len: ImOnline::keys().len() as u32, - }; - - let signature = key.sign(&heartbeat.encode()).unwrap(); - ImOnline::heartbeat(RuntimeOrigin::root(), heartbeat, signature).unwrap(); - } - - // Session::rotate_session(); BlockWeights::get().max_block } } From 808b6bd7dfacce7f79fb29340eb5d7d697243ce3 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 13:14:41 +0100 Subject: [PATCH 08/17] broken: attemp at restoring im-online base removed from main --- gn-node/src/chain_spec.rs | 10 ++++++++-- gn-runtime/Cargo.toml | 3 +++ gn-runtime/src/lib.rs | 12 ++++++++++-- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/gn-node/src/chain_spec.rs b/gn-node/src/chain_spec.rs index d49868c7..2e2e7b9a 100644 --- a/gn-node/src/chain_spec.rs +++ b/gn-node/src/chain_spec.rs @@ -1,7 +1,9 @@ use gn_runtime::{ - AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, SessionConfig, Signature, - SudoConfig, SystemConfig, ValidatorManagerConfig, WASM_BINARY, + AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, ImOnlineConfig, + SessionConfig, SessionConfig, Signature, Signature, SudoConfig, SystemConfig, + ValidatorManagerConfig, WASM_BINARY, }; +use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use sc_service::ChainType; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{sr25519, Pair, Public}; @@ -35,6 +37,7 @@ struct AuthorityKeys { account_id: AccountId, aura_id: AuraId, grandpa_id: GrandpaId, + im_online_id: ImOnlineId, } impl AuthorityKeys { @@ -43,6 +46,7 @@ impl AuthorityKeys { account_id: get_account_id_from_seed::(seed), aura_id: get_from_seed::(seed), grandpa_id: get_from_seed::(seed), + im_online_id: get_from_seed::(seed), } } @@ -50,6 +54,7 @@ impl AuthorityKeys { gn_runtime::opaque::SessionKeys { aura: self.aura_id.clone(), grandpa: self.grandpa_id.clone(), + im_online: self.im_online_id.clone(), } } } @@ -182,6 +187,7 @@ fn testnet_genesis( }) .collect(), }, + im_online: ImOnlineConfig { keys: vec![] }, aura: AuraConfig { authorities: vec![], }, diff --git a/gn-runtime/Cargo.toml b/gn-runtime/Cargo.toml index ab29589a..1cf4b468 100644 --- a/gn-runtime/Cargo.toml +++ b/gn-runtime/Cargo.toml @@ -35,6 +35,7 @@ std = [ "pallet-balances/std", "pallet-grandpa/std", "pallet-guild/std", + "pallet-im-online/std", "pallet-oracle/std", "pallet-randomness-collective-flip/std", "pallet-session/std", @@ -64,6 +65,7 @@ try-runtime = [ "pallet-balances/try-runtime", "pallet-grandpa/try-runtime", "pallet-guild/try-runtime", + "pallet-im-online/try-runtime", "pallet-oracle/try-runtime", "pallet-randomness-collective-flip/try-runtime", "pallet-sudo/try-runtime", @@ -82,6 +84,7 @@ pallet-validator-manager = { version = "0.0.0-alpha", path = "../gn-pallets/pall hex-literal = { version = "0.3.4", optional = true } parity-scale-codec = { workspace = true, features = ["derive"] } scale-info = { workspace = true, features = ["derive"] } +log = { version = "0.4.17", default-features = false } # substrate frame frame-benchmarking = { workspace = true, optional = true } diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 6e882e8f..9c75c98c 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -14,6 +14,8 @@ pub use pallet_balances::Call as BalancesCall; use pallet_grandpa::{ fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, }; +use pallet_im_online::sr25519::AuthorityId as ImOnlineId; +use parity_scale_codec::Encode; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; @@ -90,6 +92,7 @@ pub mod opaque { pub struct SessionKeys { pub aura: Aura, pub grandpa: Grandpa, + pub im_online: ImOnlineId, } } } @@ -156,6 +159,10 @@ parameter_types! { pub const MinAuthorities: u32 = 2; pub const Period: u32 = 2 * MINUTES; pub const Offset: u32 = 0; + pub const ImOnlineUnsignedPriority: TransactionPriority = TransactionPriority::max_value(); + pub const MaxKeys: u32 = 10_000; + pub const MaxPeerInHeartbeats: u32 = 10_000; + pub const MaxPeerDataEncodingSize: u32 = 1_000; } // Configure FRAME pallets to include in runtime. @@ -279,8 +286,8 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - // type ReportUnresponsiveness = (); - type ReportUnresponsiveness = ValidatorManager; + type ReportUnresponsiveness = (); + // type ReportUnresponsiveness = ValidatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -420,6 +427,7 @@ construct_runtime!( Timestamp: pallet_timestamp, ValidatorManager: pallet_validator_manager, Session: pallet_session, + ImOnline: pallet_im_online, Aura: pallet_aura, Grandpa: pallet_grandpa, From 6a928496eddc6c67d56199ecb59f6835339cf9e7 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 13:59:58 +0100 Subject: [PATCH 09/17] fix: minor inconveniences --- gn-node/src/chain_spec.rs | 3 +-- gn-runtime/src/lib.rs | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/gn-node/src/chain_spec.rs b/gn-node/src/chain_spec.rs index 2e2e7b9a..02580c8e 100644 --- a/gn-node/src/chain_spec.rs +++ b/gn-node/src/chain_spec.rs @@ -1,7 +1,6 @@ use gn_runtime::{ AccountId, AuraConfig, BalancesConfig, GenesisConfig, GrandpaConfig, ImOnlineConfig, - SessionConfig, SessionConfig, Signature, Signature, SudoConfig, SystemConfig, - ValidatorManagerConfig, WASM_BINARY, + SessionConfig, Signature, SudoConfig, SystemConfig, ValidatorManagerConfig, WASM_BINARY, }; use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use sc_service::ChainType; diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 9c75c98c..99e73137 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -37,7 +37,7 @@ use frame_support::{ construct_runtime, pallet_prelude::TransactionPriority, parameter_types, - traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem, ValidatorSet}, + traits::{ConstU32, ConstU64, ConstU8, KeyOwnerProofSystem}, weights::{ constants::{RocksDbWeight, WEIGHT_REF_TIME_PER_SECOND}, IdentityFee, Weight, @@ -92,7 +92,7 @@ pub mod opaque { pub struct SessionKeys { pub aura: Aura, pub grandpa: Grandpa, - pub im_online: ImOnlineId, + pub im_online: ImOnline, } } } @@ -287,7 +287,6 @@ impl pallet_im_online::Config for Runtime { type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; type ReportUnresponsiveness = (); - // type ReportUnresponsiveness = ValidatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; From a91d8c7c84e861a4da2fc40ce7584b50dd4e52e8 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 14:18:17 +0100 Subject: [PATCH 10/17] chore: bump version number --- gn-runtime/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 99e73137..7496f57d 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -109,7 +109,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 101, + spec_version: 102, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From 924ec695fc666abc6afbc7d7267a227e0e5c4785 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 16:31:38 +0100 Subject: [PATCH 11/17] feat: arm im-online pallet with validator manager, v103 --- gn-runtime/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 7496f57d..051edaa9 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -109,7 +109,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 102, + spec_version: 103, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -286,7 +286,7 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - type ReportUnresponsiveness = (); + type ReportUnresponsiveness = ValidatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; From c2b5ab6a365255ee8b810d8da530962970c5fe0b Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 17:59:59 +0100 Subject: [PATCH 12/17] chore: remove old runtime upgrade --- gn-runtime/src/lib.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 051edaa9..ad62a307 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -469,37 +469,6 @@ pub type Executive = frame_executive::Executive< UpgradeSessionKeys, >; -// TODO remove this after migration (validator manager pallet) -mod vm_upgrade { - use super::*; - use frame_support::traits::OnRuntimeUpgrade; - - pub struct Upgrade; - impl OnRuntimeUpgrade for Upgrade { - fn on_runtime_upgrade() -> Weight { - pallet_validator_manager::migration::on_runtime_upgrade::(); - BlockWeights::get().max_block - } - - #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, &'static str> { - assert!(!ValidatorManager::validators().is_empty(), "invalid state"); - Ok(Vec::new()) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(_state: Vec) -> Result<(), &'static str> { - assert!(!ValidatorManager::validators().is_empty(), "invalid state"); - assert_eq!( - ValidatorManager::validators(), - ValidatorManager::approved_validators(), - "migration failed" - ); - Ok(()) - } - } -} - #[cfg(feature = "runtime-benchmarks")] #[macro_use] extern crate frame_benchmarking; From 740889a9fa350d67bcce83cb728f74be8c94a1b9 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Tue, 21 Mar 2023 18:01:29 +0100 Subject: [PATCH 13/17] fix: remove storage migration from v103 --- gn-runtime/src/lib.rs | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index ad62a307..699f3786 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -79,15 +79,6 @@ pub mod opaque { /// Opaque block identifier type. pub type BlockId = generic::BlockId; - // TODO Remove after im_online runtime upgrade is done. - impl_opaque_keys! { - pub struct OldSessionKeys { - pub aura: Aura, - pub grandpa: Grandpa, - - } - } - impl_opaque_keys! { pub struct SessionKeys { pub aura: Aura, @@ -390,26 +381,6 @@ impl pallet_validator_manager::Config for Runtime { type MinAuthorities = MinAuthorities; } -// should be removed along with UpgradeSessionKeys -fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { - let dummy_id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); - - opaque::SessionKeys { - grandpa: old.grandpa, - aura: old.aura, - im_online: dummy_id, - } -} - -// When this is removed, should also remove `OldSessionKeys`. -pub struct UpgradeSessionKeys; -impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { - fn on_runtime_upgrade() -> frame_support::weights::Weight { - Session::upgrade_keys::(transform_session_keys); - BlockWeights::get().max_block - } -} - // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -466,7 +437,6 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, - UpgradeSessionKeys, >; #[cfg(feature = "runtime-benchmarks")] From 3bc48248468985a11f256ad1e79ae2f56f9a6fda Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Wed, 22 Mar 2023 12:01:13 +0100 Subject: [PATCH 14/17] chore: remove old storage migration --- gn-pallets/pallet-validator-manager/src/lib.rs | 1 - .../pallet-validator-manager/src/migration.rs | 15 --------------- 2 files changed, 16 deletions(-) delete mode 100644 gn-pallets/pallet-validator-manager/src/migration.rs diff --git a/gn-pallets/pallet-validator-manager/src/lib.rs b/gn-pallets/pallet-validator-manager/src/lib.rs index 68acfa7b..bbf9db2b 100644 --- a/gn-pallets/pallet-validator-manager/src/lib.rs +++ b/gn-pallets/pallet-validator-manager/src/lib.rs @@ -16,7 +16,6 @@ #![deny(clippy::dbg_macro)] #![deny(unused_crate_dependencies)] -pub mod migration; #[cfg(test)] mod mock; #[cfg(test)] diff --git a/gn-pallets/pallet-validator-manager/src/migration.rs b/gn-pallets/pallet-validator-manager/src/migration.rs deleted file mode 100644 index 4ee34011..00000000 --- a/gn-pallets/pallet-validator-manager/src/migration.rs +++ /dev/null @@ -1,15 +0,0 @@ -use super::*; - -pub fn on_runtime_upgrade() { - let validators = Validators::::get(); - log::info!("# validators: {}", validators.len()); - log::info!( - "# approved validators: {}", - ApprovedValidators::::get().len() - ); - ApprovedValidators::::set(validators); - log::info!( - "# approved validators post-migration: {}", - ApprovedValidators::::get().len() - ); -} From 4423dcfcfd23e6dfdf62de50e87a071165a0142c Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Wed, 22 Mar 2023 12:49:00 +0100 Subject: [PATCH 15/17] fix: reference commit v102 w/o obsolete storage migrations --- gn-runtime/src/lib.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 699f3786..f18b2306 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -79,6 +79,15 @@ pub mod opaque { /// Opaque block identifier type. pub type BlockId = generic::BlockId; + // TODO Remove after im_online runtime upgrade is done. + impl_opaque_keys! { + pub struct OldSessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, + + } + } + impl_opaque_keys! { pub struct SessionKeys { pub aura: Aura, @@ -100,7 +109,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 103, + spec_version: 102, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -277,7 +286,7 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - type ReportUnresponsiveness = ValidatorManager; + type ReportUnresponsiveness = (); type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -381,6 +390,26 @@ impl pallet_validator_manager::Config for Runtime { type MinAuthorities = MinAuthorities; } +// should be removed along with UpgradeSessionKeys +fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { + let dummy_id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); + + opaque::SessionKeys { + grandpa: old.grandpa, + aura: old.aura, + im_online: dummy_id, + } +} + +// When this is removed, should also remove `OldSessionKeys`. +pub struct UpgradeSessionKeys; +impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + Session::upgrade_keys::(transform_session_keys); + BlockWeights::get().max_block + } +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -437,6 +466,7 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, + UpgradeSessionKeys, >; #[cfg(feature = "runtime-benchmarks")] From 19076bd4b7ccbd41d886f771be9839d12ca20704 Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Wed, 22 Mar 2023 12:54:02 +0100 Subject: [PATCH 16/17] feat: remove v102 storage migrations and arm validator manager, v103 --- gn-runtime/src/lib.rs | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index f18b2306..699f3786 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -79,15 +79,6 @@ pub mod opaque { /// Opaque block identifier type. pub type BlockId = generic::BlockId; - // TODO Remove after im_online runtime upgrade is done. - impl_opaque_keys! { - pub struct OldSessionKeys { - pub aura: Aura, - pub grandpa: Grandpa, - - } - } - impl_opaque_keys! { pub struct SessionKeys { pub aura: Aura, @@ -109,7 +100,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 102, + spec_version: 103, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -286,7 +277,7 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - type ReportUnresponsiveness = (); + type ReportUnresponsiveness = ValidatorManager; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -390,26 +381,6 @@ impl pallet_validator_manager::Config for Runtime { type MinAuthorities = MinAuthorities; } -// should be removed along with UpgradeSessionKeys -fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { - let dummy_id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); - - opaque::SessionKeys { - grandpa: old.grandpa, - aura: old.aura, - im_online: dummy_id, - } -} - -// When this is removed, should also remove `OldSessionKeys`. -pub struct UpgradeSessionKeys; -impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { - fn on_runtime_upgrade() -> frame_support::weights::Weight { - Session::upgrade_keys::(transform_session_keys); - BlockWeights::get().max_block - } -} - // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -466,7 +437,6 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, - UpgradeSessionKeys, >; #[cfg(feature = "runtime-benchmarks")] From d5111430ac9ad6a8c7595f4782a72ed6c44b386c Mon Sep 17 00:00:00 2001 From: OnyxSkyscape Date: Thu, 23 Mar 2023 15:44:40 +0100 Subject: [PATCH 17/17] chore: move v103 to another branch This reverts commit 19076bd4b7ccbd41d886f771be9839d12ca20704. --- gn-runtime/src/lib.rs | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/gn-runtime/src/lib.rs b/gn-runtime/src/lib.rs index 699f3786..f18b2306 100644 --- a/gn-runtime/src/lib.rs +++ b/gn-runtime/src/lib.rs @@ -79,6 +79,15 @@ pub mod opaque { /// Opaque block identifier type. pub type BlockId = generic::BlockId; + // TODO Remove after im_online runtime upgrade is done. + impl_opaque_keys! { + pub struct OldSessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, + + } + } + impl_opaque_keys! { pub struct SessionKeys { pub aura: Aura, @@ -100,7 +109,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 103, + spec_version: 102, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -277,7 +286,7 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_session::PeriodicSessions; type ValidatorSet = ValidatorManager; - type ReportUnresponsiveness = ValidatorManager; + type ReportUnresponsiveness = (); type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -381,6 +390,26 @@ impl pallet_validator_manager::Config for Runtime { type MinAuthorities = MinAuthorities; } +// should be removed along with UpgradeSessionKeys +fn transform_session_keys(_v: AccountId, old: opaque::OldSessionKeys) -> opaque::SessionKeys { + let dummy_id = pallet_im_online::sr25519::AuthorityId::try_from(old.aura.as_ref()).unwrap(); + + opaque::SessionKeys { + grandpa: old.grandpa, + aura: old.aura, + im_online: dummy_id, + } +} + +// When this is removed, should also remove `OldSessionKeys`. +pub struct UpgradeSessionKeys; +impl frame_support::traits::OnRuntimeUpgrade for UpgradeSessionKeys { + fn on_runtime_upgrade() -> frame_support::weights::Weight { + Session::upgrade_keys::(transform_session_keys); + BlockWeights::get().max_block + } +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime where @@ -437,6 +466,7 @@ pub type Executive = frame_executive::Executive< frame_system::ChainContext, Runtime, AllPalletsWithSystem, + UpgradeSessionKeys, >; #[cfg(feature = "runtime-benchmarks")]