Add signals pallet

Resolves #353

Implements code such that:

- 80% of validators (by stake) must be in favor of a signal for the network to
  be
- 80% of networks (by stake) must be in favor of a signal for it to be locked
  in
- After a signal has been locked in for two weeks, the network halts

The intention is to:

1) Not allow validators to unilaterally declare new consensus rules.

No method of declaring new consensus rules is provided by this pallet. Solely a
way to deprecate the current rules, with a signaled for successor. All nodes
must then individually decide whether or not to download and run a new node
which has new rules, and if so, which rules.

2) Not place blobs on chain.

Even if they'd be reproducible, it's just a lot of data to chuck on the
blockchain.
This commit is contained in:
Luke Parker
2023-10-21 20:06:53 -04:00
parent b66203ae3f
commit 1bff2a0447
10 changed files with 556 additions and 38 deletions

View File

@@ -55,13 +55,17 @@ pub mod pallet {
#[pallet::storage]
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
impl<T: Config> Pallet<T> {
pub fn session(network: NetworkId) -> Session {
pub fn session(network: NetworkId) -> Option<Session> {
if network == NetworkId::Serai {
Session(pallet_session::Pallet::<T>::current_index())
Some(Session(pallet_session::Pallet::<T>::current_index()))
} else {
CurrentSession::<T>::get(network).unwrap()
CurrentSession::<T>::get(network)
}
}
pub fn latest_decided_session(network: NetworkId) -> Option<Session> {
CurrentSession::<T>::get(network)
}
}
/// The allocation required per key share.
@@ -86,9 +90,61 @@ pub mod pallet {
#[pallet::storage]
pub type InSet<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 16], Public), (), OptionQuery>;
impl<T: Config> Pallet<T> {
fn in_set_key(
network: NetworkId,
account: T::AccountId,
) -> (NetworkId, [u8; 16], T::AccountId) {
(network, sp_io::hashing::blake2_128(&(network, account).encode()), account)
}
// This exists as InSet, for Serai, is the validators set for the next session, *not* the
// current set's validators
#[inline]
fn in_active_serai_set(account: Public) -> bool {
// TODO: This is bounded O(n). Can we get O(1) via a storage lookup, like we do with InSet?
for validator in pallet_session::Pallet::<T>::validators() {
if validator == account {
return true;
}
}
false
}
/// Returns true if the account is included in an active set.
pub fn in_active_set(network: NetworkId, account: Public) -> bool {
if network == NetworkId::Serai {
Self::in_active_serai_set(account)
} else {
InSet::<T>::contains_key(Self::in_set_key(network, account))
}
}
/// Returns true if the account has been definitively included in an active or upcoming set.
pub fn in_set(network: NetworkId, account: Public) -> bool {
if InSet::<T>::contains_key(Self::in_set_key(network, account)) {
return true;
}
if network == NetworkId::Serai {
return Self::in_active_serai_set(account);
}
false
}
/// Returns true if the account is present in the latest decided set.
///
/// This is useful when working with `allocation` and `total_allocated_stake`, which return the
/// latest information.
pub fn in_latest_decided_set(network: NetworkId, account: Public) -> bool {
InSet::<T>::contains_key(Self::in_set_key(network, account))
}
}
/// The total stake allocated to this network by the active set of validators.
#[pallet::storage]
#[pallet::getter(fn total_allocated_stake)]
pub type TotalAllocatedStake<T: Config> = StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
/// The current amount allocated to a validator set by a validator.
@@ -245,13 +301,6 @@ pub mod pallet {
}
impl<T: Config> Pallet<T> {
fn in_set_key(
network: NetworkId,
account: T::AccountId,
) -> (NetworkId, [u8; 16], T::AccountId) {
(network, sp_io::hashing::blake2_128(&(network, account).encode()), account)
}
fn new_set(network: NetworkId) {
// Update CurrentSession
let session = if network != NetworkId::Serai {
@@ -261,7 +310,7 @@ pub mod pallet {
CurrentSession::<T>::set(network, Some(new_session));
new_session
} else {
Self::session(network)
Self::session(network).unwrap_or(Session(0))
};
// Clear the current InSet
@@ -512,6 +561,9 @@ pub mod pallet {
}
// Decrease the allocation now
// Since we don't also update TotalAllocatedStake here, TotalAllocatedStake may be greater
// than the sum of all allocations, according to the Allocations StorageMap
// This is intentional as this allocation has only been queued for deallocation at this time
Self::set_allocation(network, account, Amount(new_allocation));
if let Some(was_bft) = was_bft {
@@ -520,22 +572,8 @@ pub mod pallet {
}
}
// If we're not in-set, allow immediate deallocation
let mut active = InSet::<T>::contains_key(Self::in_set_key(network, account));
// If the network is Serai, also check pallet_session's list of active validators, as our
// InSet is actually the queued for next session's validators
// Only runs if active isn't already true in order to short-circuit
if (!active) && (network == NetworkId::Serai) {
// TODO: This is bounded O(n). Can we get O(1) via a storage lookup, like we do with
// InSet?
for validator in pallet_session::Pallet::<T>::validators() {
if validator == account {
active = true;
break;
}
}
}
// Also allow immediate deallocation if the key shares remain the same
// If we're not in-set, or this doesn't decrease our key shares, allow immediate deallocation
let active = Self::in_set(network, account);
if (!active) || (!decreased_key_shares) {
if active {
// Since it's being immediately deallocated, decrease TotalAllocatedStake
@@ -548,7 +586,8 @@ pub mod pallet {
}
// Set it to PendingDeallocations, letting the staking pallet release it on a future session
let mut to_unlock_on = Self::session(network);
// This unwrap should be fine as this account is active, meaning a session has occurred
let mut to_unlock_on = Self::session(network).unwrap();
if network == NetworkId::Serai {
// Since the next Serai set will already have been decided, we can only deallocate once the
// next set ends
@@ -570,7 +609,7 @@ pub mod pallet {
// Checks if this session has completed the handover from the prior session.
fn handover_completed(network: NetworkId, session: Session) -> bool {
let current_session = Self::session(network);
let Some(current_session) = Self::session(network) else { return false };
// No handover occurs on genesis
if current_session.0 == 0 {
return true;
@@ -597,7 +636,8 @@ pub mod pallet {
pub fn new_session() {
for network in serai_primitives::NETWORKS {
let current_session = Self::session(network);
// If this network hasn't started sessions yet, don't start one now
let Some(current_session) = Self::session(network) else { continue };
// Only spawn a NewSet if the current set was actually established with a completed
// handover protocol
if Self::handover_completed(network, current_session) {