From 98190b7b83f2bd674cef6e6e25f7fc59009f94c4 Mon Sep 17 00:00:00 2001 From: akildemir <34187742+akildemir@users.noreply.github.com> Date: Tue, 10 Oct 2023 13:53:24 +0300 Subject: [PATCH] Staking pallet (#373) * initial staking pallet * add staking pallet to runtime * support session rotation for serai * optimizations & cleaning * fix deny * add serai network to initial networks * a few tweaks & comments * fix some pr comments * Rewrite validator-sets with logarithmic algorithms Uses the fact the underlying DB is sorted to achieve sorting of potential validators by stake. Removes release of deallocated stake for now. --------- Co-authored-by: Luke Parker --- Cargo.lock | 20 +- Cargo.toml | 2 + coordinator/src/main.rs | 5 +- coordinator/src/substrate/mod.rs | 15 +- coordinator/src/tests/tributary/chain.rs | 22 +- coordinator/src/tributary/mod.rs | 12 +- deny.toml | 2 + substrate/client/src/serai/validator_sets.rs | 40 +-- .../client/tests/common/in_instructions.rs | 4 +- .../client/tests/common/validator_sets.rs | 14 +- substrate/client/tests/validator_sets.rs | 23 +- substrate/node/src/chain_spec.rs | 10 +- substrate/primitives/Cargo.toml | 4 +- substrate/primitives/src/networks.rs | 12 - substrate/runtime/Cargo.toml | 4 + substrate/runtime/src/lib.rs | 25 +- substrate/staking/pallet/Cargo.toml | 46 +++ substrate/staking/pallet/LICENSE | 15 + substrate/staking/pallet/src/lib.rs | 180 +++++++++++ substrate/tokens/pallet/src/lib.rs | 2 +- substrate/validator-sets/pallet/Cargo.toml | 5 + substrate/validator-sets/pallet/src/lib.rs | 291 +++++++++++++++--- .../validator-sets/primitives/src/lib.rs | 16 +- tests/coordinator/src/tests/key_gen.rs | 6 +- tests/full-stack/src/tests/mint_and_burn.rs | 9 +- 25 files changed, 635 insertions(+), 149 deletions(-) create mode 100644 substrate/staking/pallet/Cargo.toml create mode 100644 substrate/staking/pallet/LICENSE create mode 100644 substrate/staking/pallet/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index e3db9aa92..08389137a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8386,7 +8386,6 @@ dependencies = [ name = "serai-primitives" version = "0.1.0" dependencies = [ - "lazy_static", "parity-scale-codec", "scale-info", "serde", @@ -8504,6 +8503,7 @@ dependencies = [ "scale-info", "serai-in-instructions-pallet", "serai-primitives", + "serai-staking-pallet", "serai-tokens-pallet", "serai-validator-sets-pallet", "sp-api", @@ -8522,6 +8522,22 @@ dependencies = [ "substrate-wasm-builder", ] +[[package]] +name = "serai-staking-pallet" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-session", + "parity-scale-codec", + "scale-info", + "serai-primitives", + "serai-validator-sets-pallet", + "serai-validator-sets-primitives", + "sp-runtime", + "sp-std", +] + [[package]] name = "serai-tokens-pallet" version = "0.1.0" @@ -8554,12 +8570,14 @@ dependencies = [ "frame-support", "frame-system", "hashbrown 0.14.0", + "pallet-session", "parity-scale-codec", "scale-info", "serai-primitives", "serai-validator-sets-primitives", "sp-application-crypto", "sp-core", + "sp-io", "sp-runtime", "sp-std", ] diff --git a/Cargo.toml b/Cargo.toml index dbf6910e4..0cf22f314 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,8 @@ members = [ "substrate/validator-sets/primitives", "substrate/validator-sets/pallet", + "substrate/staking/pallet", + "substrate/runtime", "substrate/node", diff --git a/coordinator/src/main.rs b/coordinator/src/main.rs index 080735391..0778fb16d 100644 --- a/coordinator/src/main.rs +++ b/coordinator/src/main.rs @@ -254,7 +254,10 @@ pub(crate) async fn scan_tributaries< // TODO2: Differentiate connection errors from invariants Err(e) => { // Check if this failed because the keys were already set by someone else - if matches!(serai.get_keys(spec.set()).await, Ok(Some(_))) { + // TODO: hash_with_keys is latest, yet we'll remove old keys from storage + let hash_with_keys = serai.get_latest_block_hash().await.unwrap(); + if matches!(serai.get_keys(spec.set(), hash_with_keys).await, Ok(Some(_))) + { log::info!("another coordinator set key pair for {:?}", set); break; } diff --git a/coordinator/src/substrate/mod.rs b/coordinator/src/substrate/mod.rs index 0419bb085..60ca8d326 100644 --- a/coordinator/src/substrate/mod.rs +++ b/coordinator/src/substrate/mod.rs @@ -35,12 +35,14 @@ async fn in_set( key: &Zeroizing<::F>, serai: &Serai, set: ValidatorSet, + block_hash: [u8; 32], ) -> Result, SeraiError> { - let Some(data) = serai.get_validator_set(set).await? else { + let Some(participants) = serai.get_validator_set_participants(set.network, block_hash).await? + else { return Ok(None); }; let key = (Ristretto::generator() * key.deref()).to_bytes(); - Ok(Some(data.participants.iter().any(|(participant, _)| participant.0 == key))) + Ok(Some(participants.iter().any(|participant| participant.0 == key))) } async fn handle_new_set( @@ -51,10 +53,13 @@ async fn handle_new_set( block: &Block, set: ValidatorSet, ) -> Result<(), SeraiError> { - if in_set(key, serai, set).await?.expect("NewSet for set which doesn't exist") { + if in_set(key, serai, set, block.hash()).await?.expect("NewSet for set which doesn't exist") { log::info!("present in set {:?}", set); - let set_data = serai.get_validator_set(set).await?.expect("NewSet for set which doesn't exist"); + let set_participants = serai + .get_validator_set_participants(set.network, block.hash()) + .await? + .expect("NewSet for set which doesn't exist"); let time = if let Ok(time) = block.time() { time @@ -77,7 +82,7 @@ async fn handle_new_set( const SUBSTRATE_TO_TRIBUTARY_TIME_DELAY: u64 = 120; let time = time + SUBSTRATE_TO_TRIBUTARY_TIME_DELAY; - let spec = TributarySpec::new(block.hash(), time, set, set_data); + let spec = TributarySpec::new(block.hash(), time, set, set_participants); create_new_tributary(db, spec.clone()); } else { log::info!("not present in set {:?}", set); diff --git a/coordinator/src/tests/tributary/chain.rs b/coordinator/src/tests/tributary/chain.rs index d70d3f9eb..a7d227472 100644 --- a/coordinator/src/tests/tributary/chain.rs +++ b/coordinator/src/tests/tributary/chain.rs @@ -15,8 +15,8 @@ use ciphersuite::{ use sp_application_crypto::sr25519; use serai_client::{ - primitives::{NETWORKS, NetworkId, Amount}, - validator_sets::primitives::{Session, ValidatorSet, ValidatorSetData}, + primitives::NetworkId, + validator_sets::primitives::{Session, ValidatorSet}, }; use tokio::time::sleep; @@ -52,20 +52,12 @@ pub fn new_spec( let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin }; - let set_data = ValidatorSetData { - bond: Amount(100), - network: NETWORKS[&NetworkId::Bitcoin].clone(), - participants: keys - .iter() - .map(|key| { - (sr25519::Public((::generator() * **key).to_bytes()), Amount(100)) - }) - .collect::>() - .try_into() - .unwrap(), - }; + let set_participants = keys + .iter() + .map(|key| sr25519::Public((::generator() * **key).to_bytes())) + .collect::>(); - let res = TributarySpec::new(serai_block, start_time, set, set_data); + let res = TributarySpec::new(serai_block, start_time, set, set_participants); assert_eq!(TributarySpec::read::<&[u8]>(&mut res.serialize().as_ref()).unwrap(), res); res } diff --git a/coordinator/src/tributary/mod.rs b/coordinator/src/tributary/mod.rs index f151e18e1..ab7e3d42d 100644 --- a/coordinator/src/tributary/mod.rs +++ b/coordinator/src/tributary/mod.rs @@ -17,8 +17,8 @@ use frost::Participant; use scale::{Encode, Decode}; use serai_client::{ - primitives::NetworkId, - validator_sets::primitives::{Session, ValidatorSet, ValidatorSetData}, + primitives::{NetworkId, PublicKey}, + validator_sets::primitives::{Session, ValidatorSet}, }; #[rustfmt::skip] @@ -51,16 +51,16 @@ impl TributarySpec { serai_block: [u8; 32], start_time: u64, set: ValidatorSet, - set_data: ValidatorSetData, + set_participants: Vec, ) -> TributarySpec { let mut validators = vec![]; - for (participant, amount) in set_data.participants { + for participant in set_participants { // TODO: Ban invalid keys from being validators on the Serai side // (make coordinator key a session key?) let participant = ::read_G::<&[u8]>(&mut participant.0.as_ref()) .expect("invalid key registered as participant"); - // Give one weight on Tributary per bond instance - validators.push((participant, amount.0 / set_data.bond.0)); + // TODO: Give one weight on Tributary per bond instance + validators.push((participant, 1)); } Self { serai_block, start_time, set, validators } diff --git a/deny.toml b/deny.toml index 32443d5fc..980f78c48 100644 --- a/deny.toml +++ b/deny.toml @@ -60,6 +60,8 @@ exceptions = [ { allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-staking-pallet" }, + { allow = ["AGPL-3.0"], name = "serai-runtime" }, { allow = ["AGPL-3.0"], name = "serai-node" }, diff --git a/substrate/client/src/serai/validator_sets.rs b/substrate/client/src/serai/validator_sets.rs index 95715aba5..a00ada589 100644 --- a/substrate/client/src/serai/validator_sets.rs +++ b/substrate/client/src/serai/validator_sets.rs @@ -1,8 +1,8 @@ -use sp_core::sr25519::Signature; +use sp_core::sr25519::{Public, Signature}; use serai_runtime::{validator_sets, ValidatorSets, Runtime}; pub use validator_sets::primitives; -use primitives::{ValidatorSet, ValidatorSetData, KeyPair}; +use primitives::{ValidatorSet, KeyPair}; use subxt::utils::Encoded; @@ -31,39 +31,29 @@ impl Serai { .await } - pub async fn get_validator_set( + pub async fn get_validator_set_participants( &self, - set: ValidatorSet, - ) -> Result, SeraiError> { - self - .storage( - PALLET, - "ValidatorSets", - Some(vec![scale_value(set)]), - self.get_latest_block_hash().await?, - ) - .await + network: NetworkId, + at_hash: [u8; 32], + ) -> Result>, SeraiError> { + self.storage(PALLET, "Participants", Some(vec![scale_value(network)]), at_hash).await } pub async fn get_validator_set_musig_key( &self, set: ValidatorSet, + at_hash: [u8; 32], ) -> Result, SeraiError> { - self - .storage( - PALLET, - "MuSigKeys", - Some(vec![scale_value(set)]), - self.get_latest_block_hash().await?, - ) - .await + self.storage(PALLET, "MuSigKeys", Some(vec![scale_value(set)]), at_hash).await } // TODO: Store these separately since we almost never need both at once? - pub async fn get_keys(&self, set: ValidatorSet) -> Result, SeraiError> { - self - .storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?) - .await + pub async fn get_keys( + &self, + set: ValidatorSet, + at_hash: [u8; 32], + ) -> Result, SeraiError> { + self.storage(PALLET, "Keys", Some(vec![scale_value(set)]), at_hash).await } pub fn set_validator_set_keys( diff --git a/substrate/client/tests/common/in_instructions.rs b/substrate/client/tests/common/in_instructions.rs index aefcb7fc8..45f903e83 100644 --- a/substrate/client/tests/common/in_instructions.rs +++ b/substrate/client/tests/common/in_instructions.rs @@ -26,7 +26,9 @@ pub async fn provide_batch(batch: Batch) -> [u8; 32] { // TODO: Get the latest session let set = ValidatorSet { session: Session(0), network: batch.network }; let pair = insecure_pair_from_name(&format!("ValidatorSet {:?}", set)); - let keys = if let Some(keys) = serai.get_keys(set).await.unwrap() { + let keys = if let Some(keys) = + serai.get_keys(set, serai.get_latest_block_hash().await.unwrap()).await.unwrap() + { keys } else { let keys = (pair.public(), vec![].try_into().unwrap()); diff --git a/substrate/client/tests/common/validator_sets.rs b/substrate/client/tests/common/validator_sets.rs index 152a16c33..89cf18ebb 100644 --- a/substrate/client/tests/common/validator_sets.rs +++ b/substrate/client/tests/common/validator_sets.rs @@ -28,7 +28,11 @@ pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8 let serai = serai().await; let public_key = ::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); assert_eq!( - serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + serai + .get_validator_set_musig_key(set, serai.get_latest_block_hash().await.unwrap()) + .await + .unwrap() + .unwrap(), musig_key(set, &[public]).0 ); @@ -40,7 +44,11 @@ pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8 let threshold_keys = musig::(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); assert_eq!( - serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + serai + .get_validator_set_musig_key(set, serai.get_latest_block_hash().await.unwrap()) + .await + .unwrap() + .unwrap(), threshold_keys.group_key().to_bytes() ); @@ -66,7 +74,7 @@ pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8 serai.get_key_gen_events(block).await.unwrap(), vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] ); - assert_eq!(serai.get_keys(set).await.unwrap(), Some(key_pair)); + assert_eq!(serai.get_keys(set, block).await.unwrap(), Some(key_pair)); block } diff --git a/substrate/client/tests/validator_sets.rs b/substrate/client/tests/validator_sets.rs index 140df96d0..35b012f59 100644 --- a/substrate/client/tests/validator_sets.rs +++ b/substrate/client/tests/validator_sets.rs @@ -3,7 +3,7 @@ use rand_core::{RngCore, OsRng}; use sp_core::{sr25519::Public, Pair}; use serai_client::{ - primitives::{NETWORKS, NetworkId, insecure_pair_from_name}, + primitives::{NetworkId, insecure_pair_from_name}, validator_sets::{ primitives::{Session, ValidatorSet, musig_key}, ValidatorSetsEvent, @@ -38,7 +38,7 @@ serai_test!( .get_new_set_events(serai.get_block_by_number(0).await.unwrap().unwrap().hash()) .await .unwrap(), - [NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] + [NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] .iter() .copied() .map(|network| ValidatorSetsEvent::NewSet { @@ -47,12 +47,19 @@ serai_test!( .collect::>(), ); - let set_data = serai.get_validator_set(set).await.unwrap().unwrap(); - assert_eq!(set_data.network, NETWORKS[&NetworkId::Bitcoin]); - let participants_ref: &[_] = set_data.participants.as_ref(); - assert_eq!(participants_ref, [(public, set_data.bond)].as_ref()); + let participants = serai + .get_validator_set_participants(set.network, serai.get_latest_block_hash().await.unwrap()) + .await + .unwrap() + .unwrap(); + let participants_ref: &[_] = participants.as_ref(); + assert_eq!(participants_ref, [public].as_ref()); assert_eq!( - serai.get_validator_set_musig_key(set).await.unwrap().unwrap(), + serai + .get_validator_set_musig_key(set, serai.get_latest_block_hash().await.unwrap()) + .await + .unwrap() + .unwrap(), musig_key(set, &[public]).0 ); @@ -64,6 +71,6 @@ serai_test!( serai.get_key_gen_events(block).await.unwrap(), vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] ); - assert_eq!(serai.get_keys(set).await.unwrap(), Some(key_pair)); + assert_eq!(serai.get_keys(set, block).await.unwrap(), Some(key_pair)); } ); diff --git a/substrate/node/src/chain_spec.rs b/substrate/node/src/chain_spec.rs index 41a73df50..a758b63be 100644 --- a/substrate/node/src/chain_spec.rs +++ b/substrate/node/src/chain_spec.rs @@ -26,6 +26,7 @@ fn testnet_genesis( ( key, key, + // TODO: Properly diversify these? SessionKeys { babe: key.into(), grandpa: key.into(), authority_discovery: key.into() }, ) }; @@ -54,12 +55,9 @@ fn testnet_genesis( }, validator_sets: ValidatorSetsConfig { - bond: Amount(1_000_000 * 10_u64.pow(8)), - networks: vec![ - (NetworkId::Bitcoin, NETWORKS[&NetworkId::Bitcoin].clone()), - (NetworkId::Ethereum, NETWORKS[&NetworkId::Ethereum].clone()), - (NetworkId::Monero, NETWORKS[&NetworkId::Monero].clone()), - ], + stake: Amount(1_000_000 * 10_u64.pow(8)), + // TODO: Array of these in primitives + networks: vec![NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero], participants: validators.iter().map(|name| account_from_name(name)).collect(), }, session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() }, diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index 2cd16adee..57cbc2d13 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -12,8 +12,6 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -lazy_static = { version = "1", optional = true } - zeroize = { version = "^1.5", features = ["derive"], optional = true } serde = { version = "1", default-features = false, features = ["derive", "alloc"] } @@ -26,5 +24,5 @@ sp-core = { git = "https://github.com/serai-dex/substrate", default-features = f sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } [features] -std = ["lazy_static", "zeroize", "scale/std", "serde/std", "scale-info/std", "sp-core/std", "sp-runtime/std"] +std = ["zeroize", "scale/std", "serde/std", "scale-info/std", "sp-core/std", "sp-runtime/std"] default = ["std"] diff --git a/substrate/primitives/src/networks.rs b/substrate/primitives/src/networks.rs index 8f7a3af40..28b4c7dee 100644 --- a/substrate/primitives/src/networks.rs +++ b/substrate/primitives/src/networks.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "std")] -use std::collections::HashMap; - #[cfg(feature = "std")] use zeroize::Zeroize; @@ -120,12 +117,3 @@ impl Network { &self.coins } } - -#[cfg(feature = "std")] -lazy_static::lazy_static! { - pub static ref NETWORKS: HashMap = HashMap::from([ - (NetworkId::Bitcoin, Network::new(vec![Coin::Bitcoin]).unwrap()), - (NetworkId::Ethereum, Network::new(vec![Coin::Ether, Coin::Dai]).unwrap()), - (NetworkId::Monero, Network::new(vec![Coin::Monero]).unwrap()), - ]); -} diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index 4bac075d5..772918d32 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -50,7 +50,9 @@ pallet-transaction-payment = { git = "https://github.com/serai-dex/substrate", d tokens-pallet = { package = "serai-tokens-pallet", path = "../tokens/pallet", default-features = false } in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions/pallet", default-features = false } +staking-pallet = { package = "serai-staking-pallet", path = "../staking/pallet", default-features = false } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets/pallet", default-features = false } + pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } pallet-babe = { git = "https://github.com/serai-dex/substrate", default-features = false } pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false } @@ -102,7 +104,9 @@ std = [ "tokens-pallet/std", "in-instructions-pallet/std", + "staking-pallet/std", "validator-sets-pallet/std", + "pallet-session/std", "pallet-babe/std", "pallet-grandpa/std", diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index fe2060188..014e8e9b5 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -21,6 +21,7 @@ pub use pallet_assets as assets; pub use tokens_pallet as tokens; pub use in_instructions_pallet as in_instructions; +pub use staking_pallet as staking; pub use validator_sets_pallet as validator_sets; pub use pallet_session as session; @@ -142,7 +143,7 @@ parameter_types! { NORMAL_DISPATCH_RATIO, ); - pub const MaxAuthorities: u32 = 100; + pub const MaxAuthorities: u32 = validator_sets::primitives::MAX_VALIDATORS_PER_SET; } pub struct CallFilter; @@ -172,10 +173,24 @@ impl Contains for CallFilter { return matches!(call, in_instructions::Call::execute_batch { .. }); } + if let RuntimeCall::Staking(call) = call { + return matches!( + call, + staking::Call::stake { .. } | + staking::Call::unstake { .. } | + staking::Call::allocate { .. } | + staking::Call::deallocate { .. } + ); + } + if let RuntimeCall::ValidatorSets(call) = call { return matches!(call, validator_sets::Call::set_keys { .. }); } + if let RuntimeCall::Session(call) = call { + return matches!(call, session::Call::set_keys { .. }); + } + false } } @@ -300,6 +315,10 @@ impl in_instructions::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +impl staking::Config for Runtime { + type Currency = Balances; +} + impl validator_sets::Config for Runtime { type RuntimeEvent = RuntimeEvent; } @@ -317,7 +336,7 @@ impl session::Config for Runtime { type ValidatorIdOf = IdentityValidatorIdOf; type ShouldEndSession = Babe; type NextSessionRotation = Babe; - type SessionManager = (); // TODO? + type SessionManager = Staking; type SessionHandler = ::KeyTypeIdProviders; type Keys = SessionKeys; type WeightInfo = session::weights::SubstrateWeight; @@ -393,6 +412,8 @@ construct_runtime!( ValidatorSets: validator_sets, + Staking: staking, + Session: session, Babe: babe, Grandpa: grandpa, diff --git a/substrate/staking/pallet/Cargo.toml b/substrate/staking/pallet/Cargo.toml new file mode 100644 index 000000000..49e8b44f8 --- /dev/null +++ b/substrate/staking/pallet/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "serai-staking-pallet" +version = "0.1.0" +description = "Staking pallet for Serai" +license = "AGPL-3.0-only" +repository = "https://github.com/serai-dex/serai/tree/develop/substrate/staking/pallet" +authors = ["Luke Parker "] +edition = "2021" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parity-scale-codec = { version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2", default-features = false, features = ["derive"] } + +sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } + +frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } +frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } + +validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../../validator-sets/pallet", default-features = false } +pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } + +serai-primitives = { path = "../../primitives", default-features = false } +serai-validator-sets-primitives = { path = "../../validator-sets/primitives", default-features = false } + +[features] +std = [ + "frame-system/std", + "frame-support/std", + + "sp-std/std", + + "validator-sets-pallet/std", + "pallet-session/std", +] + +runtime-benchmarks = [ + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", +] + +default = ["std"] diff --git a/substrate/staking/pallet/LICENSE b/substrate/staking/pallet/LICENSE new file mode 100644 index 000000000..c425427c8 --- /dev/null +++ b/substrate/staking/pallet/LICENSE @@ -0,0 +1,15 @@ +AGPL-3.0-only license + +Copyright (c) 2022-2023 Luke Parker + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License Version 3 as +published by the Free Software Foundation. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . diff --git a/substrate/staking/pallet/src/lib.rs b/substrate/staking/pallet/src/lib.rs new file mode 100644 index 000000000..99b1415bf --- /dev/null +++ b/substrate/staking/pallet/src/lib.rs @@ -0,0 +1,180 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[frame_support::pallet] +pub mod pallet { + use sp_runtime::{traits::TrailingZeroInput, DispatchError}; + use sp_std::vec::Vec; + + use frame_system::pallet_prelude::*; + use frame_support::{ + pallet_prelude::*, + traits::{Currency, tokens::ExistenceRequirement}, + }; + + use serai_primitives::{NetworkId, Amount, PublicKey}; + + use validator_sets_pallet::{Config as VsConfig, Pallet as VsPallet}; + use pallet_session::{Config as SessionConfig, SessionManager}; + + #[pallet::error] + pub enum Error { + StakeUnavilable, + } + + // TODO: Event + + #[pallet::config] + pub trait Config: + frame_system::Config + VsConfig + SessionConfig + { + type Currency: Currency; + } + + #[pallet::pallet] + pub struct Pallet(PhantomData); + + /// The amount of funds this account has staked. + #[pallet::storage] + #[pallet::getter(fn staked)] + pub type Staked = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + /// The amount of stake this account has allocated to validator sets. + #[pallet::storage] + #[pallet::getter(fn allocated)] + pub type Allocated = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>; + + impl Pallet { + fn account() -> T::AccountId { + // Substrate has a pattern of using simply using 8-bytes (as a PalletId) directly as an + // AccountId. This replicates its internals to remove the 8-byte limit + T::AccountId::decode(&mut TrailingZeroInput::new(b"staking")).unwrap() + } + + fn add_stake(account: &T::AccountId, amount: u64) { + Staked::::mutate(account, |staked| *staked += amount); + } + + fn remove_stake(account: &T::AccountId, amount: u64) -> DispatchResult { + Staked::::mutate(account, |staked| { + let available = *staked - Self::allocated(account); + if available < amount { + Err(Error::::StakeUnavilable)?; + } + *staked -= amount; + Ok::<_, DispatchError>(()) + }) + } + + fn allocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error> { + Allocated::::try_mutate(account, |allocated| { + let available = Self::staked(account) - *allocated; + if available < amount { + Err(Error::::StakeUnavilable)?; + } + *allocated += amount; + Ok(()) + }) + } + + #[allow(unused)] // TODO + fn deallocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error> { + Allocated::::try_mutate(account, |allocated| { + if *allocated < amount { + Err(Error::::StakeUnavilable)?; + } + *allocated -= amount; + Ok(()) + }) + } + } + + #[pallet::call] + impl Pallet { + /// Stake funds from this account. + #[pallet::call_index(0)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn stake(origin: OriginFor, #[pallet::compact] amount: u64) -> DispatchResult { + let signer = ensure_signed(origin)?; + // Serai accounts are solely public keys. Accordingly, there's no harm to letting accounts + // die. They'll simply be re-instantiated later + // AllowDeath accordingly to not add additional requirements (and therefore annoyances) + T::Currency::transfer(&signer, &Self::account(), amount, ExistenceRequirement::AllowDeath)?; + Self::add_stake(&signer, amount); + Ok(()) + } + + /// Unstake funds from this account. Only unallocated funds may be unstaked. + #[pallet::call_index(1)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn unstake(origin: OriginFor, #[pallet::compact] amount: u64) -> DispatchResult { + let signer = ensure_signed(origin)?; + Self::remove_stake(&signer, amount)?; + // This should never be out of funds as there should always be stakers. Accordingly... + T::Currency::transfer(&Self::account(), &signer, amount, ExistenceRequirement::KeepAlive)?; + Ok(()) + } + + /// Allocate `amount` to a given validator set. + #[pallet::call_index(2)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn allocate( + origin: OriginFor, + network: NetworkId, + #[pallet::compact] amount: u64, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + + // add to amount allocated + Self::allocate_internal(&account, amount)?; + + // increase allocation for participant in validator set + VsPallet::::increase_allocation(network, account, Amount(amount)) + } + + /// Deallocate `amount` from a given validator set. + #[pallet::call_index(3)] + #[pallet::weight((0, DispatchClass::Operational))] // TODO + pub fn deallocate( + origin: OriginFor, + network: NetworkId, + #[pallet::compact] amount: u64, + ) -> DispatchResult { + let account = ensure_signed(origin)?; + + // decrease allocation in validator set + VsPallet::::decrease_allocation(network, account, Amount(amount))?; + + // We don't immediately call deallocate since the deallocation only takes effect in the next + // session + // TODO: If this validator isn't active, allow immediate deallocation + Ok(()) + } + + // TODO: Add a function to reclaim deallocated funds + } + + // Call order is end_session(i - 1) -> start_session(i) -> new_session(i + 1) + // new_session(i + 1) is called immediately after start_session(i) + // then we wait until the session ends then get a call to end_session(i) and so on. + impl SessionManager for Pallet { + fn new_session(_new_index: u32) -> Option> { + // Don't call new_session multiple times on genesis + // TODO: Will this cause pallet_session::Pallet::current_index to desync from validator-sets? + if frame_system::Pallet::::block_number() > 1u32.into() { + VsPallet::::new_session(); + } + // TODO: Where do we return their stake? + Some(VsPallet::::validators(NetworkId::Serai)) + } + + fn new_session_genesis(_: u32) -> Option> { + Some(VsPallet::::validators(NetworkId::Serai)) + } + + fn end_session(_end_index: u32) {} + + fn start_session(_start_index: u32) {} + } +} + +pub use pallet::*; diff --git a/substrate/tokens/pallet/src/lib.rs b/substrate/tokens/pallet/src/lib.rs index a82ef1de3..47744aaa5 100644 --- a/substrate/tokens/pallet/src/lib.rs +++ b/substrate/tokens/pallet/src/lib.rs @@ -53,7 +53,7 @@ pub mod pallet { } pub fn mint(address: SeraiAddress, balance: Balance) { - // TODO: Prevent minting when it'd cause an amount exceeding the bond + // TODO: Prevent minting when it'd cause an amount exceeding the allocated stake AssetsPallet::::mint( RawOrigin::Signed(ADDRESS.into()).into(), balance.coin, diff --git a/substrate/validator-sets/pallet/Cargo.toml b/substrate/validator-sets/pallet/Cargo.toml index 99849bea1..e8f119b30 100644 --- a/substrate/validator-sets/pallet/Cargo.toml +++ b/substrate/validator-sets/pallet/Cargo.toml @@ -18,6 +18,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals scale-info = { version = "2", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } +sp-io = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-std = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false } sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false } @@ -25,6 +26,8 @@ sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features frame-system = { git = "https://github.com/serai-dex/substrate", default-features = false } frame-support = { git = "https://github.com/serai-dex/substrate", default-features = false } +pallet-session = { git = "https://github.com/serai-dex/substrate", default-features = false } + serai-primitives = { path = "../../primitives", default-features = false } validator-sets-primitives = { package = "serai-validator-sets-primitives", path = "../primitives", default-features = false } @@ -39,6 +42,8 @@ std = [ "frame-system/std", "frame-support/std", + "pallet-session/std", + "serai-primitives/std", "validator-sets-primitives/std", ] diff --git a/substrate/validator-sets/pallet/src/lib.rs b/substrate/validator-sets/pallet/src/lib.rs index c3d70ab4b..f40e8909c 100644 --- a/substrate/validator-sets/pallet/src/lib.rs +++ b/substrate/validator-sets/pallet/src/lib.rs @@ -6,30 +6,34 @@ pub mod pallet { use scale_info::TypeInfo; use sp_core::sr25519::{Public, Signature}; - use sp_std::vec::Vec; + use sp_std::{vec, vec::Vec}; use sp_application_crypto::RuntimePublic; use frame_system::pallet_prelude::*; - use frame_support::pallet_prelude::*; + use frame_support::{pallet_prelude::*, StoragePrefixedMap}; use serai_primitives::*; pub use validator_sets_primitives as primitives; use primitives::*; #[pallet::config] - pub trait Config: frame_system::Config + TypeInfo { + pub trait Config: + frame_system::Config + pallet_session::Config + TypeInfo + { type RuntimeEvent: IsType<::RuntimeEvent> + From>; } #[pallet::genesis_config] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { - /// Bond requirement to join the initial validator sets. - /// Every participant at genesis will automatically be assumed to have this much bond. - /// This bond cannot be withdrawn however as there's no stake behind it. - pub bond: Amount, + /// Stake requirement to join the initial validator sets. + /// + /// Every participant at genesis will automatically be assumed to have this much stake. + /// This stake cannot be withdrawn however as there's no actual stake behind it. + // TODO: Localize stake to network? + pub stake: Amount, /// Networks to spawn Serai with. - pub networks: Vec<(NetworkId, Network)>, + pub networks: Vec, /// List of participants to place in the initial validator sets. pub participants: Vec, } @@ -37,7 +41,7 @@ pub mod pallet { impl Default for GenesisConfig { fn default() -> Self { GenesisConfig { - bond: Amount(1), + stake: Amount(1), networks: Default::default(), participants: Default::default(), } @@ -47,18 +51,82 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(PhantomData); - /// The details of a validator set instance. + /// The current session for a network. + /// + /// This does not store the current session for Serai. pallet_session handles that. + // Uses Identity for the lookup to avoid a hash of a severely limited fixed key-space. #[pallet::storage] - #[pallet::getter(fn validator_set)] - pub type ValidatorSets = - StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>; + pub type CurrentSession = StorageMap<_, Identity, NetworkId, Session, OptionQuery>; + impl Pallet { + fn session(network: NetworkId) -> Session { + if network == NetworkId::Serai { + Session(pallet_session::Pallet::::current_index()) + } else { + CurrentSession::::get(network).unwrap() + } + } + } + + /// The minimum allocation required to join a validator set. + // Uses Identity for the lookup to avoid a hash of a severely limited fixed key-space. + #[pallet::storage] + #[pallet::getter(fn minimum_allocation)] + pub type MinimumAllocation = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; + /// The validators selected to be in-set. + #[pallet::storage] + #[pallet::getter(fn participants)] + pub type Participants = StorageMap< + _, + Identity, + NetworkId, + BoundedVec>, + ValueQuery, + >; + /// The validators selected to be in-set, yet with the ability to perform a check for presence. + #[pallet::storage] + pub type InSet = StorageMap<_, Blake2_128Concat, (NetworkId, Public), (), OptionQuery>; + + /// The current amount allocated to a validator set by a validator. + #[pallet::storage] + #[pallet::getter(fn allocation)] + pub type Allocations = + StorageMap<_, Blake2_128Concat, (NetworkId, Public), Amount, OptionQuery>; + /// A sorted view of the current allocations premised on the underlying DB itself being sorted. + // Uses Identity so we can iterate over the key space from highest-to-lowest allocated. + // While this does enable attacks the hash is meant to prevent, the minimum stake should resolve + // these. + #[pallet::storage] + type SortedAllocations = + StorageMap<_, Identity, (NetworkId, [u8; 8], Public), (), OptionQuery>; + impl Pallet { + /// A function which takes an amount and generates a byte array with a lexicographic order from + /// high amount to low amount. + #[inline] + fn lexicographic_amount(amount: Amount) -> [u8; 8] { + let mut bytes = amount.0.to_be_bytes(); + for byte in &mut bytes { + *byte = !*byte; + } + bytes + } + fn set_allocation(network: NetworkId, key: Public, amount: Amount) { + let prior = Allocations::::take((network, key)); + if prior.is_some() { + SortedAllocations::::remove((network, Self::lexicographic_amount(amount), key)); + } + if amount.0 != 0 { + Allocations::::set((network, key), Some(amount)); + SortedAllocations::::set((network, Self::lexicographic_amount(amount), key), Some(())); + } + } + } /// The MuSig key for a validator set. #[pallet::storage] #[pallet::getter(fn musig_key)] pub type MuSigKeys = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>; - /// The key pair for a given validator set instance. + /// The generated key pair for a given validator set instance. #[pallet::storage] #[pallet::getter(fn keys)] pub type Keys = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>; @@ -70,33 +138,62 @@ pub mod pallet { KeyGen { set: ValidatorSet, key_pair: KeyPair }, } - #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { - fn build(&self) { - let hash_set = - self.participants.iter().map(|key| key.0).collect::>(); - if hash_set.len() != self.participants.len() { - panic!("participants contained duplicates"); - } + impl Pallet { + fn new_set(network: NetworkId) { + // Update CurrentSession + let session = if network != NetworkId::Serai { + CurrentSession::::mutate(network, |session| { + Some(session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0))) + }) + .unwrap() + } else { + Self::session(network) + }; - let mut participants = Vec::new(); - for participant in self.participants.clone() { - participants.push((participant, self.bond)); + // Clear the current InSet + { + let mut in_set_key = InSet::::final_prefix().to_vec(); + in_set_key.extend(network.encode()); + assert!(matches!( + sp_io::storage::clear_prefix(&in_set_key, Some(MAX_VALIDATORS_PER_SET)), + sp_io::KillStorageResult::AllRemoved(_) + )); } - let participants = BoundedVec::try_from(participants).unwrap(); - for (id, network) in self.networks.clone() { - let set = ValidatorSet { session: Session(0), network: id }; - // TODO: Should this be split up? Substrate will read this entire struct into mem on every - // read, not just accessed variables - ValidatorSets::::set( - set, - Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }), - ); + let mut prefix = SortedAllocations::::final_prefix().to_vec(); + prefix.extend(&network.encode()); + let prefix = prefix; + + let mut last = prefix.clone(); - MuSigKeys::::set(set, Some(musig_key(set, &self.participants))); - Pallet::::deposit_event(Event::NewSet { set }) + let mut participants = vec![]; + for _ in 0 .. MAX_VALIDATORS_PER_SET { + let Some(next) = sp_io::storage::next_key(&last) else { break }; + if !next.starts_with(&prefix) { + break; + } + assert_eq!(next.len(), (32 + 1 + 8 + 32)); + let key = Public(next[(next.len() - 32) .. next.len()].try_into().unwrap()); + + InSet::::set((network, key), Some(())); + participants.push(key); + + last = next; + } + assert!(!participants.is_empty()); + + let set = ValidatorSet { network, session }; + Pallet::::deposit_event(Event::NewSet { set }); + if network != NetworkId::Serai { + // Remove the keys for the set prior to the one now rotating out + if session.0 >= 2 { + let prior_to_now_rotating = ValidatorSet { network, session: Session(session.0 - 2) }; + MuSigKeys::::remove(prior_to_now_rotating); + Keys::::remove(prior_to_now_rotating); + } + MuSigKeys::::set(set, Some(musig_key(set, &participants))); } + Participants::::set(network, participants.try_into().unwrap()); } } @@ -104,10 +201,40 @@ pub mod pallet { pub enum Error { /// Validator Set doesn't exist. NonExistentValidatorSet, + /// Not enough stake to participate in a set. + InsufficientStake, + /// Trying to deallocate more than allocated. + InsufficientAllocation, + /// Deallocation would remove the participant from the set, despite the validator not + /// specifying so. + DeallocationWouldRemoveParticipant, /// Validator Set already generated keys. AlreadyGeneratedKeys, /// An invalid MuSig signature was provided. BadSignature, + /// Validator wasn't registered or active. + NonExistentValidator, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + { + let hash_set = + self.participants.iter().map(|key| key.0).collect::>(); + if hash_set.len() != self.participants.len() { + panic!("participants contained duplicates"); + } + } + + for id in self.networks.clone() { + MinimumAllocation::::set(id, Some(self.stake)); + for participant in self.participants.clone() { + Pallet::::set_allocation(id, participant, self.stake); + } + Pallet::::new_set(id); + } + } } impl Pallet { @@ -116,6 +243,7 @@ pub mod pallet { key_pair: &KeyPair, signature: &Signature, ) -> Result<(), Error> { + // Confirm a key hasn't been set for this set instance if Keys::::get(set).is_some() { Err(Error::AlreadyGeneratedKeys)? } @@ -141,10 +269,8 @@ pub mod pallet { ) -> DispatchResult { ensure_none(origin)?; - // TODO: Get session - let session: Session = Session(0); + let session = Session(pallet_session::Pallet::::current_index()); - // Confirm a key hasn't been set for this set instance let set = ValidatorSet { session, network }; // TODO: Is this needed? validate_unsigned should be called before this and ensure it's Ok Self::verify_signature(set, &key_pair, &signature)?; @@ -167,15 +293,17 @@ pub mod pallet { Call::__Ignore(_, _) => unreachable!(), }; - // TODO: Get the latest session - let session = Session(0); + let session = Session(pallet_session::Pallet::::current_index()); let set = ValidatorSet { session, network: *network }; match Self::verify_signature(set, key_pair, signature) { Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, - Err(Error::NonExistentValidatorSet) | Err(Error::BadSignature) => { - Err(InvalidTransaction::BadProof)? - } + Err(Error::NonExistentValidatorSet) | + Err(Error::InsufficientStake) | + Err(Error::InsufficientAllocation) | + Err(Error::DeallocationWouldRemoveParticipant) | + Err(Error::NonExistentValidator) | + Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?, Err(Error::__Ignore(_, _)) => unreachable!(), Ok(()) => (), } @@ -189,7 +317,80 @@ pub mod pallet { } } - // TODO: Support session rotation + impl Pallet { + pub fn increase_allocation( + network: NetworkId, + account: T::AccountId, + amount: Amount, + ) -> DispatchResult { + let new_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0 + amount.0; + if new_allocation < Self::minimum_allocation(network).unwrap().0 { + Err(Error::::InsufficientStake)?; + } + Self::set_allocation(network, account, Amount(new_allocation)); + Ok(()) + } + + /// Decreases a validator's allocation to a set. + /// + /// Errors if the capacity provided by this allocation is in use. + /// + /// Errors if a partial decrease of allocation which puts the allocation below the minimum. + /// + /// The capacity prior provided by the allocation is immediately removed, in order to ensure it + /// doesn't become used (preventing deallocation). + pub fn decrease_allocation( + network: NetworkId, + account: T::AccountId, + amount: Amount, + ) -> DispatchResult { + // TODO: Check it's safe to decrease this set's stake by this amount + + let new_allocation = Self::allocation((network, account)) + .ok_or(Error::::NonExistentValidator)? + .0 + .checked_sub(amount.0) + .ok_or(Error::::InsufficientAllocation)?; + // If we're not removing the entire allocation, yet the allocation is no longer at or above + // the minimum stake, error + if (new_allocation != 0) && + (new_allocation < Self::minimum_allocation(network).unwrap_or(Amount(0)).0) + { + Err(Error::::DeallocationWouldRemoveParticipant)?; + } + // TODO: Error if we're about to be removed, and the remaining set size would be <4 + + // Decrease the allocation now + Self::set_allocation(network, account, Amount(new_allocation)); + + // Set it to PendingDeallocation, letting the staking pallet release it AFTER this session + // TODO + // TODO: We can immediately free it if it doesn't cross a key share threshold + + Ok(()) + } + + pub fn new_session() { + // TODO: Define an array of all networks in primitives + let networks = [NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero]; + for network in networks { + // Handover is automatically complete for Serai as it doesn't have a handover protocol + // TODO: Update how handover completed is determined. It's not on set keys. It's on new + // set accepting responsibility + let handover_completed = (network == NetworkId::Serai) || + Keys::::contains_key(ValidatorSet { network, session: Self::session(network) }); + // Only spawn a NewSet if the current set was actually established with a completed + // handover protocol + if handover_completed { + Pallet::::new_set(network); + } + } + } + + pub fn validators(network: NetworkId) -> Vec { + Self::participants(network).into() + } + } } pub use pallet::*; diff --git a/substrate/validator-sets/primitives/src/lib.rs b/substrate/validator-sets/primitives/src/lib.rs index fc6a4491d..3cf9be528 100644 --- a/substrate/validator-sets/primitives/src/lib.rs +++ b/substrate/validator-sets/primitives/src/lib.rs @@ -13,8 +13,10 @@ use sp_core::{ConstU32, sr25519::Public, bounded::BoundedVec}; #[cfg(not(feature = "std"))] use sp_std::vec::Vec; -use serai_primitives::{NetworkId, Network, Amount}; +use serai_primitives::NetworkId; +/// The maximum amount of validators per set. +pub const MAX_VALIDATORS_PER_SET: u32 = 150; // Support keys up to 96 bytes (BLS12-381 G2). const MAX_KEY_LEN: u32 = 96; @@ -32,6 +34,7 @@ const MAX_KEY_LEN: u32 = 96; Decode, TypeInfo, MaxEncodedLen, + Default, )] #[cfg_attr(feature = "std", derive(Zeroize))] pub struct Session(pub u32); @@ -57,17 +60,6 @@ pub struct ValidatorSet { pub network: NetworkId, } -/// The data for a validator set. -#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, TypeInfo, MaxEncodedLen)] -pub struct ValidatorSetData { - pub bond: Amount, - pub network: Network, - - // Participant and their amount bonded to this set - // Limit each set to 100 participants for now - pub participants: BoundedVec<(Public, Amount), ConstU32<100>>, -} - type MaxKeyLen = ConstU32; /// The type representing a Key from an external network. pub type ExternalKey = BoundedVec; diff --git a/tests/coordinator/src/tests/key_gen.rs b/tests/coordinator/src/tests/key_gen.rs index ce4993a68..a13a9dd03 100644 --- a/tests/coordinator/src/tests/key_gen.rs +++ b/tests/coordinator/src/tests/key_gen.rs @@ -164,7 +164,11 @@ pub async fn key_gen( } } assert_eq!( - serai.get_keys(set).await.unwrap().unwrap(), + serai + .get_keys(set, serai.get_block_by_number(last_serai_block).await.unwrap().unwrap().hash()) + .await + .unwrap() + .unwrap(), (Public(substrate_key), network_key.try_into().unwrap()) ); diff --git a/tests/full-stack/src/tests/mint_and_burn.rs b/tests/full-stack/src/tests/mint_and_burn.rs index b69f03244..098feb40f 100644 --- a/tests/full-stack/src/tests/mint_and_burn.rs +++ b/tests/full-stack/src/tests/mint_and_burn.rs @@ -195,8 +195,13 @@ async fn mint_and_burn_test() { let halt_at = if additional { 5 * 10 } else { 10 * 10 }; let print_at = halt_at / 2; for i in 0 .. halt_at { - if let Some(key_pair) = - serai.get_keys(ValidatorSet { network, session: Session(0) }).await.unwrap() + if let Some(key_pair) = serai + .get_keys( + ValidatorSet { network, session: Session(0) }, + serai.get_latest_block_hash().await.unwrap(), + ) + .await + .unwrap() { return key_pair; }