diff --git a/bridges/bin/node/node/Cargo.toml b/bridges/bin/node/node/Cargo.toml index 229c3ff637e2..10122e07a1ad 100644 --- a/bridges/bin/node/node/Cargo.toml +++ b/bridges/bin/node/node/Cargo.toml @@ -115,6 +115,16 @@ version = "2.0.0-rc3" rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate.git" +[dependencies.frame-benchmarking] +version = "2.0.0-rc3" +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate.git" + +[dependencies.frame-benchmarking-cli] +version = "2.0.0-rc3" +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate.git" + [build-dependencies] vergen = "3.1.0" @@ -123,3 +133,14 @@ package = "substrate-build-script-utils" version = "2.0.0-rc3" rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate.git" + +[build-dependencies.frame-benchmarking-cli] +version = "2.0.0-rc3" +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate.git" + +[features] +default = [] +runtime-benchmarks = [ + "bridge-node-runtime/runtime-benchmarks", +] diff --git a/bridges/bin/node/node/src/cli.rs b/bridges/bin/node/node/src/cli.rs index 9be42c8f44cc..a7399fa716df 100644 --- a/bridges/bin/node/node/src/cli.rs +++ b/bridges/bin/node/node/src/cli.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -use sc_cli::{RunCmd, Subcommand}; +use sc_cli::RunCmd; use structopt::StructOpt; #[derive(Debug, StructOpt)] @@ -25,3 +25,15 @@ pub struct Cli { #[structopt(flatten)] pub run: RunCmd, } + +/// Possible subcommands of the main binary. +#[derive(Debug, StructOpt)] +pub enum Subcommand { + /// A set of base subcommands handled by `sc_cli`. + #[structopt(flatten)] + Base(sc_cli::Subcommand), + + /// The custom benchmark subcommmand benchmarking runtime pallets. + #[structopt(name = "benchmark", about = "Benchmark runtime pallets.")] + Benchmark(frame_benchmarking_cli::BenchmarkCmd), +} diff --git a/bridges/bin/node/node/src/command.rs b/bridges/bin/node/node/src/command.rs index cfe7115239ee..9a23704decd7 100644 --- a/bridges/bin/node/node/src/command.rs +++ b/bridges/bin/node/node/src/command.rs @@ -30,8 +30,9 @@ // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . -use crate::cli::Cli; +use crate::cli::{Cli, Subcommand}; use crate::service; +use bridge_node_runtime::Block; use sc_cli::SubstrateCli; use sp_consensus_aura::sr25519::AuthorityPair as AuraPair; @@ -81,7 +82,20 @@ pub fn run() -> sc_cli::Result<()> { let cli = Cli::from_args(); match &cli.subcommand { - Some(subcommand) => { + Some(Subcommand::Benchmark(cmd)) => { + if cfg!(feature = "runtime-benchmarks") { + let runner = cli.create_runner(cmd)?; + + runner.sync_run(|config| cmd.run::(config)) + } else { + println!( + "Benchmarking wasn't enabled when building the node. \ + You can enable it with `--features runtime-benchmarks`." + ); + Ok(()) + } + } + Some(Subcommand::Base(subcommand)) => { let runner = cli.create_runner(subcommand)?; runner.run_subcommand(subcommand, |config| Ok(new_full_start!(config).0)) } diff --git a/bridges/bin/node/node/src/service.rs b/bridges/bin/node/node/src/service.rs index 6e09a2c902b2..96728beb81bc 100644 --- a/bridges/bin/node/node/src/service.rs +++ b/bridges/bin/node/node/src/service.rs @@ -33,6 +33,7 @@ native_executor_instance!( pub Executor, bridge_node_runtime::api::dispatch, bridge_node_runtime::native_version, + frame_benchmarking::benchmarking::HostFunctions, ); /// Starts a `ServiceBuilder` for a full service. diff --git a/bridges/bin/node/runtime/Cargo.toml b/bridges/bin/node/runtime/Cargo.toml index 2bc3e25144b9..ddee4842a929 100644 --- a/bridges/bin/node/runtime/Cargo.toml +++ b/bridges/bin/node/runtime/Cargo.toml @@ -195,6 +195,13 @@ default-features = false rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate/" +[dependencies.frame-benchmarking] +optional = true +version = "2.0.0-rc3" +default-features = false +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate/" + [build-dependencies.wasm-builder-runner] version = "1.0.5" package = "substrate-wasm-builder-runner" @@ -209,6 +216,7 @@ std = [ "pallet-bridge-eth-poa/std", "pallet-bridge-currency-exchange/std", "codec/std", + "frame-benchmarking/std", "frame-executive/std", "frame-support/std", "frame-system/std", @@ -234,3 +242,11 @@ std = [ "pallet-timestamp/std", "pallet-transaction-payment/std", ] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-bridge-currency-exchange/runtime-benchmarks", + "pallet-bridge-eth-poa/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] diff --git a/bridges/bin/node/runtime/build.rs b/bridges/bin/node/runtime/build.rs index 81d28a1e9237..4fda040c9bd1 100644 --- a/bridges/bin/node/runtime/build.rs +++ b/bridges/bin/node/runtime/build.rs @@ -19,7 +19,7 @@ use wasm_builder_runner::WasmBuilder; fn main() { WasmBuilder::new() .with_current_project() - .with_wasm_builder_from_crates("1.0.9") + .with_wasm_builder_from_crates("1.0.11") .export_heap_base() .import_memory() .build() diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs index fe1fc8c90587..450bd4d8791d 100644 --- a/bridges/bin/node/runtime/src/lib.rs +++ b/bridges/bin/node/runtime/src/lib.rs @@ -573,6 +573,27 @@ impl_runtime_apis! { None } } + + #[cfg(feature = "runtime-benchmarks")] + impl frame_benchmarking::Benchmark for Runtime { + fn dispatch_benchmark( + pallet: Vec, + benchmark: Vec, + lowest_range_values: Vec, + highest_range_values: Vec, + steps: Vec, + repeat: u32, + ) -> Result, sp_runtime::RuntimeString> { + use frame_benchmarking::{Benchmarking, BenchmarkBatch, add_benchmark}; + let mut batches = Vec::::new(); + let params = (&pallet, &benchmark, &lowest_range_values, &highest_range_values, &steps, repeat); + + add_benchmark!(params, batches, b"bridge-eth-poa", BridgeEthPoA); + + if batches.is_empty() { return Err("Benchmark not found for this pallet.".into()) } + Ok(batches) + } + } } #[cfg(test)] diff --git a/bridges/modules/currency-exchange/Cargo.toml b/bridges/modules/currency-exchange/Cargo.toml index 1a69a41f2516..7847483bc8b7 100644 --- a/bridges/modules/currency-exchange/Cargo.toml +++ b/bridges/modules/currency-exchange/Cargo.toml @@ -35,6 +35,13 @@ default-features = false rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate/" +[dependencies.frame-benchmarking] +optional = true +version = "2.0.0-rc3" +default-features = false +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate/" + [dev-dependencies.sp-core] version = "2.0.0-rc3" rev = "606c56d2e2f69f68f3947551224be6a3515dff60" @@ -48,11 +55,13 @@ git = "https://github.com/paritytech/substrate/" [features] default = ["std"] std = [ - "serde", "codec/std", - "sp-std/std", + "frame-benchmarking/std", "frame-support/std", "frame-system/std", - "sp-runtime/std", + "serde", "sp-currency-exchange/std", + "sp-std/std", + "sp-runtime/std", ] +runtime-benchmarks = ["frame-benchmarking"] diff --git a/bridges/modules/ethereum/Cargo.toml b/bridges/modules/ethereum/Cargo.toml index 818ef12d4026..bb022397f394 100644 --- a/bridges/modules/ethereum/Cargo.toml +++ b/bridges/modules/ethereum/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" [dependencies] serde = { version = "1.0", optional = true } codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false } +hex-literal = "0.2" primitives = { package = "sp-bridge-eth-poa", path = "../../primitives/ethereum-poa", default-features = false } # Substrate Based Dependencies @@ -41,21 +42,40 @@ default-features = false rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate/" +[dependencies.frame-benchmarking] +optional = true +version = "2.0.0-rc3" +default-features = false +rev = "606c56d2e2f69f68f3947551224be6a3515dff60" +git = "https://github.com/paritytech/substrate/" + +[dependencies.libsecp256k1] +optional = true +version = "0.3.4" +default-features = false +features = ["hmac"] + # Dev Dependencies [dev-dependencies] # TODO: Stop renaming this on import primitives = { package = "sp-bridge-eth-poa", path = "../../primitives/ethereum-poa", features = ["std", "test-helpers"] } -parity-crypto = { version = "0.6", features = ["publickey"] } +libsecp256k1 = { version = "0.3.4", features = ["hmac"] } [features] default = ["std"] std = [ - "serde", "codec/std", - "sp-std/std", + "frame-benchmarking/std", "frame-support/std", - "sp-runtime/std", "frame-system/std", - "sp-io/std", "primitives/std", + "serde", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "libsecp256k1", + "primitives/test-helpers", ] diff --git a/bridges/modules/ethereum/src/benchmarking.rs b/bridges/modules/ethereum/src/benchmarking.rs new file mode 100644 index 000000000000..10136c1e5f42 --- /dev/null +++ b/bridges/modules/ethereum/src/benchmarking.rs @@ -0,0 +1,60 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common 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. + +// Parity Bridges Common 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 Parity Bridges Common. If not, see . + +use super::*; + +use crate::test_utils::{build_custom_header, build_genesis_header, validator_utils::*}; + +use frame_benchmarking::benchmarks; +use frame_system::RawOrigin; +use primitives::U256; + +benchmarks! { + _ { } + + // Benchmark `import_unsigned_header` extrinsic with the best possible conditions: + // * Parent header is finalized. + // * New header doesn't require receipts. + // * Nothing is finalized by new header. + // * Nothing is pruned by new header. + 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), + ); + + // prepare header to be inserted + let header = build_custom_header( + &validator(1), + &initial_header, + |mut header| { + header.gas_limit = header.gas_limit + U256::from(n); + header + }, + ); + + }: import_unsigned_header(RawOrigin::None, header, None) + verify { + assert_eq!(BridgeStorage::::new().best_block().0.number, 1); + } +} diff --git a/bridges/modules/ethereum/src/finality.rs b/bridges/modules/ethereum/src/finality.rs index c7bfe67d640b..e113f66831a8 100644 --- a/bridges/modules/ethereum/src/finality.rs +++ b/bridges/modules/ethereum/src/finality.rs @@ -282,13 +282,15 @@ impl Default for FinalityVotes { #[cfg(test)] mod tests { use super::*; - use crate::mock::{custom_test_ext, genesis, insert_header, validator, validators_addresses, TestRuntime}; + use crate::mock::{insert_header, run_test, validator, validators_addresses, HeaderBuilder, TestRuntime}; use crate::{BridgeStorage, FinalityCache, HeaderToImport}; use frame_support::StorageMap; + const TOTAL_VALIDATORS: usize = 5; + #[test] fn verifies_header_author() { - custom_test_ext(genesis(), validators_addresses(5)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { assert_eq!( finalize_blocks( &BridgeStorage::::new(), @@ -306,21 +308,16 @@ mod tests { #[test] fn finalize_blocks_works() { - custom_test_ext(genesis(), validators_addresses(5)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |ctx| { // let's say we have 5 validators (we need 'votes' from 3 validators to achieve // finality) let mut storage = BridgeStorage::::new(); // when header#1 is inserted, nothing is finalized (1 vote) - let header1 = Header { - author: validator(0).address().as_fixed_bytes().into(), - parent_hash: genesis().compute_hash(), - number: 1, - ..Default::default() - }; + let header1 = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(0)); let id1 = header1.compute_id(); let mut header_to_import = HeaderToImport { - context: storage.import_context(None, &genesis().compute_hash()).unwrap(), + context: storage.import_context(None, &header1.parent_hash).unwrap(), is_best: true, id: id1, header: header1, @@ -332,8 +329,8 @@ mod tests { assert_eq!( finalize_blocks( &storage, - genesis().compute_id(), - (Default::default(), &validators_addresses(5)), + ctx.genesis.compute_id(), + (Default::default(), &ctx.addresses), id1, None, &header_to_import.header, @@ -345,19 +342,14 @@ mod tests { storage.insert_header(header_to_import.clone()); // when header#2 is inserted, nothing is finalized (2 votes) - header_to_import.header = Header { - author: validator(1).address().as_fixed_bytes().into(), - parent_hash: id1.hash, - number: 2, - ..Default::default() - }; + header_to_import.header = HeaderBuilder::with_parent_hash(id1.hash).sign_by(&validator(1)); header_to_import.id = header_to_import.header.compute_id(); let id2 = header_to_import.header.compute_id(); assert_eq!( finalize_blocks( &storage, - genesis().compute_id(), - (Default::default(), &validators_addresses(5)), + ctx.genesis.compute_id(), + (Default::default(), &ctx.addresses), id2, None, &header_to_import.header, @@ -369,19 +361,14 @@ mod tests { storage.insert_header(header_to_import.clone()); // when header#3 is inserted, header#1 is finalized (3 votes) - header_to_import.header = Header { - author: validator(2).address().as_fixed_bytes().into(), - parent_hash: id2.hash, - number: 3, - ..Default::default() - }; + header_to_import.header = HeaderBuilder::with_parent_hash(id2.hash).sign_by(&validator(2)); header_to_import.id = header_to_import.header.compute_id(); let id3 = header_to_import.header.compute_id(); assert_eq!( finalize_blocks( &storage, - genesis().compute_id(), - (Default::default(), &validators_addresses(5)), + ctx.genesis.compute_id(), + (Default::default(), &ctx.addresses), id3, None, &header_to_import.header, @@ -404,11 +391,7 @@ mod tests { // 2) add votes from header#4 and header#5 let validators = validators_addresses(5); let headers = (1..6) - .map(|number| Header { - number: number, - author: validators[number as usize - 1], - ..Default::default() - }) + .map(|number| HeaderBuilder::with_number(number).sign_by(&validator(number as usize - 1))) .collect::>(); let ancestry = headers .iter() @@ -451,8 +434,7 @@ mod tests { #[test] fn prepare_votes_respects_finality_cache() { - let validators_addresses = validators_addresses(5); - custom_test_ext(genesis(), validators_addresses.clone()).execute_with(move || { + run_test(TOTAL_VALIDATORS, |ctx| { // we need signatures of 3 validators to finalize block let mut storage = BridgeStorage::::new(); @@ -462,14 +444,9 @@ mod tests { let mut hashes = Vec::new(); let mut headers = Vec::new(); let mut ancestry = Vec::new(); - let mut parent_hash = genesis().compute_hash(); + let mut parent_hash = ctx.genesis.compute_hash(); for i in 1..10 { - let header = Header { - author: validator((i - 1) / 3).address().as_fixed_bytes().into(), - parent_hash, - number: i as _, - ..Default::default() - }; + let header = HeaderBuilder::with_parent_hash(parent_hash).sign_by(&validator((i - 1) / 3)); let id = header.compute_id(); insert_header(&mut storage, header.clone()); hashes.push(id.hash); @@ -486,9 +463,9 @@ mod tests { // check that votes at #7 are computed correctly without cache let expected_votes_at_7 = FinalityVotes { votes: vec![ - (validators_addresses[0].clone(), 3), - (validators_addresses[1].clone(), 3), - (validators_addresses[2].clone(), 1), + (ctx.addresses[0].clone(), 3), + (ctx.addresses[1].clone(), 3), + (ctx.addresses[2].clone(), 1), ] .into_iter() .collect(), @@ -499,11 +476,11 @@ mod tests { prepare_votes( storage.cached_finality_votes( &headers.get(5).unwrap().compute_id(), - &genesis().compute_id(), + &ctx.genesis.compute_id(), |_| false, ), Default::default(), - &validators_addresses.iter().collect(), + &ctx.addresses.iter().collect(), id7, headers.get(6).unwrap(), None, @@ -514,12 +491,9 @@ mod tests { // cached votes at #5 let expected_votes_at_5 = FinalityVotes { - votes: vec![ - (validators_addresses[0].clone(), 3), - (validators_addresses[1].clone(), 2), - ] - .into_iter() - .collect(), + votes: vec![(ctx.addresses[0].clone(), 3), (ctx.addresses[1].clone(), 2)] + .into_iter() + .collect(), ancestry: ancestry[..5].iter().cloned().collect(), }; FinalityCache::::insert(hashes[4], expected_votes_at_5); @@ -530,11 +504,11 @@ mod tests { prepare_votes( storage.cached_finality_votes( &headers.get(5).unwrap().compute_id(), - &genesis().compute_id(), + &ctx.genesis.compute_id(), |_| false, ), Default::default(), - &validators_addresses.iter().collect(), + &ctx.addresses.iter().collect(), id7, headers.get(6).unwrap(), None, @@ -546,12 +520,9 @@ mod tests { // when we're inserting header#7 and last finalized header is 3: // check that votes at #7 are computed correctly with cache let expected_votes_at_7 = FinalityVotes { - votes: vec![ - (validators_addresses[1].clone(), 3), - (validators_addresses[2].clone(), 1), - ] - .into_iter() - .collect(), + votes: vec![(ctx.addresses[1].clone(), 3), (ctx.addresses[2].clone(), 1)] + .into_iter() + .collect(), ancestry: ancestry[3..7].iter().cloned().collect(), }; assert_eq!( @@ -562,7 +533,7 @@ mod tests { |hash| *hash == hashes[2], ), headers[2].compute_id(), - &validators_addresses.iter().collect(), + &ctx.addresses.iter().collect(), id7, headers.get(6).unwrap(), None, diff --git a/bridges/modules/ethereum/src/import.rs b/bridges/modules/ethereum/src/import.rs index 4ae91bb70c70..ab375d256b8e 100644 --- a/bridges/modules/ethereum/src/import.rs +++ b/bridges/modules/ethereum/src/import.rs @@ -162,17 +162,19 @@ pub fn header_import_requires_receipts( mod tests { use super::*; use crate::mock::{ - block_i, custom_block_i, custom_test_ext, genesis, signed_header, test_aura_config, test_validators_config, - validator, validators, validators_addresses, KeepSomeHeadersBehindBest, TestRuntime, GENESIS_STEP, + run_test, secret_to_address, test_aura_config, test_validators_config, validator, validators_addresses, + HeaderBuilder, KeepSomeHeadersBehindBest, TestRuntime, GAS_LIMIT, }; use crate::validators::ValidatorsSource; use crate::{BlocksToPrune, BridgeStorage, Headers, PruningRange}; use frame_support::{StorageMap, StorageValue}; - use parity_crypto::publickey::KeyPair; + use secp256k1::SecretKey; + + const TOTAL_VALIDATORS: usize = 3; #[test] fn rejects_finalized_block_competitors() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let mut storage = BridgeStorage::::new(); storage.finalize_and_prune_headers( Some(HeaderId { @@ -198,10 +200,9 @@ mod tests { #[test] fn rejects_known_header() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { - let validators = validators(3); + run_test(TOTAL_VALIDATORS, |ctx| { let mut storage = BridgeStorage::::new(); - let block = block_i(1, &validators); + let header = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(1)); assert_eq!( import_header( &mut storage, @@ -209,7 +210,7 @@ mod tests { &test_aura_config(), &test_validators_config(), None, - block.clone(), + header.clone(), None, ) .map(|_| ()), @@ -222,7 +223,7 @@ mod tests { &test_aura_config(), &test_validators_config(), None, - block, + header, None, ) .map(|_| ()), @@ -233,14 +234,13 @@ mod tests { #[test] fn import_header_works() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |ctx| { let validators_config = ValidatorsConfiguration::Multi(vec![ - (0, ValidatorsSource::List(validators_addresses(3))), + (0, ValidatorsSource::List(ctx.addresses.clone())), (1, ValidatorsSource::List(validators_addresses(2))), ]); - let validators = validators(3); let mut storage = BridgeStorage::::new(); - let header = block_i(1, &validators); + let header = HeaderBuilder::with_parent(&ctx.genesis).sign_by(&validator(1)); let hash = header.compute_hash(); assert_eq!( import_header( @@ -267,9 +267,9 @@ mod tests { #[test] fn headers_are_pruned_during_import() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |ctx| { let validators_config = - ValidatorsConfiguration::Single(ValidatorsSource::Contract([3; 20].into(), validators_addresses(3))); + ValidatorsConfiguration::Single(ValidatorsSource::Contract([3; 20].into(), ctx.addresses.clone())); let validators = vec![validator(0), validator(1), validator(2)]; let mut storage = BridgeStorage::::new(); @@ -277,7 +277,9 @@ mod tests { // => since we want to keep 10 finalized blocks, we aren't pruning anything let mut latest_block_id = Default::default(); for i in 1..11 { - let header = block_i(i, &validators); + let header = HeaderBuilder::with_parent_number(i - 1).sign_by_set(&validators); + let parent_id = header.parent_id().unwrap(); + let (rolling_last_block_id, finalized_blocks) = import_header( &mut storage, &mut KeepSomeHeadersBehindBest::default(), @@ -289,26 +291,24 @@ mod tests { ) .unwrap(); match i { - 2..=10 => assert_eq!( - finalized_blocks, - vec![(block_i(i - 1, &validators).compute_id(), Some(100))], - "At {}", - i, - ), + 2..=10 => assert_eq!(finalized_blocks, vec![(parent_id, Some(100))], "At {}", i,), _ => assert_eq!(finalized_blocks, vec![], "At {}", i), } latest_block_id = rolling_last_block_id; } - assert!(storage.header(&genesis().compute_hash()).is_some()); + assert!(storage.header(&ctx.genesis.compute_hash()).is_some()); // header 11 finalizes headers [10] AND schedules change // => we prune header#0 - let header11 = custom_block_i(11, &validators, |header| { - header.log_bloom = (&[0xff; 256]).into(); - header.receipts_root = "2e60346495092587026484e868a5b3063749032b2ea3843844509a6320d7f951" - .parse() - .unwrap(); - }); + let header11 = HeaderBuilder::with_parent_number(10) + .log_bloom((&[0xff; 256]).into()) + .receipts_root( + "ead6c772ba0083bbff497ba0f4efe47c199a2655401096c21ab7450b6c466d97" + .parse() + .unwrap(), + ) + .sign_by_set(&validators); + let parent_id = header11.parent_id().unwrap(); let (rolling_last_block_id, finalized_blocks) = import_header( &mut storage, &mut KeepSomeHeadersBehindBest::default(), @@ -321,29 +321,20 @@ mod tests { )]), ) .unwrap(); - assert_eq!( - finalized_blocks, - vec![(block_i(10, &validators).compute_id(), Some(100))], - ); - assert!(storage.header(&genesis().compute_hash()).is_none()); + assert_eq!(finalized_blocks, vec![(parent_id, Some(100))],); + assert!(storage.header(&ctx.genesis.compute_hash()).is_none()); latest_block_id = rolling_last_block_id; // and now let's say validators 1 && 2 went offline // => in the range 12-25 no blocks are finalized, but we still continue to prune old headers // until header#11 is met. we can't prune #11, because it schedules change - let mut step = 56; + let mut step = 56u64; let mut expected_blocks = vec![(header11.compute_id(), Some(101))]; for i in 12..25 { - let header = Header { - number: i as _, - parent_hash: latest_block_id.hash, - gas_limit: 0x2000.into(), - author: validator(2).address(), - seal: vec![vec![step].into(), vec![].into()], - difficulty: i.into(), - ..Default::default() - }; - let header = signed_header(&validators, header, step as _); + let header = HeaderBuilder::with_parent_hash(latest_block_id.hash) + .difficulty(i.into()) + .step(step) + .sign_by_set(&validators); expected_blocks.push((header.compute_id(), Some(102))); let (rolling_last_block_id, finalized_blocks) = import_header( &mut storage, @@ -370,16 +361,10 @@ mod tests { // now let's insert block signed by validator 1 // => blocks 11..24 are finalized and blocks 11..14 are pruned step -= 2; - let header = Header { - number: 25, - parent_hash: latest_block_id.hash, - gas_limit: 0x2000.into(), - author: validator(0).address(), - seal: vec![vec![step].into(), vec![].into()], - difficulty: 25.into(), - ..Default::default() - }; - let header = signed_header(&validators, header, step as _); + let header = HeaderBuilder::with_parent_hash(latest_block_id.hash) + .difficulty(25.into()) + .step(step) + .sign_by_set(&validators); let (_, finalized_blocks) = import_header( &mut storage, &mut KeepSomeHeadersBehindBest::default(), @@ -403,18 +388,9 @@ mod tests { fn import_custom_block( storage: &mut S, - validators: &[KeyPair], - number: u64, - step: u64, - customize: impl FnOnce(&mut Header), + validators: &[SecretKey], + header: Header, ) -> Result { - let header = custom_block_i(number, validators, |header| { - header.seal[0][0] = step as _; - header.author = - crate::validators::step_validator(&validators.iter().map(|kp| kp.address()).collect::>(), step); - customize(header); - }); - let header = signed_header(validators, header, step); let id = header.compute_id(); import_header( storage, @@ -422,7 +398,7 @@ mod tests { &test_aura_config(), &ValidatorsConfiguration::Single(ValidatorsSource::Contract( [0; 20].into(), - validators.iter().map(|kp| kp.address()).collect(), + validators.iter().map(secret_to_address).collect(), )), None, header, @@ -433,37 +409,41 @@ mod tests { #[test] fn import_of_non_best_block_may_finalize_blocks() { - const TOTAL_VALIDATORS: u8 = 3; - let validators_addresses = validators_addresses(TOTAL_VALIDATORS); - custom_test_ext(genesis(), validators_addresses.clone()).execute_with(move || { - let validators = validators(TOTAL_VALIDATORS); + run_test(TOTAL_VALIDATORS, |ctx| { let mut storage = BridgeStorage::::new(); // insert headers (H1, validator1), (H2, validator1), (H3, validator1) // making H3 the best header, without finalizing anything (we need 2 signatures) let mut expected_best_block = Default::default(); for i in 1..4 { - let step = GENESIS_STEP + i * TOTAL_VALIDATORS as u64; - expected_best_block = import_custom_block(&mut storage, &validators, i, step, |header| { - header.author = validators_addresses[0]; - header.seal[0][0] = step as u8; - }) + let step = 1 + i * TOTAL_VALIDATORS as u64; + expected_best_block = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(i - 1) + .step(step) + .sign_by_set(&ctx.validators), + ) .unwrap(); } let (best_block, best_difficulty) = storage.best_block(); assert_eq!(best_block, expected_best_block); - assert_eq!(storage.finalized_block(), genesis().compute_id()); + assert_eq!(storage.finalized_block(), ctx.genesis.compute_id()); // insert headers (H1', validator1), (H2', validator2), finalizing H2, even though H3 // has better difficulty than H2' (because there are more steps involved) let mut expected_finalized_block = Default::default(); - let mut parent_hash = genesis().compute_hash(); + let mut parent_hash = ctx.genesis.compute_hash(); for i in 1..3 { - let step = GENESIS_STEP + i; - let id = import_custom_block(&mut storage, &validators, i, step, |header| { - header.gas_limit += 1.into(); - header.parent_hash = parent_hash; - }) + let step = i; + let id = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_hash(parent_hash) + .step(step) + .gas_limit((GAS_LIMIT + 1).into()) + .sign_by_set(&ctx.validators), + ) .unwrap(); parent_hash = id.hash; if i == 1 { @@ -479,83 +459,117 @@ mod tests { #[test] fn append_to_unfinalized_fork_fails() { - const TOTAL_VALIDATORS: u64 = 5; - let validators_addresses = validators_addresses(TOTAL_VALIDATORS as _); - custom_test_ext(genesis(), validators_addresses.clone()).execute_with(move || { - let validators = validators(TOTAL_VALIDATORS as _); + const VALIDATORS: u64 = 5; + run_test(VALIDATORS as usize, |ctx| { let mut storage = BridgeStorage::::new(); // header1, authored by validator[2] is best common block between two competing forks - let header1 = import_custom_block(&mut storage, &validators, 1, GENESIS_STEP + 1, |_| ()).unwrap(); + let header1 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(0) + .step(2) + .sign_by_set(&ctx.validators), + ) + .unwrap(); assert_eq!(storage.best_block().0, header1); assert_eq!(storage.finalized_block().number, 0); // validator[3] has authored header2 (nothing is finalized yet) - let header2 = import_custom_block(&mut storage, &validators, 2, GENESIS_STEP + 2, |_| ()).unwrap(); + let header2 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(1) + .step(3) + .sign_by_set(&ctx.validators), + ) + .unwrap(); assert_eq!(storage.best_block().0, header2); assert_eq!(storage.finalized_block().number, 0); // validator[4] has authored header3 (header1 is finalized) - let header3 = import_custom_block(&mut storage, &validators, 3, GENESIS_STEP + 3, |_| ()).unwrap(); + let header3 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(2) + .step(4) + .sign_by_set(&ctx.validators), + ) + .unwrap(); assert_eq!(storage.best_block().0, header3); assert_eq!(storage.finalized_block(), header1); // validator[4] has authored 4 blocks: header2'...header5' (header1 is still finalized) - let header2_1 = import_custom_block(&mut storage, &validators, 2, GENESIS_STEP + 3, |header| { - header.gas_limit += 1.into(); - }) + let header2_1 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(1) + .gas_limit((GAS_LIMIT + 1).into()) + .step(4) + .sign_by_set(&ctx.validators), + ) .unwrap(); let header3_1 = import_custom_block( &mut storage, - &validators, - 3, - GENESIS_STEP + 3 + TOTAL_VALIDATORS, - |header| { - header.parent_hash = header2_1.hash; - }, + &ctx.validators, + HeaderBuilder::with_parent_hash(header2_1.hash) + .step(4 + VALIDATORS) + .sign_by_set(&ctx.validators), ) .unwrap(); let header4_1 = import_custom_block( &mut storage, - &validators, - 4, - GENESIS_STEP + 3 + TOTAL_VALIDATORS * 2, - |header| { - header.parent_hash = header3_1.hash; - }, + &ctx.validators, + HeaderBuilder::with_parent_hash(header3_1.hash) + .step(4 + VALIDATORS * 2) + .sign_by_set(&ctx.validators), ) .unwrap(); let header5_1 = import_custom_block( &mut storage, - &validators, - 5, - GENESIS_STEP + 3 + TOTAL_VALIDATORS * 3, - |header| { - header.parent_hash = header4_1.hash; - }, + &ctx.validators, + HeaderBuilder::with_parent_hash(header4_1.hash) + .step(4 + VALIDATORS * 3) + .sign_by_set(&ctx.validators), ) .unwrap(); assert_eq!(storage.best_block().0, header5_1); assert_eq!(storage.finalized_block(), header1); // when we import header4 { parent = header3 }, authored by validator[0], header2 is finalized - let header4 = import_custom_block(&mut storage, &validators, 4, GENESIS_STEP + 4, |_| ()).unwrap(); + let header4 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(3) + .step(5) + .sign_by_set(&ctx.validators), + ) + .unwrap(); assert_eq!(storage.best_block().0, header5_1); assert_eq!(storage.finalized_block(), header2); // when we import header5 { parent = header4 }, authored by validator[1], header3 is finalized - let _ = import_custom_block(&mut storage, &validators, 5, GENESIS_STEP + 5, |header| { - header.parent_hash = header4.hash; - }) + let header5 = import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_hash(header4.hash) + .step(6) + .sign_by_set(&ctx.validators), + ) .unwrap(); - assert_eq!(storage.best_block().0, header5_1); + assert_eq!(storage.best_block().0, header5); assert_eq!(storage.finalized_block(), header3); // import of header2'' { parent = header1 } fails, because it has number < best_finalized assert_eq!( - import_custom_block(&mut storage, &validators, 2, GENESIS_STEP + 3, |header| { - header.gas_limit += 2.into(); - }), + import_custom_block( + &mut storage, + &ctx.validators, + HeaderBuilder::with_parent_number(1) + .gas_limit((GAS_LIMIT + 1).into()) + .step(3) + .sign_by_set(&ctx.validators) + ), Err(Error::AncientHeader), ); @@ -564,10 +578,11 @@ mod tests { assert_eq!( import_custom_block( &mut storage, - &validators, - 6, - GENESIS_STEP + 3 + TOTAL_VALIDATORS * 4, - |_| () + &ctx.validators, + HeaderBuilder::with_parent_number(5) + .gas_limit((GAS_LIMIT + 1).into()) + .step(5 + VALIDATORS * 4) + .sign_by_set(&ctx.validators), ), Err(Error::TryingToFinalizeSibling), ); diff --git a/bridges/modules/ethereum/src/lib.rs b/bridges/modules/ethereum/src/lib.rs index 4c495a8c1489..1ff2c2a8ae0b 100644 --- a/bridges/modules/ethereum/src/lib.rs +++ b/bridges/modules/ethereum/src/lib.rs @@ -37,9 +37,15 @@ mod import; mod validators; mod verification; +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + #[cfg(test)] mod mock; +#[cfg(any(feature = "runtime-benchmarks", test))] +mod test_utils; + /// Maximal number of blocks we're pruning in single import call. const MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT: u64 = 8; @@ -464,32 +470,11 @@ decl_storage! { "Initial validators set can't be empty", ); - let initial_hash = config.initial_header.compute_hash(); - let initial_id = HeaderId { - number: config.initial_header.number, - hash: initial_hash, - }; - BestBlock::put((initial_id, config.initial_difficulty)); - FinalizedBlock::put(initial_id); - BlocksToPrune::put(PruningRange { - oldest_unpruned_block: config.initial_header.number, - oldest_block_to_keep: config.initial_header.number, - }); - HeadersByNumber::insert(config.initial_header.number, vec![initial_hash]); - Headers::::insert(initial_hash, StoredHeader { - submitter: None, - header: config.initial_header.clone(), - total_difficulty: config.initial_difficulty, - next_validators_set_id: 0, - last_signal_block: None, - }); - NextValidatorsSetId::put(1); - ValidatorsSets::insert(0, ValidatorsSet { - validators: config.initial_validators.clone(), - signal_block: None, - enact_block: initial_id, - }); - ValidatorsSetsRc::insert(0, 1); + initialize_storage::( + &config.initial_header, + config.initial_difficulty, + &config.initial_validators, + ); }) } } @@ -632,6 +617,13 @@ impl BridgeStorage { // physically remove headers and (probably) obsolete validators sets while let Some(hash) = blocks_at_number.pop() { let header = Headers::::take(&hash); + frame_support::debug::trace!( + target: "runtime", + "Pruning PoA header: ({}, {})", + number, + hash, + ); + ScheduledChanges::remove(hash); FinalityCache::::remove(hash); if let Some(header) = header { @@ -830,6 +822,53 @@ impl Storage for BridgeStorage { } } +/// Initialize storage. +pub(crate) fn initialize_storage( + initial_header: &Header, + initial_difficulty: U256, + initial_validators: &[Address], +) { + let initial_hash = initial_header.compute_hash(); + frame_support::debug::trace!( + target: "runtime", + "Initializing bridge with PoA header: ({}, {})", + initial_header.number, + initial_hash, + ); + + let initial_id = HeaderId { + number: initial_header.number, + hash: initial_hash, + }; + BestBlock::put((initial_id, initial_difficulty)); + FinalizedBlock::put(initial_id); + BlocksToPrune::put(PruningRange { + oldest_unpruned_block: initial_header.number, + oldest_block_to_keep: initial_header.number, + }); + HeadersByNumber::insert(initial_header.number, vec![initial_hash]); + Headers::::insert( + initial_hash, + StoredHeader { + submitter: None, + header: initial_header.clone(), + total_difficulty: initial_difficulty, + next_validators_set_id: 0, + last_signal_block: None, + }, + ); + NextValidatorsSetId::put(1); + ValidatorsSets::insert( + 0, + ValidatorsSet { + validators: initial_validators.to_vec(), + signal_block: None, + enact_block: initial_id, + }, + ); + ValidatorsSetsRc::insert(0, 1); +} + /// Verify that transaction is included into given finalized block. pub fn verify_transaction_finalized( storage: &S, @@ -894,10 +933,13 @@ pub(crate) mod tests { use super::*; use crate::finality::FinalityAncestor; use crate::mock::{ - block_i, custom_block_i, custom_test_ext, genesis, insert_header, validators, validators_addresses, TestRuntime, + genesis, insert_header, run_test, run_test_with_genesis, validators_addresses, HeaderBuilder, TestRuntime, + GAS_LIMIT, }; use primitives::compute_merkle_root; + const TOTAL_VALIDATORS: usize = 3; + fn example_tx() -> Vec { vec![42] } @@ -919,14 +961,13 @@ pub(crate) mod tests { } fn with_headers_to_prune(f: impl Fn(BridgeStorage) -> T) -> T { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { - let validators = validators(3); + run_test(TOTAL_VALIDATORS, |ctx| { for i in 1..10 { let mut headers_by_number = Vec::with_capacity(5); for j in 0..5 { - let header = custom_block_i(i, &validators, |header| { - header.gas_limit = header.gas_limit + U256::from(j); - }); + let header = HeaderBuilder::with_parent_number(i - 1) + .gas_limit((GAS_LIMIT + j).into()) + .sign_by_set(&ctx.validators); let hash = header.compute_hash(); headers_by_number.push(hash); Headers::::insert( @@ -1082,21 +1123,20 @@ pub(crate) mod tests { #[test] fn finality_votes_are_cached() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |ctx| { let mut storage = BridgeStorage::::new(); let interval = ::FinalityVotesCachingInterval::get().unwrap(); // for all headers with number < interval, cache entry is not created - let validators = validators(3); for i in 1..interval { - let header = block_i(i, &validators); + let header = HeaderBuilder::with_parent_number(i - 1).sign_by_set(&ctx.validators); let id = header.compute_id(); insert_header(&mut storage, header); assert_eq!(FinalityCache::::get(&id.hash), None); } // for header with number = interval, cache entry is created - let header_with_entry = block_i(interval, &validators); + let header_with_entry = HeaderBuilder::with_parent_number(interval - 1).sign_by_set(&ctx.validators); let header_with_entry_hash = header_with_entry.compute_hash(); insert_header(&mut storage, header_with_entry); assert_eq!( @@ -1116,13 +1156,12 @@ pub(crate) mod tests { #[test] fn cached_finality_votes_finds_entry() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |ctx| { // insert 5 headers - let validators = validators(3); let mut storage = BridgeStorage::::new(); let mut headers = Vec::new(); for i in 1..5 { - let header = block_i(i, &validators); + let header = HeaderBuilder::with_parent_number(i - 1).sign_by_set(&ctx.validators); headers.push(header.clone()); insert_header(&mut storage, header); } @@ -1177,19 +1216,18 @@ pub(crate) mod tests { #[test] fn cached_finality_votes_stops_at_finalized_sibling() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { - let validators = validators(3); + run_test(TOTAL_VALIDATORS, |ctx| { let mut storage = BridgeStorage::::new(); // insert header1 - let header1 = block_i(1, &validators); + let header1 = HeaderBuilder::with_parent_number(0).sign_by_set(&ctx.validators); let header1_id = header1.compute_id(); insert_header(&mut storage, header1); // insert header1' - sibling of header1 - let header1s = custom_block_i(1, &validators, |header| { - header.gas_limit += 1.into(); - }); + let header1s = HeaderBuilder::with_parent_number(0) + .gas_limit((GAS_LIMIT + 1).into()) + .sign_by_set(&ctx.validators); let header1s_id = header1s.compute_id(); insert_header(&mut storage, header1s); @@ -1214,7 +1252,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_works_for_best_finalized_header() { - custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| { let storage = BridgeStorage::::new(); assert_eq!( verify_transaction_finalized(&storage, example_header().compute_hash(), 0, &vec![example_tx()],), @@ -1225,7 +1263,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_works_for_best_finalized_header_ancestor() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let mut storage = BridgeStorage::::new(); insert_header(&mut storage, example_header_parent()); insert_header(&mut storage, example_header()); @@ -1239,7 +1277,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_proof_with_missing_tx() { - custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| { let storage = BridgeStorage::::new(); assert_eq!( verify_transaction_finalized(&storage, example_header().compute_hash(), 1, &vec![],), @@ -1250,7 +1288,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_unknown_header() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let storage = BridgeStorage::::new(); assert_eq!( verify_transaction_finalized(&storage, example_header().compute_hash(), 1, &vec![],), @@ -1261,7 +1299,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_unfinalized_header() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let mut storage = BridgeStorage::::new(); insert_header(&mut storage, example_header_parent()); insert_header(&mut storage, example_header()); @@ -1274,7 +1312,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_finalized_header_sibling() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let mut finalized_header_sibling = example_header(); finalized_header_sibling.timestamp = 1; let finalized_header_sibling_hash = finalized_header_sibling.compute_hash(); @@ -1293,7 +1331,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_finalized_header_uncle() { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let mut finalized_header_uncle = example_header_parent(); finalized_header_uncle.timestamp = 1; let finalized_header_uncle_hash = finalized_header_uncle.compute_hash(); @@ -1312,7 +1350,7 @@ pub(crate) mod tests { #[test] fn verify_transaction_finalized_rejects_invalid_proof() { - custom_test_ext(example_header(), validators_addresses(3)).execute_with(|| { + run_test_with_genesis(example_header(), TOTAL_VALIDATORS, |_| { let storage = BridgeStorage::::new(); assert_eq!( verify_transaction_finalized( diff --git a/bridges/modules/ethereum/src/mock.rs b/bridges/modules/ethereum/src/mock.rs index 1575f6cf405d..c169eaec3c87 100644 --- a/bridges/modules/ethereum/src/mock.rs +++ b/bridges/modules/ethereum/src/mock.rs @@ -14,14 +14,15 @@ // 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 primitives::signatures::secret_to_address; + use crate::finality::FinalityVotes; use crate::validators::{ValidatorsConfiguration, ValidatorsSource}; -use crate::{AuraConfiguration, GenesisConfig, HeaderToImport, HeadersByNumber, PruningStrategy, Storage, Trait}; -use frame_support::StorageMap; +use crate::{AuraConfiguration, GenesisConfig, HeaderToImport, PruningStrategy, Storage, Trait}; use frame_support::{impl_outer_origin, parameter_types, weights::Weight}; -use parity_crypto::publickey::{sign, KeyPair, Secret}; -use primitives::{rlp_encode, H520}; use primitives::{Address, Header, H256, U256}; +use secp256k1::SecretKey; use sp_runtime::{ testing::Header as SubstrateHeader, traits::{BlakeTwo256, IdentityLookup}, @@ -84,8 +85,17 @@ impl Trait for TestRuntime { type OnHeadersSubmitted = (); } -/// Step of genesis header. -pub const GENESIS_STEP: u64 = 42; +/// Test context. +pub struct TestContext { + /// Initial (genesis) header. + pub genesis: Header, + /// Number of initial validators. + pub total_validators: usize, + /// Secret keys of validators, ordered by validator index. + pub validators: Vec, + /// Addresses of validators, ordered by validator index. + pub addresses: Vec
, +} /// Aura configuration that is used in tests by default. pub fn test_aura_config() -> AuraConfiguration { @@ -108,72 +118,38 @@ pub fn test_validators_config() -> ValidatorsConfiguration { /// Genesis header that is used in tests by default. pub fn genesis() -> Header { - Header { - seal: vec![vec![GENESIS_STEP as _].into(), vec![].into()], - ..Default::default() - } -} - -/// Build default i-th block, using data from runtime storage. -pub fn block_i(number: u64, validators: &[KeyPair]) -> Header { - custom_block_i(number, validators, |_| {}) -} - -/// Build custom i-th block, using data from runtime storage. -pub fn custom_block_i(number: u64, validators: &[KeyPair], customize: impl FnOnce(&mut Header)) -> Header { - let validator_index: u8 = (number % (validators.len() as u64)) as _; - let mut header = Header { - number, - parent_hash: HeadersByNumber::get(number - 1).unwrap()[0].clone(), - gas_limit: 0x2000.into(), - author: validator(validator_index).address(), - seal: vec![vec![(number + GENESIS_STEP) as u8].into(), vec![].into()], - difficulty: number.into(), - ..Default::default() - }; - customize(&mut header); - signed_header(validators, header, number + GENESIS_STEP) -} - -/// Build signed header from given header. -pub fn signed_header(validators: &[KeyPair], mut header: Header, step: u64) -> Header { - let message = header.seal_hash(false).unwrap(); - let validator_index = (step % validators.len() as u64) as usize; - let signature = sign(validators[validator_index].secret(), &message.as_fixed_bytes().into()).unwrap(); - let signature: [u8; 65] = signature.into(); - let signature = H520::from(signature); - header.seal[1] = rlp_encode(&signature); - header -} - -/// Return key pair of given test validator. -pub fn validator(index: u8) -> KeyPair { - KeyPair::from_secret(Secret::from([index + 1; 32])).unwrap() -} - -/// Return key pairs of all test validators. -pub fn validators(count: u8) -> Vec { - (0..count).map(validator).collect() -} - -/// Return addresses of all test validators. -pub fn validators_addresses(count: u8) -> Vec
{ - (0..count).map(|i| validator(i).address()).collect() -} - -/// Prepare externalities to start with custom initial header. -pub fn custom_test_ext(initial_header: Header, initial_validators: Vec
) -> sp_io::TestExternalities { - let t = GenesisConfig { - initial_header, - initial_difficulty: 0.into(), - initial_validators, - } - .build_storage::() - .unwrap(); - sp_io::TestExternalities::new(t) -} - -/// Insert header into storage. + HeaderBuilder::genesis().sign_by(&validator(0)) +} + +/// Run test with default genesis header. +pub fn run_test(total_validators: usize, test: impl FnOnce(TestContext) -> T) -> T { + run_test_with_genesis(genesis(), total_validators, test) +} + +/// Run test with default genesis header. +pub fn run_test_with_genesis(genesis: Header, total_validators: usize, test: impl FnOnce(TestContext) -> T) -> T { + let validators = validators(total_validators); + let addresses = validators_addresses(total_validators); + sp_io::TestExternalities::new( + GenesisConfig { + initial_header: genesis.clone(), + initial_difficulty: 0.into(), + initial_validators: addresses.clone(), + } + .build_storage::() + .unwrap(), + ) + .execute_with(|| { + test(TestContext { + genesis, + total_validators, + validators, + addresses, + }) + }) +} + +/// 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(), diff --git a/bridges/modules/ethereum/src/test_utils.rs b/bridges/modules/ethereum/src/test_utils.rs new file mode 100644 index 000000000000..2a424542d76c --- /dev/null +++ b/bridges/modules/ethereum/src/test_utils.rs @@ -0,0 +1,233 @@ +// Copyright 2019-2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common 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. + +// Parity Bridges Common 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 Parity Bridges Common. If not, see . + +//! Utilities for testing and benchmarking the Ethereum Bridge Pallet. +//! +//! Although the name implies that it is used by tests, it shouldn't be be used _directly_ by tests. +//! Instead these utilities should be used by the Mock runtime, which in turn is used by tests. +//! +//! On the other hand, they may be used directly by the bechmarking module. + +use crate::verification::calculate_score; + +use primitives::{ + rlp_encode, + signatures::{secret_to_address, sign, SignHeader}, + Address, Bloom, Header, SealedEmptyStep, H256, U256, +}; +use secp256k1::SecretKey; +use sp_std::prelude::*; + +/// Gas limit valid in test environment. +pub const GAS_LIMIT: u64 = 0x2000; + +/// Test header builder. +pub struct HeaderBuilder { + header: Header, + parent_header: Header, +} + +impl HeaderBuilder { + /// Creates default genesis header. + pub fn genesis() -> Self { + let current_step = 0u64; + Self { + header: Header { + gas_limit: GAS_LIMIT.into(), + seal: vec![primitives::rlp_encode(¤t_step), vec![]], + ..Default::default() + }, + parent_header: Default::default(), + } + } + + /// Creates default header on top of parent with given hash. + #[cfg(test)] + pub fn with_parent_hash(parent_hash: H256) -> Self { + use crate::mock::TestRuntime; + use crate::Headers; + use frame_support::StorageMap; + + let parent_header = Headers::::get(&parent_hash).unwrap().header; + Self::with_parent(&parent_header) + } + + /// Creates default header on top of parent with given number. First parent is selected. + #[cfg(test)] + pub fn with_parent_number(parent_number: u64) -> Self { + use crate::HeadersByNumber; + use frame_support::StorageMap; + + let parent_hash = HeadersByNumber::get(parent_number).unwrap()[0].clone(); + Self::with_parent_hash(parent_hash) + } + + /// Creates default header on top of non-existent parent. + #[cfg(test)] + pub fn with_number(number: u64) -> Self { + Self::with_parent(&Header { + number: number - 1, + seal: vec![primitives::rlp_encode(&(number - 1)), vec![]], + ..Default::default() + }) + } + + /// Creates default header on top of given parent. + pub fn with_parent(parent_header: &Header) -> Self { + let parent_step = parent_header.step().unwrap(); + let current_step = parent_step + 1; + Self { + header: Header { + parent_hash: parent_header.compute_hash(), + number: parent_header.number + 1, + gas_limit: GAS_LIMIT.into(), + seal: vec![primitives::rlp_encode(¤t_step), vec![]], + difficulty: calculate_score(parent_step, current_step, 0), + ..Default::default() + }, + parent_header: parent_header.clone(), + } + } + + /// Update step of this header. + pub fn step(mut self, step: u64) -> Self { + let parent_step = self.parent_header.step(); + self.header.seal[0] = rlp_encode(&step); + self.header.difficulty = parent_step + .map(|parent_step| calculate_score(parent_step, step, 0)) + .unwrap_or_default(); + self + } + + /// Adds empty steps to this header. + pub fn empty_steps(mut self, empty_steps: &[(&SecretKey, u64)]) -> Self { + let sealed_empty_steps = empty_steps + .into_iter() + .map(|(author, step)| { + let mut empty_step = SealedEmptyStep { + step: *step, + signature: Default::default(), + }; + let message = empty_step.message(&self.header.parent_hash); + let signature: [u8; 65] = sign(author, message).into(); + empty_step.signature = signature.into(); + empty_step + }) + .collect::>(); + + // by default in test configuration headers are generated without empty steps seal + if self.header.seal.len() < 3 { + self.header.seal.push(Vec::new()); + } + + self.header.seal[2] = SealedEmptyStep::rlp_of(&sealed_empty_steps); + self + } + + /// Update difficulty field of this header. + pub fn difficulty(mut self, difficulty: U256) -> Self { + self.header.difficulty = difficulty; + self + } + + /// Update extra data field of this header. + pub fn extra_data(mut self, extra_data: Vec) -> Self { + self.header.extra_data = extra_data; + self + } + + /// Update gas limit field of this header. + pub fn gas_limit(mut self, gas_limit: U256) -> Self { + self.header.gas_limit = gas_limit; + self + } + + /// Update gas used field of this header. + pub fn gas_used(mut self, gas_used: U256) -> Self { + self.header.gas_used = gas_used; + self + } + + /// Update log bloom field of this header. + pub fn log_bloom(mut self, log_bloom: Bloom) -> Self { + self.header.log_bloom = log_bloom; + self + } + + /// Update receipts root field of this header. + pub fn receipts_root(mut self, receipts_root: H256) -> Self { + self.header.receipts_root = receipts_root; + self + } + + /// Update timestamp field of this header. + pub fn timestamp(mut self, timestamp: u64) -> Self { + self.header.timestamp = timestamp; + self + } + + /// Signs header by given author. + pub fn sign_by(self, author: &SecretKey) -> Header { + self.header.sign_by(author) + } + + /// Signs header by given authors set. + pub fn sign_by_set(self, authors: &[SecretKey]) -> Header { + self.header.sign_by_set(authors) + } +} + +/// Helper function for getting a genesis header which has been signed by an authority. +pub fn build_genesis_header(author: &SecretKey) -> Header { + let genesis = HeaderBuilder::genesis(); + genesis.header.sign_by(&author) +} + +/// Helper function for building a custom child header which has been signed by an authority. +pub fn build_custom_header(author: &SecretKey, previous: &Header, customize_header: F) -> Header +where + F: FnOnce(Header) -> Header, +{ + let new_header = HeaderBuilder::with_parent(&previous); + let custom_header = customize_header(new_header.header); + custom_header.sign_by(author) +} + +pub mod validator_utils { + use super::*; + + /// Return key pair of given test validator. + pub fn validator(index: usize) -> SecretKey { + let mut raw_secret = [0u8; 32]; + raw_secret[..8].copy_from_slice(&(index + 1).to_le_bytes()); + SecretKey::parse(&raw_secret).unwrap() + } + + /// Return key pairs of all test validators. + pub fn validators(count: usize) -> Vec { + (0..count).map(validator).collect() + } + + /// Return address of test validator. + pub fn validator_address(index: usize) -> Address { + secret_to_address(&validator(index)) + } + + /// Return addresses of all test validators. + pub fn validators_addresses(count: usize) -> Vec
{ + (0..count).map(validator_address).collect() + } +} diff --git a/bridges/modules/ethereum/src/validators.rs b/bridges/modules/ethereum/src/validators.rs index 70aa246c90e5..e6a454cc6377 100644 --- a/bridges/modules/ethereum/src/validators.rs +++ b/bridges/modules/ethereum/src/validators.rs @@ -273,19 +273,16 @@ impl ValidatorsSource { } } -/// Get validator that should author the block at given step. -pub fn step_validator(header_validators: &[Address], header_step: u64) -> Address { - header_validators[(header_step % header_validators.len() as u64) as usize] -} - #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::mock::{custom_test_ext, genesis, validators_addresses, TestRuntime}; + use crate::mock::{run_test, validators_addresses, TestRuntime}; use crate::{BridgeStorage, Headers, ScheduledChange, ScheduledChanges, StoredHeader}; use frame_support::StorageMap; use primitives::{TransactionOutcome, H256}; + const TOTAL_VALIDATORS: usize = 3; + pub(crate) fn validators_change_recept(parent_hash: H256) -> Receipt { Receipt { gas_used: 0.into(), @@ -425,7 +422,7 @@ pub(crate) mod tests { } fn try_finalize_with_scheduled_change(scheduled_at: Option) -> Option { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test(TOTAL_VALIDATORS, |_| { let config = ValidatorsConfiguration::Single(ValidatorsSource::Contract(Default::default(), Vec::new())); let validators = Validators::new(&config); let storage = BridgeStorage::::new(); diff --git a/bridges/modules/ethereum/src/verification.rs b/bridges/modules/ethereum/src/verification.rs index dee912e26a15..031b85ab4aba 100644 --- a/bridges/modules/ethereum/src/verification.rs +++ b/bridges/modules/ethereum/src/verification.rs @@ -15,10 +15,12 @@ // along with Parity Bridges Common. If not, see . use crate::error::Error; -use crate::validators::{step_validator, Validators, ValidatorsConfiguration}; +use crate::validators::{Validators, ValidatorsConfiguration}; use crate::{AuraConfiguration, ImportContext, PoolConfiguration, ScheduledChange, Storage}; use codec::Encode; -use primitives::{public_to_address, Address, Header, HeaderId, Receipt, SealedEmptyStep, H256, H520, U128, U256}; +use primitives::{ + public_to_address, step_validator, Address, Header, HeaderId, Receipt, SealedEmptyStep, H256, H520, U128, U256, +}; use sp_io::crypto::secp256k1_ecdsa_recover; use sp_std::{vec, vec::Vec}; @@ -157,9 +159,16 @@ pub fn verify_aura_header( contextless_checks(config, header)?; // the rest of checks requires access to the parent header - let context = storage - .import_context(submitter, &header.parent_hash) - .ok_or(Error::MissingParentBlock)?; + let context = storage.import_context(submitter, &header.parent_hash).ok_or_else(|| { + frame_support::debug::warn!( + target: "runtime", + "Missing parent PoA block: ({:?}, {})", + header.number.checked_sub(1), + header.parent_hash, + ); + + Error::MissingParentBlock + })?; let header_step = contextual_checks(config, &context, None, header)?; validator_checks(config, &context.validators_set().validators, header, header_step)?; @@ -263,7 +272,7 @@ fn validator_checks( header: &Header, header_step: u64, ) -> Result<(), Error> { - let expected_validator = step_validator(validators, header_step); + let expected_validator = *step_validator(validators, header_step); if header.author != expected_validator { return Err(Error::NotValidator); } @@ -282,7 +291,7 @@ fn validator_checks( /// Returns expected number of seal fields in the header. fn expected_header_seal_fields(config: &AuraConfiguration, header: &Header) -> usize { - if header.number >= config.empty_steps_transition { + if header.number != u64::max_value() && header.number >= config.empty_steps_transition { 3 } else { 2 @@ -291,13 +300,13 @@ fn expected_header_seal_fields(config: &AuraConfiguration, header: &Header) -> u /// Verify single sealed empty step. fn verify_empty_step(parent_hash: &H256, step: &SealedEmptyStep, validators: &[Address]) -> bool { - let expected_validator = step_validator(validators, step.step); + let expected_validator = *step_validator(validators, step.step); let message = step.message(parent_hash); verify_signature(&expected_validator, &step.signature, &message) } /// Chain scoring: total weight is sqrt(U256::max_value())*height - step -fn calculate_score(parent_step: u64, current_step: u64, current_empty_steps: usize) -> U256 { +pub(crate) fn calculate_score(parent_step: u64, current_step: u64, current_empty_steps: usize) -> U256 { U256::from(U128::max_value()) + U256::from(parent_step) - U256::from(current_step) + U256::from(current_empty_steps) } @@ -344,8 +353,8 @@ fn find_next_validators_signal(storage: &S, context: &ImportContext< mod tests { use super::*; use crate::mock::{ - block_i, custom_block_i, custom_test_ext, genesis, insert_header, signed_header, test_aura_config, validator, - validators_addresses, AccountId, TestRuntime, + insert_header, run_test_with_genesis, test_aura_config, validator, validator_address, validators_addresses, + AccountId, HeaderBuilder, TestRuntime, GAS_LIMIT, }; use crate::validators::{tests::validators_change_recept, ValidatorsSource}; use crate::{ @@ -353,25 +362,18 @@ mod tests { ScheduledChanges, ValidatorsSet, ValidatorsSets, }; use frame_support::{StorageMap, StorageValue}; - use parity_crypto::publickey::{sign, KeyPair}; use primitives::{rlp_encode, TransactionOutcome, H520}; + use secp256k1::SecretKey; - fn sealed_empty_step(validators: &[KeyPair], parent_hash: &H256, step: u64) -> SealedEmptyStep { - let mut empty_step = SealedEmptyStep { - step, - signature: Default::default(), - }; - let message = empty_step.message(parent_hash); - let validator_index = (step % validators.len() as u64) as usize; - let signature: [u8; 65] = sign(validators[validator_index].secret(), &message.as_fixed_bytes().into()) - .unwrap() - .into(); - empty_step.signature = signature.into(); - empty_step + const GENESIS_STEP: u64 = 42; + const TOTAL_VALIDATORS: usize = 3; + + fn genesis() -> Header { + HeaderBuilder::genesis().step(GENESIS_STEP).sign_by(&validator(0)) } fn verify_with_config(config: &AuraConfiguration, header: &Header) -> Result, Error> { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test_with_genesis(genesis(), TOTAL_VALIDATORS, |_| { let storage = BridgeStorage::::new(); verify_aura_header(&storage, &config, None, header) }) @@ -382,17 +384,17 @@ mod tests { } fn default_accept_into_pool( - mut make_header: impl FnMut(&[KeyPair]) -> (Header, Option>), + mut make_header: impl FnMut(&[SecretKey]) -> (Header, Option>), ) -> Result<(Vec>, Vec>), Error> { - custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { + run_test_with_genesis(genesis(), TOTAL_VALIDATORS, |_| { let validators = vec![validator(0), validator(1), validator(2)]; let mut storage = BridgeStorage::::new(); - let block1 = block_i(1, &validators); + let block1 = HeaderBuilder::with_parent_number(0).sign_by_set(&validators); insert_header(&mut storage, block1); - let block2 = block_i(2, &validators); + let block2 = HeaderBuilder::with_parent_number(1).sign_by_set(&validators); let block2_id = block2.compute_id(); insert_header(&mut storage, block2); - let block3 = block_i(3, &validators); + let block3 = HeaderBuilder::with_parent_number(2).sign_by_set(&validators); insert_header(&mut storage, block3); FinalizedBlock::put(block2_id); @@ -468,32 +470,26 @@ mod tests { #[test] fn verifies_header_number() { // when number is u64::max_value() - let mut header = Header { - seal: vec![vec![].into(), vec![].into(), vec![].into()], - number: u64::max_value(), - ..Default::default() - }; + let header = HeaderBuilder::with_number(u64::max_value()).sign_by(&validator(0)); assert_eq!(default_verify(&header), Err(Error::RidiculousNumber)); // when header is < u64::max_value() - header.seal = vec![vec![].into(), vec![].into()]; - header.number -= 1; + let header = HeaderBuilder::with_number(u64::max_value() - 1).sign_by(&validator(0)); assert_ne!(default_verify(&header), Err(Error::RidiculousNumber)); } #[test] fn verifies_gas_used() { // when gas used is larger than gas limit - let mut header = Header { - seal: vec![vec![].into(), vec![].into()], - gas_used: 1.into(), - gas_limit: 0.into(), - ..Default::default() - }; + let header = HeaderBuilder::with_number(1) + .gas_used((GAS_LIMIT + 1).into()) + .sign_by(&validator(0)); assert_eq!(default_verify(&header), Err(Error::TooMuchGasUsed)); // when gas used is less than gas limit - header.gas_limit = 1.into(); + let header = HeaderBuilder::with_number(1) + .gas_used((GAS_LIMIT - 1).into()) + .sign_by(&validator(0)); assert_ne!(default_verify(&header), Err(Error::TooMuchGasUsed)); } @@ -504,67 +500,62 @@ mod tests { config.max_gas_limit = 200.into(); // when limit is lower than expected - let mut header = Header { - seal: vec![vec![].into(), vec![].into()], - gas_limit: 50.into(), - ..Default::default() - }; + let header = HeaderBuilder::with_number(1) + .gas_limit(50.into()) + .sign_by(&validator(0)); assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit)); // when limit is larger than expected - header.gas_limit = 250.into(); + let header = HeaderBuilder::with_number(1) + .gas_limit(250.into()) + .sign_by(&validator(0)); assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit)); // when limit is within expected range - header.gas_limit = 150.into(); + let header = HeaderBuilder::with_number(1) + .gas_limit(150.into()) + .sign_by(&validator(0)); assert_ne!(verify_with_config(&config, &header), Err(Error::InvalidGasLimit)); } #[test] fn verifies_extra_data_len() { // when extra data is too large - let mut header = Header { - seal: vec![vec![].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - extra_data: std::iter::repeat(42).take(1000).collect::>().into(), - number: 1, - ..Default::default() - }; + let header = HeaderBuilder::with_number(1) + .extra_data(std::iter::repeat(42).take(1000).collect::>()) + .sign_by(&validator(0)); assert_eq!(default_verify(&header), Err(Error::ExtraDataOutOfBounds)); // when extra data size is OK - header.extra_data = std::iter::repeat(42).take(10).collect::>().into(); + let header = HeaderBuilder::with_number(1) + .extra_data(std::iter::repeat(42).take(10).collect::>()) + .sign_by(&validator(0)); assert_ne!(default_verify(&header), Err(Error::ExtraDataOutOfBounds)); } #[test] fn verifies_timestamp() { // when timestamp overflows i32 - let mut header = Header { - seal: vec![vec![].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - timestamp: i32::max_value() as u64 + 1, - ..Default::default() - }; + let header = HeaderBuilder::with_number(1) + .timestamp(i32::max_value() as u64 + 1) + .sign_by(&validator(0)); assert_eq!(default_verify(&header), Err(Error::TimestampOverflow)); // when timestamp doesn't overflow i32 - header.timestamp -= 1; + let header = HeaderBuilder::with_number(1) + .timestamp(i32::max_value() as u64) + .sign_by(&validator(0)); assert_ne!(default_verify(&header), Err(Error::TimestampOverflow)); } #[test] fn verifies_parent_existence() { // when there's no parent in the storage - let mut header = Header { - seal: vec![vec![].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - ..Default::default() - }; + let header = HeaderBuilder::with_number(1).sign_by(&validator(0)); assert_eq!(default_verify(&header), Err(Error::MissingParentBlock)); // when parent is in the storage - header.parent_hash = genesis().compute_hash(); + let header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(0)); assert_ne!(default_verify(&header), Err(Error::MissingParentBlock)); } @@ -580,11 +571,11 @@ mod tests { assert_eq!(default_verify(&header), Err(Error::MissingStep)); // when step is the same as for the parent block - header.seal = vec![vec![42].into(), vec![].into()]; + header.seal[0] = rlp_encode(&42u64); assert_eq!(default_verify(&header), Err(Error::DoubleVote)); // when step is OK - header.seal = vec![vec![43].into(), vec![].into()]; + header.seal[0] = rlp_encode(&43u64); assert_ne!(default_verify(&header), Err(Error::DoubleVote)); // now check with validate_step check enabled @@ -592,52 +583,47 @@ mod tests { config.validate_step_transition = 0; // when step is lesser that for the parent block + header.seal[0] = rlp_encode(&40u64); header.seal = vec![vec![40].into(), vec![].into()]; assert_eq!(verify_with_config(&config, &header), Err(Error::DoubleVote)); // when step is OK - header.seal = vec![vec![44].into(), vec![].into()]; + header.seal[0] = rlp_encode(&44u64); assert_ne!(verify_with_config(&config, &header), Err(Error::DoubleVote)); } #[test] fn verifies_empty_step() { - let validators = vec![validator(0), validator(1), validator(2)]; let mut config = test_aura_config(); config.empty_steps_transition = 0; // when empty step duplicates parent step - let mut header = Header { - seal: vec![ - vec![45].into(), - vec![142].into(), - SealedEmptyStep::rlp_of(&[sealed_empty_step(&validators, &genesis().compute_hash(), 42)]), - ], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: genesis().compute_hash(), - ..Default::default() - }; + let header = HeaderBuilder::with_parent(&genesis()) + .empty_steps(&[(&validator(0), GENESIS_STEP)]) + .step(GENESIS_STEP + 3) + .sign_by(&validator(3)); assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof)); // when empty step signature check fails - let mut wrong_sealed_empty_step = sealed_empty_step(&validators, &genesis().compute_hash(), 43); - wrong_sealed_empty_step.signature = Default::default(); - header.seal[2] = SealedEmptyStep::rlp_of(&[wrong_sealed_empty_step]); + let header = HeaderBuilder::with_parent(&genesis()) + .empty_steps(&[(&validator(100), GENESIS_STEP + 1)]) + .step(GENESIS_STEP + 3) + .sign_by(&validator(3)); assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof)); // when we are accepting strict empty steps and they come not in order config.strict_empty_steps_transition = 0; - header.seal[2] = SealedEmptyStep::rlp_of(&[ - sealed_empty_step(&validators, &genesis().compute_hash(), 44), - sealed_empty_step(&validators, &genesis().compute_hash(), 43), - ]); + let header = HeaderBuilder::with_parent(&genesis()) + .empty_steps(&[(&validator(2), GENESIS_STEP + 2), (&validator(1), GENESIS_STEP + 1)]) + .step(GENESIS_STEP + 3) + .sign_by(&validator(3)); assert_eq!(verify_with_config(&config, &header), Err(Error::InsufficientProof)); // when empty steps are OK - header.seal[2] = SealedEmptyStep::rlp_of(&[ - sealed_empty_step(&validators, &genesis().compute_hash(), 43), - sealed_empty_step(&validators, &genesis().compute_hash(), 44), - ]); + let header = HeaderBuilder::with_parent(&genesis()) + .empty_steps(&[(&validator(1), GENESIS_STEP + 1), (&validator(2), GENESIS_STEP + 2)]) + .step(GENESIS_STEP + 3) + .sign_by(&validator(3)); assert_ne!(verify_with_config(&config, &header), Err(Error::InsufficientProof)); } @@ -647,33 +633,19 @@ mod tests { config.validate_score_transition = 0; // when chain score is invalid - let mut header = Header { - seal: vec![vec![43].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: genesis().compute_hash(), - ..Default::default() - }; + let header = HeaderBuilder::with_parent(&genesis()) + .difficulty(100.into()) + .sign_by(&validator(0)); assert_eq!(verify_with_config(&config, &header), Err(Error::InvalidDifficulty)); // when chain score is accepted - header.difficulty = calculate_score(42, 43, 0); + let header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(0)); assert_ne!(verify_with_config(&config, &header), Err(Error::InvalidDifficulty)); } #[test] fn verifies_validator() { - let validators = vec![validator(0), validator(1), validator(2)]; - let good_header = signed_header( - &validators, - Header { - author: validators[1].address().as_fixed_bytes().into(), - seal: vec![vec![43].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: genesis().compute_hash(), - ..Default::default() - }, - 43, - ); + let good_header = HeaderBuilder::with_parent(&genesis()).sign_by(&validator(1)); // when header author is invalid let mut header = good_header.clone(); @@ -693,7 +665,7 @@ mod tests { fn pool_verifies_known_blocks() { // when header is known assert_eq!( - default_accept_into_pool(|validators| (block_i(3, validators), None)), + default_accept_into_pool(|validators| (HeaderBuilder::with_parent_number(2).sign_by_set(validators), None)), Err(Error::KnownHeader), ); } @@ -703,7 +675,9 @@ mod tests { // when header number is less than finalized assert_eq!( default_accept_into_pool(|validators| ( - custom_block_i(2, validators, |header| header.gas_limit += 1.into()), + HeaderBuilder::with_parent_number(1) + .gas_limit((GAS_LIMIT + 1).into()) + .sign_by_set(validators), None, ),), Err(Error::AncientHeader), @@ -731,7 +705,7 @@ mod tests { fn pool_rejects_headers_with_redundant_receipts() { assert_eq!( default_accept_into_pool(|validators| ( - block_i(4, validators), + HeaderBuilder::with_parent_number(3).sign_by_set(validators), Some(vec![Receipt { gas_used: 1.into(), log_bloom: (&[0xff; 256]).into(), @@ -747,7 +721,7 @@ mod tests { fn pool_verifies_future_block_number() { // when header is too far from the future assert_eq!( - default_accept_into_pool(|validators| (custom_block_i(4, validators, |header| header.number = 100), None,),), + default_accept_into_pool(|validators| (HeaderBuilder::with_number(100).sign_by_set(&validators), None),), Err(Error::UnsignedTooFarInTheFuture), ); } @@ -758,8 +732,9 @@ mod tests { // checks for DoubleVote assert_eq!( default_accept_into_pool(|validators| ( - custom_block_i(4, validators, |header| header.seal[0] = - block_i(3, validators).seal[0].clone()), + HeaderBuilder::with_parent_number(3) + .step(GENESIS_STEP + 3) + .sign_by_set(&validators), None, ),), Err(Error::DoubleVote), @@ -772,21 +747,7 @@ mod tests { // (even if header will be considered invalid/duplicate later, we can use this signature // as a proof of malicious action by this validator) assert_eq!( - default_accept_into_pool(|validators| ( - signed_header( - validators, - Header { - author: validators[1].address().as_fixed_bytes().into(), - seal: vec![vec![8].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: [42; 32].into(), - number: 8, - ..Default::default() - }, - 43 - ), - None, - )), + default_accept_into_pool(|_| (HeaderBuilder::with_number(8).step(8).sign_by(&validator(1)), None,)), Err(Error::NotValidator), ); } @@ -796,7 +757,7 @@ mod tests { let mut hash = None; assert_eq!( default_accept_into_pool(|validators| { - let header = block_i(4, &validators); + let header = HeaderBuilder::with_parent_number(3).sign_by_set(validators); hash = Some(header.compute_hash()); (header, None) }), @@ -814,32 +775,22 @@ mod tests { #[test] fn pool_verifies_header_with_unknown_parent() { - let mut hash = None; + let mut id = None; + let mut parent_id = None; assert_eq!( default_accept_into_pool(|validators| { - let header = signed_header( - validators, - Header { - author: validators[2].address().as_fixed_bytes().into(), - seal: vec![vec![47].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: [42; 32].into(), - number: 5, - ..Default::default() - }, - 47, - ); - hash = Some(header.compute_hash()); + let header = HeaderBuilder::with_number(5) + .step(GENESIS_STEP + 5) + .sign_by_set(validators); + id = Some(header.compute_id()); + parent_id = header.parent_id(); (header, None) }), Ok(( // parent tag required - vec![(4u64, [42u8; 32]).encode(),], + vec![parent_id.unwrap().encode()], // header provides two tags - vec![ - (5u64, validators_addresses(3)[2]).encode(), - (5u64, hash.unwrap()).encode(), - ], + vec![(5u64, validator_address(2)).encode(), id.unwrap().encode(),], )), ); } @@ -852,55 +803,36 @@ mod tests { change_validators_set_at(3, validators_addresses(1), None); // header is signed using wrong set - let header = signed_header( - actual_validators, - Header { - author: actual_validators[2].address().as_fixed_bytes().into(), - seal: vec![vec![47].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: [42; 32].into(), - number: 5, - ..Default::default() - }, - 47, - ); + let header = HeaderBuilder::with_number(5) + .step(GENESIS_STEP + 2) + .sign_by_set(actual_validators); (header, None) }), Err(Error::NotValidator), ); - let mut hash = None; + let mut id = None; + let mut parent_id = None; assert_eq!( default_accept_into_pool(|actual_validators| { // change finalized set at parent header + signal valid set at parent block change_validators_set_at(3, validators_addresses(10), Some(validators_addresses(3))); // header is signed using wrong set - let header = signed_header( - actual_validators, - Header { - author: actual_validators[2].address().as_fixed_bytes().into(), - seal: vec![vec![47].into(), vec![].into()], - gas_limit: test_aura_config().min_gas_limit, - parent_hash: [42; 32].into(), - number: 5, - ..Default::default() - }, - 47, - ); - hash = Some(header.compute_hash()); + let header = HeaderBuilder::with_number(5) + .step(GENESIS_STEP + 2) + .sign_by_set(actual_validators); + id = Some(header.compute_id()); + parent_id = header.parent_id(); (header, None) }), Ok(( // parent tag required - vec![(4u64, [42u8; 32]).encode(),], + vec![parent_id.unwrap().encode(),], // header provides two tags - vec![ - (5u64, validators_addresses(3)[2]).encode(), - (5u64, hash.unwrap()).encode(), - ], + vec![(5u64, validator_address(2)).encode(), id.unwrap().encode(),], )), ); } @@ -909,9 +841,9 @@ mod tests { fn pool_rejects_headers_with_invalid_receipts() { assert_eq!( default_accept_into_pool(|validators| { - let header = custom_block_i(4, &validators, |header| { - header.log_bloom = (&[0xff; 256]).into(); - }); + let header = HeaderBuilder::with_parent_number(3) + .log_bloom((&[0xff; 256]).into()) + .sign_by_set(validators); (header, Some(vec![validators_change_recept(Default::default())])) }), Err(Error::TransactionsReceiptsMismatch), @@ -923,12 +855,14 @@ mod tests { let mut hash = None; assert_eq!( default_accept_into_pool(|validators| { - let header = custom_block_i(4, &validators, |header| { - header.log_bloom = (&[0xff; 256]).into(); - header.receipts_root = "81ce88dc524403b796222046bf3daf543978329b87ffd50228f1d3987031dc45" - .parse() - .unwrap(); - }); + let header = HeaderBuilder::with_parent_number(3) + .log_bloom((&[0xff; 256]).into()) + .receipts_root( + "81ce88dc524403b796222046bf3daf543978329b87ffd50228f1d3987031dc45" + .parse() + .unwrap(), + ) + .sign_by_set(validators); hash = Some(header.compute_hash()); (header, Some(vec![validators_change_recept(Default::default())])) }), diff --git a/bridges/primitives/ethereum-poa/Cargo.toml b/bridges/primitives/ethereum-poa/Cargo.toml index 96c60c3efc6f..a444d659bcee 100644 --- a/bridges/primitives/ethereum-poa/Cargo.toml +++ b/bridges/primitives/ethereum-poa/Cargo.toml @@ -20,9 +20,6 @@ hash-db = { version = "0.15.2", default-features = false } triehash = { version = "0.8.2", default-features = false } plain_hasher = { version = "0.2.2", default-features = false } -[dev-dependencies] -hex-literal = "0.2" - # Substrate Based Dependencies [dependencies.sp-api] version = "2.0.0-rc3" @@ -48,9 +45,18 @@ default-features = false rev = "606c56d2e2f69f68f3947551224be6a3515dff60" git = "https://github.com/paritytech/substrate.git" +[dependencies.libsecp256k1] +optional = true +version = "0.3.4" +default-features = false +features = ["hmac"] + +[dev-dependencies] +hex-literal = "0.2" + [features] default = ["std"] -test-helpers = [] +test-helpers = ["libsecp256k1"] std = [ "serde/std", "serde-big-array", diff --git a/bridges/primitives/ethereum-poa/src/lib.rs b/bridges/primitives/ethereum-poa/src/lib.rs index 12ee57131535..ca804b32bda9 100644 --- a/bridges/primitives/ethereum-poa/src/lib.rs +++ b/bridges/primitives/ethereum-poa/src/lib.rs @@ -18,8 +18,6 @@ pub use parity_bytes::Bytes; pub use primitive_types::{H160, H256, H512, U128, U256}; - -#[cfg(feature = "test-helpers")] pub use rlp::encode as rlp_encode; use codec::{Decode, Encode}; @@ -49,6 +47,9 @@ pub type RawTransaction = Vec; /// An ethereum address. pub type Address = H160; +#[cfg(any(feature = "test-helpers", test))] +pub mod signatures; + /// Complete header id. #[derive(Encode, Decode, Default, RuntimeDebug, PartialEq, Clone, Copy)] pub struct HeaderId { @@ -59,8 +60,8 @@ pub struct HeaderId { } /// An Aura header. -#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug)] -#[cfg_attr(feature = "std", derive(Default, Serialize, Deserialize))] +#[derive(Clone, Default, Encode, Decode, PartialEq, RuntimeDebug)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] pub struct Header { /// Parent block hash. pub parent_hash: H256, @@ -457,6 +458,11 @@ pub fn compute_merkle_root>(items: impl Iterator) -> H2 triehash::ordered_trie_root::(items) } +/// Get validator that should author the block at given step. +pub fn step_validator(header_validators: &[T], header_step: u64) -> &T { + &header_validators[(header_step % header_validators.len() as u64) as usize] +} + sp_api::decl_runtime_apis! { /// API for headers submitters. pub trait EthereumHeadersApi { diff --git a/bridges/primitives/ethereum-poa/src/signatures.rs b/bridges/primitives/ethereum-poa/src/signatures.rs new file mode 100644 index 000000000000..4eae9d16b133 --- /dev/null +++ b/bridges/primitives/ethereum-poa/src/signatures.rs @@ -0,0 +1,66 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Parity Bridges Common. + +// Parity Bridges Common 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. + +// Parity Bridges Common 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 Parity Bridges Common. If not, see . +// + +//! Helpers related to signatures. +//! +//! Used for testing and benchmarking. + +use crate::{public_to_address, rlp_encode, step_validator, Address, Header, H256, H520}; + +use secp256k1::{Message, PublicKey, SecretKey}; + +/// Utilities for signing headers. +pub trait SignHeader { + /// Signs header by given author. + fn sign_by(self, author: &SecretKey) -> Header; + /// Signs header by given authors set. + fn sign_by_set(self, authors: &[SecretKey]) -> Header; +} + +impl SignHeader for Header { + fn sign_by(mut self, author: &SecretKey) -> Self { + self.author = secret_to_address(author); + + let message = self.seal_hash(false).unwrap(); + let signature = sign(author, message); + self.seal[1] = rlp_encode(&signature); + self + } + + fn sign_by_set(self, authors: &[SecretKey]) -> Self { + let step = self.step().unwrap(); + let author = step_validator(authors, step); + self.sign_by(author) + } +} + +/// Return author's signature over given message. +pub fn sign(author: &SecretKey, message: H256) -> H520 { + let (signature, recovery_id) = secp256k1::sign(&Message::parse(message.as_fixed_bytes()), author); + let mut raw_signature = [0u8; 65]; + raw_signature[..64].copy_from_slice(&signature.serialize()); + raw_signature[64] = recovery_id.serialize(); + raw_signature.into() +} + +/// Returns address corresponding to given secret key. +pub fn secret_to_address(secret: &SecretKey) -> Address { + let public = PublicKey::from_secret_key(secret); + let mut raw_public = [0u8; 64]; + raw_public.copy_from_slice(&public.serialize()[1..]); + public_to_address(&raw_public) +}