From 8bafeab5b3799f981ee1f7b7c228924902d351e6 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 16 Sep 2025 08:42:54 -0400 Subject: [PATCH] Tidy `serai-signals-pallet` Adds `serai-validator-sets-pallet` and `serai-signals-pallet` to the runtime. --- substrate/abi/src/transaction.rs | 9 +- substrate/primitives/src/address.rs | 2 +- substrate/primitives/src/lib.rs | 2 +- substrate/primitives/src/signals.rs | 12 +- .../primitives/src/validator_sets/mod.rs | 2 + substrate/runtime/Cargo.toml | 9 + substrate/runtime/src/lib.rs | 13 +- substrate/signals/Cargo.toml | 6 +- substrate/signals/README.md | 1 + substrate/signals/src/lib.rs | 618 +++++++++--------- substrate/validator-sets/src/lib.rs | 16 +- substrate/validator-sets/src/sessions.rs | 8 + 12 files changed, 381 insertions(+), 317 deletions(-) create mode 100644 substrate/signals/README.md diff --git a/substrate/abi/src/transaction.rs b/substrate/abi/src/transaction.rs index dde1ca25..d5fc1f08 100644 --- a/substrate/abi/src/transaction.rs +++ b/substrate/abi/src/transaction.rs @@ -237,7 +237,7 @@ mod substrate { use scale::{Encode, Decode}; use sp_runtime::{ transaction_validity::*, - traits::{Verify, ExtrinsicLike, Dispatchable, ValidateUnsigned, Checkable, Applyable}, + traits::{Verify, ExtrinsicLike, ExtrinsicCall, Dispatchable, ValidateUnsigned, Checkable, Applyable}, Weight, }; #[rustfmt::skip] @@ -318,6 +318,13 @@ mod substrate { } } + impl ExtrinsicCall for Transaction { + type Call = Self; + fn call(&self) -> &Self { + self + } + } + impl GetDispatchInfo for TransactionWithContext { fn get_dispatch_info(&self) -> DispatchInfo { match &self.0 { diff --git a/substrate/primitives/src/address.rs b/substrate/primitives/src/address.rs index 193c8856..ed59c9bf 100644 --- a/substrate/primitives/src/address.rs +++ b/substrate/primitives/src/address.rs @@ -26,7 +26,7 @@ const HUMAN_READABLE_PART: bech32::Hrp = bech32::Hrp::parse_unchecked("sri"); #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[cfg_attr( feature = "non_canonical_scale_derivations", - derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking) )] pub struct SeraiAddress(pub [u8; 32]); diff --git a/substrate/primitives/src/lib.rs b/substrate/primitives/src/lib.rs index 49d0df31..4de37302 100644 --- a/substrate/primitives/src/lib.rs +++ b/substrate/primitives/src/lib.rs @@ -96,7 +96,7 @@ pub mod prelude { pub use crate::coin::*; pub use crate::balance::*; pub use crate::network_id::*; - pub use crate::validator_sets::{Session, ValidatorSet, ExternalValidatorSet, Slash, SlashReport}; + pub use crate::validator_sets::*; pub use crate::instructions::*; } diff --git a/substrate/primitives/src/signals.rs b/substrate/primitives/src/signals.rs index 462d80d0..fa33038a 100644 --- a/substrate/primitives/src/signals.rs +++ b/substrate/primitives/src/signals.rs @@ -5,19 +5,21 @@ use crate::{network_id::ExternalNetworkId, address::SeraiAddress}; /// The ID of an protocol. pub type ProtocolId = [u8; 32]; +/// The ID of a signal. +pub type SignalId = [u8; 32]; /// A signal. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[cfg_attr( feature = "non_canonical_scale_derivations", allow(clippy::cast_possible_truncation), - derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking) )] pub enum Signal { /// A signal to retire the current protocol. Retire { - /// The protocol to retire in favor of. - in_favor_of: ProtocolId, + /// The ID of the retirement signal being favored. + signal_id: SignalId, }, /// A signal to halt an external network. Halt(ExternalNetworkId), @@ -27,7 +29,7 @@ pub enum Signal { #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[cfg_attr( feature = "non_canonical_scale_derivations", - derive(scale::Encode, scale::Decode, scale::MaxEncodedLen) + derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking) )] pub struct RegisteredRetirementSignal { /// The protocol to retire in favor of. @@ -40,7 +42,7 @@ pub struct RegisteredRetirementSignal { impl RegisteredRetirementSignal { /// The ID of this signal. - pub fn id(&self) -> ProtocolId { + pub fn id(&self) -> SignalId { sp_core::blake2_256(&borsh::to_vec(self).unwrap()) } } diff --git a/substrate/primitives/src/validator_sets/mod.rs b/substrate/primitives/src/validator_sets/mod.rs index c84dbdac..d81e7c2a 100644 --- a/substrate/primitives/src/validator_sets/mod.rs +++ b/substrate/primitives/src/validator_sets/mod.rs @@ -112,6 +112,8 @@ impl ExternalValidatorSet { pub struct KeyShares(pub u16); impl KeyShares { + /// Zero key shares. + pub const ZERO: KeyShares = KeyShares(0); /// One key share. pub const ONE: KeyShares = KeyShares(1); /// The maximum amount of key shares per set. diff --git a/substrate/runtime/Cargo.toml b/substrate/runtime/Cargo.toml index cab23200..99d122df 100644 --- a/substrate/runtime/Cargo.toml +++ b/substrate/runtime/Cargo.toml @@ -34,6 +34,8 @@ frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = frame-executive = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543", default-features = false } serai-coins-pallet = { path = "../coins", default-features = false } +serai-validator-sets-pallet = { path = "../validator-sets", default-features = false } +serai-signals-pallet = { path = "../signals", default-features = false } [build-dependencies] substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543" } @@ -54,6 +56,8 @@ std = [ "frame-executive/std", "serai-coins-pallet/std", + "serai-validator-sets-pallet/std", + "serai-signals-pallet/std", ] try-runtime = [ @@ -66,6 +70,8 @@ try-runtime = [ "frame-executive/try-runtime", "serai-coins-pallet/try-runtime", + "serai-validator-sets-pallet/try-runtime", + "serai-signals-pallet/try-runtime", ] runtime-benchmarks = [ @@ -73,6 +79,9 @@ runtime-benchmarks = [ "frame-system/runtime-benchmarks", "frame-support/runtime-benchmarks", + + "serai-validator-sets-pallet/runtime-benchmarks", + "serai-signals-pallet/runtime-benchmarks", ] default = ["std"] diff --git a/substrate/runtime/src/lib.rs b/substrate/runtime/src/lib.rs index b5aa080d..c7e2a31a 100644 --- a/substrate/runtime/src/lib.rs +++ b/substrate/runtime/src/lib.rs @@ -82,6 +82,12 @@ mod runtime { #[runtime::pallet_index(3)] pub type LiquidityTokens = serai_coins_pallet::Pallet; + + #[runtime::pallet_index(4)] + pub type ValidatorSets = serai_validator_sets_pallet::Pallet; + + #[runtime::pallet_index(5)] + pub type Signals = serai_signals_pallet::Pallet; } impl frame_system::Config for Runtime { @@ -127,13 +133,16 @@ impl frame_system::Config for Runtime { impl core_pallet::Config for Runtime {} impl serai_coins_pallet::Config for Runtime { - type RuntimeEvent = RuntimeEvent; type AllowMint = serai_coins_pallet::AlwaysAllowMint; // TODO } impl serai_coins_pallet::Config for Runtime { - type RuntimeEvent = RuntimeEvent; type AllowMint = serai_coins_pallet::AlwaysAllowMint; } +impl serai_validator_sets_pallet::Config for Runtime {} +impl serai_signals_pallet::Config for Runtime { + type RetirementValidityDuration = sp_core::ConstU64<0>; // TODO + type RetirementLockInDuration = sp_core::ConstU64<0>; // TODO +} impl From> for RuntimeOrigin { fn from(signer: Option) -> Self { diff --git a/substrate/signals/Cargo.toml b/substrate/signals/Cargo.toml index a7254381..87309165 100644 --- a/substrate/signals/Cargo.toml +++ b/substrate/signals/Cargo.toml @@ -27,10 +27,9 @@ sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c frame-system = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543", default-features = false } frame-support = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543", default-features = false } -serai-primitives = { path = "../primitives", default-features = false } +serai-abi = { path = "../abi", default-features = false, features = ["substrate"] } validator-sets-pallet = { package = "serai-validator-sets-pallet", path = "../validator-sets", default-features = false } -in-instructions-pallet = { package = "serai-in-instructions-pallet", path = "../in-instructions", default-features = false } [features] std = [ @@ -42,10 +41,9 @@ std = [ "frame-system/std", "frame-support/std", - "serai-primitives/std", + "serai-abi/std", "validator-sets-pallet/std", - "in-instructions-pallet/std", ] runtime-benchmarks = [ diff --git a/substrate/signals/README.md b/substrate/signals/README.md new file mode 100644 index 00000000..71511bcf --- /dev/null +++ b/substrate/signals/README.md @@ -0,0 +1 @@ +# Serai Signals Pallet diff --git a/substrate/signals/src/lib.rs b/substrate/signals/src/lib.rs index 96ae8a08..c57e337a 100644 --- a/substrate/signals/src/lib.rs +++ b/substrate/signals/src/lib.rs @@ -1,33 +1,31 @@ +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] +#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] -#[allow( - deprecated, - unreachable_patterns, - clippy::let_unit_value, - clippy::cast_possible_truncation, - clippy::ignored_unit_patterns -)] // TODO +extern crate alloc; + +#[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] pub mod pallet { use sp_core::sr25519::Public; - use sp_io::hashing::blake2_256; + + use serai_abi::{primitives::{prelude::*, signals::*}, SubstrateBlock}; use frame_system::pallet_prelude::*; - // False positive - #[allow(unused)] - use frame_support::{pallet_prelude::*, sp_runtime}; + use frame_support::pallet_prelude::*; - use serai_primitives::*; - use serai_signals_primitives::SignalId; - use validator_sets_pallet::{primitives::ValidatorSet, Config as VsConfig, Pallet as VsPallet}; - use in_instructions_pallet::{Config as IiConfig, Pallet as InInstructions}; + use validator_sets_pallet::{Config as VsConfig, Pallet as VsPallet}; #[pallet::config] - pub trait Config: frame_system::Config + VsConfig + IiConfig { - type RuntimeEvent: IsType<::RuntimeEvent> + From>; - - type RetirementValidityDuration: Get; - type RetirementLockInDuration: Get; + pub trait Config: frame_system::Config + VsConfig { + /// How long a candidate retirement signal is valid for. + /// + /// This MUST be equal to the rate at which new sets are attempted. + // TODO: Fetch from `validator_sets::Config`. + type RetirementValidityDuration: Get; + /// How long a retirement signal is locked-in for before retirement.. + type RetirementLockInDuration: Get; } #[pallet::genesis_config] @@ -43,8 +41,12 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { - // Assert the validity duration is less than the lock-in duration so lock-in periods - // automatically invalidate other retirement signals + /* + Assert the validity duration is less than the lock-in duration. + + This way, while the the signal is locked-in, any/all other candidate retirement signals + will expire. + */ assert!(T::RetirementValidityDuration::get() < T::RetirementLockInDuration::get()); } } @@ -52,204 +54,200 @@ pub mod pallet { #[pallet::pallet] pub struct Pallet(PhantomData); - #[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen)] - pub struct RegisteredRetirementSignal { - in_favor_of: [u8; 32], - registrant: T::AccountId, - registered_at: BlockNumberFor, - } - - impl RegisteredRetirementSignal { - fn id(&self) -> [u8; 32] { - let mut preimage = b"Signal".to_vec(); - preimage.extend(&self.encode()); - blake2_256(&preimage) - } - } - + /// The registered retirement signals. #[pallet::storage] type RegisteredRetirementSignals = - StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal, OptionQuery>; + StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal, OptionQuery>; + /// The registered favors. #[pallet::storage] - pub type Favors = StorageDoubleMap< + type Favors = StorageDoubleMap< _, Blake2_128Concat, - (SignalId, NetworkId), + (Signal, NetworkId), Blake2_128Concat, T::AccountId, (), OptionQuery, >; + /// The networks in favor of a signal. #[pallet::storage] - pub type SetsInFavor = - StorageMap<_, Blake2_128Concat, (SignalId, ValidatorSet), (), OptionQuery>; + type NetworksInFavor = + StorageMap<_, Blake2_128Concat, (Signal, NetworkId), (), OptionQuery>; + /// The locked-in retirement signal. + /// + /// This is in the format `(protocol_id, retiry_block)`. #[pallet::storage] - pub type LockedInRetirement = - StorageValue<_, ([u8; 32], BlockNumberFor), OptionQuery>; + type LockedInRetirement = + StorageValue<_, (ProtocolId, BlockNumberFor), OptionQuery>; - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - RetirementSignalRegistered { - signal_id: [u8; 32], - in_favor_of: [u8; 32], - registrant: T::AccountId, - }, - RetirementSignalRevoked { - signal_id: [u8; 32], - }, - SignalFavored { - signal_id: SignalId, - by: T::AccountId, - for_network: NetworkId, - }, - SetInFavor { - signal_id: SignalId, - set: ValidatorSet, - }, - RetirementSignalLockedIn { - signal_id: [u8; 32], - }, - SetNoLongerInFavor { - signal_id: SignalId, - set: ValidatorSet, - }, - FavorRevoked { - signal_id: SignalId, - by: T::AccountId, - for_network: NetworkId, - }, - AgainstSignal { - signal_id: SignalId, - who: T::AccountId, - for_network: NetworkId, - }, + /// Halted networks. + /// + /// Halted networks will be halted for the remainder of this protocol's lifetime. + #[pallet::storage] + type Halted = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>; + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(current_number: BlockNumberFor) -> Weight { + /* + If this is the block at which a locked-in retirement signal has been locked-in for long + enough, panic, halting the blockchain, and retiring the current protocol. + */ + if let Some((protocol_id, block_number)) = LockedInRetirement::::get() { + if block_number == current_number { + panic!( + "protocol retired in favor of {}", + sp_core::hexdisplay::HexDisplay::from(&protocol_id) + ); + } + } + // Using `Weight::zero()` is fine here as this is a minute operation + Weight::zero() + } } - #[pallet::error] - pub enum Error { - RetirementSignalLockedIn, - RetirementSignalAlreadyRegistered, - NotRetirementSignalRegistrant, - NonExistentRetirementSignal, - ExpiredRetirementSignal, - NotValidator, - RevokingNonExistentFavor, - } - - // 80% threshold - // TODO: Use 34% for halting a set (not 80%) - const REQUIREMENT_NUMERATOR: u64 = 4; - const REQUIREMENT_DIVISOR: u64 = 5; - impl Pallet { - // Returns true if this network's current set is in favor of the signal. - // - // Must only be called for networks which have a set decided. - fn tally_for_network(signal_id: SignalId, network: NetworkId) -> bool { - let this_network_session = VsPallet::::latest_decided_session(network).unwrap(); - let this_set = ValidatorSet { network, session: this_network_session }; + /// Tally the support for a signal by a network's current validator set. + /// + /// This will mutate the storage with the result. + /// + /// This returns `true` if the network is sufficiently in favor of the signal. + fn tally_for_network(signal: Signal, network: NetworkId) -> bool { + let Some(current_session) = VsPallet::::current_session(network) else { return false }; + let current_set = ValidatorSet { network, session: current_session }; + let Some(latest_session) = VsPallet::::latest_decided_session(network) else { + panic!("current session yet no latest decided session") + }; + let latest_set = ValidatorSet { network, session: latest_session }; - // This is a bounded O(n) (which is still acceptable) due to the infeasibility of caching - // here - // TODO: Make caching feasible? Do a first-pass with cache then actual pass before - // execution? - let mut iter = Favors::::iter_prefix_values((signal_id, network)); - let mut needed_favor = (VsPallet::::total_allocated_stake(network).unwrap().0 * - REQUIREMENT_NUMERATOR) - .div_ceil(REQUIREMENT_DIVISOR); - while iter.next().is_some() && (needed_favor != 0) { - let item_key = iter.last_raw_key(); - // `.len() - 32` is safe because AccountId is bound to being Public, which is 32 bytes - let account = T::AccountId::decode(&mut &item_key[(item_key.len() - 32) ..]).unwrap(); - if VsPallet::::in_latest_decided_set(network, account) { - // This call uses the current allocation, not the allocation at the time of set - // decision - // This is deemed safe due to the validator-set pallet's deallocation scheduling - // unwrap is safe due to being in the latest decided set - needed_favor = - needed_favor.saturating_sub(VsPallet::::allocation((network, account)).unwrap().0); - } + /* + The following uses key shares, not allocations, as key shares are static while allocations + fluctuate during the duration of a validator set. + */ + + let mut needed_favor = { + let current = VsPallet::::key_shares(current_set) + .expect("current validator set without key shares set") + .0; + let latest = VsPallet::::key_shares(latest_set) + .expect("latest validator set without key shares set") + .0; + current.max(latest) + }; + for (validator, ()) in Favors::::iter_prefix((signal, network)) { + /* + Fetch the amount of key shares the validator has. + + This uses the minimum amount of key shares across the current validator set and the + latest decided validator set to ensure this validator represents this network and will + continue to do so. + */ + let key_shares = { + let current = VsPallet::::key_shares_possessed_by_validator(current_set, validator) + .unwrap_or(KeyShares::ZERO); + let latest = VsPallet::::key_shares_possessed_by_validator(latest_set, validator) + .unwrap_or(KeyShares::ZERO); + current.0.min(latest.0) + }; + + let Some(still_needed_favor) = needed_favor.checked_sub(key_shares) else { + needed_favor = 0; + break; + }; + needed_favor = still_needed_favor; } - if needed_favor == 0 { - // Set the set as in favor until someone triggers a re-tally - // - // Since a re-tally is an extra step we can't assume will occur, this effectively means a - // network in favor across any point in its Session is in favor for its entire Session - // While a malicious actor could increase their stake, favor a signal, then deallocate, - // this is largely prevented by deallocation scheduling - // - // At any given point, only just under 50% of a set can be immediately deallocated - // (if each validator has just under two key shares, they can deallocate the entire amount - // above a single key share) - // - // This means that if a signal has a 67% adoption threshold, and someone executes this - // attack, they still have a majority of the allocated stake (though less of a majority - // than desired) - // - // With the 80% threshold, removing 39.9% creates a 40.1% to 20% ratio, which is still - // the BFT threshold of 67% - if !SetsInFavor::::contains_key((signal_id, this_set)) { - SetsInFavor::::set((signal_id, this_set), Some(())); - Self::deposit_event(Event::SetInFavor { signal_id, set: this_set }); + let now_in_favor = needed_favor == 0; + + // Update the storage and emit an event, if appropriate + if now_in_favor { + let prior_in_favor = NetworksInFavor::::contains_key((signal, network)); + NetworksInFavor::::set((signal, network), Some(())); + if !prior_in_favor { + todo!("Event"); } - true } else { - if SetsInFavor::::contains_key((signal_id, this_set)) { - // This should no longer be under the current tally - SetsInFavor::::remove((signal_id, this_set)); - Self::deposit_event(Event::SetNoLongerInFavor { signal_id, set: this_set }); + #[allow(clippy::collapsible_else_if)] + if NetworksInFavor::::take((signal, network)).is_some() { + todo!("Event"); } - false } + + now_in_favor } - fn tally_for_all_networks(signal_id: SignalId) -> bool { + /// Tally support for a signal across all networks, weighted by stake. + /// + /// Returns `true` if the signal has sufficient support. + fn tally_for_all_networks(signal: Signal) -> bool { let mut total_in_favor_stake = 0; let mut total_allocated_stake = 0; - for network in serai_primitives::NETWORKS { - let Some(latest_decided_session) = VsPallet::::latest_decided_session(network) else { - continue; - }; - // If it has a session, it should have a total allocated stake value - let network_stake = VsPallet::::total_allocated_stake(network).unwrap(); - if SetsInFavor::::contains_key(( - signal_id, - ValidatorSet { network, session: latest_decided_session }, - )) { + for network in NetworkId::all() { + /* + This doesn't consider if the latest decided validator set has considerably less stake, + yet the bound validators vote by the minimum of their key shares, against the maximum of + the total key shares, should be sufficient in this regard. + */ + let network_stake = + VsPallet::::stake_for_current_validator_set(network).unwrap_or(Amount(0)); + if NetworksInFavor::::contains_key((signal, network)) { total_in_favor_stake += network_stake.0; } total_allocated_stake += network_stake.0; } - total_in_favor_stake >= - (total_allocated_stake * REQUIREMENT_NUMERATOR).div_ceil(REQUIREMENT_DIVISOR) + /* + We use a 80% threshold for retirement, calculated as defined above, but just a 34% + threshold for halting another validator set. This is representative of how 34% of + validators can cause a liveness failure during asynchronous BFT> + */ + let threshold = match signal { + Signal::Retire { .. } => (total_allocated_stake * 4) / 5, + Signal::Halt { .. } => (total_allocated_stake * 2) / 3, + }; + total_in_favor_stake > threshold } fn revoke_favor_internal( - account: T::AccountId, - signal_id: SignalId, + validator: T::AccountId, + signal: Signal, for_network: NetworkId, ) -> DispatchResult { - if !Favors::::contains_key((signal_id, for_network), account) { + if !Favors::::contains_key((signal, for_network), validator) { Err::<(), _>(Error::::RevokingNonExistentFavor)?; } - Favors::::remove((signal_id, for_network), account); - Self::deposit_event(Event::::FavorRevoked { signal_id, by: account, for_network }); - // tally_for_network assumes the network is active, which is implied by having prior set a - // favor for it - // Technically, this tally may make the network in favor and justify re-tallying for all - // networks - // Its assumed not to - Self::tally_for_network(signal_id, for_network); + Favors::::remove((signal, for_network), validator); + // TODO: Event + + // Update the tally for this network + Self::tally_for_network(signal, for_network); + Ok(()) } } + /// An error from the `signals` pallet. + #[pallet::error] + pub enum Error { + /// A retirement signal has already been locked in. + RetirementSignalLockedIn, + /// This retirement signal has already been registered. + RetirementSignalAlreadyRegistered, + /// The caller is not the registrant of the retirement signal. + NotRetirementSignalRegistrant, + /// The retirement signal does not exist. + NonExistentRetirementSignal, + /// The retirement signal has expired. + ExpiredRetirementSignal, + /// The caller is already in favor. + AlreadyInFavor, + /// Revoking favor when no favor has been expressed. + RevokingNonExistentFavor, + } + #[pallet::call] impl Pallet { /// Register a retirement signal, declaring the consensus protocol this signal is in favor of. @@ -257,7 +255,7 @@ pub mod pallet { /// Retirement signals are registered so that the proposer, presumably a developer, can revoke /// the signal if there's a fault discovered. #[pallet::call_index(0)] - #[pallet::weight(0)] // TODO + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn register_retirement_signal( origin: OriginFor, in_favor_of: [u8; 32], @@ -267,14 +265,17 @@ pub mod pallet { Err::<(), _>(Error::::RetirementSignalLockedIn)?; } - let account = ensure_signed(origin)?; + let validator = ensure_signed(origin)?; - // Bind the signal ID to the proposer - // This prevents a malicious actor from frontrunning a proposal, causing them to be the - // registrant, just to cancel it later + /* + Bind the signal ID to the proposer. + + This prevents a malicious actor from frontrunning a proposal, causing them to be the + registrant, just to cancel it later. + */ let signal = RegisteredRetirementSignal { in_favor_of, - registrant: account, + registrant: validator.into(), registered_at: frame_system::Pallet::::block_number(), }; let signal_id = signal.id(); @@ -282,122 +283,108 @@ pub mod pallet { if RegisteredRetirementSignals::::get(signal_id).is_some() { Err::<(), _>(Error::::RetirementSignalAlreadyRegistered)?; } - - Self::deposit_event(Event::::RetirementSignalRegistered { - signal_id, - in_favor_of, - registrant: account, - }); RegisteredRetirementSignals::::set(signal_id, Some(signal)); + + // TODO: Event Ok(()) } + /// Revoke a retirement signal. #[pallet::call_index(1)] - #[pallet::weight(0)] // TODO + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn revoke_retirement_signal( origin: OriginFor, - retirement_signal_id: [u8; 32], + retirement_signal: [u8; 32], ) -> DispatchResult { - let account = ensure_signed(origin)?; - let Some(registered_signal) = RegisteredRetirementSignals::::get(retirement_signal_id) + let validator = ensure_signed(origin)?; + let Some(registered_signal) = RegisteredRetirementSignals::::get(retirement_signal) else { return Err::<(), _>(Error::::NonExistentRetirementSignal.into()); }; - if account != registered_signal.registrant { + if SeraiAddress::from(validator) != registered_signal.registrant { Err::<(), _>(Error::::NotRetirementSignalRegistrant)?; } - RegisteredRetirementSignals::::remove(retirement_signal_id); + RegisteredRetirementSignals::::remove(retirement_signal); - // If this signal was locked in, remove it - // This lets a post-lock-in discovered fault be prevented from going live without - // intervention by all validators - if LockedInRetirement::::get().map(|(signal_id, _block_number)| signal_id) == - Some(retirement_signal_id) + /* + If this signal was locked in, remove it. + + This lets a post-lock-in discovered fault be prevented from going live without intervention + by a supermajority of validators. + */ + if LockedInRetirement::::get().map(|(signal, _block_number)| signal) == + Some(retirement_signal) { LockedInRetirement::::kill(); } - Self::deposit_event(Event::::RetirementSignalRevoked { signal_id: retirement_signal_id }); + // TODO: Event Ok(()) } + /// Favor a signal. #[pallet::call_index(2)] - #[pallet::weight(0)] // TODO + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn favor( origin: OriginFor, - signal_id: SignalId, + signal: Signal, for_network: NetworkId, ) -> DispatchResult { - let account = ensure_signed(origin)?; + let validator = ensure_signed(origin)?; - // If this is a retirement signal, perform the relevant checks - if let SignalId::Retirement(signal_id) = signal_id { - // Make sure a retirement hasn't already been locked in - if LockedInRetirement::::exists() { - Err::<(), _>(Error::::RetirementSignalLockedIn)?; - } + // Perform the relevant checks for this class of signal + match signal { + Signal::Retire { signal_id } => { + // Make sure a retirement hasn't already been locked in + if LockedInRetirement::::exists() { + Err::<(), _>(Error::::RetirementSignalLockedIn)?; + } - // Make sure this is a registered retirement - // We don't have to do this for a `Halt` signal as `Halt` doesn't have the registration - // process - let Some(registered_signal) = RegisteredRetirementSignals::::get(signal_id) else { - return Err::<(), _>(Error::::NonExistentRetirementSignal.into()); - }; + /* + Make sure this is a registered retirement. - // Check the signal isn't out of date - // This isn't truly necessary since we only track votes from the most recent validator - // sets, ensuring modern relevancy - // The reason to still have it is because locking in a dated runtime may cause a corrupt - // blockchain and lead to a failure in system integrity - // `Halt`, which doesn't have this check, at worst causes temporary downtime - if (registered_signal.registered_at + T::RetirementValidityDuration::get().into()) < - frame_system::Pallet::::block_number() - { - Err::<(), _>(Error::::ExpiredRetirementSignal)?; - } + We don't have to do this for a `Halt` signal as `Halt` doesn't have the registration + process. + */ + let Some(registered_signal) = RegisteredRetirementSignals::::get(signal_id) else { + return Err::<(), _>(Error::::NonExistentRetirementSignal.into()) + }; + + // Check the signal isn't out of date, and its tallies with it. + if (registered_signal.registered_at + T::RetirementValidityDuration::get()) < + frame_system::Pallet::::block_number() + { + Err::<(), _>(Error::::ExpiredRetirementSignal)?; + } + }, + Signal::Halt { .. } => {} } - // Check the signer is a validator - // Technically, in the case of Serai, this will check they're planned to be in the next set, - // not that they are in the current set - // This is a practical requirement due to the lack of tracking historical allocations, and - // fine for the purposes here - if !VsPallet::::in_latest_decided_set(for_network, account) { - Err::<(), _>(Error::::NotValidator)?; + if Favors::::contains_key((signal, for_network), validator) { + Err::<(), _>(Error::::AlreadyInFavor)?; } - // Set them as in-favor - // Doesn't error if they already voted in order to let any validator trigger a re-tally - if !Favors::::contains_key((signal_id, for_network), account) { - Favors::::set((signal_id, for_network), account, Some(())); - Self::deposit_event(Event::SignalFavored { signal_id, by: account, for_network }); - } + // Set the validator as in favor + Favors::::set((signal, for_network), validator, Some(())); + // TODO: Event // Check if the network is in favor - // tally_for_network expects the network to be active, which is implied by being in the - // latest decided set - let network_in_favor = Self::tally_for_network(signal_id, for_network); + let network_in_favor = Self::tally_for_network(signal, for_network); // If this network is in favor, check if enough networks are - // We could optimize this by only running the following code when the network is *newly* in - // favor - // Re-running the following code ensures that if networks' allocated stakes change relative - // to each other, any new votes will cause a re-tally - if network_in_favor { + if network_in_favor && Self::tally_for_all_networks(signal) { // If enough are, lock in the signal - if Self::tally_for_all_networks(signal_id) { - match signal_id { - SignalId::Retirement(signal_id) => { - LockedInRetirement::::set(Some(( - signal_id, - frame_system::Pallet::::block_number() + - T::RetirementLockInDuration::get().into(), - ))); - Self::deposit_event(Event::RetirementSignalLockedIn { signal_id }); - } - SignalId::Halt(network) => { - InInstructions::::halt(network)?; - } + match signal { + Signal::Retire { signal_id } => { + LockedInRetirement::::set(Some(( + signal_id, + frame_system::Pallet::::block_number() + T::RetirementLockInDuration::get() + ))); + // TODO: Event + } + Signal::Halt(network) => { + Halted::::set(network, Some(())); + // TODO: Event } } } @@ -407,75 +394,110 @@ pub mod pallet { /// Revoke favor into an abstaining position. #[pallet::call_index(3)] - #[pallet::weight(0)] // TODO + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn revoke_favor( origin: OriginFor, - signal_id: SignalId, + signal: Signal, for_network: NetworkId, ) -> DispatchResult { - if matches!(&signal_id, SignalId::Retirement(_)) && LockedInRetirement::::exists() { - Err::<(), _>(Error::::RetirementSignalLockedIn)?; + match signal { + Signal::Retire { .. } => { + if LockedInRetirement::::exists() { + Err::<(), _>(Error::::RetirementSignalLockedIn)?; + } + } + Signal::Halt { .. } => {} } - // Doesn't check the signal exists due to later checking the favor exists - // While the signal may have been revoked, making this pointless, it's not worth the storage - // read on every call to check - // Since revoke will re-tally, this does technically mean a network will become in-favor of a - // revoked signal. Since revoke won't re-tally for all networks/lock-in, this is also fine - - Self::revoke_favor_internal(ensure_signed(origin)?, signal_id, for_network) + let validator = ensure_signed(origin)?; + Self::revoke_favor_internal(validator, signal, for_network) } /// Emit an event standing against the signal. /// + /// While disapprovals aren't tracked explicitly, this is used to at least label a validator's + /// opinion and allow better collection of data. + /// /// If the origin is currently in favor of the signal, their favor will be revoked. #[pallet::call_index(4)] - #[pallet::weight(0)] // TODO + #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn stand_against( origin: OriginFor, - signal_id: SignalId, + signal: Signal, for_network: NetworkId, ) -> DispatchResult { - if LockedInRetirement::::exists() { - Err::<(), _>(Error::::RetirementSignalLockedIn)?; + match signal { + Signal::Retire { .. } => { + if LockedInRetirement::::exists() { + Err::<(), _>(Error::::RetirementSignalLockedIn)?; + } + } + Signal::Halt { .. } => {} } - let account = ensure_signed(origin)?; + let validator = ensure_signed(origin)?; // If currently in favor, revoke the favor - if Favors::::contains_key((signal_id, for_network), account) { - Self::revoke_favor_internal(account, signal_id, for_network)?; + if Favors::::contains_key((signal, for_network), validator) { + Self::revoke_favor_internal(validator, signal, for_network)?; } else { - // Check this Signal exists (which would've been implied by Favors for it existing) - if let SignalId::Retirement(signal_id) = signal_id { - if RegisteredRetirementSignals::::get(signal_id).is_none() { - Err::<(), _>(Error::::NonExistentRetirementSignal)?; + // Check this Signal exists (which would've been implied by `Favors` for it existing) + match signal { + Signal::Retire { signal_id } => { + if RegisteredRetirementSignals::::get(signal_id).is_none() { + Err::<(), _>(Error::::NonExistentRetirementSignal)?; + } } + Signal::Halt { .. } => {} } } - // Emit an event that we're against the signal - // No actual effects happen besides this - Self::deposit_event(Event::::AgainstSignal { signal_id, who: account, for_network }); + // Emit the event + // TODO: Event + Ok(()) } } - #[pallet::hooks] - impl Hooks> for Pallet { - fn on_initialize(current_number: BlockNumberFor) -> Weight { - // If this is the block at which a locked-in signal has been set for long enough, panic - // This will prevent this block from executing and halt the chain - if let Some((signal, block_number)) = LockedInRetirement::::get() { - if block_number == current_number { - panic!( - "locked-in signal {} has been set for too long", - sp_core::hexdisplay::HexDisplay::from(&signal), - ); - } - } - Weight::zero() // TODO - } + /* TODO + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + RetirementSignalRegistered { + signal: [u8; 32], + in_favor_of: [u8; 32], + registrant: T::AccountId, + }, + RetirementSignalRevoked { + signal_id: [u8; 32], + }, + SignalFavored { + signal_id: Signal, + by: T::AccountId, + for_network: NetworkId, + }, + SetInFavor { + signal_id: Signal, + set: ValidatorSet, + }, + RetirementSignalLockedIn { + signal_id: [u8; 32], + }, + SetNoLongerInFavor { + signal_id: Signal, + set: ValidatorSet, + }, + FavorRevoked { + signal_id: Signal, + by: T::AccountId, + for_network: NetworkId, + }, + AgainstSignal { + signal_id: Signal, + who: T::AccountId, + for_network: NetworkId, + }, } + */ } pub use pallet::*; diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index 59b3dfd9..54123349 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -3,6 +3,7 @@ #![cfg_attr(not(feature = "std"), no_std)] extern crate alloc; +use alloc::vec::Vec; mod embedded_elliptic_curve_keys; use embedded_elliptic_curve_keys::*; @@ -72,7 +73,7 @@ impl GetValidatorCount for MembershipProof { } */ -#[expect(clippy::ignored_unit_patterns, clippy::cast_possible_truncation)] +#[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { use sp_core::sr25519::Public; @@ -94,7 +95,7 @@ mod pallet { use super::*; #[pallet::config] - pub trait Config: frame_system::Config + coins_pallet::Config { + pub trait Config: frame_system::Config + coins_pallet::Config { // type ShouldEndSession: ShouldEndSession>; } @@ -383,6 +384,11 @@ mod pallet { Abstractions::::key_shares_possessed_by_validator(set, validator) } + /// The stake for the current validator set. + pub fn stake_for_current_validator_set(network: NetworkId) -> Option { + Abstractions::::stake_for_current_validator_set(network) + } + /* // is_bft returns if the network is able to survive any single node becoming byzantine. fn is_bft(network: NetworkId) -> bool { @@ -840,7 +846,7 @@ mod pallet { #[pallet::weight((0, DispatchClass::Normal))] // TODO pub fn allocate(origin: OriginFor, network: NetworkId, amount: Amount) -> DispatchResult { let validator = ensure_signed(origin)?; - Coins::::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?; + Coins::::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?; Abstractions::::increase_allocation(network, validator, amount, false) .map_err(Error::::AllocationError)?; Ok(()) @@ -854,7 +860,7 @@ mod pallet { let deallocation_timeline = Abstractions::::decrease_allocation(network, account, amount) .map_err(Error::::DeallocationError)?; if matches!(deallocation_timeline, DeallocationTimeline::Immediate) { - Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; + Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; } Ok(()) @@ -870,7 +876,7 @@ mod pallet { let account = ensure_signed(origin)?; let amount = Abstractions::::claim_delayed_deallocation(account, network, session) .map_err(Error::::DeallocationError)?; - Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; + Coins::::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?; Ok(()) } } diff --git a/substrate/validator-sets/src/sessions.rs b/substrate/validator-sets/src/sessions.rs index e6ec262a..68f3ca86 100644 --- a/substrate/validator-sets/src/sessions.rs +++ b/substrate/validator-sets/src/sessions.rs @@ -1,3 +1,4 @@ +use alloc::vec::Vec; use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec}; use serai_primitives::{ @@ -211,6 +212,9 @@ pub(crate) trait Sessions { set: ValidatorSet, validator: Public, ) -> Option; + + /// The stake for the current validator set. + fn stake_for_current_validator_set(network: NetworkId) -> Option; } impl Sessions for Storage { @@ -516,4 +520,8 @@ impl Sessions for Storage { ) -> Option { Storage::SelectedValidators::get(selected_validators_key(set, validator)) } + + fn stake_for_current_validator_set(network: NetworkId) -> Option { + Storage::TotalAllocatedStake::get(network) + } }