diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index afd2ad0a..5ed222d3 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -3,21 +3,13 @@ use borsh::{BorshSerialize, BorshDeserialize}; use sp_core::{ConstU32, bounded::BoundedVec}; use serai_primitives::{ - crypto::{ExternalKey, KeyPair, Signature}, + crypto::{ExternalKey, EmbeddedEllipticCurveKeys, KeyPair, Signature}, address::SeraiAddress, balance::Amount, network_id::*, validator_sets::*, }; -/// Key(s) on embedded elliptic curve(s). -/// -/// This may be a single key if the external network uses the same embedded elliptic curve as -/// used for the key to oraclize onto Serai. Else, it'll be a key on the embedded elliptic curve -/// used for the key to oraclize onto Serai concatenated with the key on the embedded elliptic -/// curve used for the external network. -pub type EmbeddedEllipticCurveKeys = BoundedVec<u8, ConstU32<{ 2 * ExternalKey::MAX_LEN }>>; - /// A call to the validator sets. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] pub enum Call { diff --git a/substrate/primitives/src/crypto.rs b/substrate/primitives/src/crypto.rs index 12398aa8..1e32b04c 100644 --- a/substrate/primitives/src/crypto.rs +++ b/substrate/primitives/src/crypto.rs @@ -5,6 +5,10 @@ use sp_core::{ConstU32, bounded::BoundedVec}; /// A Ristretto public key. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub struct Public(pub [u8; 32]); impl From<sp_core::sr25519::Public> for Public { fn from(public: sp_core::sr25519::Public) -> Self { @@ -19,6 +23,10 @@ impl From<Public> for sp_core::sr25519::Public { /// A sr25519 signature. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub struct Signature(pub [u8; 64]); impl From<sp_core::sr25519::Signature> for Signature { fn from(signature: sp_core::sr25519::Signature) -> Self { @@ -33,6 +41,10 @@ impl From<Signature> for sp_core::sr25519::Signature { /// A key for an external network. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub struct ExternalKey( #[borsh( serialize_with = "crate::borsh_serialize_bounded_vec", @@ -58,9 +70,21 @@ impl ExternalKey { pub const MAX_LEN: u32 = 96; } +/// Key(s) on embedded elliptic curve(s). +/// +/// This may be a single key if the external network uses the same embedded elliptic curve as +/// used for the key to oraclize onto Serai. Else, it'll be a key on the embedded elliptic curve +/// used for the key to oraclize onto Serai concatenated with the key on the embedded elliptic +/// curve used for the external network. +pub type EmbeddedEllipticCurveKeys = BoundedVec<u8, ConstU32<{ 2 * ExternalKey::MAX_LEN }>>; + /// The key pair for a validator set. /// /// This is their Ristretto key, used for publishing data onto Serai, and their key on the external /// network. #[derive(Clone, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub struct KeyPair(pub Public, pub ExternalKey); diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index ca2e6821..b047dbb8 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -84,3 +84,15 @@ impl From<sp_core::H256> for BlockHash { // These share encodings as 32-byte arrays impl scale::EncodeLike<sp_core::H256> for BlockHash {} impl scale::EncodeLike<sp_core::H256> for &BlockHash {} + +#[doc(hidden)] +pub mod prelude { + pub use crate::{BlockNumber, BlockHash}; + pub use crate::constants::*; + pub use crate::address::*; + pub use crate::coin::*; + pub use crate::balance::*; + pub use crate::network_id::*; + pub use crate::validator_sets::*; + pub use crate::instructions::*; +} diff --git a/substrate/primitives/src/validator_sets/slashes.rs b/substrate/primitives/src/validator_sets/slashes.rs index acc4a68d..15ad8495 100644 --- a/substrate/primitives/src/validator_sets/slashes.rs +++ b/substrate/primitives/src/validator_sets/slashes.rs @@ -20,6 +20,10 @@ fn downtime_per_slash_point(validators: NonZero<u16>) -> Duration { /// A slash for a validator. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub enum Slash { /// The slash points accumulated by this validator. /// @@ -198,6 +202,10 @@ impl Slash { /// A report of all slashes incurred for a `ValidatorSet`. #[derive(Clone, PartialEq, Eq, Debug, BorshSerialize, BorshDeserialize)] +#[cfg_attr( + feature = "non_canonical_scale_derivations", + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) +)] pub struct SlashReport( #[borsh( serialize_with = "crate::borsh_serialize_bounded_vec", diff --git a/substrate/validator-sets/src/allocations.rs b/substrate/validator-sets/src/allocations.rs index fb018f1f..d6e070ee 100644 --- a/substrate/validator-sets/src/allocations.rs +++ b/substrate/validator-sets/src/allocations.rs @@ -5,9 +5,9 @@ use serai_primitives::{constants::MAX_KEY_SHARES_PER_SET, network_id::NetworkId, use frame_support::storage::{StorageMap, StoragePrefixedMap}; /// The key to use for the allocations map. -type AllocationsKey = (NetworkId, Public); +pub(crate) type AllocationsKey = (NetworkId, Public); /// The key to use for the sorted allocations map. -type SortedAllocationsKey = (NetworkId, [u8; 8], [u8; 16], Public); +pub(crate) type SortedAllocationsKey = (NetworkId, [u8; 8], [u8; 16], Public); /// The storage underlying `Allocations`. /// @@ -150,11 +150,8 @@ impl<Storage: AllocationsStorage> Allocations for Storage { } fn expected_key_shares(network: NetworkId, allocation_per_key_share: Amount) -> u64 { - let mut validators_len = 0; let mut total_key_shares = 0; for (_, amount) in Self::iter_allocations(network, allocation_per_key_share) { - validators_len += 1; - let key_shares = amount.0 / allocation_per_key_share.0; total_key_shares += key_shares; diff --git a/substrate/validator-sets/src/sessions.rs b/substrate/validator-sets/src/sessions.rs index 4fd05476..841648f6 100644 --- a/substrate/validator-sets/src/sessions.rs +++ b/substrate/validator-sets/src/sessions.rs @@ -7,21 +7,21 @@ use serai_primitives::{ validator_sets::{Session, ValidatorSet, amortize_excess_key_shares}, }; -use frame_support::storage::{StorageValue, StorageMap, StoragePrefixedMap}; +use frame_support::storage::{StorageValue, StorageMap, StorageDoubleMap, StoragePrefixedMap}; use crate::allocations::*; /// The list of genesis validators. -type GenesisValidators = BoundedVec<Public, ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>; +pub(crate) type GenesisValidators = BoundedVec<Public, ConstU32<{ MAX_KEY_SHARES_PER_SET_U32 }>>; /// The key for the SelectedValidators map. -type SelectedValidatorsKey = (ValidatorSet, [u8; 16], Public); +pub(crate) type SelectedValidatorsKey = (ValidatorSet, [u8; 16], Public); pub(crate) trait SessionsStorage: AllocationsStorage { /// The genesis validators /// /// The usage of is shared with the rest of the pallet. `Sessions` only reads it. - type GenesisValidators: StorageValue<GenesisValidators, Query = GenesisValidators>; + type GenesisValidators: StorageValue<GenesisValidators, Query = Option<GenesisValidators>>; /// The allocation required for a key share. /// @@ -44,12 +44,17 @@ pub(crate) trait SessionsStorage: AllocationsStorage { /// /// This is opaque and to be exclusively read/write by `Sessions`. // The value is how many key shares the validator has. - type SelectedValidators: StorageMap<SelectedValidatorsKey, u64> + StoragePrefixedMap<()>; + type SelectedValidators: StorageMap<SelectedValidatorsKey, u64> + StoragePrefixedMap<u64>; /// The total allocated stake for a network. /// /// This is opaque and to be exclusively read/write by `Sessions`. type TotalAllocatedStake: StorageMap<NetworkId, Amount, Query = Option<Amount>>; + + /// The delayed deallocations. + /// + /// This is opaque and to be exclusively read/write by `Sessions`. + type DelayedDeallocations: StorageDoubleMap<Public, Session, Amount, Query = Option<Amount>>; } /// The storage key for the SelectedValidators map. @@ -58,7 +63,7 @@ fn selected_validators_key(set: ValidatorSet, key: Public) -> SelectedValidators (set, hash, key) } -fn selected_validators<Storage: StorageMap<SelectedValidatorsKey, u64> + StoragePrefixedMap<()>>( +fn selected_validators<Storage: StoragePrefixedMap<u64>>( set: ValidatorSet, ) -> impl Iterator<Item = (Public, u64)> { let mut prefix = Storage::final_prefix().to_vec(); @@ -77,11 +82,7 @@ fn selected_validators<Storage: StorageMap<SelectedValidatorsKey, u64> + Storage ) } -fn clear_selected_validators< - Storage: StorageMap<SelectedValidatorsKey, u64> + StoragePrefixedMap<()>, ->( - set: ValidatorSet, -) { +fn clear_selected_validators<Storage: StoragePrefixedMap<u64>>(set: ValidatorSet) { let mut prefix = Storage::final_prefix().to_vec(); prefix.extend(&set.encode()); assert!(matches!( @@ -96,6 +97,17 @@ pub(crate) enum AllocationError { IntroducesSinglePointOfFailure, } +#[must_use] +pub(crate) enum DeallocationTimeline { + Immediate, + Delayed { unlocks_at: Session }, +} +pub(crate) enum DeallocationError { + NoAllocationPerKeyShareSet, + NotEnoughAllocated, + RemainingAllocationLessThanKeyShare, +} + pub(crate) trait Sessions { /// Attempt to spawn a new session for the specified network. /// @@ -115,11 +127,6 @@ pub(crate) trait Sessions { /// latest-decided session. fn accept_handover(network: NetworkId); - /// Retire a validator set. - /// - /// This MUST be called only for sessions which are no longer current. - fn retire(set: ValidatorSet); - /// Increase a validator's allocation. /// /// This does not perform any transfers of any coins/tokens. It solely performs the book-keeping @@ -129,6 +136,16 @@ pub(crate) trait Sessions { validator: Public, amount: Amount, ) -> Result<(), AllocationError>; + + /// Decrease a validator's allocation. + /// + /// This does not perform any transfers of any coins/tokens. It solely performs the book-keeping + /// of it. + fn decrease_allocation( + network: NetworkId, + validator: Public, + amount: Amount, + ) -> Result<DeallocationTimeline, DeallocationError>; } impl<Storage: SessionsStorage> Sessions for Storage { @@ -176,6 +193,7 @@ impl<Storage: SessionsStorage> Sessions for Storage { if include_genesis_validators { let mut genesis_validators = Storage::GenesisValidators::get() + .expect("genesis validators wasn't set") .into_iter() .map(|validator| (validator, 1)) .collect::<Vec<_>>(); @@ -232,16 +250,14 @@ impl<Storage: SessionsStorage> Sessions for Storage { } // Update the total allocated stake variable to the current session Storage::TotalAllocatedStake::set(network, Some(total_allocated_stake)); - } - fn retire(set: ValidatorSet) { - assert!( - Some(set.session).map(|session| session.0) < - Storage::CurrentSession::get(set.network).map(|session| session.0), - "retiring a set which is active/upcoming" - ); - // Clean-up this set's storage - clear_selected_validators::<Storage::SelectedValidators>(set); + // Clean-up the historic set's storage, if one exists + if let Some(historic_session) = current.0.checked_sub(2).map(Session) { + clear_selected_validators::<Storage::SelectedValidators>(ValidatorSet { + network, + session: historic_session, + }); + } } fn increase_allocation( @@ -310,4 +326,86 @@ impl<Storage: SessionsStorage> Sessions for Storage { Ok(()) } + + fn decrease_allocation( + network: NetworkId, + validator: Public, + amount: Amount, + ) -> Result<DeallocationTimeline, DeallocationError> { + /* + Decrease the allocation. + + This doesn't affect the key shares, as that's immutable after creation, and doesn't affect + affect the `TotalAllocatedStake` as the validator either isn't current or the deallocation + will be queued *but is still considered allocated for this session*. + + When the next set is selected, and becomes current, `TotalAllocatedStake` will be updated + per the allocations as-is. + */ + { + let Some(allocation_per_key_share) = Storage::AllocationPerKeyShare::get(network) else { + Err(DeallocationError::NoAllocationPerKeyShareSet)? + }; + + let existing_allocation = Self::get_allocation(network, validator).unwrap_or(Amount(0)); + let new_allocation = + (existing_allocation - amount).ok_or(DeallocationError::NotEnoughAllocated)?; + if (new_allocation != Amount(0)) && (new_allocation < allocation_per_key_share) { + Err(DeallocationError::RemainingAllocationLessThanKeyShare)? + } + + Self::set_allocation(network, validator, new_allocation); + } + + /* + For a validator present in set #n, they should only be able to deallocate once set #n+2 is + current. That means if set #n is malicious, and they rotate to a malicious set #n+1 with a + reduced stake requirement, further handovers can be stopped during set #n+1 (along with + stopping any pending deallocations). + */ + { + let check_presence = |session| { + Storage::SelectedValidators::contains_key(selected_validators_key( + ValidatorSet { network, session }, + validator, + )) + }; + // Find the latest set this validator was present in, which isn't historic + let find_latest_session = || { + // Check the latest decided session + if let Some(latest) = Storage::LatestDecidedSession::get(network) { + if check_presence(latest) { + return Some(latest); + } + + // If there was a latest decided session, but we weren't in it, check current + if let Some(current) = Storage::CurrentSession::get(network) { + if check_presence(current) { + return Some(current); + } + // Finally, check the prior session, as we shouldn't be able to deallocate from a + // session we were in solely because we weren't selected for further sessions + if let Some(prior) = current.0.checked_sub(1).map(Session) { + if check_presence(prior) { + return Some(prior); + } + } + } + } + None + }; + if let Some(present) = find_latest_session() { + // Because they were present in this session, determine the session this unlocks at + let unlocks_at = Session(present.0 + 2); + Storage::DelayedDeallocations::mutate(validator, unlocks_at, |delayed| { + *delayed = Some((delayed.unwrap_or(Amount(0)) + amount).unwrap()); + }); + return Ok(DeallocationTimeline::Delayed { unlocks_at }); + } + } + + // Because the network either doesn't have a current session, or this validator wasn't present, + // immediately handle the deallocation + Ok(DeallocationTimeline::Immediate) + } }