Skip to content

Commit

Permalink
fix : Accepting a RefreshProposal should clear all pending unsigned R…
Browse files Browse the repository at this point in the history
…efreshProposals (#725)
  • Loading branch information
1xstj authored Oct 24, 2023
1 parent d5512ea commit e20c92d
Show file tree
Hide file tree
Showing 3 changed files with 152 additions and 42 deletions.
26 changes: 26 additions & 0 deletions pallets/dkg-proposal-handler/src/impls.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use super::*;
use sp_std::vec;

impl<T: Config> ProposalHandlerTrait for Pallet<T> {
type BatchId = T::BatchId;
Expand Down Expand Up @@ -57,10 +58,13 @@ impl<T: Config> ProposalHandlerTrait for Pallet<T> {
"submit_signed_proposal: proposal exist in the unsigned queue"
);

// Accept all signatures to make testing easier
#[cfg(not(test))]
ensure!(
Self::validate_proposal_signature(&prop.data(), &prop.signature),
Error::<T>::ProposalSignatureInvalid
);

// Log that the signature is valid
log::debug!(
target: "runtime::dkg_proposal_handler",
Expand All @@ -78,6 +82,28 @@ impl<T: Config> ProposalHandlerTrait for Pallet<T> {

UnsignedProposalQueue::<T>::remove(id.typed_chain_id, prop.batch_id);

// if we accepted a RefreshProposal event, then remove all existing RefreshProposals
// this is required since in case of any reset or stall of DKG we would end up with
// multiple RefreshProposals in queue, but we only want to rotate once
for proposal in prop.proposals.clone().into_iter() {
if let ProposalKind::Refresh = proposal.kind() {
let mut batch_ids_to_remove: Vec<<T as Config>::BatchId> = vec![];
for (_typed_chain_id, batch_id, unsigned_batch) in
UnsignedProposalQueue::<T>::iter()
{
for proposal in unsigned_batch.proposals {
if let ProposalKind::Refresh = proposal.proposal.kind() {
batch_ids_to_remove.push(batch_id);
}
}
}

for batch_id in batch_ids_to_remove {
UnsignedProposalQueue::<T>::remove(id.typed_chain_id, batch_id);
}
}
}

// Emit RuntimeEvent so frontend can react to it.
let signed_proposal_events = prop
.proposals
Expand Down
5 changes: 5 additions & 0 deletions pallets/dkg-proposal-handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,11 @@ pub mod pallet {
&prop_batch.signature,
&data,
);

// Accept all signatures to make testing easier
#[cfg(test)]
let result: Result<(), dkg_runtime_primitives::utils::SignatureError> = Ok(());

match result {
Ok(_) => {
// Do nothing, it is all good.
Expand Down
163 changes: 121 additions & 42 deletions pallets/dkg-proposal-handler/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,60 @@
//
#![allow(clippy::unwrap_used)]
use super::mock::DKGProposalHandler;

use crate::{mock::*, Error, SignedProposalBatchOf};
use codec::Encode;
use dkg_runtime_primitives::{
offchain::storage_keys::OFFCHAIN_SIGNED_PROPOSALS, ProposalHandlerTrait, TransactionV2,
TypedChainId,
keccak_256, offchain::storage_keys::OFFCHAIN_SIGNED_PROPOSALS, utils::ecdsa,
AggregatedPublicKeys, MaxProposalLength, MisbehaviourType, ProposalHandlerTrait,
ProposalHeader, TransactionV2, TypedChainId, KEY_TYPE,
};
use frame_support::{
assert_err, assert_noop, assert_ok,
traits::{Hooks, OnFinalize},
weights::constants::RocksDbWeight,
BoundedVec,
};
use pallet_dkg_metadata::DKGPublicKey;
use sp_core::sr25519;
use sp_runtime::offchain::storage::{StorageRetrievalError, StorageValueRef};
use sp_io::crypto::{ecdsa_generate, ecdsa_sign_prehashed};
use sp_runtime::{
app_crypto::ecdsa::Public,
offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
RuntimeAppPublic,
};
use sp_std::vec::Vec;

use dkg_runtime_primitives::ProposalHeader;
use sp_runtime::offchain::storage::MutateStorageError;
use webb_proposals::{Proposal, ProposalKind};

fn mock_pub_key() -> ecdsa::Public {
ecdsa_generate(KEY_TYPE, None)
}

pub fn mock_signed_refresh_proposal_batch(
pub_key: ecdsa::Public,
batch_id: u32,
unsigned_proposal: Proposal<MaxProposalLength>,
) -> SignedProposalBatchOf<Test> {
let data_to_sign = SignedProposalBatchOf::<Test> {
proposals: vec![unsigned_proposal.clone()].try_into().unwrap(),
batch_id,
signature: vec![].try_into().unwrap(),
};

let data_ser = data_to_sign.data();

let hash = keccak_256(&data_ser);
let sig = mock_sign_msg(&hash).unwrap().unwrap();

let mut sig_vec: Vec<u8> = Vec::new();
let signature = ecdsa_sign_prehashed(KEY_TYPE, &pub_key, &hash).unwrap();

SignedProposalBatchOf::<Test> {
proposals: vec![unsigned_proposal].try_into().unwrap(),
batch_id,
signature: sig_vec.try_into().unwrap(),
}
}

// *** Utility ***

fn add_proposal_to_offchain_storage(prop: SignedProposalBatchOf<Test>) {
Expand Down Expand Up @@ -65,6 +99,10 @@ fn check_offchain_proposals_num_eq(num: usize) {
assert_eq!(stored_props.unwrap().len(), num);
}

pub fn mock_dkg_id(id: u8) -> DKGId {
DKGId::from(Public::from_raw([id; 33]))
}

// helper function to skip blocks
pub fn run_n_blocks(n: u64) -> u64 {
// lets leave enough weight to read a queue with length one and remove one item
Expand Down Expand Up @@ -267,41 +305,6 @@ fn submit_signed_proposal_batch_already_exists() {
});
}

#[test]
fn submit_signed_proposal_fail_invalid_sig() {
execute_test_with(|| {
let tx_v_2 = TransactionV2::EIP2930(mock_eth_tx_eip2930(0));

assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
RuntimeOrigin::root(),
Proposal::Unsigned {
kind: ProposalKind::EVM,
data: tx_v_2.encode().try_into().unwrap()
},
));

let mut invalid_sig: Vec<u8> = Vec::new();
invalid_sig.extend_from_slice(&[0u8, 64]);

let mut signed_proposal = mock_signed_proposal_batch(tx_v_2);
signed_proposal.signature = invalid_sig.try_into().unwrap();

// it does not return an error, however the proposal is not added to the list.
// This is because the signature is invalid, and we are batch processing.
// we could check for the RuntimeEvent that is emitted.
assert_ok!(DKGProposalHandler::submit_signed_proposals(
RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])),
vec![signed_proposal]
));

assert!(
DKGProposalHandler::signed_proposals(TypedChainId::Evm(0), 1).is_none(),
"{}",
true
);
});
}

pub fn make_header(chain: TypedChainId) -> ProposalHeader {
match chain {
TypedChainId::Evm(_) => ProposalHeader::new(
Expand Down Expand Up @@ -769,3 +772,79 @@ fn offence_reporting_accepts_proposal_signed_not_in_queue() {
);
});
}

#[test]
fn submitting_a_refresh_proposal_will_remove_all_pending_refesh_proposals() {
execute_test_with(|| {
// The goal of this test is to ensure that with multiple refresh proposals in the unsigned
// queue we do not want to sign them all, we should only sign the latest and remove all the
// others This scenario can only happen when the DKG has stalled and we call force_rotate,
// in that case the current pending refreshProposal and the newly generated refreshProposal
// will be signed together resulting in double session rotation.

// create refresh proposal for session 100
let next_dkg_key = ecdsa_generate(KEY_TYPE, None);
let refresh_proposal_100 = pallet_dkg_metadata::Pallet::<Test>::create_refresh_proposal(
next_dkg_key.0.to_vec(),
100,
)
.unwrap();
assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
RuntimeOrigin::root(),
refresh_proposal_100.clone()
));

// lets time travel to 5 blocks later and ensure a batch is created
run_n_blocks(5);

// create refresh proposal for session 101
let refresh_proposal_101 = pallet_dkg_metadata::Pallet::<Test>::create_refresh_proposal(
next_dkg_key.0.to_vec(),
101,
)
.unwrap();
assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
RuntimeOrigin::root(),
refresh_proposal_101.clone()
));

// lets time travel to 5 blocks later and ensure a batch is created
run_n_blocks(10);

// there should be two batches with refresh for 100 and 101
let unsigned_proposal_batch_100 =
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 0);
assert!(unsigned_proposal_batch_100.is_some());
let unsigned_proposal_batch_101 =
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 1);
assert!(unsigned_proposal_batch_101.is_some());

// set the DKG key so our signature is accepted
let mock_dkg_key = mock_pub_key();
let input: BoundedVec<_, _> = mock_dkg_key.0.to_vec().try_into().unwrap();
DKGPublicKey::<Test>::put((0, input));

// sign the refresh proposal for session 101
let signed_proposal =
mock_signed_refresh_proposal_batch(mock_dkg_key, 1, refresh_proposal_101.clone());

assert_ok!(DKGProposalHandler::submit_signed_proposals(
RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])),
vec![signed_proposal.clone()]
));

// both refresh for 100 and 101 should be removed from queue
let unsigned_proposal_batch_100 =
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 0);
assert!(unsigned_proposal_batch_100.is_none());
let unsigned_proposal_batch_101 =
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 1);
assert!(unsigned_proposal_batch_101.is_none());

// sanity check, only 101 is signed, 100 is not
let signed_proposal_batch_100 = crate::SignedProposals::<Test>::get(TypedChainId::None, 0);
assert!(signed_proposal_batch_100.is_none());
let signed_proposal_batch_101 = crate::SignedProposals::<Test>::get(TypedChainId::None, 1);
assert!(signed_proposal_batch_101.is_some());
});
}

0 comments on commit e20c92d

Please sign in to comment.