From cbf998ff305294d3b4a5c7d622f3f80035009535 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 20 Sep 2025 04:16:01 -0400 Subject: [PATCH] Restore `report_slashes` This does not yet handle the `SlashReport`. It solely handles the routing for it. --- substrate/abi/src/validator_sets.rs | 8 +++ substrate/validator-sets/src/keys.rs | 7 +++ substrate/validator-sets/src/lib.rs | 77 +++++------------------- substrate/validator-sets/src/sessions.rs | 75 +++++++++++++++++++++-- 4 files changed, 99 insertions(+), 68 deletions(-) diff --git a/substrate/abi/src/validator_sets.rs b/substrate/abi/src/validator_sets.rs index 673aa60b..a8a22c35 100644 --- a/substrate/abi/src/validator_sets.rs +++ b/substrate/abi/src/validator_sets.rs @@ -109,6 +109,14 @@ pub enum Event { /// The set which accepted responsibility from the prior set. set: ValidatorSet, }, + /// A slash report has been entered for this validator set. + /// + /// This may be due to a slash report being published or a default being used due to one not + /// being received within time. + SlashReport { + /// The set whose slash report has been entered. + set: ExternalValidatorSet, + }, /// A validator set their keys on an embedded elliptic curve for a network. SetEmbeddedEllipticCurveKeys { /// The validator which set their keys. diff --git a/substrate/validator-sets/src/keys.rs b/substrate/validator-sets/src/keys.rs index 4eb7a53b..d5f22dc6 100644 --- a/substrate/validator-sets/src/keys.rs +++ b/substrate/validator-sets/src/keys.rs @@ -30,6 +30,9 @@ pub(crate) trait Keys { /// Clear a historic set of keys. fn clear_keys(set: ExternalValidatorSet); + + /// The oraclization key for a validator set. + fn oraclization_key(set: ExternalValidatorSet) -> Option; } impl Keys for S { @@ -46,4 +49,8 @@ impl Keys for S { S::OraclizationKeys::remove(set); S::ExternalKeys::remove(set); } + + fn oraclization_key(set: ExternalValidatorSet) -> Option { + S::OraclizationKeys::get(set) + } } diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index b5ffad64..ef37b163 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -21,6 +21,7 @@ use keys::{KeysStorage, Keys as _}; #[frame_support::pallet] mod pallet { use sp_core::sr25519::Public; + use sp_application_crypto::RuntimePublic; use frame_system::pallet_prelude::*; use frame_support::{pallet_prelude::*, traits::OneSessionHandler}; @@ -35,7 +36,9 @@ mod pallet { network_id::*, coin::*, balance::*, - validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares as KeySharesStruct}, + validator_sets::{ + Session, ExternalValidatorSet, ValidatorSet, KeyShares as KeySharesStruct, SlashReport, + }, address::SeraiAddress, }, economic_security::EconomicSecurity, @@ -126,6 +129,8 @@ mod pallet { #[pallet::storage] type DelayedDeallocations = StorageDoubleMap<_, Blake2_128Concat, Public, Identity, Session, Amount, OptionQuery>; + #[pallet::storage] + type PendingSlashReport = StorageMap<_, Identity, ExternalNetworkId, (), OptionQuery>; impl SessionsStorage for Abstractions { type Config = T; @@ -138,6 +143,7 @@ mod pallet { type SelectedValidators = SelectedValidators; type TotalAllocatedStake = TotalAllocatedStake; type DelayedDeallocations = DelayedDeallocations; + type PendingSlashReport = PendingSlashReport; } // Satisfy the `Keys` abstractions @@ -153,13 +159,6 @@ mod pallet { type ExternalKeys = ExternalKeys; } - /* TODO - /// The key for validator sets which can (and still need to) publish their slash reports. - #[pallet::storage] - pub type PendingSlashReport = - StorageMap<_, Identity, ExternalNetworkId, Public, OptionQuery>; - */ - #[pallet::error] pub enum Error { /// The provided embedded elliptic curve keys were invalid. @@ -324,34 +323,6 @@ mod pallet { Sessions::::decrease_allocation(network, account, amount) } - // TODO: This is called retire_set, yet just starts retiring the set - // Update the nomenclature within this function - pub fn retire_set(set: ValidatorSet) { - // Serai doesn't set keys and network slashes are handled by BABE/GRANDPA - if let NetworkId::External(n) = set.network { - // If the prior prior set didn't report, emit they're retired now - if PendingSlashReport::::get(n).is_some() { - Self::deposit_event(Event::SetRetired { - set: ValidatorSet { network: set.network, session: Session(set.session.0 - 1) }, - }); - } - - // This overwrites the prior value as the prior to-report set's stake presumably just - // unlocked, making their report unenforceable - let keys = - Keys::::take(ExternalValidatorSet { network: n, session: set.session }).unwrap(); - PendingSlashReport::::set(n, Some(keys.0)); - } else { - // emit the event for serai network - Self::deposit_event(Event::SetRetired { set }); - } - - // We're retiring this set because the set after it accepted the handover - Self::deposit_event(Event::AcceptedHandover { - set: ValidatorSet { network: set.network, session: Session(set.session.0 + 1) }, - }); - } - /// Returns the required stake in terms SRI for a given `Balance`. pub fn required_stake(balance: &ExternalBalance) -> SubstrateAmount { use dex_pallet::HigherPrecisionBalance; @@ -520,7 +491,6 @@ mod pallet { Ok(()) } - /* TODO #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn report_slashes( @@ -531,24 +501,13 @@ mod pallet { ) -> DispatchResult { ensure_none(origin)?; - // signature isn't checked as this is an unsigned transaction, and validate_unsigned - // (called by pre_dispatch) checks it + // `signature` is checked within `ValidateUnsigned` let _ = signature; - // TODO: Handle slashes - let _ = slashes; - - // Emit set retireed - Pallet::::deposit_event(Event::SetRetired { - set: ValidatorSet { - network: network.into(), - session: Session(Self::session(NetworkId::from(network)).unwrap().0 - 1), - }, - }); + Abstractions::::handle_slash_report(network, slashes); Ok(()) } - */ #[pallet::call_index(2)] #[pallet::weight((0, DispatchClass::Normal))] // TODO @@ -693,7 +652,6 @@ mod pallet { } // Verify the signature with the MuSig key of the signers - use sp_application_crypto::RuntimePublic; if !set.musig_key(&signers).verify(&set.set_keys_message(key_pair), &signature.0.into()) { Err(InvalidTransaction::BadProof)?; } @@ -704,30 +662,23 @@ mod pallet { .propagate(true) .build() } - /* TODO Call::report_slashes { network, ref slashes, ref signature } => { let network = *network; - let Some(key) = PendingSlashReport::::take(network) else { - // Assumed already published + + let Some(key) = Abstractions::::waiting_for_slash_report(network) else { Err(InvalidTransaction::Stale)? }; - // There must have been a previous session is PendingSlashReport is populated - let set = ExternalValidatorSet { - network, - session: Session(Self::session(NetworkId::from(network)).unwrap().0 - 1), - }; - if !key.verify(&slashes.report_slashes_message(), signature) { + if !key.verify(&slashes.report_slashes_message(), &signature.0.into()) { Err(InvalidTransaction::BadProof)?; } ValidTransaction::with_tag_prefix("ValidatorSets") - .and_provides((1, set)) - .longevity(MAX_KEY_SHARES_PER_SET_U32.into()) + .and_provides((1, key)) + .longevity(KeySharesStruct::MAX_PER_SET_U32.into()) .propagate(true) .build() } - */ Call::set_embedded_elliptic_curve_keys { .. } | Call::allocate { .. } | Call::deallocate { .. } | diff --git a/substrate/validator-sets/src/sessions.rs b/substrate/validator-sets/src/sessions.rs index 0459c382..fd139668 100644 --- a/substrate/validator-sets/src/sessions.rs +++ b/substrate/validator-sets/src/sessions.rs @@ -3,9 +3,11 @@ use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec}; use serai_abi::{ primitives::{ - network_id::NetworkId, + network_id::{ExternalNetworkId, NetworkId}, balance::Amount, - validator_sets::{KeyShares as KeySharesStruct, Session, ExternalValidatorSet, ValidatorSet}, + validator_sets::{ + KeyShares as KeySharesStruct, Session, ExternalValidatorSet, ValidatorSet, SlashReport, + }, }, validator_sets::{DeallocationTimeline, Event}, }; @@ -76,6 +78,11 @@ pub(crate) trait SessionsStorage: EmbeddedEllipticCurveKeys + Allocations + Keys /// /// This is opaque and to be exclusively read/write by `Sessions`. type DelayedDeallocations: StorageDoubleMap>; + + /// Networks for which we're awaiting slash reports. + /// + /// This is opaque and to be exclusively read/write by `Sessions`. + type PendingSlashReport: StorageMap>; } /// The storage key for the SelectedValidators map. @@ -196,6 +203,11 @@ pub(crate) trait Sessions { session: Session, ) -> Result; + /// Handle a slash report. + /// + /// This will panic if this slash report isn't pending. + fn handle_slash_report(network: ExternalNetworkId, slashes: SlashReport); + /// The currently active session for a network. fn current_session(network: NetworkId) -> Option; @@ -235,6 +247,11 @@ pub(crate) trait Sessions { .map(|(validator, _key_shares)| (validator, validator)) .collect() } + + /// If this network is awaiting a slash report. + /// + /// If so, this returns the key which should publish the slash report. + fn waiting_for_slash_report(network: ExternalNetworkId) -> Option; } impl Sessions for Storage { @@ -322,7 +339,7 @@ impl Sessions for Storage { } fn accept_handover(network: NetworkId) { - let current = { + let (prior, current) = { let current = Storage::CurrentSession::get(network); let latest_decided = Storage::LatestDecidedSession::get(network) .expect("accepting handover but never decided a session"); @@ -333,8 +350,8 @@ impl Sessions for Storage { ); // Set the CurrentSession variable Storage::CurrentSession::set(network, Some(latest_decided)); - // Return `latest_decided` as `current` as it is now current - latest_decided + // Return `latest_decided` as `current` as it is now current, and `current` as `prior` + (current, latest_decided) }; let mut total_allocated_stake = Amount(0); @@ -348,6 +365,23 @@ impl Sessions for Storage { // Update the total allocated stake variable to the current session Storage::TotalAllocatedStake::set(network, Some(total_allocated_stake)); + match network { + NetworkId::Serai => {} + NetworkId::External(network) => { + // If this network never submitted its slash report, treat it as submitting `vec![]` + if Storage::PendingSlashReport::take(network).is_some() { + Core::::emit_event(Event::SlashReport { + set: ExternalValidatorSet { + network, + session: prior.expect("pending slash report yet no prior session"), + }, + }); + } + // Mark this network as pending a slash report + Storage::PendingSlashReport::insert(network, ()); + } + } + // Clean-up the historic set's storage, if one exists if let Some(historic_session) = current.0.checked_sub(2).map(Session) { let historic_set = ValidatorSet { network, session: historic_session }; @@ -537,6 +571,22 @@ impl Sessions for Storage { Ok(DeallocationTimeline::Immediate) } + fn handle_slash_report(network: ExternalNetworkId, _slashes: SlashReport) { + Storage::PendingSlashReport::take(network) + .expect("handling a slash report which wasn't pending"); + + let current_session = + Self::current_session(network.into()).expect("handling slash report yet no current session"); + let prior_session = Session( + current_session.0.checked_sub(1).expect("handling slash report yet no prior session"), + ); + Core::::emit_event(Event::SlashReport { + set: ExternalValidatorSet { network, session: prior_session }, + }); + + // TODO: Actually handle `_slashes` + } + fn claim_delayed_deallocation( validator: Public, network: NetworkId, @@ -581,4 +631,19 @@ impl Sessions for Storage { fn selected_validators(set: ValidatorSet) -> impl Iterator { selected_validators::(set) } + + fn waiting_for_slash_report(network: ExternalNetworkId) -> Option { + if !Storage::PendingSlashReport::contains_key(network) { + None?; + } + let current_session = Self::current_session(network.into()) + .expect("network awaiting slash report yet no current session"); + let prior_session = Session( + current_session.0.checked_sub(1).expect("network awaiting slash report yet no prior session"), + ); + Some( + Storage::oraclization_key(ExternalValidatorSet { network, session: prior_session }) + .expect("no oraclization key for set waiting for a slash report"), + ) + } }