#![cfg_attr(not(feature = "std"), no_std)] #[allow(deprecated, clippy::let_unit_value)] // TODO #[frame_support::pallet] pub mod pallet { use scale_info::TypeInfo; use sp_core::sr25519::{Public, Signature}; use sp_std::{vec, vec::Vec}; use sp_application_crypto::RuntimePublic; use frame_system::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 + pallet_session::Config + TypeInfo { type RuntimeEvent: IsType<::RuntimeEvent> + From>; } #[pallet::genesis_config] #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode)] pub struct GenesisConfig { /// 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, /// List of participants to place in the initial validator sets. pub participants: Vec, } impl Default for GenesisConfig { fn default() -> Self { GenesisConfig { stake: Amount(1), networks: Default::default(), participants: Default::default(), } } } #[pallet::pallet] pub struct Pallet(PhantomData); /// 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] pub type CurrentSession = StorageMap<_, Identity, NetworkId, Session, OptionQuery>; impl Pallet { fn session(network: NetworkId) -> Session { if network == NetworkId::Serai { Session(pallet_session::Pallet::::current_index()) } else { CurrentSession::::get(network).unwrap() } } } /// The minimum allocation required to join a validator set. // Uses Identity for the lookup to avoid a hash of a severely limited fixed key-space. #[pallet::storage] #[pallet::getter(fn minimum_allocation)] pub type MinimumAllocation = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>; /// The validators selected to be in-set. #[pallet::storage] #[pallet::getter(fn participants)] pub type Participants = StorageMap< _, Identity, NetworkId, BoundedVec>, ValueQuery, >; /// The validators selected to be in-set, yet with the ability to perform a check for presence. #[pallet::storage] pub type InSet = StorageMap<_, Blake2_128Concat, (NetworkId, Public), (), OptionQuery>; /// The current amount allocated to a validator set by a validator. #[pallet::storage] #[pallet::getter(fn allocation)] pub type Allocations = StorageMap<_, Blake2_128Concat, (NetworkId, Public), Amount, OptionQuery>; /// A sorted view of the current allocations premised on the underlying DB itself being sorted. // Uses Identity so we can iterate over the key space from highest-to-lowest allocated. // While this does enable attacks the hash is meant to prevent, the minimum stake should resolve // these. #[pallet::storage] type SortedAllocations = StorageMap<_, Identity, (NetworkId, [u8; 8], Public), (), OptionQuery>; impl Pallet { /// A function which takes an amount and generates a byte array with a lexicographic order from /// high amount to low amount. #[inline] fn lexicographic_amount(amount: Amount) -> [u8; 8] { let mut bytes = amount.0.to_be_bytes(); for byte in &mut bytes { *byte = !*byte; } bytes } fn set_allocation(network: NetworkId, key: Public, amount: Amount) { let prior = Allocations::::take((network, key)); if prior.is_some() { SortedAllocations::::remove((network, Self::lexicographic_amount(amount), key)); } if amount.0 != 0 { Allocations::::set((network, key), Some(amount)); SortedAllocations::::set((network, Self::lexicographic_amount(amount), key), Some(())); } } } /// The MuSig key for a validator set. #[pallet::storage] #[pallet::getter(fn musig_key)] pub type MuSigKeys = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>; /// The generated key pair for a given validator set instance. #[pallet::storage] #[pallet::getter(fn keys)] pub type Keys = StorageMap<_, Twox64Concat, ValidatorSet, KeyPair, OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { NewSet { set: ValidatorSet }, KeyGen { set: ValidatorSet, key_pair: KeyPair }, } impl Pallet { fn new_set(network: NetworkId) { // Update CurrentSession let session = if network != NetworkId::Serai { CurrentSession::::mutate(network, |session| { Some(session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0))) }) .unwrap() } else { Self::session(network) }; // Clear the current InSet { let mut in_set_key = InSet::::final_prefix().to_vec(); in_set_key.extend(network.encode()); assert!(matches!( sp_io::storage::clear_prefix(&in_set_key, Some(MAX_VALIDATORS_PER_SET)), sp_io::KillStorageResult::AllRemoved(_) )); } let mut prefix = SortedAllocations::::final_prefix().to_vec(); prefix.extend(&network.encode()); let prefix = prefix; let mut last = prefix.clone(); let mut participants = vec![]; for _ in 0 .. MAX_VALIDATORS_PER_SET { let Some(next) = sp_io::storage::next_key(&last) else { break }; if !next.starts_with(&prefix) { break; } assert_eq!(next.len(), (32 + 1 + 8 + 32)); let key = Public(next[(next.len() - 32) .. next.len()].try_into().unwrap()); InSet::::set((network, key), Some(())); participants.push(key); last = next; } assert!(!participants.is_empty()); let set = ValidatorSet { network, session }; Pallet::::deposit_event(Event::NewSet { set }); if network != NetworkId::Serai { // Remove the keys for the set prior to the one now rotating out if session.0 >= 2 { let prior_to_now_rotating = ValidatorSet { network, session: Session(session.0 - 2) }; MuSigKeys::::remove(prior_to_now_rotating); Keys::::remove(prior_to_now_rotating); } MuSigKeys::::set(set, Some(musig_key(set, &participants))); } Participants::::set(network, participants.try_into().unwrap()); } } #[pallet::error] pub enum Error { /// Validator Set doesn't exist. NonExistentValidatorSet, /// Not enough stake to participate in a set. InsufficientStake, /// Trying to deallocate more than allocated. InsufficientAllocation, /// Deallocation would remove the participant from the set, despite the validator not /// specifying so. DeallocationWouldRemoveParticipant, /// Validator Set already generated keys. AlreadyGeneratedKeys, /// An invalid MuSig signature was provided. BadSignature, /// Validator wasn't registered or active. NonExistentValidator, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { { let hash_set = self.participants.iter().map(|key| key.0).collect::>(); if hash_set.len() != self.participants.len() { panic!("participants contained duplicates"); } } for id in self.networks.clone() { MinimumAllocation::::set(id, Some(self.stake)); for participant in self.participants.clone() { Pallet::::set_allocation(id, participant, self.stake); } Pallet::::new_set(id); } } } impl Pallet { fn verify_signature( set: ValidatorSet, key_pair: &KeyPair, signature: &Signature, ) -> Result<(), Error> { // Confirm a key hasn't been set for this set instance if Keys::::get(set).is_some() { Err(Error::AlreadyGeneratedKeys)? } let Some(musig_key) = MuSigKeys::::get(set) else { Err(Error::NonExistentValidatorSet)? }; if !musig_key.verify(&set_keys_message(&set, key_pair), signature) { Err(Error::BadSignature)?; } Ok(()) } } #[pallet::call] impl Pallet { #[pallet::call_index(0)] #[pallet::weight(0)] // TODO pub fn set_keys( origin: OriginFor, network: NetworkId, key_pair: KeyPair, signature: Signature, ) -> DispatchResult { ensure_none(origin)?; let session = Session(pallet_session::Pallet::::current_index()); 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)?; Keys::::set(set, Some(key_pair.clone())); Self::deposit_event(Event::KeyGen { set, key_pair }); Ok(()) } } #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity { // Match to be exhaustive let (network, key_pair, signature) = match call { Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature), Call::__Ignore(_, _) => unreachable!(), }; let session = Session(pallet_session::Pallet::::current_index()); let set = ValidatorSet { session, network: *network }; match Self::verify_signature(set, key_pair, signature) { Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?, Err(Error::NonExistentValidatorSet) | Err(Error::InsufficientStake) | Err(Error::InsufficientAllocation) | Err(Error::DeallocationWouldRemoveParticipant) | Err(Error::NonExistentValidator) | Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?, Err(Error::__Ignore(_, _)) => unreachable!(), Ok(()) => (), } ValidTransaction::with_tag_prefix("validator-sets") .and_provides(set) // Set a 10 block longevity, though this should be included in the next block .longevity(10) .propagate(true) .build() } } impl Pallet { pub fn increase_allocation( network: NetworkId, account: T::AccountId, amount: Amount, ) -> DispatchResult { let new_allocation = Self::allocation((network, account)).unwrap_or(Amount(0)).0 + amount.0; if new_allocation < Self::minimum_allocation(network).unwrap().0 { Err(Error::::InsufficientStake)?; } Self::set_allocation(network, account, Amount(new_allocation)); Ok(()) } /// Decreases a validator's allocation to a set. /// /// Errors if the capacity provided by this allocation is in use. /// /// Errors if a partial decrease of allocation which puts the allocation below the minimum. /// /// The capacity prior provided by the allocation is immediately removed, in order to ensure it /// doesn't become used (preventing deallocation). pub fn decrease_allocation( network: NetworkId, account: T::AccountId, amount: Amount, ) -> DispatchResult { // TODO: Check it's safe to decrease this set's stake by this amount let new_allocation = Self::allocation((network, account)) .ok_or(Error::::NonExistentValidator)? .0 .checked_sub(amount.0) .ok_or(Error::::InsufficientAllocation)?; // If we're not removing the entire allocation, yet the allocation is no longer at or above // the minimum stake, error if (new_allocation != 0) && (new_allocation < Self::minimum_allocation(network).unwrap_or(Amount(0)).0) { Err(Error::::DeallocationWouldRemoveParticipant)?; } // TODO: Error if we're about to be removed, and the remaining set size would be <4 // Decrease the allocation now Self::set_allocation(network, account, Amount(new_allocation)); // Set it to PendingDeallocation, letting the staking pallet release it AFTER this session // TODO // TODO: We can immediately free it if it doesn't cross a key share threshold Ok(()) } pub fn new_session() { // TODO: Define an array of all networks in primitives let networks = [NetworkId::Serai, NetworkId::Bitcoin, NetworkId::Ethereum, NetworkId::Monero]; for network in networks { // Handover is automatically complete for Serai as it doesn't have a handover protocol // TODO: Update how handover completed is determined. It's not on set keys. It's on new // set accepting responsibility let handover_completed = (network == NetworkId::Serai) || Keys::::contains_key(ValidatorSet { network, session: Self::session(network) }); // Only spawn a NewSet if the current set was actually established with a completed // handover protocol if handover_completed { Pallet::::new_set(network); } } } pub fn validators(network: NetworkId) -> Vec { Self::participants(network).into() } } } pub use pallet::*;