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 <lukeparker5132@gmail.com>
This commit is contained in:
akildemir 2023-10-10 13:53:24 +03:00 committed by GitHub
parent 2f45bba2d4
commit 98190b7b83
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 635 additions and 149 deletions

20
Cargo.lock generated
View file

@ -8386,7 +8386,6 @@ dependencies = [
name = "serai-primitives" name = "serai-primitives"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"lazy_static",
"parity-scale-codec", "parity-scale-codec",
"scale-info", "scale-info",
"serde", "serde",
@ -8504,6 +8503,7 @@ dependencies = [
"scale-info", "scale-info",
"serai-in-instructions-pallet", "serai-in-instructions-pallet",
"serai-primitives", "serai-primitives",
"serai-staking-pallet",
"serai-tokens-pallet", "serai-tokens-pallet",
"serai-validator-sets-pallet", "serai-validator-sets-pallet",
"sp-api", "sp-api",
@ -8522,6 +8522,22 @@ dependencies = [
"substrate-wasm-builder", "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]] [[package]]
name = "serai-tokens-pallet" name = "serai-tokens-pallet"
version = "0.1.0" version = "0.1.0"
@ -8554,12 +8570,14 @@ dependencies = [
"frame-support", "frame-support",
"frame-system", "frame-system",
"hashbrown 0.14.0", "hashbrown 0.14.0",
"pallet-session",
"parity-scale-codec", "parity-scale-codec",
"scale-info", "scale-info",
"serai-primitives", "serai-primitives",
"serai-validator-sets-primitives", "serai-validator-sets-primitives",
"sp-application-crypto", "sp-application-crypto",
"sp-core", "sp-core",
"sp-io",
"sp-runtime", "sp-runtime",
"sp-std", "sp-std",
] ]

View file

@ -46,6 +46,8 @@ members = [
"substrate/validator-sets/primitives", "substrate/validator-sets/primitives",
"substrate/validator-sets/pallet", "substrate/validator-sets/pallet",
"substrate/staking/pallet",
"substrate/runtime", "substrate/runtime",
"substrate/node", "substrate/node",

View file

@ -254,7 +254,10 @@ pub(crate) async fn scan_tributaries<
// TODO2: Differentiate connection errors from invariants // TODO2: Differentiate connection errors from invariants
Err(e) => { Err(e) => {
// Check if this failed because the keys were already set by someone else // 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); log::info!("another coordinator set key pair for {:?}", set);
break; break;
} }

View file

@ -35,12 +35,14 @@ async fn in_set(
key: &Zeroizing<<Ristretto as Ciphersuite>::F>, key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
serai: &Serai, serai: &Serai,
set: ValidatorSet, set: ValidatorSet,
block_hash: [u8; 32],
) -> Result<Option<bool>, SeraiError> { ) -> Result<Option<bool>, 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); return Ok(None);
}; };
let key = (Ristretto::generator() * key.deref()).to_bytes(); 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<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>( async fn handle_new_set<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>(
@ -51,10 +53,13 @@ async fn handle_new_set<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>(
block: &Block, block: &Block,
set: ValidatorSet, set: ValidatorSet,
) -> Result<(), SeraiError> { ) -> 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); 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() { let time = if let Ok(time) = block.time() {
time time
@ -77,7 +82,7 @@ async fn handle_new_set<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>(
const SUBSTRATE_TO_TRIBUTARY_TIME_DELAY: u64 = 120; const SUBSTRATE_TO_TRIBUTARY_TIME_DELAY: u64 = 120;
let time = time + SUBSTRATE_TO_TRIBUTARY_TIME_DELAY; 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()); create_new_tributary(db, spec.clone());
} else { } else {
log::info!("not present in set {:?}", set); log::info!("not present in set {:?}", set);

View file

@ -15,8 +15,8 @@ use ciphersuite::{
use sp_application_crypto::sr25519; use sp_application_crypto::sr25519;
use serai_client::{ use serai_client::{
primitives::{NETWORKS, NetworkId, Amount}, primitives::NetworkId,
validator_sets::primitives::{Session, ValidatorSet, ValidatorSetData}, validator_sets::primitives::{Session, ValidatorSet},
}; };
use tokio::time::sleep; use tokio::time::sleep;
@ -52,20 +52,12 @@ pub fn new_spec<R: RngCore + CryptoRng>(
let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin }; let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin };
let set_data = ValidatorSetData { let set_participants = keys
bond: Amount(100),
network: NETWORKS[&NetworkId::Bitcoin].clone(),
participants: keys
.iter() .iter()
.map(|key| { .map(|key| sr25519::Public((<Ristretto as Ciphersuite>::generator() * **key).to_bytes()))
(sr25519::Public((<Ristretto as Ciphersuite>::generator() * **key).to_bytes()), Amount(100)) .collect::<Vec<_>>();
})
.collect::<Vec<_>>()
.try_into()
.unwrap(),
};
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); assert_eq!(TributarySpec::read::<&[u8]>(&mut res.serialize().as_ref()).unwrap(), res);
res res
} }

