Remove the staking pallet for validator-sets alone

The staking pallet is an indirection which offered no practical benefit yet
increased the overhead of every call.
This commit is contained in:
Luke Parker
2023-10-22 03:59:21 -04:00
parent a702d65c3d
commit d66a7ee43e
10 changed files with 193 additions and 434 deletions

View File

@@ -16,9 +16,12 @@ pub mod pallet {
pub use validator_sets_primitives as primitives;
use primitives::*;
use coins_pallet::Pallet as Coins;
#[pallet::config]
pub trait Config:
frame_system::Config<AccountId = Public>
+ coins_pallet::Config
+ pallet_session::Config<ValidatorId = Public>
+ TypeInfo
{
@@ -245,37 +248,6 @@ pub mod pallet {
}
}
impl<T: Config> Pallet<T> {
fn is_bft(network: NetworkId) -> bool {
let allocation_per_key_share = AllocationPerKeyShare::<T>::get(network).unwrap().0;
let mut validators_len = 0;
let mut top = None;
let mut key_shares = 0;
for (_, amount) in SortedAllocationsIter::<T>::new(network) {
validators_len += 1;
key_shares += amount.0 / allocation_per_key_share;
if top.is_none() {
top = Some(key_shares);
}
if key_shares > u64::from(MAX_KEY_SHARES_PER_SET) {
break;
}
}
let Some(top) = top else { return false };
// key_shares may be over MAX_KEY_SHARES_PER_SET, which will cause an off-chain reduction of
// each validator's key shares until their sum is MAX_KEY_SHARES_PER_SET
// post_amortization_key_shares_for_top_validator yields what the top validator's key shares
// would be after such a reduction, letting us evaluate this correctly
let top = post_amortization_key_shares_for_top_validator(validators_len, top, key_shares);
(top * 3) < key_shares.min(MAX_KEY_SHARES_PER_SET.into())
}
}
/// Pending deallocations, keyed by the Session they become unlocked on.
#[pallet::storage]
type PendingDeallocations<T: Config> =
@@ -312,6 +284,11 @@ pub mod pallet {
amount: Amount,
delayed_until: Option<Session>,
},
DeallocationClaimed {
validator: T::AccountId,
network: NetworkId,
session: Session,
},
SetRetired {
set: ValidatorSet,
},
@@ -383,6 +360,8 @@ pub mod pallet {
DeallocationWouldRemoveParticipant,
/// Deallocation would cause the validator set to no longer achieve fault tolerance.
DeallocationWouldRemoveFaultTolerance,
/// Deallocation to be claimed doesn't exist.
NonExistentDeallocation,
/// Validator Set already generated keys.
AlreadyGeneratedKeys,
/// An invalid MuSig signature was provided.
@@ -426,78 +405,41 @@ pub mod pallet {
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
pub fn set_keys(
origin: OriginFor<T>,
network: NetworkId,
key_pair: KeyPair,
signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
// signature isn't checked as this is an unsigned transaction, and validate_unsigned
// (called by pre_dispatch) checks it
let _ = signature;
let session = Session(pallet_session::Pallet::<T>::current_index());
let set = ValidatorSet { session, network };
Keys::<T>::set(set, Some(key_pair.clone()));
Self::deposit_event(Event::KeyGen { set, key_pair });
Ok(())
fn account() -> T::AccountId {
system_address(b"validator-sets").into()
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn is_bft(network: NetworkId) -> bool {
let allocation_per_key_share = AllocationPerKeyShare::<T>::get(network).unwrap().0;
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
// Match to be exhaustive
let (network, key_pair, signature) = match call {
Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature),
Call::__Ignore(_, _) => unreachable!(),
};
let mut validators_len = 0;
let mut top = None;
let mut key_shares = 0;
for (_, amount) in SortedAllocationsIter::<T>::new(network) {
validators_len += 1;
let session = Session(pallet_session::Pallet::<T>::current_index());
key_shares += amount.0 / allocation_per_key_share;
if top.is_none() {
top = Some(key_shares);
}
let set = ValidatorSet { session, network: *network };
match Self::verify_signature(set, key_pair, signature) {
Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?,
Err(Error::NonExistentValidatorSet) |
Err(Error::InsufficientAllocation) |
Err(Error::NotEnoughAllocated) |
Err(Error::AllocationWouldRemoveFaultTolerance) |
Err(Error::DeallocationWouldRemoveParticipant) |
Err(Error::DeallocationWouldRemoveFaultTolerance) |
Err(Error::NonExistentValidator) |
Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
Err(Error::__Ignore(_, _)) => unreachable!(),
Ok(()) => (),
if key_shares > u64::from(MAX_KEY_SHARES_PER_SET) {
break;
}
}
ValidTransaction::with_tag_prefix("validator-sets")
.and_provides(set)
// Set a 10 block longevity, though this should be included in the next block
.longevity(10)
.propagate(true)
.build()
let Some(top) = top else { return false };
// key_shares may be over MAX_KEY_SHARES_PER_SET, which will cause an off-chain reduction of
// each validator's key shares until their sum is MAX_KEY_SHARES_PER_SET
// post_amortization_key_shares_for_top_validator yields what the top validator's key shares
// would be after such a reduction, letting us evaluate this correctly
let top = post_amortization_key_shares_for_top_validator(validators_len, top, key_shares);
(top * 3) < key_shares.min(MAX_KEY_SHARES_PER_SET.into())
}
// Explicitly provide a pre-dispatch which calls validate_unsigned
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into)
}
}
impl<T: Config> Pallet<T> {
#[frame_support::transactional]
pub fn increase_allocation(
fn increase_allocation(
network: NetworkId,
account: T::AccountId,
amount: Amount,
@@ -548,8 +490,7 @@ pub mod pallet {
/// doesn't become used (preventing deallocation).
///
/// Returns if the amount is immediately eligible for deallocation.
#[frame_support::transactional]
pub fn decrease_allocation(
fn decrease_allocation(
network: NetworkId,
account: T::AccountId,
amount: Amount,
@@ -609,7 +550,7 @@ pub mod pallet {
return Ok(true);
}
// Set it to PendingDeallocations, letting the staking pallet release it on a future session
// Set it to PendingDeallocations, letting it be released upon a future session
// 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 {
@@ -665,7 +606,7 @@ pub mod pallet {
true
}
pub fn new_session() {
fn new_session() {
for network in serai_primitives::NETWORKS {
// If this network hasn't started sessions yet, don't start one now
let Some(current_session) = Self::session(network) else { continue };
@@ -677,10 +618,6 @@ pub mod pallet {
}
}
pub fn select_validators(network: NetworkId) -> Vec<Public> {
Self::participants(network).into()
}
pub fn retire_set(set: ValidatorSet) {
MuSigKeys::<T>::remove(set);
Keys::<T>::remove(set);
@@ -690,7 +627,7 @@ pub mod pallet {
/// Take the amount deallocatable.
///
/// `session` refers to the Session the stake becomes deallocatable on.
pub fn take_deallocatable_amount(
fn take_deallocatable_amount(
network: NetworkId,
session: Session,
key: Public,
@@ -702,6 +639,154 @@ pub mod pallet {
PendingDeallocations::<T>::take((network, session, key))
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(0)] // TODO
pub fn set_keys(
origin: OriginFor<T>,
network: NetworkId,
key_pair: KeyPair,
signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
// signature isn't checked as this is an unsigned transaction, and validate_unsigned
// (called by pre_dispatch) checks it
let _ = signature;
let session = Session(pallet_session::Pallet::<T>::current_index());
let set = ValidatorSet { session, network };
Keys::<T>::set(set, Some(key_pair.clone()));
Self::deposit_event(Event::KeyGen { set, key_pair });
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(0)] // TODO
pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let validator = ensure_signed(origin)?;
Coins::<T>::transfer_internal(
validator,
Self::account(),
Balance { coin: Coin::Serai, amount },
)?;
Self::increase_allocation(network, validator, amount)
}
#[pallet::call_index(2)]
#[pallet::weight(0)] // TODO
pub fn deallocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let account = ensure_signed(origin)?;
let can_immediately_deallocate = Self::decrease_allocation(network, account, amount)?;
if can_immediately_deallocate {
Coins::<T>::transfer_internal(
Self::account(),
account,
Balance { coin: Coin::Serai, amount },
)?;
}
Ok(())
}
#[pallet::call_index(3)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn claim_deallocation(
origin: OriginFor<T>,
network: NetworkId,
session: Session,
) -> DispatchResult {
let account = ensure_signed(origin)?;
let Some(amount) = Self::take_deallocatable_amount(network, session, account) else {
Err(Error::<T>::NonExistentDeallocation)?
};
Coins::<T>::transfer_internal(
Self::account(),
account,
Balance { coin: Coin::Serai, amount },
)?;
Self::deposit_event(Event::DeallocationClaimed {
validator: account,
network,
session,
});
Ok(())
}
}
#[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
fn validate_unsigned(_: TransactionSource, call: &Self::Call) -> TransactionValidity {
// Match to be exhaustive
let (network, key_pair, signature) = match call {
Call::set_keys { network, ref key_pair, ref signature } => (network, key_pair, signature),
Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => {
Err(InvalidTransaction::Call)?
}
Call::__Ignore(_, _) => unreachable!(),
};
let session = Session(pallet_session::Pallet::<T>::current_index());
let set = ValidatorSet { session, network: *network };
match Self::verify_signature(set, key_pair, signature) {
Err(Error::AlreadyGeneratedKeys) => Err(InvalidTransaction::Stale)?,
Err(Error::NonExistentValidatorSet) |
Err(Error::InsufficientAllocation) |
Err(Error::NotEnoughAllocated) |
Err(Error::AllocationWouldRemoveFaultTolerance) |
Err(Error::DeallocationWouldRemoveParticipant) |
Err(Error::DeallocationWouldRemoveFaultTolerance) |
Err(Error::NonExistentDeallocation) |
Err(Error::NonExistentValidator) |
Err(Error::BadSignature) => Err(InvalidTransaction::BadProof)?,
Err(Error::__Ignore(_, _)) => unreachable!(),
Ok(()) => (),
}
ValidTransaction::with_tag_prefix("validator-sets")
.and_provides(set)
// Set a 10 block longevity, though this should be included in the next block
.longevity(10)
.propagate(true)
.build()
}
// Explicitly provide a pre-dispatch which calls validate_unsigned
fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()).map_err(Into::into)
}
}
// Call order is end_session(i - 1) -> start_session(i) -> new_session(i + 1)
// new_session(i + 1) is called immediately after start_session(i)
// then we wait until the session ends then get a call to end_session(i) and so on.
impl<T: Config> pallet_session::SessionManager<T::ValidatorId> for Pallet<T> {
fn new_session(_new_index: u32) -> Option<Vec<T::ValidatorId>> {
Self::new_session();
// TODO: Where do we return their stake?
Some(Self::participants(NetworkId::Serai).into())
}
fn new_session_genesis(_: u32) -> Option<Vec<T::ValidatorId>> {
// TODO: Because we don't call new_session here, we don't emit NewSet { Serai, session: 1 }
Some(Self::participants(NetworkId::Serai).into())
}
fn end_session(end_index: u32) {
Self::retire_set(ValidatorSet { network: NetworkId::Serai, session: Session(end_index) })
}
fn start_session(_start_index: u32) {}
}
}
pub use pallet::*;