mirror of
https://github.com/serai-dex/serai.git
synced 2025-12-08 12:19:24 +00:00
Tidy serai-signals-pallet
Adds `serai-validator-sets-pallet` and `serai-signals-pallet` to the runtime.
This commit is contained in:
@@ -237,7 +237,7 @@ mod substrate {
|
|||||||
use scale::{Encode, Decode};
|
use scale::{Encode, Decode};
|
||||||
use sp_runtime::{
|
use sp_runtime::{
|
||||||
transaction_validity::*,
|
transaction_validity::*,
|
||||||
traits::{Verify, ExtrinsicLike, Dispatchable, ValidateUnsigned, Checkable, Applyable},
|
traits::{Verify, ExtrinsicLike, ExtrinsicCall, Dispatchable, ValidateUnsigned, Checkable, Applyable},
|
||||||
Weight,
|
Weight,
|
||||||
};
|
};
|
||||||
#[rustfmt::skip]
|
#[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> {
|
impl<Context: TransactionContext> GetDispatchInfo for TransactionWithContext<Context> {
|
||||||
fn get_dispatch_info(&self) -> DispatchInfo {
|
fn get_dispatch_info(&self) -> DispatchInfo {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
|
|||||||
@@ -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)]
|
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "non_canonical_scale_derivations",
|
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]);
|
pub struct SeraiAddress(pub [u8; 32]);
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ pub mod prelude {
|
|||||||
pub use crate::coin::*;
|
pub use crate::coin::*;
|
||||||
pub use crate::balance::*;
|
pub use crate::balance::*;
|
||||||
pub use crate::network_id::*;
|
pub use crate::network_id::*;
|
||||||
pub use crate::validator_sets::{Session, ValidatorSet, ExternalValidatorSet, Slash, SlashReport};
|
pub use crate::validator_sets::*;
|
||||||
pub use crate::instructions::*;
|
pub use crate::instructions::*;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,19 +5,21 @@ use crate::{network_id::ExternalNetworkId, address::SeraiAddress};
|
|||||||
|
|
||||||
/// The ID of an protocol.
|
/// The ID of an protocol.
|
||||||
pub type ProtocolId = [u8; 32];
|
pub type ProtocolId = [u8; 32];
|
||||||
|
/// The ID of a signal.
|
||||||
|
pub type SignalId = [u8; 32];
|
||||||
|
|
||||||
/// A signal.
|
/// A signal.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "non_canonical_scale_derivations",
|
feature = "non_canonical_scale_derivations",
|
||||||
allow(clippy::cast_possible_truncation),
|
allow(clippy::cast_possible_truncation),
|
||||||
derive(scale::Encode, scale::Decode, scale::MaxEncodedLen)
|
derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking)
|
||||||
)]
|
)]
|
||||||
pub enum Signal {
|
pub enum Signal {
|
||||||
/// A signal to retire the current protocol.
|
/// A signal to retire the current protocol.
|
||||||
Retire {
|
Retire {
|
||||||
/// The protocol to retire in favor of.
|
/// The ID of the retirement signal being favored.
|
||||||
in_favor_of: ProtocolId,
|
signal_id: SignalId,
|
||||||
},
|
},
|
||||||
/// A signal to halt an external network.
|
/// A signal to halt an external network.
|
||||||
Halt(ExternalNetworkId),
|
Halt(ExternalNetworkId),
|
||||||
@@ -27,7 +29,7 @@ pub enum Signal {
|
|||||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)]
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "non_canonical_scale_derivations",
|
feature = "non_canonical_scale_derivations",
|
||||||
derive(scale::Encode, scale::Decode, scale::MaxEncodedLen)
|
derive(scale::Encode, scale::Decode, scale::MaxEncodedLen, scale::DecodeWithMemTracking)
|
||||||
)]
|
)]
|
||||||
pub struct RegisteredRetirementSignal {
|
pub struct RegisteredRetirementSignal {
|
||||||
/// The protocol to retire in favor of.
|
/// The protocol to retire in favor of.
|
||||||
@@ -40,7 +42,7 @@ pub struct RegisteredRetirementSignal {
|
|||||||
|
|
||||||
impl RegisteredRetirementSignal {
|
impl RegisteredRetirementSignal {
|
||||||
/// The ID of this signal.
|
/// The ID of this signal.
|
||||||
pub fn id(&self) -> ProtocolId {
|
pub fn id(&self) -> SignalId {
|
||||||
sp_core::blake2_256(&borsh::to_vec(self).unwrap())
|
sp_core::blake2_256(&borsh::to_vec(self).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,8 @@ impl ExternalValidatorSet {
|
|||||||
pub struct KeyShares(pub u16);
|
pub struct KeyShares(pub u16);
|
||||||
|
|
||||||
impl KeyShares {
|
impl KeyShares {
|
||||||
|
/// Zero key shares.
|
||||||
|
pub const ZERO: KeyShares = KeyShares(0);
|
||||||
/// One key share.
|
/// One key share.
|
||||||
pub const ONE: KeyShares = KeyShares(1);
|
pub const ONE: KeyShares = KeyShares(1);
|
||||||
/// The maximum amount of key shares per set.
|
/// The maximum amount of key shares per set.
|
||||||
|
|||||||
@@ -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 }
|
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-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]
|
[build-dependencies]
|
||||||
substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543" }
|
substrate-wasm-builder = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "d4624c561765c13b38eb566e435131a8c329a543" }
|
||||||
@@ -54,6 +56,8 @@ std = [
|
|||||||
"frame-executive/std",
|
"frame-executive/std",
|
||||||
|
|
||||||
"serai-coins-pallet/std",
|
"serai-coins-pallet/std",
|
||||||
|
"serai-validator-sets-pallet/std",
|
||||||
|
"serai-signals-pallet/std",
|
||||||
]
|
]
|
||||||
|
|
||||||
try-runtime = [
|
try-runtime = [
|
||||||
@@ -66,6 +70,8 @@ try-runtime = [
|
|||||||
"frame-executive/try-runtime",
|
"frame-executive/try-runtime",
|
||||||
|
|
||||||
"serai-coins-pallet/try-runtime",
|
"serai-coins-pallet/try-runtime",
|
||||||
|
"serai-validator-sets-pallet/try-runtime",
|
||||||
|
"serai-signals-pallet/try-runtime",
|
||||||
]
|
]
|
||||||
|
|
||||||
runtime-benchmarks = [
|
runtime-benchmarks = [
|
||||||
@@ -73,6 +79,9 @@ runtime-benchmarks = [
|
|||||||
|
|
||||||
"frame-system/runtime-benchmarks",
|
"frame-system/runtime-benchmarks",
|
||||||
"frame-support/runtime-benchmarks",
|
"frame-support/runtime-benchmarks",
|
||||||
|
|
||||||
|
"serai-validator-sets-pallet/runtime-benchmarks",
|
||||||
|
"serai-signals-pallet/runtime-benchmarks",
|
||||||
]
|
]
|
||||||
|
|
||||||
default = ["std"]
|
default = ["std"]
|
||||||
|
|||||||
@@ -82,6 +82,12 @@ mod runtime {
|
|||||||
|
|
||||||
#[runtime::pallet_index(3)]
|
#[runtime::pallet_index(3)]
|
||||||
pub type LiquidityTokens = serai_coins_pallet::Pallet<Runtime, LiquidityTokensInstance>;
|
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 {
|
impl frame_system::Config for Runtime {
|
||||||
@@ -127,13 +133,16 @@ impl frame_system::Config for Runtime {
|
|||||||
impl core_pallet::Config for Runtime {}
|
impl core_pallet::Config for Runtime {}
|
||||||
|
|
||||||
impl serai_coins_pallet::Config<CoinsInstance> for Runtime {
|
impl serai_coins_pallet::Config<CoinsInstance> for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
|
||||||
type AllowMint = serai_coins_pallet::AlwaysAllowMint; // TODO
|
type AllowMint = serai_coins_pallet::AlwaysAllowMint; // TODO
|
||||||
}
|
}
|
||||||
impl serai_coins_pallet::Config<LiquidityTokensInstance> for Runtime {
|
impl serai_coins_pallet::Config<LiquidityTokensInstance> for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
|
||||||
type AllowMint = serai_coins_pallet::AlwaysAllowMint;
|
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 {
|
impl From<Option<SeraiAddress>> for RuntimeOrigin {
|
||||||
fn from(signer: Option<SeraiAddress>) -> Self {
|
fn from(signer: Option<SeraiAddress>) -> Self {
|
||||||
|
|||||||
@@ -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-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 }
|
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 }
|
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]
|
[features]
|
||||||
std = [
|
std = [
|
||||||
@@ -42,10 +41,9 @@ std = [
|
|||||||
"frame-system/std",
|
"frame-system/std",
|
||||||
"frame-support/std",
|
"frame-support/std",
|
||||||
|
|
||||||
"serai-primitives/std",
|
"serai-abi/std",
|
||||||
|
|
||||||
"validator-sets-pallet/std",
|
"validator-sets-pallet/std",
|
||||||
"in-instructions-pallet/std",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
runtime-benchmarks = [
|
runtime-benchmarks = [
|
||||||
|
|||||||
1
substrate/signals/README.md
Normal file
1
substrate/signals/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Serai Signals Pallet
|
||||||
@@ -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)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
#[allow(
|
extern crate alloc;
|
||||||
deprecated,
|
|
||||||
unreachable_patterns,
|
#[expect(clippy::cast_possible_truncation)]
|
||||||
clippy::let_unit_value,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::ignored_unit_patterns
|
|
||||||
)] // TODO
|
|
||||||
#[frame_support::pallet]
|
#[frame_support::pallet]
|
||||||
pub mod pallet {
|
pub mod pallet {
|
||||||
use sp_core::sr25519::Public;
|
use sp_core::sr25519::Public;
|
||||||
use sp_io::hashing::blake2_256;
|
|
||||||
|
use serai_abi::{primitives::{prelude::*, signals::*}, SubstrateBlock};
|
||||||
|
|
||||||
use frame_system::pallet_prelude::*;
|
use frame_system::pallet_prelude::*;
|
||||||
// False positive
|
use frame_support::pallet_prelude::*;
|
||||||
#[allow(unused)]
|
|
||||||
use frame_support::{pallet_prelude::*, sp_runtime};
|
|
||||||
|
|
||||||
use serai_primitives::*;
|
use validator_sets_pallet::{Config as VsConfig, Pallet as VsPallet};
|
||||||
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};
|
|
||||||
|
|
||||||
#[pallet::config]
|
#[pallet::config]
|
||||||
pub trait Config: frame_system::Config<AccountId = Public> + VsConfig + IiConfig {
|
pub trait Config: frame_system::Config<AccountId = Public, Block = SubstrateBlock> + VsConfig {
|
||||||
type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;
|
/// How long a candidate retirement signal is valid for.
|
||||||
|
///
|
||||||
type RetirementValidityDuration: Get<u32>;
|
/// This MUST be equal to the rate at which new sets are attempted.
|
||||||
type RetirementLockInDuration: Get<u32>;
|
// 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]
|
#[pallet::genesis_config]
|
||||||
@@ -43,8 +41,12 @@ pub mod pallet {
|
|||||||
#[pallet::genesis_build]
|
#[pallet::genesis_build]
|
||||||
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
|
||||||
fn build(&self) {
|
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());
|
assert!(T::RetirementValidityDuration::get() < T::RetirementLockInDuration::get());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,204 +54,200 @@ pub mod pallet {
|
|||||||
#[pallet::pallet]
|
#[pallet::pallet]
|
||||||
pub struct Pallet<T>(PhantomData<T>);
|
pub struct Pallet<T>(PhantomData<T>);
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug, Encode, Decode, MaxEncodedLen)]
|
/// The registered retirement signals.
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
type RegisteredRetirementSignals<T: Config> =
|
type RegisteredRetirementSignals<T: Config> =
|
||||||
StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal<T>, OptionQuery>;
|
StorageMap<_, Blake2_128Concat, [u8; 32], RegisteredRetirementSignal, OptionQuery>;
|
||||||
|
|
||||||
|
/// The registered favors.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
pub type Favors<T: Config> = StorageDoubleMap<
|
type Favors<T: Config> = StorageDoubleMap<
|
||||||
_,
|
_,
|
||||||
Blake2_128Concat,
|
Blake2_128Concat,
|
||||||
(SignalId, NetworkId),
|
(Signal, NetworkId),
|
||||||
Blake2_128Concat,
|
Blake2_128Concat,
|
||||||
T::AccountId,
|
T::AccountId,
|
||||||
(),
|
(),
|
||||||
OptionQuery,
|
OptionQuery,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
/// The networks in favor of a signal.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
pub type SetsInFavor<T: Config> =
|
type NetworksInFavor<T: Config> =
|
||||||
StorageMap<_, Blake2_128Concat, (SignalId, ValidatorSet), (), OptionQuery>;
|
StorageMap<_, Blake2_128Concat, (Signal, NetworkId), (), OptionQuery>;
|
||||||
|
|
||||||
|
/// The locked-in retirement signal.
|
||||||
|
///
|
||||||
|
/// This is in the format `(protocol_id, retiry_block)`.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
pub type LockedInRetirement<T: Config> =
|
type LockedInRetirement<T: Config> =
|
||||||
StorageValue<_, ([u8; 32], BlockNumberFor<T>), OptionQuery>;
|
StorageValue<_, (ProtocolId, BlockNumberFor<T>), OptionQuery>;
|
||||||
|
|
||||||
#[pallet::event]
|
/// Halted networks.
|
||||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
///
|
||||||
pub enum Event<T: Config> {
|
/// Halted networks will be halted for the remainder of this protocol's lifetime.
|
||||||
RetirementSignalRegistered {
|
#[pallet::storage]
|
||||||
signal_id: [u8; 32],
|
type Halted<T: Config> = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>;
|
||||||
in_favor_of: [u8; 32],
|
|
||||||
registrant: T::AccountId,
|
#[pallet::hooks]
|
||||||
},
|
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||||
RetirementSignalRevoked {
|
fn on_initialize(current_number: BlockNumberFor<T>) -> Weight {
|
||||||
signal_id: [u8; 32],
|
/*
|
||||||
},
|
If this is the block at which a locked-in retirement signal has been locked-in for long
|
||||||
SignalFavored {
|
enough, panic, halting the blockchain, and retiring the current protocol.
|
||||||
signal_id: SignalId,
|
*/
|
||||||
by: T::AccountId,
|
if let Some((protocol_id, block_number)) = LockedInRetirement::<T>::get() {
|
||||||
for_network: NetworkId,
|
if block_number == current_number {
|
||||||
},
|
panic!(
|
||||||
SetInFavor {
|
"protocol retired in favor of {}",
|
||||||
signal_id: SignalId,
|
sp_core::hexdisplay::HexDisplay::from(&protocol_id)
|
||||||
set: ValidatorSet,
|
);
|
||||||
},
|
}
|
||||||
RetirementSignalLockedIn {
|
}
|
||||||
signal_id: [u8; 32],
|
// Using `Weight::zero()` is fine here as this is a minute operation
|
||||||
},
|
Weight::zero()
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[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> {
|
impl<T: Config> Pallet<T> {
|
||||||
// Returns true if this network's current set is in favor of the signal.
|
/// Tally the support for a signal by a network's current validator set.
|
||||||
//
|
///
|
||||||
// Must only be called for networks which have a set decided.
|
/// This will mutate the storage with the result.
|
||||||
fn tally_for_network(signal_id: SignalId, network: NetworkId) -> bool {
|
///
|
||||||
let this_network_session = VsPallet::<T>::latest_decided_session(network).unwrap();
|
/// This returns `true` if the network is sufficiently in favor of the signal.
|
||||||
let this_set = ValidatorSet { network, session: this_network_session };
|
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
|
The following uses key shares, not allocations, as key shares are static while allocations
|
||||||
// TODO: Make caching feasible? Do a first-pass with cache then actual pass before
|
fluctuate during the duration of a validator set.
|
||||||
// execution?
|
*/
|
||||||
let mut iter = Favors::<T>::iter_prefix_values((signal_id, network));
|
|
||||||
let mut needed_favor = (VsPallet::<T>::total_allocated_stake(network).unwrap().0 *
|
let mut needed_favor = {
|
||||||
REQUIREMENT_NUMERATOR)
|
let current = VsPallet::<T>::key_shares(current_set)
|
||||||
.div_ceil(REQUIREMENT_DIVISOR);
|
.expect("current validator set without key shares set")
|
||||||
while iter.next().is_some() && (needed_favor != 0) {
|
.0;
|
||||||
let item_key = iter.last_raw_key();
|
let latest = VsPallet::<T>::key_shares(latest_set)
|
||||||
// `.len() - 32` is safe because AccountId is bound to being Public, which is 32 bytes
|
.expect("latest validator set without key shares set")
|
||||||
let account = T::AccountId::decode(&mut &item_key[(item_key.len() - 32) ..]).unwrap();
|
.0;
|
||||||
if VsPallet::<T>::in_latest_decided_set(network, account) {
|
current.max(latest)
|
||||||
// This call uses the current allocation, not the allocation at the time of set
|
};
|
||||||
// decision
|
for (validator, ()) in Favors::<T>::iter_prefix((signal, network)) {
|
||||||
// This is deemed safe due to the validator-set pallet's deallocation scheduling
|
/*
|
||||||
// unwrap is safe due to being in the latest decided set
|
Fetch the amount of key shares the validator has.
|
||||||
needed_favor =
|
|
||||||
needed_favor.saturating_sub(VsPallet::<T>::allocation((network, account)).unwrap().0);
|
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 {
|
let now_in_favor = needed_favor == 0;
|
||||||
// Set the set as in favor until someone triggers a re-tally
|
|
||||||
//
|
// Update the storage and emit an event, if appropriate
|
||||||
// Since a re-tally is an extra step we can't assume will occur, this effectively means a
|
if now_in_favor {
|
||||||
// network in favor across any point in its Session is in favor for its entire Session
|
let prior_in_favor = NetworksInFavor::<T>::contains_key((signal, network));
|
||||||
// While a malicious actor could increase their stake, favor a signal, then deallocate,
|
NetworksInFavor::<T>::set((signal, network), Some(()));
|
||||||
// this is largely prevented by deallocation scheduling
|
if !prior_in_favor {
|
||||||
//
|
todo!("Event");
|
||||||
// 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 });
|
|
||||||
}
|
}
|
||||||
true
|
|
||||||
} else {
|
} else {
|
||||||
if SetsInFavor::<T>::contains_key((signal_id, this_set)) {
|
#[allow(clippy::collapsible_else_if)]
|
||||||
// This should no longer be under the current tally
|
if NetworksInFavor::<T>::take((signal, network)).is_some() {
|
||||||
SetsInFavor::<T>::remove((signal_id, this_set));
|
todo!("Event");
|
||||||
Self::deposit_event(Event::SetNoLongerInFavor { signal_id, set: this_set });
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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_in_favor_stake = 0;
|
||||||
let mut total_allocated_stake = 0;
|
let mut total_allocated_stake = 0;
|
||||||
for network in serai_primitives::NETWORKS {
|
for network in NetworkId::all() {
|
||||||
let Some(latest_decided_session) = VsPallet::<T>::latest_decided_session(network) else {
|
/*
|
||||||
continue;
|
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
|
||||||
// If it has a session, it should have a total allocated stake value
|
the total key shares, should be sufficient in this regard.
|
||||||
let network_stake = VsPallet::<T>::total_allocated_stake(network).unwrap();
|
*/
|
||||||
if SetsInFavor::<T>::contains_key((
|
let network_stake =
|
||||||
signal_id,
|
VsPallet::<T>::stake_for_current_validator_set(network).unwrap_or(Amount(0));
|
||||||
ValidatorSet { network, session: latest_decided_session },
|
if NetworksInFavor::<T>::contains_key((signal, network)) {
|
||||||
)) {
|
|
||||||
total_in_favor_stake += network_stake.0;
|
total_in_favor_stake += network_stake.0;
|
||||||
}
|
}
|
||||||
total_allocated_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(
|
fn revoke_favor_internal(
|
||||||
account: T::AccountId,
|
validator: T::AccountId,
|
||||||
signal_id: SignalId,
|
signal: Signal,
|
||||||
for_network: NetworkId,
|
for_network: NetworkId,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
if !Favors::<T>::contains_key((signal_id, for_network), account) {
|
if !Favors::<T>::contains_key((signal, for_network), validator) {
|
||||||
Err::<(), _>(Error::<T>::RevokingNonExistentFavor)?;
|
Err::<(), _>(Error::<T>::RevokingNonExistentFavor)?;
|
||||||
}
|
}
|
||||||
Favors::<T>::remove((signal_id, for_network), account);
|
Favors::<T>::remove((signal, for_network), validator);
|
||||||
Self::deposit_event(Event::<T>::FavorRevoked { signal_id, by: account, for_network });
|
// TODO: Event
|
||||||
// tally_for_network assumes the network is active, which is implied by having prior set a
|
|
||||||
// favor for it
|
// Update the tally for this network
|
||||||
// Technically, this tally may make the network in favor and justify re-tallying for all
|
Self::tally_for_network(signal, for_network);
|
||||||
// networks
|
|
||||||
// Its assumed not to
|
|
||||||
Self::tally_for_network(signal_id, for_network);
|
|
||||||
Ok(())
|
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]
|
#[pallet::call]
|
||||||
impl<T: Config> Pallet<T> {
|
impl<T: Config> Pallet<T> {
|
||||||
/// Register a retirement signal, declaring the consensus protocol this signal is in favor of.
|
/// 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
|
/// Retirement signals are registered so that the proposer, presumably a developer, can revoke
|
||||||
/// the signal if there's a fault discovered.
|
/// the signal if there's a fault discovered.
|
||||||
#[pallet::call_index(0)]
|
#[pallet::call_index(0)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn register_retirement_signal(
|
pub fn register_retirement_signal(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
in_favor_of: [u8; 32],
|
in_favor_of: [u8; 32],
|
||||||
@@ -267,14 +265,17 @@ pub mod pallet {
|
|||||||
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
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
|
Bind the signal ID to the proposer.
|
||||||
// registrant, just to cancel it later
|
|
||||||
|
This prevents a malicious actor from frontrunning a proposal, causing them to be the
|
||||||
|
registrant, just to cancel it later.
|
||||||
|
*/
|
||||||
let signal = RegisteredRetirementSignal {
|
let signal = RegisteredRetirementSignal {
|
||||||
in_favor_of,
|
in_favor_of,
|
||||||
registrant: account,
|
registrant: validator.into(),
|
||||||
registered_at: frame_system::Pallet::<T>::block_number(),
|
registered_at: frame_system::Pallet::<T>::block_number(),
|
||||||
};
|
};
|
||||||
let signal_id = signal.id();
|
let signal_id = signal.id();
|
||||||
@@ -282,122 +283,108 @@ pub mod pallet {
|
|||||||
if RegisteredRetirementSignals::<T>::get(signal_id).is_some() {
|
if RegisteredRetirementSignals::<T>::get(signal_id).is_some() {
|
||||||
Err::<(), _>(Error::<T>::RetirementSignalAlreadyRegistered)?;
|
Err::<(), _>(Error::<T>::RetirementSignalAlreadyRegistered)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::deposit_event(Event::<T>::RetirementSignalRegistered {
|
|
||||||
signal_id,
|
|
||||||
in_favor_of,
|
|
||||||
registrant: account,
|
|
||||||
});
|
|
||||||
RegisteredRetirementSignals::<T>::set(signal_id, Some(signal));
|
RegisteredRetirementSignals::<T>::set(signal_id, Some(signal));
|
||||||
|
|
||||||
|
// TODO: Event
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Revoke a retirement signal.
|
||||||
#[pallet::call_index(1)]
|
#[pallet::call_index(1)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn revoke_retirement_signal(
|
pub fn revoke_retirement_signal(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
retirement_signal_id: [u8; 32],
|
retirement_signal: [u8; 32],
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
let account = ensure_signed(origin)?;
|
let validator = ensure_signed(origin)?;
|
||||||
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(retirement_signal_id)
|
let Some(registered_signal) = RegisteredRetirementSignals::<T>::get(retirement_signal)
|
||||||
else {
|
else {
|
||||||
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
|
return Err::<(), _>(Error::<T>::NonExistentRetirementSignal.into());
|
||||||
};
|
};
|
||||||
if account != registered_signal.registrant {
|
if SeraiAddress::from(validator) != registered_signal.registrant {
|
||||||
Err::<(), _>(Error::<T>::NotRetirementSignalRegistrant)?;
|
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
|
If this signal was locked in, remove it.
|
||||||
// intervention by all validators
|
|
||||||
if LockedInRetirement::<T>::get().map(|(signal_id, _block_number)| signal_id) ==
|
This lets a post-lock-in discovered fault be prevented from going live without intervention
|
||||||
Some(retirement_signal_id)
|
by a supermajority of validators.
|
||||||
|
*/
|
||||||
|
if LockedInRetirement::<T>::get().map(|(signal, _block_number)| signal) ==
|
||||||
|
Some(retirement_signal)
|
||||||
{
|
{
|
||||||
LockedInRetirement::<T>::kill();
|
LockedInRetirement::<T>::kill();
|
||||||
}
|
}
|
||||||
|
|
||||||
Self::deposit_event(Event::<T>::RetirementSignalRevoked { signal_id: retirement_signal_id });
|
// TODO: Event
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Favor a signal.
|
||||||
#[pallet::call_index(2)]
|
#[pallet::call_index(2)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn favor(
|
pub fn favor(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
signal_id: SignalId,
|
signal: Signal,
|
||||||
for_network: NetworkId,
|
for_network: NetworkId,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
let account = ensure_signed(origin)?;
|
let validator = ensure_signed(origin)?;
|
||||||
|
|
||||||
// If this is a retirement signal, perform the relevant checks
|
// Perform the relevant checks for this class of signal
|
||||||
if let SignalId::Retirement(signal_id) = signal_id {
|
match signal {
|
||||||
|
Signal::Retire { signal_id } => {
|
||||||
// Make sure a retirement hasn't already been locked in
|
// Make sure a retirement hasn't already been locked in
|
||||||
if LockedInRetirement::<T>::exists() {
|
if LockedInRetirement::<T>::exists() {
|
||||||
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
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
|
Make sure this is a registered retirement.
|
||||||
// process
|
|
||||||
|
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 {
|
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
|
// Check the signal isn't out of date, and its tallies with it.
|
||||||
// This isn't truly necessary since we only track votes from the most recent validator
|
if (registered_signal.registered_at + T::RetirementValidityDuration::get()) <
|
||||||
// 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::<T>::block_number()
|
frame_system::Pallet::<T>::block_number()
|
||||||
{
|
{
|
||||||
Err::<(), _>(Error::<T>::ExpiredRetirementSignal)?;
|
Err::<(), _>(Error::<T>::ExpiredRetirementSignal)?;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
Signal::Halt { .. } => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check the signer is a validator
|
if Favors::<T>::contains_key((signal, for_network), validator) {
|
||||||
// Technically, in the case of Serai, this will check they're planned to be in the next set,
|
Err::<(), _>(Error::<T>::AlreadyInFavor)?;
|
||||||
// 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)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set them as in-favor
|
// Set the validator as in favor
|
||||||
// Doesn't error if they already voted in order to let any validator trigger a re-tally
|
Favors::<T>::set((signal, for_network), validator, Some(()));
|
||||||
if !Favors::<T>::contains_key((signal_id, for_network), account) {
|
// TODO: Event
|
||||||
Favors::<T>::set((signal_id, for_network), account, Some(()));
|
|
||||||
Self::deposit_event(Event::SignalFavored { signal_id, by: account, for_network });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the network is in favor
|
// Check if the network is in favor
|
||||||
// tally_for_network expects the network to be active, which is implied by being in the
|
let network_in_favor = Self::tally_for_network(signal, for_network);
|
||||||
// latest decided set
|
|
||||||
let network_in_favor = Self::tally_for_network(signal_id, for_network);
|
|
||||||
|
|
||||||
// If this network is in favor, check if enough networks are
|
// 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
|
if network_in_favor && Self::tally_for_all_networks(signal) {
|
||||||
// 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 enough are, lock in the signal
|
// If enough are, lock in the signal
|
||||||
if Self::tally_for_all_networks(signal_id) {
|
match signal {
|
||||||
match signal_id {
|
Signal::Retire { signal_id } => {
|
||||||
SignalId::Retirement(signal_id) => {
|
|
||||||
LockedInRetirement::<T>::set(Some((
|
LockedInRetirement::<T>::set(Some((
|
||||||
signal_id,
|
signal_id,
|
||||||
frame_system::Pallet::<T>::block_number() +
|
frame_system::Pallet::<T>::block_number() + T::RetirementLockInDuration::get()
|
||||||
T::RetirementLockInDuration::get().into(),
|
|
||||||
)));
|
)));
|
||||||
Self::deposit_event(Event::RetirementSignalLockedIn { signal_id });
|
// TODO: Event
|
||||||
}
|
|
||||||
SignalId::Halt(network) => {
|
|
||||||
InInstructions::<T>::halt(network)?;
|
|
||||||
}
|
}
|
||||||
|
Signal::Halt(network) => {
|
||||||
|
Halted::<T>::set(network, Some(()));
|
||||||
|
// TODO: Event
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,75 +394,110 @@ pub mod pallet {
|
|||||||
|
|
||||||
/// Revoke favor into an abstaining position.
|
/// Revoke favor into an abstaining position.
|
||||||
#[pallet::call_index(3)]
|
#[pallet::call_index(3)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn revoke_favor(
|
pub fn revoke_favor(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
signal_id: SignalId,
|
signal: Signal,
|
||||||
for_network: NetworkId,
|
for_network: NetworkId,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
if matches!(&signal_id, SignalId::Retirement(_)) && LockedInRetirement::<T>::exists() {
|
match signal {
|
||||||
|
Signal::Retire { .. } => {
|
||||||
|
if LockedInRetirement::<T>::exists() {
|
||||||
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Signal::Halt { .. } => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Doesn't check the signal exists due to later checking the favor exists
|
let validator = ensure_signed(origin)?;
|
||||||
// While the signal may have been revoked, making this pointless, it's not worth the storage
|
Self::revoke_favor_internal(validator, signal, for_network)
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Emit an event standing against the signal.
|
/// 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.
|
/// If the origin is currently in favor of the signal, their favor will be revoked.
|
||||||
#[pallet::call_index(4)]
|
#[pallet::call_index(4)]
|
||||||
#[pallet::weight(0)] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn stand_against(
|
pub fn stand_against(
|
||||||
origin: OriginFor<T>,
|
origin: OriginFor<T>,
|
||||||
signal_id: SignalId,
|
signal: Signal,
|
||||||
for_network: NetworkId,
|
for_network: NetworkId,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
|
match signal {
|
||||||
|
Signal::Retire { .. } => {
|
||||||
if LockedInRetirement::<T>::exists() {
|
if LockedInRetirement::<T>::exists() {
|
||||||
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
Err::<(), _>(Error::<T>::RetirementSignalLockedIn)?;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
Signal::Halt { .. } => {}
|
||||||
|
}
|
||||||
|
|
||||||
let account = ensure_signed(origin)?;
|
let validator = ensure_signed(origin)?;
|
||||||
// If currently in favor, revoke the favor
|
// If currently in favor, revoke the favor
|
||||||
if Favors::<T>::contains_key((signal_id, for_network), account) {
|
if Favors::<T>::contains_key((signal, for_network), validator) {
|
||||||
Self::revoke_favor_internal(account, signal_id, for_network)?;
|
Self::revoke_favor_internal(validator, signal, for_network)?;
|
||||||
} else {
|
} else {
|
||||||
// Check this Signal exists (which would've been implied by Favors for it existing)
|
// Check this Signal exists (which would've been implied by `Favors` for it existing)
|
||||||
if let SignalId::Retirement(signal_id) = signal_id {
|
match signal {
|
||||||
|
Signal::Retire { signal_id } => {
|
||||||
if RegisteredRetirementSignals::<T>::get(signal_id).is_none() {
|
if RegisteredRetirementSignals::<T>::get(signal_id).is_none() {
|
||||||
Err::<(), _>(Error::<T>::NonExistentRetirementSignal)?;
|
Err::<(), _>(Error::<T>::NonExistentRetirementSignal)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Signal::Halt { .. } => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit an event that we're against the signal
|
// Emit the event
|
||||||
// No actual effects happen besides this
|
// TODO: Event
|
||||||
Self::deposit_event(Event::<T>::AgainstSignal { signal_id, who: account, for_network });
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[pallet::hooks]
|
/* TODO
|
||||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
#[pallet::event]
|
||||||
fn on_initialize(current_number: BlockNumberFor<T>) -> Weight {
|
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||||
// If this is the block at which a locked-in signal has been set for long enough, panic
|
pub enum Event<T: Config> {
|
||||||
// This will prevent this block from executing and halt the chain
|
RetirementSignalRegistered {
|
||||||
if let Some((signal, block_number)) = LockedInRetirement::<T>::get() {
|
signal: [u8; 32],
|
||||||
if block_number == current_number {
|
in_favor_of: [u8; 32],
|
||||||
panic!(
|
registrant: T::AccountId,
|
||||||
"locked-in signal {} has been set for too long",
|
},
|
||||||
sp_core::hexdisplay::HexDisplay::from(&signal),
|
RetirementSignalRevoked {
|
||||||
);
|
signal_id: [u8; 32],
|
||||||
}
|
},
|
||||||
}
|
SignalFavored {
|
||||||
Weight::zero() // TODO
|
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::*;
|
pub use pallet::*;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
#![cfg_attr(not(feature = "std"), no_std)]
|
||||||
|
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
|
use alloc::vec::Vec;
|
||||||
|
|
||||||
mod embedded_elliptic_curve_keys;
|
mod embedded_elliptic_curve_keys;
|
||||||
use 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]
|
#[frame_support::pallet]
|
||||||
mod pallet {
|
mod pallet {
|
||||||
use sp_core::sr25519::Public;
|
use sp_core::sr25519::Public;
|
||||||
@@ -94,7 +95,7 @@ mod pallet {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[pallet::config]
|
#[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>>;
|
// type ShouldEndSession: ShouldEndSession<BlockNumberFor<Self>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,6 +384,11 @@ mod pallet {
|
|||||||
Abstractions::<T>::key_shares_possessed_by_validator(set, validator)
|
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.
|
// is_bft returns if the network is able to survive any single node becoming byzantine.
|
||||||
fn is_bft(network: NetworkId) -> bool {
|
fn is_bft(network: NetworkId) -> bool {
|
||||||
@@ -840,7 +846,7 @@ mod pallet {
|
|||||||
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
#[pallet::weight((0, DispatchClass::Normal))] // TODO
|
||||||
pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
|
pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
|
||||||
let validator = ensure_signed(origin)?;
|
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)
|
Abstractions::<T>::increase_allocation(network, validator, amount, false)
|
||||||
.map_err(Error::<T>::AllocationError)?;
|
.map_err(Error::<T>::AllocationError)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -854,7 +860,7 @@ mod pallet {
|
|||||||
let deallocation_timeline = Abstractions::<T>::decrease_allocation(network, account, amount)
|
let deallocation_timeline = Abstractions::<T>::decrease_allocation(network, account, amount)
|
||||||
.map_err(Error::<T>::DeallocationError)?;
|
.map_err(Error::<T>::DeallocationError)?;
|
||||||
if matches!(deallocation_timeline, DeallocationTimeline::Immediate) {
|
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(())
|
Ok(())
|
||||||
@@ -870,7 +876,7 @@ mod pallet {
|
|||||||
let account = ensure_signed(origin)?;
|
let account = ensure_signed(origin)?;
|
||||||
let amount = Abstractions::<T>::claim_delayed_deallocation(account, network, session)
|
let amount = Abstractions::<T>::claim_delayed_deallocation(account, network, session)
|
||||||
.map_err(Error::<T>::DeallocationError)?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use alloc::vec::Vec;
|
||||||
use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec};
|
use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec};
|
||||||
|
|
||||||
use serai_primitives::{
|
use serai_primitives::{
|
||||||
@@ -211,6 +212,9 @@ pub(crate) trait Sessions {
|
|||||||
set: ValidatorSet,
|
set: ValidatorSet,
|
||||||
validator: Public,
|
validator: Public,
|
||||||
) -> Option<KeySharesStruct>;
|
) -> 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 {
|
impl<Storage: SessionsStorage> Sessions for Storage {
|
||||||
@@ -516,4 +520,8 @@ impl<Storage: SessionsStorage> Sessions for Storage {
|
|||||||
) -> Option<KeySharesStruct> {
|
) -> Option<KeySharesStruct> {
|
||||||
Storage::SelectedValidators::get(selected_validators_key(set, validator))
|
Storage::SelectedValidators::get(selected_validators_key(set, validator))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stake_for_current_validator_set(network: NetworkId) -> Option<Amount> {
|
||||||
|
Storage::TotalAllocatedStake::get(network)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user