View file

@ -17,8 +17,8 @@ use frost::Participant;
use scale::{Encode, Decode}; use scale::{Encode, Decode};
use serai_client::{ use serai_client::{
primitives::NetworkId, primitives::{NetworkId, PublicKey},
validator_sets::primitives::{Session, ValidatorSet, ValidatorSetData}, validator_sets::primitives::{Session, ValidatorSet},
}; };
#[rustfmt::skip] #[rustfmt::skip]
@ -51,16 +51,16 @@ impl TributarySpec {
serai_block: [u8; 32], serai_block: [u8; 32],
start_time: u64, start_time: u64,
set: ValidatorSet, set: ValidatorSet,
set_data: ValidatorSetData, set_participants: Vec<PublicKey>,
) -> TributarySpec { ) -> TributarySpec {
let mut validators = vec![]; 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 // TODO: Ban invalid keys from being validators on the Serai side
// (make coordinator key a session key?) // (make coordinator key a session key?)
let participant = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut participant.0.as_ref()) let participant = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut participant.0.as_ref())
.expect("invalid key registered as participant"); .expect("invalid key registered as participant");
// Give one weight on Tributary per bond instance // TODO: Give one weight on Tributary per bond instance
validators.push((participant, amount.0 / set_data.bond.0)); validators.push((participant, 1));
} }
Self { serai_block, start_time, set, validators } Self { serai_block, start_time, set, validators }

View file

