diff --git a/Cargo.lock b/Cargo.lock index 752a8a421..d52a2f203 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -192,7 +192,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1371,9 +1371,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec 0.7.6", @@ -2001,7 +2001,7 @@ checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" dependencies = [ "serde", "termcolor", - "unicode-width 0.2.2", + "unicode-width 0.1.14", ] [[package]] @@ -2016,7 +2016,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -3356,7 +3356,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3824,7 +3824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5605,7 +5605,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2 0.6.3", "system-configuration 0.7.0", "tokio", "tower-service", @@ -5640,7 +5640,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.57.0", ] [[package]] @@ -6038,7 +6038,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7829,7 +7829,6 @@ dependencies = [ "sc-keystore", "sc-network", "sc-offchain", - "sc-partner-chains-consensus-aura", "sc-rpc", "sc-rpc-api", "sc-service", @@ -7863,7 +7862,7 @@ dependencies = [ "sp-keystore", "sp-mmr-primitives", "sp-partner-chains-bridge", - "sp-partner-chains-consensus-aura", + "sp-partner-chains-consensus", "sp-runtime", "sp-session-validator-management", "sp-session-validator-management-query", @@ -9667,7 +9666,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" dependencies = [ "libc", - "windows-sys 0.45.0", + "windows-sys 0.61.2", ] [[package]] @@ -10981,7 +10980,6 @@ dependencies = [ "sc-consensus-grandpa-rpc", "sc-executor", "sc-network", - "sc-partner-chains-consensus-aura", "sc-rpc", "sc-service", "sc-telemetry", @@ -10999,7 +10997,7 @@ dependencies = [ "sp-inherents", "sp-io", "sp-partner-chains-bridge", - "sp-partner-chains-consensus-aura", + "sp-partner-chains-consensus", "sp-runtime", "sp-session-validator-management", "sp-session-validator-management-query", @@ -12451,7 +12449,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls", - "socket2 0.5.10", + "socket2 0.6.3", "thiserror 2.0.18", "tokio", "tracing", @@ -12489,7 +12487,7 @@ dependencies = [ "cfg_aliases 0.2.1", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.3", "tracing", "windows-sys 0.60.2", ] @@ -13327,7 +13325,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -14359,43 +14357,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "sc-partner-chains-consensus-aura" -version = "1.8.1" -dependencies = [ - "async-trait", - "futures", - "log", - "parity-scale-codec", - "parking_lot 0.12.5", - "sc-block-builder", - "sc-client-api", - "sc-consensus", - "sc-consensus-aura", - "sc-consensus-slots", - "sc-keystore", - "sc-network-test", - "sc-telemetry", - "sp-api", - "sp-application-crypto", - "sp-block-builder", - "sp-blockchain", - "sp-consensus", - "sp-consensus-aura", - "sp-consensus-slots", - "sp-core", - "sp-inherents", - "sp-keyring", - "sp-keystore", - "sp-partner-chains-consensus-aura", - "sp-runtime", - "sp-timestamp", - "sp-tracing", - "substrate-test-runtime-client", - "tempfile", - "tokio", -] - [[package]] name = "sc-proposer-metrics" version = "0.20.0" @@ -15667,7 +15628,7 @@ dependencies = [ "sp-consensus", "sp-consensus-slots", "sp-inherents", - "sp-partner-chains-consensus-aura", + "sp-partner-chains-consensus", "sp-runtime", "sp-timestamp", "thiserror 2.0.18", @@ -16113,7 +16074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -16618,15 +16579,25 @@ dependencies = [ ] [[package]] -name = "sp-partner-chains-consensus-aura" +name = "sp-partner-chains-consensus" version = "1.8.1" dependencies = [ + "async-trait", "futures", + "log", + "parity-scale-codec", + "sc-consensus", + "sc-network-test", + "sp-api", + "sp-block-builder", "sp-blockchain", "sp-consensus", "sp-consensus-slots", "sp-inherents", "sp-runtime", + "sp-tracing", + "substrate-test-runtime-client", + "tokio", ] [[package]] @@ -17239,7 +17210,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -18264,7 +18235,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -18283,7 +18254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix 1.1.4", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -20379,7 +20350,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index dfe459d54..0ac08e866 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,6 +34,7 @@ members = [ "partner-chains/demo/node", "partner-chains/toolkit/committee-selection/selection-simulator", "partner-chains/toolkit/data-sources/cli", + "partner-chains/toolkit/consensus", ] [workspace.package] license-file = "LICENSE" @@ -159,8 +160,7 @@ sp-session-validator-management = { default-features = false, path = "partner-ch sp-session-validator-management-query = { default-features = false, path = "partner-chains/toolkit/committee-selection/query" } time-source = { default-features = false, path = "partner-chains/toolkit/utils/time-source" } pallet-partner-chains-session = { default-features = false, path = "partner-chains/substrate-extensions/partner-chains-session", features = ["pallet-session-compat"] } -sp-partner-chains-consensus-aura = { default-features = false, path = "partner-chains/substrate-extensions/aura/primitives" } -sc-partner-chains-consensus-aura = { default-features = false, path = "partner-chains/substrate-extensions/aura/consensus" } +sp-partner-chains-consensus = { path = "partner-chains/toolkit/consensus" } # Substrate dependencies binary-merkle-tree = { default-features = false, git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-stable2603" } diff --git a/changes/node/changed/remove-aura-fork.md b/changes/node/changed/remove-aura-fork.md new file mode 100644 index 000000000..a2d3dcda7 --- /dev/null +++ b/changes/node/changed/remove-aura-fork.md @@ -0,0 +1,14 @@ +#node #partner-chains +# Replace the forked consensus crates with consensus-agnostic wrappers + +Remove `sc-partner-chains-consensus-aura` and `sp-partner-chains-consensus-aura`. +Add consensus-agnostic `sp-partner-chains-consensus`, which runs the full Partner Chains +inherent check at whichever point the consensus gadget checks inherents: +`PartnerChainsBlockImport` (with `PartnerChainsBodyRestore`) wraps a consensus block import +for stacks that check inherents at import (e.g. `BabeBlockImport`), and +`PartnerChainsVerifier` wraps the import-queue verifier for stacks that check inherents +there instead (e.g. Aura). `PartnerChainsProposerFactory` injects the `mcsh` digest at +proposal time. + +PR: https://github.com/midnightntwrk/midnight-node/pull/1700 +Issue: diff --git a/node/Cargo.toml b/node/Cargo.toml index 0f9a52fa0..291e7dc49 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -123,8 +123,7 @@ pallet-sidechain-rpc.workspace = true sidechain-slots.workspace = true sidechain-domain = { workspace = true, features = ["serde"] } sidechain-mc-hash = { workspace = true, features = ["mock"] } -sp-partner-chains-consensus-aura.workspace = true -sc-partner-chains-consensus-aura.workspace = true +sp-partner-chains-consensus.workspace = true sp-session-validator-management-query.workspace = true partner-chains-node-commands.workspace = true partner-chains-cli.workspace = true diff --git a/node/src/inherent_data.rs b/node/src/inherent_data.rs index 31c1a9831..8e30b1628 100644 --- a/node/src/inherent_data.rs +++ b/node/src/inherent_data.rs @@ -39,7 +39,6 @@ use sp_inherents::CreateInherentDataProviders; use sp_partner_chains_bridge::{ TokenBridgeDataSource, TokenBridgeIDPRuntimeApi, TokenBridgeInherentDataProvider, }; -use sp_partner_chains_consensus_aura::CurrentSlotProvider; use sp_runtime::traits::{Block as BlockT, Header, Zero}; use sp_session_validator_management::SessionValidatorManagementApi; use sp_timestamp::Timestamp; @@ -212,12 +211,6 @@ pub struct VerifierCIDP { bridge_data_source: Arc + Send + Sync>, } -impl CurrentSlotProvider for VerifierCIDP { - fn slot(&self) -> Slot { - *timestamp_and_slot_cidp(self.config.slot_duration(), self.config.time_source.clone()).0 - } -} - #[async_trait] impl CreateInherentDataProviders for VerifierCIDP where diff --git a/node/src/service.rs b/node/src/service.rs index e33ff0b8e..120ba57d9 100644 --- a/node/src/service.rs +++ b/node/src/service.rs @@ -33,11 +33,10 @@ use midnight_primitives_mainchain_follower::MidnightDataSourceMetrics; use parity_scale_codec::{Decode, Encode}; use partner_chains_db_sync_data_sources::register_metrics_warn_errors; use sc_client_api::{Backend, BlockImportOperation, ExecutorProvider}; -use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams}; +use sc_consensus_aura::{SlotProportion, StartAuraParams}; use sc_consensus_grandpa::SharedVoterState; use sc_consensus_slots::BackoffAuthoringOnFinalizedHeadLagging; use sc_executor::RuntimeVersionOf; -use sc_partner_chains_consensus_aura::import_queue as partner_chains_aura_import_queue; use sc_service::DatabaseSource; use sc_service::{ BuildGenesisBlock, Configuration, TaskManager, WarpSyncConfig, error::Error as ServiceError, @@ -54,7 +53,7 @@ use crate::reference_hardware::MIDNIGHT_REFERENCE_HARDWARE; use mmr_gadget::MmrGadget; use sc_rpc::SubscriptionTaskExecutor; use sp_core::storage::Storage; -use sp_partner_chains_consensus_aura::block_proposal::PartnerChainsProposerFactory; +use sp_partner_chains_consensus::PartnerChainsProposerFactory; use sp_runtime::traits::{Block as BlockT, Hash as HashT, HashingFor, Header as HeaderT, Zero}; use sp_runtime::{Digest, DigestItem}; use std::{ @@ -65,6 +64,19 @@ use std::{ }; use time_source::SystemTimeSource; +/// [`sp_partner_chains_consensus::SlotExtractor`] reading the slot from the Aura +/// pre-runtime digest. +pub struct AuraSlotExtractor; + +impl sp_partner_chains_consensus::SlotExtractor for AuraSlotExtractor { + fn extract_slot(header: &::Header) -> Result { + sc_consensus_aura::find_pre_digest::::Signature>( + header, + ) + .map_err(|e| e.to_string()) + } +} + pub struct StorageInit { pub separation: StorageSeparation, /// Used only when separation == 'separate' @@ -229,6 +241,9 @@ const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; type TransactionPool = FilteringTransactionPool; +type GrandpaBlockImport = + sc_consensus_grandpa::GrandpaBlockImport; + type MidnightService = sc_service::PartialComponents< FullClient, FullBackend, @@ -236,7 +251,7 @@ type MidnightService = sc_service::PartialComponents< sc_consensus::DefaultImportQueue, TransactionPool, ( - sc_consensus_grandpa::GrandpaBlockImport, + GrandpaBlockImport, sc_consensus_grandpa::LinkHalf, sc_consensus_beefy::BeefyVoterLinks, sc_consensus_beefy::BeefyRPCLinks, @@ -420,19 +435,36 @@ pub fn new_partial( let time_source = Arc::new(SystemTimeSource); let inherent_config = CreateInherentDataConfig::new(epoch_config, sc_slot_config, time_source); - let import_queue = partner_chains_aura_import_queue::import_queue::< - AuraPair, - _, + let slot_duration = sc_consensus_aura::slot_duration(&*client)?; + + let aura_verifier = sc_consensus_aura::build_verifier::( + sc_consensus_aura::BuildVerifierParams { + client: client.clone(), + create_inherent_data_providers: move |_parent_hash, ()| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( + *timestamp, + slot_duration, + ); + Ok((slot, timestamp)) + }, + check_for_equivocation: Default::default(), + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + }, + ); + + let verifier = sp_partner_chains_consensus::PartnerChainsVerifier::< _, _, _, _, + AuraSlotExtractor, McHashInherentDigest, - >(ImportQueueParams { - block_import: grandpa_block_import.clone(), - justification_import: Some(Box::new(grandpa_block_import.clone())), - client: client.clone(), - create_inherent_data_providers: VerifierCIDP::new( + >::new( + aura_verifier, + client.clone(), + VerifierCIDP::new( inherent_config, client.clone(), data_sources.mc_hash.clone(), @@ -441,12 +473,15 @@ pub fn new_partial( data_sources.federated_authority_observation.clone(), data_sources.bridge.clone(), ), - spawner: &task_manager.spawn_essential_handle(), - registry: config.prometheus_registry(), - check_for_equivocation: Default::default(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; + ); + + let import_queue = sc_consensus::import_queue::BasicQueue::new( + verifier, + Box::new(grandpa_block_import.clone()), + Some(Box::new(grandpa_block_import.clone())), + &task_manager.spawn_essential_handle(), + config.prometheus_registry(), + ); let partial_components = sc_service::PartialComponents { client: client.clone(), @@ -718,44 +753,33 @@ pub async fn new_full(StartAuraParams { - slot_duration: sc_slot_config.slot_duration, - client: client.clone(), - select_chain, - block_import, - proposer_factory, - create_inherent_data_providers: ProposalCIDP::new( - inherent_config, - client.clone(), - data_sources.mc_hash.clone(), - data_sources.authority_selection.clone(), - data_sources.cnight_observation.clone(), - data_sources.federated_authority_observation.clone(), - data_sources.bridge.clone(), - ), - force_authoring, - backoff_authoring_blocks, - keystore: keystore_container.keystore(), - sync_oracle: sync_service.clone(), - justification_sync_link: sync_service.clone(), - block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), - max_block_proposal_slot_portion: None, - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; + let aura = sc_consensus_aura::start_aura::( + StartAuraParams { + slot_duration: sc_slot_config.slot_duration, + client: client.clone(), + select_chain, + block_import, + proposer_factory, + create_inherent_data_providers: ProposalCIDP::new( + inherent_config, + client.clone(), + data_sources.mc_hash.clone(), + data_sources.authority_selection.clone(), + data_sources.cnight_observation.clone(), + data_sources.federated_authority_observation.clone(), + data_sources.bridge.clone(), + ), + force_authoring, + backoff_authoring_blocks, + keystore: keystore_container.keystore(), + sync_oracle: sync_service.clone(), + justification_sync_link: sync_service.clone(), + block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), + max_block_proposal_slot_portion: None, + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + }, + )?; // the AURA authoring task is considered essential, i.e. if it // fails we take down the service with it. diff --git a/partner-chains/changelog.md b/partner-chains/changelog.md index cb7814192..ef7410d68 100644 --- a/partner-chains/changelog.md +++ b/partner-chains/changelog.md @@ -7,9 +7,16 @@ This changelog is based on [Keep A Changelog](https://keepachangelog.com/en/1.1. ## Changed * Updated polkadot-sdk dependency to polkadot-stable2603. +* Replaced forked consensus crates with consensus-agnostic `sp-partner-chains-consensus`. + It runs the Partner Chains inherent check at block import (`PartnerChainsBlockImport` + with `PartnerChainsBodyRestore`, e.g. for BABE) or in the import-queue verifier + (`PartnerChainsVerifier`, e.g. for Aura), and injects the `mcsh` digest at proposal time + via `PartnerChainsProposerFactory`. ## Removed +* `sc-partner-chains-consensus-aura` and `sp-partner-chains-consensus-aura`. + ## Fixed ## Added diff --git a/partner-chains/demo/node/Cargo.toml b/partner-chains/demo/node/Cargo.toml index f059649a8..cabcb2579 100644 --- a/partner-chains/demo/node/Cargo.toml +++ b/partner-chains/demo/node/Cargo.toml @@ -31,9 +31,8 @@ sc-telemetry = { workspace = true } sc-transaction-pool = { workspace = true } sc-transaction-pool-api = { workspace = true } sc-consensus-aura = { workspace = true } -sc-partner-chains-consensus-aura = { workspace = true } sp-consensus-aura = { workspace = true } -sp-partner-chains-consensus-aura = { workspace = true } +sp-partner-chains-consensus = { workspace = true } sc-consensus = { workspace = true } sc-consensus-grandpa = { workspace = true } sc-consensus-grandpa-rpc = { workspace = true } diff --git a/partner-chains/demo/node/src/inherent_data.rs b/partner-chains/demo/node/src/inherent_data.rs index a24a04d1b..95838819e 100644 --- a/partner-chains/demo/node/src/inherent_data.rs +++ b/partner-chains/demo/node/src/inherent_data.rs @@ -23,7 +23,6 @@ use sp_inherents::CreateInherentDataProviders; use sp_partner_chains_bridge::{ TokenBridgeDataSource, TokenBridgeIDPRuntimeApi, TokenBridgeInherentDataProvider, }; -use sp_partner_chains_consensus_aura::CurrentSlotProvider; use sp_runtime::traits::{Block as BlockT, Header, Zero}; use sp_session_validator_management::SessionValidatorManagementApi; use sp_timestamp::{InherentDataProvider as TimestampIDP, Timestamp}; @@ -112,12 +111,6 @@ pub struct VerifierCIDP { bridge_data_source: Arc + Send + Sync>, } -impl CurrentSlotProvider for VerifierCIDP { - fn slot(&self) -> Slot { - *timestamp_and_slot_cidp(self.config.slot_duration(), self.config.time_source.clone()).0 - } -} - #[async_trait] impl CreateInherentDataProviders for VerifierCIDP where diff --git a/partner-chains/demo/node/src/service.rs b/partner-chains/demo/node/src/service.rs index 729ddb17b..347c4db93 100644 --- a/partner-chains/demo/node/src/service.rs +++ b/partner-chains/demo/node/src/service.rs @@ -8,17 +8,16 @@ use partner_chains_db_sync_data_sources::McFollowerMetrics; use partner_chains_db_sync_data_sources::register_metrics_warn_errors; use partner_chains_demo_runtime::{self, RuntimeApi, opaque::Block}; use sc_client_api::BlockBackend; -use sc_consensus_aura::{ImportQueueParams, SlotProportion, StartAuraParams}; +use sc_consensus_aura::{SlotProportion, StartAuraParams}; use sc_consensus_grandpa::{GrandpaPruningFilter, SharedVoterState}; pub use sc_executor::WasmExecutor; -use sc_partner_chains_consensus_aura::import_queue as partner_chains_aura_import_queue; use sc_service::{Configuration, TaskManager, WarpSyncConfig, error::Error as ServiceError}; use sc_telemetry::{Telemetry, TelemetryWorker}; use sc_transaction_pool_api::OffchainTransactionPoolFactory; use sidechain_domain::mainchain_epoch::MainchainEpochConfig; use sidechain_mc_hash::McHashInherentDigest; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; -use sp_partner_chains_consensus_aura::block_proposal::PartnerChainsProposerFactory; +use sp_partner_chains_consensus::PartnerChainsProposerFactory; use sp_runtime::traits::Block as BlockT; use std::{sync::Arc, time::Duration}; use time_source::SystemTimeSource; @@ -35,6 +34,19 @@ type FullSelectChain = sc_consensus::LongestChain; /// imported and generated. const GRANDPA_JUSTIFICATION_PERIOD: u32 = 512; +/// [`sp_partner_chains_consensus::SlotExtractor`] reading the slot from the Aura +/// pre-runtime digest. +pub struct AuraSlotExtractor; + +impl sp_partner_chains_consensus::SlotExtractor for AuraSlotExtractor { + fn extract_slot(header: &::Header) -> Result { + sc_consensus_aura::find_pre_digest::::Signature>( + header, + ) + .map_err(|e| e.to_string()) + } +} + /// This function provides dependencies of [partner_chains_node_commands::PartnerChainsSubcommand]. /// It is not mandatory to have such a dedicated function, [new_partial] could be enough, /// however using such a specialized function decreases number of possible failures and wiring time. @@ -148,31 +160,51 @@ pub fn new_partial( .map_err(|err| ServiceError::Application(err.into()))?; let inherent_config = CreateInherentDataConfig::new(epoch_config, sc_slot_config, time_source); - let import_queue = partner_chains_aura_import_queue::import_queue::< - AuraPair, - _, + let slot_duration = sc_consensus_aura::slot_duration(&*client)?; + + let aura_verifier = sc_consensus_aura::build_verifier::( + sc_consensus_aura::BuildVerifierParams { + client: client.clone(), + create_inherent_data_providers: move |_parent_hash, ()| async move { + let timestamp = sp_timestamp::InherentDataProvider::from_system_time(); + let slot = sp_consensus_aura::inherents::InherentDataProvider::from_timestamp_and_slot_duration( + *timestamp, + slot_duration, + ); + Ok((slot, timestamp)) + }, + check_for_equivocation: Default::default(), + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + }, + ); + + let verifier = sp_partner_chains_consensus::PartnerChainsVerifier::< _, _, _, _, + AuraSlotExtractor, McHashInherentDigest, - >(ImportQueueParams { - block_import: grandpa_block_import.clone(), - justification_import: Some(Box::new(grandpa_block_import.clone())), - client: client.clone(), - create_inherent_data_providers: VerifierCIDP::new( + >::new( + aura_verifier, + client.clone(), + VerifierCIDP::new( inherent_config, client.clone(), data_sources.mc_hash.clone(), data_sources.authority_selection.clone(), data_sources.bridge.clone(), ), - spawner: &task_manager.spawn_essential_handle(), - registry: config.prometheus_registry(), - check_for_equivocation: Default::default(), - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; + ); + + let import_queue = sc_consensus::import_queue::BasicQueue::new( + verifier, + Box::new(grandpa_block_import.clone()), + Some(Box::new(grandpa_block_import.clone())), + &task_manager.spawn_essential_handle(), + config.prometheus_registry(), + ); Ok(sc_service::PartialComponents { client, @@ -330,42 +362,31 @@ pub async fn new_full_base(StartAuraParams { - slot_duration: sc_slot_config.slot_duration, - client: client.clone(), - select_chain, - block_import, - proposer_factory, - create_inherent_data_providers: ProposalCIDP::new( - inherent_config, - client.clone(), - data_sources.mc_hash.clone(), - data_sources.authority_selection.clone(), - data_sources.bridge.clone(), - ), - force_authoring, - backoff_authoring_blocks, - keystore: keystore_container.keystore(), - sync_oracle: sync_service.clone(), - justification_sync_link: sync_service.clone(), - block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), - max_block_proposal_slot_portion: None, - telemetry: telemetry.as_ref().map(|x| x.handle()), - compatibility_mode: Default::default(), - })?; + let aura = sc_consensus_aura::start_aura::( + StartAuraParams { + slot_duration: sc_slot_config.slot_duration, + client: client.clone(), + select_chain, + block_import, + proposer_factory, + create_inherent_data_providers: ProposalCIDP::new( + inherent_config, + client.clone(), + data_sources.mc_hash.clone(), + data_sources.authority_selection.clone(), + data_sources.bridge.clone(), + ), + force_authoring, + backoff_authoring_blocks, + keystore: keystore_container.keystore(), + sync_oracle: sync_service.clone(), + justification_sync_link: sync_service.clone(), + block_proposal_slot_portion: SlotProportion::new(2f32 / 3f32), + max_block_proposal_slot_portion: None, + telemetry: telemetry.as_ref().map(|x| x.handle()), + compatibility_mode: Default::default(), + }, + )?; // the AURA authoring task is considered essential, i.e. if it // fails we take down the service with it. diff --git a/partner-chains/substrate-extensions/aura/consensus/Cargo.toml b/partner-chains/substrate-extensions/aura/consensus/Cargo.toml deleted file mode 100644 index 5fd95b305..000000000 --- a/partner-chains/substrate-extensions/aura/consensus/Cargo.toml +++ /dev/null @@ -1,57 +0,0 @@ -[package] -name = "sc-partner-chains-consensus-aura" -version = "1.8.1" -description = "Partner Chains modification to the Substrate Aura consensus engine" -authors.workspace = true -homepage.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later WITH Classpath-exception-2.0" -repository.workspace = true -readme = "README.md" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true - -[dependencies] -async-trait = { workspace = true } -parity-scale-codec = { workspace = true, default-features = true } -futures = { workspace = true } -log = { workspace = true, default-features = true } -parking_lot = { workspace = true, default-features = true } -sc-client-api = { workspace = true, default-features = true } -sc-consensus = { workspace = true, default-features = true } -sc-consensus-aura = { workspace = true, default-features = true } -sc-consensus-slots = { workspace = true, default-features = true } -sc-telemetry = { workspace = true, default-features = true } -sp-api = { workspace = true, default-features = true } -sp-application-crypto = { workspace = true, default-features = true } -sp-blockchain = { workspace = true, default-features = true } -sp-block-builder = { workspace = true, default-features = true } -sp-consensus = { workspace = true, default-features = true } -sp-consensus-aura = { workspace = true, default-features = true } -sp-consensus-slots = { workspace = true, default-features = true } -sp-core = { workspace = true, default-features = true } -sp-inherents = { workspace = true, default-features = true } -sp-keystore = { workspace = true, default-features = true } -sp-runtime = { workspace = true, default-features = true } -sp-partner-chains-consensus-aura = { workspace = true, default-features = true } -#thiserror = { workspace = true } - -[dev-dependencies] -parking_lot = { workspace = true, default-features = true } -tempfile = { workspace = true } -sc-block-builder = { workspace = true, default-features = true } -sc-keystore = { workspace = true, default-features = true } -#sc-network = { workspace = true, default-features = true } -sc-network-test = { workspace = true } -sp-keyring = { workspace = true, default-features = true } -sp-timestamp = { workspace = true, default-features = true } -sp-tracing = { workspace = true, default-features = true } -substrate-test-runtime-client = { workspace = true } -tokio = { workspace = true, default-features = true } - -[lib] -doctest = false diff --git a/partner-chains/substrate-extensions/aura/consensus/README.md b/partner-chains/substrate-extensions/aura/consensus/README.md deleted file mode 100644 index dd4f45bbd..000000000 --- a/partner-chains/substrate-extensions/aura/consensus/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Partner Chains Aura Workers - -This crate is base on the [Substrate Aura Consensus crate](https://github.com/paritytech/polkadot-sdk/tree/polkadot-stable2407/substrate/client/consensus/aura). -It is customized for Partner Chains needs: -* during block verification it uses block slot to call Partner Chains InherentDataProvider -* verifies that the given block header has proper InherentDigest (digest of data from Partner Chains InherentDataProvider). - -Please note that it requires usage of custom `Proposer` that comes in `sp-partner-chains-consensus-aura` crate. -See `service.rs` in the `node` crate to see how to use it. - -License: GPL-3.0-or-later WITH Classpath-exception-2.0 diff --git a/partner-chains/substrate-extensions/aura/consensus/src/import_queue.rs b/partner-chains/substrate-extensions/aura/consensus/src/import_queue.rs deleted file mode 100644 index f7a5cb3da..000000000 --- a/partner-chains/substrate-extensions/aura/consensus/src/import_queue.rs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// Additional modifications by Input Output Global, Inc. -// Copyright (C) 2024, Input Output Global, Inc. - -//! Module implementing the logic for verifying and importing AuRa blocks. - -use crate::{AuthorityId, LOG_TARGET, authorities}; -use log::{debug, info, trace}; -use parity_scale_codec::Codec; -use sc_client_api::{BlockOf, UsageProvider, backend::AuxStore}; -use sc_consensus::{ - block_import::{BlockImport, BlockImportParams, ForkChoiceStrategy}, - import_queue::{BasicQueue, DefaultImportQueue, Verifier}, -}; -use sc_consensus_aura::{ - CheckForEquivocation, CompatibilityMode, Error, ImportQueueParams, - standalone::SealVerificationError, -}; -use sc_consensus_slots::{CheckedHeader, check_equivocation}; -use sc_telemetry::{CONSENSUS_DEBUG, CONSENSUS_TRACE, TelemetryHandle, telemetry}; -use sp_api::{ApiExt, ProvideRuntimeApi}; -use sp_block_builder::BlockBuilder as BlockBuilderApi; -use sp_blockchain::{HeaderBackend, HeaderMetadata}; -use sp_consensus::Error as ConsensusError; -use sp_consensus_aura::AuraApi; -use sp_consensus_slots::Slot; -use sp_core::crypto::Pair; -use sp_inherents::{CreateInherentDataProviders, InherentData, InherentDataProvider}; -use sp_partner_chains_consensus_aura::{CurrentSlotProvider, InherentDigest}; -use sp_runtime::{ - DigestItem, - traits::{Block as BlockT, Header, NumberFor}, -}; -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. -/// -/// This digest item will always return `Some` when used with `as_aura_seal`. -#[allow(clippy::type_complexity)] -fn check_header( - client: &C, - slot_now: Slot, - header: B::Header, - hash: B::Hash, - authorities: &[AuthorityId

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

(slot, authorities); - let should_equiv_check = matches!(check_for_equivocation, CheckForEquivocation::Yes); - 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))) - }, - Err(SealVerificationError::Deferred(header, slot)) => { - Ok(CheckedHeader::Deferred(header, slot)) - }, - Err(SealVerificationError::Unsealed) => Err(Error::HeaderUnsealed(hash)), - Err(SealVerificationError::BadSeal) => Err(Error::HeaderBadSeal(hash)), - Err(SealVerificationError::BadSignature) => Err(Error::BadSignature(hash)), - Err(SealVerificationError::SlotAuthorNotFound) => Err(Error::SlotAuthorNotFound), - Err(SealVerificationError::InvalidPreDigest(e)) => Err(Error::from(e)), - } -} - -/// A verifier for Aura blocks, with added ID phantom type. -pub struct AuraVerifier { - client: Arc, - create_inherent_data_providers: CIDP, - check_for_equivocation: CheckForEquivocation, - telemetry: Option, - compatibility_mode: CompatibilityMode>, - _phantom: PhantomData<(fn() -> P, ID)>, -} - -impl AuraVerifier { - pub(crate) fn new( - client: Arc, - create_inherent_data_providers: CIDP, - check_for_equivocation: CheckForEquivocation, - telemetry: Option, - compatibility_mode: CompatibilityMode>, - ) -> Self { - Self { - client: client.clone(), - create_inherent_data_providers, - check_for_equivocation, - telemetry, - compatibility_mode, - _phantom: PhantomData, - } - } -} - -#[async_trait::async_trait] -impl Verifier for AuraVerifier -where - B: BlockT, - C: HeaderBackend - + HeaderMetadata - + ProvideRuntimeApi - + Send - + Sync - + sc_client_api::backend::AuxStore, - C::Api: BlockBuilderApi + AuraApi> + ApiExt, - P: Pair, - P::Public: Codec + Debug, - P::Signature: Codec, - CIDP: CurrentSlotProvider - + CreateInherentDataProviders::Value)> - + Send - + Sync, - ID: InherentDigest + Send + Sync + 'static, -{ - async fn verify( - &self, - mut block: BlockImportParams, - ) -> Result, String> { - // Skip checks that include execution, if being told so or when importing only state. - // - // This is done for example when gap syncing and it is expected that the block after the gap - // was checked/chosen properly, e.g. by warp syncing to this block using a finality proof. - // Or when we are importing state only and can not verify the seal. - if block.with_state() || block.state_action.skip_execution_checks() { - // When we are importing only the state of a block, it will be the best block. - block.fork_choice = Some(ForkChoiceStrategy::Custom(block.with_state())); - - return Ok(block); - } - - let hash = block.header.hash(); - let parent_hash = *block.header.parent_hash(); - let authorities = authorities( - self.client.as_ref(), - parent_hash, - *block.header.number(), - &self.compatibility_mode, - ) - .map_err(|e| format!("Could not fetch authorities at {:?}: {}", parent_hash, e))?; - - let slot_now = self.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.clone(), - hash, - &authorities[..], - self.check_for_equivocation, - ) - .map_err(|e| e.to_string())?; - let inherent_digest = ::value_from_digest( - block.header.digest().logs(), - ) - .map_err(|e| { - format!("Failed to retrieve inherent digest from header at {:?}: {}", parent_hash, e) - })?; - match checked_header { - CheckedHeader::Checked(pre_header, (slot, seal)) => { - // 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() { - let new_block = B::new(pre_header.clone(), inner_body); - - let inherent_data_providers = create_inherent_data_provider( - &self.create_inherent_data_providers, - parent_hash, - (slot, inherent_digest), - ) - .await?; - - // skip the inherents verification if the runtime API is old or not expected to - // exist. - if self - .client - .runtime_api() - .has_api_with::, _>(parent_hash, |v| v >= 2) - .map_err(|e| e.to_string())? - { - let inherent_data = - create_inherent_data::(&inherent_data_providers).await?; - sp_block_builder::check_inherents_with_data( - self.client.clone(), - parent_hash, - new_block.clone(), - &inherent_data_providers, - inherent_data, - ) - .await - .map_err(|e| format!("Error checking block inherents {:?}", e))?; - } - - let (_, inner_body) = new_block.deconstruct(); - block.body = Some(inner_body); - } - - trace!(target: LOG_TARGET, "Checked {:?}; importing.", pre_header); - telemetry!( - self.telemetry; - CONSENSUS_TRACE; - "aura.checked_and_importing"; - "pre_header" => ?pre_header, - ); - - block.header = pre_header; - block.post_digests.push(seal); - block.fork_choice = Some(ForkChoiceStrategy::LongestChain); - block.post_hash = Some(hash); - - Ok(block) - }, - CheckedHeader::Deferred(a, b) => { - debug!(target: LOG_TARGET, "Checking {:?} failed; {:?}, {:?}.", hash, a, b); - telemetry!( - self.telemetry; - CONSENSUS_DEBUG; - "aura.header_too_far_in_future"; - "hash" => ?hash, - "a" => ?a, - "b" => ?b, - ); - Err(format!("Header {:?} rejected: too far in the future", hash)) - }, - } - } -} - -/// Start an import queue for the Aura consensus algorithm. -pub fn import_queue( - ImportQueueParams { - block_import, - justification_import, - client, - create_inherent_data_providers, - spawner, - registry, - check_for_equivocation, - telemetry, - compatibility_mode, - }: ImportQueueParams, -) -> Result, sp_consensus::Error> -where - Block: BlockT, - C::Api: BlockBuilderApi + AuraApi> + ApiExt, - C: 'static - + ProvideRuntimeApi - + BlockOf - + Send - + Sync - + AuxStore - + UsageProvider - + HeaderBackend - + HeaderMetadata, - I: BlockImport + Send + Sync + 'static, - P: Pair + 'static, - P::Public: Codec + Debug, - P::Signature: Codec, - S: sp_core::traits::SpawnEssentialNamed, - CIDP: CurrentSlotProvider - + CreateInherentDataProviders::Value)> - + Sync - + Send - + 'static, - ID: InherentDigest + Send + Sync + 'static, -{ - let verifier = AuraVerifier::<_, P, _, _, ID>::new( - client, - create_inherent_data_providers, - check_for_equivocation, - telemetry, - compatibility_mode, - ); - - Ok(BasicQueue::new(verifier, Box::new(block_import), justification_import, spawner, registry)) -} - -async fn create_inherent_data_provider( - cidp: &CIDP, - hash: B::Hash, - extra_arg: ExtraArg, -) -> Result -where - CIDP: CreateInherentDataProviders, -{ - cidp.create_inherent_data_providers(hash, extra_arg) - .await - .map_err(|e| Error::::Client(sp_blockchain::Error::Application(e)).into()) -} - -async fn create_inherent_data( - provider: &impl InherentDataProvider, -) -> Result> { - provider.create_inherent_data().await.map_err(Error::::Inherent) -} diff --git a/partner-chains/substrate-extensions/aura/consensus/src/lib.rs b/partner-chains/substrate-extensions/aura/consensus/src/lib.rs deleted file mode 100644 index 9fad8a838..000000000 --- a/partner-chains/substrate-extensions/aura/consensus/src/lib.rs +++ /dev/null @@ -1,703 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 - -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . - -// Additional modifications by Input Output Global, Inc. -// Copyright (C) 2024, Input Output Global, Inc. - -pub mod import_queue; - -use futures::prelude::*; -use parity_scale_codec::Codec; -use sc_client_api::{BlockOf, backend::AuxStore}; -use sc_consensus::block_import::BlockImport; -use sc_consensus::{BlockImportParams, ForkChoiceStrategy, StateAction}; -use sc_consensus_aura::{ - BuildAuraWorkerParams, CompatibilityMode, StartAuraParams, find_pre_digest, -}; -use sc_consensus_slots::{ - BackoffAuthoringBlocksStrategy, InherentDataProviderExt, SimpleSlotWorkerToSlotWorker, - SlotInfo, SlotProportion, StorageChanges, -}; -use sc_telemetry::TelemetryHandle; -use sp_api::{Core, ProvideRuntimeApi}; -use sp_application_crypto::AppPublic; -use sp_blockchain::HeaderBackend; -use sp_consensus::{ - BlockOrigin, Environment, Error as ConsensusError, Proposer, SelectChain, SyncOracle, -}; -use sp_consensus_aura::AuraApi; -use sp_consensus_slots::Slot; -use sp_core::crypto::Pair; -use sp_inherents::CreateInherentDataProviders; -use sp_keystore::KeystorePtr; -use sp_partner_chains_consensus_aura::InherentDigest; -use sp_runtime::traits::{Block as BlockT, Header, Member, NumberFor}; -use std::{fmt::Debug, marker::PhantomData, pin::Pin, sync::Arc}; - -type AuthorityId

