Skip to content

Commit

Permalink
Benchmark Ethereum Pallet (paritytech#149)
Browse files Browse the repository at this point in the history
* Add skeleton for worst case import_unsigned_header

* Fix a typo

* Add benchmark test for best case unsigned header import

* Add finality verification to worst case bench

* Move `insert_header()` from mock to test_utils

Allows the benchmarking code to use this without having to pull it in from the mock.

* Add a rough bench to test a finalizing a "long" chain

* Try to use complexity parameter for finality bench

* Improve long finality bench

* Remove stray dot file

* Remove old "worst" case bench

* Scribble some ideas down for pruning bench

* Prune headers during benchmarking

* Clean up some comments

* Make finality bench work for entire range of complexity parameter

* Place initialization code into a function

* Add bench for block finalization with caching

* First attempt at bench with receipts

* Try and trigger validator set change

* Perform a validator set change during benchmarking

* Move `validators_change_receipt()` to shared location

Allows unit tests and benchmarks to access the same helper function
and const

* Extract a test receipt root into a constant

* Clean up description of pruning bench

* Fix cache and pruning tests

* Remove unecessary `build_custom_header` usage

* Get rid of warnings

* Remove code duplication comment

I don't think its entirely worth it to split out so few lines of code.
The benches aren't particularly hard to read anyways.

* Increase the range of the complexity parameter

* Use dynamic number of receipts while benchmarking

As part of this change we have removed the hardcoded TEST_RECEIPT_ROOT
and instead chose to calculate the receipt root on the fly. This will
make tests and benches less fragile.

* Prune a dynamic number of headers
  • Loading branch information
HCastano authored Jul 10, 2020
1 parent 20eaa7b commit df9e1ec
Show file tree
Hide file tree
Showing 9 changed files with 315 additions and 71 deletions.
22 changes: 22 additions & 0 deletions bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> = 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<u64> = Some(16);
pub KovanAuraConfiguration: pallet_bridge_eth_poa::AuraConfiguration = kovan::kovan_aura_configuration();
Expand Down
237 changes: 225 additions & 12 deletions modules/ethereum/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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! {
_ { }
Expand All @@ -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::<T>(
&initial_header,
initial_difficulty,
&validators_addresses(2),
);
let num_validators = 2;
let initial_header = initialize_bench::<T>(num_validators);

// prepare header to be inserted
let header = build_custom_header(
Expand All @@ -55,6 +51,223 @@ benchmarks! {

}: import_unsigned_header(RawOrigin::None, header, None)
verify {
assert_eq!(BridgeStorage::<T>::new().best_block().0.number, 1);
let storage = BridgeStorage::<T>::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::<T>::new();
let num_validators: u32 = 2;
let initial_header = initialize_bench::<T>(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::<T>::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::<T>::new();
let num_validators: u32 = 2;
let initial_header = initialize_bench::<T>(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::<T>::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::<T>::new();

let num_validators = 3;
let initial_header = initialize_bench::<T>(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::<T>::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::<T>::new();

let num_validators = 1;
let initial_header = initialize_bench::<T>(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::<T>::new();
assert_eq!(storage.best_block().0.number, 2);
}
}

fn initialize_bench<T: Trait>(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::<T>(&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::<TestRuntime>());
});
}

#[test]
fn insert_unsigned_header_finality() {
run_test(1, |_| {
assert_ok!(test_benchmark_import_unsigned_finality::<TestRuntime>());
});
}

#[test]
fn insert_unsigned_header_finality_with_cache() {
run_test(1, |_| {
assert_ok!(test_benchmark_import_unsigned_finality_with_cache::<TestRuntime>());
});
}

#[test]
fn insert_unsigned_header_pruning() {
run_test(1, |_| {
assert_ok!(test_benchmark_import_unsigned_pruning::<TestRuntime>());
});
}

#[test]
fn insert_unsigned_header_receipts() {
run_test(1, |_| {
assert_ok!(test_benchmark_import_unsigned_with_receipts::<TestRuntime>());
});
}
}
6 changes: 2 additions & 4 deletions modules/ethereum/src/import.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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))],);
Expand Down
8 changes: 7 additions & 1 deletion modules/ethereum/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -563,6 +563,7 @@ impl<T: Trait> BridgeStorage<T> {
// 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 {
Expand All @@ -588,6 +589,11 @@ impl<T: Trait> BridgeStorage<T> {

// 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
Expand Down
19 changes: 2 additions & 17 deletions modules/ethereum/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.

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;
Expand Down Expand Up @@ -149,20 +148,6 @@ pub fn run_test_with_genesis<T>(genesis: Header, total_validators: usize, test:
})
}

/// Insert unverified header into storage.
pub fn insert_header<S: Storage>(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);

Expand Down
Loading

0 comments on commit df9e1ec

Please sign in to comment.