@ -60,6 +60,8 @@ exceptions = [
{ allow = ["AGPL-3.0"], name = "serai-validator-sets-pallet" }, { 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-runtime" },
{ allow = ["AGPL-3.0"], name = "serai-node" }, { allow = ["AGPL-3.0"], name = "serai-node" },

View file

@ -1,8 +1,8 @@
use sp_core::sr25519::Signature; use sp_core::sr25519::{Public, Signature};
use serai_runtime::{validator_sets, ValidatorSets, Runtime}; use serai_runtime::{validator_sets, ValidatorSets, Runtime};
pub use validator_sets::primitives; pub use validator_sets::primitives;
use primitives::{ValidatorSet, ValidatorSetData, KeyPair}; use primitives::{ValidatorSet, KeyPair};
use subxt::utils::Encoded; use subxt::utils::Encoded;
@ -31,39 +31,29 @@ impl Serai {
.await .await
} }
pub async fn get_validator_set( pub async fn get_validator_set_participants(
&self, &self,
set: ValidatorSet, network: NetworkId,
) -> Result<Option<ValidatorSetData>, SeraiError> { at_hash: [u8; 32],
self ) -> Result<Option<Vec<Public>>, SeraiError> {
.storage( self.storage(PALLET, "Participants", Some(vec![scale_value(network)]), at_hash).await
PALLET,
"ValidatorSets",
Some(vec![scale_value(set)]),
self.get_latest_block_hash().await?,
)
.await
} }
pub async fn get_validator_set_musig_key( pub async fn get_validator_set_musig_key(
&self, &self,
set: ValidatorSet, set: ValidatorSet,
at_hash: [u8; 32],
) -> Result<Option<[u8; 32]>, SeraiError> { ) -> Result<Option<[u8; 32]>, SeraiError> {
self self.storage(PALLET, "MuSigKeys", Some(vec![scale_value(set)]), at_hash).await
.storage(
PALLET,
"MuSigKeys",
Some(vec![scale_value(set)]),
self.get_latest_block_hash().await?,
)
.await
} }
// TODO: Store these separately since we almost never need both at once? // TODO: Store these separately since we almost never need both at once?
pub async fn get_keys(&self, set: ValidatorSet) -> Result<Option<KeyPair>, SeraiError> { pub async fn get_keys(
self &self,
.storage(PALLET, "Keys", Some(vec![scale_value(set)]), self.get_latest_block_hash().await?) set: ValidatorSet,
.await at_hash: [u8; 32],
) -> Result<Option<KeyPair>, SeraiError> {
self.storage(PALLET, "Keys", Some(vec![scale_value(set)]), at_hash).await
} }
pub fn set_validator_set_keys( pub fn set_validator_set_keys(

View file

@ -26,7 +26,9 @@ pub async fn provide_batch(batch: Batch) -> [u8; 32] {
// TODO: Get the latest session // TODO: Get the latest session
let set = ValidatorSet { session: Session(0), network: batch.network }; let set = ValidatorSet { session: Session(0), network: batch.network };
let pair = insecure_pair_from_name(&format!("ValidatorSet {:?}", set)); 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 keys
} else { } else {
let keys = (pair.public(), vec![].try_into().unwrap()); let keys = (pair.public(), vec![].try_into().unwrap());

View file

@ -28,7 +28,11 @@ pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8
let serai = serai().await; let serai = serai().await;
let public_key = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap(); let public_key = <Ristretto as Ciphersuite>::read_G::<&[u8]>(&mut public.0.as_ref()).unwrap();
assert_eq!( 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 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 = let threshold_keys =
musig::<Ristretto>(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap(); musig::<Ristretto>(&musig_context(set), &Zeroizing::new(secret_key), &[public_key]).unwrap();
assert_eq!( 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() 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(), serai.get_key_gen_events(block).await.unwrap(),
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] 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 block
} }

View file

@ -3,7 +3,7 @@ use rand_core::{RngCore, OsRng};
use sp_core::{sr25519::Public, Pair}; use sp_core::{sr25519::Public, Pair};
use serai_client::{ use serai_client::{
primitives::{NETWORKS, NetworkId, insecure_pair_from_name}, primitives::{NetworkId, insecure_pair_from_name},
validator_sets::{ validator_sets::{
primitives::{Session, ValidatorSet, musig_key}, primitives::{Session, ValidatorSet, musig_key},
ValidatorSetsEvent, ValidatorSetsEvent,
@ -38,7 +38,7 @@ serai_test!(
.get_new_set_events(serai.get_block_by_number(0).await.unwrap().unwrap().hash()) .get_new_set_events(serai.get_block_by_number(0).await.unwrap().unwrap().hash())
.await .await
.unwrap(), .unwrap(),
[NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero] [NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero]
.iter() .iter()
.copied() .copied()
.map(|network| ValidatorSetsEvent::NewSet { .map(|network| ValidatorSetsEvent::NewSet {
@ -47,12 +47,19 @@ serai_test!(
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
let set_data = serai.get_validator_set(set).await.unwrap().unwrap(); let participants = serai
assert_eq!(set_data.network, NETWORKS[&NetworkId::Bitcoin]); .get_validator_set_participants(set.network, serai.get_latest_block_hash().await.unwrap())
let participants_ref: &[_] = set_data.participants.as_ref(); .await
assert_eq!(participants_ref, [(public, set_data.bond)].as_ref()); .unwrap()
.unwrap();
let participants_ref: &[_] = participants.as_ref();
assert_eq!(participants_ref, [public].as_ref());
assert_eq!( 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 musig_key(set, &[public]).0
); );
@ -64,6 +71,6 @@ serai_test!(
serai.get_key_gen_events(block).await.unwrap(), serai.get_key_gen_events(block).await.unwrap(),
vec![ValidatorSetsEvent::KeyGen { set, key_pair: key_pair.clone() }] 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));
} }
); );

View file

@ -26,6 +26,7 @@ fn testnet_genesis(
( (
key, key,
key, key,
// TODO: Properly diversify these?
SessionKeys { babe: key.into(), grandpa: key.into(), authority_discovery: key.into() }, SessionKeys { babe: key.into(), grandpa: key.into(), authority_discovery: key.into() },
) )
}; };
@ -54,12 +55,9 @@ fn testnet_genesis(
}, },
validator_sets: ValidatorSetsConfig { validator_sets: ValidatorSetsConfig {
bond: Amount(1_000_000 * 10_u64.pow(8)), stake: Amount(1_000_000 * 10_u64.pow(8)),
networks: vec![ // TODO: Array of these in primitives
(NetworkId::Bitcoin, NETWORKS[&NetworkId::Bitcoin].clone()), networks: vec![NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero],
(NetworkId::Ethereum, NETWORKS[&NetworkId::Ethereum].clone()),
(NetworkId::Monero, NETWORKS[&NetworkId::Monero].clone()),
],
participants: validators.iter().map(|name| account_from_name(name)).collect(), participants: validators.iter().map(|name| account_from_name(name)).collect(),
}, },
session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() }, session: SessionConfig { keys: validators.iter().map(|name| session_key(*name)).collect() },

View file

@ -12,8 +12,6 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
lazy_static = { version = "1", optional = true }
zeroize = { version = "^1.5", features = ["derive"], optional = true } zeroize = { version = "^1.5", features = ["derive"], optional = true }
serde = { version = "1", default-features = false, features = ["derive", "alloc"] } 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 } sp-runtime = { git = "https://github.com/serai-dex/substrate", default-features = false }
[features] [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"] default = ["std"]

View file

@ -1,6 +1,3 @@
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use zeroize::Zeroize; use zeroize::Zeroize;
@ -120,12 +117,3 @@ impl Network {
&self.coins &self.coins
} }
} }
#[cfg(feature = "std")]
lazy_static::lazy_static! {
pub static ref NETWORKS: HashMap<NetworkId, Network> = 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()),
]);
}

View file

@ -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 } 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 } 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 } 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-session = { git = "https://github.com/serai-dex/substrate", default-features = false }
pallet-babe = { 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 } pallet-grandpa = { git = "https://github.com/serai-dex/substrate", default-features = false }
@ -102,7 +104,9 @@ std = [
"tokens-pallet/std", "tokens-pallet/std",
"in-instructions-pallet/std", "in-instructions-pallet/std",
"staking-pallet/std",
"validator-sets-pallet/std", "validator-sets-pallet/std",
"pallet-session/std", "pallet-session/std",
"pallet-babe/std", "pallet-babe/std",
"pallet-grandpa/std", "pallet-grandpa/std",

View file

@ -21,6 +21,7 @@ pub use pallet_assets as assets;
pub use tokens_pallet as tokens; pub use tokens_pallet as tokens;
pub use in_instructions_pallet as in_instructions; pub use in_instructions_pallet as in_instructions;
pub use staking_pallet as staking;
pub use validator_sets_pallet as validator_sets; pub use validator_sets_pallet as validator_sets;
pub use pallet_session as session; pub use pallet_session as session;
@ -142,7 +143,7 @@ parameter_types! {
NORMAL_DISPATCH_RATIO, NORMAL_DISPATCH_RATIO,
); );
pub const MaxAuthorities: u32 = 100; pub const MaxAuthorities: u32 = validator_sets::primitives::MAX_VALIDATORS_PER_SET;
} }
pub struct CallFilter; pub struct CallFilter;
@ -172,10 +173,24 @@ impl Contains<RuntimeCall> for CallFilter {
return matches!(call, in_instructions::Call::execute_batch { .. }); 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 { if let RuntimeCall::ValidatorSets(call) = call {
return matches!(call, validator_sets::Call::set_keys { .. }); return matches!(call, validator_sets::Call::set_keys { .. });
} }
if let RuntimeCall::Session(call) = call {
return matches!(call, session::Call::set_keys { .. });
}
false false
} }
} }
@ -300,6 +315,10 @@ impl in_instructions::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
} }
impl staking::Config for Runtime {
type Currency = Balances;
}
impl validator_sets::Config for Runtime { impl validator_sets::Config for Runtime {
type RuntimeEvent = RuntimeEvent; type RuntimeEvent = RuntimeEvent;
} }
@ -317,7 +336,7 @@ impl session::Config for Runtime {
type ValidatorIdOf = IdentityValidatorIdOf; type ValidatorIdOf = IdentityValidatorIdOf;
type ShouldEndSession = Babe; type ShouldEndSession = Babe;
type NextSessionRotation = Babe; type NextSessionRotation = Babe;
type SessionManager = (); // TODO? type SessionManager = Staking;
type SessionHandler = <SessionKeys as OpaqueKeys>::KeyTypeIdProviders; type SessionHandler = <SessionKeys as OpaqueKeys>::KeyTypeIdProviders;
type Keys = SessionKeys; type Keys = SessionKeys;
type WeightInfo = session::weights::SubstrateWeight<Runtime>; type WeightInfo = session::weights::SubstrateWeight<Runtime>;
@ -393,6 +412,8 @@ construct_runtime!(
ValidatorSets: validator_sets, ValidatorSets: validator_sets,
Staking: staking,
Session: session, Session: session,
Babe: babe, Babe: babe,
Grandpa: grandpa, Grandpa: grandpa,

View file

@ -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 <lukeparker5132@gmail.com>"]
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"]

View file

@ -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 <http://www.gnu.org/licenses/>.

View file

@ -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<T> {
StakeUnavilable,
}
// TODO: Event
#[pallet::config]
pub trait Config:
frame_system::Config + VsConfig + SessionConfig<ValidatorId = PublicKey>
{
type Currency: Currency<Self::AccountId, Balance = u64>;
}
#[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>);
/// The amount of funds this account has staked.
#[pallet::storage]
#[pallet::getter(fn staked)]
pub type Staked<T: Config> = 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<T: Config> = StorageMap<_, Blake2_128Concat, T::AccountId, u64, ValueQuery>;
impl<T: Config> Pallet<T> {
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::<T>::mutate(account, |staked| *staked += amount);
}
fn remove_stake(account: &T::AccountId, amount: u64) -> DispatchResult {
Staked::<T>::mutate(account, |staked| {
let available = *staked - Self::allocated(account);
if available < amount {
Err(Error::<T>::StakeUnavilable)?;
}
*staked -= amount;
Ok::<_, DispatchError>(())
})
}
fn allocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error<T>> {
Allocated::<T>::try_mutate(account, |allocated| {
let available = Self::staked(account) - *allocated;
if available < amount {
Err(Error::<T>::StakeUnavilable)?;
}
*allocated += amount;
Ok(())
})
}
#[allow(unused)] // TODO
fn deallocate_internal(account: &T::AccountId, amount: u64) -> Result<(), Error<T>> {
Allocated::<T>::try_mutate(account, |allocated| {
if *allocated < amount {
Err(Error::<T>::StakeUnavilable)?;
}
*allocated -= amount;
Ok(())
})
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Stake funds from this account.
#[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn stake(origin: OriginFor<T>, #[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<T>, #[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<T>,
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::<T>::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<T>,
network: NetworkId,
#[pallet::compact] amount: u64,
) -> DispatchResult {
let account = ensure_signed(origin)?;
// decrease allocation in validator set
VsPallet::<T>::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<T: Config> SessionManager<T::ValidatorId> for Pallet<T> {
fn new_session(_new_index: u32) -> Option<Vec<T::ValidatorId>> {
// 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::<T>::block_number() > 1u32.into() {
VsPallet::<T>::new_session();
}
// TODO: Where do we return their stake?
Some(VsPallet::<T>::validators(NetworkId::Serai))
}
fn new_session_genesis(_: u32) -> Option<Vec<T::ValidatorId>> {
Some(VsPallet::<T>::validators(NetworkId::Serai))
}
fn end_session(_end_index: u32) {}
fn start_session(_start_index: u32) {}
}
}
pub use pallet::*;

View file

@ -53,7 +53,7 @@ pub mod pallet {
} }
pub fn mint(address: SeraiAddress, balance: Balance) { 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::<T>::mint( AssetsPallet::<T>::mint(
RawOrigin::Signed(ADDRESS.into()).into(), RawOrigin::Signed(ADDRESS.into()).into(),
balance.coin, balance.coin,

View file

@ -18,6 +18,7 @@ scale = { package = "parity-scale-codec", version = "3", default-features = fals
scale-info = { version = "2", default-features = false, features = ["derive"] } scale-info = { version = "2", default-features = false, features = ["derive"] }
sp-core = { git = "https://github.com/serai-dex/substrate", default-features = false } 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-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-application-crypto = { git = "https://github.com/serai-dex/substrate", default-features = false }
sp-runtime = { 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-system = { git = "https://github.com/serai-dex/substrate", default-features = false }
frame-support = { 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 } serai-primitives = { path = "../../primitives", default-features = false }
validator-sets-primitives = { package = "serai-validator-sets-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-system/std",
"frame-support/std", "frame-support/std",
"pallet-session/std",
"serai-primitives/std", "serai-primitives/std",
"validator-sets-primitives/std", "validator-sets-primitives/std",
] ]

View file

@ -6,30 +6,34 @@ pub mod pallet {
use scale_info::TypeInfo; use scale_info::TypeInfo;
use sp_core::sr25519::{Public, Signature}; use sp_core::sr25519::{Public, Signature};
use sp_std::vec::Vec; use sp_std::{vec, vec::Vec};
use sp_application_crypto::RuntimePublic; use sp_application_crypto::RuntimePublic;
use frame_system::pallet_prelude::*; use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::*; use frame_support::{pallet_prelude::*, StoragePrefixedMap};
use serai_primitives::*; use serai_primitives::*;
pub use validator_sets_primitives as primitives; pub use validator_sets_primitives as primitives;
use primitives::*; use primitives::*;
#[pallet::config] #[pallet::config]
pub trait Config: frame_system::Config<AccountId = Public> + TypeInfo { pub trait Config:
frame_system::Config<AccountId = Public> + pallet_session::Config + TypeInfo
{
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>; type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
} }
#[pallet::genesis_config] #[pallet::genesis_config]
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
pub struct GenesisConfig<T: Config> { pub struct GenesisConfig<T: Config> {
/// Bond requirement to join the initial validator sets. /// Stake 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. /// Every participant at genesis will automatically be assumed to have this much stake.
pub bond: Amount, /// 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. /// Networks to spawn Serai with.
pub networks: Vec<(NetworkId, Network)>, pub networks: Vec<NetworkId>,
/// List of participants to place in the initial validator sets. /// List of participants to place in the initial validator sets.
pub participants: Vec<T::AccountId>, pub participants: Vec<T::AccountId>,
} }
@ -37,7 +41,7 @@ pub mod pallet {
impl<T: Config> Default for GenesisConfig<T> { impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self { fn default() -> Self {
GenesisConfig { GenesisConfig {
bond: Amount(1), stake: Amount(1),
networks: Default::default(), networks: Default::default(),
participants: Default::default(), participants: Default::default(),
} }
@ -47,18 +51,82 @@ pub mod pallet {
#[pallet::pallet] #[pallet::pallet]
pub struct Pallet<T>(PhantomData<T>); pub struct Pallet<T>(PhantomData<T>);
/// 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::storage]
#[pallet::getter(fn validator_set)] pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
pub type ValidatorSets<T: Config> = impl<T: Config> Pallet<T> {
StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>; fn session(network: NetworkId) -> Session {
if network == NetworkId::Serai {
Session(pallet_session::Pallet::<T>::current_index())
} else {
CurrentSession::<T>::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<T: Config> = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
/// The validators selected to be in-set.
#[pallet::storage]
#[pallet::getter(fn participants)]
pub type Participants<T: Config> = StorageMap<
_,
Identity,
NetworkId,
BoundedVec<Public, ConstU32<{ MAX_VALIDATORS_PER_SET }>>,
ValueQuery,
>;
/// The validators selected to be in-set, yet with the ability to perform a check for presence.
#[pallet::storage]
pub type InSet<T: Config> = 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<T: Config> =
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<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 8], Public), (), OptionQuery>;
impl<T: Config> Pallet<T> {
/// 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::<T>::take((network, key));
if prior.is_some() {
SortedAllocations::<T>::remove((network, Self::lexicographic_amount(amount), key));
}
if amount.0 != 0 {
Allocations::<T>::set((network, key), Some(amount));
SortedAllocations::<T>::set((network, Self::lexicographic_amount(amount), key), Some(()));
}
}
}
/// The MuSig key for a validator set. /// The MuSig key for a validator set.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn musig_key)] #[pallet::getter(fn musig_key)]
pub type MuSigKeys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>; pub type MuSigKeys<T: Config> = 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::storage]
#[pallet::getter(fn keys)] #[pallet::getter(fn keys)]
pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>; pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>;
@ -70,33 +138,62 @@ pub mod pallet {
KeyGen { set: ValidatorSet, key_pair: KeyPair }, KeyGen { set: ValidatorSet, key_pair: KeyPair },
} }
#[pallet::genesis_build] impl<T: Config> Pallet<T> {
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> { fn new_set(network: NetworkId) {
fn build(&self) { // Update CurrentSession
let hash_set = let session = if network != NetworkId::Serai {
self.participants.iter().map(|key| key.0).collect::<hashbrown::HashSet<[u8; 32]>>(); CurrentSession::<T>::mutate(network, |session| {
if hash_set.len() != self.participants.len() { Some(session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0)))
panic!("participants contained duplicates"); })
.unwrap()
} else {
Self::session(network)
};
// Clear the current InSet
{
let mut in_set_key = InSet::<T>::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 mut participants = Vec::new(); let mut prefix = SortedAllocations::<T>::final_prefix().to_vec();
for participant in self.participants.clone() { prefix.extend(&network.encode());
participants.push((participant, self.bond)); let prefix = prefix;
}
let participants = BoundedVec::try_from(participants).unwrap();
for (id, network) in self.networks.clone() { let mut last = prefix.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::<T>::set(
set,
Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }),
);
MuSigKeys::<T>::set(set, Some(musig_key(set, &self.participants))); let mut participants = vec![];
Pallet::<T>::deposit_event(Event::NewSet { set }) 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::<T>::set((network, key), Some(()));
participants.push(key);
last = next;
}
assert!(!participants.is_empty());
let set = ValidatorSet { network, session };
Pallet::<T>::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::<T>::remove(prior_to_now_rotating);
Keys::<T>::remove(prior_to_now_rotating);
}
MuSigKeys::<T>::set(set, Some(musig_key(set, &participants)));
}
Participants::<T>::set(network, participants.try_into().unwrap());
} }
} }
@ -104,10 +201,40 @@ pub mod pallet {
pub enum Error<T> { pub enum Error<T> {
/// Validator Set doesn't exist. /// Validator Set doesn't exist.
NonExistentValidatorSet, 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. /// Validator Set already generated keys.
AlreadyGeneratedKeys, AlreadyGeneratedKeys,
/// An invalid MuSig signature was provided. /// An invalid MuSig signature was provided.
BadSignature, BadSignature,
/// Validator wasn't registered or active.
NonExistentValidator,
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
{
let hash_set =
self.participants.iter().map(|key| key.0).collect::<hashbrown::HashSet<[u8; 32]>>();
if hash_set.len() != self.participants.len() {
panic!("participants contained duplicates");
}
}
for id in self.networks.clone() {
MinimumAllocation::<T>::set(id, Some(self.stake));
for participant in self.participants.clone() {
Pallet::<T>::set_allocation(id, participant, self.stake);
}
Pallet::<T>::new_set(id);
}
}
} }
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
@ -116,6 +243,7 @@ pub mod pallet {
key_pair: &KeyPair, key_pair: &KeyPair,
signature: &Signature, signature: &Signature,
) -> Result<(), Error<T>> { ) -> Result<(), Error<T>> {
// Confirm a key hasn't been set for this set instance
if Keys::<T>::get(set).is_some() { if Keys::<T>::get(set).is_some() {
Err(Error::AlreadyGeneratedKeys)? Err(Error::AlreadyGeneratedKeys)?
} }
@ -141,10 +269,8 @@ pub mod pallet {
) -> DispatchResult { ) -> DispatchResult {
ensure_none(origin)?; ensure_none(origin)?;
// TODO: Get session let session = Session(pallet_session::Pallet::<T>::current_index());
let session: Session = Session(0);
// Confirm a key hasn't been set for this set instance
let set = ValidatorSet { session, network }; let set = ValidatorSet { session, network };
// TODO: Is this needed? validate_unsigned should be called before this and ensure it's Ok // TODO: Is this needed? validate_unsigned should be called before this and ensure it's Ok
Self::verify_signature(set, &key_pair, &signature)?; Self::verify_signature(set, &key_pair, &signature)?;
@ -167,15 +293,17 @@ pub mod pallet {
Call::__Ignore(_, _) => unreachable!(), Call::__Ignore(_, _) => unreachable!(),
}; };
// TODO: Get the latest session let session = Session(pallet_session::Pallet::<T>::current_index());
let session = Session(0);
let set = ValidatorSet { session, network: *network }; let set = ValidatorSet { session, network: *network };
match Self::verify_signature(set, key_pair, signature) { match Self::verify_signature(set, key_pair, signature) {
Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?,
Err(Error::NonExistentValidatorSet) | Err(Error::BadSignature) => { Err(Error::NonExistentValidatorSet) |
Err(InvalidTransaction::BadProof)? Err(Error::InsufficientStake) |
} Err(Error::InsufficientAllocation) |
Err(Error::DeallocationWouldRemoveParticipant) |
Err(Error::NonExistentValidator) |
Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
Err(Error::__Ignore(_, _)) => unreachable!(), Err(Error::__Ignore(_, _)) => unreachable!(),
Ok(()) => (), Ok(()) => (),
} }
@ -189,7 +317,80 @@ pub mod pallet {
} }
} }
// TODO: Support session rotation impl<T: Config> Pallet<T> {
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::<T>::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::<T>::NonExistentValidator)?
.0
.checked_sub(amount.0)
.ok_or(Error::<T>::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::<T>::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::<T>::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::<T>::new_set(network);
}
}
}
pub fn validators(network: NetworkId) -> Vec<Public> {
Self::participants(network).into()
}
}
} }
pub use pallet::*; pub use pallet::*;

