diff --git a/bin/node/runtime/src/lib.rs b/bin/node/runtime/src/lib.rs index b967cf07f760..a68ec33fa0e5 100644 --- a/bin/node/runtime/src/lib.rs +++ b/bin/node/runtime/src/lib.rs @@ -215,6 +215,28 @@ impl pallet_aura::Trait for Runtime { type AuthorityId = AuraId; } +// We want to use a different validator configuration for benchmarking than what's used in Kovan, +// but we can't configure a new validator set on the fly which means we need to wire the runtime +// together like this +#[cfg(feature = "runtime-benchmarks")] +use pallet_bridge_eth_poa::{ValidatorsConfiguration, ValidatorsSource}; + +#[cfg(feature = "runtime-benchmarks")] +parameter_types! { + pub const FinalityVotesCachingInterval: Option = Some(16); + pub KovanAuraConfiguration: pallet_bridge_eth_poa::AuraConfiguration = kovan::kovan_aura_configuration(); + pub KovanValidatorsConfiguration: pallet_bridge_eth_poa::ValidatorsConfiguration = bench_validator_config(); +} + +#[cfg(feature = "runtime-benchmarks")] +fn bench_validator_config() -> ValidatorsConfiguration { + ValidatorsConfiguration::Multi(vec![ + (0, ValidatorsSource::List(vec![[1; 20].into()])), + (1, ValidatorsSource::Contract([3; 20].into(), vec![[1; 20].into()])), + ]) +} + +#[cfg(not(feature = "runtime-benchmarks"))] parameter_types! { pub const FinalityVotesCachingInterval: Option = Some(16); pub KovanAuraConfiguration: pallet_bridge_eth_poa::AuraConfiguration = kovan::kovan_aura_configuration(); diff --git a/modules/ethereum/src/benchmarking.rs b/modules/ethereum/src/benchmarking.rs index 10136c1e5f42..d31d41039122 100644 --- a/modules/ethereum/src/benchmarking.rs +++ b/modules/ethereum/src/benchmarking.rs @@ -16,11 +16,14 @@ use super::*; -use crate::test_utils::{build_custom_header, build_genesis_header, validator_utils::*}; +use crate::test_utils::{ + build_custom_header, build_genesis_header, insert_header, validator_utils::*, validators_change_receipt, + HeaderBuilder, +}; use frame_benchmarking::benchmarks; use frame_system::RawOrigin; -use primitives::U256; +use primitives::{compute_merkle_root, U256}; benchmarks! { _ { } @@ -33,15 +36,8 @@ benchmarks! { import_unsigned_header_best_case { let n in 1..1000; - // initialize storage with some initial header - let initial_header = build_genesis_header(&validator(0)); - let initial_header_hash = initial_header.compute_hash(); - let initial_difficulty = initial_header.difficulty; - initialize_storage::( - &initial_header, - initial_difficulty, - &validators_addresses(2), - ); + let num_validators = 2; + let initial_header = initialize_bench::(num_validators); // prepare header to be inserted let header = build_custom_header( @@ -55,6 +51,223 @@ benchmarks! { }: import_unsigned_header(RawOrigin::None, header, None) verify { - assert_eq!(BridgeStorage::::new().best_block().0.number, 1); + let storage = BridgeStorage::::new(); + assert_eq!(storage.best_block().0.number, 1); + assert_eq!(storage.finalized_block().number, 0); + } + + // Our goal with this bench is to try and see the effect that finalizing difference ranges of + // blocks has on our import time. As such we need to make sure that we keep the number of + // validators fixed while changing the number blocks finalized (the complexity parameter) by + // importing the last header. + // + // One important thing to keep in mind is that the runtime provides a finality cache in order to + // reduce the overhead of header finalization. However, this is only triggered every 16 blocks. + import_unsigned_finality { + // Our complexity parameter, n, will represent the number of blocks imported before + // finalization. + let n in 1..7; + + let mut storage = BridgeStorage::::new(); + let num_validators: u32 = 2; + let initial_header = initialize_bench::(num_validators as usize); + + // Since we only have two validators we need to make sure the number of blocks is even to + // make sure the right validator signs the final block + let num_blocks = 2 * n; + let mut headers = Vec::new(); + let mut parent = initial_header.clone(); + + // Import a bunch of headers without any verification, will ensure that they're not + // finalized prematurely + for i in 1..=num_blocks { + let header = HeaderBuilder::with_parent(&parent).sign_by(&validator(0)); + let id = header.compute_id(); + insert_header(&mut storage, header.clone()); + headers.push(header.clone()); + parent = header; + } + + let last_header = headers.last().unwrap().clone(); + let last_authority = validator(1); + + // Need to make sure that the header we're going to import hasn't been inserted + // into storage already + let header = HeaderBuilder::with_parent(&last_header).sign_by(&last_authority); + }: import_unsigned_header(RawOrigin::None, header, None) + verify { + let storage = BridgeStorage::::new(); + assert_eq!(storage.best_block().0.number, (num_blocks + 1) as u64); + assert_eq!(storage.finalized_block().number, num_blocks as u64); + } + + // Basically the exact same as `import_unsigned_finality` but with a different range for the + // complexity parameter. In this bench we use a larger range of blocks to see how performance + // changes when the finality cache kicks in (>16 blocks). + import_unsigned_finality_with_cache { + // Our complexity parameter, n, will represent the number of blocks imported before + // finalization. + let n in 7..100; + + let mut storage = BridgeStorage::::new(); + let num_validators: u32 = 2; + let initial_header = initialize_bench::(num_validators as usize); + + // Since we only have two validators we need to make sure the number of blocks is even to + // make sure the right validator signs the final block + let num_blocks = 2 * n; + let mut headers = Vec::new(); + let mut parent = initial_header.clone(); + + // Import a bunch of headers without any verification, will ensure that they're not + // finalized prematurely + for i in 1..=num_blocks { + let header = HeaderBuilder::with_parent(&parent).sign_by(&validator(0)); + let id = header.compute_id(); + insert_header(&mut storage, header.clone()); + headers.push(header.clone()); + parent = header; + } + + let last_header = headers.last().unwrap().clone(); + let last_authority = validator(1); + + // Need to make sure that the header we're going to import hasn't been inserted + // into storage already + let header = HeaderBuilder::with_parent(&last_header).sign_by(&last_authority); + }: import_unsigned_header(RawOrigin::None, header, None) + verify { + let storage = BridgeStorage::::new(); + assert_eq!(storage.best_block().0.number, (num_blocks + 1) as u64); + assert_eq!(storage.finalized_block().number, num_blocks as u64); + } + + // A block import may trigger a pruning event, which adds extra work to the import progress. + // In this bench we trigger a pruning event in order to see how much extra time is spent by the + // runtime dealing with it. In the Ethereum Pallet, we're limited pruning to eight blocks in a + // single import, as dictated by MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT. + import_unsigned_pruning { + let n in 1..MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT as u32; + + let mut storage = BridgeStorage::::new(); + + let num_validators = 3; + let initial_header = initialize_bench::(num_validators as usize); + let validators = validators(num_validators); + + // Want to prune eligible blocks between [0, n) + BlocksToPrune::put(PruningRange { + oldest_unpruned_block: 0, + oldest_block_to_keep: n as u64, + }); + + let mut parent = initial_header; + for i in 1..=n { + let header = HeaderBuilder::with_parent(&parent).sign_by_set(&validators); + let id = header.compute_id(); + insert_header(&mut storage, header.clone()); + parent = header; + } + + let header = HeaderBuilder::with_parent(&parent).sign_by_set(&validators); + }: import_unsigned_header(RawOrigin::None, header, None) + verify { + let storage = BridgeStorage::::new(); + let max_pruned: u64 = (n - 1) as _; + assert_eq!(storage.best_block().0.number, (n + 1) as u64); + assert!(HeadersByNumber::get(&0).is_none()); + assert!(HeadersByNumber::get(&max_pruned).is_none()); + } + + // The goal of this bench is to import a block which contains a transaction receipt. The receipt + // will contain a validator set change. Verifying the receipt root is an expensive operation to + // do, which is why we're interested in benchmarking it. + import_unsigned_with_receipts { + let n in 1..100; + + let mut storage = BridgeStorage::::new(); + + let num_validators = 1; + let initial_header = initialize_bench::(num_validators as usize); + + let mut receipts = vec![]; + for i in 1..=n { + let receipt = validators_change_receipt(Default::default()); + receipts.push(receipt) + } + let encoded_receipts = receipts.iter().map(|r| r.rlp()); + + // We need this extra header since this is what signals a validator set transition. This + // will ensure that the next header is within the "Contract" window + let header1 = HeaderBuilder::with_parent(&initial_header).sign_by(&validator(0)); + insert_header(&mut storage, header1.clone()); + + let header = build_custom_header( + &validator(0), + &header1, + |mut header| { + // Logs Bloom signals a change in validator set + header.log_bloom = (&[0xff; 256]).into(); + header.receipts_root = compute_merkle_root(encoded_receipts); + header + }, + ); + }: import_unsigned_header(RawOrigin::None, header, Some(receipts)) + verify { + let storage = BridgeStorage::::new(); + assert_eq!(storage.best_block().0.number, 2); + } +} + +fn initialize_bench(num_validators: usize) -> Header { + // Initialize storage with some initial header + let initial_header = build_genesis_header(&validator(0)); + let initial_difficulty = initial_header.difficulty; + let initial_validators = validators_addresses(num_validators as usize); + + initialize_storage::(&initial_header, initial_difficulty, &initial_validators); + + initial_header +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::{run_test, TestRuntime}; + use frame_support::assert_ok; + + #[test] + fn insert_unsigned_header_best_case() { + run_test(1, |_| { + assert_ok!(test_benchmark_import_unsigned_header_best_case::()); + }); + } + + #[test] + fn insert_unsigned_header_finality() { + run_test(1, |_| { + assert_ok!(test_benchmark_import_unsigned_finality::()); + }); + } + + #[test] + fn insert_unsigned_header_finality_with_cache() { + run_test(1, |_| { + assert_ok!(test_benchmark_import_unsigned_finality_with_cache::()); + }); + } + + #[test] + fn insert_unsigned_header_pruning() { + run_test(1, |_| { + assert_ok!(test_benchmark_import_unsigned_pruning::()); + }); + } + + #[test] + fn insert_unsigned_header_receipts() { + run_test(1, |_| { + assert_ok!(test_benchmark_import_unsigned_with_receipts::()); + }); } } diff --git a/modules/ethereum/src/import.rs b/modules/ethereum/src/import.rs index ab375d256b8e..588b4dfdbf2c 100644 --- a/modules/ethereum/src/import.rs +++ b/modules/ethereum/src/import.rs @@ -163,7 +163,7 @@ mod tests { use super::*; use crate::mock::{ run_test, secret_to_address, test_aura_config, test_validators_config, validator, validators_addresses, - HeaderBuilder, KeepSomeHeadersBehindBest, TestRuntime, GAS_LIMIT, + validators_change_receipt, HeaderBuilder, KeepSomeHeadersBehindBest, TestRuntime, GAS_LIMIT, }; use crate::validators::ValidatorsSource; use crate::{BlocksToPrune, BridgeStorage, Headers, PruningRange}; @@ -316,9 +316,7 @@ mod tests { &validators_config, Some(101), header11.clone(), - Some(vec![crate::validators::tests::validators_change_recept( - latest_block_id.hash, - )]), + Some(vec![validators_change_receipt(latest_block_id.hash)]), ) .unwrap(); assert_eq!(finalized_blocks, vec![(parent_id, Some(100))],); diff --git a/modules/ethereum/src/lib.rs b/modules/ethereum/src/lib.rs index 1ff2c2a8ae0b..1a87111e57df 100644 --- a/modules/ethereum/src/lib.rs +++ b/modules/ethereum/src/lib.rs @@ -463,7 +463,7 @@ decl_storage! { // the initial blocks should be selected so that: // 1) it doesn't signal validators changes; // 2) there are no scheduled validators changes from previous blocks; - // 3) (implied) all direct children of initial block are authred by the same validators set. + // 3) (implied) all direct children of initial block are authored by the same validators set. assert!( !config.initial_validators.is_empty(), @@ -563,6 +563,7 @@ impl BridgeStorage { // start pruning blocks let begin = new_pruning_range.oldest_unpruned_block; let end = new_pruning_range.oldest_block_to_keep; + frame_support::debug::trace!(target: "runtime", "Pruning blocks in range [{}..{})", begin, end); for number in begin..end { // if we can't prune anything => break if max_blocks_to_prune == 0 { @@ -588,6 +589,11 @@ impl BridgeStorage { // we have pruned all headers at number new_pruning_range.oldest_unpruned_block = number + 1; + frame_support::debug::trace!( + target: "runtime", + "Oldest unpruned PoA header is now: {}", + new_pruning_range.oldest_unpruned_block, + ); } // update pruning range in storage diff --git a/modules/ethereum/src/mock.rs b/modules/ethereum/src/mock.rs index c169eaec3c87..94986d3d5815 100644 --- a/modules/ethereum/src/mock.rs +++ b/modules/ethereum/src/mock.rs @@ -14,12 +14,11 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -pub use crate::test_utils::{validator_utils::*, HeaderBuilder, GAS_LIMIT}; +pub use crate::test_utils::{insert_header, validator_utils::*, validators_change_receipt, HeaderBuilder, GAS_LIMIT}; pub use primitives::signatures::secret_to_address; -use crate::finality::FinalityVotes; use crate::validators::{ValidatorsConfiguration, ValidatorsSource}; -use crate::{AuraConfiguration, GenesisConfig, HeaderToImport, PruningStrategy, Storage, Trait}; +use crate::{AuraConfiguration, GenesisConfig, PruningStrategy, Trait}; use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; use primitives::{Address, Header, H256, U256}; use secp256k1::SecretKey; @@ -149,20 +148,6 @@ pub fn run_test_with_genesis(genesis: Header, total_validators: usize, test: }) } -/// Insert unverified header into storage. -pub fn insert_header(storage: &mut S, header: Header) { - storage.insert_header(HeaderToImport { - context: storage.import_context(None, &header.parent_hash).unwrap(), - is_best: true, - id: header.compute_id(), - header, - total_difficulty: 0.into(), - enacted_change: None, - scheduled_change: None, - finality_votes: FinalityVotes::default(), - }); -} - /// Pruning strategy that keeps 10 headers behind best block. pub struct KeepSomeHeadersBehindBest(pub u64); diff --git a/modules/ethereum/src/test_utils.rs b/modules/ethereum/src/test_utils.rs index 2a424542d76c..a4c51e4ac686 100644 --- a/modules/ethereum/src/test_utils.rs +++ b/modules/ethereum/src/test_utils.rs @@ -21,12 +21,18 @@ //! //! On the other hand, they may be used directly by the bechmarking module. +// Since this is test code it's fine that not everything is used +#![allow(dead_code)] + +use crate::finality::FinalityVotes; +use crate::validators::CHANGE_EVENT_HASH; use crate::verification::calculate_score; +use crate::{HeaderToImport, Storage}; use primitives::{ rlp_encode, signatures::{secret_to_address, sign, SignHeader}, - Address, Bloom, Header, SealedEmptyStep, H256, U256, + Address, Bloom, Header, Receipt, SealedEmptyStep, H256, U256, }; use secp256k1::SecretKey; use sp_std::prelude::*; @@ -206,6 +212,39 @@ where custom_header.sign_by(author) } +/// Insert unverified header into storage. +pub fn insert_header(storage: &mut S, header: Header) { + storage.insert_header(HeaderToImport { + context: storage.import_context(None, &header.parent_hash).unwrap(), + is_best: true, + id: header.compute_id(), + header, + total_difficulty: 0.into(), + enacted_change: None, + scheduled_change: None, + finality_votes: FinalityVotes::default(), + }); +} + +pub fn validators_change_receipt(parent_hash: H256) -> Receipt { + use primitives::{LogEntry, TransactionOutcome}; + + Receipt { + gas_used: 0.into(), + log_bloom: (&[0xff; 256]).into(), + outcome: TransactionOutcome::Unknown, + logs: vec![LogEntry { + address: [3; 20].into(), + topics: vec![CHANGE_EVENT_HASH.into(), parent_hash], + data: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + ], + }], + } +} + pub mod validator_utils { use super::*; diff --git a/modules/ethereum/src/validators.rs b/modules/ethereum/src/validators.rs index e6a454cc6377..5620bcb86c0a 100644 --- a/modules/ethereum/src/validators.rs +++ b/modules/ethereum/src/validators.rs @@ -20,7 +20,7 @@ use primitives::{Address, Header, HeaderId, LogEntry, Receipt, U256}; use sp_std::prelude::*; /// The hash of InitiateChange event of the validators set contract. -const CHANGE_EVENT_HASH: &'static [u8; 32] = &[ +pub(crate) const CHANGE_EVENT_HASH: &'static [u8; 32] = &[ 0x55, 0x25, 0x2f, 0xa6, 0xee, 0xe4, 0x74, 0x1b, 0x4e, 0x24, 0xa7, 0x4a, 0x70, 0xe9, 0xc1, 0x1f, 0xd2, 0xc2, 0x28, 0x1d, 0xf8, 0xd6, 0xea, 0x13, 0x12, 0x6f, 0xf8, 0x45, 0xf7, 0x82, 0x5c, 0x89, ]; @@ -39,7 +39,7 @@ pub enum ValidatorsConfiguration { /// This source is valid within some blocks range. The blocks range could /// cover multiple epochs - i.e. the validators that are authoring blocks /// within this range could change, but the source itself can not. -#[cfg_attr(test, derive(Debug, PartialEq))] +#[cfg_attr(any(test, feature = "runtime-benchmarks"), derive(Debug, PartialEq))] pub enum ValidatorsSource { /// The validators addresses are hardcoded and never change. List(Vec
), @@ -276,30 +276,13 @@ impl ValidatorsSource { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::mock::{run_test, validators_addresses, TestRuntime}; + use crate::mock::{run_test, validators_addresses, validators_change_receipt, TestRuntime}; use crate::{BridgeStorage, Headers, ScheduledChange, ScheduledChanges, StoredHeader}; use frame_support::StorageMap; - use primitives::{TransactionOutcome, H256}; + use primitives::compute_merkle_root; const TOTAL_VALIDATORS: usize = 3; - pub(crate) fn validators_change_recept(parent_hash: H256) -> Receipt { - Receipt { - gas_used: 0.into(), - log_bloom: (&[0xff; 256]).into(), - outcome: TransactionOutcome::Unknown, - logs: vec![LogEntry { - address: [3; 20].into(), - topics: vec![CHANGE_EVENT_HASH.into(), parent_hash], - data: vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 7, 7, - 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, - ], - }], - } - } - #[test] fn source_at_works() { let config = ValidatorsConfiguration::Multi(vec![ @@ -405,10 +388,8 @@ pub(crate) mod tests { // when we're inside contract range and logs bloom signals change // and there's change in receipts - let receipts = vec![validators_change_recept(Default::default())]; - header.receipts_root = "81ce88dc524403b796222046bf3daf543978329b87ffd50228f1d3987031dc45" - .parse() - .unwrap(); + let receipts = vec![validators_change_receipt(Default::default())]; + header.receipts_root = compute_merkle_root(receipts.iter().map(|r| r.rlp())); assert_eq!( validators.extract_validators_change(&header, Some(receipts)), Ok((Some(vec![[7; 20].into()]), None)), diff --git a/modules/ethereum/src/verification.rs b/modules/ethereum/src/verification.rs index 031b85ab4aba..3c49e81eac1c 100644 --- a/modules/ethereum/src/verification.rs +++ b/modules/ethereum/src/verification.rs @@ -140,6 +140,7 @@ pub fn accept_aura_header_into_pool( // the heaviest, but rare operation - we do not want invalid receipts in the pool if let Some(receipts) = receipts { + frame_support::debug::trace!(target: "runtime", "Got receipts! {:?}", receipts); if !header.verify_receipts_root(receipts) { return Err(Error::TransactionsReceiptsMismatch); } @@ -354,15 +355,15 @@ mod tests { use super::*; use crate::mock::{ insert_header, run_test_with_genesis, test_aura_config, validator, validator_address, validators_addresses, - AccountId, HeaderBuilder, TestRuntime, GAS_LIMIT, + validators_change_receipt, AccountId, HeaderBuilder, TestRuntime, GAS_LIMIT, }; - use crate::validators::{tests::validators_change_recept, ValidatorsSource}; + use crate::validators::ValidatorsSource; use crate::{ pool_configuration, BridgeStorage, FinalizedBlock, Headers, HeadersByNumber, NextValidatorsSetId, ScheduledChanges, ValidatorsSet, ValidatorsSets, }; use frame_support::{StorageMap, StorageValue}; - use primitives::{rlp_encode, TransactionOutcome, H520}; + use primitives::{compute_merkle_root, rlp_encode, TransactionOutcome, H520}; use secp256k1::SecretKey; const GENESIS_STEP: u64 = 42; @@ -844,7 +845,7 @@ mod tests { let header = HeaderBuilder::with_parent_number(3) .log_bloom((&[0xff; 256]).into()) .sign_by_set(validators); - (header, Some(vec![validators_change_recept(Default::default())])) + (header, Some(vec![validators_change_receipt(Default::default())])) }), Err(Error::TransactionsReceiptsMismatch), ); @@ -853,18 +854,17 @@ mod tests { #[test] fn pool_accepts_headers_with_valid_receipts() { let mut hash = None; + let receipts = vec![validators_change_receipt(Default::default())]; + let receipts_root = compute_merkle_root(receipts.iter().map(|r| r.rlp())); + assert_eq!( default_accept_into_pool(|validators| { let header = HeaderBuilder::with_parent_number(3) .log_bloom((&[0xff; 256]).into()) - .receipts_root( - "81ce88dc524403b796222046bf3daf543978329b87ffd50228f1d3987031dc45" - .parse() - .unwrap(), - ) + .receipts_root(receipts_root) .sign_by_set(validators); hash = Some(header.compute_hash()); - (header, Some(vec![validators_change_recept(Default::default())])) + (header, Some(receipts.clone())) }), Ok(( // no tags are required diff --git a/primitives/ethereum-poa/src/lib.rs b/primitives/ethereum-poa/src/lib.rs index a4fac244bf79..95e69f62c513 100644 --- a/primitives/ethereum-poa/src/lib.rs +++ b/primitives/ethereum-poa/src/lib.rs @@ -322,7 +322,7 @@ impl UnsignedTransaction { impl Receipt { /// Returns receipt RLP. - fn rlp(&self) -> Bytes { + pub fn rlp(&self) -> Bytes { let mut s = RlpStream::new(); match self.outcome { TransactionOutcome::Unknown => {