Properly define the on-chain handover protocol

The new key publishing `Batch`s is more than sufficient.

Also uses the correct key to verify the published `Batch`s authenticity.
This commit is contained in:
Luke Parker 2023-10-10 23:55:59 -04:00
parent 1a0b4198ba
commit 13cbc99149
No known key found for this signature in database
5 changed files with 55 additions and 36 deletions

View file

@ -898,6 +898,8 @@ async fn handle_processor_messages<D: Db, Pro: Processors, P: P2p>(
if id.attempt == 0 { if id.attempt == 0 {
MainDb::<D>::save_first_preprocess(&mut txn, spec.set().network, id.id, preprocess); MainDb::<D>::save_first_preprocess(&mut txn, spec.set().network, id.id, preprocess);
// TODO: If this is the new key's first Batch, only create this TX once we verify
// all prior published `Batch`s
Some(Transaction::Batch(block.0, id.id)) Some(Transaction::Batch(block.0, id.id))
} else { } else {
Some(Transaction::BatchPreprocess(SignData { Some(Transaction::BatchPreprocess(SignData {

View file

@ -122,9 +122,9 @@ The following timeline is established:
Once all the 6 hour period has expired, no `Eventuality`s remain, and all Once all the 6 hour period has expired, no `Eventuality`s remain, and all
outputs are forwarded, the multisig publishes a final `Batch` of the first outputs are forwarded, the multisig publishes a final `Batch` of the first
block, plus `CONFIRMATIONS`, which met these conditions, regardless of if it block, plus `CONFIRMATIONS`, which met these conditions, regardless of if it
would've otherwise had a `Batch`. Then, it reports to Substrate has closed. would've otherwise had a `Batch`. No further actions by it, nor its
No further actions by it, nor its validators, are expected (unless those validators, are expected (unless, of course, those validators remain present
validators remain present in the new multisig). in the new multisig).
7) The new multisig confirms all transactions from all prior multisigs were made 7) The new multisig confirms all transactions from all prior multisigs were made
as expected, including the reported `Batch`s. as expected, including the reported `Batch`s.
@ -151,8 +151,8 @@ The following timeline is established:
actually be achieved (at least, not without a ZK proof the published `Batch`s actually be achieved (at least, not without a ZK proof the published `Batch`s
were correct). were correct).
8) The new multisig reports a successful close of the prior multisig, and 8) The new multisig publishes the next `Batch`, signifying the accepting of full
becomes the sole multisig with full responsibilities. responsibilities and a successful close of the prior multisig.
### Latency and Fees ### Latency and Fees

View file

@ -50,5 +50,6 @@ std = [
"in-instructions-primitives/std", "in-instructions-primitives/std",
"tokens-pallet/std", "tokens-pallet/std",
"validator-sets-pallet/std",
] ]
default = ["std"] default = ["std"]

View file

@ -80,29 +80,21 @@ pub mod pallet {
} }
} }
fn key_for_network<T: Config>(network: NetworkId) -> Result<Public, InvalidTransaction> { fn key_for_network<T: Config>(network: NetworkId) -> Result<(Session, Option<Public>, Option<Public>), InvalidTransaction> {
// TODO: Get the latest session let session = ValidatorSets::<T>::session(network);
let session = Session(0);
let mut set = ValidatorSet { session, network }; let mut set = ValidatorSet { session, network };
// TODO: If this session just set their keys, it'll invalidate any batches in the mempool let latest = ValidatorSets::<T>::keys(set).map(|keys| keys.0);
// Should there be a transitory period/future-set cut off? let prior = if set.session.0 != 0 {
if let Some(keys) = ValidatorSets::<T>::keys(set) {
Ok(keys.0)
} else {
// If this set hasn't set their keys yet, use the previous set's
if set.session.0 == 0 {
// Since there haven't been any keys set, no signature can legitimately exist
Err(InvalidTransaction::BadProof)?;
}
set.session.0 -= 1; set.session.0 -= 1;
ValidatorSets::<T>::keys(set).map(|keys| keys.0)
if let Some(keys) = ValidatorSets::<T>::keys(set) { } else {
Ok(keys.0) None
} else { };
Err(InvalidTransaction::BadProof)? // If there's no keys set, then this must be an invalid signature
} if prior.is_none() && latest.is_none() {
Err(InvalidTransaction::BadProof)?;
} }
Ok((session, prior, latest))
} }
#[pallet::call] #[pallet::call]
@ -155,7 +147,7 @@ pub mod pallet {
}; };
let network = batch.batch.network; let network = batch.batch.network;
let key = key_for_network::<T>(network)?; let (current_session, prior, current) = key_for_network::<T>(network)?;
// verify the batch size // verify the batch size
// TODO: Merge this encode with the one done by batch_message // TODO: Merge this encode with the one done by batch_message
@ -164,10 +156,26 @@ pub mod pallet {
} }
// verify the signature // verify the signature
if !key.verify(&batch_message(&batch.batch), &batch.signature) { let batch_message = batch_message(&batch.batch);
// Check the prior key first since only a single `Batch` (the last one) will be when prior is
// Some yet prior wasn't the signing key
let valid_by_prior = if let Some(key) = prior {
key.verify(&batch_message, &batch.signature)
} else { false };
let valid = valid_by_prior || (if let Some(key) = current {
key.verify(&batch_message, &batch.signature)
} else { false });
if !valid {
Err(InvalidTransaction::BadProof)?; Err(InvalidTransaction::BadProof)?;
} }
// If it wasn't valid by the prior key, meaning it was valid by the current key, the current
// key is publishing `Batch`s. This should only happen once the current key has verified all
// `Batch`s published by the prior key, meaning they are accepting the hand-over.
if prior.is_some() && (!valid_by_prior) {
ValidatorSets::<T>::retire_session(network, Session(current_session.0 - 1));
}
// check that this validator set isn't publishing a batch more than once per block // check that this validator set isn't publishing a batch more than once per block
let current_block = <frame_system::Pallet<T>>::block_number(); let current_block = <frame_system::Pallet<T>>::block_number();
let last_block = LastBatchBlock::<T>::get(network).unwrap_or(Zero::zero()); let last_block = LastBatchBlock::<T>::get(network).unwrap_or(Zero::zero());

