From 0eb527f807d204497b366fed9605dc5f1debff48 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Tue, 25 Jul 2023 18:36:01 +0530 Subject: [PATCH] Add slashing/offences on submission of signed non existent proposals/malformed proposals (#694) * setup report offences * complete tests * cargo fmt * cargo fmt * fix test --- Cargo.lock | 22 +- Cargo.toml | 3 +- pallets/dkg-proposal-handler/Cargo.toml | 5 +- pallets/dkg-proposal-handler/src/functions.rs | 30 +++ pallets/dkg-proposal-handler/src/lib.rs | 147 +++++++++++++- pallets/dkg-proposal-handler/src/mock.rs | 33 +++ pallets/dkg-proposal-handler/src/offences.rs | 64 ++++++ pallets/dkg-proposal-handler/src/tests.rs | 190 ++++++++++++++++++ pallets/dkg-proposals/Cargo.toml | 2 + pallets/dkg-proposals/src/mock.rs | 33 +++ standalone/runtime/Cargo.toml | 2 + standalone/runtime/src/lib.rs | 11 +- 12 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 pallets/dkg-proposal-handler/src/offences.rs diff --git a/Cargo.lock b/Cargo.lock index efb64a7e0..1e0143418 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2615,6 +2615,7 @@ dependencies = [ "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-nomination-pools", + "pallet-offences", "pallet-session", "pallet-staking", "pallet-staking-reward-curve", @@ -6597,6 +6598,7 @@ name = "pallet-dkg-proposal-handler" version = "0.1.0" dependencies = [ "dkg-runtime-primitives", + "ethabi", "frame-benchmarking", "frame-support", "frame-system", @@ -6642,6 +6644,7 @@ dependencies = [ "sp-core 7.0.0", "sp-io 7.0.0", "sp-runtime 7.0.0", + "sp-staking", "sp-std 5.0.0", "webb-proposals 0.5.24", ] @@ -6806,6 +6809,23 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-offences" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.43#5e49f6e44820affccaf517fd22af564f4b495d40" +dependencies = [ + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-runtime 7.0.0", + "sp-staking", + "sp-std 5.0.0", +] + [[package]] name = "pallet-root-testing" version = "1.0.0-dev" @@ -12668,7 +12688,7 @@ checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", "digest 0.10.7", - "rand 0.4.6", + "rand 0.6.5", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 634399e70..5f59b01fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -80,8 +80,9 @@ scale-info = { version = "2.1.1", default-features = false, features = ["derive" pallet-aura = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } pallet-indices = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } -pallet-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } +pallet-session = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false, features = ["historical"] } pallet-staking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } +pallet-offences = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } pallet-grandpa = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } pallet-randomness-collective-flip = { package = "pallet-insecure-randomness-collective-flip", git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.43", default-features = false } diff --git a/pallets/dkg-proposal-handler/Cargo.toml b/pallets/dkg-proposal-handler/Cargo.toml index 244428cfd..c2ead84f4 100644 --- a/pallets/dkg-proposal-handler/Cargo.toml +++ b/pallets/dkg-proposal-handler/Cargo.toml @@ -16,7 +16,7 @@ targets = ["x86_64-unknown-linux-gnu"] codec = { package = "parity-scale-codec", version = "3", features = ["derive"], default-features = false } scale-info = { workspace = true } log = { workspace = true } - +ethabi = { workspace = true } dkg-runtime-primitives = { workspace = true } frame-benchmarking = { workspace = true, optional = true } frame-support = { workspace = true } @@ -24,6 +24,7 @@ frame-system = { workspace = true } sp-io = { workspace = true } sp-std = { workspace = true } sp-runtime = { workspace = true } +sp-staking = { workspace = true } sp-core = { workspace = true } pallet-dkg-metadata = { workspace = true } webb-proposals = { workspace = true, default-features = false } @@ -49,10 +50,12 @@ runtime-benchmarks = [ ] std = [ "codec/std", + "ethabi/std", "scale-info/std", "sp-std/std", "sp-io/std", "sp-runtime/std", + "sp-staking/std", "sp-core/std", "frame-support/std", "frame-system/std", diff --git a/pallets/dkg-proposal-handler/src/functions.rs b/pallets/dkg-proposal-handler/src/functions.rs index 99175f414..8b9b5b20c 100644 --- a/pallets/dkg-proposal-handler/src/functions.rs +++ b/pallets/dkg-proposal-handler/src/functions.rs @@ -102,6 +102,36 @@ impl Pallet { }) } + // report an offence against current active validator set + pub fn report_offence( + offence_type: DKGMisbehaviorOffenceType, + ) -> Result<(), sp_staking::offence::OffenceError> { + let session_index = T::ValidatorSet::session_index(); + + // The current dkg authorities are the same as the current validator set + // so we pick the current validator set, this results in easier + // conversion to pallet offences index + let current_validators = T::ValidatorSet::validators(); + let offenders = current_validators + .into_iter() + .enumerate() + .filter_map(|(_, id)| { + >::IdentificationOf::convert( + id.clone() + ).map(|full_id| (id, full_id)) + }) + .collect::>>(); + + // we report an offence against the current DKG authorities + let offence = DKGMisbehaviourOffence { + offence: offence_type, + session_index, + validator_set_count: offenders.len() as u32, + offenders, + }; + T::ReportOffences::report_offence(sp_std::vec![], offence) + } + // *** Offchain worker methods *** /// Offchain worker function that submits signed proposals from the offchain storage on-chain diff --git a/pallets/dkg-proposal-handler/src/lib.rs b/pallets/dkg-proposal-handler/src/lib.rs index fb06f2067..e8c735f93 100644 --- a/pallets/dkg-proposal-handler/src/lib.rs +++ b/pallets/dkg-proposal-handler/src/lib.rs @@ -112,7 +112,11 @@ use dkg_runtime_primitives::{ OffchainSignedProposalBatches, ProposalHandlerTrait, ProposalKind, SignedProposalBatch, TypedChainId, }; -use frame_support::{dispatch::fmt::Debug, pallet_prelude::*}; +use frame_support::{ + dispatch::fmt::Debug, + pallet_prelude::*, + traits::{ValidatorSet, ValidatorSetWithIdentification}, +}; use frame_system::offchain::{AppCrypto, SendSignedTransaction, SignMessage, Signer}; pub use pallet::*; use sp_runtime::{ @@ -120,14 +124,20 @@ use sp_runtime::{ storage::StorageValueRef, storage_lock::{StorageLock, Time}, }, - traits::{AtLeast32BitUnsigned, Zero}, + traits::{AtLeast32BitUnsigned, Convert, Zero}, +}; +use sp_staking::{ + offence::{DisableStrategy, Kind, Offence, ReportOffence}, + SessionIndex, }; use sp_std::{convert::TryInto, vec::Vec}; use webb_proposals::Proposal; pub use weights::WeightInfo; mod impls; +mod offences; pub use impls::*; +pub use offences::*; mod functions; pub use functions::*; @@ -174,6 +184,20 @@ pub mod pallet { ::MaxSignatureLength, >; + /// A type for representing the validator id in a session. + pub type ValidatorId = <::ValidatorSet as ValidatorSet< + ::AccountId, + >>::ValidatorId; + + /// A tuple of (ValidatorId, Identification) where `Identification` is the full identification + /// of `ValidatorId`. + pub type IdentificationTuple = ( + ValidatorId, + <::ValidatorSet as ValidatorSetWithIdentification< + ::AccountId, + >>::Identification, + ); + #[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] pub struct SignedProposalEventData { pub kind: ProposalKind, @@ -222,6 +246,16 @@ pub mod pallet { /// privileged attributes. type ForceOrigin: EnsureOrigin; + /// A type for retrieving the validators supposed to be online in a session. + type ValidatorSet: ValidatorSetWithIdentification; + + /// A type that gives us the ability to submit offence reports for DKG misbehaviours + type ReportOffences: ReportOffence< + Self::AccountId, + IdentificationTuple, + DKGMisbehaviourOffence>, + >; + /// Pallet weight information type WeightInfo: WeightInfo; } @@ -318,6 +352,13 @@ pub mod pallet { /// Signature of the hash of the proposal data. signature: Vec, }, + /// Offence reported against current DKG + SigningOffenceReported { + /// the type of offence reported + offence: DKGMisbehaviorOffenceType, + /// the signed data that is the source of the report + signed_data: SignedProposalBatchOf, + }, } // Errors inform users that something went wrong. @@ -353,6 +394,12 @@ pub mod pallet { ArithmeticOverflow, /// Batch does not contain proposals EmptyBatch, + /// The signature does not match current active key + NotSignedByCurrentDKG, + /// the signed data is invalid + InvalidSignedData, + /// the prposal exists on runtime and is valid + ProposalExistsAndIsValid, /// Proposal batch does not exist ProposalBatchNotFound, } @@ -491,8 +538,102 @@ pub mod pallet { } } - #[pallet::weight(::WeightInfo::force_submit_unsigned_proposal())] + #[pallet::weight(::WeightInfo::submit_signed_proposals(signed_data.proposals.len() as u32))] #[pallet::call_index(2)] + pub fn submit_dkg_signing_offence( + origin: OriginFor, + signed_data: SignedProposalBatchOf, + ) -> DispatchResult { + let _caller = ensure_signed(origin)?; + + // sanity check + ensure!(!signed_data.proposals.is_empty(), Error::::InvalidSignedData); + + // is the signature valid + let result = ensure_signed_by_dkg::>( + &signed_data.signature, + &signed_data.data(), + ); + + // sanity check, does the signature match current DKG + // we can only report the current DKG, this maybe a valid signature + // from a previous DKG, but that is not considered here + ensure!(result.is_ok(), Error::::NotSignedByCurrentDKG); + + // retreive the typed chain id + let common_typed_chain_id = match decode_proposal_identifier( + signed_data.proposals.first().expect("Batch cannot be empty, checked above"), + ) { + Ok(v) => v, + Err(e) => return Err(Self::handle_validation_error(e).into()), + }; + + // check if all the proposals have the same typed_chain_id + for proposal in signed_data.proposals.iter() { + let proposal_typed_chain_id = match decode_proposal_identifier(proposal) { + Ok(v) => v, + Err(e) => return Err(Self::handle_validation_error(e).into()), + }; + + if proposal_typed_chain_id != common_typed_chain_id { + // this is a malformed proposal, this has a valid signature + // but the typed chain id is not common, + // this means that the signature happened outside of pallet, pallet will never + // create a mixed typed_chain proposal + // report an offence + let _ = + Self::report_offence(DKGMisbehaviorOffenceType::SignedMalformedProposal); + + Self::deposit_event(Event::SigningOffenceReported { + offence: DKGMisbehaviorOffenceType::SignedMalformedProposal, + signed_data, + }); + return Ok(()) + } + } + + // is this a real batch? + let expected_batch = SignedProposals::::get( + common_typed_chain_id.typed_chain_id, + signed_data.batch_id, + ); + + if expected_batch.is_none() { + // ensure that this isnt a valid signed batch with just the batch id changed + // this is expensive, the weight should be paid by the caller + let batches_iterator = + SignedProposals::::iter_prefix_values(common_typed_chain_id.typed_chain_id); + for batch in batches_iterator { + if batch.data() == signed_data.data() { + return Err(Error::::ProposalExistsAndIsValid.into()) + } + } + + // make sure this is not front running the submitSignedProposal flow + // this proposal could be valid and waiting in the UnsignedProposalQueue + let unsigned_batches_iterator = UnsignedProposalQueue::::iter_prefix_values( + common_typed_chain_id.typed_chain_id, + ); + for batch in unsigned_batches_iterator { + if batch.data() == signed_data.data() { + return Err(Error::::ProposalExistsAndIsValid.into()) + } + } + + // the batch was never part of unsigned proposal queue, report an offence + let _ = Self::report_offence(DKGMisbehaviorOffenceType::SignedProposalNotInQueue); + Self::deposit_event(Event::SigningOffenceReported { + offence: DKGMisbehaviorOffenceType::SignedProposalNotInQueue, + signed_data, + }); + Ok(()) + } else { + Err(Error::::ProposalExistsAndIsValid.into()) + } + } + + #[pallet::weight(::WeightInfo::force_submit_unsigned_proposal())] + #[pallet::call_index(3)] pub fn force_remove_unsigned_proposal_batch( origin: OriginFor, typed_chain_id: TypedChainId, diff --git a/pallets/dkg-proposal-handler/src/mock.rs b/pallets/dkg-proposal-handler/src/mock.rs index cbaeaf7ff..e2c755040 100644 --- a/pallets/dkg-proposal-handler/src/mock.rs +++ b/pallets/dkg-proposal-handler/src/mock.rs @@ -23,6 +23,7 @@ use frame_support::{parameter_types, traits::Everything, BoundedVec, PalletId}; use frame_system as system; use frame_system::EnsureRoot; use pallet_dkg_proposals::DKGEcdsaToEthereumAddress; +use pallet_session::historical as pallet_session_historical; use sp_core::{sr25519::Signature, H256}; use sp_runtime::{ impl_opaque_keys, @@ -33,6 +34,10 @@ use sp_runtime::{ }, Percent, Permill, }; +use sp_staking::{ + offence::{OffenceError, ReportOffence}, + SessionIndex, +}; use sp_core::offchain::{testing, OffchainDbExt, OffchainWorkerExt, TransactionPoolExt}; @@ -72,6 +77,7 @@ frame_support::construct_runtime!( DKGProposals: pallet_dkg_proposals::{Pallet, Call, Storage, Event}, DKGProposalHandler: pallet_dkg_proposal_handler::{Pallet, Call, Storage, Event}, Aura: pallet_aura::{Pallet, Storage, Config}, + Historical: pallet_session_historical::{Pallet}, } ); @@ -169,6 +175,26 @@ where } } +type IdentificationTuple = (AccountId, AccountId); +type Offence = crate::DKGMisbehaviourOffence; + +parameter_types! { + pub static Offences: Vec<(Vec, Offence)> = vec![]; +} + +/// A mock offence report handler. +pub struct OffenceHandler; +impl ReportOffence for OffenceHandler { + fn report_offence(reporters: Vec, offence: Offence) -> Result<(), OffenceError> { + Offences::mutate(|l| l.push((reporters, offence))); + Ok(()) + } + + fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &SessionIndex) -> bool { + false + } +} + parameter_types! { #[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, scale_info::TypeInfo, Ord, PartialOrd)] pub const MaxProposers : u32 = 100; @@ -184,6 +210,8 @@ impl pallet_dkg_proposal_handler::Config for Test { type BatchId = u32; type MaxProposalsPerBatch = MaxProposalsPerBatch; type ForceOrigin = EnsureRoot; + type ValidatorSet = Historical; + type ReportOffences = OffenceHandler; type WeightInfo = (); } @@ -248,6 +276,11 @@ impl pallet_session::Config for Test { type WeightInfo = (); } +impl pallet_session::historical::Config for Test { + type FullIdentification = AccountId; + type FullIdentificationOf = ConvertInto; +} + parameter_types! { #[derive(Default, Clone, Encode, Decode, Debug, Eq, PartialEq, scale_info::TypeInfo, Ord, PartialOrd, MaxEncodedLen)] pub const VoteLength: u32 = 64; diff --git a/pallets/dkg-proposal-handler/src/offences.rs b/pallets/dkg-proposal-handler/src/offences.rs new file mode 100644 index 000000000..31b514924 --- /dev/null +++ b/pallets/dkg-proposal-handler/src/offences.rs @@ -0,0 +1,64 @@ +use super::*; +use sp_runtime::{Perbill, Saturating}; + +/// The type of DKG misbehaviour offences +#[derive(Clone, RuntimeDebug, TypeInfo, Encode, Decode, PartialEq, Eq)] +pub enum DKGMisbehaviorOffenceType { + /// Signed a proposal not part of unsigned queue + SignedProposalNotInQueue, + /// Signed a malformed proposal, also not part of unsigned queue + SignedMalformedProposal, +} + +/// An offence that is filed if a DKG authority misbehaves +#[derive(Clone, RuntimeDebug, TypeInfo, PartialEq, Eq)] +pub struct DKGMisbehaviourOffence { + /// The current session index in which we report the unresponsive validators. + /// + /// It acts as a time measure for unresponsiveness reports and effectively will always point + /// at the end of the session. + pub session_index: SessionIndex, + /// The size of the validator set in current session/era. + pub validator_set_count: u32, + /// The type of offence + pub offence: DKGMisbehaviorOffenceType, + /// Authorities that were unresponsive during the current era. + pub offenders: Vec, +} + +impl Offence for DKGMisbehaviourOffence { + const ID: Kind = *b"im-online:offlin"; + type TimeSlot = SessionIndex; + + fn offenders(&self) -> Vec { + self.offenders.clone() + } + + fn session_index(&self) -> SessionIndex { + self.session_index + } + + fn validator_set_count(&self) -> u32 { + self.validator_set_count + } + + fn time_slot(&self) -> Self::TimeSlot { + self.session_index + } + + fn disable_strategy(&self) -> DisableStrategy { + DisableStrategy::Never + } + + fn slash_fraction(&self, offenders: u32) -> Perbill { + // the formula is min((3 * (k - (n / 10 + 1))) / n, 1) * 0.07 + // basically, 10% can be offline with no slash, but after that, it linearly climbs up to 7% + // when 13/30 are offline (around 5% when 1/3 are offline). + if let Some(threshold) = offenders.checked_sub(self.validator_set_count / 10 + 1) { + let x = Perbill::from_rational(3 * threshold, self.validator_set_count); + x.saturating_mul(Perbill::from_percent(7)) + } else { + Perbill::default() + } + } +} diff --git a/pallets/dkg-proposal-handler/src/tests.rs b/pallets/dkg-proposal-handler/src/tests.rs index acd564eb8..80ba6b6d2 100644 --- a/pallets/dkg-proposal-handler/src/tests.rs +++ b/pallets/dkg-proposal-handler/src/tests.rs @@ -579,3 +579,193 @@ fn force_submit_should_work_with_valid_proposals() { )); }); } + +#[test] +fn offence_reporting_rejects_an_existing_proposal() { + execute_test_with(|| { + // First submission + 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() + }, + )); + + // lets time travel to 5 blocks later and ensure a batch is created + run_n_blocks(5); + + let mut signed_proposal = mock_signed_proposal_batch(tx_v_2.clone()); + + assert_ok!(DKGProposalHandler::submit_signed_proposals( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + vec![signed_proposal.clone()] + )); + + // Report offence on a valid signed batch should be rejected + assert_noop!( + DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal.clone() + ), + Error::::ProposalExistsAndIsValid + ); + + // Report offence on a valid signed batch with batch_id changed should be rejected + signed_proposal.batch_id = 999; + assert_noop!( + DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal.clone() + ), + Error::::ProposalExistsAndIsValid + ); + + // Should reject a signed proposal without any proposals + signed_proposal.proposals = vec![].try_into().unwrap(); + assert_noop!( + DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal + ), + Error::::InvalidSignedData + ); + }); +} + +#[test] +fn offence_reporting_rejects_malformed_proposal() { + execute_test_with(|| { + // First submission + 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() + }, + )); + + // lets time travel to 5 blocks later and ensure a batch is created + run_n_blocks(5); + + let mut signed_proposal = mock_signed_proposal_batch(tx_v_2.clone()); + + assert_ok!(DKGProposalHandler::submit_signed_proposals( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + vec![signed_proposal.clone()] + )); + + // Should reject a signed proposal with extra data inserted + let unsigned_proposal_1 = Proposal::Unsigned { + kind: ProposalKind::EVM, + data: tx_v_2.encode().try_into().unwrap(), + }; + let unsigned_proposal_2 = Proposal::Unsigned { + kind: ProposalKind::EVM, + data: tx_v_2.encode().try_into().unwrap(), + }; + signed_proposal.proposals = + vec![unsigned_proposal_1, unsigned_proposal_2].try_into().unwrap(); + assert_noop!( + DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal + ), + Error::::NotSignedByCurrentDKG + ); + }); +} + +#[test] +fn offence_reporting_rejects_proposal_in_current_unsigned_queue() { + execute_test_with(|| { + // First submission + 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() + }, + )); + + // lets time travel to 5 blocks later and ensure a batch is created + run_n_blocks(5); + + let signed_proposal = mock_signed_proposal_batch(tx_v_2.clone()); + + // the signed proposal was never added to a batch but is waiting in unsigned queue + // this should be rejected + assert_noop!( + DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal.clone() + ), + Error::::ProposalExistsAndIsValid + ); + + // as a sanity test, clean unsigned queue and this should be accepted + #[allow(deprecated)] + crate::UnsignedProposalQueue::::remove_prefix(TypedChainId::Evm(0), None); + + assert_ok!(DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal + )); + }); +} + +#[test] +fn offence_reporting_accepts_proposal_signed_not_in_queue() { + execute_test_with(|| { + // First submission + 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() + }, + )); + + // lets time travel to 5 blocks later and ensure a batch is created + run_n_blocks(5); + + let signed_proposal = mock_signed_proposal_batch(tx_v_2.clone()); + + assert_ok!(DKGProposalHandler::submit_signed_proposals( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + vec![signed_proposal.clone()] + )); + + // clean the queue + #[allow(deprecated)] + crate::SignedProposals::::remove_prefix(TypedChainId::Evm(0), None); + + // Report offence on a non existent signed batch should be accepted + assert_ok!(DKGProposalHandler::submit_dkg_signing_offence( + RuntimeOrigin::signed(sr25519::Public::from_raw([1; 32])), + signed_proposal.clone() + )); + + let current_offences_reported = Offences::get(); + assert_eq!( + current_offences_reported, + vec![( + vec![], + crate::DKGMisbehaviourOffence { + session_index: 0, + validator_set_count: 0, + offence: crate::DKGMisbehaviorOffenceType::SignedProposalNotInQueue, + offenders: [].into() + } + )] + ); + }); +} diff --git a/pallets/dkg-proposals/Cargo.toml b/pallets/dkg-proposals/Cargo.toml index 139ab1faf..037cf1cbb 100644 --- a/pallets/dkg-proposals/Cargo.toml +++ b/pallets/dkg-proposals/Cargo.toml @@ -33,6 +33,7 @@ pallet-balances = { workspace = true } pallet-timestamp = { workspace = true } frame-benchmarking = { workspace = true, optional = true } pallet-dkg-metadata = { workspace = true } +sp-staking = { workspace = true } [dev-dependencies] webb-proposals = { workspace = true, default-features = false } @@ -63,6 +64,7 @@ std = [ "webb-proposals/std", "pallet-collator-selection/std", "pallet-dkg-metadata/std", + "sp-staking/std", ] runtime-benchmarks = [ "frame-benchmarking", diff --git a/pallets/dkg-proposals/src/mock.rs b/pallets/dkg-proposals/src/mock.rs index c719573bf..6239bd7d1 100644 --- a/pallets/dkg-proposals/src/mock.rs +++ b/pallets/dkg-proposals/src/mock.rs @@ -30,6 +30,7 @@ use frame_support::{ use frame_system as system; use frame_system::EnsureRoot; pub use pallet_balances; +use pallet_session::historical as pallet_session_historical; use sp_core::{sr25519::Signature, H256}; use sp_runtime::{ app_crypto::{ecdsa::Public, sr25519}, @@ -40,6 +41,10 @@ use sp_runtime::{ }, Percent, }; +use sp_staking::{ + offence::{OffenceError, ReportOffence}, + SessionIndex, +}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; @@ -65,6 +70,7 @@ frame_support::construct_runtime!( DKGMetadata: pallet_dkg_metadata::{Pallet, Call, Config, Event, Storage}, DKGProposals: pallet_dkg_proposals::{Pallet, Call, Storage, Event}, DKGProposalHandler: pallet_dkg_proposal_handler::{Pallet, Call, Storage, Event}, + Historical: pallet_session_historical::{Pallet}, } ); @@ -224,6 +230,11 @@ impl pallet_session::Config for Test { type WeightInfo = (); } +impl pallet_session::historical::Config for Test { + type FullIdentification = AccountId; + type FullIdentificationOf = ConvertInto; +} + parameter_types! { pub const PotId: PalletId = PalletId(*b"PotStake"); pub const MaxCandidates: u32 = 1000; @@ -248,6 +259,26 @@ impl pallet_collator_selection::Config for Test { type WeightInfo = (); } +type IdentificationTuple = (AccountId, AccountId); +type Offence = pallet_dkg_proposal_handler::DKGMisbehaviourOffence; + +parameter_types! { + pub static Offences: Vec<(Vec, Offence)> = vec![]; +} + +/// A mock offence report handler. +pub struct OffenceHandler; +impl ReportOffence for OffenceHandler { + fn report_offence(reporters: Vec, offence: Offence) -> Result<(), OffenceError> { + Offences::mutate(|l| l.push((reporters, offence))); + Ok(()) + } + + fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &SessionIndex) -> bool { + false + } +} + parameter_types! { #[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, scale_info::TypeInfo, Ord, PartialOrd)] pub const MaxVotes : u32 = 100; @@ -264,6 +295,8 @@ impl pallet_dkg_proposal_handler::Config for Test { type UnsignedProposalExpiry = frame_support::traits::ConstU64<10>; type SignedProposalHandler = (); type MaxProposalsPerBatch = MaxProposers; + type ValidatorSet = Historical; + type ReportOffences = OffenceHandler; type ForceOrigin = EnsureRoot; type WeightInfo = (); } diff --git a/standalone/runtime/Cargo.toml b/standalone/runtime/Cargo.toml index 26151657b..e06376451 100644 --- a/standalone/runtime/Cargo.toml +++ b/standalone/runtime/Cargo.toml @@ -45,6 +45,7 @@ sp-core = { workspace = true } sp-inherents = { workspace = true } sp-offchain = { workspace = true } sp-runtime = { workspace = true } +pallet-offences = { workspace = true } sp-session = { workspace = true } sp-std = { workspace = true } sp-transaction-pool = { workspace = true } @@ -97,6 +98,7 @@ std = [ "sp-core/std", "sp-inherents/std", "sp-offchain/std", + "pallet-offences/std", "sp-runtime/std", "sp-session/std", "sp-std/std", diff --git a/standalone/runtime/src/lib.rs b/standalone/runtime/src/lib.rs index 30f036512..2e6fdf021 100644 --- a/standalone/runtime/src/lib.rs +++ b/standalone/runtime/src/lib.rs @@ -646,9 +646,17 @@ impl pallet_dkg_proposal_handler::Config for Runtime { type UnsignedProposalExpiry = UnsignedProposalExpiry; type SignedProposalHandler = (BridgeRegistry, DKG); type ForceOrigin = EnsureRoot; + type ValidatorSet = Historical; + type ReportOffences = Offences; type WeightInfo = pallet_dkg_proposal_handler::weights::WebbWeight; } +impl pallet_offences::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type IdentificationTuple = pallet_session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; +} + parameter_types! { #[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, scale_info::TypeInfo, Ord, PartialOrd)] pub const MaxVotes : u32 = 100; @@ -754,7 +762,7 @@ impl pallet_im_online::Config for Runtime { type RuntimeEvent = RuntimeEvent; type NextSessionRotation = pallet_dkg_metadata::DKGPeriodicSessions; type ValidatorSet = Historical; - type ReportUnresponsiveness = (); + type ReportUnresponsiveness = Offences; type UnsignedPriority = ImOnlineUnsignedPriority; type WeightInfo = pallet_im_online::weights::SubstrateWeight; type MaxKeys = MaxKeys; @@ -791,6 +799,7 @@ construct_runtime!( ElectionProviderMultiPhase: pallet_election_provider_multi_phase, BagsList: pallet_bags_list, NominationPools: pallet_nomination_pools, + Offences: pallet_offences, Staking: pallet_staking, Session: pallet_session, Historical: pallet_session_historical,