Tidy serai-signals-pallet

Adds `serai-validator-sets-pallet` and `serai-signals-pallet` to the runtime.
This commit is contained in:
Luke Parker
2025-09-16 08:42:54 -04:00
parent 3722df7326
commit 8bafeab5b3
12 changed files with 381 additions and 317 deletions

View File

@@ -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<Context: TransactionContext> GetDispatchInfo for TransactionWithContext<Context> {
fn get_dispatch_info(&self) -> DispatchInfo {
match &self.0 {

View File

@@ -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]);

View File

@@ -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::*;
}

View File

@@ -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())
}
}

View File

@@ -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.

View File

@@ -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"]

View File

@@ -82,6 +82,12 @@ mod runtime {
#[runtime::pallet_index(3)]
pub type LiquidityTokens = serai_coins_pallet::Pallet<Runtime, LiquidityTokensInstance>;
#[runtime::pallet_index(4)]
pub type ValidatorSets = serai_validator_sets_pallet::Pallet<Runtime>;
#[runtime::pallet_index(5)]
pub type Signals = serai_signals_pallet::Pallet<Runtime>;
}
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<CoinsInstance> for Runtime {
type RuntimeEvent = RuntimeEvent;
type AllowMint = serai_coins_pallet::AlwaysAllowMint; // TODO
}
impl serai_coins_pallet::Config<LiquidityTokensInstance> 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<Option<SeraiAddress>> for RuntimeOrigin {
fn from(signer: Option<SeraiAddress>) -> Self {

View File

@@ -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 = [

View File

@@ -0,0 +1 @@
# Serai Signals Pallet

View File

@@ -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<AccountId = Public> + VsConfig + IiConfig {
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
type RetirementValidityDuration: Get<u32>;
type RetirementLockInDuration: Get<u32>;
pub trait Config: frame_system::Config<AccountId = Public, Block = SubstrateBlock> + 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<u64>;
/// How long a retirement signal is locked-in for before retirement..
type RetirementLockInDuration: Get<u64>;
}
#[pallet::genesis_config]
@@ -43,8 +41,12 @@ pub mod pallet {
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
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<T>(PhantomData<T>);
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen)]
pub struct RegisteredRetirementSignal<T: Config> {
in_favor_of: [u8; 32],
registrant: T::AccountId,
registered_at: BlockNumberFor<T>,
}
impl<T: Config> RegisteredRetirementSignal<T> {
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<T: Config> =
StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal<T>, OptionQuery>;
StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal, OptionQuery>;
/// The registered favors.
#[pallet::storage]
pub type Favors<T: Config> = StorageDoubleMap<
type Favors<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
(SignalId, NetworkId),
(Signal, NetworkId),
Blake2_128Concat,
T::AccountId,
(),
OptionQuery,
>;
/// The networks in favor of a signal.
#[pallet::storage]
pub type SetsInFavor<T: Config> =
StorageMap<_, Blake2_128Concat, (SignalId, ValidatorSet), (), OptionQuery>;
type NetworksInFavor<T: Config> =
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<T: Config> =
StorageValue<_, ([u8; 32], BlockNumberFor<T>), OptionQuery>;
type LockedInRetirement<T: Config> =
StorageValue<_, (ProtocolId, BlockNumberFor<T>), OptionQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
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<T: Config> = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>;
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(current_number: BlockNumberFor<T>) -> 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::<T>::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<T> {
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<T: Config> Pallet<T> {
// 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::<T>::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::<T>::current_session(network) else { return false };
let current_set = ValidatorSet { network, session: current_session };
let Some(latest_session) = VsPallet::<T>::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::<T>::iter_prefix_values((signal_id, network));
let mut needed_favor = (VsPallet::<T>::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::<T>::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::<T>::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::<T>::key_shares(current_set)
.expect("current validator set without key shares set")
.0;
let latest = VsPallet::<T>::key_shares(latest_set)
.expect("latest validator set without key shares set")
.0;
current.max(latest)
};
for (validator, ()) in Favors::<T>::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::<T>::key_shares_possessed_by_validator(current_set, validator)
.unwrap_or(KeyShares::ZERO);
let latest = VsPallet::<T>::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::<T>::contains_key((signal_id, this_set)) {
SetsInFavor::<T>::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::<T>::contains_key((signal, network));
NetworksInFavor::<T>::set((signal, network), Some(()));
if !prior_in_favor {
todo!("Event");
}
true
} else {
if SetsInFavor::<T>::contains_key((signal_id, this_set)) {
// This should no longer be under the current tally
SetsInFavor::<T>::remove((signal_id, this_set));
Self::deposit_event(Event::SetNoLongerInFavor { signal_id, set: this_set });
}
false
#[allow(clippy::collapsible_else_if)]
if NetworksInFavor::<T>::take((signal, network)).is_some() {
todo!("Event");
}
}
fn tally_for_all_networks(signal_id: SignalId) -> bool {
now_in_favor
}
/// 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::<T>::latest_decided_session(network) else {
continue;
};
// If it has a session, it should have a total allocated stake value
let network_stake = VsPallet::<T>::total_allocated_stake(network).unwrap();
if SetsInFavor::<T>::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::<T>::stake_for_current_validator_set(network).unwrap_or(Amount(0));
if NetworksInFavor::<T>::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::<T>::contains_key((signal_id, for_network), account) {
if !Favors::<T>::contains_key((signal, for_network), validator) {
Err::<(), _>(Error::<T>::RevokingNonExistentFavor)?;
}
Favors::<T>::remove((signal_id, for_network), account);
Self::deposit_event(Event::<T>::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::<T>::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<T> {
/// 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<T: Config> Pallet<T> {
/// 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<T>,
in_favor_of: [u8; 32],
@@ -267,14 +265,17 @@ pub mod pallet {
Err::<(), _>(Error::<T>::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::<T>::block_number(),
};
let signal_id = signal.id();
@@ -282,122 +283,108 @@ pub mod pallet {
if RegisteredRetirementSignals::<T>::get(signal_id).is_some() {
Err::<(), _>(Error::<T>::RetirementSignalAlreadyRegistered)?;
}
Self::deposit_event(Event::<T>::RetirementSignalRegistered {
signal_id,
in_favor_of,
registrant: account,
});
RegisteredRetirementSignals::<T>::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<T>,
retirement_signal_id: [u8; 32],
retirement_signal: [u8; 32],
) -> DispatchResult {
let account = ensure_signed(origin)?;
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(retirement_signal_id)
let validator = ensure_signed(origin)?;
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(retirement_signal)
else {
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
};
if account != registered_signal.registrant {
if SeraiAddress::from(validator) != registered_signal.registrant {
Err::<(), _>(Error::<T>::NotRetirementSignalRegistrant)?;
}
RegisteredRetirementSignals::<T>::remove(retirement_signal_id);
RegisteredRetirementSignals::<T>::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::<T>::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::<T>::get().map(|(signal, _block_number)| signal) ==
Some(retirement_signal)
{
LockedInRetirement::<T>::kill();
}
Self::deposit_event(Event::<T>::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<T>,
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 {
// 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::<T>::exists() {
Err::<(), _>(Error::<T>::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
/*
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::<T>::get(signal_id) else {
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into())
};
// 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()) <
// Check the signal isn't out of date, and its tallies with it.
if (registered_signal.registered_at + T::RetirementValidityDuration::get()) <
frame_system::Pallet::<T>::block_number()
{
Err::<(), _>(Error::<T>::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::<T>::in_latest_decided_set(for_network, account) {
Err::<(), _>(Error::<T>::NotValidator)?;
if Favors::<T>::contains_key((signal, for_network), validator) {
Err::<(), _>(Error::<T>::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::<T>::contains_key((signal_id, for_network), account) {
Favors::<T>::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::<T>::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) => {
match signal {
Signal::Retire { signal_id } => {
LockedInRetirement::<T>::set(Some((
signal_id,
frame_system::Pallet::<T>::block_number() +
T::RetirementLockInDuration::get().into(),
frame_system::Pallet::<T>::block_number() + T::RetirementLockInDuration::get()
)));
Self::deposit_event(Event::RetirementSignalLockedIn { signal_id });
}
SignalId::Halt(network) => {
InInstructions::<T>::halt(network)?;
// TODO: Event
}
Signal::Halt(network) => {
Halted::<T>::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<T>,
signal_id: SignalId,
signal: Signal,
for_network: NetworkId,
) -> DispatchResult {
if matches!(&signal_id, SignalId::Retirement(_)) && LockedInRetirement::<T>::exists() {
match signal {
Signal::Retire { .. } => {
if LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::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<T>,
signal_id: SignalId,
signal: Signal,
for_network: NetworkId,
) -> DispatchResult {
match signal {
Signal::Retire { .. } => {
if LockedInRetirement::<T>::exists() {
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
}
}
Signal::Halt { .. } => {}
}
let account = ensure_signed(origin)?;
let validator = ensure_signed(origin)?;
// If currently in favor, revoke the favor
if Favors::<T>::contains_key((signal_id, for_network), account) {
Self::revoke_favor_internal(account, signal_id, for_network)?;
if Favors::<T>::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 {
// Check this Signal exists (which would've been implied by `Favors` for it existing)
match signal {
Signal::Retire { signal_id } => {
if RegisteredRetirementSignals::<T>::get(signal_id).is_none() {
Err::<(), _>(Error::<T>::NonExistentRetirementSignal)?;
}
}
Signal::Halt { .. } => {}
}
}
// Emit an event that we're against the signal
// No actual effects happen besides this
Self::deposit_event(Event::<T>::AgainstSignal { signal_id, who: account, for_network });
// Emit the event
// TODO: Event
Ok(())
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(current_number: BlockNumberFor<T>) -> 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::<T>::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<T: Config> {
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::*;

View File

@@ -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<T: pallet::Config> GetValidatorCount for MembershipProof<T> {
}
*/
#[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<coins_pallet::CoinsInstance> {
// type ShouldEndSession: ShouldEndSession<BlockNumberFor<Self>>;
}
@@ -383,6 +384,11 @@ mod pallet {
Abstractions::<T>::key_shares_possessed_by_validator(set, validator)
}
/// The stake for the current validator set.
pub fn stake_for_current_validator_set(network: NetworkId) -> Option<Amount> {
Abstractions::<T>::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<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let validator = ensure_signed(origin)?;
Coins::<T>::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?;
Coins::<T, coins_pallet::CoinsInstance>::transfer_fn(validator, Self::account(), Balance { coin: Coin::Serai, amount })?;
Abstractions::<T>::increase_allocation(network, validator, amount, false)
.map_err(Error::<T>::AllocationError)?;
Ok(())
@@ -854,7 +860,7 @@ mod pallet {
let deallocation_timeline = Abstractions::<T>::decrease_allocation(network, account, amount)
.map_err(Error::<T>::DeallocationError)?;
if matches!(deallocation_timeline, DeallocationTimeline::Immediate) {
Coins::<T>::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?;
Coins::<T, coins_pallet::CoinsInstance>::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::<T>::claim_delayed_deallocation(account, network, session)
.map_err(Error::<T>::DeallocationError)?;
Coins::<T>::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?;
Coins::<T, coins_pallet::CoinsInstance>::transfer_fn(Self::account(), account, Balance { coin: Coin::Serai, amount })?;
Ok(())
}
}

View File

@@ -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<KeySharesStruct>;
/// The stake for the current validator set.
fn stake_for_current_validator_set(network: NetworkId) -> Option<Amount>;
}
impl<Storage: SessionsStorage> Sessions for Storage {
@@ -516,4 +520,8 @@ impl<Storage: SessionsStorage> Sessions for Storage {
) -> Option<KeySharesStruct> {
Storage::SelectedValidators::get(selected_validators_key(set, validator))
}
fn stake_for_current_validator_set(network: NetworkId) -> Option<Amount> {
Storage::TotalAllocatedStake::get(network)
}
}