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-coins-pallet",
"sp-api",
"sp-application-crypto",
"sp-core",
"sp-io",
"zeroize",

View File

@@ -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(
<Ristretto as GroupIo>::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(
<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.
@@ -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::<u16>();
let mut actual_len = validators.len();
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"] }
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",

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
/// 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<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 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.

View File

@@ -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,

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;
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<T>;
}
/* TODO
/// The generated key pair for a given validator set instance.
// Satisfy the `Keys` abstractions
#[pallet::storage]
#[pallet::getter(fn keys)]
pub type Keys<T: Config> =
StorageMap<_, Twox64Concat, ExternalValidatorSet, KeyPair, OptionQuery>;
type OraclizationKeys<T: Config> =
StorageMap<_, Identity, ExternalValidatorSet, Public, 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.
#[pallet::storage]
pub type PendingSlashReport<T: Config> =
@@ -196,6 +206,15 @@ mod pallet {
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
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 (network, keys) in ExternalNetworkId::all().zip(keys.iter().cloned()) {
assert_eq!(network, keys.network());
@@ -504,7 +523,6 @@ mod pallet {
#[pallet::call]
impl<T: Config> Pallet<T> {
/* 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::<T>::set_keys(set, key_pair);
Keys::<T>::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::<T>::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<T: Config> ValidateUnsigned for Pallet<T> {
type Call = Call<T>;
@@ -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::<T>::get(set).is_some() {
// Confirm this set has yet to set keys
if Abstractions::<T>::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::<T>::latest_decided_session(network.into()), Some(current_session));
let participants = Participants::<T>::get(NetworkId::from(network))
.expect("session existed without participants");
let participants = Abstractions::<T>::selected_validators(set.into()).collect::<Vec<_>>();
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::<T>::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::<T>::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<T: Config> AllowMint for Pallet<T> {
fn is_allowed(balance: &ExternalBalance) -> bool {
// get the required stake

View File

@@ -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<Storage: SessionsStorage> 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<Storage: SessionsStorage> Sessions for Storage {
let historic_set = ValidatorSet { network, session: historic_session };
Storage::KeyShares::remove(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 })
}
}
}
}