diff --git a/bridges/bin/node/node/src/chain_spec.rs b/bridges/bin/node/node/src/chain_spec.rs index 0e81f7f167..13bb3f167d 100644 --- a/bridges/bin/node/node/src/chain_spec.rs +++ b/bridges/bin/node/node/src/chain_spec.rs @@ -15,8 +15,8 @@ // along with Parity Bridges Common. If not, see . use bridge_node_runtime::{ - AccountId, AuraConfig, BalancesConfig, BridgeEthPoAConfig, GenesisConfig, GrandpaConfig, Signature, SudoConfig, - SystemConfig, WASM_BINARY, + AccountId, AuraConfig, BalancesConfig, BridgeEthPoAConfig, GenesisConfig, GrandpaConfig, SessionConfig, + SessionKeys, Signature, SudoConfig, SystemConfig, WASM_BINARY, }; use grandpa_primitives::AuthorityId as GrandpaId; use sc_service; @@ -34,6 +34,8 @@ pub type ChainSpec = sc_service::ChainSpec; pub enum Alternative { /// Whatever the current runtime is, with just Alice as an auth. Development, + /// Whatever the current runtime is, with simple Alice/Bob/Charlie/Dave/Eve auths. + LocalTestnet, } /// Helper function to generate a crypto pair from seed @@ -54,8 +56,12 @@ where } /// Helper function to generate an authority key for Aura -pub fn get_authority_keys_from_seed(s: &str) -> (AuraId, GrandpaId) { - (get_from_seed::(s), get_from_seed::(s)) +pub fn get_authority_keys_from_seed(s: &str) -> (AccountId, AuraId, GrandpaId) { + ( + get_account_id_from_seed::(s), + get_from_seed::(s), + get_from_seed::(s), + ) } impl Alternative { @@ -84,19 +90,60 @@ impl Alternative { None, None, ), + Alternative::LocalTestnet => ChainSpec::from_genesis( + "Local Testnet", + "local_testnet", + || { + testnet_genesis( + vec![ + get_authority_keys_from_seed("Alice"), + get_authority_keys_from_seed("Bob"), + get_authority_keys_from_seed("Charlie"), + get_authority_keys_from_seed("Dave"), + get_authority_keys_from_seed("Eve"), + ], + get_account_id_from_seed::("Alice"), + vec![ + get_account_id_from_seed::("Alice"), + get_account_id_from_seed::("Bob"), + get_account_id_from_seed::("Charlie"), + get_account_id_from_seed::("Dave"), + get_account_id_from_seed::("Eve"), + get_account_id_from_seed::("Ferdie"), + get_account_id_from_seed::("Alice//stash"), + get_account_id_from_seed::("Bob//stash"), + get_account_id_from_seed::("Charlie//stash"), + get_account_id_from_seed::("Dave//stash"), + get_account_id_from_seed::("Eve//stash"), + get_account_id_from_seed::("Ferdie//stash"), + ], + true, + ) + }, + vec![], + None, + None, + None, + None, + ), }) } pub(crate) fn from(s: &str) -> Option { match s { "" | "dev" => Some(Alternative::Development), + "local" => Some(Alternative::LocalTestnet), _ => None, } } } +fn session_keys(aura: AuraId, grandpa: GrandpaId) -> SessionKeys { + SessionKeys { aura, grandpa } +} + fn testnet_genesis( - initial_authorities: Vec<(AuraId, GrandpaId)>, + initial_authorities: Vec<(AccountId, AuraId, GrandpaId)>, root_key: AccountId, endowed_accounts: Vec, _enable_println: bool, @@ -110,13 +157,19 @@ fn testnet_genesis( balances: endowed_accounts.iter().cloned().map(|k| (k, 1 << 60)).collect(), }), pallet_aura: Some(AuraConfig { - authorities: initial_authorities.iter().map(|x| (x.0.clone())).collect(), + authorities: Vec::new(), }), pallet_bridge_eth_poa: load_kovan_config(), pallet_grandpa: Some(GrandpaConfig { - authorities: initial_authorities.iter().map(|x| (x.1.clone(), 1)).collect(), + authorities: Vec::new(), }), pallet_sudo: Some(SudoConfig { key: root_key }), + pallet_session: Some(SessionConfig { + keys: initial_authorities + .iter() + .map(|x| (x.0.clone(), x.0.clone(), session_keys(x.1.clone(), x.2.clone()))) + .collect::>(), + }), } } diff --git a/bridges/bin/node/runtime/Cargo.toml b/bridges/bin/node/runtime/Cargo.toml index 06c8d93070..394db75e17 100644 --- a/bridges/bin/node/runtime/Cargo.toml +++ b/bridges/bin/node/runtime/Cargo.toml @@ -59,6 +59,12 @@ default-features = false rev = "2afecf81ee19b8a6edb364b419190ea47c4a4a31" git = "https://github.com/paritytech/substrate/" +[dependencies.pallet-session] +version = "2.0.0-alpha.2" +default-features = false +rev = "2afecf81ee19b8a6edb364b419190ea47c4a4a31" +git = "https://github.com/paritytech/substrate/" + [dependencies.frame-system] version = "2.0.0-alpha.2" default-features = false @@ -149,6 +155,12 @@ default-features = false rev = "2afecf81ee19b8a6edb364b419190ea47c4a4a31" git = "https://github.com/paritytech/substrate/" +[dependencies.sp-staking] +version = "2.0.0-alpha.2" +default-features = false +rev = "2afecf81ee19b8a6edb364b419190ea47c4a4a31" +git = "https://github.com/paritytech/substrate/" + [dependencies.sp-std] version = "2.0.0-alpha.2" default-features = false diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs index 1ec47fde80..5128ec58e9 100644 --- a/bridges/bin/node/runtime/src/lib.rs +++ b/bridges/bin/node/runtime/src/lib.rs @@ -24,12 +24,15 @@ #[cfg(feature = "std")] include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +use codec::{Decode, Encode}; use pallet_grandpa::fg_primitives; use pallet_grandpa::AuthorityList as GrandpaAuthorityList; use sp_api::impl_runtime_apis; use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::OpaqueMetadata; -use sp_runtime::traits::{BlakeTwo256, Block as BlockT, ConvertInto, IdentifyAccount, IdentityLookup, Verify}; +use sp_runtime::traits::{ + BlakeTwo256, Block as BlockT, ConvertInto, IdentifyAccount, IdentityLookup, OpaqueKeys, Verify, +}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, transaction_validity::TransactionValidity, ApplyExtrinsicResult, MultiSignature, @@ -89,12 +92,12 @@ pub mod opaque { pub type Block = generic::Block; /// Opaque block identifier type. pub type BlockId = generic::BlockId; +} - impl_opaque_keys! { - pub struct SessionKeys { - pub aura: Aura, - pub grandpa: Grandpa, - } +impl_opaque_keys! { + pub struct SessionKeys { + pub aura: Aura, + pub grandpa: Grandpa, } } @@ -233,6 +236,72 @@ impl pallet_sudo::Trait for Runtime { type Call = Call; } +parameter_types! { + pub const Period: BlockNumber = 4; + pub const Offset: BlockNumber = 0; +} + +impl pallet_session::Trait for Runtime { + type Event = Event; + type ValidatorId = ::AccountId; + type ValidatorIdOf = (); + type ShouldEndSession = pallet_session::PeriodicSessions; + type SessionManager = ShiftSessionManager; + type SessionHandler = ::KeyTypeIdProviders; + type Keys = SessionKeys; + type DisabledValidatorsThreshold = (); +} + +pub struct ShiftSessionManager; + +impl ShiftSessionManager { + /// Select validators for session. + fn select_validators( + session_index: sp_staking::SessionIndex, + available_validators: &[AccountId], + ) -> Vec { + let available_validators_count = available_validators.len(); + let count = sp_std::cmp::max(1, 2 * available_validators_count / 3); + let offset = session_index as usize % available_validators_count; + let end = offset + count; + let session_validators = match end.overflowing_sub(available_validators_count) { + (wrapped_end, false) if wrapped_end != 0 => available_validators[offset..] + .iter() + .chain(available_validators[..wrapped_end].iter()) + .cloned() + .collect(), + _ => available_validators[offset..end].to_vec(), + }; + + session_validators + } +} + +impl pallet_session::SessionManager for ShiftSessionManager { + fn end_session(_: sp_staking::SessionIndex) {} + fn new_session(session_index: sp_staking::SessionIndex) -> Option> { + // can't access genesis config here :/ + if session_index == 0 || session_index == 1 { + return None; + } + + // the idea that on first call (i.e. when session 1 ends) we're reading current + // set of validators from session module (they are initial validators) and save + // in our 'local storage'. + // then for every session we select (deterministically) 2/3 of these initial + // validators to serve validators of new session + let available_validators = sp_io::storage::get(b":available_validators") + .and_then(|validators| Decode::decode(&mut &validators[..]).ok()) + .unwrap_or_else(|| { + let validators = >::validators(); + sp_io::storage::set(b":available_validators", &validators.encode()); + validators + }); + + Some(Self::select_validators(session_index, &available_validators)) + } +} + construct_runtime!( pub enum Runtime where Block = Block, @@ -247,6 +316,7 @@ construct_runtime!( Balances: pallet_balances::{Module, Call, Storage, Config, Event}, TransactionPayment: pallet_transaction_payment::{Module, Storage}, Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, + Session: pallet_session::{Module, Call, Storage, Event, Config}, BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage, ValidateUnsigned}, } ); @@ -374,13 +444,13 @@ impl_runtime_apis! { impl sp_session::SessionKeys for Runtime { fn generate_session_keys(seed: Option>) -> Vec { - opaque::SessionKeys::generate(seed) + SessionKeys::generate(seed) } fn decode_session_keys( encoded: Vec, ) -> Option, sp_core::crypto::KeyTypeId)>> { - opaque::SessionKeys::decode_into_raw_public_keys(&encoded) + SessionKeys::decode_into_raw_public_keys(&encoded) } } @@ -390,3 +460,48 @@ impl_runtime_apis! { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn shift_session_manager_works() { + let acc1 = AccountId::from([1u8; 32]); + let acc2 = AccountId::from([2u8; 32]); + let acc3 = AccountId::from([3u8; 32]); + let acc4 = AccountId::from([4u8; 32]); + let acc5 = AccountId::from([5u8; 32]); + let all_accs = vec![acc1.clone(), acc2.clone(), acc3.clone(), acc4.clone(), acc5.clone()]; + + // at least 1 validator is selected + assert_eq!( + ShiftSessionManager::select_validators(0, &[acc1.clone()]), + vec![acc1.clone()], + ); + + // at session#0, shift is also 0 + assert_eq!( + ShiftSessionManager::select_validators(0, &all_accs), + vec![acc1.clone(), acc2.clone(), acc3.clone()], + ); + + // at session#1, shift is also 1 + assert_eq!( + ShiftSessionManager::select_validators(1, &all_accs), + vec![acc2.clone(), acc3.clone(), acc4.clone()], + ); + + // at session#3, we're wrapping + assert_eq!( + ShiftSessionManager::select_validators(3, &all_accs), + vec![acc4, acc5, acc1.clone()], + ); + + // at session#5, we're starting from the beginning again + assert_eq!( + ShiftSessionManager::select_validators(5, &all_accs), + vec![acc1, acc2, acc3], + ); + } +}