From 88ba0244890f6290d7222cd3c84e34050a5af33f Mon Sep 17 00:00:00 2001 From: Fedor Sakharov Date: Sun, 22 Mar 2020 09:48:35 +0300 Subject: [PATCH] Parachains double vote handler initial implementation. (#840) * Parachains double vote handler initial implementation. * Make tests test the actual slashing. * Implement SignedExtension validation of double vote reports. * Fixes build after merge * Review fixes * Adds historical session proofs * Review fixes. * Bump runtime spec_version * Get the session number from the proof * Check that proof matches session * Change signature type on DoubleVoteReport * Adds docs and removes blank lines * Removes leftover code * Fix build * Fix build after a merge * Apply suggestions from code review Co-Authored-By: Robert Habermeier * Prune ParentToSessionIndex * Remove a clone and a warning Co-authored-by: Robert Habermeier Co-authored-by: Gavin Wood --- polkadot/Cargo.lock | 2 + polkadot/primitives/src/parachain.rs | 5 +- polkadot/runtime/common/Cargo.toml | 1 + polkadot/runtime/common/src/parachains.rs | 1045 ++++++++++++++++++++- polkadot/runtime/common/src/registrar.rs | 56 +- polkadot/runtime/kusama/src/lib.rs | 13 +- polkadot/runtime/polkadot/src/lib.rs | 13 +- polkadot/runtime/test-runtime/Cargo.toml | 2 + polkadot/runtime/test-runtime/src/lib.rs | 20 +- 9 files changed, 1121 insertions(+), 36 deletions(-) diff --git a/polkadot/Cargo.lock b/polkadot/Cargo.lock index 44ae302688..d6995eddde 100644 --- a/polkadot/Cargo.lock +++ b/polkadot/Cargo.lock @@ -4043,6 +4043,7 @@ dependencies = [ "pallet-authorship 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-babe 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-balances 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", + "pallet-offences 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-randomness-collective-flip 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-session 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-staking 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", @@ -4154,6 +4155,7 @@ dependencies = [ "pallet-grandpa 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-indices 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-nicks 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", + "pallet-offences 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-randomness-collective-flip 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-session 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", "pallet-staking 2.0.0-alpha.4 (git+https://github.com/paritytech/substrate)", diff --git a/polkadot/primitives/src/parachain.rs b/polkadot/primitives/src/parachain.rs index 5a088b0f04..5e558667ac 100644 --- a/polkadot/primitives/src/parachain.rs +++ b/polkadot/primitives/src/parachain.rs @@ -580,7 +580,7 @@ pub struct Activity(#[cfg_attr(feature = "std", serde(with="bytes"))] pub Vec> ParachainCurrency for T where /// Interface to the persistent (stash) identities of the current validators. pub struct ValidatorIdentities(sp_std::marker::PhantomData); +/// A structure used to report conflicting votes by validators. +/// +/// It is generic over two parameters: +/// `Proof` - proof of historical ownership of a key by some validator. +/// `Hash` - a type of a hash used in the runtime. +#[derive(RuntimeDebug, Encode, Decode)] +#[derive(Clone, Eq, PartialEq)] +pub struct DoubleVoteReport { + /// Identity of the double-voter. + pub identity: ValidatorId, + /// First vote of the double-vote. + pub first: (Statement, ValidatorSignature), + /// Second vote of the double-vote. + pub second: (Statement, ValidatorSignature), + /// Proof that the validator with `identity` id was actually a validator at `parent_hash`. + pub proof: Proof, + /// Parent hash of the block this offence was commited. + pub parent_hash: Hash, +} + +impl> DoubleVoteReport { + fn verify>( + &self, + ) -> Result<(), DoubleVoteValidityError> { + let first = self.first.clone(); + let second = self.second.clone(); + let id = self.identity.encode(); + + T::KeyOwnerProofSystem::check_proof((PARACHAIN_KEY_TYPE_ID, id), self.proof.clone()) + .ok_or(DoubleVoteValidityError::InvalidProof)?; + + // Check signatures. + Self::verify_vote(&first, &self.parent_hash, &self.identity)?; + Self::verify_vote(&second, &self.parent_hash, &self.identity)?; + + match (&first.0, &second.0) { + // If issuing a `Candidate` message on a parachain block, neither a `Valid` or + // `Invalid` vote cannot be issued on that parachain block, as the `Candidate` + // message is an implicit validity vote. + (Statement::Candidate(candidate_hash), Statement::Valid(hash)) | + (Statement::Candidate(candidate_hash), Statement::Invalid(hash)) | + (Statement::Valid(hash), Statement::Candidate(candidate_hash)) | + (Statement::Invalid(hash), Statement::Candidate(candidate_hash)) + if *candidate_hash == *hash => {}, + // Otherwise, it is illegal to cast both a `Valid` and + // `Invalid` vote on a given parachain block. + (Statement::Valid(hash_1), Statement::Invalid(hash_2)) | + (Statement::Invalid(hash_1), Statement::Valid(hash_2)) + if *hash_1 == *hash_2 => {}, + _ => { + return Err(DoubleVoteValidityError::NotDoubleVote); + } + } + + Ok(()) + } + + fn verify_vote( + vote: &(Statement, ValidatorSignature), + parent_hash: &Hash, + authority: &ValidatorId, + ) -> Result<(), DoubleVoteValidityError> { + let payload = localized_payload(vote.0.clone(), parent_hash); + + if !vote.1.verify(&payload[..], authority) { + return Err(DoubleVoteValidityError::InvalidSignature); + } + + Ok(()) + } +} + impl Get> for ValidatorIdentities { fn get() -> Vec { >::validators() } } -pub trait Trait: attestations::Trait { +/// A trait to get a session number the `Proof` belongs to. +pub trait GetSessionNumber { + fn session(&self) -> SessionIndex; +} + +impl GetSessionNumber for session::historical::Proof { + fn session(&self) -> SessionIndex { + self.session() + } +} + +pub trait Trait: attestations::Trait + session::historical::Trait + staking::Trait { /// The outer origin type. type Origin: From + From>; @@ -132,6 +229,30 @@ pub trait Trait: attestations::Trait { /// Max head data size. type MaxHeadDataSize: Get; + + /// Proof type. + /// + /// We need this type to bind the `KeyOwnerProofSystem::Proof` to necessary bounds. + /// As soon as https://rust-lang.github.io/rfcs/2289-associated-type-bounds.html + /// gets in this can be simplified. + type Proof: Parameter + GetSessionNumber; + + /// Compute and check proofs of historical key owners. + type KeyOwnerProofSystem: KeyOwnerProofSystem< + (KeyTypeId, Vec), + Proof = Self::Proof, + IdentificationTuple = Self::IdentificationTuple, + >; + + /// An identification tuple type bound to `Parameter`. + type IdentificationTuple: Parameter; + + /// Report an offence. + type ReportOffence: ReportOffence< + Self::AccountId, + Self::IdentificationTuple, + DoubleVoteOffence + >; } /// Origin for the parachains module. @@ -142,6 +263,44 @@ pub enum Origin { Parachain(ParaId), } +/// An offence that is filed if the validator has submitted a double vote. +#[derive(RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))] +pub struct DoubleVoteOffence { + /// The current session index in which we report a validator. + session_index: SessionIndex, + /// The size of the validator set in current session/era. + validator_set_count: u32, + /// An offender that has submitted two conflicting votes. + offender: Offender, +} + +impl Offence for DoubleVoteOffence { + const ID: Kind = *b"para:double-vote"; + type TimeSlot = SessionIndex; + + fn offenders(&self) -> Vec { + vec![self.offender.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 slash_fraction(_offenders_count: u32, _validator_set_count: u32) -> Perbill { + // Slash 100%. + Perbill::from_percent(100) + } +} + /// Total number of individual messages allowed in the parachain -> relay-chain message queue. const MAX_QUEUE_COUNT: usize = 100; /// Total size of messages allowed in the parachain -> relay-chain message queue before which no @@ -173,6 +332,18 @@ decl_storage! { /// /// `None` if not yet updated. pub DidUpdate: Option>; + + /// The mapping from parent block hashes to session indexes. + /// + /// Used for double vote report validation. + pub ParentToSessionIndex get(session_at_block): + map hasher(twox_64_concat) T::Hash => SessionIndex; + + /// The era that is active currently. + /// + /// Changes with the `ActiveEra` from `staking`. Upon these changes `ParentToSessionIndex` + /// is pruned. + ActiveEra get(active_era): Option; } add_extra_genesis { config(authorities): Vec; @@ -304,8 +475,69 @@ decl_module! { Ok(()) } + /// Provide a proof that some validator has commited a double-vote. + /// + /// The weight is 0; in order to avoid DoS a `SignedExtension` validation + /// is implemented. + #[weight = SimpleDispatchInfo::FixedNormal(0)] + pub fn report_double_vote( + origin, + report: DoubleVoteReport< + )>>::Proof, + T::Hash, + >, + ) -> DispatchResult { + let reporter = ensure_signed(origin)?; + + let validators = >::validators(); + let validator_set_count = validators.len() as u32; + + let session_index = report.proof.session(); + let DoubleVoteReport { identity, proof, .. } = report; + + // We have already checked this proof in `SignedExtension`, but we need + // this here to get the full identification of the offender. + let offender = T::KeyOwnerProofSystem::check_proof( + (PARACHAIN_KEY_TYPE_ID, identity.encode()), + proof, + ).ok_or("Invalid/outdated key ownership proof.")?; + + let offence = DoubleVoteOffence { + session_index, + validator_set_count, + offender, + }; + + // Checks if this is actually a double vote are + // implemented in `ValidateDoubleVoteReports::validete`. + T::ReportOffence::report_offence(vec![reporter], offence) + .map_err(|_| "Failed to report offence")?; + + Ok(()) + } + fn on_initialize() { ::DidUpdate::kill(); + + let current_session = >::current_index(); + let parent_hash = >::parent_hash(); + + match Self::active_era() { + Some(era) => { + if let Some(active_era) = >::current_era() { + if era != active_era { + ::ActiveEra::put(active_era); + >::remove_all(); + } + } + } + None => { + if let Some(active_era) = >::current_era() { + ::ActiveEra::set(Some(active_era)); + } + } + } + >::insert(parent_hash, current_session); } fn on_finalize() { @@ -587,9 +819,6 @@ impl Module { active_parachains: &[(ParaId, Option<(CollatorId, Retriable)>)] ) -> sp_std::result::Result, sp_runtime::DispatchError> { - use primitives::parachain::ValidityAttestation; - use sp_runtime::traits::AppVerify; - // returns groups of slices that have the same chain ID. // assumes the inner slice is sorted by id. struct GroupedDutyIter<'a> { @@ -866,6 +1095,103 @@ pub fn ensure_parachain(o: OuterOrigin) -> result::Result(sp_std::marker::PhantomData); + +impl sp_std::fmt::Debug for ValidateDoubleVoteReports where +{ + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "ValidateDoubleVoteReports") + } +} + +/// Custom validity error used while validating double vote reports. +#[derive(RuntimeDebug)] +#[repr(u8)] +pub enum DoubleVoteValidityError { + /// The authority being reported is not in the authority set. + NotAnAuthority = 0, + + /// Failed to convert offender's `FullIdentificationOf`. + FailedToConvertId = 1, + + /// The signature on one or both of the statements in the report is wrong. + InvalidSignature = 2, + + /// The two statements in the report are not conflicting. + NotDoubleVote = 3, + + /// Invalid report. Indicates that statement doesn't match the attestation on one of the votes. + InvalidReport = 4, + + /// The proof provided in the report is not valid. + InvalidProof = 5, +} + +impl SignedExtension for ValidateDoubleVoteReports where + ::Call: IsSubType, T> +{ + const IDENTIFIER: &'static str = "ValidateDoubleVoteReports"; + type AccountId = T::AccountId; + type Call = ::Call; + type AdditionalSigned = (); + type Pre = (); + type DispatchInfo = DispatchInfo; + + fn additional_signed(&self) + -> sp_std::result::Result + { + Ok(()) + } + + fn validate( + &self, + _who: &Self::AccountId, + call: &Self::Call, + _info: DispatchInfo, + _len: usize, + ) -> TransactionValidity { + let r = ValidTransaction::default(); + + if let Some(local_call) = call.is_sub_type() { + if let Call::report_double_vote(report) = local_call { + let validators = >::validators(); + let parent_hash = report.parent_hash; + + let expected_session = Module::::session_at_block(parent_hash); + let session = report.proof.session(); + + if session != expected_session { + return Err(InvalidTransaction::BadProof.into()); + } + + let authorities = Module::::authorities(); + let offender_idx = match authorities.iter().position(|a| *a == report.identity) { + Some(idx) => idx, + None => return Err(InvalidTransaction::Custom( + DoubleVoteValidityError::NotAnAuthority as u8).into() + ), + }; + + if T::FullIdentificationOf::convert(validators[offender_idx].clone()).is_none() { + return Err(InvalidTransaction::Custom( + DoubleVoteValidityError::FailedToConvertId as u8).into() + ); + } + + report + .verify::() + .map_err(|e| TransactionValidityError::from(InvalidTransaction::Custom(e as u8)))?; + } + } + + Ok(r) + } +} + + #[cfg(test)] mod tests { use super::*; @@ -875,8 +1201,12 @@ mod tests { use sp_core::{H256, Blake2Hasher}; use sp_trie::NodeCodec; use sp_runtime::{ - Perbill, curve::PiecewiseLinear, testing::{UintAuthorityId, Header}, - traits::{BlakeTwo256, IdentityLookup, OnInitialize, OnFinalize}, + impl_opaque_keys, + Perbill, curve::PiecewiseLinear, testing::{Header}, + traits::{ + BlakeTwo256, IdentityLookup, OnInitialize, OnFinalize, SaturatedConversion, + OpaqueKeys, + }, }; use primitives::{ parachain::{ @@ -892,6 +1222,8 @@ mod tests { use crate::parachains; use crate::registrar; use crate::slots; + use session::{SessionHandler, SessionManager}; + use staking::EraIndex; // result of as trie_db::NodeCodec>::hashed_null_node() const EMPTY_TRIE_ROOT: [u8; 32] = [ @@ -911,6 +1243,12 @@ mod tests { } } + impl_opaque_keys! { + pub struct TestSessionKeys { + pub parachain_validator: super::Module, + } + } + #[derive(Clone, Eq, PartialEq)] pub struct Test; parameter_types! { @@ -948,14 +1286,28 @@ mod tests { pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17); } + /// Custom `SessionHandler` since we use `TestSessionKeys` as `Keys`. + pub struct TestSessionHandler; + impl SessionHandler for TestSessionHandler { + const KEY_TYPE_IDS: &'static [KeyTypeId] = &[PARACHAIN_KEY_TYPE_ID]; + + fn on_genesis_session(_: &[(AId, Ks)]) {} + + fn on_new_session(_: bool, _: &[(AId, Ks)], _: &[(AId, Ks)]) {} + + fn on_before_session_ending() {} + + fn on_disabled(_: usize) {} + } + impl session::Trait for Test { type Event = (); type ValidatorId = u64; type ValidatorIdOf = staking::StashOf; type ShouldEndSession = session::PeriodicSessions; - type SessionManager = (); - type SessionHandler = session::TestSessionHandler; - type Keys = UintAuthorityId; + type SessionManager = session::historical::NoteHistoricalRoot; + type SessionHandler = TestSessionHandler; + type Keys = TestSessionKeys; type DisabledValidatorsThreshold = DisabledValidatorsThreshold; } @@ -1018,17 +1370,27 @@ mod tests { } parameter_types! { - pub const SessionsPerEra: sp_staking::SessionIndex = 6; - pub const BondingDuration: staking::EraIndex = 28; - pub const SlashDeferDuration: staking::EraIndex = 7; + pub const SessionsPerEra: sp_staking::SessionIndex = 3; + pub const BondingDuration: staking::EraIndex = 3; + pub const SlashDeferDuration: staking::EraIndex = 0; pub const AttestationPeriod: BlockNumber = 100; pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; pub const MaxNominatorRewardedPerValidator: u32 = 64; } + pub struct CurrencyToVoteHandler; + + impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u128 { x } + } + + impl Convert for CurrencyToVoteHandler { + fn convert(x: u128) -> u64 { x.saturated_into() } + } + impl staking::Trait for Test { type RewardRemainder = (); - type CurrencyToVote = (); + type CurrencyToVote = CurrencyToVoteHandler; type Event = (); type Currency = Balances; type Slash = (); @@ -1079,6 +1441,12 @@ mod tests { type MaxRetries = MaxRetries; } + impl offences::Trait for Test { + type Event = (); + type IdentificationTuple = session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; + } + parameter_types! { pub const MaxHeadDataSize: u32 = 100; pub const MaxCodeSize: u32 = 100; @@ -1093,13 +1461,22 @@ mod tests { type Registrar = registrar::Module; type MaxCodeSize = MaxCodeSize; type MaxHeadDataSize = MaxHeadDataSize; + type Proof = )>>::Proof; + type IdentificationTuple = )>>::IdentificationTuple; + type ReportOffence = Offences; + type KeyOwnerProofSystem = Historical; } type Parachains = Module; type Balances = balances::Module; type System = system::Module; + type Offences = offences::Module; + type Staking = staking::Module; + type Session = session::Module; + type Timestamp = timestamp::Module; type RandomnessCollectiveFlip = randomness_collective_flip::Module; type Registrar = registrar::Module; + type Historical = session::historical::Module; fn new_test_ext(parachains: Vec<(ParaId, Vec, Vec)>) -> TestExternalities { use staking::StakerStatus; @@ -1120,7 +1497,9 @@ mod tests { // stashes are the index. let session_keys: Vec<_> = authority_keys.iter().enumerate() - .map(|(i, _k)| (i as u64, i as u64, UintAuthorityId(i as u64))) + .map(|(i, k)| (i as u64, i as u64, TestSessionKeys { + parachain_validator: ValidatorId::from(k.public()), + })) .collect(); let authorities: Vec<_> = authority_keys.iter().map(|k| ValidatorId::from(k.public())).collect(); @@ -1162,8 +1541,9 @@ mod tests { staking::GenesisConfig:: { stakers, - validator_count: 10, - minimum_validator_count: 8, + validator_count: 8, + force_era: staking::Forcing::ForceNew, + minimum_validator_count: 0, invulnerables: vec![], .. Default::default() }.assimilate_storage(&mut t).unwrap(); @@ -1175,6 +1555,18 @@ mod tests { ParachainsCall::set_heads(v) } + fn report_double_vote( + report: DoubleVoteReport::Hash>, + ) -> Result, TransactionValidityError> { + let inner = ParachainsCall::report_double_vote(report); + let call = Call::Parachains(inner.clone()); + + ValidateDoubleVoteReports::(sp_std::marker::PhantomData) + .validate(&0, &call, DispatchInfo::default(), 0)?; + + Ok(inner) + } + // creates a template candidate which pins to correct relay-chain state. fn raw_candidate(para_id: ParaId) -> CandidateReceipt { CandidateReceipt { @@ -1266,8 +1658,48 @@ mod tests { make_blank_attested(raw_candidate) } + fn start_session(session_index: SessionIndex) { + let mut parent_hash = System::parent_hash(); + use sp_runtime::traits::Header; + + for i in Session::current_index()..session_index { + println!("session index {}", i); + Staking::on_finalize(System::block_number()); + System::set_block_number((i + 1).into()); + Timestamp::set_timestamp(System::block_number() * 6000); + + // In order to be able to use `System::parent_hash()` in the tests + // we need to first get it via `System::finalize` and then set it + // the `System::initialize`. However, it is needed to be taken into + // consideration that finalizing will prune some data in `System` + // storage including old values `BlockHash` if that reaches above + // `BlockHashCount` capacity. + if System::block_number() > 1 { + let hdr = System::finalize(); + parent_hash = hdr.hash(); + } + + System::initialize( + &(i as u64 + 1), + &parent_hash, + &Default::default(), + &Default::default(), + Default::default(), + ); + init_block(); + } + + assert_eq!(Session::current_index(), session_index); + } + + fn start_era(era_index: EraIndex) { + start_session((era_index * 3).into()); + assert_eq!(Staking::current_era(), Some(era_index)); + } + fn init_block() { println!("Initializing {}", System::block_number()); + Session::on_initialize(System::block_number()); System::on_initialize(System::block_number()); Registrar::on_initialize(System::block_number()); Parachains::on_initialize(System::block_number()); @@ -1281,6 +1713,7 @@ mod tests { Registrar::on_finalize(System::block_number()); System::on_finalize(System::block_number()); } + Staking::new_session(System::block_number() as u32); System::set_block_number(System::block_number() + 1); init_block(); } @@ -1744,4 +2177,572 @@ mod tests { let hashed_null_node = as trie_db::NodeCodec>::hashed_null_node(); assert_eq!(hashed_null_node, EMPTY_TRIE_ROOT.into()) } + + #[test] + fn double_vote_candidate_and_valid_works() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that a Candidate and Valid statements on the same candidate get slashed. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_index = 0; + let key = extract_key(authorities[authority_index].clone()); + + let statement_candidate = Statement::Candidate(candidate_hash.clone()); + let statement_valid = Statement::Valid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_candidate.clone(), parent_hash); + let payload_2 = localized_payload(statement_valid.clone(), parent_hash); + + let signature_1 = key.sign(&payload_1[..]).into(); + let signature_2 = key.sign(&payload_2[..]).into(); + + // Check that in the beginning the genesis balances are there. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let encoded_key = key.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key.public()), + first: (statement_candidate, signature_1), + second: (statement_valid, signature_2), + proof, + parent_hash, + }; + + let inner = report_double_vote(report).unwrap(); + + assert_ok!(Parachains::dispatch(inner, Origin::signed(1))); + + start_era(2); + + // Check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(2, 0), + staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // Check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); + } + + #[test] + fn double_vote_candidate_and_invalid_works() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that a Candidate and Invalid statements on the same candidate get slashed. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_index = 0; + let key = extract_key(authorities[authority_index].clone()); + + let statement_candidate = Statement::Candidate(candidate_hash); + let statement_invalid = Statement::Invalid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_candidate.clone(), parent_hash); + let payload_2 = localized_payload(statement_invalid.clone(), parent_hash); + + let signature_1 = key.sign(&payload_1[..]).into(); + let signature_2 = key.sign(&payload_2[..]).into(); + + // Check that in the beginning the genesis balances are there. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let encoded_key = key.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key.public()), + first: (statement_candidate, signature_1), + second: (statement_invalid, signature_2), + proof, + parent_hash, + }; + + assert_ok!(Parachains::dispatch( + report_double_vote(report).unwrap(), + Origin::signed(1), + )); + + start_era(2); + + // Check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(Staking::current_era().unwrap(), 0), + staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // Check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + }); + } + + #[test] + fn double_vote_valid_and_invalid_works() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that an Invalid and Valid statements on the same candidate get slashed. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_index = 0; + let key = extract_key(authorities[authority_index].clone()); + + let statement_invalid = Statement::Invalid(candidate_hash.clone()); + let statement_valid = Statement::Valid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_invalid.clone(), parent_hash); + let payload_2 = localized_payload(statement_valid.clone(), parent_hash); + + let signature_1 = key.sign(&payload_1[..]).into(); + let signature_2 = key.sign(&payload_2[..]).into(); + + // Check that in the beginning the genesis balances are there. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let encoded_key = key.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key.public()), + first: (statement_invalid, signature_1), + second: (statement_valid, signature_2), + proof, + parent_hash, + }; + + assert_ok!(Parachains::dispatch( + report_double_vote(report).unwrap(), + Origin::signed(1), + )); + + start_era(2); + + // Check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(2, 0), + staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // Check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); + } + + // Check that submitting the same report twice errors. + #[test] + fn double_vote_submit_twice_works() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that a Candidate and Valid statements on the same candidate get slashed. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_index = 0; + let key = extract_key(authorities[authority_index].clone()); + + let statement_candidate = Statement::Candidate(candidate_hash.clone()); + let statement_valid = Statement::Valid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_candidate.clone(), parent_hash); + let payload_2 = localized_payload(statement_valid.clone(), parent_hash); + + let signature_1 = key.sign(&payload_1[..]).into(); + let signature_2 = key.sign(&payload_2[..]).into(); + + // Check that in the beginning the genesis balances are there. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + let encoded_key = key.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key.public()), + first: (statement_candidate, signature_1), + second: (statement_valid, signature_2), + proof, + parent_hash, + }; + + assert_ok!(Parachains::dispatch( + report_double_vote(report.clone()).unwrap(), + Origin::signed(1), + )); + + assert!(Parachains::dispatch( + report_double_vote(report).unwrap(), + Origin::signed(1), + ).is_err() + ); + + start_era(2); + + // Check that the balance of 0-th validator is slashed 100%. + assert_eq!(Balances::total_balance(&0), 10_000_000 - 10_000); + assert_eq!(Staking::slashable_balance_of(&0), 0); + + assert_eq!( + Staking::eras_stakers(2, 0), + staking::Exposure { + total: 0, + own: 0, + others: vec![], + }, + ); + + // Check that the balances of all other validators are left intact. + for i in 1..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(2, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); + } + + // Check that submitting invalid reports fail. + #[test] + fn double_vote_submit_invalid_works() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that a Candidate and Valid statements on the same candidate get slashed. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_1_index = 0; + let authority_2_index = 1; + let key_1 = extract_key(authorities[authority_1_index].clone()); + let key_2 = extract_key(authorities[authority_2_index].clone()); + + let statement_candidate = Statement::Candidate(candidate_hash.clone()); + let statement_valid = Statement::Valid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_candidate.clone(), parent_hash); + let payload_2 = localized_payload(statement_valid.clone(), parent_hash); + + let signature_1 = key_1.sign(&payload_1[..]).into(); + let signature_2 = key_2.sign(&payload_2[..]).into(); + + let encoded_key = key_1.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key_1.public()), + first: (statement_candidate, signature_1), + second: (statement_valid, signature_2), + proof, + parent_hash, + }; + + assert_eq!( + report_double_vote(report.clone()), + Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(DoubleVoteValidityError::InvalidSignature as u8) + ) + ), + ); + }); + } + + #[test] + fn double_vote_proof_session_mismatch_fails() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + let extract_key = |public: ValidatorId| { + let mut raw_public = [0; 32]; + raw_public.copy_from_slice(public.as_ref()); + Sr25519Keyring::from_raw_public(raw_public).unwrap() + }; + + // Test that submitting a report with a session mismatch between the `parent_hash` + // and the proof itself fails. + new_test_ext(parachains.clone()).execute_with(|| { + let candidate = raw_candidate(1.into()).abridge().0; + let candidate_hash = candidate.hash(); + + assert_eq!(Staking::current_era(), Some(0)); + assert_eq!(Session::current_index(), 0); + + start_era(1); + + let authorities = Parachains::authorities(); + let authority_index = 0; + let key = extract_key(authorities[authority_index].clone()); + + let statement_candidate = Statement::Candidate(candidate_hash.clone()); + let statement_valid = Statement::Valid(candidate_hash.clone()); + let parent_hash = System::parent_hash(); + + let payload_1 = localized_payload(statement_candidate.clone(), parent_hash); + let payload_2 = localized_payload(statement_valid.clone(), parent_hash); + + let signature_1 = key.sign(&payload_1[..]).into(); + let signature_2 = key.sign(&payload_2[..]).into(); + + // Check that in the beginning the genesis balances are there. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + + // Get the proof from another session. + start_era(2); + let encoded_key = key.encode(); + let proof = Historical::prove((PARACHAIN_KEY_TYPE_ID, &encoded_key[..])).unwrap(); + + let report = DoubleVoteReport { + identity: ValidatorId::from(key.public()), + first: (statement_candidate, signature_1), + second: (statement_valid, signature_2), + proof, + parent_hash, + }; + + assert!(report_double_vote(report.clone()).is_err()); + + start_era(3); + + // Check that the balances are unchanged. + for i in 0..authorities.len() { + assert_eq!(Balances::total_balance(&(i as u64)), 10_000_000); + assert_eq!(Staking::slashable_balance_of(&(i as u64)), 10_000); + + assert_eq!( + Staking::eras_stakers(1, i as u64), + staking::Exposure { + total: 10_000, + own: 10_000, + others: vec![], + }, + ); + } + }); + } + + #[test] + fn double_vote_hash_to_session_gets_pruned() { + let parachains = vec![ + (1u32.into(), vec![], vec![]), + ]; + + // Test that `ParentToSessionIndex` is pruned upon eras turn. + new_test_ext(parachains.clone()).execute_with(|| { + start_era(1); + + let parent_hash_1 = System::parent_hash(); + // The mapping should know about the session at this block. + assert_eq!(Parachains::session_at_block(parent_hash_1), 3); + + start_era(2); + + let parent_hash_2 = System::parent_hash(); + + // Info about a block from pevious era is removed. + assert_eq!(Parachains::session_at_block(parent_hash_1), 0); + // Info about a block form this era is present. + assert_eq!(Parachains::session_at_block(parent_hash_2), 6); + }); + } } diff --git a/polkadot/runtime/common/src/registrar.rs b/polkadot/runtime/common/src/registrar.rs index 2bdbbc1a7f..c2101da771 100644 --- a/polkadot/runtime/common/src/registrar.rs +++ b/polkadot/runtime/common/src/registrar.rs @@ -312,7 +312,7 @@ decl_module! { ) { let who = ensure_signed(origin)?; - T::Currency::reserve(&who, T::ParathreadDeposit::get())?; + ::Currency::reserve(&who, T::ParathreadDeposit::get())?; let info = ParaInfo { scheduling: Scheduling::Dynamic, @@ -371,7 +371,7 @@ decl_module! { Self::force_unschedule(|i| i == id); let debtor = >::take(id); - let _ = T::Currency::unreserve(&debtor, T::ParathreadDeposit::get()); + let _ = ::Currency::unreserve(&debtor, T::ParathreadDeposit::get()); Self::deposit_event(Event::ParathreadRegistered(id)); } @@ -646,7 +646,7 @@ mod tests { traits::{ BlakeTwo256, IdentityLookup, OnInitialize, OnFinalize, Dispatchable, AccountIdConversion, - }, testing::{UintAuthorityId, Header}, Perbill + }, testing::{UintAuthorityId, Header}, KeyTypeId, Perbill, curve::PiecewiseLinear, }; use primitives::{ parachain::{ @@ -657,6 +657,7 @@ mod tests { Balance, BlockNumber, }; use frame_support::{ + traits::KeyOwnerProofSystem, impl_outer_origin, impl_outer_dispatch, assert_ok, parameter_types, assert_noop, }; use keyring::Sr25519Keyring; @@ -678,6 +679,17 @@ mod tests { } } + pallet_staking_reward_curve::build! { + const REWARD_CURVE: PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); + } + #[derive(Clone, Eq, PartialEq)] pub struct Test; parameter_types! { @@ -735,7 +747,12 @@ mod tests { } parameter_types!{ + pub const SlashDeferDuration: staking::EraIndex = 7; pub const AttestationPeriod: BlockNumber = 100; + pub const MinimumPeriod: u64 = 3; + pub const SessionsPerEra: sp_staking::SessionIndex = 6; + pub const BondingDuration: staking::EraIndex = 28; + pub const MaxNominatorRewardedPerValidator: u32 = 64; } impl attestations::Trait for Test { @@ -748,6 +765,7 @@ mod tests { pub const Period: BlockNumber = 1; pub const Offset: BlockNumber = 0; pub const DisabledValidatorsThreshold: Perbill = Perbill::from_percent(17); + pub const RewardCurve: &'static PiecewiseLinear<'static> = &REWARD_CURVE; } impl session::Trait for Test { @@ -766,6 +784,34 @@ mod tests { pub const MaxCodeSize: u32 = 100; } + impl staking::Trait for Test { + type RewardRemainder = (); + type CurrencyToVote = (); + type Event = (); + type Currency = balances::Module; + type Slash = (); + type Reward = (); + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SlashDeferDuration = SlashDeferDuration; + type SlashCancelOrigin = system::EnsureRoot; + type SessionInterface = Self; + type Time = timestamp::Module; + type RewardCurve = RewardCurve; + type MaxNominatorRewardedPerValidator = MaxNominatorRewardedPerValidator; + } + + impl timestamp::Trait for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + } + + impl session::historical::Trait for Test { + type FullIdentification = staking::Exposure; + type FullIdentificationOf = staking::ExposureOf; + } + impl parachains::Trait for Test { type Origin = Origin; type Call = Call; @@ -775,6 +821,10 @@ mod tests { type Randomness = RandomnessCollectiveFlip; type MaxCodeSize = MaxCodeSize; type MaxHeadDataSize = MaxHeadDataSize; + type Proof = session::historical::Proof; + type KeyOwnerProofSystem = session::historical::Module; + type IdentificationTuple = )>>::IdentificationTuple; + type ReportOffence = (); } parameter_types! { diff --git a/polkadot/runtime/kusama/src/lib.rs b/polkadot/runtime/kusama/src/lib.rs index 2dd1b1bdfb..0f68654ca6 100644 --- a/polkadot/runtime/kusama/src/lib.rs +++ b/polkadot/runtime/kusama/src/lib.rs @@ -34,7 +34,7 @@ use runtime_common::{attestations, claims, parachains, registrar, slots, }; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - ApplyExtrinsicResult, Percent, Permill, Perbill, RuntimeDebug, + ApplyExtrinsicResult, KeyTypeId, Percent, Permill, Perbill, RuntimeDebug, transaction_validity::{TransactionValidity, InvalidTransaction, TransactionValidityError}, curve::PiecewiseLinear, traits::{BlakeTwo256, Block as BlockT, SignedExtension, OpaqueKeys, ConvertInto, IdentityLookup}, @@ -48,13 +48,14 @@ use version::NativeVersion; use sp_core::OpaqueMetadata; use sp_staking::SessionIndex; use frame_support::{ - parameter_types, construct_runtime, traits::{SplitTwoWays, Randomness}, + parameter_types, construct_runtime, traits::{KeyOwnerProofSystem, SplitTwoWays, Randomness}, weights::DispatchInfo, }; use im_online::sr25519::AuthorityId as ImOnlineId; use authority_discovery_primitives::AuthorityId as AuthorityDiscoveryId; use system::offchain::TransactionSubmitter; use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; +use session::{historical as session_historical}; #[cfg(feature = "std")] pub use staking::StakerStatus; @@ -491,6 +492,10 @@ impl parachains::Trait for Runtime { type Registrar = Registrar; type MaxCodeSize = MaxCodeSize; type MaxHeadDataSize = MaxHeadDataSize; + type Proof = session::historical::Proof; + type KeyOwnerProofSystem = session::historical::Module; + type IdentificationTuple = )>>::IdentificationTuple; + type ReportOffence = Offences; } parameter_types! { @@ -648,6 +653,7 @@ construct_runtime! { Authorship: authorship::{Module, Call, Storage}, Staking: staking::{Module, Call, Storage, Config, Event}, Offences: offences::{Module, Call, Storage, Event}, + Historical: session_historical::{Module}, Session: session::{Module, Call, Storage, Event, Config}, FinalityTracker: finality_tracker::{Module, Call, Storage, Inherent}, Grandpa: grandpa::{Module, Call, Storage, Config, Event}, @@ -708,7 +714,8 @@ pub type SignedExtra = ( system::CheckNonce, system::CheckWeight, transaction_payment::ChargeTransactionPayment::, - registrar::LimitParathreadCommits + registrar::LimitParathreadCommits, + parachains::ValidateDoubleVoteReports, ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; diff --git a/polkadot/runtime/polkadot/src/lib.rs b/polkadot/runtime/polkadot/src/lib.rs index 93b50d0ef8..1799517c01 100644 --- a/polkadot/runtime/polkadot/src/lib.rs +++ b/polkadot/runtime/polkadot/src/lib.rs @@ -35,7 +35,7 @@ use primitives::{ }; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - ApplyExtrinsicResult, Percent, Permill, Perbill, RuntimeDebug, + ApplyExtrinsicResult, KeyTypeId, Percent, Permill, Perbill, RuntimeDebug, transaction_validity::{TransactionValidity, InvalidTransaction, TransactionValidityError}, curve::PiecewiseLinear, traits::{ @@ -52,13 +52,14 @@ use version::NativeVersion; use sp_core::OpaqueMetadata; use sp_staking::SessionIndex; use frame_support::{ - parameter_types, construct_runtime, traits::{SplitTwoWays, Randomness}, + parameter_types, construct_runtime, traits::{KeyOwnerProofSystem, SplitTwoWays, Randomness}, weights::DispatchInfo, }; use im_online::sr25519::AuthorityId as ImOnlineId; use authority_discovery_primitives::AuthorityId as AuthorityDiscoveryId; use system::offchain::TransactionSubmitter; use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; +use session::{historical as session_historical}; #[cfg(feature = "std")] pub use staking::StakerStatus; @@ -499,6 +500,10 @@ impl parachains::Trait for Runtime { type Registrar = Registrar; type MaxCodeSize = MaxCodeSize; type MaxHeadDataSize = MaxHeadDataSize; + type Proof = session::historical::Proof; + type KeyOwnerProofSystem = session::historical::Module; + type IdentificationTuple = )>>::IdentificationTuple; + type ReportOffence = Offences; } parameter_types! { @@ -579,6 +584,7 @@ construct_runtime! { Authorship: authorship::{Module, Call, Storage}, Staking: staking::{Module, Call, Storage, Config, Event}, Offences: offences::{Module, Call, Storage, Event}, + Historical: session_historical::{Module}, Session: session::{Module, Call, Storage, Event, Config}, FinalityTracker: finality_tracker::{Module, Call, Storage, Inherent}, Grandpa: grandpa::{Module, Call, Storage, Config, Event}, @@ -630,7 +636,8 @@ pub type SignedExtra = ( system::CheckNonce, system::CheckWeight, transaction_payment::ChargeTransactionPayment::, - registrar::LimitParathreadCommits + registrar::LimitParathreadCommits, + parachains::ValidateDoubleVoteReports ); /// Unchecked extrinsic type as expected by this runtime. pub type UncheckedExtrinsic = generic::UncheckedExtrinsic; diff --git a/polkadot/runtime/test-runtime/Cargo.toml b/polkadot/runtime/test-runtime/Cargo.toml index 65d31e6f01..791f91b2d0 100644 --- a/polkadot/runtime/test-runtime/Cargo.toml +++ b/polkadot/runtime/test-runtime/Cargo.toml @@ -36,6 +36,7 @@ finality-tracker = { package = "pallet-finality-tracker", git = "https://github. grandpa = { package = "pallet-grandpa", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } indices = { package = "pallet-indices", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } nicks = { package = "pallet-nicks", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } +offences = { package = "pallet-offences", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } randomness-collective-flip = { package = "pallet-randomness-collective-flip", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } session = { package = "pallet-session", git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } frame-support = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false } @@ -89,6 +90,7 @@ std = [ "grandpa/std", "indices/std", "nicks/std", + "offences/std", "sp-runtime/std", "sp-staking/std", "session/std", diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index a70e186024..f5a135442b 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -34,7 +34,7 @@ use runtime_common::{attestations, claims, parachains, registrar, slots, use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - ApplyExtrinsicResult, Perbill, RuntimeDebug, + ApplyExtrinsicResult, Perbill, RuntimeDebug, KeyTypeId, transaction_validity::{TransactionValidity, InvalidTransaction, TransactionValidityError}, curve::PiecewiseLinear, traits::{BlakeTwo256, Block as BlockT, StaticLookup, SignedExtension, OpaqueKeys, ConvertInto}, @@ -46,10 +46,12 @@ use version::NativeVersion; use sp_core::OpaqueMetadata; use sp_staking::SessionIndex; use frame_support::{ - parameter_types, construct_runtime, traits::Randomness, + parameter_types, construct_runtime, + traits::{KeyOwnerProofSystem, Randomness}, weights::DispatchInfo, }; use pallet_transaction_payment_rpc_runtime_api::RuntimeDispatchInfo; +use session::{historical as session_historical}; #[cfg(feature = "std")] pub use staking::StakerStatus; @@ -311,6 +313,18 @@ impl parachains::Trait for Runtime { type Registrar = Registrar; type MaxCodeSize = MaxCodeSize; type MaxHeadDataSize = MaxHeadDataSize; + type Proof = session::historical::Proof; + type KeyOwnerProofSystem = session::historical::Module; + type IdentificationTuple = < + Self::KeyOwnerProofSystem as KeyOwnerProofSystem<(KeyTypeId, Vec)> + >::IdentificationTuple; + type ReportOffence = Offences; +} + +impl offences::Trait for Runtime { + type Event = Event; + type IdentificationTuple = session::historical::IdentificationTuple; + type OnOffenceHandler = Staking; } parameter_types! { @@ -385,6 +399,8 @@ construct_runtime! { // Consensus support. Authorship: authorship::{Module, Call, Storage}, Staking: staking::{Module, Call, Storage, Config, Event}, + Offences: offences::{Module, Call, Storage, Event}, + Historical: session_historical::{Module}, Session: session::{Module, Call, Storage, Event, Config}, Grandpa: grandpa::{Module, Call, Storage, Config, Event},