View file

@ -58,7 +58,7 @@ pub mod pallet {
#[pallet::storage] #[pallet::storage]
pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>; pub type CurrentSession<T: Config> = StorageMap<_, Identity, NetworkId, Session, OptionQuery>;
impl<T: Config> Pallet<T> { impl<T: Config> Pallet<T> {
fn session(network: NetworkId) -> Session { pub fn session(network: NetworkId) -> Session {
if network == NetworkId::Serai { if network == NetworkId::Serai {
Session(pallet_session::Pallet::<T>::current_index()) Session(pallet_session::Pallet::<T>::current_index())
} else { } else {
@ -206,12 +206,6 @@ pub mod pallet {
let set = ValidatorSet { network, session }; let set = ValidatorSet { network, session };
Pallet::<T>::deposit_event(Event::NewSet { set }); Pallet::<T>::deposit_event(Event::NewSet { set });
if network != NetworkId::Serai { if network != NetworkId::Serai {
// Remove the keys for the set prior to the one now rotating out
if session.0 >= 2 {
let prior_to_now_rotating = ValidatorSet { network, session: Session(session.0 - 2) };
MuSigKeys::<T>::remove(prior_to_now_rotating);
Keys::<T>::remove(prior_to_now_rotating);
}
MuSigKeys::<T>::set(set, Some(musig_key(set, &participants))); MuSigKeys::<T>::set(set, Some(musig_key(set, &participants)));
} }
Participants::<T>::set(network, participants.try_into().unwrap()); Participants::<T>::set(network, participants.try_into().unwrap());
@ -398,8 +392,16 @@ pub mod pallet {
// Handover is automatically complete for Serai as it doesn't have a handover protocol // Handover is automatically complete for Serai as it doesn't have a handover protocol
// TODO: Update how handover completed is determined. It's not on set keys. It's on new // TODO: Update how handover completed is determined. It's not on set keys. It's on new
// set accepting responsibility // set accepting responsibility
let handover_completed = (network == NetworkId::Serai) || let handover_completed = (network == NetworkId::Serai) || {
Keys::<T>::contains_key(ValidatorSet { network, session: Self::session(network) }); let current_session = Self::session(network);
// This function shouldn't be used on genesis
debug_assert!(current_session != Session(0));
// Check the prior session had its keys cleared, which happens once its retired
!Keys::<T>::contains_key(ValidatorSet {
network,
session: Session(current_session.0 - 1),
})
};
// Only spawn a NewSet if the current set was actually established with a completed // Only spawn a NewSet if the current set was actually established with a completed
// handover protocol // handover protocol
if handover_completed { if handover_completed {
@ -411,6 +413,12 @@ pub mod pallet {
pub fn validators(network: NetworkId) -> Vec<Public> { pub fn validators(network: NetworkId) -> Vec<Public> {
Self::participants(network).into() Self::participants(network).into()
} }
pub fn retire_session(network: NetworkId, session: Session) {
let set = ValidatorSet { network, session };
MuSigKeys::<T>::remove(set);
Keys::<T>::remove(set);
}
} }
} }