Restore the set_keys call

This commit is contained in:
Luke Parker
2025-09-20 02:53:14 -04:00
parent a04215bc13
commit ffae6753ec
8 changed files with 153 additions and 61 deletions

1
Cargo.lock generated
View File

@@ -9748,6 +9748,7 @@ dependencies = [
"serai-abi", "serai-abi",
"serai-coins-pallet", "serai-coins-pallet",
"sp-api", "sp-api",
"sp-application-crypto",
"sp-core", "sp-core",
"sp-io", "sp-io",
"zeroize", "zeroize",

View File

@@ -1,4 +1,4 @@
use alloc::vec::Vec; use alloc::{vec, vec::Vec};
use zeroize::Zeroize; use zeroize::Zeroize;
use borsh::{BorshSerialize, BorshDeserialize}; use borsh::{BorshSerialize, BorshDeserialize};
@@ -6,8 +6,10 @@ use borsh::{BorshSerialize, BorshDeserialize};
use ciphersuite::{group::GroupEncoding, GroupIo}; use ciphersuite::{group::GroupEncoding, GroupIo};
use dalek_ff_group::Ristretto; use dalek_ff_group::Ristretto;
use sp_core::sr25519::Public;
use crate::{ use crate::{
crypto::{Public, KeyPair}, crypto::KeyPair,
network_id::{ExternalNetworkId, NetworkId}, network_id::{ExternalNetworkId, NetworkId},
balance::Amount, balance::Amount,
}; };
@@ -86,15 +88,17 @@ impl ExternalValidatorSet {
/// The MuSig public key for a validator set. /// The MuSig public key for a validator set.
/// ///
/// This function panics on invalid input, per the definition of `dkg::musig::musig_key`. /// This function panics on invalid points as keys and on invalid input, per the definition of
pub fn musig_key(&self, set_keys: &[Public]) -> Public { /// `dkg::musig::musig_key`.
let mut keys = Vec::new(); pub fn musig_key(&self, keys: &[Public]) -> Public {
for key in set_keys { let mut decompressed_keys = vec![];
keys.push( for key in keys {
<Ristretto as GroupIo>::read_G::<&[u8]>(&mut key.0.as_ref()).expect("invalid participant"), decompressed_keys.push(
<Ristretto as GroupIo>::read_G::<&[u8]>(&mut key.0.as_slice())
.expect("invalid participant"),
); );
} }
Public(dkg::musig_key::<Ristretto>(self.musig_context(), &keys).unwrap().to_bytes()) dkg::musig_key::<Ristretto>(self.musig_context(), &decompressed_keys).unwrap().to_bytes().into()
} }
/// The message for the `set_keys` signature. /// 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 /// Reduction occurs by reducing each validator in a reverse round-robin. This means the
/// validators with the least key shares are evicted first. /// validators with the least key shares are evicted first.
#[must_use] #[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::<u16>(); let total_key_shares = validators.iter().map(|(_key, shares)| shares.0).sum::<u16>();
let mut actual_len = validators.len(); let mut actual_len = validators.len();
let mut offset = 1; let mut offset = 1;

View File

@@ -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"] } 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-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-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 } 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" zeroize = "^1.5"
rand_core = "0.6" rand_core = "0.6"
borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] } borsh = { version = "1", default-features = false, features = ["derive", "de_strict_order"] }
[features] [features]
@@ -48,6 +50,7 @@ std = [
"scale/std", "scale/std",
"sp-core/std", "sp-core/std",
"sp-application-crypto/std",
"sp-io/std", "sp-io/std",
"sp-api/std", "sp-api/std",

View File

@@ -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 /// This storage is expected to be owned by the `Allocations` interface and not directly read/write
/// to. /// to.
pub(crate) trait AllocationsStorage { 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<AllocationsKey, Amount, Query = Option<Amount>>; type Allocations: StorageMap<AllocationsKey, Amount, Query = Option<Amount>>;
/// 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 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 This is premised on the underlying trie iterating from keys with low-bytes to keys with
high-bytes. high-bytes.

View File

@@ -5,7 +5,9 @@ use serai_abi::primitives::{crypto::SignedEmbeddedEllipticCurveKeys, network_id:
use frame_support::storage::StorageDoubleMap; use frame_support::storage::StorageDoubleMap;
pub(crate) trait EmbeddedEllipticCurveKeysStorage { 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< type EmbeddedEllipticCurveKeys: StorageDoubleMap<
ExternalNetworkId, ExternalNetworkId,
Public, Public,

View File

@@ -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<ExternalValidatorSet, Public, Query = Option<Public>>;
/// 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<ExternalValidatorSet, ExternalKey, Query = Option<ExternalKey>>;
}
/// 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<S: KeysStorage> 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);
}
}

View File

@@ -14,6 +14,9 @@ use allocations::*;
mod sessions; mod sessions;
use sessions::{*, GenesisValidators as GenesisValidatorsContainer}; use sessions::{*, GenesisValidators as GenesisValidatorsContainer};
mod keys;
use keys::{KeysStorage, Keys as _};
#[expect(clippy::cast_possible_truncation)] #[expect(clippy::cast_possible_truncation)]
#[frame_support::pallet] #[frame_support::pallet]
mod pallet { mod pallet {
@@ -28,11 +31,11 @@ mod pallet {
use serai_abi::{ use serai_abi::{
primitives::{ primitives::{
crypto::SignedEmbeddedEllipticCurveKeys, crypto::{SignedEmbeddedEllipticCurveKeys, ExternalKey, KeyPair, Signature},
network_id::*, network_id::*,
coin::*, coin::*,
balance::*, balance::*,
validator_sets::{Session, ValidatorSet, KeyShares as KeySharesStruct}, validator_sets::{Session, ExternalValidatorSet, ValidatorSet, KeyShares as KeySharesStruct},
address::SeraiAddress, address::SeraiAddress,
}, },
economic_security::EconomicSecurity, economic_security::EconomicSecurity,
@@ -132,13 +135,20 @@ mod pallet {
type DelayedDeallocations = DelayedDeallocations<T>; type DelayedDeallocations = DelayedDeallocations<T>;
} }
/* TODO // Satisfy the `Keys` abstractions
/// The generated key pair for a given validator set instance.
#[pallet::storage] #[pallet::storage]
#[pallet::getter(fn keys)] type OraclizationKeys<T: Config> =
pub type Keys<T: Config> = StorageMap<_, Identity, ExternalValidatorSet, Public, OptionQuery>;
StorageMap<_, Twox64Concat, ExternalValidatorSet, KeyPair, OptionQuery>; #[pallet::storage]
type ExternalKeys<T: Config> =
StorageMap<_, Identity, ExternalValidatorSet, ExternalKey, OptionQuery>;
impl<T: Config> KeysStorage for Abstractions<T> {
type OraclizationKeys = OraclizationKeys<T>;
type ExternalKeys = ExternalKeys<T>;
}
/* TODO
/// The key for validator sets which can (and still need to) publish their slash reports. /// The key for validator sets which can (and still need to) publish their slash reports.
#[pallet::storage] #[pallet::storage]
pub type PendingSlashReport<T: Config> = pub type PendingSlashReport<T: Config> =
@@ -196,6 +206,15 @@ 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) {
GenesisValidators::<T>::set(Some(
self
.participants
.iter()
.map(|(participant, _keys)| *participant)
.collect::<Vec<_>>()
.try_into()
.expect("amount of genesis validators exceeded the maximum allowed per set"),
));
for (participant, keys) in &self.participants { for (participant, keys) in &self.participants {
for (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) { for (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) {
assert_eq!(network, keys.network()); assert_eq!(network, keys.network());
@@ -504,7 +523,6 @@ mod pallet {
#[pallet::call] #[pallet::call]
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
/* TODO
#[pallet::call_index(0)] #[pallet::call_index(0)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO #[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn set_keys( pub fn set_keys(
@@ -516,28 +534,24 @@ mod pallet {
) -> DispatchResult { ) -> DispatchResult {
ensure_none(origin)?; ensure_none(origin)?;
// signature isn't checked as this is an unsigned transaction, and validate_unsigned // `signature_participants`, `signature` are checked within `ValidateUnsigned`
// (called by pre_dispatch) checks it
let _ = signature_participants; let _ = signature_participants;
let _ = signature; 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 }; let set = ExternalValidatorSet { network, session };
Abstractions::<T>::set_keys(set, key_pair);
Keys::<T>::set(set, Some(key_pair.clone())); // If this is the first session of an external network, mark them current, not solely decided
// 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 session == Session(0) { if session == Session(0) {
Self::set_total_allocated_stake(NetworkId::from(network)); Abstractions::<T>::accept_handover(network.into());
} }
Self::deposit_event(Event::KeyGen { set, key_pair });
Ok(()) Ok(())
} }
/* TODO
#[pallet::call_index(1)] #[pallet::call_index(1)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO #[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn report_slashes( pub fn report_slashes(
@@ -632,7 +646,6 @@ mod pallet {
} }
} }
/* TODO
#[pallet::validate_unsigned] #[pallet::validate_unsigned]
impl<T: Config> ValidateUnsigned for Pallet<T> { impl<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>; type Call = Call<T>;
@@ -643,58 +656,57 @@ mod pallet {
Call::set_keys { network, ref key_pair, ref signature_participants, ref signature } => { Call::set_keys { network, ref key_pair, ref signature_participants, ref signature } => {
let network = *network; let network = *network;
// Confirm this set has a session // Confirm this network has a session decided
let Some(current_session) = Self::session(NetworkId::from(network)) else { let Some(latest_decided_session) = Self::latest_decided_session(network.into()) else {
Err(InvalidTransaction::Custom(1))? 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 // Confirm this set has yet to set keys
if Keys::<T>::get(set).is_some() { if Abstractions::<T>::needs_to_set_keys(set) {
Err(InvalidTransaction::Stale)?; Err(InvalidTransaction::Stale)?;
} }
// This is a needed precondition as this uses storage variables for the latest decided let participants = Abstractions::<T>::selected_validators(set.into()).collect::<Vec<_>>();
// session on this assumption assert!(
assert_eq!(Pallet::<T>::latest_decided_session(network.into()), Some(current_session)); !participants.is_empty(),
"set which was decided had no selected participants stored"
let participants = Participants::<T>::get(NetworkId::from(network)) );
.expect("session existed without participants");
// Check the bitvec is of the proper length // Check the bitvec is of the proper length
if participants.len() != signature_participants.len() { 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 all_key_shares = 0;
let mut signers = vec![]; let mut signers = vec![];
let mut signing_key_shares = 0; let mut signing_key_shares = 0;
for (participant, in_use) in participants.into_iter().zip(signature_participants) { for ((participant, shares), in_use) in
let participant = participant.0; participants.into_iter().zip(signature_participants)
let shares = InSet::<T>::get(NetworkId::from(network), participant) {
.expect("participant from Participants wasn't InSet"); all_key_shares += shares.0;
all_key_shares += shares;
if !in_use { if !in_use {
continue; continue;
} }
signers.push(participant); signers.push(participant);
signing_key_shares += shares; signing_key_shares += shares.0;
} }
// Check enough validators participated
{ {
let f = all_key_shares - signing_key_shares; let f = all_key_shares - signing_key_shares;
if signing_key_shares < ((2 * f) + 1) { if signing_key_shares < ((2 * f) + 1) {
Err(InvalidTransaction::Custom(3))?; Err(InvalidTransaction::BadSigner)?;
} }
} }
// Verify the signature with the MuSig key of the signers // 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 use sp_application_crypto::RuntimePublic;
// key we're signing with effectively already does so, yet there's no reason not to if !set.musig_key(&signers).verify(&set.set_keys_message(key_pair), &signature.0.into()) {
if !musig_key(set.into(), &signers).verify(&set_keys_message(&set, key_pair), signature) {
Err(InvalidTransaction::BadProof)?; Err(InvalidTransaction::BadProof)?;
} }
@@ -704,6 +716,7 @@ mod pallet {
.propagate(true) .propagate(true)
.build() .build()
} }
/* TODO
Call::report_slashes { network, ref slashes, ref signature } => { Call::report_slashes { network, ref slashes, ref signature } => {
let network = *network; let network = *network;
let Some(key) = PendingSlashReport::<T>::take(network) else { let Some(key) = PendingSlashReport::<T>::take(network) else {
@@ -726,7 +739,8 @@ mod pallet {
.propagate(true) .propagate(true)
.build() .build()
} }
Call::set_embedded_elliptic_curve_key { .. } | */
Call::set_embedded_elliptic_curve_keys { .. } |
Call::allocate { .. } | Call::allocate { .. } |
Call::deallocate { .. } | Call::deallocate { .. } |
Call::claim_deallocation { .. } => Err(InvalidTransaction::Call)?, 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> { fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> {
Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ()) Self::validate_unsigned(TransactionSource::InBlock, call).map(|_| ())
} }
} }
/* TODO
impl<T: Config> AllowMint for Pallet<T> { impl<T: Config> AllowMint for Pallet<T> {
fn is_allowed(balance: &ExternalBalance) -> bool { fn is_allowed(balance: &ExternalBalance) -> bool {
// get the required stake // get the required stake

View File

@@ -4,12 +4,14 @@ use sp_core::{Encode, Decode, ConstU32, sr25519::Public, bounded::BoundedVec};
use serai_abi::primitives::{ use serai_abi::primitives::{
network_id::NetworkId, network_id::NetworkId,
balance::Amount, 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 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. /// The list of genesis validators.
pub(crate) type GenesisValidators = pub(crate) type GenesisValidators =
@@ -18,7 +20,7 @@ pub(crate) type GenesisValidators =
/// The key for the SelectedValidators map. /// The key for the SelectedValidators map.
pub(crate) type SelectedValidatorsKey = (ValidatorSet, [u8; 16], Public); 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 genesis validators
/// ///
/// The usage of is shared with the rest of the pallet. `Sessions` only reads it. /// The usage of is shared with the rest of the pallet. `Sessions` only reads it.
@@ -288,6 +290,11 @@ impl<Storage: SessionsStorage> Sessions for Storage {
selected_validators.append(&mut genesis_validators); 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 latest_decided_session = Storage::LatestDecidedSession::mutate(network, |session| {
let next_session = session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0)); let next_session = session.map(|session| Session(session.0 + 1)).unwrap_or(Session(0));
*session = Some(next_session); *session = Some(next_session);
@@ -341,6 +348,12 @@ impl<Storage: SessionsStorage> Sessions for Storage {
let historic_set = ValidatorSet { network, session: historic_session }; let historic_set = ValidatorSet { network, session: historic_session };
Storage::KeyShares::remove(historic_set); Storage::KeyShares::remove(historic_set);
clear_selected_validators::<Storage::SelectedValidators>(historic_set); clear_selected_validators::<Storage::SelectedValidators>(historic_set);
match historic_set.network {
NetworkId::Serai => {}
NetworkId::External(network) => {
Storage::clear_keys(ExternalValidatorSet { network, session: historic_session })
}
}
} }
} }