Skip to content

Commit

Permalink
Add slashing/offences on submission of signed non existent proposals/…
Browse files Browse the repository at this point in the history
…malformed proposals (#694)

* setup report offences

* complete tests

* cargo fmt

* cargo fmt

* fix test
  • Loading branch information
1xstj authored Jul 25, 2023
1 parent e5dcecb commit 0eb527f
Show file tree
Hide file tree
Showing 12 changed files with 535 additions and 7 deletions.
22 changes: 21 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
5 changes: 4 additions & 1 deletion pallets/dkg-proposal-handler/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ 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 }
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 }
Expand All @@ -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",
Expand Down
30 changes: 30 additions & 0 deletions pallets/dkg-proposal-handler/src/functions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,36 @@ impl<T: Config> Pallet<T> {
})
}

// 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)| {
<T::ValidatorSet as ValidatorSetWithIdentification<T::AccountId>>::IdentificationOf::convert(
id.clone()
).map(|full_id| (id, full_id))
})
.collect::<Vec<IdentificationTuple<T>>>();

// 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
Expand Down
147 changes: 144 additions & 3 deletions pallets/dkg-proposal-handler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,22 +112,32 @@ 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::{
offchain::{
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::*;
Expand Down Expand Up @@ -174,6 +184,20 @@ pub mod pallet {
<T as pallet_dkg_metadata::Config>::MaxSignatureLength,
>;

/// A type for representing the validator id in a session.
pub type ValidatorId<T> = <<T as Config>::ValidatorSet as ValidatorSet<
<T as frame_system::Config>::AccountId,
>>::ValidatorId;

/// A tuple of (ValidatorId, Identification) where `Identification` is the full identification
/// of `ValidatorId`.
pub type IdentificationTuple<T> = (
ValidatorId<T>,
<<T as Config>::ValidatorSet as ValidatorSetWithIdentification<
<T as frame_system::Config>::AccountId,
>>::Identification,
);

#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
pub struct SignedProposalEventData {
pub kind: ProposalKind,
Expand Down Expand Up @@ -222,6 +246,16 @@ pub mod pallet {
/// privileged attributes.
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;

/// A type for retrieving the validators supposed to be online in a session.
type ValidatorSet: ValidatorSetWithIdentification<Self::AccountId>;

/// A type that gives us the ability to submit offence reports for DKG misbehaviours
type ReportOffences: ReportOffence<
Self::AccountId,
IdentificationTuple<Self>,
DKGMisbehaviourOffence<IdentificationTuple<Self>>,
>;

/// Pallet weight information
type WeightInfo: WeightInfo;
}
Expand Down Expand Up @@ -318,6 +352,13 @@ pub mod pallet {
/// Signature of the hash of the proposal data.
signature: Vec<u8>,
},
/// 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<T>,
},
}

// Errors inform users that something went wrong.
Expand Down Expand Up @@ -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,
}
Expand Down Expand Up @@ -491,8 +538,102 @@ pub mod pallet {
}
}

#[pallet::weight(<T as Config>::WeightInfo::force_submit_unsigned_proposal())]
#[pallet::weight(<T as Config>::WeightInfo::submit_signed_proposals(signed_data.proposals.len() as u32))]
#[pallet::call_index(2)]
pub fn submit_dkg_signing_offence(
origin: OriginFor<T>,
signed_data: SignedProposalBatchOf<T>,
) -> DispatchResult {
let _caller = ensure_signed(origin)?;

// sanity check
ensure!(!signed_data.proposals.is_empty(), Error::<T>::InvalidSignedData);

// is the signature valid
let result = ensure_signed_by_dkg::<pallet_dkg_metadata::Pallet<T>>(
&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::<T>::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::<T>::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::<T>::iter_prefix_values(common_typed_chain_id.typed_chain_id);
for batch in batches_iterator {
if batch.data() == signed_data.data() {
return Err(Error::<T>::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::<T>::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::<T>::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::<T>::ProposalExistsAndIsValid.into())
}
}

#[pallet::weight(<T as Config>::WeightInfo::force_submit_unsigned_proposal())]
#[pallet::call_index(3)]
pub fn force_remove_unsigned_proposal_batch(
origin: OriginFor<T>,
typed_chain_id: TypedChainId,
Expand Down
Loading

0 comments on commit 0eb527f

Please sign in to comment.