mirror of
https://github.com/serai-dex/serai.git
synced 2024-11-16 17:07:35 +00:00
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:
parent
2f45bba2d4
commit
98190b7b83
25 changed files with 635 additions and 149 deletions
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -46,6 +46,8 @@ members = [
|
|||
"substrate/validator-sets/primitives",
|
||||
"substrate/validator-sets/pallet",
|
||||
|
||||
"substrate/staking/pallet",
|
||||
|
||||
"substrate/runtime",
|
||||
"substrate/node",
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -35,12 +35,14 @@ async fn in_set(
|
|||
key: &Zeroizing<<Ristretto as Ciphersuite>::F>,
|
||||
serai: &Serai,
|
||||
set: ValidatorSet,
|
||||
block_hash: [u8; 32],
|
||||
) -> 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);
|
||||
};
|
||||
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)>(
|
||||
|
@ -51,10 +53,13 @@ async fn handle_new_set<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>(
|
|||
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<D: Db, CNT: Clone + Fn(&mut D, TributarySpec)>(
|
|||
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);
|
||||
|
|
|
@ -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<R: RngCore + CryptoRng>(
|
|||
|
||||
let set = ValidatorSet { session: Session(0), network: NetworkId::Bitcoin };
|
||||
|
||||
let set_data = ValidatorSetData {
|
||||
bond: Amount(100),
|
||||
network: NETWORKS[&NetworkId::Bitcoin].clone(),
|
||||
participants: keys
|
||||
let set_participants = keys
|
||||
.iter()
|
||||
.map(|key| {
|
||||
(sr25519::Public((<Ristretto as Ciphersuite>::generator() * **key).to_bytes()), Amount(100))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.try_into()
|
||||
.unwrap(),
|
||||
};
|
||||
.map(|key| sr25519::Public((<Ristretto as Ciphersuite>::generator() * **key).to_bytes()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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<PublicKey>,
|
||||
) -> 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 = <Ristretto as Ciphersuite>::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 }
|
||||
|
|
|
@ -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" },
|
||||
|
||||
|
|
|
@ -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<Option<ValidatorSetData>, SeraiError> {
|
||||
self
|
||||
.storage(
|
||||
PALLET,
|
||||
"ValidatorSets",
|
||||
Some(vec![scale_value(set)]),
|
||||
self.get_latest_block_hash().await?,
|
||||
)
|
||||
.await
|
||||
network: NetworkId,
|
||||
at_hash: [u8; 32],
|
||||
) -> Result<Option<Vec<Public>>, 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<Option<[u8; 32]>, 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<Option<KeyPair>, 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<Option<KeyPair>, SeraiError> {
|
||||
self.storage(PALLET, "Keys", Some(vec![scale_value(set)]), at_hash).await
|
||||
}
|
||||
|
||||
pub fn set_validator_set_keys(
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -28,7 +28,11 @@ pub async fn set_validator_set_keys(set: ValidatorSet, key_pair: KeyPair) -> [u8
|
|||
let serai = serai().await;
|
||||
let public_key = <Ristretto as Ciphersuite>::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::<Ristretto>(&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
|
||||
}
|
||||
|
|
|
@ -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::<Vec<_>>(),
|
||||
);
|
||||
|
||||
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));
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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() },
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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<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()),
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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<RuntimeCall> 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 = <SessionKeys as OpaqueKeys>::KeyTypeIdProviders;
|
||||
type Keys = SessionKeys;
|
||||
type WeightInfo = session::weights::SubstrateWeight<Runtime>;
|
||||
|
@ -393,6 +412,8 @@ construct_runtime!(
|
|||
|
||||
ValidatorSets: validator_sets,
|
||||
|
||||
Staking: staking,
|
||||
|
||||
Session: session,
|
||||
Babe: babe,
|
||||
Grandpa: grandpa,
|
||||
|
|
46
substrate/staking/pallet/Cargo.toml
Normal file
46
substrate/staking/pallet/Cargo.toml
Normal 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"]
|
15
substrate/staking/pallet/LICENSE
Normal file
15
substrate/staking/pallet/LICENSE
Normal 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/>.
|
180
substrate/staking/pallet/src/lib.rs
Normal file
180
substrate/staking/pallet/src/lib.rs
Normal 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::*;
|
|
@ -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::<T>::mint(
|
||||
RawOrigin::Signed(ADDRESS.into()).into(),
|
||||
balance.coin,
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
|
|
|
@ -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<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>>;
|
||||
}
|
||||
|
||||
#[pallet::genesis_config]
|
||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)]
|
||||
pub struct GenesisConfig<T: Config> {
|
||||
/// 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<NetworkId>,
|
||||
/// List of participants to place in the initial validator sets.
|
||||
pub participants: Vec<T::AccountId>,
|
||||
}
|
||||
|
@ -37,7 +41,7 @@ pub mod pallet {
|
|||
impl<T: Config> Default for GenesisConfig<T> {
|
||||
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<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::getter(fn validator_set)]
|
||||
pub type ValidatorSets<T: Config> =
|
||||
StorageMap<_, Twox64Concat, ValidatorSet, ValidatorSetData, OptionQuery>;
|
||||
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
|
||||
impl<T: Config> Pallet<T> {
|
||||
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.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn musig_key)]
|
||||
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::getter(fn keys)]
|
||||
pub type Keys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>;
|
||||
|
@ -70,33 +138,62 @@ pub mod pallet {
|
|||
KeyGen { set: ValidatorSet, key_pair: KeyPair },
|
||||
}
|
||||
|
||||
#[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");
|
||||
impl<T: Config> Pallet<T> {
|
||||
fn new_set(network: NetworkId) {
|
||||
// Update CurrentSession
|
||||
let session = if network != NetworkId::Serai {
|
||||
CurrentSession::<T>::mutate(network, |session| {
|
||||
Some(session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0)))
|
||||
})
|
||||
.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();
|
||||
for participant in self.participants.clone() {
|
||||
participants.push((participant, self.bond));
|
||||
}
|
||||
let participants = BoundedVec::try_from(participants).unwrap();
|
||||
let mut prefix = SortedAllocations::<T>::final_prefix().to_vec();
|
||||
prefix.extend(&network.encode());
|
||||
let prefix = prefix;
|
||||
|
||||
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::<T>::set(
|
||||
set,
|
||||
Some(ValidatorSetData { bond: self.bond, network, participants: participants.clone() }),
|
||||
);
|
||||
let mut last = prefix.clone();
|
||||
|
||||
MuSigKeys::<T>::set(set, Some(musig_key(set, &self.participants)));
|
||||
Pallet::<T>::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::<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> {
|
||||
/// 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<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> {
|
||||
|
@ -116,6 +243,7 @@ pub mod pallet {
|
|||
key_pair: &KeyPair,
|
||||
signature: &Signature,
|
||||
) -> Result<(), Error<T>> {
|
||||
// Confirm a key hasn't been set for this set instance
|
||||
if Keys::<T>::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::<T>::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::<T>::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<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::*;
|
||||
|
|
|
@ -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<MAX_KEY_LEN>;
|
||||
/// The type representing a Key from an external network.
|
||||
pub type ExternalKey = BoundedVec<u8, MaxKeyLen>;
|
||||
|
|
|
@ -164,7 +164,11 @@ pub async fn key_gen<C: Ciphersuite>(
|
|||
}
|
||||
}
|
||||
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())
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue