diff --git a/Cargo.lock b/Cargo.lock index e0ca0b012c64..d26b139cc24c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8830,6 +8830,8 @@ dependencies = [ "frame-support", "frame-system", "log", + "pallet-authorship", + "pallet-session", "pallet-timestamp", "parity-scale-codec", "scale-info", @@ -8838,6 +8840,8 @@ dependencies = [ "sp-core", "sp-io", "sp-runtime", + "sp-session", + "sp-staking", "sp-std", ] @@ -14530,6 +14534,7 @@ dependencies = [ "sc-network", "sc-network-test", "sc-telemetry", + "sc-transaction-pool-api", "sp-api", "sp-application-crypto", "sp-block-builder", diff --git a/substrate/bin/node-template/node/src/service.rs b/substrate/bin/node-template/node/src/service.rs index 403202829241..064fd6ce13ca 100644 --- a/substrate/bin/node-template/node/src/service.rs +++ b/substrate/bin/node-template/node/src/service.rs @@ -110,10 +110,11 @@ pub fn new_partial( let slot_duration = sc_consensus_aura::slot_duration(&*client)?; let import_queue = - sc_consensus_aura::import_queue::(ImportQueueParams { + sc_consensus_aura::import_queue::(ImportQueueParams { block_import: grandpa_block_import.clone(), justification_import: Some(Box::new(grandpa_block_import.clone())), client: client.clone(), + select_chain: select_chain.clone(), create_inherent_data_providers: move |_, ()| async move { let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); @@ -129,6 +130,7 @@ pub fn new_partial( registry: config.prometheus_registry(), check_for_equivocation: Default::default(), telemetry: telemetry.as_ref().map(|x| x.handle()), + offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool.clone()), compatibility_mode: Default::default(), })?; diff --git a/substrate/bin/node-template/runtime/src/lib.rs b/substrate/bin/node-template/runtime/src/lib.rs index 4653b49bf2c3..cbe394d6720b 100644 --- a/substrate/bin/node-template/runtime/src/lib.rs +++ b/substrate/bin/node-template/runtime/src/lib.rs @@ -206,21 +206,21 @@ impl frame_system::Config for Runtime { impl pallet_aura::Config for Runtime { type AuthorityId = AuraId; type DisabledValidators = (); + type WeightInfo = pallet_aura::default_weights::SubstrateWeight<0>; type MaxAuthorities = ConstU32<32>; type AllowMultipleBlocksPerSlot = ConstBool; - + type KeyOwnerProof = sp_core::Void; + type EquivocationReportSystem = (); #[cfg(feature = "experimental")] type SlotDuration = pallet_aura::MinimumPeriodTimesTwo; } impl pallet_grandpa::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); type MaxAuthorities = ConstU32<32>; type MaxNominators = ConstU32<0>; type MaxSetIdSessionEntries = ConstU64<0>; - type KeyOwnerProof = sp_core::Void; type EquivocationReportSystem = (); } @@ -408,6 +408,18 @@ impl_runtime_apis! { } } + impl sp_session::SessionKeys for Runtime { + fn generate_session_keys(seed: Option>) -> Vec { + opaque::SessionKeys::generate(seed) + } + + fn decode_session_keys( + encoded: Vec, + ) -> Option, KeyTypeId)>> { + opaque::SessionKeys::decode_into_raw_public_keys(&encoded) + } + } + impl sp_consensus_aura::AuraApi for Runtime { fn slot_duration() -> sp_consensus_aura::SlotDuration { sp_consensus_aura::SlotDuration::from_millis(Aura::slot_duration()) @@ -416,17 +428,22 @@ impl_runtime_apis! { fn authorities() -> Vec { Aura::authorities().into_inner() } - } - impl sp_session::SessionKeys for Runtime { - fn generate_session_keys(seed: Option>) -> Vec { - opaque::SessionKeys::generate(seed) + fn generate_key_ownership_proof( + _slot: sp_consensus_aura::Slot, + _authority_id: AuraId, + ) -> Option { + None } - fn decode_session_keys( - encoded: Vec, - ) -> Option, KeyTypeId)>> { - opaque::SessionKeys::decode_into_raw_public_keys(&encoded) + fn submit_report_equivocation_unsigned_extrinsic( + _equivocation_proof: sp_consensus_aura::EquivocationProof< + ::Header, + AuraId, + >, + _key_owner_proof: sp_consensus_aura::OpaqueKeyOwnershipProof, + ) -> Option<()> { + None } } @@ -439,6 +456,13 @@ impl_runtime_apis! { Grandpa::current_set_id() } + fn generate_key_ownership_proof( + _set_id: sp_consensus_grandpa::SetId, + _authority_id: GrandpaId, + ) -> Option { + None + } + fn submit_report_equivocation_unsigned_extrinsic( _equivocation_proof: sp_consensus_grandpa::EquivocationProof< ::Hash, @@ -448,16 +472,6 @@ impl_runtime_apis! { ) -> Option<()> { None } - - fn generate_key_ownership_proof( - _set_id: sp_consensus_grandpa::SetId, - _authority_id: GrandpaId, - ) -> Option { - // NOTE: this is the only implementation possible since we've - // defined our key owner proof type as a bottom type (i.e. a type - // with no values). - None - } } impl frame_system_rpc_runtime_api::AccountNonceApi for Runtime { diff --git a/substrate/client/consensus/aura/Cargo.toml b/substrate/client/consensus/aura/Cargo.toml index bc9648f683a8..ef5370e594a6 100644 --- a/substrate/client/consensus/aura/Cargo.toml +++ b/substrate/client/consensus/aura/Cargo.toml @@ -24,6 +24,7 @@ sc-client-api = { path = "../../api" } sc-consensus = { path = "../common" } sc-consensus-slots = { path = "../slots" } sc-telemetry = { path = "../../telemetry" } +sc-transaction-pool-api = { path = "../../transaction-pool/api" } sp-api = { path = "../../../primitives/api" } sp-application-crypto = { path = "../../../primitives/application-crypto" } sp-block-builder = { path = "../../../primitives/block-builder" } diff --git a/substrate/client/consensus/aura/src/import_queue.rs b/substrate/client/consensus/aura/src/import_queue.rs index a8777ef8788c..79e051c27db6 100644 --- a/substrate/client/consensus/aura/src/import_queue.rs +++ b/substrate/client/consensus/aura/src/import_queue.rs @@ -16,14 +16,14 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Module implementing the logic for verifying and importing AuRa blocks. +//! Module implementing the logic for verifying and importing AURA blocks. use crate::{ authorities, standalone::SealVerificationError, AuthorityId, CompatibilityMode, Error, LOG_TARGET, }; use codec::Codec; -use log::{debug, info, trace}; +use log::{debug, info, trace, warn}; use prometheus_endpoint::Registry; use sc_client_api::{backend::AuxStore, BlockOf, UsageProvider}; use sc_consensus::{ @@ -32,10 +32,11 @@ use sc_consensus::{ }; use sc_consensus_slots::{check_equivocation, CheckedHeader, InherentDataProviderExt}; use sc_telemetry::{telemetry, TelemetryHandle, CONSENSUS_DEBUG, CONSENSUS_TRACE}; +use sc_transaction_pool_api::OffchainTransactionPoolFactory; use sp_api::{ApiExt, ProvideRuntimeApi}; use sp_block_builder::BlockBuilder as BlockBuilderApi; use sp_blockchain::HeaderBackend; -use sp_consensus::Error as ConsensusError; +use sp_consensus::{BlockOrigin, Error as ConsensusError, SelectChain}; use sp_consensus_aura::{inherents::AuraInherentData, AuraApi}; use sp_consensus_slots::Slot; use sp_core::crypto::Pair; @@ -46,47 +47,37 @@ use sp_runtime::{ }; use std::{fmt::Debug, marker::PhantomData, sync::Arc}; -/// check a header has been signed by the right key. If the slot is too far in the future, an error -/// will be returned. If it's successful, returns the pre-header and the digest item -/// containing the seal. +// Checked header return information. +struct VerifiedHeaderInfo { + slot: Slot, + seal: DigestItem, + author: AuthorityId

, +} + +/// Check if a header has been signed by the right key. /// -/// This digest item will always return `Some` when used with `as_aura_seal`. -fn check_header( - client: &C, +/// If the slot is too far in the future, an error will be returned. +/// If it's successful, returns the checked header and some information +/// which is required by the current callers. +fn check_header( slot_now: Slot, header: B::Header, hash: B::Hash, authorities: &[AuthorityId

], - check_for_equivocation: CheckForEquivocation, -) -> Result, Error> +) -> Result>, Error> where P::Public: Codec, P::Signature: Codec, - C: sc_client_api::backend::AuxStore, { let check_result = crate::standalone::check_header_slot_and_seal::(slot_now, header, authorities); match check_result { Ok((header, slot, seal)) => { - let expected_author = crate::standalone::slot_author::

(slot, &authorities); - let should_equiv_check = check_for_equivocation.check_for_equivocation(); - if let (true, Some(expected)) = (should_equiv_check, expected_author) { - if let Some(equivocation_proof) = - check_equivocation(client, slot_now, slot, &header, expected) - .map_err(Error::Client)? - { - info!( - target: LOG_TARGET, - "Slot author is equivocating at slot {} with headers {:?} and {:?}", - slot, - equivocation_proof.first_header.hash(), - equivocation_proof.second_header.hash(), - ); - } - } - - Ok(CheckedHeader::Checked(header, (slot, seal))) + let author = crate::standalone::slot_author::

(slot, &authorities) + .ok_or(Error::SlotAuthorNotFound)? + .clone(); + Ok(CheckedHeader::Checked(header, VerifiedHeaderInfo { slot, seal, author })) }, Err(SealVerificationError::Deferred(header, slot)) => Ok(CheckedHeader::Deferred(header, slot)), @@ -98,51 +89,59 @@ where } } -/// A verifier for Aura blocks. -pub struct AuraVerifier { +/// A verifier for AURA blocks. +pub struct AuraVerifier { client: Arc, + select_chain: SC, create_inherent_data_providers: CIDP, check_for_equivocation: CheckForEquivocation, telemetry: Option, + offchain_tx_pool_factory: OffchainTransactionPoolFactory, compatibility_mode: CompatibilityMode, _phantom: PhantomData P>, } -impl AuraVerifier { +impl AuraVerifier { pub(crate) fn new( client: Arc, + select_chain: SC, create_inherent_data_providers: CIDP, check_for_equivocation: CheckForEquivocation, telemetry: Option, + offchain_tx_pool_factory: OffchainTransactionPoolFactory, compatibility_mode: CompatibilityMode, ) -> Self { Self { client, + select_chain, create_inherent_data_providers, check_for_equivocation, telemetry, + offchain_tx_pool_factory, compatibility_mode, _phantom: PhantomData, } } } -impl AuraVerifier +impl AuraVerifier where + B: BlockT, + C: ProvideRuntimeApi + AuxStore, + C::Api: AuraApi> + BlockBuilderApi, + P: Pair, + P::Public: Codec, + SC: SelectChain, + CIDP: CreateInherentDataProviders, CIDP: Send, { - async fn check_inherents( + async fn check_inherents( &self, block: B, at_hash: B::Hash, inherent_data: sp_inherents::InherentData, create_inherent_data_providers: CIDP::InherentDataProviders, - ) -> Result<(), Error> - where - C: ProvideRuntimeApi, - C::Api: BlockBuilderApi, - CIDP: CreateInherentDataProviders, - { + ) -> Result<(), Error> { let inherent_res = self .client .runtime_api() @@ -160,13 +159,98 @@ where Ok(()) } + + async fn check_and_report_equivocation( + &self, + slot_now: Slot, + slot: Slot, + header: &B::Header, + author: &AuthorityId

, + origin: &BlockOrigin, + ) -> Result<(), Error> { + // Don't report any equivocations during initial sync as they are most likely stale. + if !self.check_for_equivocation.0 || *origin == BlockOrigin::NetworkInitialSync { + return Ok(()) + } + + // Check if authorship of this header is an equivocation and return a proof if so. + let Some(equivocation_proof) = + check_equivocation(&*self.client, slot_now, slot, header, author) + .map_err(Error::Client)? + else { + return Ok(()) + }; + + info!( + target: LOG_TARGET, + "Equivocation at slot {} with headers {:?} and {:?}", + slot, + equivocation_proof.first_header.hash(), + equivocation_proof.second_header.hash(), + ); + + // Get the best block on which we will build and send the equivocation report. + let best_hash = self + .select_chain + .best_chain() + .await + .map(|h| h.hash()) + .map_err(|e| Error::Client(e.into()))?; + + let mut runtime_api = self.client.runtime_api(); + + // Generate a key ownership proof. We start by trying to generate the + // key ownership proof at the parent of the equivocating header, this + // will make sure that proof generation is successful since it happens + // during the on-going session (i.e. session keys are available in the + // state to be able to generate the proof). This might fail if the + // equivocation happens on the first block of the session, in which case + // its parent would be on the previous session. If generation on the + // parent header fails we try with best block as well. + let generate_key_owner_proof = |at_hash| { + runtime_api + .generate_key_ownership_proof(at_hash, slot, equivocation_proof.offender.clone()) + .map_err(Error::::RuntimeApi) + }; + + let parent_hash = *header.parent_hash(); + let key_owner_proof = match generate_key_owner_proof(parent_hash)? { + Some(proof) => proof, + None => match generate_key_owner_proof(best_hash)? { + Some(proof) => proof, + None => { + warn!( + target: LOG_TARGET, + "Equivocation offender is not part of the authority set." + ); + return Ok(()) + }, + }, + }; + + // Register the offchain tx pool to be able to use it from the runtime. + runtime_api + .register_extension(self.offchain_tx_pool_factory.offchain_transaction_pool(best_hash)); + + // Submit equivocation report at best block. + runtime_api + .submit_report_equivocation_unsigned_extrinsic( + best_hash, + equivocation_proof, + key_owner_proof, + ) + .map_err(Error::RuntimeApi)?; + + Ok(()) + } } #[async_trait::async_trait] -impl Verifier for AuraVerifier> +impl Verifier for AuraVerifier> where - C: ProvideRuntimeApi + Send + Sync + sc_client_api::backend::AuxStore, + C: ProvideRuntimeApi + Send + Sync + AuxStore, C::Api: BlockBuilderApi + AuraApi> + ApiExt, + SC: SelectChain, P: Pair, P::Public: Codec + Debug, P::Signature: Codec, @@ -205,36 +289,36 @@ where .await .map_err(|e| Error::::Client(sp_blockchain::Error::Application(e)))?; - let mut inherent_data = create_inherent_data_providers - .create_inherent_data() - .await - .map_err(Error::::Inherent)?; - let slot_now = create_inherent_data_providers.slot(); - // we add one to allow for some small drift. - // FIXME #1019 in the future, alter this queue to allow deferring of - // headers - let checked_header = check_header::( - &self.client, - slot_now + 1, - block.header, - hash, - &authorities[..], - self.check_for_equivocation, - ) - .map_err(|e| e.to_string())?; + // We add one to allow for some small drift. + // FIXME #1019 in the future, alter this queue to allow deferring of headers + let checked_header = + check_header::(slot_now + 1, block.header.clone(), hash, &authorities) + .map_err(|e| e.to_string())?; + match checked_header { - CheckedHeader::Checked(pre_header, (slot, seal)) => { - // if the body is passed through, we need to use the runtime + CheckedHeader::Checked(pre_header, verified_info) => { + if let Err(err) = self + .check_and_report_equivocation( + slot_now, + verified_info.slot, + &block.header, + &verified_info.author, + &block.origin, + ) + .await + { + warn!(target: LOG_TARGET, "Error checking/reporting AURA equivocation: {}", err) + }; + + // If the body is passed through, we need to use the runtime // to check that the internally-set timestamp in the inherents // actually matches the slot set in the seal. - if let Some(inner_body) = block.body.take() { + if let Some(inner_body) = block.body { let new_block = B::new(pre_header.clone(), inner_body); - inherent_data.aura_replace_inherent_data(slot); - - // skip the inherents verification if the runtime API is old or not expected to + // Skip the inherents verification if the runtime API is old or not expected to // exist. if self .client @@ -242,6 +326,13 @@ where .has_api_with::, _>(parent_hash, |v| v >= 2) .map_err(|e| e.to_string())? { + let mut inherent_data = create_inherent_data_providers + .create_inherent_data() + .await + .map_err(Error::::Inherent)?; + + inherent_data.aura_replace_inherent_data(verified_info.slot); + self.check_inherents( new_block.clone(), parent_hash, @@ -265,7 +356,7 @@ where ); block.header = pre_header; - block.post_digests.push(seal); + block.post_digests.push(verified_info.seal); block.fork_choice = Some(ForkChoiceStrategy::LongestChain); block.post_hash = Some(hash); @@ -288,37 +379,39 @@ where } /// Should we check for equivocation of a block author? -#[derive(Debug, Clone, Copy)] -pub enum CheckForEquivocation { - /// Yes, check for equivocation. - /// - /// This is the default setting for this. - Yes, - /// No, don't check for equivocation. - No, +/// +/// Implemented as a `bool` newtype (default: true) +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CheckForEquivocation(pub bool); + +impl Default for CheckForEquivocation { + fn default() -> Self { + Self(true) + } } -impl CheckForEquivocation { - /// Should we check for equivocation? - fn check_for_equivocation(self) -> bool { - matches!(self, Self::Yes) +impl From for CheckForEquivocation { + fn from(value: bool) -> Self { + Self(value) } } -impl Default for CheckForEquivocation { - fn default() -> Self { - Self::Yes +impl From for bool { + fn from(value: CheckForEquivocation) -> Self { + value.0 } } /// Parameters of [`import_queue`]. -pub struct ImportQueueParams<'a, Block: BlockT, I, C, S, CIDP> { +pub struct ImportQueueParams<'a, B: BlockT, I, C, SC, S, CIDP> { /// The block import to use. pub block_import: I, /// The justification import. - pub justification_import: Option>, + pub justification_import: Option>, /// The client to interact with the chain. pub client: Arc, + /// Chain selection system. + pub select_chain: SC, /// Something that can create the inherent data providers. pub create_inherent_data_providers: CIDP, /// The spawner to spawn background tasks. @@ -329,50 +422,59 @@ pub struct ImportQueueParams<'a, Block: BlockT, I, C, S, CIDP> { pub check_for_equivocation: CheckForEquivocation, /// Telemetry instance used to report telemetry metrics. pub telemetry: Option, + /// The offchain transaction pool factory. + /// + /// Will be used when sending equivocation reports. + pub offchain_tx_pool_factory: OffchainTransactionPoolFactory, /// Compatibility mode that should be used. /// /// If in doubt, use `Default::default()`. - pub compatibility_mode: CompatibilityMode>, + pub compatibility_mode: CompatibilityMode>, } -/// Start an import queue for the Aura consensus algorithm. -pub fn import_queue( +/// Start an import queue for the AURA consensus algorithm. +pub fn import_queue( ImportQueueParams { block_import, justification_import, client, + select_chain, create_inherent_data_providers, spawner, registry, check_for_equivocation, telemetry, + offchain_tx_pool_factory, compatibility_mode, - }: ImportQueueParams, -) -> Result, sp_consensus::Error> + }: ImportQueueParams, +) -> Result, sp_consensus::Error> where - Block: BlockT, - C::Api: BlockBuilderApi + AuraApi> + ApiExt, + B: BlockT, + C::Api: BlockBuilderApi + AuraApi> + ApiExt, C: 'static - + ProvideRuntimeApi + + ProvideRuntimeApi + BlockOf + Send + Sync + AuxStore - + UsageProvider - + HeaderBackend, - I: BlockImport + Send + Sync + 'static, + + UsageProvider + + HeaderBackend, + SC: SelectChain + 'static, + I: BlockImport + Send + Sync + 'static, P: Pair + 'static, P::Public: Codec + Debug, P::Signature: Codec, S: sp_core::traits::SpawnEssentialNamed, - CIDP: CreateInherentDataProviders + Sync + Send + 'static, + CIDP: CreateInherentDataProviders + Sync + Send + 'static, CIDP::InherentDataProviders: InherentDataProviderExt + Send + Sync, { - let verifier = build_verifier::(BuildVerifierParams { + let verifier = build_verifier::(BuildVerifierParams { client, + select_chain, create_inherent_data_providers, check_for_equivocation, telemetry, + offchain_tx_pool_factory, compatibility_mode, }); @@ -380,15 +482,21 @@ where } /// Parameters of [`build_verifier`]. -pub struct BuildVerifierParams { +pub struct BuildVerifierParams { /// The client to interact with the chain. pub client: Arc, + /// Chain selection system. + pub select_chain: SC, /// Something that can create the inherent data providers. pub create_inherent_data_providers: CIDP, /// Should we check for equivocation? pub check_for_equivocation: CheckForEquivocation, /// Telemetry instance used to report telemetry metrics. pub telemetry: Option, + /// The offchain transaction pool factory. + /// + /// Will be used when sending equivocation reports. + pub offchain_tx_pool_factory: OffchainTransactionPoolFactory, /// Compatibility mode that should be used. /// /// If in doubt, use `Default::default()`. @@ -396,20 +504,24 @@ pub struct BuildVerifierParams { } /// Build the [`AuraVerifier`] -pub fn build_verifier( +pub fn build_verifier( BuildVerifierParams { client, + select_chain, create_inherent_data_providers, check_for_equivocation, telemetry, + offchain_tx_pool_factory, compatibility_mode, - }: BuildVerifierParams, -) -> AuraVerifier { - AuraVerifier::<_, P, _, _>::new( + }: BuildVerifierParams, +) -> AuraVerifier { + AuraVerifier::::new( client, + select_chain, create_inherent_data_providers, check_for_equivocation, telemetry, + offchain_tx_pool_factory, compatibility_mode, ) } diff --git a/substrate/client/consensus/aura/src/lib.rs b/substrate/client/consensus/aura/src/lib.rs index 2ed451ef663e..f2f7f14a4550 100644 --- a/substrate/client/consensus/aura/src/lib.rs +++ b/substrate/client/consensus/aura/src/lib.rs @@ -16,9 +16,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -//! Aura (Authority-round) consensus in substrate. +//! AURA (Authority-round) consensus in Substrate. //! -//! Aura works by having a list of authorities A who are expected to roughly +//! AURA works by having a list of authorities A who are expected to roughly //! agree on the current time. Time is divided up into discrete slots of t //! seconds each. For each slot s, the author of that slot is A[s % |A|]. //! @@ -28,7 +28,8 @@ //! Blocks from future steps will be either deferred or rejected depending on how //! far in the future they are. //! -//! NOTE: Aura itself is designed to be generic over the crypto used. +//! NOTE: AURA itself is designed to be generic over the crypto used. + #![forbid(missing_docs, unsafe_code)] use std::{fmt::Debug, marker::PhantomData, pin::Pin, sync::Arc}; @@ -487,6 +488,9 @@ pub enum Error { /// Inherents Error #[error("Inherent error: {0}")] Inherent(sp_inherents::Error), + /// Runtime Api error. + #[error(transparent)] + RuntimeApi(sp_api::ApiError), } impl From> for String { @@ -554,9 +558,10 @@ mod tests { use sc_consensus_slots::{BackoffAuthoringOnFinalizedHeadLagging, SimpleSlotWorker}; use sc_keystore::LocalKeystore; use sc_network_test::{Block as TestBlock, *}; - use sp_application_crypto::{key_types::AURA, AppCrypto}; + use sc_transaction_pool_api::{OffchainTransactionPoolFactory, RejectAllTxPool}; + use sp_application_crypto::AppCrypto; use sp_consensus::{DisableProofRecording, NoNetwork as DummyOracle, Proposal}; - use sp_consensus_aura::sr25519::AuthorityPair; + use sp_consensus_aura::{sr25519::AuthorityPair, KEY_TYPE}; use sp_inherents::InherentData; use sp_keyring::sr25519::Keyring; use sp_keystore::Keystore; @@ -614,9 +619,14 @@ mod tests { } } + type TestSelectChain = + sc_consensus::LongestChain; + type AuraVerifier = import_queue::AuraVerifier< + TestBlock, PeersFullClient, AuthorityPair, + TestSelectChain, Box< dyn CreateInherentDataProviders< TestBlock, @@ -639,12 +649,14 @@ mod tests { type BlockImport = PeersClient; fn make_verifier(&self, client: PeersClient, _peer_data: &()) -> Self::Verifier { + let backend = client.as_backend(); let client = client.as_client(); + let select_chain = TestSelectChain::new(backend); let slot_duration = slot_duration(&*client).expect("slot duration available"); - assert_eq!(slot_duration.as_millis() as u64, SLOT_DURATION_MS); import_queue::AuraVerifier::new( client, + select_chain, Box::new(|_, _| async { let slot = InherentDataProvider::from_timestamp_and_slot_duration( Timestamp::current(), @@ -652,8 +664,9 @@ mod tests { ); Ok((slot,)) }), - CheckForEquivocation::Yes, + true.into(), None, + OffchainTransactionPoolFactory::new(RejectAllTxPool::default()), CompatibilityMode::None, ) } @@ -709,7 +722,7 @@ mod tests { ); keystore - .sr25519_generate_new(AURA, Some(&key.to_seed())) + .sr25519_generate_new(KEY_TYPE, Some(&key.to_seed())) .expect("Creates authority key"); keystore_paths.push(keystore_path); diff --git a/substrate/client/consensus/aura/src/standalone.rs b/substrate/client/consensus/aura/src/standalone.rs index 0f9b8668d447..33630dd1f986 100644 --- a/substrate/client/consensus/aura/src/standalone.rs +++ b/substrate/client/consensus/aura/src/standalone.rs @@ -18,11 +18,9 @@ //! Standalone functions used within the implementation of Aura. -use std::fmt::Debug; - -use log::trace; - use codec::Codec; +use log::trace; +use std::fmt::Debug; use sc_client_api::{backend::AuxStore, UsageProvider}; use sp_api::{Core, ProvideRuntimeApi}; @@ -95,7 +93,7 @@ pub async fn claim_slot( ) -> Option { let expected_author = slot_author::

(slot, authorities); expected_author.and_then(|p| { - if keystore.has_keys(&[(p.to_raw_vec(), sp_application_crypto::key_types::AURA)]) { + if keystore.has_keys(&[(p.to_raw_vec(), sp_consensus_aura::KEY_TYPE)]) { Some(p.clone()) } else { None @@ -304,19 +302,18 @@ where if slot > slot_now { header.digest_mut().push(seal); return Err(SealVerificationError::Deferred(header, slot)) - } else { - // check the signature is valid under the expected authority and - // chain state. - let expected_author = - slot_author::

(slot, authorities).ok_or(SealVerificationError::SlotAuthorNotFound)?; + } - let pre_hash = header.hash(); + // Check the signature is valid under the expected authority and chain state. + let expected_author = + slot_author::

(slot, authorities).ok_or(SealVerificationError::SlotAuthorNotFound)?; - if P::verify(&sig, pre_hash.as_ref(), expected_author) { - Ok((header, slot, seal)) - } else { - Err(SealVerificationError::BadSignature) - } + let pre_hash = header.hash(); + + if P::verify(&sig, pre_hash.as_ref(), expected_author) { + Ok((header, slot, seal)) + } else { + Err(SealVerificationError::BadSignature) } } diff --git a/substrate/client/consensus/babe/src/lib.rs b/substrate/client/consensus/babe/src/lib.rs index ccf72939631a..0ff53a2d90c6 100644 --- a/substrate/client/consensus/babe/src/lib.rs +++ b/substrate/client/consensus/babe/src/lib.rs @@ -1089,13 +1089,13 @@ where }, }; - // submit equivocation report at best block. let mut runtime_api = self.client.runtime_api(); // Register the offchain tx pool to be able to use it from the runtime. runtime_api .register_extension(self.offchain_tx_pool_factory.offchain_transaction_pool(best_hash)); + // Submit equivocation report at best block. runtime_api .submit_report_equivocation_unsigned_extrinsic( best_hash, diff --git a/substrate/frame/aura/Cargo.toml b/substrate/frame/aura/Cargo.toml index 3d2879bb89f5..b2b0ba908458 100644 --- a/substrate/frame/aura/Cargo.toml +++ b/substrate/frame/aura/Cargo.toml @@ -18,15 +18,21 @@ log = { version = "0.4.17", default-features = false } scale-info = { version = "2.5.0", default-features = false, features = ["derive"] } frame-support = { path = "../support", default-features = false} frame-system = { path = "../system", default-features = false} -pallet-timestamp = { path = "../timestamp", default-features = false} +pallet-authorship = { path = "../authorship", default-features = false, optional = true } +pallet-session = { path = "../session", default-features = false, optional = true } +pallet-timestamp = { path = "../timestamp", default-features = false } sp-application-crypto = { path = "../../primitives/application-crypto", default-features = false} sp-consensus-aura = { path = "../../primitives/consensus/aura", default-features = false} sp-runtime = { path = "../../primitives/runtime", default-features = false} +sp-session = { path = "../../primitives/session", default-features = false} +sp-staking = { path = "../../primitives/staking", default-features = false, features = ["serde"]} sp-std = { path = "../../primitives/std", default-features = false} [dev-dependencies] sp-core = { path = "../../primitives/core", default-features = false} sp-io = { path = "../../primitives/io" } +pallet-authorship = { path = "../authorship" } +pallet-session = { path = "../session" } [features] default = [ "std" ] @@ -35,6 +41,7 @@ std = [ "frame-support/std", "frame-system/std", "log/std", + "pallet-authorship/std", "pallet-timestamp/std", "scale-info/std", "sp-application-crypto/std", @@ -42,6 +49,8 @@ std = [ "sp-core/std", "sp-io/std", "sp-runtime/std", + "sp-session/std", + "sp-staking/std", "sp-std/std", ] try-runtime = [ @@ -50,4 +59,9 @@ try-runtime = [ "pallet-timestamp/try-runtime", "sp-runtime/try-runtime", ] +# Provide the default equivocation report system. +default-equivocation-report-system = [ + "pallet-authorship", + "pallet-session", +] experimental = [] diff --git a/substrate/frame/aura/src/default_weights.rs b/substrate/frame/aura/src/default_weights.rs new file mode 100644 index 000000000000..61e284b71013 --- /dev/null +++ b/substrate/frame/aura/src/default_weights.rs @@ -0,0 +1,54 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Default weights for the AURA Pallet. +//! This file was not auto-generated. + +use frame_support::weights::{ + constants::{RocksDbWeight as DbWeight, WEIGHT_REF_TIME_PER_MICROS, WEIGHT_REF_TIME_PER_NANOS}, + Weight, +}; + +/// Default `WeightInfo` implementation generic over the max number of validator's nominators (`N`). +pub struct SubstrateWeight; + +impl crate::WeightInfo for SubstrateWeight { + fn report_equivocation(validator_count: u32) -> Weight { + // We take the validator set count from the membership proof to + // calculate the weight but we set a floor of 100 validators. + let validator_count = validator_count.max(100) as u64; + let max_nominators_per_validator = N; + + // Checking membership proof + Weight::from_parts(35u64 * WEIGHT_REF_TIME_PER_MICROS, 0) + .saturating_add( + Weight::from_parts(175u64 * WEIGHT_REF_TIME_PER_NANOS, 0) + .saturating_mul(validator_count), + ) + .saturating_add(DbWeight::get().reads(5)) + // Check equivocation proof + .saturating_add(Weight::from_parts(110u64 * WEIGHT_REF_TIME_PER_MICROS, 0)) + // Report offence + .saturating_add(Weight::from_parts(110u64 * WEIGHT_REF_TIME_PER_MICROS, 0)) + .saturating_add(Weight::from_parts( + 25u64 * WEIGHT_REF_TIME_PER_MICROS * max_nominators_per_validator as u64, + 0, + )) + .saturating_add(DbWeight::get().reads(14 + 3 * max_nominators_per_validator as u64)) + .saturating_add(DbWeight::get().writes(10 + 3 * max_nominators_per_validator as u64)) + } +} diff --git a/substrate/frame/aura/src/equivocation.rs b/substrate/frame/aura/src/equivocation.rs new file mode 100644 index 000000000000..319fb236acea --- /dev/null +++ b/substrate/frame/aura/src/equivocation.rs @@ -0,0 +1,224 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! An opt-in utility module for reporting equivocations. +//! +//! This module defines: +//! - an offence type for AURA equivocations; +//! - a system for reporting offences; +//! - a system for signing and submitting transactions; +//! - a way to get the current block author; +//! - a key ownership proof system to prove that a given authority was part of a session; +//! +//! These can be used in an offchain context in order to submit equivocation +//! reporting extrinsics (from the client that's running the AURA protocol) and +//! in a runtime context, to validate the equivocation proofs in the extrinsic +//! and report the offences. + +use frame_support::traits::{Get, KeyOwnerProofSystem}; +use frame_system::{ + offchain::SubmitTransaction, + pallet_prelude::{BlockNumberFor, HeaderFor}, +}; + +use sp_consensus_aura::{EquivocationProof, Slot, KEY_TYPE}; +use sp_runtime::{ + traits::{CheckedDiv, Header as _, Saturating, Zero}, + transaction_validity::{InvalidTransaction, TransactionValidityError}, + DispatchError, KeyTypeId, Perbill, RuntimeDebug, +}; +use sp_session::{GetSessionNumber, GetValidatorCount}; +use sp_staking::{ + offence::{Kind, Offence, OffenceReportSystem, ReportOffence}, + SessionIndex, +}; +use sp_std::prelude::*; + +use log::{error, info}; + +use crate::{Call, Config, Error, LOG_TARGET}; + +/// AURA equivocation offence report. +/// +/// When a validator released two or more blocks at the same slot. +#[derive(Clone, PartialEq, Eq, RuntimeDebug)] +pub struct EquivocationOffence { + /// The slot in which this incident happened. + pub slot: Slot, + /// The session index in which the incident happened. + pub session_index: SessionIndex, + /// The size of the validator set at the time of the offence. + pub validator_set_count: u32, + /// The authority that produced the equivocation. + pub offender: Offender, +} + +impl Offence for EquivocationOffence { + const ID: Kind = *b"aura:equivocatio"; + type TimeSlot = Slot; + + 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.slot + } + + // The formula is min((3k / n)^2, 1) + // where k = offenders_number and n = validators_number + fn slash_fraction(&self, offenders_count: u32) -> Perbill { + // Perbill type domain is [0, 1] by definition + Perbill::from_rational(3 * offenders_count, self.validator_set_count).square() + } +} + +/// AURA equivocation offence report system. +/// +/// This type implements `OffenceReportSystem` trait such that: +/// - Equivocation reports are published on-chain as unsigned extrinsic via +/// `offchain::SendTransactionTypes`. +/// - On-chain validity checks and processing are mostly delegated to the user-provided generic +/// types implementing `KeyOwnerProofSystem` and `ReportOffence` traits. +/// - Offence reporter for unsigned transactions is fetched via the the authorship pallet. +/// +/// Depends on: +/// - pallet-authorship: to get reporter identity when missing during offence evidence processing. +/// - pallet-session: to check the `KeyOwnerProof` validity. +/// +/// In order to check for `KeyOwnerProof` validity we need a way to compare the session index +/// contained within the proof and the session index relative to the produced blocks. +/// In order to map block number to session index this implementation requires the +/// `[pallet_session::Config::NextSessionRotation]` to be `[pallet_session::PeriodicSessions]`. +/// In this way the mapping is performed by just divinding the block number by the session period +/// duration. +pub struct EquivocationReportSystem(sp_std::marker::PhantomData<(T, R, P, L)>); + +impl + OffenceReportSystem< + Option, + (EquivocationProof, T::AuthorityId>, T::KeyOwnerProof), + > for EquivocationReportSystem +where + T: Config + + frame_system::Config + + pallet_authorship::Config + + pallet_session::Config< + NextSessionRotation = pallet_session::PeriodicSessions, + > + frame_system::offchain::SendTransactionTypes>, + R: ReportOffence< + T::AccountId, + P::IdentificationTuple, + EquivocationOffence, + >, + P: KeyOwnerProofSystem<(KeyTypeId, T::AuthorityId), Proof = T::KeyOwnerProof>, + P::IdentificationTuple: Clone, + L: Get, + Period: Get>, + Offset: Get>, +{ + type Longevity = L; + + fn publish_evidence( + evidence: (EquivocationProof, T::AuthorityId>, T::KeyOwnerProof), + ) -> Result<(), ()> { + let (equivocation_proof, key_owner_proof) = evidence; + + let call = Call::report_equivocation_unsigned { + equivocation_proof: Box::new(equivocation_proof), + key_owner_proof, + }; + let res = SubmitTransaction::>::submit_unsigned_transaction(call.into()); + match res { + Ok(_) => info!(target: LOG_TARGET, "Submitted equivocation report"), + Err(e) => error!(target: LOG_TARGET, "Error submitting equivocation report: {:?}", e), + } + res + } + + fn check_evidence( + evidence: (EquivocationProof, T::AuthorityId>, T::KeyOwnerProof), + ) -> Result<(), TransactionValidityError> { + let (equivocation_proof, key_owner_proof) = evidence; + + // Check the membership proof to extract the offender's id + let key = (sp_consensus_aura::KEY_TYPE, equivocation_proof.offender.clone()); + let offender = + P::check_proof(key, key_owner_proof.clone()).ok_or(InvalidTransaction::BadProof)?; + + // Check if the offence has already been reported, and if so then we can discard the report. + if R::is_known_offence(&[offender], &equivocation_proof.slot) { + Err(InvalidTransaction::Stale.into()) + } else { + Ok(()) + } + } + + fn process_evidence( + reporter: Option, + evidence: (EquivocationProof, T::AuthorityId>, T::KeyOwnerProof), + ) -> Result<(), DispatchError> { + let (equivocation_proof, key_owner_proof) = evidence; + let reporter = reporter.or_else(|| >::author()); + + let offender = equivocation_proof.offender.clone(); + let slot = equivocation_proof.slot; + + let block_num1 = *equivocation_proof.first_header.number(); + let block_num2 = *equivocation_proof.second_header.number(); + + // Validate the equivocation proof (check votes are different and signatures are valid) + if !sp_consensus_aura::check_equivocation_proof(equivocation_proof) { + return Err(Error::::InvalidEquivocationProof.into()) + } + + let validator_set_count = key_owner_proof.validator_count(); + + let session_index = key_owner_proof.session(); + + let idx1 = block_num1 + .saturating_sub(Offset::get()) + .checked_div(&Period::get()) + .unwrap_or(Zero::zero()); + let idx2 = block_num2 + .saturating_sub(Offset::get()) + .checked_div(&Period::get()) + .unwrap_or(Zero::zero()); + + if BlockNumberFor::::from(session_index as u32) != idx1 || idx1 != idx2 { + return Err(Error::::InvalidKeyOwnershipProof.into()) + } + + // Check the membership proof and extract the offender's id + let offender = P::check_proof((KEY_TYPE, offender), key_owner_proof) + .ok_or(Error::::InvalidKeyOwnershipProof)?; + + let offence = EquivocationOffence { slot, session_index, validator_set_count, offender }; + R::report_offence(reporter.into_iter().collect(), offence) + .map_err(|_| Error::::DuplicateOffenceReport)?; + + Ok(()) + } +} diff --git a/substrate/frame/aura/src/lib.rs b/substrate/frame/aura/src/lib.rs index b314a3601e15..c9b6cfdc030a 100644 --- a/substrate/frame/aura/src/lib.rs +++ b/substrate/frame/aura/src/lib.rs @@ -41,25 +41,37 @@ use codec::{Decode, Encode, MaxEncodedLen}; use frame_support::{ traits::{DisabledValidators, FindAuthor, Get, OnTimestampSet, OneSessionHandler}, + weights::Weight, BoundedSlice, BoundedVec, ConsensusEngineId, Parameter, }; -use log; -use sp_consensus_aura::{AuthorityIndex, ConsensusLog, Slot, AURA_ENGINE_ID}; +use frame_system::pallet_prelude::HeaderFor; +use sp_consensus_aura::{AuthorityIndex, ConsensusLog, EquivocationProof, Slot, AURA_ENGINE_ID}; use sp_runtime::{ generic::DigestItem, traits::{IsMember, Member, SaturatedConversion, Saturating, Zero}, RuntimeAppPublic, }; +use sp_session::{GetSessionNumber, GetValidatorCount}; +use sp_staking::offence::OffenceReportSystem; use sp_std::prelude::*; -pub mod migrations; mod mock; mod tests; +pub mod default_weights; +pub mod migrations; + +#[cfg(any(feature = "default-equivocation-report-system", test))] +pub mod equivocation; + pub use pallet::*; const LOG_TARGET: &str = "runtime::aura"; +pub trait WeightInfo { + fn report_equivocation(validator_count: u32) -> Weight; +} + /// A slot duration provider which infers the slot duration from the /// [`pallet_timestamp::Config::MinimumPeriod`] by multiplying it by two, to ensure /// that authors have the majority of their slot to author within. @@ -91,7 +103,12 @@ pub mod pallet { + RuntimeAppPublic + MaybeSerializeDeserialize + MaxEncodedLen; + + /// Helper for weights computations. + type WeightInfo: WeightInfo; + /// The maximum number of authorities that the pallet can hold. + #[pallet::constant] type MaxAuthorities: Get; /// A way to check whether a given validator is disabled and should not be authoring blocks. @@ -122,11 +139,36 @@ pub mod pallet { /// feature. #[cfg(feature = "experimental")] type SlotDuration: Get<::Moment>; + + /// The proof of key ownership, used for validating equivocation reports. + /// + /// The proof must include the session index and validator count of the + /// session at which the equivocation occurred. + type KeyOwnerProof: Parameter + GetSessionNumber + GetValidatorCount; + + /// Equivocation handling subsystem. + /// + /// Defines methods to check/report an offence and for submitting a transaction to report + /// an equivocation (from an offchain context). + type EquivocationReportSystem: OffenceReportSystem< + Option, + (EquivocationProof, Self::AuthorityId>, Self::KeyOwnerProof), + >; } #[pallet::pallet] pub struct Pallet(sp_std::marker::PhantomData); + #[pallet::error] + pub enum Error { + /// Equivocation proof provided as part of an equivocation report is invalid. + InvalidEquivocationProof, + /// Key ownership proof provided as part of an equivocation report is invalid. + InvalidKeyOwnershipProof, + /// Equivocation report is valid but already previously reported. + DuplicateOffenceReport, + } + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(_: BlockNumberFor) -> Weight { @@ -166,6 +208,110 @@ pub mod pallet { } } + #[pallet::call] + impl Pallet { + /// Report authority equivocation/misbehavior. + /// + /// This method will verify the equivocation proof and validate the given + /// key ownership proof against the extracted offender. If both are valid, + /// the offence will be reported. + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::report_equivocation( + key_owner_proof.validator_count(), + ))] + pub fn report_equivocation( + origin: OriginFor, + equivocation_proof: Box, T::AuthorityId>>, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResultWithPostInfo { + let reporter = ensure_signed(origin)?; + T::EquivocationReportSystem::process_evidence( + Some(reporter), + (*equivocation_proof, key_owner_proof), + )?; + // Waive the fee since the report is valid and beneficial + Ok(Pays::No.into()) + } + + /// Report authority equivocation/misbehavior. + /// + /// This method will verify the equivocation proof and validate the given + /// key ownership proof against the extracted offender. If both are valid, + /// the offence will be reported. + /// + /// This extrinsic must be called unsigned and it is expected that only + /// block authors will call it (validated in `ValidateUnsigned`), as such + /// if the block author is defined it will be defined as the equivocation + /// reporter. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::report_equivocation( + key_owner_proof.validator_count(), + ))] + pub fn report_equivocation_unsigned( + origin: OriginFor, + equivocation_proof: Box, T::AuthorityId>>, + key_owner_proof: T::KeyOwnerProof, + ) -> DispatchResultWithPostInfo { + ensure_none(origin)?; + T::EquivocationReportSystem::process_evidence( + None, + (*equivocation_proof, key_owner_proof), + )?; + Ok(Pays::No.into()) + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { + if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call + { + // Discard equivocation report not coming from the local node + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ }, + _ => { + log::warn!( + target: LOG_TARGET, + "rejecting unsigned report equivocation transaction because it is not local/in-block.", + ); + + return InvalidTransaction::Call.into() + }, + } + + // Check report validity + let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); + T::EquivocationReportSystem::check_evidence(evidence)?; + + let longevity = + >::Longevity::get(); + + ValidTransaction::with_tag_prefix("AuraEquivocation") + // We assign the maximum priority for any equivocation report. + .priority(TransactionPriority::max_value()) + // Only one equivocation report for the same offender at the same slot. + .and_provides((equivocation_proof.offender.clone(), *equivocation_proof.slot)) + .longevity(longevity) + // We don't propagate this. This can never be included on a remote node. + .propagate(false) + .build() + } else { + Err(InvalidTransaction::Call.into()) + } + } + + fn pre_dispatch(call: &Self::Call) -> Result<(), TransactionValidityError> { + if let Call::report_equivocation_unsigned { equivocation_proof, key_owner_proof } = call + { + let evidence = (*equivocation_proof.clone(), key_owner_proof.clone()); + T::EquivocationReportSystem::check_evidence(evidence) + } else { + Err(InvalidTransaction::Call.into()) + } + } + } + /// The current authority set. #[pallet::storage] #[pallet::getter(fn authorities)] @@ -306,6 +452,17 @@ impl Pallet { Ok(()) } + + /// Submits an extrinsic to report an equivocation. + /// + /// This method will create an unsigned extrinsic with a call to `report_equivocation_unsigned` + /// and will push the transaction to the pool. Only useful in an offchain context. + pub fn submit_unsigned_equivocation_report( + equivocation_proof: EquivocationProof, T::AuthorityId>, + key_owner_proof: T::KeyOwnerProof, + ) -> Option<()> { + T::EquivocationReportSystem::publish_evidence((equivocation_proof, key_owner_proof)).ok() + } } impl sp_runtime::BoundToRuntimeAppPublic for Pallet { @@ -328,21 +485,23 @@ impl OneSessionHandler for Pallet { I: Iterator, { // instant changes - if changed { - let next_authorities = validators.map(|(_, k)| k).collect::>(); - let last_authorities = Self::authorities(); - if last_authorities != next_authorities { - if next_authorities.len() as u32 > T::MaxAuthorities::get() { - log::warn!( - target: LOG_TARGET, - "next authorities list larger than {}, truncating", - T::MaxAuthorities::get(), - ); - } - let bounded = >::truncate_from(next_authorities); - Self::change_authorities(bounded); - } + if !changed { + return + } + let next_authorities = validators.map(|(_, k)| k).collect::>(); + let last_authorities = Self::authorities(); + if last_authorities == next_authorities { + return + } + if next_authorities.len() as u32 > T::MaxAuthorities::get() { + log::warn!( + target: LOG_TARGET, + "next authorities list larger than {}, truncating", + T::MaxAuthorities::get(), + ); } + let bounded = >::truncate_from(next_authorities); + Self::change_authorities(bounded); } fn on_disabled(i: u32) { diff --git a/substrate/frame/aura/src/mock.rs b/substrate/frame/aura/src/mock.rs index 39b798c2f684..ebd4a94ae92b 100644 --- a/substrate/frame/aura/src/mock.rs +++ b/substrate/frame/aura/src/mock.rs @@ -19,28 +19,47 @@ #![cfg(test)] -use crate as pallet_aura; +use crate::{self as pallet_aura, CurrentSlot}; use frame_support::{ parameter_types, - traits::{ConstU32, ConstU64, DisabledValidators}, + traits::{ConstU32, ConstU64, DisabledValidators, KeyOwnerProofSystem}, }; -use sp_consensus_aura::{ed25519::AuthorityId, AuthorityIndex}; -use sp_core::H256; -use sp_runtime::{testing::UintAuthorityId, traits::IdentityLookup, BuildStorage}; +use sp_consensus_aura::{ + digests::CompatibleDigestItem, + ed25519::{AuthorityId, AuthorityPair, AuthoritySignature}, + AuthorityIndex, EquivocationProof, Slot, +}; +use sp_core::{crypto::Pair, H256, U256}; +use sp_runtime::{ + impl_opaque_keys, + testing::{Digest, DigestItem, Header, TestXt}, + traits::{Convert, Header as _, IdentityLookup, OpaqueKeys}, + BuildStorage, +}; +use sp_staking::offence::{OffenceError, ReportOffence}; type Block = frame_system::mocking::MockBlock; const SLOT_DURATION: u64 = 2; frame_support::construct_runtime!( - pub enum Test - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, - Aura: pallet_aura::{Pallet, Storage, Config}, + pub enum Test { + System: frame_system, + Authorship: pallet_authorship, + Session: pallet_session, + Timestamp: pallet_timestamp, + Aura: pallet_aura, } ); +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type OverarchingCall = RuntimeCall; + type Extrinsic = TestXt; +} + impl frame_system::Config for Test { type BaseCallFilter = frame_support::traits::Everything; type BlockWeights = (); @@ -67,6 +86,42 @@ impl frame_system::Config for Test { type MaxConsumers = frame_support::traits::ConstU32<16>; } +impl pallet_authorship::Config for Test { + type FindAuthor = (); + type EventHandler = (); +} + +parameter_types! { + pub const Period: u32 = 10; + pub const Offset: u32 = 0; +} + +impl_opaque_keys! { + pub struct MockSessionKeys { + pub aura_authority: super::Pallet, + } +} + +pub struct ValidatorIdOf(sp_std::marker::PhantomData); + +impl Convert> for ValidatorIdOf { + fn convert(controller: T::AccountId) -> Option { + Some(controller) + } +} + +impl pallet_session::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ValidatorId = ::AccountId; + type ValidatorIdOf = ValidatorIdOf; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionManager = (); + type SessionHandler = ::KeyTypeIdProviders; + type Keys = MockSessionKeys; + type WeightInfo = (); +} + impl pallet_timestamp::Config for Test { type Moment = u64; type OnTimestampSet = Aura; @@ -97,30 +152,184 @@ impl DisabledValidators for MockDisabledValidators { } } +/// A mock offence report handler. +type IdentificationTuple = (sp_core::crypto::KeyTypeId, AuthorityId); + +type EquivocationOffence = crate::equivocation::EquivocationOffence; + +type MembershipProof = sp_session::MembershipProof; + +pub struct TestOffenceHandler; + +// This is used to persist reported offences during tests. +parameter_types! { + pub static Offences: Vec<(Vec, EquivocationOffence)> = vec![]; +} + +impl ReportOffence for TestOffenceHandler { + fn report_offence( + reporters: Vec, + offence: EquivocationOffence, + ) -> Result<(), OffenceError> { + let offences = Offences::get(); + if offences.iter().find(|item| item.1 == offence).is_some() { + return Err(OffenceError::DuplicateReport) + } + Offences::mutate(|l| l.push((reporters, offence))); + Ok(()) + } + + fn is_known_offence(_offenders: &[IdentificationTuple], _time_slot: &Slot) -> bool { + false + } +} + +pub struct TestKeyOwnerProofSystem; + +impl KeyOwnerProofSystem for TestKeyOwnerProofSystem { + type Proof = MembershipProof; + type IdentificationTuple = IdentificationTuple; + + fn prove(_key: IdentificationTuple) -> Option { + None + } + + fn check_proof( + key: IdentificationTuple, + _proof: Self::Proof, + ) -> Option { + Some(key) + } +} + impl pallet_aura::Config for Test { type AuthorityId = AuthorityId; type DisabledValidators = MockDisabledValidators; + type WeightInfo = pallet_aura::default_weights::SubstrateWeight<0>; type MaxAuthorities = ConstU32<10>; type AllowMultipleBlocksPerSlot = AllowMultipleBlocksPerSlot; - + type KeyOwnerProof = MembershipProof; + type EquivocationReportSystem = pallet_aura::equivocation::EquivocationReportSystem< + Self, + TestOffenceHandler, + TestKeyOwnerProofSystem, + sp_core::ConstU64<{ u64::MAX }>, + >; #[cfg(feature = "experimental")] type SlotDuration = ConstU64; } -fn build_ext(authorities: Vec) -> sp_io::TestExternalities { +pub fn new_test_ext_and_execute( + authorities_len: usize, + test: impl FnOnce(Vec) -> (), +) { + let (pairs, mut ext) = new_test_ext_with_pairs(authorities_len); + ext.execute_with(|| { + test(pairs); + Aura::do_try_state().expect("Storage invariants should hold") + }); +} + +pub fn new_test_ext_with_pairs( + authorities_len: usize, +) -> (Vec, sp_io::TestExternalities) { + let pairs = (0..authorities_len) + .map(|i| AuthorityPair::from_seed(&U256::from(i).into())) + .collect::>(); + + let public = pairs.iter().map(|p| p.public()).collect(); + + (pairs, new_test_ext_raw_authorities(public)) +} + +pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestExternalities { let mut storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_aura::GenesisConfig:: { - authorities: authorities.into_iter().map(|a| UintAuthorityId(a).to_public_key()).collect(), - } - .assimilate_storage(&mut storage) - .unwrap(); + + pallet_aura::GenesisConfig:: { authorities } + .assimilate_storage(&mut storage) + .unwrap(); + storage.into() } -pub fn build_ext_and_execute_test(authorities: Vec, test: impl FnOnce() -> ()) { - let mut ext = build_ext(authorities); - ext.execute_with(|| { - test(); - Aura::do_try_state().expect("Storage invariants should hold") - }); +pub fn go_to_block(n: u64, s: u64) { + use frame_support::traits::{OnFinalize, OnInitialize}; + + Aura::on_finalize(System::block_number()); + + let parent_hash = if System::block_number() > 1 { + let hdr = System::finalize(); + hdr.hash() + } else { + System::parent_hash() + }; + + let digest = make_digest(s.into(), None); + + System::reset_events(); + System::initialize(&n, &parent_hash, &digest); + + Aura::on_initialize(n); +} + +/// Slots will grow accordingly to blocks +pub fn progress_to_block(n: u64) { + let mut slot = u64::from(Aura::current_slot()) + 1; + for i in System::block_number() + 1..=n { + go_to_block(i, slot); + slot += 1; + } +} + +fn make_digest(slot: Slot, other: Option>) -> Digest { + let item = >::aura_pre_digest(slot); + let mut logs = vec![item]; + if let Some(other) = other { + logs.push(DigestItem::Other(other)); + } + Digest { logs } +} + +/// Creates an equivocation proof at the current block for current slot and block number +pub fn make_equivocation_proof( + offender_authority_pair: &AuthorityPair, +) -> (EquivocationProof, MembershipProof) { + let current_block = System::block_number(); + let current_slot = CurrentSlot::::get(); + let validator_count = Aura::authorities().len() as u32; + + let make_header = |additional_data| { + let mut header = Header::new( + current_block, + Default::default(), + Default::default(), + Default::default(), + make_digest(current_slot, additional_data), + ); + + let seal = >::aura_seal( + offender_authority_pair.sign(header.hash().as_ref()), + ); + header.digest_mut().push(seal); + + header + }; + + // Generate two different headers for `slot`. + let first_header = make_header(None); + let second_header = make_header(Some(vec![0xFF])); + + let equivocation_proof = EquivocationProof { + slot: current_slot, + offender: offender_authority_pair.public(), + first_header, + second_header, + }; + + let session = current_block as u32 / Period::get(); + + // Dummy key owner proof + let membership_proof = MembershipProof { session, trie_nodes: vec![], validator_count }; + + (equivocation_proof, membership_proof) } diff --git a/substrate/frame/aura/src/tests.rs b/substrate/frame/aura/src/tests.rs index d3ce877d3e60..5eac5963bacf 100644 --- a/substrate/frame/aura/src/tests.rs +++ b/substrate/frame/aura/src/tests.rs @@ -19,15 +19,19 @@ #![cfg(test)] -use crate::mock::{build_ext_and_execute_test, Aura, MockDisabledValidators, System}; +use crate::mock::{ + make_equivocation_proof, new_test_ext_and_execute, progress_to_block, Aura, + MockDisabledValidators, Offences, RuntimeOrigin, System, +}; use codec::Encode; -use frame_support::traits::OnInitialize; -use sp_consensus_aura::{Slot, AURA_ENGINE_ID}; +use frame_support::{pallet_prelude::Pays, traits::OnInitialize}; +use sp_consensus_aura::{Slot, AURA_ENGINE_ID, KEY_TYPE}; +use sp_core::crypto::Pair; use sp_runtime::{Digest, DigestItem}; #[test] fn initial_values() { - build_ext_and_execute_test(vec![0, 1, 2, 3], || { + new_test_ext_and_execute(4, |_| { assert_eq!(Aura::current_slot(), 0u64); assert_eq!(Aura::authorities().len(), 4); }); @@ -38,7 +42,7 @@ fn initial_values() { expected = "Validator with index 1 is disabled and should not be attempting to author blocks." )] fn disabled_validators_cannot_author_blocks() { - build_ext_and_execute_test(vec![0, 1, 2, 3], || { + new_test_ext_and_execute(4, |_| { // slot 1 should be authored by validator at index 1 let slot = Slot::from(1); let pre_digest = @@ -58,7 +62,7 @@ fn disabled_validators_cannot_author_blocks() { #[test] #[should_panic(expected = "Slot must increase")] fn pallet_requires_slot_to_increase_unless_allowed() { - build_ext_and_execute_test(vec![0, 1, 2, 3], || { + new_test_ext_and_execute(4, |_| { crate::mock::AllowMultipleBlocksPerSlot::set(false); let slot = Slot::from(1); @@ -76,7 +80,7 @@ fn pallet_requires_slot_to_increase_unless_allowed() { #[test] fn pallet_can_allow_unchanged_slot() { - build_ext_and_execute_test(vec![0, 1, 2, 3], || { + new_test_ext_and_execute(4, |_| { let slot = Slot::from(1); let pre_digest = Digest { logs: vec![DigestItem::PreRuntime(AURA_ENGINE_ID, slot.encode())] }; @@ -95,7 +99,7 @@ fn pallet_can_allow_unchanged_slot() { #[test] #[should_panic(expected = "Slot must not decrease")] fn pallet_always_rejects_decreasing_slot() { - build_ext_and_execute_test(vec![0, 1, 2, 3], || { + new_test_ext_and_execute(4, |_| { let slot = Slot::from(2); let pre_digest = Digest { logs: vec![DigestItem::PreRuntime(AURA_ENGINE_ID, slot.encode())] }; @@ -115,3 +119,73 @@ fn pallet_always_rejects_decreasing_slot() { Aura::on_initialize(43); }); } + +#[test] +fn report_equivocation_works() { + use crate::equivocation::EquivocationOffence; + use sp_runtime::DispatchError; + + new_test_ext_and_execute(4, |pairs| { + progress_to_block(3); + + let authorities = Aura::authorities(); + + // We will use the validator at index 1 as the offending authority. + let offending_validator_index = 1; + // let offending_validator_id = Session::validators()[offending_validator_index]; + let offending_authority_pair = pairs + .into_iter() + .find(|p| p.public() == authorities[offending_validator_index]) + .unwrap(); + + // Generate an equivocation proof. + let (equivocation_proof, mut key_owner_proof) = + make_equivocation_proof(&offending_authority_pair); + + // Report the equivocation + let res = Aura::report_equivocation( + RuntimeOrigin::signed(1), + Box::new(equivocation_proof.clone()), + key_owner_proof.clone(), + ) + .unwrap(); + assert_eq!(res.pays_fee, Pays::No); + + // Report duplicated equivocation + let res = Aura::report_equivocation( + RuntimeOrigin::signed(2), + Box::new(equivocation_proof.clone()), + key_owner_proof.clone(), + ) + .unwrap_err(); + assert_eq!(res.post_info.pays_fee, Pays::Yes); + let DispatchError::Module(err) = res.error else { + panic!("Unexpected error type"); + }; + assert_eq!(err.message, Some("DuplicateOffenceReport")); + + // Check reported offences content + let offences = Offences::take(); + let expected_offence = EquivocationOffence { + slot: equivocation_proof.slot, + session_index: key_owner_proof.session, + validator_set_count: key_owner_proof.validator_count, + offender: (KEY_TYPE, equivocation_proof.offender.clone()), + }; + assert_eq!(offences, vec![(vec![1], expected_offence)]); + + // Report invalid equivocation + key_owner_proof.session += 1; + let res = Aura::report_equivocation( + RuntimeOrigin::signed(2), + Box::new(equivocation_proof.clone()), + key_owner_proof.clone(), + ) + .unwrap_err(); + assert_eq!(res.post_info.pays_fee, Pays::Yes); + let DispatchError::Module(err) = res.error else { + panic!("Unexpected error type"); + }; + assert_eq!(err.message, Some("InvalidKeyOwnershipProof")); + }) +} diff --git a/substrate/frame/babe/src/equivocation.rs b/substrate/frame/babe/src/equivocation.rs index ed1df640583b..77c6c3420b0e 100644 --- a/substrate/frame/babe/src/equivocation.rs +++ b/substrate/frame/babe/src/equivocation.rs @@ -19,9 +19,11 @@ //! //! This module defines an offence type for BABE equivocations //! and some utility traits to wire together: +//! - an offence type for BABE equivocations; //! - a system for reporting offences; //! - a system for submitting unsigned transactions; //! - a way to get the current block author; +//! - a key ownership proof system to prove that a given authority was part of a session; //! //! These can be used in an offchain context in order to submit equivocation //! reporting extrinsics (from the client that's import BABE blocks). diff --git a/substrate/primitives/consensus/aura/src/lib.rs b/substrate/primitives/consensus/aura/src/lib.rs index 78409e84e93a..c4f30dc8fe64 100644 --- a/substrate/primitives/consensus/aura/src/lib.rs +++ b/substrate/primitives/consensus/aura/src/lib.rs @@ -20,12 +20,21 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Codec, Decode, Encode}; -use sp_runtime::ConsensusEngineId; +use scale_info::TypeInfo; + +use sp_application_crypto::RuntimeAppPublic; +use sp_runtime::{generic::DigestItem, traits::Header, ConsensusEngineId}; use sp_std::vec::Vec; +pub use sp_consensus_slots::{Slot, SlotDuration}; + pub mod digests; pub mod inherents; +/// Key type for AURA module. +pub const KEY_TYPE: sp_application_crypto::sp_core::crypto::KeyTypeId = + sp_application_crypto::key_types::AURA; + pub mod sr25519 { mod app_sr25519 { use sp_application_crypto::{app_crypto, key_types::AURA, sr25519}; @@ -62,8 +71,6 @@ pub mod ed25519 { pub type AuthorityId = app_ed25519::Public; } -pub use sp_consensus_slots::{Slot, SlotDuration}; - /// The `ConsensusEngineId` of AuRa. pub const AURA_ENGINE_ID: ConsensusEngineId = [b'a', b'u', b'r', b'a']; @@ -81,6 +88,63 @@ pub enum ConsensusLog { OnDisabled(AuthorityIndex), } +/// An equivocation proof for multiple block authorships on the same slot. +pub type EquivocationProof = sp_consensus_slots::EquivocationProof; + +/// Opaque type representing the key ownership proof. +/// +/// The inner value is an encoded representation of the actual key ownership +/// proof which will be parameterized when defining the runtime. +/// Outside the runtime boundary this type is unknown and as such we keep this +/// opaque representation. +#[derive(Decode, Encode, PartialEq, TypeInfo)] +pub struct OpaqueKeyOwnershipProof(pub Vec); + +/// Verifies an equivocation proof. +/// +/// Makes sure that both headers have different hashes, are targetting the same slot, +/// and have valid signatures by the same authority. +pub fn check_equivocation_proof( + proof: EquivocationProof, +) -> bool { + use digests::CompatibleDigestItem; + + let find_pre_digest = |header: &H| { + header.digest().logs().iter().find_map(|log| { + >::as_aura_pre_digest(log) + }) + }; + + let verify_signature = |mut header: H, offender: &AuthorityId| { + let seal = header.digest_mut().pop()?.as_aura_seal()?; + let pre_hash = header.hash(); + offender.verify(&pre_hash.as_ref(), &seal).then(|| ()) + }; + + let verify_proof = || { + // We must have different headers for the equivocation to be valid. + if proof.first_header.hash() == proof.second_header.hash() { + return None + } + + let first_slot = find_pre_digest(&proof.first_header)?; + let second_slot = find_pre_digest(&proof.second_header)?; + + // Both headers must target the slot in the proof. + if proof.slot != first_slot || first_slot != second_slot { + return None + } + + // Finally verify that the authority has signed both headers. + verify_signature(proof.first_header, &proof.offender)?; + verify_signature(proof.second_header, &proof.offender)?; + + Some(()) + }; + + verify_proof().is_some() +} + sp_api::decl_runtime_apis! { /// API necessary for block authorship with aura. pub trait AuraApi { @@ -91,5 +155,93 @@ sp_api::decl_runtime_apis! { /// Return the current set of authorities. fn authorities() -> Vec; + + /// Generates a proof of key ownership for the given authority in the + /// current epoch. + /// + /// An example usage of this module is coupled with the session historical + /// module to prove that a given authority key is tied to a given staking + /// identity during a specific session. Proofs of key ownership are necessary + /// for submitting equivocation reports. + fn generate_key_ownership_proof( + slot: Slot, + authority_id: AuthorityId, + ) -> Option; + + /// Submits an unsigned extrinsic to report an equivocation. + /// + /// The caller must provide the equivocation proof and a key ownership + /// proof (should be obtained using `generate_key_ownership_proof`). + /// The extrinsic will be unsigned and should only be accepted for local + /// authorship (not to be broadcast to the network). This method returns + /// `None` when creation of the extrinsic fails, e.g. if equivocation + /// reporting is disabled for the given runtime (i.e. this method is + /// hardcoded to return `None`). Only useful in an offchain context. + fn submit_report_equivocation_unsigned_extrinsic( + equivocation_proof: EquivocationProof, + key_owner_proof: OpaqueKeyOwnershipProof, + ) -> Option<()>; + } +} + +#[cfg(test)] +mod tests { + use super::{ + check_equivocation_proof, + digests::CompatibleDigestItem, + ed25519::{AuthorityId, AuthorityPair, AuthoritySignature}, + }; + use sp_application_crypto::sp_core::crypto::Pair; + use sp_runtime::{testing::Header, traits::Header as _, DigestItem}; + + type EquivocationProof = super::EquivocationProof; + + #[test] + fn check_equivocation_proof_works() { + let first_header = Header::new( + 3u64, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ); + let second_header = Header::new( + 999u64, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + ); + let slot = 7.into(); + let pair = AuthorityPair::generate().0; + let offender = pair.public(); + + let mut proof = EquivocationProof { offender, slot, first_header, second_header }; + + let pre_digest = + >::aura_pre_digest(slot); + + assert!(!check_equivocation_proof(proof.clone())); + + proof.first_header.digest_mut().push(pre_digest.clone()); + assert!(!check_equivocation_proof(proof.clone())); + + proof.second_header.digest_mut().push(pre_digest); + assert!(!check_equivocation_proof(proof.clone())); + + let push_seal = |header: &mut Header| { + let sig = pair.sign(header.hash().as_bytes()); + let seal = >::aura_seal(sig); + header.digest_mut().push(seal); + }; + + push_seal(&mut proof.first_header); + assert!(!check_equivocation_proof(proof.clone())); + + push_seal(&mut proof.second_header); + assert!(check_equivocation_proof(proof.clone())); + + proof.slot += 1.into(); + assert!(!check_equivocation_proof(proof.clone())); } } diff --git a/substrate/primitives/consensus/babe/src/lib.rs b/substrate/primitives/consensus/babe/src/lib.rs index c083bfd9a313..33e1c52387d0 100644 --- a/substrate/primitives/consensus/babe/src/lib.rs +++ b/substrate/primitives/consensus/babe/src/lib.rs @@ -256,9 +256,10 @@ pub struct BabeEpochConfiguration { pub allowed_slots: AllowedSlots, } -/// Verifies the equivocation proof by making sure that: both headers have -/// different hashes, are targetting the same slot, and have valid signatures by -/// the same authority. +/// Verifies the equivocation proof. +/// +/// Makes sure that both headers have different hashes, are targetting the same slot, +/// and have valid signatures by the same authority. pub fn check_equivocation_proof(proof: EquivocationProof) -> bool where H: Header, diff --git a/substrate/test-utils/runtime/src/lib.rs b/substrate/test-utils/runtime/src/lib.rs index 687790f2ffaf..1a2b624a14b4 100644 --- a/substrate/test-utils/runtime/src/lib.rs +++ b/substrate/test-utils/runtime/src/lib.rs @@ -626,6 +626,23 @@ impl_runtime_apis! { fn authorities() -> Vec { SubstrateTest::authorities().into_iter().map(|auth| AuraId::from(auth)).collect() } + + fn submit_report_equivocation_unsigned_extrinsic( + _equivocation_proof: sp_consensus_aura::EquivocationProof< + ::Header, + AuraId, + >, + _key_owner_proof: sp_consensus_aura::OpaqueKeyOwnershipProof, + ) -> Option<()> { + None + } + + fn generate_key_ownership_proof( + _slot: sp_consensus_aura::Slot, + _authority_id: AuraId, + ) -> Option { + None + } } impl sp_consensus_babe::BabeApi for Runtime {