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"
|
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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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",
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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" },
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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() },
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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()),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
|
|
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) {
|
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,
|
||||||
|
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
@ -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::*;
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue