DKG Removals (#467)

* Update ValidatorSets with a remove_participant call

* Add DkgRemoval, a sign machine for producing the relevant MuSig signatures

* Don't use position-dependent u8s yet Public when removing validators from the DKG

* Add DkgRemovalPreprocess, DkgRemovalShares

Implementation is via a new publish_tributary_tx lambda.

This is code is a copy-pasted mess which will need to be cleaned up.

* Only allow non-removed validators to vote for removals

Otherwise, it's risked that the remaining validators fall below 67% of the
original set.

* Correct publish_serai_tx, which was prior publish_set_keys in practice
This commit is contained in:
Luke Parker
2023-12-04 07:04:44 -05:00
committed by GitHub
parent 99c6375605
commit 797ed49e7b
11 changed files with 1022 additions and 170 deletions

View File

@@ -85,22 +85,49 @@ pub mod pallet {
#[pallet::getter(fn allocation_per_key_share)]
pub type AllocationPerKeyShare<T: Config> =
StorageMap<_, Identity, NetworkId, Amount, OptionQuery>;
/// The validators selected to be in-set.
/// The validators selected to be in-set who haven't been removed.
#[pallet::storage]
#[pallet::getter(fn participants)]
pub type Participants<T: Config> = StorageMap<
pub(crate) type Participants<T: Config> = StorageMap<
_,
Identity,
NetworkId,
BoundedVec<(Public, u64), ConstU32<{ MAX_KEY_SHARES_PER_SET }>>,
ValueQuery,
OptionQuery,
>;
/// The validators selected to be in-set, yet with the ability to perform a check for presence.
/// The validators selected to be in-set, regardless of if removed, with the ability to perform a
/// check for presence.
// Uses Identity so we can call clear_prefix over network, manually inserting a Blake2 hash
// before the spammable key.
#[pallet::storage]
pub type InSet<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 16], Public), (), OptionQuery>;
pub(crate) type InSet<T: Config> =
StorageMap<_, Identity, (NetworkId, [u8; 16], Public), u64, OptionQuery>;
// TODO: Merge this with SortedAllocationsIter
struct InSetIter<T: Config> {
_t: PhantomData<T>,
prefix: Vec<u8>,
last: Vec<u8>,
}
impl<T: Config> InSetIter<T> {
fn new(network: NetworkId) -> Self {
let mut prefix = InSet::<T>::final_prefix().to_vec();
prefix.extend(&network.encode());
Self { _t: PhantomData, prefix: prefix.clone(), last: prefix }
}
}
impl<T: Config> Iterator for InSetIter<T> {
type Item = u64;
fn next(&mut self) -> Option<Self::Item> {
let next = sp_io::storage::next_key(&self.last)?;
if !next.starts_with(&self.prefix) {
return None;
}
let res = u64::decode(&mut sp_io::storage::get(&next).unwrap().as_ref()).unwrap();
self.last = next;
Some(res)
}
}
impl<T: Config> Pallet<T> {
fn in_set_key(
network: NetworkId,
@@ -118,6 +145,8 @@ pub mod pallet {
}
/// Returns true if the account is included in an active set.
///
/// This will still include participants which were removed from the DKG.
pub fn in_active_set(network: NetworkId, account: Public) -> bool {
if network == NetworkId::Serai {
Self::in_active_serai_set(account)
@@ -127,6 +156,8 @@ pub mod pallet {
}
/// Returns true if the account has been definitively included in an active or upcoming set.
///
/// This will still include participants which were removed from the DKG.
pub fn in_set(network: NetworkId, account: Public) -> bool {
if InSet::<T>::contains_key(Self::in_set_key(network, account)) {
return true;
@@ -258,7 +289,6 @@ pub mod pallet {
/// The MuSig key for a validator set.
#[pallet::storage]
#[pallet::getter(fn musig_key)]
pub type MuSigKeys<T: Config> = StorageMap<_, Twox64Concat, ValidatorSet, Public, OptionQuery>;
/// The generated key pair for a given validator set instance.
@@ -272,6 +302,10 @@ pub mod pallet {
NewSet {
set: ValidatorSet,
},
ParticipantRemoved {
set: ValidatorSet,
removed: T::AccountId,
},
KeyGen {
set: ValidatorSet,
key_pair: KeyPair,
@@ -328,7 +362,7 @@ pub mod pallet {
let Some((key, amount)) = iter.next() else { break };
let these_key_shares = amount.0 / allocation_per_key_share;
InSet::<T>::set(Self::in_set_key(network, key), Some(()));
InSet::<T>::set(Self::in_set_key(network, key), Some(these_key_shares));
participants.push((key, these_key_shares));
// This can technically set key_shares to a value exceeding MAX_KEY_SHARES_PER_SET
@@ -348,7 +382,7 @@ pub mod pallet {
Some(musig_key(set, &participants.iter().map(|(id, _)| *id).collect::<Vec<_>>())),
);
}
Participants::<T>::set(network, participants.try_into().unwrap());
Participants::<T>::set(network, Some(participants.try_into().unwrap()));
}
}
@@ -672,7 +706,8 @@ pub mod pallet {
}
fn rotate_session() {
let prior_serai_participants = Self::participants(NetworkId::Serai);
let prior_serai_participants = Participants::<T>::get(NetworkId::Serai)
.expect("no Serai participants upon rotate_session");
let prior_serai_session = Self::session(NetworkId::Serai).unwrap();
// TODO: T::SessionHandler::on_before_session_ending() was here.
@@ -685,7 +720,8 @@ pub mod pallet {
// Update Babe and Grandpa
let session = prior_serai_session.0 + 1;
let validators = prior_serai_participants;
let next_validators = Self::participants(NetworkId::Serai);
let next_validators =
Participants::<T>::get(NetworkId::Serai).expect("no Serai participants after new_session");
Babe::<T>::enact_epoch_change(
WeakBoundedVec::force_from(
validators.iter().copied().map(|(id, w)| (BabeAuthorityId::from(id), w)).collect(),
@@ -733,6 +769,26 @@ pub mod pallet {
#[pallet::call_index(1)]
#[pallet::weight(0)] // TODO
pub fn remove_participant(
origin: OriginFor<T>,
network: NetworkId,
to_remove: Public,
signers: Vec<Public>,
signature: Signature,
) -> DispatchResult {
ensure_none(origin)?;
// Nothing occurs here as validate_unsigned does everything
let _ = network;
let _ = to_remove;
let _ = signers;
let _ = signature;
Ok(())
}
#[pallet::call_index(2)]
#[pallet::weight(0)] // TODO
pub fn allocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let validator = ensure_signed(origin)?;
Coins::<T>::transfer_internal(
@@ -743,7 +799,7 @@ pub mod pallet {
Self::increase_allocation(network, validator, amount)
}
#[pallet::call_index(2)]
#[pallet::call_index(3)]
#[pallet::weight(0)] // TODO
pub fn deallocate(origin: OriginFor<T>, network: NetworkId, amount: Amount) -> DispatchResult {
let account = ensure_signed(origin)?;
@@ -760,7 +816,7 @@ pub mod pallet {
Ok(())
}
#[pallet::call_index(3)]
#[pallet::call_index(4)]
#[pallet::weight((0, DispatchClass::Operational))] // TODO
pub fn claim_deallocation(
origin: OriginFor<T>,
@@ -787,46 +843,137 @@ pub mod pallet {
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),
match call {
Call::set_keys { network, ref key_pair, ref signature } => {
// Don't allow the Serai set to set_keys, as they have no reason to do so
// This should already be covered by the lack of key in MuSigKeys, yet it doesn't hurt to
// be explicit
if network == &NetworkId::Serai {
Err(InvalidTransaction::Custom(0))?;
}
let session = Self::session(NetworkId::Serai).unwrap();
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::AllocationWouldPreventFaultTolerance) |
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("ValidatorSets")
.and_provides((0, set))
.longevity(u64::MAX)
.propagate(true)
.build()
}
Call::remove_participant { network, to_remove, signers, signature } => {
if network == &NetworkId::Serai {
Err(InvalidTransaction::Custom(0))?;
}
// Confirm this set has a session
let Some(current_session) = Self::session(*network) else {
Err(InvalidTransaction::Custom(1))?
};
// This is needed as modify storage variables of the latest decided session
assert_eq!(Pallet::<T>::latest_decided_session(*network), Some(current_session));
let set = ValidatorSet { network: *network, session: current_session };
// Confirm it has yet to set keys
if Keys::<T>::get(set).is_some() {
Err(InvalidTransaction::Custom(2))?;
}
let mut participants =
Participants::<T>::get(network).expect("session existed without participants");
// Require signers be sorted to ensure no duplicates are present
let mut last_signer = None;
let mut signing_key_shares = 0;
for signer in signers {
if let Some(last_signer) = last_signer {
if last_signer >= signer {
Err(InvalidTransaction::Custom(3))?;
}
}
last_signer = Some(signer);
// Doesn't use InSet as InSet *includes* removed validators
// Only non-removed validators should be considered as contributing
let Some(shares) = participants
.iter()
.find(|(participant, _)| participant == to_remove)
.map(|(_, shares)| shares)
else {
Err(InvalidTransaction::Custom(4))?
};
signing_key_shares += shares;
}
// Check 67% are participating in this removal
// This is done by iterating over InSet, which isn't mutated on removal, and reading the
// shares from that
let mut all_key_shares = 0;
for shares in InSetIter::<T>::new(*network) {
all_key_shares += shares;
}
// 2f + 1
if signing_key_shares < ((2 * (all_key_shares - signing_key_shares)) + 1) {
Err(InvalidTransaction::Custom(5))?;
}
// Perform the removal
let Some(removal_index) =
participants.iter().position(|participant| &participant.0 == to_remove)
else {
Err(InvalidTransaction::Custom(6))?
};
participants.remove(removal_index);
// Verify the signature with the MuSig key of the signers
if !musig_key(set, signers)
.verify(&remove_participant_message(&set, *to_remove), signature)
{
Err(InvalidTransaction::BadProof)?;
}
// Set the new MuSig key
MuSigKeys::<T>::set(
set,
Some(musig_key(set, &participants.iter().map(|(id, _)| *id).collect::<Vec<_>>())),
);
Participants::<T>::set(network, Some(participants));
// This does not remove from TotalAllocatedStake or InSet in order to:
// 1) Not decrease the stake present in this set. This means removed participants are
// still liable for the economic security of the external network. This prevents
// a decided set, which is economically secure, from falling below the threshold.
// 2) Not allow parties removed to immediately deallocate, per commentary on deallocation
// scheduling (https://github.com/serai-dex/serai/issues/394).
Pallet::<T>::deposit_event(Event::ParticipantRemoved { set, removed: *to_remove });
ValidTransaction::with_tag_prefix("ValidatorSets")
.and_provides((1, set, to_remove))
.longevity(u64::MAX)
.propagate(true)
.build()
}
Call::allocate { .. } | Call::deallocate { .. } | Call::claim_deallocation { .. } => {
Err(InvalidTransaction::Call)?
}
Call::__Ignore(_, _) => unreachable!(),
};
// Don't allow the Serai set to set_keys, as they have no reason to do so
// This should already be covered by the lack of key in MuSigKeys, yet it doesn't hurt to be
// explicit
if network == &NetworkId::Serai {
Err(InvalidTransaction::Custom(0))?;
}
let session = Self::session(NetworkId::Serai).unwrap();
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::AllocationWouldPreventFaultTolerance) |
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

View File

@@ -98,9 +98,14 @@ pub fn musig_key(set: ValidatorSet, set_keys: &[Public]) -> Public {
Public(dkg::musig::musig_key::<Ristretto>(&musig_context(set), &keys).unwrap().to_bytes())
}
/// The message for the remove_participant signature.
pub fn remove_participant_message(set: &ValidatorSet, removed: Public) -> Vec<u8> {
(b"ValidatorSets-remove_participant", set, removed).encode()
}
/// The message for the set_keys signature.
pub fn set_keys_message(set: &ValidatorSet, key_pair: &KeyPair) -> Vec<u8> {
[b"ValidatorSets-key_pair".as_ref(), &(set, key_pair).encode()].concat()
(b"ValidatorSets-set_keys", set, key_pair).encode()
}
/// For a set of validators whose key shares may exceed the maximum, reduce until they equal the