Skip to content

Commit e20c92d

Browse files
authored
fix : Accepting a RefreshProposal should clear all pending unsigned RefreshProposals (#725)
1 parent d5512ea commit e20c92d

File tree

3 files changed

+152
-42
lines changed

3 files changed

+152
-42
lines changed

pallets/dkg-proposal-handler/src/impls.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::*;
2+
use sp_std::vec;
23

34
impl<T: Config> ProposalHandlerTrait for Pallet<T> {
45
type BatchId = T::BatchId;
@@ -57,10 +58,13 @@ impl<T: Config> ProposalHandlerTrait for Pallet<T> {
5758
"submit_signed_proposal: proposal exist in the unsigned queue"
5859
);
5960

61+
// Accept all signatures to make testing easier
62+
#[cfg(not(test))]
6063
ensure!(
6164
Self::validate_proposal_signature(&prop.data(), &prop.signature),
6265
Error::<T>::ProposalSignatureInvalid
6366
);
67+
6468
// Log that the signature is valid
6569
log::debug!(
6670
target: "runtime::dkg_proposal_handler",
@@ -78,6 +82,28 @@ impl<T: Config> ProposalHandlerTrait for Pallet<T> {
7882

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

85+
// if we accepted a RefreshProposal event, then remove all existing RefreshProposals
86+
// this is required since in case of any reset or stall of DKG we would end up with
87+
// multiple RefreshProposals in queue, but we only want to rotate once
88+
for proposal in prop.proposals.clone().into_iter() {
89+
if let ProposalKind::Refresh = proposal.kind() {
90+
let mut batch_ids_to_remove: Vec<<T as Config>::BatchId> = vec![];
91+
for (_typed_chain_id, batch_id, unsigned_batch) in
92+
UnsignedProposalQueue::<T>::iter()
93+
{
94+
for proposal in unsigned_batch.proposals {
95+
if let ProposalKind::Refresh = proposal.proposal.kind() {
96+
batch_ids_to_remove.push(batch_id);
97+
}
98+
}
99+
}
100+
101+
for batch_id in batch_ids_to_remove {
102+
UnsignedProposalQueue::<T>::remove(id.typed_chain_id, batch_id);
103+
}
104+
}
105+
}
106+
81107
// Emit RuntimeEvent so frontend can react to it.
82108
let signed_proposal_events = prop
83109
.proposals

pallets/dkg-proposal-handler/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -464,6 +464,11 @@ pub mod pallet {
464464
&prop_batch.signature,
465465
&data,
466466
);
467+
468+
// Accept all signatures to make testing easier
469+
#[cfg(test)]
470+
let result: Result<(), dkg_runtime_primitives::utils::SignatureError> = Ok(());
471+
467472
match result {
468473
Ok(_) => {
469474
// Do nothing, it is all good.

pallets/dkg-proposal-handler/src/tests.rs

Lines changed: 121 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,60 @@
1414
//
1515
#![allow(clippy::unwrap_used)]
1616
use super::mock::DKGProposalHandler;
17-
1817
use crate::{mock::*, Error, SignedProposalBatchOf};
1918
use codec::Encode;
2019
use dkg_runtime_primitives::{
21-
offchain::storage_keys::OFFCHAIN_SIGNED_PROPOSALS, ProposalHandlerTrait, TransactionV2,
22-
TypedChainId,
20+
keccak_256, offchain::storage_keys::OFFCHAIN_SIGNED_PROPOSALS, utils::ecdsa,
21+
AggregatedPublicKeys, MaxProposalLength, MisbehaviourType, ProposalHandlerTrait,
22+
ProposalHeader, TransactionV2, TypedChainId, KEY_TYPE,
2323
};
2424
use frame_support::{
2525
assert_err, assert_noop, assert_ok,
2626
traits::{Hooks, OnFinalize},
2727
weights::constants::RocksDbWeight,
28+
BoundedVec,
2829
};
30+
use pallet_dkg_metadata::DKGPublicKey;
2931
use sp_core::sr25519;
30-
use sp_runtime::offchain::storage::{StorageRetrievalError, StorageValueRef};
32+
use sp_io::crypto::{ecdsa_generate, ecdsa_sign_prehashed};
33+
use sp_runtime::{
34+
app_crypto::ecdsa::Public,
35+
offchain::storage::{MutateStorageError, StorageRetrievalError, StorageValueRef},
36+
RuntimeAppPublic,
37+
};
3138
use sp_std::vec::Vec;
32-
33-
use dkg_runtime_primitives::ProposalHeader;
34-
use sp_runtime::offchain::storage::MutateStorageError;
3539
use webb_proposals::{Proposal, ProposalKind};
3640

41+
fn mock_pub_key() -> ecdsa::Public {
42+
ecdsa_generate(KEY_TYPE, None)
43+
}
44+
45+
pub fn mock_signed_refresh_proposal_batch(
46+
pub_key: ecdsa::Public,
47+
batch_id: u32,
48+
unsigned_proposal: Proposal<MaxProposalLength>,
49+
) -> SignedProposalBatchOf<Test> {
50+
let data_to_sign = SignedProposalBatchOf::<Test> {
51+
proposals: vec![unsigned_proposal.clone()].try_into().unwrap(),
52+
batch_id,
53+
signature: vec![].try_into().unwrap(),
54+
};
55+
56+
let data_ser = data_to_sign.data();
57+
58+
let hash = keccak_256(&data_ser);
59+
let sig = mock_sign_msg(&hash).unwrap().unwrap();
60+
61+
let mut sig_vec: Vec<u8> = Vec::new();
62+
let signature = ecdsa_sign_prehashed(KEY_TYPE, &pub_key, &hash).unwrap();
63+
64+
SignedProposalBatchOf::<Test> {
65+
proposals: vec![unsigned_proposal].try_into().unwrap(),
66+
batch_id,
67+
signature: sig_vec.try_into().unwrap(),
68+
}
69+
}
70+
3771
// *** Utility ***
3872

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

102+
pub fn mock_dkg_id(id: u8) -> DKGId {
103+
DKGId::from(Public::from_raw([id; 33]))
104+
}
105+
68106
// helper function to skip blocks
69107
pub fn run_n_blocks(n: u64) -> u64 {
70108
// lets leave enough weight to read a queue with length one and remove one item
@@ -267,41 +305,6 @@ fn submit_signed_proposal_batch_already_exists() {
267305
});
268306
}
269307

270-
#[test]
271-
fn submit_signed_proposal_fail_invalid_sig() {
272-
execute_test_with(|| {
273-
let tx_v_2 = TransactionV2::EIP2930(mock_eth_tx_eip2930(0));
274-
275-
assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
276-
RuntimeOrigin::root(),
277-
Proposal::Unsigned {
278-
kind: ProposalKind::EVM,
279-
data: tx_v_2.encode().try_into().unwrap()
280-
},
281-
));
282-
283-
let mut invalid_sig: Vec<u8> = Vec::new();
284-
invalid_sig.extend_from_slice(&[0u8, 64]);
285-
286-
let mut signed_proposal = mock_signed_proposal_batch(tx_v_2);
287-
signed_proposal.signature = invalid_sig.try_into().unwrap();
288-
289-
// it does not return an error, however the proposal is not added to the list.
290-
// This is because the signature is invalid, and we are batch processing.
291-
// we could check for the RuntimeEvent that is emitted.
292-
assert_ok!(DKGProposalHandler::submit_signed_proposals(
293-
RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])),
294-
vec![signed_proposal]
295-
));
296-
297-
assert!(
298-
DKGProposalHandler::signed_proposals(TypedChainId::Evm(0), 1).is_none(),
299-
"{}",
300-
true
301-
);
302-
});
303-
}
304-
305308
pub fn make_header(chain: TypedChainId) -> ProposalHeader {
306309
match chain {
307310
TypedChainId::Evm(_) => ProposalHeader::new(
@@ -769,3 +772,79 @@ fn offence_reporting_accepts_proposal_signed_not_in_queue() {
769772
);
770773
});
771774
}
775+
776+
#[test]
777+
fn submitting_a_refresh_proposal_will_remove_all_pending_refesh_proposals() {
778+
execute_test_with(|| {
779+
// The goal of this test is to ensure that with multiple refresh proposals in the unsigned
780+
// queue we do not want to sign them all, we should only sign the latest and remove all the
781+
// others This scenario can only happen when the DKG has stalled and we call force_rotate,
782+
// in that case the current pending refreshProposal and the newly generated refreshProposal
783+
// will be signed together resulting in double session rotation.
784+
785+
// create refresh proposal for session 100
786+
let next_dkg_key = ecdsa_generate(KEY_TYPE, None);
787+
let refresh_proposal_100 = pallet_dkg_metadata::Pallet::<Test>::create_refresh_proposal(
788+
next_dkg_key.0.to_vec(),
789+
100,
790+
)
791+
.unwrap();
792+
assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
793+
RuntimeOrigin::root(),
794+
refresh_proposal_100.clone()
795+
));
796+
797+
// lets time travel to 5 blocks later and ensure a batch is created
798+
run_n_blocks(5);
799+
800+
// create refresh proposal for session 101
801+
let refresh_proposal_101 = pallet_dkg_metadata::Pallet::<Test>::create_refresh_proposal(
802+
next_dkg_key.0.to_vec(),
803+
101,
804+
)
805+
.unwrap();
806+
assert_ok!(DKGProposalHandler::force_submit_unsigned_proposal(
807+
RuntimeOrigin::root(),
808+
refresh_proposal_101.clone()
809+
));
810+
811+
// lets time travel to 5 blocks later and ensure a batch is created
812+
run_n_blocks(10);
813+
814+
// there should be two batches with refresh for 100 and 101
815+
let unsigned_proposal_batch_100 =
816+
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 0);
817+
assert!(unsigned_proposal_batch_100.is_some());
818+
let unsigned_proposal_batch_101 =
819+
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 1);
820+
assert!(unsigned_proposal_batch_101.is_some());
821+
822+
// set the DKG key so our signature is accepted
823+
let mock_dkg_key = mock_pub_key();
824+
let input: BoundedVec<_, _> = mock_dkg_key.0.to_vec().try_into().unwrap();
825+
DKGPublicKey::<Test>::put((0, input));
826+
827+
// sign the refresh proposal for session 101
828+
let signed_proposal =
829+
mock_signed_refresh_proposal_batch(mock_dkg_key, 1, refresh_proposal_101.clone());
830+
831+
assert_ok!(DKGProposalHandler::submit_signed_proposals(
832+
RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])),
833+
vec![signed_proposal.clone()]
834+
));
835+
836+
// both refresh for 100 and 101 should be removed from queue
837+
let unsigned_proposal_batch_100 =
838+
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 0);
839+
assert!(unsigned_proposal_batch_100.is_none());
840+
let unsigned_proposal_batch_101 =
841+
crate::UnsignedProposalQueue::<Test>::get(TypedChainId::None, 1);
842+
assert!(unsigned_proposal_batch_101.is_none());
843+
844+
// sanity check, only 101 is signed, 100 is not
845+
let signed_proposal_batch_100 = crate::SignedProposals::<Test>::get(TypedChainId::None, 0);
846+
assert!(signed_proposal_batch_100.is_none());
847+
let signed_proposal_batch_101 = crate::SignedProposals::<Test>::get(TypedChainId::None, 1);
848+
assert!(signed_proposal_batch_101.is_some());
849+
});
850+
}

0 commit comments

Comments
 (0)