=

::Public; - -const LOG_TARGET: &str = "aura"; - -/// Start the aura worker. The returned future should be run in a futures executor. -pub fn start_aura( - StartAuraParams { - slot_duration, - client, - select_chain, - block_import, - proposer_factory, - sync_oracle, - justification_sync_link, - create_inherent_data_providers, - force_authoring, - backoff_authoring_blocks, - keystore, - block_proposal_slot_portion, - max_block_proposal_slot_portion, - telemetry, - compatibility_mode, - }: StartAuraParams>, -) -> Result, ConsensusError> -where - P: Pair, - P::Public: AppPublic + Member, - P::Signature: TryFrom> + Member + Codec, - B: BlockT, - C: ProvideRuntimeApi + BlockOf + AuxStore + HeaderBackend + Send + Sync, - C::Api: AuraApi>, - SC: SelectChain, - I: BlockImport + Send + Sync + 'static, - PF: Environment + Send + Sync + 'static, - PF::Proposer: Proposer, - SO: SyncOracle + Send + Sync + Clone, - L: sc_consensus::JustificationSyncLink, - CIDP: CreateInherentDataProviders + Send + 'static, - CIDP::InherentDataProviders: InherentDataProviderExt + Send, - BS: BackoffAuthoringBlocksStrategy> + Send + Sync + 'static, - Error: std::error::Error + Send + From + 'static, - ID: InherentDigest + Send + Sync + 'static, -{ - let worker = build_aura_worker::(BuildAuraWorkerParams { - client, - block_import, - proposer_factory, - keystore, - sync_oracle: sync_oracle.clone(), - justification_sync_link, - force_authoring, - backoff_authoring_blocks, - telemetry, - block_proposal_slot_portion, - max_block_proposal_slot_portion, - compatibility_mode, - }); - - Ok(sc_consensus_slots::start_slot_worker( - slot_duration, - select_chain, - SimpleSlotWorkerToSlotWorker(worker), - sync_oracle, - create_inherent_data_providers, - )) -} - -/// Build the aura worker. -/// -/// The caller is responsible for running this worker, otherwise it will do nothing. -pub fn build_aura_worker( - BuildAuraWorkerParams { - client, - block_import, - proposer_factory, - sync_oracle, - justification_sync_link, - backoff_authoring_blocks, - keystore, - block_proposal_slot_portion, - max_block_proposal_slot_portion, - telemetry, - force_authoring, - compatibility_mode, - }: BuildAuraWorkerParams>, -) -> impl sc_consensus_slots::SimpleSlotWorker< - B, - Proposer = PF::Proposer, - BlockImport = I, - SyncOracle = SO, - JustificationSyncLink = L, - Claim = P::Public, - AuxData = Vec>, -> -where - B: BlockT, - C: ProvideRuntimeApi + BlockOf + AuxStore + HeaderBackend + Send + Sync, - C::Api: AuraApi>, - PF: Environment + Send + Sync + 'static, - PF::Proposer: Proposer, - P: Pair, - P::Public: AppPublic + Member, - P::Signature: TryFrom> + Member + Codec, - I: BlockImport + Send + Sync + 'static, - Error: std::error::Error + Send + From + 'static, - SO: SyncOracle + Send + Sync + Clone, - L: sc_consensus::JustificationSyncLink, - BS: BackoffAuthoringBlocksStrategy> + Send + Sync + 'static, - ID: InherentDigest + Send + Sync + 'static, -{ - AuraWorker { - client, - block_import, - env: proposer_factory, - keystore, - sync_oracle, - justification_sync_link, - force_authoring, - backoff_authoring_blocks, - telemetry, - block_proposal_slot_portion, - max_block_proposal_slot_portion, - compatibility_mode, - _phantom: PhantomData::<(fn() -> P, ID)>, - } -} - -struct AuraWorker { - client: Arc, - block_import: I, - env: E, - keystore: KeystorePtr, - sync_oracle: SO, - justification_sync_link: L, - force_authoring: bool, - backoff_authoring_blocks: Option, - block_proposal_slot_portion: SlotProportion, - max_block_proposal_slot_portion: Option, - telemetry: Option, - compatibility_mode: CompatibilityMode, - _phantom: PhantomData<(fn() -> P, ID)>, -} - -#[async_trait::async_trait] -impl sc_consensus_slots::SimpleSlotWorker - for AuraWorker, ID> -where - B: BlockT, - C: ProvideRuntimeApi + BlockOf + HeaderBackend + Sync, - C::Api: AuraApi>, - E: Environment + Send + Sync, - E::Proposer: Proposer, - I: BlockImport + Send + Sync + 'static, - P: Pair, - P::Public: AppPublic + Member, - P::Signature: TryFrom> + Member + Codec, - SO: SyncOracle + Send + Clone + Sync, - L: sc_consensus::JustificationSyncLink, - BS: BackoffAuthoringBlocksStrategy> + Send + Sync + 'static, - Error: std::error::Error + Send + From + 'static, - ID: InherentDigest + Send + Sync + 'static, -{ - type BlockImport = I; - type SyncOracle = SO; - type JustificationSyncLink = L; - type CreateProposer = - Pin> + Send + 'static>>; - type Proposer = E::Proposer; - type Claim = P::Public; - type AuxData = Vec>; - - fn logging_target(&self) -> &'static str { - "aura" - } - - fn block_import(&mut self) -> &mut Self::BlockImport { - &mut self.block_import - } - - fn aux_data(&self, header: &B::Header, _slot: Slot) -> Result { - authorities( - self.client.as_ref(), - header.hash(), - *header.number() + 1u32.into(), - &self.compatibility_mode, - ) - } - - fn authorities_len(&self, authorities: &Self::AuxData) -> Option { - Some(authorities.len()) - } - - async fn claim_slot( - &mut self, - _header: &B::Header, - slot: Slot, - authorities: &Self::AuxData, - ) -> Option { - sc_consensus_aura::standalone::claim_slot::

