Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Benchmark Ethereum Pallet #149

Merged
merged 30 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ff61e17
Add skeleton for worst case import_unsigned_header
HCastano Jun 25, 2020
bdeb817
Fix a typo
HCastano Jun 25, 2020
765ed91
Add benchmark test for best case unsigned header import
HCastano Jun 25, 2020
0201c8c
Add finality verification to worst case bench
HCastano Jun 25, 2020
5b87f54
Move `insert_header()` from mock to test_utils
HCastano Jun 25, 2020
6ff214c
Add a rough bench to test a finalizing a "long" chain
HCastano Jun 25, 2020
c4131e3
Try to use complexity parameter for finality bench
HCastano Jun 26, 2020
e2b3461
Improve long finality bench
HCastano Jun 27, 2020
b345e94
Remove stray dot file
HCastano Jun 27, 2020
199c925
Remove old "worst" case bench
HCastano Jun 27, 2020
92a845d
Scribble some ideas down for pruning bench
HCastano Jun 27, 2020
d464d07
Prune headers during benchmarking
HCastano Jun 27, 2020
4475995
Clean up some comments
HCastano Jun 29, 2020
92c1bf0
Make finality bench work for entire range of complexity parameter
HCastano Jun 29, 2020
4174441
Place initialization code into a function
HCastano Jun 29, 2020
fdb126f
Add bench for block finalization with caching
HCastano Jun 29, 2020
a8e6c56
First attempt at bench with receipts
HCastano Jun 30, 2020
c032a96
Try and trigger validator set change
HCastano Jul 1, 2020
044b2e2
Merge branch 'master' into hc-bench-eth-pallet
HCastano Jul 2, 2020
ca5c1f2
Perform a validator set change during benchmarking
HCastano Jul 2, 2020
b21d07a
Move `validators_change_receipt()` to shared location
HCastano Jul 2, 2020
751929e
Extract a test receipt root into a constant
HCastano Jul 2, 2020
d5eab22
Clean up description of pruning bench
HCastano Jul 3, 2020
1c85d7b
Fix cache and pruning tests
HCastano Jul 3, 2020
51245da
Remove unecessary `build_custom_header` usage
HCastano Jul 3, 2020
a6517ae
Get rid of warnings
HCastano Jul 3, 2020
f8ddaef
Remove code duplication comment
HCastano Jul 5, 2020
029d966
Increase the range of the complexity parameter
HCastano Jul 5, 2020
33035d1
Use dynamic number of receipts while benchmarking
HCastano Jul 6, 2020
777500a
Prune a dynamic number of headers
HCastano Jul 6, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
HCastano marked this conversation as resolved.
Show resolved Hide resolved
#[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;
HCastano marked this conversation as resolved.
Show resolved Hide resolved

// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One last thing I forgot to add to my top-level comment - imo weight(import_signed_headers) is actually weight(import_unsigned_header) * len(headers) + some_constant, where some_constant stands for recovering signature + iterating headers + invoking callbacks (which are noop in our runtime - not sure how to measure that in general case?). So we may only introduce benchmarks for unsigned version and use results for signed version.

// Our complexity parameter, n, will represent the number of blocks imported before
// finalization.
let n in 1..7;
HCastano marked this conversation as resolved.
Show resolved Hide resolved

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;
HCastano marked this conversation as resolved.
Show resolved Hide resolved

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;
HCastano marked this conversation as resolved.
Show resolved Hide resolved

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