From ffae6753ece9e583a8b6fdd2fc222e685f4925b4 Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Sat, 20 Sep 2025 02:53:14 -0400 Subject: [PATCH] Restore the `set_keys` call --- Cargo.lock | 1 + .../primitives/src/validator_sets/mod.rs | 24 ++-- substrate/validator-sets/Cargo.toml | 3 + substrate/validator-sets/src/allocations.rs | 9 +- .../src/embedded_elliptic_curve_keys.rs | 4 +- substrate/validator-sets/src/keys.rs | 49 ++++++++ substrate/validator-sets/src/lib.rs | 105 ++++++++++-------- substrate/validator-sets/src/sessions.rs | 19 +++- 8 files changed, 153 insertions(+), 61 deletions(-) create mode 100644 substrate/validator-sets/src/keys.rs diff --git a/Cargo.lock b/Cargo.lock index 29ca6cfa..bad575e2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9748,6 +9748,7 @@ dependencies = [ "serai-abi", "serai-coins-pallet", "sp-api", + "sp-application-crypto", "sp-core", "sp-io", "zeroize", diff --git a/substrate/primitives/src/validator_sets/mod.rs b/substrate/primitives/src/validator_sets/mod.rs index d81e7c2a..b8649829 100644 --- a/substrate/primitives/src/validator_sets/mod.rs +++ b/substrate/primitives/src/validator_sets/mod.rs @@ -1,4 +1,4 @@ -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; use zeroize::Zeroize; use borsh::{BorshSerialize, BorshDeserialize}; @@ -6,8 +6,10 @@ use borsh::{BorshSerialize, BorshDeserialize}; use ciphersuite::{group::GroupEncoding, GroupIo}; use dalek_ff_group::Ristretto; +use sp_core::sr25519::Public; + use crate::{ - crypto::{Public, KeyPair}, + crypto::KeyPair, network_id::{ExternalNetworkId, NetworkId}, balance::Amount, }; @@ -86,15 +88,17 @@ impl ExternalValidatorSet { /// The MuSig public key for a validator set. /// - /// This function panics on invalid input, per the definition of `dkg::musig::musig_key`. - pub fn musig_key(&self, set_keys: &[Public]) -> Public { - let mut keys = Vec::new(); - for key in set_keys { - keys.push( - ::read_G::<&[u8]>(&mut key.0.as_ref()).expect("invalid participant"), + /// This function panics on invalid points as keys and on invalid input, per the definition of + /// `dkg::musig::musig_key`. + pub fn musig_key(&self, keys: &[Public]) -> Public { + let mut decompressed_keys = vec![]; + for key in keys { + decompressed_keys.push( + ::read_G::<&[u8]>(&mut key.0.as_slice()) + .expect("invalid participant"), ); } - Public(dkg::musig_key::(self.musig_context(), &keys).unwrap().to_bytes()) + dkg::musig_key::(self.musig_context(), &decompressed_keys).unwrap().to_bytes().into() } /// The message for the `set_keys` signature. @@ -150,7 +154,7 @@ impl KeyShares { /// Reduction occurs by reducing each validator in a reverse round-robin. This means the /// validators with the least key shares are evicted first. #[must_use] - pub fn amortize_excess(validators: &mut [(sp_core::sr25519::Public, KeyShares)]) -> usize { + pub fn amortize_excess(validators: &mut [(Public, KeyShares)]) -> usize { let total_key_shares = validators.iter().map(|(_key, shares)| shares.0).sum::(); let mut actual_len = validators.len(); let mut offset = 1; diff --git a/substrate/validator-sets/Cargo.toml b/substrate/validator-sets/Cargo.toml index 0c344597..fc1b44c2 100644 --- a/substrate/validator-sets/Cargo.toml +++ b/substrate/validator-sets/Cargo.toml @@ -21,6 +21,7 @@ bitvec = { version = "1", default-features = false, features = ["alloc", "serde" scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive", "bit-vec"] } sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "16336c737dbe833e9d138a256af99698aba637c7", default-features = false } +sp-application-crypto = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "16336c737dbe833e9d138a256af99698aba637c7", default-features = false } sp-io = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "16336c737dbe833e9d138a256af99698aba637c7", default-features = false } sp-api = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "16336c737dbe833e9d138a256af99698aba637c7", default-features = false } @@ -39,6 +40,7 @@ serai-coins-pallet = { path = "../coins", default-features = false } zeroize = "^1.5" rand_core = "0.6" + borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } [features] @@ -48,6 +50,7 @@ std = [ "scale/std", "sp-core/std", + "sp-application-crypto/std", "sp-io/std", "sp-api/std", diff --git a/substrate/validator-sets/src/allocations.rs b/substrate/validator-sets/src/allocations.rs index 08332f62..3c5e2e2c 100644 --- a/substrate/validator-sets/src/allocations.rs +++ b/substrate/validator-sets/src/allocations.rs @@ -14,11 +14,16 @@ pub(crate) type SortedAllocationsKey = (NetworkId, [u8; 8], [u8; 16], Public); /// This storage is expected to be owned by the `Allocations` interface and not directly read/write /// to. pub(crate) trait AllocationsStorage { - /// An opaque map storing allocations. + /// An map storing allocations. + /// + /// This is opaque and to be exclusively read/write by `Allocations`. type Allocations: StorageMap>; - /// An opaque map storing allocations in a sorted manner. + + /// An map storing allocations in a sorted manner. /// /// This MUST be instantiated with a map using `Identity` for its hasher. + /// + /// This is opaque and to be exclusively read/write by `Allocations`. /* This is premised on the underlying trie iterating from keys with low-bytes to keys with high-bytes. diff --git a/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs b/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs index e9ea748a..b00cab26 100644 --- a/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs +++ b/substrate/validator-sets/src/embedded_elliptic_curve_keys.rs @@ -5,7 +5,9 @@ use serai_abi::primitives::{crypto::SignedEmbeddedEllipticCurveKeys, network_id: use frame_support::storage::StorageDoubleMap; pub(crate) trait EmbeddedEllipticCurveKeysStorage { - /// An opaque map storing keys on an embedded elliptic curve. + /// An map storing keys on an embedded elliptic curve. + /// + /// This is opaque and to be exclusively read/write by `EmbeddedEllipticCurveKeys`. type EmbeddedEllipticCurveKeys: StorageDoubleMap< ExternalNetworkId, Public, diff --git a/substrate/validator-sets/src/keys.rs b/substrate/validator-sets/src/keys.rs new file mode 100644 index 00000000..4eb7a53b --- /dev/null +++ b/substrate/validator-sets/src/keys.rs @@ -0,0 +1,49 @@ +use sp_core::sr25519::Public; + +use serai_abi::primitives::{ + crypto::{ExternalKey, KeyPair}, + validator_sets::ExternalValidatorSet, +}; + +use frame_support::storage::StorageMap; + +pub(crate) trait KeysStorage { + /// An map storing keys validator sets use for oraclization. + /// + /// This is opaque and to be exclusively read/write by `Keys`. + type OraclizationKeys: StorageMap>; + + /// An map storing keys validator sets use for interacting with external networks. + /// + /// This is opaque and to be exclusively read/write by `Keys`. + type ExternalKeys: StorageMap>; +} + +/// An interface for managing validators' embedded elliptic curve keys. +pub(crate) trait Keys { + /// If a validator set has yet to set keys. + #[must_use] + fn needs_to_set_keys(set: ExternalValidatorSet) -> bool; + + /// Set the pair of keys for an external network. + fn set_keys(set: ExternalValidatorSet, key_pair: KeyPair); + + /// Clear a historic set of keys. + fn clear_keys(set: ExternalValidatorSet); +} + +impl Keys for S { + fn needs_to_set_keys(set: ExternalValidatorSet) -> bool { + S::OraclizationKeys::contains_key(set) + } + + fn set_keys(set: ExternalValidatorSet, key_pair: KeyPair) { + S::OraclizationKeys::insert(set, Public::from(key_pair.0 .0)); + S::ExternalKeys::insert(set, key_pair.1); + } + + fn clear_keys(set: ExternalValidatorSet) { + S::OraclizationKeys::remove(set); + S::ExternalKeys::remove(set); + } +} diff --git a/substrate/validator-sets/src/lib.rs b/substrate/validator-sets/src/lib.rs index 2e144109..378b7571 100644 --- a/substrate/validator-sets/src/lib.rs +++ b/substrate/validator-sets/src/lib.rs @@ -14,6 +14,9 @@ use allocations::*; mod sessions; use sessions::{*, GenesisValidators as GenesisValidatorsContainer}; +mod keys; +use keys::{KeysStorage, Keys as _}; + #[expect(clippy::cast_possible_truncation)] #[frame_support::pallet] mod pallet { @@ -28,11 +31,11 @@ mod pallet { use serai_abi::{ primitives::{ - crypto::SignedEmbeddedEllipticCurveKeys, + crypto::{SignedEmbeddedEllipticCurveKeys, ExternalKey, KeyPair, Signature}, network_id::*, coin::*, balance::*, - validator_sets::{Session, ValidatorSet, KeyShares as KeySharesStruct}, + validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares as KeySharesStruct}, address::SeraiAddress, }, economic_security::EconomicSecurity, @@ -132,13 +135,20 @@ mod pallet { type DelayedDeallocations = DelayedDeallocations; } - /* TODO - /// The generated key pair for a given validator set instance. + // Satisfy the `Keys` abstractions #[pallet::storage] - #[pallet::getter(fn keys)] - pub type Keys = - StorageMap<_, Twox64Concat, ExternalValidatorSet, KeyPair, OptionQuery>; + type OraclizationKeys = + StorageMap<_, Identity, ExternalValidatorSet, Public, OptionQuery>; + #[pallet::storage] + type ExternalKeys = + StorageMap<_, Identity, ExternalValidatorSet, ExternalKey, OptionQuery>; + impl KeysStorage for Abstractions { + type OraclizationKeys = OraclizationKeys; + type ExternalKeys = ExternalKeys; + } + + /* TODO /// The key for validator sets which can (and still need to) publish their slash reports. #[pallet::storage] pub type PendingSlashReport = @@ -196,6 +206,15 @@ mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { + GenesisValidators::::set(Some( + self + .participants + .iter() + .map(|(participant, _keys)| *participant) + .collect::>() + .try_into() + .expect("amount of genesis validators exceeded the maximum allowed per set"), + )); for (participant, keys) in &self.participants { for (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) { assert_eq!(network, keys.network()); @@ -504,7 +523,6 @@ mod pallet { #[pallet::call] impl Pallet { - /* TODO #[pallet::call_index(0)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn set_keys( @@ -516,28 +534,24 @@ 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_participants`, `signature` are checked within `ValidateUnsigned` let _ = signature_participants; let _ = signature; - let session = Self::session(NetworkId::from(network)).unwrap(); + let session = Self::current_session(NetworkId::from(network)) + .expect("validated `set_keys` for a non-existent session"); let set = ExternalValidatorSet { network, session }; + Abstractions::::set_keys(set, key_pair); - Keys::::set(set, Some(key_pair.clone())); - - // If this is the first ever set for this network, set TotalAllocatedStake now - // We generally set TotalAllocatedStake when the prior set retires, and the new set is fully - // active and liable. Since this is the first set, there is no prior set to wait to retire + // If this is the first session of an external network, mark them current, not solely decided if session == Session(0) { - Self::set_total_allocated_stake(NetworkId::from(network)); + Abstractions::::accept_handover(network.into()); } - Self::deposit_event(Event::KeyGen { set, key_pair }); - Ok(()) } + /* TODO #[pallet::call_index(1)] #[pallet::weight((0, DispatchClass::Operational))] // TODO pub fn report_slashes( @@ -632,7 +646,6 @@ mod pallet { } } - /* TODO #[pallet::validate_unsigned] impl ValidateUnsigned for Pallet { type Call = Call; @@ -643,58 +656,57 @@ mod pallet { Call::set_keys { network, ref key_pair, ref signature_participants, ref signature } => { let network = *network; - // Confirm this set has a session - let Some(current_session) = Self::session(NetworkId::from(network)) else { - Err(InvalidTransaction::Custom(1))? + // Confirm this network has a session decided + let Some(latest_decided_session) = Self::latest_decided_session(network.into()) else { + Err(InvalidTransaction::BadSigner)? }; - let set = ExternalValidatorSet { network, session: current_session }; + let set = ExternalValidatorSet { network, session: latest_decided_session }; - // Confirm it has yet to set keys - if Keys::::get(set).is_some() { + // Confirm this set has yet to set keys + if Abstractions::::needs_to_set_keys(set) { Err(InvalidTransaction::Stale)?; } - // This is a needed precondition as this uses storage variables for the latest decided - // session on this assumption - assert_eq!(Pallet::::latest_decided_session(network.into()), Some(current_session)); - - let participants = Participants::::get(NetworkId::from(network)) - .expect("session existed without participants"); + let participants = Abstractions::::selected_validators(set.into()).collect::>(); + assert!( + !participants.is_empty(), + "set which was decided had no selected participants stored" + ); // Check the bitvec is of the proper length if participants.len() != signature_participants.len() { - Err(InvalidTransaction::Custom(2))?; + Err(InvalidTransaction::BadProof)?; } + // Find the participating, total key shares let mut all_key_shares = 0; let mut signers = vec![]; let mut signing_key_shares = 0; - for (participant, in_use) in participants.into_iter().zip(signature_participants) { - let participant = participant.0; - let shares = InSet::::get(NetworkId::from(network), participant) - .expect("participant from Participants wasn't InSet"); - all_key_shares += shares; + for ((participant, shares), in_use) in + participants.into_iter().zip(signature_participants) + { + all_key_shares += shares.0; if !in_use { continue; } signers.push(participant); - signing_key_shares += shares; + signing_key_shares += shares.0; } + // Check enough validators participated { let f = all_key_shares - signing_key_shares; if signing_key_shares < ((2 * f) + 1) { - Err(InvalidTransaction::Custom(3))?; + Err(InvalidTransaction::BadSigner)?; } } // Verify the signature with the MuSig key of the signers - // We theoretically don't need set_keys_message to bind to removed_participants, as the - // key we're signing with effectively already does so, yet there's no reason not to - if !musig_key(set.into(), &signers).verify(&set_keys_message(&set, key_pair), signature) { + use sp_application_crypto::RuntimePublic; + if !set.musig_key(&signers).verify(&set.set_keys_message(key_pair), &signature.0.into()) { Err(InvalidTransaction::BadProof)?; } @@ -704,6 +716,7 @@ mod pallet { .propagate(true) .build() } + /* TODO Call::report_slashes { network, ref slashes, ref signature } => { let network = *network; let Some(key) = PendingSlashReport::::take(network) else { @@ -726,7 +739,8 @@ mod pallet { .propagate(true) .build() } - Call::set_embedded_elliptic_curve_key { .. } | + */ + Call::set_embedded_elliptic_curve_keys { .. } | Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => Err(InvalidTransaction::Call)?, @@ -734,12 +748,13 @@ mod pallet { } } - // Explicitly provide a pre-dispatch which calls validate_unsigned + // Explicitly provide a pre-dispatch which calls `validate_unsigned` fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()) } } + /* TODO impl AllowMint for Pallet { fn is_allowed(balance: &ExternalBalance) -> bool { // get the required stake diff --git a/substrate/validator-sets/src/sessions.rs b/substrate/validator-sets/src/sessions.rs index b54ddef0..66876f30 100644 --- a/substrate/validator-sets/src/sessions.rs +++ b/substrate/validator-sets/src/sessions.rs @@ -4,12 +4,14 @@ use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec}; use serai_abi::primitives::{ network_id::NetworkId, balance::Amount, - validator_sets::{KeyShares as KeySharesStruct, Session, ValidatorSet}, + validator_sets::{KeyShares as KeySharesStruct, Session, ExternalValidatorSet, ValidatorSet}, }; use frame_support::storage::{StorageValue, StorageMap, StorageDoubleMap, StoragePrefixedMap}; -use crate::{embedded_elliptic_curve_keys::EmbeddedEllipticCurveKeys, allocations::Allocations}; +use crate::{ + embedded_elliptic_curve_keys::EmbeddedEllipticCurveKeys, allocations::Allocations, keys::Keys, +}; /// The list of genesis validators. pub(crate) type GenesisValidators = @@ -18,7 +20,7 @@ pub(crate) type GenesisValidators = /// The key for the SelectedValidators map. pub(crate) type SelectedValidatorsKey = (ValidatorSet, [u8; 16], Public); -pub(crate) trait SessionsStorage: EmbeddedEllipticCurveKeys + Allocations { +pub(crate) trait SessionsStorage: EmbeddedEllipticCurveKeys + Allocations + Keys { /// The genesis validators /// /// The usage of is shared with the rest of the pallet. `Sessions` only reads it. @@ -288,6 +290,11 @@ impl Sessions for Storage { selected_validators.append(&mut genesis_validators); } + // If we failed to select any validators, return `false` now + if total_key_shares == 0 { + return false; + } + let latest_decided_session = Storage::LatestDecidedSession::mutate(network, |session| { let next_session = session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0)); *session = Some(next_session); @@ -341,6 +348,12 @@ impl Sessions for Storage { let historic_set = ValidatorSet { network, session: historic_session }; Storage::KeyShares::remove(historic_set); clear_selected_validators::(historic_set); + match historic_set.network { + NetworkId::Serai => {} + NetworkId::External(network) => { + Storage::clear_keys(ExternalValidatorSet { network, session: historic_session }) + } + } } }