(slot, authorities, &self.keystore).await - } - - fn pre_digest_data(&self, slot: Slot, _claim: &Self::Claim) -> Vec { - vec![sc_consensus_aura::standalone::pre_digest::

(slot)] - } - - async fn block_import_params( - &self, - header: B::Header, - header_hash: &B::Hash, - body: Vec, - storage_changes: StorageChanges, - public: Self::Claim, - _authorities: Self::AuxData, - ) -> Result, ConsensusError> { - let signature_digest_item = - sc_consensus_aura::standalone::seal::<_, P>(header_hash, &public, &self.keystore)?; - - let mut import_block = BlockImportParams::new(BlockOrigin::Own, header); - import_block.post_digests.push(signature_digest_item); - import_block.body = Some(body); - import_block.state_action = - StateAction::ApplyChanges(sc_consensus::StorageChanges::Changes(storage_changes)); - import_block.fork_choice = Some(ForkChoiceStrategy::LongestChain); - - Ok(import_block) - } - - fn force_authoring(&self) -> bool { - self.force_authoring - } - - fn should_backoff(&self, slot: Slot, chain_head: &B::Header) -> bool { - if let Some(ref strategy) = self.backoff_authoring_blocks { - if let Ok(chain_head_slot) = find_pre_digest::(chain_head) { - return strategy.should_backoff( - *chain_head.number(), - chain_head_slot, - self.client.info().finalized_number, - slot, - self.logging_target(), - ); - } - } - false - } - - fn sync_oracle(&mut self) -> &mut Self::SyncOracle { - &mut self.sync_oracle - } - - fn justification_sync_link(&mut self) -> &mut Self::JustificationSyncLink { - &mut self.justification_sync_link - } - - fn proposer(&mut self, block: &B::Header) -> Self::CreateProposer { - self.env - .init(block) - .map_err(|e| ConsensusError::ClientImport(format!("{:?}", e))) - .boxed() - } - - fn telemetry(&self) -> Option { - self.telemetry.clone() - } - - fn proposing_remaining_duration(&self, slot_info: &SlotInfo) -> std::time::Duration { - let parent_slot = find_pre_digest::(&slot_info.chain_head).ok(); - - sc_consensus_slots::proposing_remaining_duration( - parent_slot, - slot_info, - &self.block_proposal_slot_portion, - self.max_block_proposal_slot_portion.as_ref(), - sc_consensus_slots::SlotLenienceType::Exponential, - self.logging_target(), - ) - } -} - -fn authorities( - client: &C, - parent_hash: B::Hash, - context_block_number: NumberFor, - compatibility_mode: &CompatibilityMode>, -) -> Result, ConsensusError> -where - A: Codec + Debug, - B: BlockT, - C: ProvideRuntimeApi, - C::Api: AuraApi, -{ - let runtime_api = client.runtime_api(); - - match compatibility_mode { - CompatibilityMode::None => {}, - // Use `initialize_block` until we hit the block that should disable the mode. - CompatibilityMode::UseInitializeBlock { until } => { - if *until > context_block_number { - runtime_api - .initialize_block( - parent_hash, - &B::Header::new( - context_block_number, - Default::default(), - Default::default(), - parent_hash, - Default::default(), - ), - ) - .map_err(|_| ConsensusError::InvalidAuthoritiesSet)?; - } - }, - } - - runtime_api - .authorities(parent_hash) - .ok() - .ok_or(ConsensusError::InvalidAuthoritiesSet) -} - -#[cfg(test)] -mod tests { - use super::*; - use parking_lot::Mutex; - use sc_block_builder::BlockBuilderBuilder; - use sc_client_api::BlockchainEvents; - use sc_consensus::BoxJustificationImport; - use sc_consensus_aura::{CheckForEquivocation, standalone::slot_duration}; - use sc_consensus_slots::{BackoffAuthoringOnFinalizedHeadLagging, SimpleSlotWorker}; - use sc_keystore::LocalKeystore; - use sc_network_test::{Block as TestBlock, *}; - use sp_application_crypto::{AppCrypto, key_types::AURA}; - use sp_consensus::{NoNetwork as DummyOracle, Proposal, ProposeArgs}; - use sp_consensus_aura::SlotDuration; - use sp_consensus_aura::inherents::InherentDataProvider; - use sp_consensus_aura::sr25519::AuthorityPair; - use sp_keyring::sr25519::Keyring; - use sp_keystore::Keystore; - use sp_partner_chains_consensus_aura::CurrentSlotProvider; - use sp_runtime::traits::{Block as BlockT, Header as _}; - use sp_timestamp::Timestamp; - use std::{ - task::Poll, - time::{Duration, Instant}, - }; - use substrate_test_runtime_client::{ - TestClient, - runtime::{H256, Header}, - }; - - const SLOT_DURATION_MS: u64 = 1000; - - type Error = sp_blockchain::Error; - - struct DummyFactory(Arc); - struct DummyProposer(Arc); - - impl Environment for DummyFactory { - type Proposer = DummyProposer; - type CreateProposer = futures::future::Ready>; - type Error = Error; - - fn init(&mut self, _: &::Header) -> Self::CreateProposer { - futures::future::ready(Ok(DummyProposer(self.0.clone()))) - } - } - - impl Proposer for DummyProposer { - type Error = Error; - type Proposal = future::Ready, Error>>; - - fn propose(self, args: ProposeArgs) -> Self::Proposal { - let ProposeArgs { inherent_digests: digests, .. } = args; - let r = BlockBuilderBuilder::new(&*self.0) - .on_parent_block(self.0.chain_info().best_hash) - .fetch_parent_block_number(&*self.0) - .unwrap() - .with_inherent_digests(digests) - .build() - .unwrap() - .build(); - - future::ready( - r.map(|b| Proposal { block: b.block, storage_changes: b.storage_changes }), - ) - } - } - - type AuraVerifier = - import_queue::AuraVerifier; - type AuraPeer = Peer<(), PeersClient>; - - #[derive(Default)] - pub struct AuraTestNet { - peers: Vec, - } - - pub struct TestCIDP; - - #[async_trait::async_trait] - impl CreateInherentDataProviders for TestCIDP { - type InherentDataProviders = (); - - async fn create_inherent_data_providers( - &self, - _parent: ::Hash, - _extra_args: (Slot, ()), - ) -> Result> { - Ok(()) - } - } - - impl CurrentSlotProvider for TestCIDP { - fn slot(&self) -> Slot { - Slot::from_timestamp(Timestamp::current(), SlotDuration::from_millis(SLOT_DURATION_MS)) - } - } - - impl TestNetFactory for AuraTestNet { - type Verifier = AuraVerifier; - type PeerData = (); - type BlockImport = PeersClient; - - fn make_verifier(&self, client: PeersClient, _peer_data: &()) -> Self::Verifier { - let client = client.as_client(); - let slot_duration = slot_duration(&*client).expect("slot duration available"); - - assert_eq!(slot_duration.as_millis() as u64, SLOT_DURATION_MS); - AuraVerifier::new( - client, - TestCIDP, - CheckForEquivocation::Yes, - None, - CompatibilityMode::None, - ) - } - - fn make_block_import( - &self, - client: PeersClient, - ) -> ( - BlockImportAdapter, - Option>, - Self::PeerData, - ) { - (client.as_block_import(), None, ()) - } - - fn peer(&mut self, i: usize) -> &mut AuraPeer { - &mut self.peers[i] - } - - fn peers(&self) -> &Vec { - &self.peers - } - - fn peers_mut(&mut self) -> &mut Vec { - &mut self.peers - } - - fn mut_peers)>(&mut self, closure: F) { - closure(&mut self.peers); - } - } - - #[tokio::test] - async fn authoring_blocks() { - sp_tracing::try_init_simple(); - let net = AuraTestNet::new(3); - - let peers = &[(0, Keyring::Alice), (1, Keyring::Bob), (2, Keyring::Charlie)]; - - let net = Arc::new(Mutex::new(net)); - let mut import_notifications = Vec::new(); - let mut aura_futures = Vec::new(); - - let mut keystore_paths = Vec::new(); - for (peer_id, key) in peers { - let mut net = net.lock(); - let peer = net.peer(*peer_id); - let client = peer.client().as_client(); - let select_chain = peer.select_chain().expect("full client has a select chain"); - let keystore_path = tempfile::tempdir().expect("Creates keystore path"); - let keystore = Arc::new( - LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore."), - ); - - keystore - .sr25519_generate_new(AURA, Some(&key.to_seed())) - .expect("Creates authority key"); - keystore_paths.push(keystore_path); - - let environ = DummyFactory(client.clone()); - import_notifications.push( - client - .import_notification_stream() - .take_while(|n| { - future::ready(!(n.origin != BlockOrigin::Own && n.header.number() < &5)) - }) - .for_each(move |_| future::ready(())), - ); - - let slot_duration = slot_duration(&*client).expect("slot duration available"); - - aura_futures.push( - start_aura::(StartAuraParams { - slot_duration, - block_import: client.clone(), - select_chain, - client, - proposer_factory: environ, - sync_oracle: DummyOracle, - justification_sync_link: (), - create_inherent_data_providers: |_, _| async { - let slot = InherentDataProvider::from_timestamp_and_slot_duration( - Timestamp::current(), - SlotDuration::from_millis(SLOT_DURATION_MS), - ); - - Ok((slot,)) - }, - force_authoring: false, - backoff_authoring_blocks: Some( - BackoffAuthoringOnFinalizedHeadLagging::default(), - ), - keystore, - block_proposal_slot_portion: SlotProportion::new(0.5), - max_block_proposal_slot_portion: None, - telemetry: None, - compatibility_mode: CompatibilityMode::None, - }) - .expect("Starts aura"), - ); - } - - future::select( - future::poll_fn(move |cx| { - net.lock().poll(cx); - Poll::<()>::Pending - }), - future::select(future::join_all(aura_futures), future::join_all(import_notifications)), - ) - .await; - } - - #[tokio::test] - async fn current_node_authority_should_claim_slot() { - let net = AuraTestNet::new(4); - - let mut authorities = vec![ - Keyring::Alice.public().into(), - Keyring::Bob.public().into(), - Keyring::Charlie.public().into(), - ]; - - let keystore_path = tempfile::tempdir().expect("Creates keystore path"); - let keystore = LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore."); - let public = keystore - .sr25519_generate_new(AuthorityPair::ID, None) - .expect("Key should be created"); - authorities.push(public.into()); - - let net = Arc::new(Mutex::new(net)); - - let mut net = net.lock(); - let peer = net.peer(3); - let client = peer.client().as_client(); - let environ = DummyFactory(client.clone()); - - let mut worker = AuraWorker { - client: client.clone(), - block_import: client, - env: environ, - keystore: keystore.into(), - sync_oracle: DummyOracle, - justification_sync_link: (), - force_authoring: false, - backoff_authoring_blocks: Some(BackoffAuthoringOnFinalizedHeadLagging::default()), - telemetry: None, - block_proposal_slot_portion: SlotProportion::new(0.5), - max_block_proposal_slot_portion: None, - compatibility_mode: Default::default(), - _phantom: PhantomData::<(fn() -> AuthorityPair, ())>, - }; - - let head = Header::new( - 1, - H256::from_low_u64_be(0), - H256::from_low_u64_be(0), - Default::default(), - Default::default(), - ); - assert!(worker.claim_slot(&head, 0.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 1.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 2.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 3.into(), &authorities).await.is_some()); - assert!(worker.claim_slot(&head, 4.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 5.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 6.into(), &authorities).await.is_none()); - assert!(worker.claim_slot(&head, 7.into(), &authorities).await.is_some()); - } - - #[tokio::test] - async fn on_slot_returns_correct_block() { - let net = AuraTestNet::new(4); - - let keystore_path = tempfile::tempdir().expect("Creates keystore path"); - let keystore = LocalKeystore::open(keystore_path.path(), None).expect("Creates keystore."); - keystore - .sr25519_generate_new(AuthorityPair::ID, Some(&Keyring::Alice.to_seed())) - .expect("Key should be created"); - - let net = Arc::new(Mutex::new(net)); - - let mut net = net.lock(); - let peer = net.peer(3); - let client = peer.client().as_client(); - let environ = DummyFactory(client.clone()); - - let mut worker = AuraWorker { - client: client.clone(), - block_import: client.clone(), - env: environ, - keystore: keystore.into(), - sync_oracle: DummyOracle, - justification_sync_link: (), - force_authoring: false, - backoff_authoring_blocks: Option::<()>::None, - telemetry: None, - block_proposal_slot_portion: SlotProportion::new(0.5), - max_block_proposal_slot_portion: None, - compatibility_mode: Default::default(), - _phantom: PhantomData::<(fn() -> AuthorityPair, ())>, - }; - - let head = client.expect_header(client.info().genesis_hash).unwrap(); - - let block = worker - .on_slot(SlotInfo { - slot: 0.into(), - ends_at: Instant::now() + Duration::from_secs(100), - create_inherent_data: Box::new(()), - duration: Duration::from_millis(1000), - chain_head: head, - block_size_limit: None, - storage_proof_recorder: None, - }) - .await - .unwrap(); - - // The returned block should be imported and we should be able to get its header by now. - assert!(client.header(block.hash()).unwrap().is_some()); - } -} diff --git a/partner-chains/substrate-extensions/aura/primitives/Cargo.toml b/partner-chains/substrate-extensions/aura/primitives/Cargo.toml deleted file mode 100644 index c00e556be..000000000 --- a/partner-chains/substrate-extensions/aura/primitives/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "sp-partner-chains-consensus-aura" -version = "1.8.1" -description = "Primitives required by Partner Chains customized Aura consensus" -authors.workspace = true -homepage.workspace = true -edition.workspace = true -license = "Apache-2.0" -repository.workspace = true -readme = "README.md" - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] - -[lints] -workspace = true - -[dependencies] -futures = {workspace = true} -sp-consensus = { workspace = true } -sp-consensus-slots = { workspace = true } -sp-runtime = { optional = true, workspace = true } -sp-inherents = { optional = true, workspace = true } - -[dev-dependencies] -sp-blockchain = { workspace = true } - -[features] -default = ["std"] -std = [ - "sp-consensus-slots/std", - "sp-runtime/std", - "sp-inherents/std", -] - -[lib] -doctest = false diff --git a/partner-chains/substrate-extensions/aura/primitives/README.md b/partner-chains/substrate-extensions/aura/primitives/README.md deleted file mode 100644 index 2fa7807f9..000000000 --- a/partner-chains/substrate-extensions/aura/primitives/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Partner Chains Aura Consensus primitives - -This crate provides primitives used by Partner Chains Aura Consensus. -Items provided: -* `CurrentSlotProvider` for consensus to know current slot according to wall-clock -* `InherentDigest` for consensus to digest InherentData and compare this digest block import -* `PartnerChainsProposer` is a Proposer that additionally adds inherent data digests to logs - -License: Apache-2.0 diff --git a/partner-chains/substrate-extensions/aura/primitives/src/lib.rs b/partner-chains/substrate-extensions/aura/primitives/src/lib.rs deleted file mode 100644 index d9a9704bb..000000000 --- a/partner-chains/substrate-extensions/aura/primitives/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod block_proposal; -pub mod inherent_digest; - -pub use inherent_digest::InherentDigest; -use sp_consensus_slots::Slot; - -/// Provides the current slot for Aura verification purpose. -pub trait CurrentSlotProvider { - /// Returns the current slot, according to wall-time and slot duration configuration. - fn slot(&self) -> Slot; -} diff --git a/partner-chains/toolkit/consensus/Cargo.toml b/partner-chains/toolkit/consensus/Cargo.toml new file mode 100644 index 000000000..5f92f596b --- /dev/null +++ b/partner-chains/toolkit/consensus/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "sp-partner-chains-consensus" +version = "1.8.1" +description = "Partner Chains consensus primitives: inherent digest, block proposal, and inherent checking at block import or in the import-queue verifier" +authors.workspace = true +homepage.workspace = true +edition.workspace = true +license = "Apache-2.0" +repository.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lints] +workspace = true + +[dependencies] +async-trait.workspace = true +futures.workspace = true +log.workspace = true +sc-consensus = { workspace = true, default-features = true } +sp-api = { workspace = true, default-features = true } +sp-block-builder = { workspace = true, default-features = true } +sp-consensus.workspace = true +sp-consensus-slots = { workspace = true, default-features = true } +sp-inherents = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + +[dev-dependencies] +parity-scale-codec = { workspace = true, default-features = true } +sc-network-test = { workspace = true } +sp-blockchain = { workspace = true } +sp-tracing = { workspace = true, default-features = true } +substrate-test-runtime-client = { workspace = true } +tokio.workspace = true + +[lib] +doctest = false diff --git a/partner-chains/toolkit/consensus/README.md b/partner-chains/toolkit/consensus/README.md new file mode 100644 index 000000000..4eaf73490 --- /dev/null +++ b/partner-chains/toolkit/consensus/README.md @@ -0,0 +1,30 @@ +# Partner Chains Consensus + +This crate provides the consensus components used by Partner Chains nodes to attach +Partner Chains inherent data to block headers and validate it during block import, +without forking the block production gadget. Items provided: + +* `InherentDigest` — maps inherent data to block header digest items and back +* `PartnerChainsProposerFactory` — wraps a `Proposer` so that each produced block header + contains the `InherentDigest` items + +The complete Partner Chains inherent check — recreating inherent data from Partner Chains +data sources and checking the block's inherents against it — is run at whichever point the +consensus gadget checks inherents. Two wrappers cover the two cases: + +* `PartnerChainsBlockImport` + `PartnerChainsBodyRestore` — for stacks that check inherents + during block import (e.g. `BabeBlockImport`). The outer wrapper runs the Partner Chains + inherent check and withholds the body from the consensus import (so the consensus import's + own inherent check is skipped); the restore stage placed directly beneath it puts the body + back for GRANDPA and the client. +* `PartnerChainsVerifier` — for stacks that check inherents in the import-queue verifier + instead (e.g. the Aura verifier). It wraps that verifier and runs the same Partner Chains + inherent check. The inner verifier still performs its header-level consensus checks (seal + signature, slot author, equivocation), but its own inherent check is skipped in favour of + the complete Partner Chains one. +* `SlotExtractor` — implemented by the node for its block production gadget (e.g. via + `sc_consensus_aura::find_pre_digest` for Aura), keeping this crate consensus-agnostic + +See `service.rs` in the `node` crate for usage. + +License: Apache-2.0 diff --git a/partner-chains/toolkit/consensus/src/block_import.rs b/partner-chains/toolkit/consensus/src/block_import.rs new file mode 100644 index 000000000..6dc09306f --- /dev/null +++ b/partner-chains/toolkit/consensus/src/block_import.rs @@ -0,0 +1,277 @@ +use crate::inherent_check::check_partner_chains_inherents; +use crate::{InherentDigest, SlotExtractor}; +use sc_consensus::block_import::{BlockCheckParams, BlockImport, BlockImportParams, ImportResult}; +use sp_consensus::Error as ConsensusError; +use sp_consensus_slots::Slot; +use sp_inherents::CreateInherentDataProviders; +use sp_runtime::traits::Block as BlockT; +use std::{marker::PhantomData, sync::Arc}; + +/// Intermediate key under which [`PartnerChainsBlockImport`] stashes the block body +/// for [`PartnerChainsBodyRestore`] to put back. +const BODY_INTERMEDIATE_KEY: &[u8] = b"partner-chains/body"; + +/// Partner Chains block import wrapper, for consensus stacks that check inherents +/// during block import rather than in the import queue verifier. +/// +/// Some consensus block imports (e.g. `BabeBlockImport`) check the block's inherents +/// with inherent data created from the parent hash only, which cannot include the +/// Partner Chains inherents — any block containing them would be rejected. Analogously +/// to [`PartnerChainsVerifier`](crate::PartnerChainsVerifier) on the verifier level, +/// this wrapper performs the complete Partner Chains inherent check itself and +/// withholds the block body from the wrapped import, so that the consensus logic +/// (e.g. epoch changes, equivocation reporting) still runs while its body-gated +/// inherent check is skipped. +/// +/// Since the imports below the consensus one need the body (the client executes and +/// stores the block), the body is stashed as an import intermediate and must be +/// restored by [`PartnerChainsBodyRestore`] placed directly beneath the consensus +/// import. For BABE the import chain composes as: +/// +/// ```text +/// PartnerChainsBlockImport>>> +/// ``` +/// +/// Nodes whose consensus checks inherents in the verifier (e.g. Aura) do not need +/// this wrapper: use [`PartnerChainsVerifier`](crate::PartnerChainsVerifier) alone. +pub struct PartnerChainsBlockImport { + inner: Inner, + client: Arc, + create_inherent_data_providers: CIDP, + _phantom: PhantomData (B, SE, ID)>, +} + +impl PartnerChainsBlockImport { + /// Creates a new block import wrapping `inner`. + pub fn new(inner: Inner, client: Arc, create_inherent_data_providers: CIDP) -> Self { + Self { inner, client, create_inherent_data_providers, _phantom: PhantomData } + } +} + +#[async_trait::async_trait] +impl BlockImport + for PartnerChainsBlockImport +where + B: BlockT, + Inner: BlockImport + Send + Sync, + C: sp_api::ProvideRuntimeApi + Send + Sync, + C::Api: sp_block_builder::BlockBuilder + sp_api::ApiExt, + CIDP: CreateInherentDataProviders + Send + Sync, + SE: SlotExtractor, + ID: InherentDigest + Send + Sync + 'static, +{ + type Error = ConsensusError; + + async fn check_block(&self, block: BlockCheckParams) -> Result { + self.inner.check_block(block).await + } + + async fn import_block( + &self, + mut block: BlockImportParams, + ) -> Result { + // Skip checks that include execution, e.g. when importing only the state after warp sync. + if block.with_state() || block.state_action.skip_execution_checks() { + return self.inner.import_block(block).await; + } + + check_partner_chains_inherents::( + &self.client, + &self.create_inherent_data_providers, + &block.header, + block.body.as_ref(), + block.post_hash(), + ) + .await + .map_err(ConsensusError::ClientImport)?; + + // Withhold the body from the wrapped consensus import so its body-gated inherent + // check — which would run against inherent data missing the Partner Chains + // inherents — is skipped. PartnerChainsBodyRestore puts the body back beneath it. + if let Some(body) = block.body.take() { + block.insert_intermediate(BODY_INTERMEDIATE_KEY, body); + } + + self.inner.import_block(block).await + } +} + +/// Restores the block body stashed by [`PartnerChainsBlockImport`]. +/// +/// Must be placed in the block import chain directly beneath the consensus import +/// wrapped by [`PartnerChainsBlockImport`], so that the imports below it (e.g. GRANDPA +/// and the client) receive the complete block. See [`PartnerChainsBlockImport`]. +pub struct PartnerChainsBodyRestore { + inner: Inner, + _phantom: PhantomData B>, +} + +impl PartnerChainsBodyRestore { + /// Creates a new block import wrapping `inner`. + pub fn new(inner: Inner) -> Self { + Self { inner, _phantom: PhantomData } + } +} + +#[async_trait::async_trait] +impl BlockImport for PartnerChainsBodyRestore +where + B: BlockT, + Inner: BlockImport + Send + Sync, +{ + type Error = Inner::Error; + + async fn check_block(&self, block: BlockCheckParams) -> Result { + self.inner.check_block(block).await + } + + async fn import_block( + &self, + mut block: BlockImportParams, + ) -> Result { + if let Ok(body) = block.remove_intermediate::>(BODY_INTERMEDIATE_KEY) { + block.body = Some(body); + } + self.inner.import_block(block).await + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::*; + use sc_consensus::block_import::ForkChoiceStrategy; + use std::sync::Mutex; + use std::sync::atomic::{AtomicBool, Ordering}; + + /// Stand-in for a consensus block import (e.g. `BabeBlockImport`): records whether + /// it received the block body, as its inherent check is gated on its presence. + struct ConsensusImportStub { + inner: Inner, + saw_body: Arc, + } + + #[async_trait::async_trait] + impl + Send + Sync> BlockImport + for ConsensusImportStub + { + type Error = ConsensusError; + + async fn check_block( + &self, + block: BlockCheckParams, + ) -> Result { + self.inner.check_block(block).await + } + + async fn import_block( + &self, + block: BlockImportParams, + ) -> Result { + self.saw_body.store(block.body.is_some(), Ordering::SeqCst); + self.inner.import_block(block).await + } + } + + /// Innermost import (e.g. the client): records the body it received. + struct TerminalImport { + received_body: Arc::Extrinsic>>>>>, + } + + #[async_trait::async_trait] + impl BlockImport for TerminalImport { + type Error = ConsensusError; + + async fn check_block( + &self, + _block: BlockCheckParams, + ) -> Result { + Ok(ImportResult::imported(false)) + } + + async fn import_block( + &self, + block: BlockImportParams, + ) -> Result { + *self.received_body.lock().unwrap() = Some(block.body); + Ok(ImportResult::imported(false)) + } + } + + struct Sandwich { + import: PartnerChainsBlockImport< + ConsensusImportStub>, + TestClient, + TestCIDP, + Block, + TestSlotExtractor, + TestInherentDigest, + >, + check_inherents_called: Arc, + consensus_saw_body: Arc, + terminal_received_body: Arc::Extrinsic>>>>>, + } + + /// Composes the documented chain: + /// `PartnerChainsBlockImport>>`. + fn sandwich(fail_inherent_check: bool) -> Sandwich { + let (client, check_inherents_called) = test_client(fail_inherent_check); + let consensus_saw_body = Arc::new(AtomicBool::new(false)); + let terminal_received_body = Arc::new(Mutex::new(None)); + + let terminal = TerminalImport { received_body: terminal_received_body.clone() }; + let consensus = ConsensusImportStub { + inner: PartnerChainsBodyRestore::new(terminal), + saw_body: consensus_saw_body.clone(), + }; + let import = + PartnerChainsBlockImport::new(consensus, client, test_create_inherent_data_providers()); + + Sandwich { import, check_inherents_called, consensus_saw_body, terminal_received_body } + } + + fn importable(body: Option::Extrinsic>>) -> BlockImportParams { + let mut block = block_import_params(body); + block.fork_choice = Some(ForkChoiceStrategy::LongestChain); + block + } + + #[tokio::test] + async fn checks_inherents_and_delivers_body_to_imports_beneath_the_consensus_one() { + let sandwich = sandwich(false); + + sandwich + .import + .import_block(importable(Some(vec![]))) + .await + .expect("import succeeds"); + + assert!(sandwich.check_inherents_called.load(Ordering::SeqCst)); + // The consensus import must not see the body (its inherent check is suppressed), + // while the import below the restore stage receives the complete block. + assert!(!sandwich.consensus_saw_body.load(Ordering::SeqCst)); + assert_eq!(*sandwich.terminal_received_body.lock().unwrap(), Some(Some(vec![]))); + } + + #[tokio::test] + async fn rejects_block_when_inherent_check_fails_without_importing() { + let sandwich = sandwich(true); + + let result = sandwich.import.import_block(importable(Some(vec![]))).await; + + assert!( + matches!(result, Err(ConsensusError::ClientImport(e)) if e.contains("Inherent check failed")) + ); + assert!(sandwich.terminal_received_body.lock().unwrap().is_none()); + } + + #[tokio::test] + async fn passes_header_only_import_through_without_inherent_check() { + let sandwich = sandwich(false); + + sandwich.import.import_block(importable(None)).await.expect("import succeeds"); + + assert!(!sandwich.check_inherents_called.load(Ordering::SeqCst)); + assert_eq!(*sandwich.terminal_received_body.lock().unwrap(), Some(None)); + } +} diff --git a/partner-chains/substrate-extensions/aura/primitives/src/block_proposal.rs b/partner-chains/toolkit/consensus/src/block_proposal.rs similarity index 68% rename from partner-chains/substrate-extensions/aura/primitives/src/block_proposal.rs rename to partner-chains/toolkit/consensus/src/block_proposal.rs index 84a5a9846..fa077b367 100644 --- a/partner-chains/substrate-extensions/aura/primitives/src/block_proposal.rs +++ b/partner-chains/toolkit/consensus/src/block_proposal.rs @@ -1,5 +1,5 @@ use crate::InherentDigest; -use futures::FutureExt; +use futures::{FutureExt, future}; use sp_consensus::{Environment, ProposeArgs, Proposer}; use sp_runtime::traits::Block as BlockT; use sp_runtime::{Digest, DigestItem}; @@ -13,6 +13,7 @@ pub struct PartnerChainsProposerFactory, ID> { } impl, ID> PartnerChainsProposerFactory { + /// Creates a new factory wrapping the proposer environment `env`. pub fn new(env: E) -> Self { Self { env, phantom_data: PhantomData } } @@ -49,7 +50,10 @@ impl, ID: InherentDigest> Proposer for PartnerChainsProposer { type Error =

>::Error; - type Proposal =

>::Proposal; + type Proposal = future::Either< +

>::Proposal, + future::Ready, Self::Error>>, + >; fn propose(self, args: ProposeArgs) -> Self::Proposal { let ProposeArgs { @@ -60,19 +64,27 @@ impl, ID: InherentDigest> Proposer storage_proof_recorder, extra_extensions, } = args; + let mut inherent_logs = match ID::from_inherent_data(&inherent_data) { + Ok(logs) => logs, + Err(e) => { + // Fail this proposal instead of panicking the authorship task: the next + // slot gets a fresh chance with newly created inherent data. + return future::Either::Right(future::ready(Err(sp_consensus::Error::Other( + format!("Failed to create inherent digest from inherent data: {e}").into(), + ) + .into()))); + }, + }; let mut logs: Vec = Vec::from(inherent_digests.logs()); - // It is a programmatic error to try to propose a block that has inherent data from which declared InherentDigest cannot be created. - let mut inherent_logs = ID::from_inherent_data(&inherent_data) - .expect("InherentDigest can be created from inherent data"); logs.append(&mut inherent_logs); - self.proposer.propose(ProposeArgs { + future::Either::Left(self.proposer.propose(ProposeArgs { inherent_data, inherent_digests: Digest { logs }, max_duration, block_size_limit, storage_proof_recorder, extra_extensions, - }) + })) } } @@ -116,6 +128,24 @@ mod tests { } } + struct FailingInherentDigest; + + impl InherentDigest for FailingInherentDigest { + type Value = (); + + fn from_inherent_data( + _inherent_data: &InherentData, + ) -> Result, Box> { + Err("no digest for you".into()) + } + + fn value_from_digest( + _digests: &[DigestItem], + ) -> Result> { + todo!() + } + } + struct TestProposer { expected_digest: Digest, } @@ -155,15 +185,32 @@ mod tests { TestProposer { expected_digest: Digest { logs: vec![other_item(), expected_item()] } }; let proposer: PartnerChainsProposer = PartnerChainsProposer::new(test_proposer); - let proposal = proposer - .propose(ProposeArgs { - inherent_data, - inherent_digests, - max_duration: std::time::Duration::from_secs(0), - block_size_limit: None, - ..Default::default() - }) - .into_inner(); + let proposal = futures::executor::block_on(proposer.propose(ProposeArgs { + inherent_data, + inherent_digests, + max_duration: std::time::Duration::from_secs(0), + block_size_limit: None, + ..Default::default() + })); assert!(proposal.is_ok()); } + + #[test] + fn inherent_digest_failure_fails_proposal_without_panicking() { + let test_proposer = TestProposer { expected_digest: Digest { logs: vec![] } }; + let proposer: PartnerChainsProposer = + PartnerChainsProposer::new(test_proposer); + let proposal = futures::executor::block_on(proposer.propose(ProposeArgs { + inherent_data: InherentData::new(), + inherent_digests: Digest { logs: vec![] }, + max_duration: std::time::Duration::from_secs(0), + block_size_limit: None, + ..Default::default() + })); + let error = match proposal { + Err(error) => error.to_string(), + Ok(_) => panic!("proposal should fail"), + }; + assert!(error.contains("no digest for you"), "unexpected error: {error}"); + } } diff --git a/partner-chains/toolkit/consensus/src/inherent_check.rs b/partner-chains/toolkit/consensus/src/inherent_check.rs new file mode 100644 index 000000000..2e7483b7d --- /dev/null +++ b/partner-chains/toolkit/consensus/src/inherent_check.rs @@ -0,0 +1,80 @@ +use crate::{InherentDigest, SlotExtractor}; +use log::warn; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder as BlockBuilderApi; +use sp_consensus_slots::Slot; +use sp_inherents::{CreateInherentDataProviders, InherentDataProvider}; +use sp_runtime::traits::{Block as BlockT, Header}; +use std::sync::Arc; + +const LOG_TARGET: &str = "partner-chains-consensus"; + +/// The complete Partner Chains inherent check, shared by +/// [`PartnerChainsVerifier`](crate::PartnerChainsVerifier) and +/// [`PartnerChainsBlockImport`](crate::PartnerChainsBlockImport). +/// +/// Extracts the slot and the [`InherentDigest`] value from the block header, recreates +/// the inherent data providers parameterised by them, and checks the block's inherents +/// against the recreated inherent data. The check is skipped for blocks without a body +/// and for runtimes too old to support it; the header extraction is performed (and +/// validated) regardless. +pub(crate) async fn check_partner_chains_inherents( + client: &Arc, + create_inherent_data_providers: &CIDP, + header: &B::Header, + body: Option<&Vec>, + post_hash: B::Hash, +) -> Result<(), String> +where + B: BlockT, + C: ProvideRuntimeApi + Send + Sync, + C::Api: BlockBuilderApi + ApiExt, + CIDP: CreateInherentDataProviders + Send + Sync, + SE: SlotExtractor, + ID: InherentDigest + Send + Sync + 'static, +{ + let parent_hash = *header.parent_hash(); + let slot = SE::extract_slot(header)?; + let digest_value = ID::value_from_digest(header.digest().logs()).map_err(|e| { + format!("Failed to retrieve inherent digest from header of block {post_hash:?}: {e}") + })?; + + if let Some(inner_body) = body { + // Skip the inherents check if the runtime API is too old to support it. + if client + .runtime_api() + .has_api_with::, _>(parent_hash, |v| v >= 2) + .map_err(|e| e.to_string())? + { + let inherent_data_providers = create_inherent_data_providers + .create_inherent_data_providers(parent_hash, (slot, digest_value)) + .await + .map_err(|e| format!("Failed to create inherent data providers: {e}"))?; + + let inherent_data = inherent_data_providers + .create_inherent_data() + .await + .map_err(|e| format!("Failed to create inherent data: {e}"))?; + + let check_block = B::new(header.clone(), inner_body.clone()); + + sp_block_builder::check_inherents_with_data( + client.clone(), + parent_hash, + check_block, + &inherent_data_providers, + inherent_data, + ) + .await + .map_err(|e| { + warn!( + target: LOG_TARGET, + "Rejecting block {post_hash:?}: inherent check failed: {e:?}", + ); + format!("Inherent check failed: {e:?}") + })?; + } + } + + Ok(()) +} diff --git a/partner-chains/substrate-extensions/aura/primitives/src/inherent_digest.rs b/partner-chains/toolkit/consensus/src/inherent_digest.rs similarity index 95% rename from partner-chains/substrate-extensions/aura/primitives/src/inherent_digest.rs rename to partner-chains/toolkit/consensus/src/inherent_digest.rs index bdbb6387d..f2e971ce2 100644 --- a/partner-chains/substrate-extensions/aura/primitives/src/inherent_digest.rs +++ b/partner-chains/toolkit/consensus/src/inherent_digest.rs @@ -1,4 +1,3 @@ -#[cfg(feature = "std")] /// Defines parts of inherent data that should be included in header digest pub trait InherentDigest { /// Rust type of the inherent digest value @@ -15,7 +14,6 @@ pub trait InherentDigest { ) -> Result>; } -#[cfg(feature = "std")] impl InherentDigest for () { type Value = (); diff --git a/partner-chains/toolkit/consensus/src/lib.rs b/partner-chains/toolkit/consensus/src/lib.rs new file mode 100644 index 000000000..046e70510 --- /dev/null +++ b/partner-chains/toolkit/consensus/src/lib.rs @@ -0,0 +1,29 @@ +//! Partner Chains consensus components. +//! +//! This crate provides the pieces needed to attach Partner Chains inherent data to block +//! headers and validate it during block import, independently of the block production +//! gadget in use: +//! * [`InherentDigest`] — maps inherent data to header digest items and back +//! * [`PartnerChainsProposerFactory`] — wraps a `Proposer` to add [`InherentDigest`] items +//! to the header of each produced block +//! * [`PartnerChainsBlockImport`] and [`PartnerChainsBodyRestore`] — wrap a consensus block +//! import to run the full Partner Chains inherent check against inherent data recreated +//! from Partner Chains data sources, for stacks that check inherents during block import +//! (e.g. BABE) +//! * [`PartnerChainsVerifier`] — wraps an import-queue `Verifier` to run the same check, for +//! stacks that check inherents in the verifier instead (e.g. Aura) +//! * [`SlotExtractor`] — implemented by the node for its block production gadget +//! (e.g. via `sc_consensus_aura::find_pre_digest` for Aura) + +mod block_import; +mod block_proposal; +mod inherent_check; +mod inherent_digest; +#[cfg(test)] +mod test_support; +mod verifier; + +pub use block_import::{PartnerChainsBlockImport, PartnerChainsBodyRestore}; +pub use block_proposal::{PartnerChainsProposer, PartnerChainsProposerFactory}; +pub use inherent_digest::InherentDigest; +pub use verifier::{PartnerChainsVerifier, SlotExtractor}; diff --git a/partner-chains/toolkit/consensus/src/test_support.rs b/partner-chains/toolkit/consensus/src/test_support.rs new file mode 100644 index 000000000..69c4707be --- /dev/null +++ b/partner-chains/toolkit/consensus/src/test_support.rs @@ -0,0 +1,162 @@ +//! Scaffolding shared by the unit tests of [`crate::PartnerChainsVerifier`] and +//! [`crate::PartnerChainsBlockImport`]. + +use crate::{InherentDigest, SlotExtractor}; +use sc_consensus::block_import::BlockImportParams; +use sp_api::{ApiRef, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder as BlockBuilderApi; +use sp_consensus::BlockOrigin; +use sp_consensus_slots::Slot; +use sp_inherents::{CheckInherentsResult, InherentData, InherentDataProvider, InherentIdentifier}; +use sp_runtime::generic::Header; +use sp_runtime::traits::{BlakeTwo256, Block as BlockT}; +use sp_runtime::{DigestItem, OpaqueExtrinsic}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +pub(crate) type Block = sp_runtime::generic::Block, OpaqueExtrinsic>; + +/// Slot [`TestSlotExtractor`] extracts from every header. +pub(crate) const TEST_SLOT: u64 = 7; +/// Value [`TestInherentDigest`] extracts from every header. +pub(crate) const TEST_DIGEST_VALUE: u32 = 42; + +pub(crate) struct TestSlotExtractor; + +impl SlotExtractor for TestSlotExtractor { + fn extract_slot(_header: &::Header) -> Result { + Ok(Slot::from(TEST_SLOT)) + } +} + +pub(crate) struct TestInherentDigest; + +impl InherentDigest for TestInherentDigest { + type Value = u32; + + fn from_inherent_data( + _inherent_data: &InherentData, + ) -> Result, Box> { + Ok(vec![]) + } + + fn value_from_digest( + _digests: &[DigestItem], + ) -> Result> { + Ok(TEST_DIGEST_VALUE) + } +} + +pub(crate) struct TestIDP; + +#[async_trait::async_trait] +impl InherentDataProvider for TestIDP { + async fn provide_inherent_data( + &self, + _inherent_data: &mut InherentData, + ) -> Result<(), sp_inherents::Error> { + Ok(()) + } + + async fn try_handle_error( + &self, + _identifier: &InherentIdentifier, + _error: &[u8], + ) -> Option> { + None + } +} + +pub(crate) type TestCIDP = + fn( + ::Hash, + (Slot, u32), + ) -> futures::future::Ready>>; + +fn create_inherent_data_providers( + _parent_hash: ::Hash, + (slot, digest_value): (Slot, u32), +) -> futures::future::Ready>> { + // The Partner Chains inherent check must be parameterised with the values + // extracted from the block header. + assert_eq!(slot, Slot::from(TEST_SLOT)); + assert_eq!(digest_value, TEST_DIGEST_VALUE); + futures::future::ready(Ok(TestIDP)) +} + +pub(crate) fn test_create_inherent_data_providers() -> TestCIDP { + create_inherent_data_providers +} + +#[derive(Clone)] +pub(crate) struct MockApi { + check_inherents_called: Arc, + fail_inherent_check: bool, +} + +sp_api::mock_impl_runtime_apis! { + impl BlockBuilderApi for MockApi { + fn apply_extrinsic(&self, _: ::Extrinsic) -> sp_runtime::ApplyExtrinsicResult { + unimplemented!() + } + + fn finalize_block(&self) -> ::Header { + unimplemented!() + } + + fn inherent_extrinsics(&self, _: InherentData) -> Vec<::Extrinsic> { + unimplemented!() + } + + fn check_inherents(&self, _: ::LazyBlock, _: InherentData) -> CheckInherentsResult { + self.check_inherents_called.store(true, Ordering::SeqCst); + let mut result = CheckInherentsResult::new(); + if self.fail_inherent_check { + result + .put_error(*b"testinh0", &sp_inherents::MakeFatalError::from(())) + .expect("error can be put into a fresh result"); + } + result + } + } +} + +pub(crate) struct TestClient { + api: MockApi, +} + +impl ProvideRuntimeApi for TestClient { + type Api = MockApi; + + fn runtime_api(&self) -> ApiRef<'_, Self::Api> { + self.api.clone().into() + } +} + +/// A client whose runtime reports inherent check success or failure as configured, +/// together with a flag recording whether `check_inherents` was invoked. +pub(crate) fn test_client(fail_inherent_check: bool) -> (Arc, Arc) { + let check_inherents_called = Arc::new(AtomicBool::new(false)); + let client = TestClient { + api: MockApi { + check_inherents_called: check_inherents_called.clone(), + fail_inherent_check, + }, + }; + (Arc::new(client), check_inherents_called) +} + +pub(crate) fn block_import_params( + body: Option::Extrinsic>>, +) -> BlockImportParams { + let header = Header { + parent_hash: Default::default(), + number: 1, + state_root: Default::default(), + extrinsics_root: Default::default(), + digest: Default::default(), + }; + let mut block = BlockImportParams::new(BlockOrigin::NetworkInitialSync, header); + block.body = body; + block +} diff --git a/partner-chains/toolkit/consensus/src/verifier.rs b/partner-chains/toolkit/consensus/src/verifier.rs new file mode 100644 index 000000000..4723878cd --- /dev/null +++ b/partner-chains/toolkit/consensus/src/verifier.rs @@ -0,0 +1,216 @@ +use crate::InherentDigest; +use crate::inherent_check::check_partner_chains_inherents; +use sc_consensus::block_import::BlockImportParams; +use sc_consensus::import_queue::Verifier; +use sp_api::{ApiExt, ProvideRuntimeApi}; +use sp_block_builder::BlockBuilder as BlockBuilderApi; +use sp_consensus_slots::Slot; +use sp_inherents::CreateInherentDataProviders; +use sp_runtime::traits::Block as BlockT; +use std::{marker::PhantomData, sync::Arc}; + +/// Extracts the authoring slot from a block header's pre-runtime digests. +/// +/// Abstracts over the consensus mechanism (Aura, Babe, etc.) so that +/// [`PartnerChainsVerifier`] does not depend on a specific block production gadget. +pub trait SlotExtractor: Send + Sync + 'static { + /// Extract the slot under which the block was authored from its header. + fn extract_slot(header: &B::Header) -> Result; +} + +/// Partner Chains verifier wrapper. +/// +/// Wraps an inner `Verifier` (e.g. the Aura verifier) and checks block inherents against +/// inherent data recreated from Partner Chains data sources, parameterised by the block's +/// slot and the value of its [`InherentDigest`] (e.g. the mainchain reference hash). +/// +/// The block body is withheld from the inner verifier, so that consensus-specific checks +/// (seal, equivocation) still run, but the inner verifier's own inherent check is skipped. +/// That check would run against inherent data missing the Partner Chains inherents and +/// reject any block containing them. This verifier performs the single, complete inherent +/// check itself. +/// +/// This covers consensus stacks that check inherents in the import queue verifier, as +/// Aura does. For stacks that check inherents during block import instead (e.g. BABE), +/// use [`PartnerChainsBlockImport`](crate::PartnerChainsBlockImport) in addition. +/// +/// Generic over: +/// - `Inner`: the wrapped verifier (e.g. `AuraVerifier`) +/// - `C`: the client (for runtime API calls) +/// - `CIDP`: creates inherent data providers parameterised by `(Slot, ID::Value)` +/// - `SE`: extracts the slot from the block header +/// - `ID`: the [`InherentDigest`] carrying inherent data in the block header +pub struct PartnerChainsVerifier { + inner: Inner, + client: Arc, + create_inherent_data_providers: CIDP, + _phantom: PhantomData (B, SE, ID)>, +} + +impl PartnerChainsVerifier { + /// Creates a new verifier wrapping `inner`. + pub fn new(inner: Inner, client: Arc, create_inherent_data_providers: CIDP) -> Self { + Self { inner, client, create_inherent_data_providers, _phantom: PhantomData } + } +} + +#[async_trait::async_trait] +impl Verifier for PartnerChainsVerifier +where + B: BlockT, + Inner: Verifier, + C: ProvideRuntimeApi + Send + Sync, + C::Api: BlockBuilderApi + ApiExt, + CIDP: CreateInherentDataProviders + Send + Sync, + SE: SlotExtractor, + ID: InherentDigest + Send + Sync + 'static, +{ + async fn verify( + &self, + mut block: BlockImportParams, + ) -> Result, String> { + // Skip checks that include execution, e.g. when importing only the state after warp sync. + if block.with_state() || block.state_action.skip_execution_checks() { + return self.inner.verify(block).await; + } + + // Withhold the body from the inner verifier. All of its header-level consensus + // checks (e.g. for Aura: seal signature, slot author, slot-not-in-future, + // equivocation) run regardless of the body; the only body-gated logic is its + // inherent check, which would run against inherent data missing the Partner + // Chains inherents and reject any block containing them. The complete inherent + // check is performed below instead. + let body = block.body.take(); + let mut block = self.inner.verify(block).await?; + block.body = body; + + check_partner_chains_inherents::( + &self.client, + &self.create_inherent_data_providers, + &block.header, + block.body.as_ref(), + block.post_hash(), + ) + .await?; + + Ok(block) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_support::*; + use sp_runtime::{DigestItem, OpaqueExtrinsic}; + use std::sync::atomic::{AtomicBool, Ordering}; + + /// Post-digest the inner verifier stub leaves on verified blocks, standing in for + /// the consensus seal a real verifier (e.g. Aura) moves into the post-digests. + const INNER_VERIFIER_SEAL: &[u8] = b"inner-verifier-seal"; + + /// Stand-in for a consensus verifier (e.g. Aura). + struct InnerVerifier { + fail: bool, + } + + #[async_trait::async_trait] + impl Verifier for InnerVerifier { + async fn verify( + &self, + mut block: BlockImportParams, + ) -> Result, String> { + if self.fail { + return Err("rejected by the inner verifier".to_string()); + } + block.post_digests.push(DigestItem::Other(INNER_VERIFIER_SEAL.to_vec())); + Ok(block) + } + } + + type TestVerifier = PartnerChainsVerifier< + InnerVerifier, + TestClient, + TestCIDP, + Block, + TestSlotExtractor, + TestInherentDigest, + >; + + fn test_verifier( + inner_fail: bool, + fail_inherent_check: bool, + ) -> (TestVerifier, Arc) { + let (client, check_inherents_called) = test_client(fail_inherent_check); + let verifier = PartnerChainsVerifier::new( + InnerVerifier { fail: inner_fail }, + client, + test_create_inherent_data_providers(), + ); + (verifier, check_inherents_called) + } + + fn has_inner_verifier_seal(block: &BlockImportParams) -> bool { + block + .post_digests + .iter() + .any(|item| matches!(item, DigestItem::Other(data) if data == INNER_VERIFIER_SEAL)) + } + + #[tokio::test] + async fn accepts_block_passing_consensus_and_inherent_checks() { + let (verifier, check_inherents_called) = test_verifier(false, false); + + let verified = verifier + .verify(block_import_params(Some(vec![]))) + .await + .expect("verification succeeds"); + + // The consensus checks of the inner verifier ran and their outcome (e.g. the + // extracted seal) is passed on, together with the body, for import. + assert!(has_inner_verifier_seal(&verified)); + assert_eq!(verified.body, Some(Vec::::new())); + // The inherent check ran against inherent data recreated from the slot and + // inherent digest of the header (asserted in the test CIDP). + assert!(check_inherents_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn rejects_block_when_inner_verifier_rejects() { + let (verifier, check_inherents_called) = test_verifier(true, false); + + let error = match verifier.verify(block_import_params(Some(vec![]))).await { + Err(error) => error, + Ok(_) => panic!("verification should fail"), + }; + + assert!(error.contains("rejected by the inner verifier")); + // A block failing consensus checks is rejected without the cost of recreating + // Partner Chains inherent data. + assert!(!check_inherents_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn rejects_block_when_inherent_check_fails() { + let (verifier, check_inherents_called) = test_verifier(false, true); + + let error = match verifier.verify(block_import_params(Some(vec![]))).await { + Err(error) => error, + Ok(_) => panic!("verification should fail"), + }; + + assert!(error.contains("Inherent check failed")); + assert!(check_inherents_called.load(Ordering::SeqCst)); + } + + #[tokio::test] + async fn skips_inherent_check_for_header_only_import() { + let (verifier, check_inherents_called) = test_verifier(false, false); + + let verified = + verifier.verify(block_import_params(None)).await.expect("verification succeeds"); + + assert!(has_inner_verifier_seal(&verified)); + assert!(verified.body.is_none()); + assert!(!check_inherents_called.load(Ordering::SeqCst)); + } +} diff --git a/partner-chains/toolkit/consensus/tests/verifier_network.rs b/partner-chains/toolkit/consensus/tests/verifier_network.rs new file mode 100644 index 000000000..2d973d591 --- /dev/null +++ b/partner-chains/toolkit/consensus/tests/verifier_network.rs @@ -0,0 +1,221 @@ +//! Network integration test for [`PartnerChainsVerifier`], using +//! `sc_network_test::TestNetFactory`. +//! +//! Blocks are generated with the inherent digest and a test slot pre-digest directly +//! in their headers (no block production gadget), and the verifier wraps the framework's +//! `PassThroughVerifier` instead of a consensus verifier. +//! +//! One peer generates blocks; the other peers sync them through import queues running +//! [`PartnerChainsVerifier`], which must extract the slot and the inherent digest value +//! from each header and recreate the inherent data providers parameterised by them. +//! The test asserts the digest value stored at block creation round-trips to every +//! syncing peer's verifier. + +use parity_scale_codec::{Decode, Encode}; +use sc_consensus::{BoxJustificationImport, ForkChoiceStrategy}; +use sc_network_test::{ + Block as TestBlock, BlockImportAdapter, PassThroughVerifier, Peer, PeersClient, + PeersFullClient, TestNetFactory, +}; +use sp_consensus::BlockOrigin; +use sp_consensus_slots::Slot; +use sp_inherents::{CreateInherentDataProviders, InherentData, InherentIdentifier}; +use sp_partner_chains_consensus::{InherentDigest, PartnerChainsVerifier, SlotExtractor}; +use sp_runtime::generic::BlockId; +use sp_runtime::traits::Block as BlockT; +use sp_runtime::{Digest, DigestItem}; +use std::sync::{Arc, Mutex}; + +const BLOCK_COUNT: u64 = 5; + +/// Inherent data carrying the slot number, standing in for a consensus slot provider. +const SLOT_INHERENT_ID: InherentIdentifier = *b"testslot"; +/// Pre-runtime digest written by the (simulated) block production gadget. +const SLOT_PRE_DIGEST_ID: [u8; 4] = *b"slot"; +/// Pre-runtime digest carrying the Partner Chains inherent digest value. +const PC_DIGEST_ID: [u8; 4] = *b"pcsl"; + +/// Stand-in for a Partner Chains inherent digest (e.g. the mainchain reference hash): +/// stores the slot from the inherent data in the header, so the test can verify the +/// digest value round-trips from block creation to verification on other peers. +struct SlotInherentDigest; + +impl InherentDigest for SlotInherentDigest { + type Value = u64; + + fn from_inherent_data( + inherent_data: &InherentData, + ) -> Result, Box> { + let slot = inherent_data + .get_data::(&SLOT_INHERENT_ID)? + .ok_or("no slot in inherent data")?; + Ok(vec![DigestItem::PreRuntime(PC_DIGEST_ID, slot.encode())]) + } + + fn value_from_digest( + digests: &[DigestItem], + ) -> Result> { + decode_pre_digest(digests, PC_DIGEST_ID) + .ok_or_else(|| "no inherent digest in header".into()) + } +} + +/// [`SlotExtractor`] reading the slot from the test pre-runtime digest. +struct TestSlotExtractor; + +impl SlotExtractor for TestSlotExtractor { + fn extract_slot(header: &::Header) -> Result { + decode_pre_digest(header.digest.logs(), SLOT_PRE_DIGEST_ID) + .map(Slot::from) + .ok_or_else(|| "no slot pre-digest in header".to_string()) + } +} + +fn decode_pre_digest(digests: &[DigestItem], id: [u8; 4]) -> Option { + digests.iter().find_map(|item| match item { + DigestItem::PreRuntime(item_id, data) if *item_id == id => u64::decode(&mut &data[..]).ok(), + _ => None, + }) +} + +/// Records the `(slot, digest value)` pairs the verifier recreates inherent data with, +/// standing in for the Partner Chains data source backed providers of a real node. +struct RecordingVerifierCIDP { + recorded: Arc>>, +} + +#[async_trait::async_trait] +impl CreateInherentDataProviders for RecordingVerifierCIDP { + type InherentDataProviders = (); + + async fn create_inherent_data_providers( + &self, + _parent: ::Hash, + (slot, digest_value): (Slot, u64), + ) -> Result> { + self.recorded.lock().unwrap().push((slot, digest_value)); + Ok(()) + } +} + +type TestVerifier = PartnerChainsVerifier< + PassThroughVerifier, + PeersFullClient, + RecordingVerifierCIDP, + TestBlock, + TestSlotExtractor, + SlotInherentDigest, +>; +type PartnerChainsPeer = Peer<(), PeersClient>; + +#[derive(Default)] +struct PartnerChainsTestNet { + peers: Vec, + recorded: Arc>>, +} + +impl TestNetFactory for PartnerChainsTestNet { + type Verifier = TestVerifier; + type PeerData = (); + type BlockImport = PeersClient; + + fn make_verifier(&self, client: PeersClient, _peer_data: &()) -> Self::Verifier { + PartnerChainsVerifier::new( + PassThroughVerifier::new(false), + client.as_client(), + RecordingVerifierCIDP { recorded: self.recorded.clone() }, + ) + } + + fn make_block_import( + &self, + client: PeersClient, + ) -> ( + BlockImportAdapter, + Option>, + Self::PeerData, + ) { + (client.as_block_import(), None, ()) + } + + fn peer(&mut self, i: usize) -> &mut PartnerChainsPeer { + &mut self.peers[i] + } + + fn peers(&self) -> &Vec { + &self.peers + } + + fn peers_mut(&mut self) -> &mut Vec { + &mut self.peers + } + + fn mut_peers)>(&mut self, closure: F) { + closure(&mut self.peers); + } +} + +/// Header digests for the block at height `number`: the slot pre-digest a block +/// production gadget would add, plus the Partner Chains inherent digest created from +/// the inherent data, as `PartnerChainsProposer` does during block proposal. +fn block_digests(number: u64) -> Digest { + let slot = number; + let mut inherent_data = InherentData::new(); + inherent_data + .put_data(SLOT_INHERENT_ID, &slot) + .expect("fresh inherent data accepts slot"); + + let mut logs = vec![DigestItem::PreRuntime(SLOT_PRE_DIGEST_ID, slot.encode())]; + logs.extend( + SlotInherentDigest::from_inherent_data(&inherent_data) + .expect("inherent digest can be created from the slot inherent data"), + ); + Digest { logs } +} + +#[tokio::test] +async fn syncing_peers_verify_blocks_against_inherent_digests() { + sp_tracing::try_init_simple(); + let mut net = PartnerChainsTestNet::new(3); + let recorded = net.recorded.clone(); + + let hashes = net.peer(0).generate_blocks_at_with_inherent_digests( + BlockId::Number(0), + BLOCK_COUNT as usize, + BlockOrigin::File, + |builder| builder.build().unwrap().block, + |i| block_digests(i as u64 + 1), + false, + true, + true, + ForkChoiceStrategy::LongestChain, + ); + + net.run_until_sync().await; + + let best_hash = *hashes.last().expect("blocks were generated"); + assert!(net.peer(1).has_block(best_hash), "peer 1 should sync all blocks"); + assert!(net.peer(2).has_block(best_hash), "peer 2 should sync all blocks"); + + let recorded = recorded.lock().unwrap(); + // Each peer verifies every generated block: peer 0 while generating, peers 1 and 2 + // while importing through their PartnerChainsVerifier-backed import queues. + assert!( + recorded.len() >= 3 * BLOCK_COUNT as usize, + "every peer should have verified every block, got {} verifications", + recorded.len() + ); + for (slot, digest_value) in recorded.iter() { + assert_eq!( + u64::from(*slot), + *digest_value, + "digest value stored at block creation should round-trip to verification" + ); + } + for number in 1..=BLOCK_COUNT { + assert!( + recorded.iter().any(|(_, value)| *value == number), + "block at height {number} should have been verified" + ); + } +} diff --git a/partner-chains/toolkit/sidechain/sidechain-mc-hash/Cargo.toml b/partner-chains/toolkit/sidechain/sidechain-mc-hash/Cargo.toml index 1e0b4e949..cbec6ad0a 100644 --- a/partner-chains/toolkit/sidechain/sidechain-mc-hash/Cargo.toml +++ b/partner-chains/toolkit/sidechain/sidechain-mc-hash/Cargo.toml @@ -14,7 +14,7 @@ workspace = true [dependencies] async-trait = { workspace = true } sp-consensus-slots = { workspace = true } -sp-partner-chains-consensus-aura = { workspace = true, features = ["std"] } +sp-partner-chains-consensus = { workspace = true } sidechain-domain = { workspace = true, features = ["std"] } sp-consensus = { workspace = true } sp-blockchain = { workspace = true } diff --git a/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/lib.rs b/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/lib.rs index c8b84211a..7f3316780 100644 --- a/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/lib.rs +++ b/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/lib.rs @@ -15,7 +15,7 @@ //! //! # Prerequisites //! -//! This feature uses the [InherentDigest] mechanism from [sp_partner_chains_consensus_aura] crate for storing inherent +//! This feature uses the [InherentDigest] mechanism from [sp_partner_chains_consensus] crate for storing inherent //! data in the block header. Your node must use the [PartnerChainsProposer] defined by that crate for this feature to work. //! //! # Adding to the node @@ -149,7 +149,7 @@ //! ```rust //! use sidechain_mc_hash::McHashInherentDigest; //! use sp_consensus::Environment; -//! use sp_partner_chains_consensus_aura::block_proposal::PartnerChainsProposerFactory; +//! use sp_partner_chains_consensus::PartnerChainsProposerFactory; //! use sp_runtime::traits::Block as BlockT; //! //! fn new_full>( @@ -165,8 +165,8 @@ //! } //! ``` //! -//! [PartnerChainsProposer]: sp_partner_chains_consensus_aura::block_proposal::PartnerChainsProposer -//! [PartnerChainsProposerFactory]: sp_partner_chains_consensus_aura::block_proposal::PartnerChainsProposerFactory +//! [PartnerChainsProposer]: sp_partner_chains_consensus::PartnerChainsProposer +//! [PartnerChainsProposerFactory]: sp_partner_chains_consensus::PartnerChainsProposerFactory #![warn(missing_docs)] use crate::McHashInherentError::StableBlockNotFound; @@ -175,7 +175,7 @@ use sidechain_domain::{byte_string::ByteString, *}; use sp_blockchain::HeaderBackend; use sp_consensus_slots::{Slot, SlotDuration}; use sp_inherents::{InherentData, InherentDataProvider, InherentIdentifier}; -use sp_partner_chains_consensus_aura::inherent_digest::InherentDigest; +use sp_partner_chains_consensus::InherentDigest; use sp_runtime::{ DigestItem, traits::{Block as BlockT, Header as HeaderT, Zero}, diff --git a/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/test.rs b/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/test.rs index 9fac25eee..edb0dca7e 100644 --- a/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/test.rs +++ b/partner-chains/toolkit/sidechain/sidechain-mc-hash/src/test.rs @@ -1,7 +1,7 @@ mod inherent_digest_tests { use crate::mock::*; use crate::*; - use sp_partner_chains_consensus_aura::inherent_digest::InherentDigest; + use sp_partner_chains_consensus::InherentDigest; #[tokio::test] async fn from_inherent_data_works() {