mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 20:29:23 +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:
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user