Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix : Accepting a RefreshProposal should clear all pending unsigned RefreshProposals #725

Merged
merged 3 commits into from
Oct 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
});
}
Loading