View file

@ -13,8 +13,10 @@ use sp_core::{ConstU32, sr25519::Public, bounded::BoundedVec};
#[cfg(not(feature = "std"))] #[cfg(not(feature = "std"))]
use sp_std::vec::Vec; 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). // Support keys up to 96 bytes (BLS12-381 G2).
const MAX_KEY_LEN: u32 = 96; const MAX_KEY_LEN: u32 = 96;
@ -32,6 +34,7 @@ const MAX_KEY_LEN: u32 = 96;
Decode, Decode,
TypeInfo, TypeInfo,
MaxEncodedLen, MaxEncodedLen,
Default,
)] )]
#[cfg_attr(feature = "std", derive(Zeroize))] #[cfg_attr(feature = "std", derive(Zeroize))]
pub struct Session(pub u32); pub struct Session(pub u32);
@ -57,17 +60,6 @@ pub struct ValidatorSet {
pub network: NetworkId, 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<MAX_KEY_LEN>; type MaxKeyLen = ConstU32<MAX_KEY_LEN>;
/// The type representing a Key from an external network. /// The type representing a Key from an external network.
pub type ExternalKey = BoundedVec<u8, MaxKeyLen>; pub type ExternalKey = BoundedVec<u8, MaxKeyLen>;

View file

@ -164,7 +164,11 @@ pub async fn key_gen<C: Ciphersuite>(
} }
} }
assert_eq!( 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()) (Public(substrate_key), network_key.try_into().unwrap())
); );

View file

@ -195,8 +195,13 @@ async fn mint_and_burn_test() {
let halt_at = if additional { 5 * 10 } else { 10 * 10 }; let halt_at = if additional { 5 * 10 } else { 10 * 10 };
let print_at = halt_at / 2; let print_at = halt_at / 2;
for i in 0 .. halt_at { for i in 0 .. halt_at {
if let Some(key_pair) = if let Some(key_pair) = serai
serai.get_keys(ValidatorSet { network, session: Session(0) }).await.unwrap() .get_keys(
ValidatorSet { network, session: Session(0) },
serai.get_latest_block_hash().await.unwrap(),
)
.await
.unwrap()
{ {
return key_pair; return key_pair;
} }