From c6c46462ab6df693b17463aa552ee1b6a41dd4e6 Mon Sep 17 00:00:00 2001 From: Svyatoslav Nikolsky Date: Tue, 7 Apr 2020 15:53:59 +0300 Subject: [PATCH] Sync ethereum headers using unsigned (substrate) transactions (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * reward submitters on finalization * PoA -> Substrate: unsigned_import_header API * fix grumble * make submitter part of ImportContext * verify using next validators set + tests * fix nostd compilation * add sub-tx-mode argument * support sub-tx-mode * impl ValidateUnsigned for Runtime * do not submit too much transactions to the pool * cargo fmt * fix bad merge * revert license fix * Update modules/ethereum/src/lib.rs Co-Authored-By: Hernando Castano * Update modules/ethereum/src/verification.rs Co-Authored-By: Hernando Castano * updated comment * validate receipts before accepting unsigned tx to pool * cargo fmt * fix comment * fix grumbles * Update modules/ethereum/src/verification.rs Co-Authored-By: Hernando Castano * cargo fmt --all * struct ChangeToEnact * updated doc * fix doc * add docs to the code method * simplify fn ancestry * finish incomplete docs * Update modules/ethereum/src/lib.rs Co-Authored-By: Tomasz Drwięga * Update modules/ethereum/src/lib.rs Co-Authored-By: Tomasz Drwięga * return err from unsigned_import_header * get header once * Update relays/ethereum/src/ethereum_sync.rs Co-Authored-By: Tomasz Drwięga * fix * UnsignedTooFarInTheFuture -> Custom(err.code()) * updated ImportContext::last_signal_block * cargo fmt --all * rename runtime calls Co-authored-by: Hernando Castano Co-authored-by: Tomasz Drwięga --- bridges/bin/node/runtime/src/lib.rs | 2 +- bridges/modules/ethereum/src/error.rs | 51 +- bridges/modules/ethereum/src/finality.rs | 33 +- bridges/modules/ethereum/src/import.rs | 32 +- bridges/modules/ethereum/src/lib.rs | 370 +++++++++--- bridges/modules/ethereum/src/validators.rs | 9 +- bridges/modules/ethereum/src/verification.rs | 558 ++++++++++++++++-- bridges/relays/ethereum/src/cli.yml | 13 +- bridges/relays/ethereum/src/ethereum_sync.rs | 45 +- .../relays/ethereum/src/ethereum_sync_loop.rs | 35 +- bridges/relays/ethereum/src/main.rs | 13 + .../relays/ethereum/src/substrate_client.rs | 93 ++- 12 files changed, 1043 insertions(+), 211 deletions(-) diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs index 7912d7fc8f44..1ec47fde8016 100644 --- a/bridges/bin/node/runtime/src/lib.rs +++ b/bridges/bin/node/runtime/src/lib.rs @@ -247,7 +247,7 @@ construct_runtime!( Balances: pallet_balances::{Module, Call, Storage, Config, Event}, TransactionPayment: pallet_transaction_payment::{Module, Storage}, Sudo: pallet_sudo::{Module, Call, Config, Storage, Event}, - BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage}, + BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage, ValidateUnsigned}, } ); diff --git a/bridges/modules/ethereum/src/error.rs b/bridges/modules/ethereum/src/error.rs index 82a2c8273638..4c887a4a493e 100644 --- a/bridges/modules/ethereum/src/error.rs +++ b/bridges/modules/ethereum/src/error.rs @@ -17,45 +17,49 @@ use sp_runtime::RuntimeDebug; /// Header import error. -#[derive(RuntimeDebug)] +#[derive(Clone, Copy, RuntimeDebug)] #[cfg_attr(feature = "std", derive(PartialEq))] pub enum Error { - /// The header is beyound last finalized and can not be imported. - AncientHeader, + /// The header is beyond last finalized and can not be imported. + AncientHeader = 0, /// The header is already imported. - KnownHeader, + KnownHeader = 1, /// Seal has an incorrect format. - InvalidSealArity, + InvalidSealArity = 2, /// Block number isn't sensible. - RidiculousNumber, + RidiculousNumber = 3, /// Block has too much gas used. - TooMuchGasUsed, + TooMuchGasUsed = 4, /// Gas limit header field is invalid. - InvalidGasLimit, + InvalidGasLimit = 5, /// Extra data is of an invalid length. - ExtraDataOutOfBounds, + ExtraDataOutOfBounds = 6, /// Timestamp header overflowed. - TimestampOverflow, + TimestampOverflow = 7, /// The parent header is missing from the blockchain. - MissingParentBlock, + MissingParentBlock = 8, /// The header step is missing from the header. - MissingStep, + MissingStep = 9, /// The header signature is missing from the header. - MissingSignature, + MissingSignature = 10, /// Empty steps are missing from the header. - MissingEmptySteps, + MissingEmptySteps = 11, /// The same author issued different votes at the same step. - DoubleVote, + DoubleVote = 12, /// Validation proof insufficient. - InsufficientProof, + InsufficientProof = 13, /// Difficulty header field is invalid. - InvalidDifficulty, + InvalidDifficulty = 14, /// The received block is from an incorrect proposer. - NotValidator, + NotValidator = 15, /// Missing transaction receipts for the operation. - MissingTransactionsReceipts, + MissingTransactionsReceipts = 16, + /// Redundant transaction receipts are provided. + RedundantTransactionsReceipts = 17, /// Provided transactions receipts are not matching the header. - TransactionsReceiptsMismatch, + TransactionsReceiptsMismatch = 18, + /// Can't accept unsigned header from the far future. + UnsignedTooFarInTheFuture = 19, } impl Error { @@ -78,7 +82,14 @@ impl Error { Error::InvalidDifficulty => "Header has invalid difficulty", Error::NotValidator => "Header is sealed by unexpected validator", Error::MissingTransactionsReceipts => "The import operation requires transactions receipts", + Error::RedundantTransactionsReceipts => "Redundant transactions receipts are provided", Error::TransactionsReceiptsMismatch => "Invalid transactions receipts provided", + Error::UnsignedTooFarInTheFuture => "The unsigned header is too far in future", } } + + /// Return unique error code. + pub fn code(&self) -> u8 { + *self as u8 + } } diff --git a/bridges/modules/ethereum/src/finality.rs b/bridges/modules/ethereum/src/finality.rs index 62fc3be01629..1ee9f356028e 100644 --- a/bridges/modules/ethereum/src/finality.rs +++ b/bridges/modules/ethereum/src/finality.rs @@ -15,15 +15,18 @@ // along with Parity Bridges Common. If not, see . use crate::error::Error; -use crate::{ancestry, Storage}; +use crate::Storage; use primitives::{public_to_address, Address, Header, SealedEmptyStep, H256}; use sp_io::crypto::secp256k1_ecdsa_recover; -use sp_std::collections::{ - btree_map::{BTreeMap, Entry}, - btree_set::BTreeSet, - vec_deque::VecDeque, -}; use sp_std::prelude::*; +use sp_std::{ + collections::{ + btree_map::{BTreeMap, Entry}, + btree_set::BTreeSet, + vec_deque::VecDeque, + }, + iter::from_fn, +}; /// Tries to finalize blocks when given block is imported. /// @@ -190,6 +193,24 @@ fn empty_step_signer(empty_step: &SealedEmptyStep, parent_hash: &H256) -> Option .map(|public| public_to_address(&public)) } +/// Return iterator of given header ancestors. +pub(crate) fn ancestry<'a, S: Storage>( + storage: &'a S, + header: &Header, +) -> impl Iterator)> + 'a { + let mut parent_hash = header.parent_hash.clone(); + from_fn(move || { + let (header, submitter) = storage.header(&parent_hash)?; + if header.number == 0 { + return None; + } + + let hash = parent_hash.clone(); + parent_hash = header.parent_hash.clone(); + Some((hash, header, submitter)) + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/bridges/modules/ethereum/src/import.rs b/bridges/modules/ethereum/src/import.rs index 82e84f706b43..20a5d10e457e 100644 --- a/bridges/modules/ethereum/src/import.rs +++ b/bridges/modules/ethereum/src/import.rs @@ -17,8 +17,8 @@ use crate::error::Error; use crate::finality::finalize_blocks; use crate::validators::{Validators, ValidatorsConfiguration}; -use crate::verification::verify_aura_header; -use crate::{AuraConfiguration, Storage}; +use crate::verification::{is_importable_header, verify_aura_header}; +use crate::{AuraConfiguration, ChangeToEnact, Storage}; use primitives::{Header, Receipt, H256}; use sp_std::{collections::btree_map::BTreeMap, prelude::*}; @@ -105,16 +105,22 @@ pub fn import_header( let (scheduled_change, enacted_change) = validators.extract_validators_change(&header, receipts)?; // check if block finalizes some other blocks and corresponding scheduled validators + let validators_set = import_context.validators_set(); let finalized_blocks = finalize_blocks( storage, &prev_finalized_hash, - (import_context.validators_start(), import_context.validators()), + (&validators_set.enact_block, &validators_set.validators), &hash, import_context.submitter(), &header, aura_config.two_thirds_majority_transition, )?; - let enacted_change = enacted_change.or_else(|| validators.finalize_validators_change(storage, &finalized_blocks)); + let enacted_change = enacted_change + .map(|validators| ChangeToEnact { + signal_block: None, + validators, + }) + .or_else(|| validators.finalize_validators_change(storage, &finalized_blocks)); // NOTE: we can't return Err() from anywhere below this line // (because otherwise we'll have inconsistent storage if transaction will fail) @@ -157,24 +163,6 @@ pub fn header_import_requires_receipts( .unwrap_or(false) } -/// Checks that we are able to ***try to** import this header. -/// Returns error if we should not try to import this block. -/// Returns hash of the header and number of the last finalized block. -fn is_importable_header(storage: &S, header: &Header) -> Result<(H256, H256), Error> { - // we never import any header that competes with finalized header - let (finalized_block_number, finalized_block_hash) = storage.finalized_block(); - if header.number <= finalized_block_number { - return Err(Error::AncientHeader); - } - // we never import any header with known hash - let hash = header.hash(); - if storage.header(&hash).is_some() { - return Err(Error::KnownHeader); - } - - Ok((hash, finalized_block_hash)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/bridges/modules/ethereum/src/lib.rs b/bridges/modules/ethereum/src/lib.rs index 398104fdbff1..b7bdd066b256 100644 --- a/bridges/modules/ethereum/src/lib.rs +++ b/bridges/modules/ethereum/src/lib.rs @@ -19,8 +19,14 @@ use codec::{Decode, Encode}; use frame_support::{decl_module, decl_storage}; use primitives::{Address, Header, Receipt, H256, U256}; -use sp_runtime::RuntimeDebug; -use sp_std::{cmp::Ord, collections::btree_map::BTreeMap, iter::from_fn, prelude::*}; +use sp_runtime::{ + transaction_validity::{ + InvalidTransaction, TransactionLongevity, TransactionPriority, TransactionValidity, UnknownTransaction, + ValidTransaction, + }, + RuntimeDebug, +}; +use sp_std::{cmp::Ord, collections::btree_map::BTreeMap, prelude::*}; use validators::{ValidatorsConfiguration, ValidatorsSource}; pub use import::{header_import_requires_receipts, import_header}; @@ -52,6 +58,18 @@ pub struct AuraConfiguration { pub maximum_extra_data_size: u64, } +/// Transaction pool configuration. +/// +/// This is used to limit number of unsigned headers transactions in +/// the pool. We never use it to verify signed transactions. +pub struct PoolConfiguration { + /// Maximal difference between number of header from unsigned transaction + /// and current best block. This must be selected with caution - the more + /// is the difference, the more (potentially invalid) transactions could be + /// accepted to the pool and mined later (filling blocks with spam). + pub max_future_number_difference: u64, +} + /// Block header as it is stored in the runtime storage. #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug)] pub struct StoredHeader { @@ -67,6 +85,32 @@ pub struct StoredHeader { /// this is the set that has produced the block itself. /// The hash is the hash of block where validators set has been enacted. pub next_validators_set_id: u64, + /// Hash of the last block which has **SCHEDULED** validators set change. + /// Note that signal doesn't mean that the set has been (or ever will be) enacted. + /// Note that the header may already be pruned. + pub last_signal_block: Option, +} + +/// Validators set as it is stored in the runtime storage. +#[derive(Encode, Decode, PartialEq, RuntimeDebug)] +#[cfg_attr(test, derive(Clone))] +pub struct ValidatorsSet { + /// Validators of this set. + pub validators: Vec
, + /// Hash of the block where this set has been signalled. None if this is the first set. + pub signal_block: Option, + /// Hash of the block where this set has been enacted. + pub enact_block: H256, +} + +/// Validators set change as it is stored in the runtime storage. +#[derive(Encode, Decode, PartialEq, RuntimeDebug)] +#[cfg_attr(test, derive(Clone))] +pub struct ScheduledChange { + /// Validators of this set. + pub validators: Vec
, + /// Hash of the block which has emitted previous validators change signal. + pub prev_signal_block: Option, } /// Header that we're importing. @@ -83,41 +127,44 @@ pub struct HeaderToImport { pub header: Header, /// Total chain difficulty at the header. pub total_difficulty: U256, - /// Validators set enacted change, if happened at the header. - pub enacted_change: Option>, + /// New validators set and the hash of block where it has been scheduled (if applicable). + /// Some if set is is enacted by this header. + pub enacted_change: Option, /// Validators set scheduled change, if happened at the header. pub scheduled_change: Option>, } +/// Header that we're importing. +#[derive(RuntimeDebug)] +#[cfg_attr(test, derive(Clone, PartialEq))] +pub struct ChangeToEnact { + /// The hash of the header where change has been scheduled. + /// None if it is a first set within current `ValidatorsSource`. + pub signal_block: Option, + /// Validators set that is enacted. + pub validators: Vec
, +} + /// Header import context. +/// +/// The import context contains information needed by the header verification +/// pipeline which is not directly part of the header being imported. This includes +/// information relating to its parent, and the current validator set (which +/// provide _context_ for the current header). #[derive(RuntimeDebug)] #[cfg_attr(test, derive(Clone, PartialEq))] pub struct ImportContext { submitter: Option, + parent_hash: H256, parent_header: Header, parent_total_difficulty: U256, - next_validators_set_id: u64, - next_validators_set: (H256, Vec
), + parent_scheduled_change: Option, + validators_set_id: u64, + validators_set: ValidatorsSet, + last_signal_block: Option, } impl ImportContext { - /// Create import context using passing parameters; - pub fn new( - submitter: Option, - parent_header: Header, - parent_total_difficulty: U256, - next_validators_set_id: u64, - next_validators_set: (H256, Vec
), - ) -> Self { - ImportContext { - submitter, - parent_header, - parent_total_difficulty, - next_validators_set_id, - next_validators_set, - } - } - /// Returns reference to header submitter (if known). pub fn submitter(&self) -> Option<&Submitter> { self.submitter.as_ref() @@ -133,19 +180,28 @@ impl ImportContext { &self.parent_total_difficulty } + /// Returns the validator set change if the parent header has signaled a change. + pub fn parent_scheduled_change(&self) -> Option<&ScheduledChange> { + self.parent_scheduled_change.as_ref() + } + /// Returns id of the set of validators. pub fn validators_set_id(&self) -> u64 { - self.next_validators_set_id + self.validators_set_id } - /// Returns block whenre validators set has been enacted. - pub fn validators_start(&self) -> &H256 { - &self.next_validators_set.0 + /// Returns reference to validators set for the block we're going to import. + pub fn validators_set(&self) -> &ValidatorsSet { + &self.validators_set } - /// Returns reference to the set of validators of the block we're going to import. - pub fn validators(&self) -> &[Address] { - &self.next_validators_set.1 + /// Returns reference to the latest block which has signalled change of validators set. + /// This may point to parent if parent has signalled change. + pub fn last_signal_block(&self) -> Option<&H256> { + match self.parent_scheduled_change { + Some(_) => Some(&self.parent_hash), + None => self.last_signal_block.as_ref(), + } } /// Converts import context into header we're going to import. @@ -155,7 +211,7 @@ impl ImportContext { hash: H256, header: Header, total_difficulty: U256, - enacted_change: Option>, + enacted_change: Option, scheduled_change: Option>, ) -> HeaderToImport { HeaderToImport { @@ -191,8 +247,9 @@ pub trait Storage { submitter: Option, parent_hash: &H256, ) -> Option>; - /// Get new validators that are scheduled by given header. - fn scheduled_change(&self, hash: &H256) -> Option>; + /// Get new validators that are scheduled by given header and hash of the previous + /// block that has scheduled change. + fn scheduled_change(&self, hash: &H256) -> Option; /// Insert imported header. fn insert_header(&mut self, header: HeaderToImport); /// Finalize given block and prune all headers with number < prune_end. @@ -235,13 +292,28 @@ pub trait Trait: frame_system::Trait { decl_module! { pub struct Module for enum Call where origin: T::Origin { - /// Import Aura chain headers. Ignores non-fatal errors (like when known - /// header is provided), rewards for successful headers import and penalizes - /// for fatal errors. + /// Import single Aura header. Requires transaction to be **UNSIGNED**. + pub fn import_unsigned_header(origin, header: Header, receipts: Option>) { + frame_system::ensure_none(origin)?; + + import_header( + &mut BridgeStorage::::new(), + &kovan_aura_config(), + &kovan_validators_config(), + crate::import::PRUNE_DEPTH, + None, + header, + receipts, + ).map_err(|e| e.msg())?; + } + + /// Import Aura chain headers in a single **SIGNED** transaction. + /// Ignores non-fatal errors (like when known header is provided), rewards + /// for successful headers import and penalizes for fatal errors. /// /// This should be used with caution - passing too many headers could lead to /// enormous block production/import time. - pub fn import_headers(origin, headers_with_receipts: Vec<(Header, Option>)>) { + pub fn import_signed_headers(origin, headers_with_receipts: Vec<(Header, Option>)>) { let submitter = frame_system::ensure_signed(origin)?; let mut finalized_headers = BTreeMap::new(); let import_result = import::import_headers( @@ -293,13 +365,13 @@ decl_storage! { /// The ID of next validator set. NextValidatorsSetId: u64; /// Map of validators sets by their id. - ValidatorsSets: map hasher(blake2_256) u64 => Option<(H256, Vec
)>; + ValidatorsSets: map hasher(blake2_256) u64 => Option; /// Validators sets reference count. Each header that is authored by this set increases /// the reference count. When we prune this header, we decrease the reference count. /// When it reaches zero, we are free to prune validator set as well. ValidatorsSetsRc: map hasher(blake2_256) u64 => Option; /// Map of validators set changes scheduled by given header. - ScheduledChanges: map hasher(blake2_256) H256 => Option>; + ScheduledChanges: map hasher(blake2_256) H256 => Option; } add_extra_genesis { config(initial_header): Header; @@ -326,9 +398,14 @@ decl_storage! { 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, (initial_hash, config.initial_validators.clone())); + ValidatorsSets::insert(0, ValidatorsSet { + validators: config.initial_validators.clone(), + signal_block: None, + enact_block: initial_hash, + }); ValidatorsSetsRc::insert(0, 1); }) } @@ -354,6 +431,43 @@ impl Module { } } +impl frame_support::unsigned::ValidateUnsigned for Module { + type Call = Call; + + fn validate_unsigned(call: &Self::Call) -> TransactionValidity { + match *call { + Self::Call::import_unsigned_header(ref header, ref receipts) => { + let accept_result = verification::accept_aura_header_into_pool( + &BridgeStorage::::new(), + &kovan_aura_config(), + &kovan_validators_config(), + &pool_configuration(), + header, + receipts.as_ref(), + ); + + match accept_result { + Ok((requires, provides)) => Ok(ValidTransaction { + priority: TransactionPriority::max_value(), + requires, + provides, + longevity: TransactionLongevity::max_value(), + propagate: true, + }), + // UnsignedTooFarInTheFuture is the special error code used to limit + // number of transactions in the pool - we do not want to ban transaction + // in this case (see verification.rs for details) + Err(error::Error::UnsignedTooFarInTheFuture) => { + UnknownTransaction::Custom(error::Error::UnsignedTooFarInTheFuture.code()).into() + } + Err(error) => InvalidTransaction::Custom(error.code()).into(), + } + } + _ => InvalidTransaction::Call.into(), + } + } +} + /// Runtime bridge storage. #[derive(Default)] struct BridgeStorage(sp_std::marker::PhantomData); @@ -385,20 +499,23 @@ impl Storage for BridgeStorage { parent_hash: &H256, ) -> Option> { Headers::::get(parent_hash).map(|parent_header| { - let (next_validators_set_start, next_validators) = - ValidatorsSets::get(parent_header.next_validators_set_id) - .expect("validators set is only pruned when last ref is pruned; there is a ref; qed"); + let validators_set = ValidatorsSets::get(parent_header.next_validators_set_id) + .expect("validators set is only pruned when last ref is pruned; there is a ref; qed"); + let parent_scheduled_change = ScheduledChanges::get(parent_hash); ImportContext { submitter, + parent_hash: *parent_hash, parent_header: parent_header.header, parent_total_difficulty: parent_header.total_difficulty, - next_validators_set_id: parent_header.next_validators_set_id, - next_validators_set: (next_validators_set_start, next_validators), + parent_scheduled_change, + validators_set_id: parent_header.next_validators_set_id, + validators_set, + last_signal_block: parent_header.last_signal_block, } }) } - fn scheduled_change(&self, hash: &H256) -> Option> { + fn scheduled_change(&self, hash: &H256) -> Option { ScheduledChanges::get(hash) } @@ -407,7 +524,13 @@ impl Storage for BridgeStorage { BestBlock::put((header.header.number, header.hash, header.total_difficulty)); } if let Some(scheduled_change) = header.scheduled_change { - ScheduledChanges::insert(&header.hash, scheduled_change); + ScheduledChanges::insert( + &header.hash, + ScheduledChange { + validators: scheduled_change, + prev_signal_block: header.context.last_signal_block, + }, + ); } let next_validators_set_id = match header.enacted_change { Some(enacted_change) => { @@ -416,19 +539,27 @@ impl Storage for BridgeStorage { *set_id += 1; next_set_id }); - ValidatorsSets::insert(next_validators_set_id, (header.hash, enacted_change)); + ValidatorsSets::insert( + next_validators_set_id, + ValidatorsSet { + validators: enacted_change.validators, + enact_block: header.hash, + signal_block: enacted_change.signal_block, + }, + ); ValidatorsSetsRc::insert(next_validators_set_id, 1); next_validators_set_id } None => { - ValidatorsSetsRc::mutate(header.context.next_validators_set_id, |rc| { + ValidatorsSetsRc::mutate(header.context.validators_set_id, |rc| { *rc = Some(rc.map(|rc| rc + 1).unwrap_or(1)); *rc }); - header.context.next_validators_set_id + header.context.validators_set_id } }; + let last_signal_block = header.context.last_signal_block().cloned(); HeadersByNumber::append_or_insert(header.header.number, vec![header.hash]); Headers::::insert( &header.hash, @@ -437,6 +568,7 @@ impl Storage for BridgeStorage { header: header.header, total_difficulty: header.total_difficulty, next_validators_set_id, + last_signal_block, }, ); } @@ -616,27 +748,11 @@ pub fn kovan_validators_config() -> ValidatorsConfiguration { ]) } -/// Return iterator of given header ancestors. -pub(crate) fn ancestry<'a, S: Storage>( - storage: &'a S, - header: &Header, -) -> impl Iterator)> + 'a { - let mut parent_hash = header.parent_hash.clone(); - from_fn(move || { - let header_and_submitter = storage.header(&parent_hash); - match header_and_submitter { - Some((header, submitter)) => { - if header.number == 0 { - return None; - } - - let hash = parent_hash.clone(); - parent_hash = header.parent_hash.clone(); - Some((hash, header, submitter)) - } - None => None, - } - }) +/// Transaction pool configuration. +fn pool_configuration() -> PoolConfiguration { + PoolConfiguration { + max_future_number_difference: 10, + } } #[cfg(test)] @@ -706,9 +822,9 @@ pub(crate) mod tests { headers: HashMap>, headers_by_number: HashMap>, next_validators_set_id: u64, - validators_sets: HashMap)>, + validators_sets: HashMap, validators_sets_rc: HashMap, - scheduled_changes: HashMap>, + scheduled_changes: HashMap, } impl InMemoryStorage { @@ -726,17 +842,82 @@ pub(crate) mod tests { header: initial_header, total_difficulty: 0.into(), next_validators_set_id: 0, + last_signal_block: None, }, )] .into_iter() .collect(), next_validators_set_id: 1, - validators_sets: vec![(0, (hash, initial_validators))].into_iter().collect(), + validators_sets: vec![( + 0, + ValidatorsSet { + validators: initial_validators, + signal_block: None, + enact_block: hash, + }, + )] + .into_iter() + .collect(), validators_sets_rc: vec![(0, 1)].into_iter().collect(), scheduled_changes: HashMap::new(), } } + pub(crate) fn insert(&mut self, header: Header) { + let hash = header.hash(); + self.headers_by_number.entry(header.number).or_default().push(hash); + self.headers.insert( + hash, + StoredHeader { + submitter: None, + header, + total_difficulty: 0.into(), + next_validators_set_id: 0, + last_signal_block: None, + }, + ); + } + + pub(crate) fn change_validators_set_at( + &mut self, + number: u64, + finalized_set: Vec
, + signalled_set: Option>, + ) { + let set_id = self.next_validators_set_id; + self.next_validators_set_id += 1; + self.validators_sets.insert( + set_id, + ValidatorsSet { + validators: finalized_set, + signal_block: None, + enact_block: self.headers_by_number[&0][0], + }, + ); + + let mut header = self.headers.get_mut(&self.headers_by_number[&number][0]).unwrap(); + header.next_validators_set_id = set_id; + if let Some(signalled_set) = signalled_set { + header.last_signal_block = Some(self.headers_by_number[&(number - 1)][0]); + self.scheduled_changes.insert( + self.headers_by_number[&(number - 1)][0], + ScheduledChange { + validators: signalled_set, + prev_signal_block: None, + }, + ); + } + } + + pub(crate) fn set_best_block(&mut self, best_block: (u64, H256)) { + self.best_block.0 = best_block.0; + self.best_block.1 = best_block.1; + } + + pub(crate) fn set_finalized_block(&mut self, finalized_block: (u64, H256)) { + self.finalized_block = finalized_block; + } + pub(crate) fn oldest_unpruned_block(&self) -> u64 { self.oldest_unpruned_block } @@ -769,19 +950,26 @@ pub(crate) mod tests { parent_hash: &H256, ) -> Option> { self.headers.get(parent_hash).map(|parent_header| { - let (next_validators_set_start, next_validators) = - self.validators_sets.get(&parent_header.next_validators_set_id).unwrap(); + let validators_set = self + .validators_sets + .get(&parent_header.next_validators_set_id) + .unwrap() + .clone(); + let parent_scheduled_change = self.scheduled_changes.get(parent_hash).cloned(); ImportContext { submitter, + parent_hash: *parent_hash, parent_header: parent_header.header.clone(), parent_total_difficulty: parent_header.total_difficulty, - next_validators_set_id: parent_header.next_validators_set_id, - next_validators_set: (*next_validators_set_start, next_validators.clone()), + parent_scheduled_change, + validators_set_id: parent_header.next_validators_set_id, + validators_set, + last_signal_block: parent_header.last_signal_block, } }) } - fn scheduled_change(&self, hash: &H256) -> Option> { + fn scheduled_change(&self, hash: &H256) -> Option { self.scheduled_changes.get(hash).cloned() } @@ -790,26 +978,39 @@ pub(crate) mod tests { self.best_block = (header.header.number, header.hash, header.total_difficulty); } if let Some(scheduled_change) = header.scheduled_change { - self.scheduled_changes.insert(header.hash, scheduled_change); + self.scheduled_changes.insert( + header.hash, + ScheduledChange { + validators: scheduled_change, + prev_signal_block: header.context.last_signal_block, + }, + ); } let next_validators_set_id = match header.enacted_change { Some(enacted_change) => { let next_validators_set_id = self.next_validators_set_id; self.next_validators_set_id += 1; - self.validators_sets - .insert(next_validators_set_id, (header.hash, enacted_change)); + self.validators_sets.insert( + next_validators_set_id, + ValidatorsSet { + validators: enacted_change.validators, + enact_block: header.hash, + signal_block: enacted_change.signal_block, + }, + ); self.validators_sets_rc.insert(next_validators_set_id, 1); next_validators_set_id } None => { *self .validators_sets_rc - .entry(header.context.next_validators_set_id) + .entry(header.context.validators_set_id) .or_default() += 1; - header.context.next_validators_set_id + header.context.validators_set_id } }; + let last_signal_block = header.context.last_signal_block().cloned(); self.headers_by_number .entry(header.header.number) .or_default() @@ -821,6 +1022,7 @@ pub(crate) mod tests { header: header.header, total_difficulty: header.total_difficulty, next_validators_set_id, + last_signal_block, }, ); } diff --git a/bridges/modules/ethereum/src/validators.rs b/bridges/modules/ethereum/src/validators.rs index 24e34eb1674b..a93c21f8de4e 100644 --- a/bridges/modules/ethereum/src/validators.rs +++ b/bridges/modules/ethereum/src/validators.rs @@ -15,7 +15,7 @@ // along with Parity Bridges Common. If not, see . use crate::error::Error; -use crate::Storage; +use crate::{ChangeToEnact, Storage}; use primitives::{Address, Header, LogEntry, Receipt, H256, U256}; use sp_std::prelude::*; @@ -183,10 +183,13 @@ impl<'a> Validators<'a> { &self, storage: &mut S, finalized_blocks: &[(u64, H256, Option)], - ) -> Option> { + ) -> Option { for (_, finalized_hash, _) in finalized_blocks.iter().rev() { if let Some(changes) = storage.scheduled_change(finalized_hash) { - return Some(changes); + return Some(ChangeToEnact { + signal_block: Some(*finalized_hash), + validators: changes.validators, + }); } } None diff --git a/bridges/modules/ethereum/src/verification.rs b/bridges/modules/ethereum/src/verification.rs index 74bba803fb8a..23f8980b3a0b 100644 --- a/bridges/modules/ethereum/src/verification.rs +++ b/bridges/modules/ethereum/src/verification.rs @@ -15,39 +15,205 @@ // along with Parity Bridges Common. If not, see . use crate::error::Error; -use crate::validators::step_validator; -use crate::{AuraConfiguration, ImportContext, Storage}; -use primitives::{public_to_address, Address, Header, SealedEmptyStep, H256, H520, U128, U256}; +use crate::validators::{step_validator, Validators, ValidatorsConfiguration}; +use crate::{AuraConfiguration, ImportContext, PoolConfiguration, ScheduledChange, Storage}; +use codec::Encode; +use primitives::{public_to_address, Address, Header, Receipt, SealedEmptyStep, H256, H520, U128, U256}; use sp_io::crypto::secp256k1_ecdsa_recover; +use sp_std::{vec, vec::Vec}; + +/// Pre-check to see if should try and import this header. +/// Returns error if we should not try to import this block. +/// Returns hash of the header and number of the last finalized block otherwise. +pub fn is_importable_header(storage: &S, header: &Header) -> Result<(H256, H256), Error> { + // we never import any header that competes with finalized header + let (finalized_block_number, finalized_block_hash) = storage.finalized_block(); + if header.number <= finalized_block_number { + return Err(Error::AncientHeader); + } + // we never import any header with known hash + let hash = header.hash(); + if storage.header(&hash).is_some() { + return Err(Error::KnownHeader); + } + + Ok((hash, finalized_block_hash)) +} + +/// Try accept unsigned aura header into transaction pool. +pub fn accept_aura_header_into_pool( + storage: &S, + config: &AuraConfiguration, + validators_config: &ValidatorsConfiguration, + pool_config: &PoolConfiguration, + header: &Header, + receipts: Option<&Vec>, +) -> Result<(Vec>, Vec>), Error> { + // check if we can verify further + let (hash, _) = is_importable_header(storage, header)?; + + // we can always do contextless checks + contextless_checks(config, header)?; + + // we want to avoid having same headers twice in the pool + // => we're strict about receipts here - if we need them, we require receipts to be Some, + // otherwise we require receipts to be None + let receipts_required = Validators::new(validators_config).maybe_signals_validators_change(header); + match (receipts_required, receipts.is_some()) { + (true, false) => return Err(Error::MissingTransactionsReceipts), + (false, true) => return Err(Error::RedundantTransactionsReceipts), + _ => (), + } + + // we do not want to have all future headers in the pool at once + // => if we see header with number > maximal ever seen header number + LIMIT, + // => we consider this transaction invalid, but only at this moment (we do not want to ban it) + // => let's mark it as Unknown transaction + let (best_number, best_hash, _) = storage.best_block(); + let difference = header.number.saturating_sub(best_number); + if difference > pool_config.max_future_number_difference { + return Err(Error::UnsignedTooFarInTheFuture); + } + + // TODO: only accept new headers when we're at the tip of PoA chain + // https://github.com/paritytech/parity-bridges-common/issues/38 + + // we want to see at most one header with given number from single authority + // => every header is providing tag (block_number + authority) + // => since only one tx in the pool can provide the same tag, they're auto-deduplicated + let provides_number_and_authority_tag = (header.number, header.author).encode(); + + // we want to see several 'future' headers in the pool at once, but we may not have access to + // previous headers here + // => we can at least 'verify' that headers comprise a chain by providing and requiring + // tag (header.number, header.hash) + let provides_header_number_and_hash_tag = (header.number, hash).encode(); + + // depending on whether parent header is available, we either perform full or 'shortened' check + let context = storage.import_context(None, &header.parent_hash); + let tags = match context { + Some(context) => { + let header_step = contextual_checks(config, &context, None, header)?; + validator_checks(config, &context.validators_set().validators, header, header_step)?; + + // since our parent is already in the storage, we do not require it + // to be in the transaction pool + ( + vec![], + vec![provides_number_and_authority_tag, provides_header_number_and_hash_tag], + ) + } + None => { + // we know nothing about parent header + // => the best thing we can do is to believe that there are no forks in + // PoA chain AND that the header is produced either by previous, or next + // scheduled validators set change + let header_step = header.step().ok_or(Error::MissingStep)?; + let best_context = storage.import_context(None, &best_hash).expect( + "import context is None only when header is missing from the storage;\ + best header is always in the storage; qed", + ); + let validators_check_result = + validator_checks(config, &best_context.validators_set().validators, header, header_step); + if let Err(error) = validators_check_result { + find_next_validators_signal(storage, &best_context) + .ok_or_else(|| error) + .and_then(|next_validators| validator_checks(config, &next_validators, header, header_step))?; + } + + // since our parent is missing from the storage, we **DO** require it + // to be in the transaction pool + // (- 1 can't underflow because there's always best block in the header) + let requires_header_number_and_hash_tag = (header.number - 1, header.parent_hash).encode(); + ( + vec![requires_header_number_and_hash_tag], + vec![provides_number_and_authority_tag, provides_header_number_and_hash_tag], + ) + } + }; + + // the heaviest, but rare operation - we do not want invalid receipts in the pool + if let Some(receipts) = receipts { + if !header.check_transactions_receipts(receipts) { + return Err(Error::TransactionsReceiptsMismatch); + } + } + + Ok(tags) +} /// Verify header by Aura rules. pub fn verify_aura_header( storage: &S, - params: &AuraConfiguration, + config: &AuraConfiguration, submitter: Option, header: &Header, ) -> Result, Error> { // let's do the lightest check first - contextless_checks(params, header)?; + contextless_checks(config, header)?; - // the rest of checks requires parent + // the rest of checks requires access to the parent header let context = storage .import_context(submitter, &header.parent_hash) .ok_or(Error::MissingParentBlock)?; - let validators = context.validators(); + let header_step = contextual_checks(config, &context, None, header)?; + validator_checks(config, &context.validators_set().validators, header, header_step)?; + + Ok(context) +} + +/// Perform basic checks that only require header itself. +fn contextless_checks(config: &AuraConfiguration, header: &Header) -> Result<(), Error> { + let expected_seal_fields = expected_header_seal_fields(config, header); + if header.seal.len() != expected_seal_fields { + return Err(Error::InvalidSealArity); + } + if header.number >= u64::max_value() { + return Err(Error::RidiculousNumber); + } + if header.gas_used > header.gas_limit { + return Err(Error::TooMuchGasUsed); + } + if header.gas_limit < config.min_gas_limit { + return Err(Error::InvalidGasLimit); + } + if header.gas_limit > config.max_gas_limit { + return Err(Error::InvalidGasLimit); + } + if header.number != 0 && header.extra_data.len() as u64 > config.maximum_extra_data_size { + return Err(Error::ExtraDataOutOfBounds); + } + + // we can't detect if block is from future in runtime + // => let's only do an overflow check + if header.timestamp > i32::max_value() as u64 { + return Err(Error::TimestampOverflow); + } + + Ok(()) +} + +/// Perform checks that require access to parent header. +fn contextual_checks( + config: &AuraConfiguration, + context: &ImportContext, + validators_override: Option<&[Address]>, + header: &Header, +) -> Result { + let validators = validators_override.unwrap_or_else(|| &context.validators_set().validators); let header_step = header.step().ok_or(Error::MissingStep)?; let parent_step = context.parent_header().step().ok_or(Error::MissingStep)?; // Ensure header is from the step after context. - if header_step == parent_step || (header.number >= params.validate_step_transition && header_step <= parent_step) { + if header_step == parent_step || (header.number >= config.validate_step_transition && header_step <= parent_step) { return Err(Error::DoubleVote); } // If empty step messages are enabled we will validate the messages in the seal, missing messages are not // reported as there's no way to tell whether the empty step message was never sent or simply not included. - let empty_steps_len = match header.number >= params.empty_steps_transition { + let empty_steps_len = match header.number >= config.empty_steps_transition { true => { - let strict_empty_steps = header.number >= params.strict_empty_steps_transition; + let strict_empty_steps = header.number >= config.strict_empty_steps_transition; let empty_steps = header.empty_steps().ok_or(Error::MissingEmptySteps)?; let empty_steps_len = empty_steps.len(); let mut prev_empty_step = 0; @@ -76,13 +242,23 @@ pub fn verify_aura_header( }; // Validate chain score. - if header.number >= params.validate_score_transition { + if header.number >= config.validate_score_transition { let expected_difficulty = calculate_score(parent_step, header_step, empty_steps_len as _); if header.difficulty != expected_difficulty { return Err(Error::InvalidDifficulty); } } + Ok(header_step) +} + +/// Check that block is produced by expected validator. +fn validator_checks( + config: &AuraConfiguration, + validators: &[Address], + header: &Header, + header_step: u64, +) -> Result<(), Error> { let expected_validator = step_validator(validators, header_step); if header.author != expected_validator { return Err(Error::NotValidator); @@ -90,44 +266,13 @@ pub fn verify_aura_header( let validator_signature = header.signature().ok_or(Error::MissingSignature)?; let header_seal_hash = header - .seal_hash(header.number >= params.empty_steps_transition) + .seal_hash(header.number >= config.empty_steps_transition) .ok_or(Error::MissingEmptySteps)?; let is_invalid_proposer = !verify_signature(&expected_validator, &validator_signature, &header_seal_hash); if is_invalid_proposer { return Err(Error::NotValidator); } - Ok(context) -} - -/// Perform basic checks that only require header iteself. -fn contextless_checks(config: &AuraConfiguration, header: &Header) -> Result<(), Error> { - let expected_seal_fields = expected_header_seal_fields(config, header); - if header.seal.len() != expected_seal_fields { - return Err(Error::InvalidSealArity); - } - if header.number >= u64::max_value() { - return Err(Error::RidiculousNumber); - } - if header.gas_used > header.gas_limit { - return Err(Error::TooMuchGasUsed); - } - if header.gas_limit < config.min_gas_limit { - return Err(Error::InvalidGasLimit); - } - if header.gas_limit > config.max_gas_limit { - return Err(Error::InvalidGasLimit); - } - if header.number != 0 && header.extra_data.len() as u64 > config.maximum_extra_data_size { - return Err(Error::ExtraDataOutOfBounds); - } - - // we can't detect if block is from future in runtime - // => let's only do an overflow check - if header.timestamp > i32::max_value() as u64 { - return Err(Error::TimestampOverflow); - } - Ok(()) } @@ -160,13 +305,47 @@ fn verify_signature(expected_validator: &Address, signature: &H520, message: &H2 .unwrap_or(false) } +/// Find next unfinalized validators set change after finalized set. +fn find_next_validators_signal(storage: &S, context: &ImportContext) -> Option> { + // that's the earliest block number we may met in following loop + // it may be None if that's the first set + let best_set_signal_block = context.validators_set().signal_block; + + // if parent schedules validators set change, then it may be our set + // else we'll start with last known change + let mut current_set_signal_block = context.last_signal_block().cloned(); + let mut next_scheduled_set: Option = None; + + loop { + // if we have reached block that signals finalized change, then + // next_current_block_hash points to the block that schedules next + // change + let current_scheduled_set = match current_set_signal_block { + Some(current_set_signal_block) if Some(¤t_set_signal_block) == best_set_signal_block.as_ref() => { + return next_scheduled_set.map(|scheduled_set| scheduled_set.validators) + } + None => return next_scheduled_set.map(|scheduled_set| scheduled_set.validators), + Some(current_set_signal_block) => storage.scheduled_change(¤t_set_signal_block).expect( + "header that is associated with this change is not pruned;\ + scheduled changes are only removed when header is pruned; qed", + ), + }; + + current_set_signal_block = current_scheduled_set.prev_signal_block; + next_scheduled_set = Some(current_scheduled_set); + } +} + #[cfg(test)] mod tests { use super::*; - use crate::kovan_aura_config; - use crate::tests::{genesis, signed_header, validator, validators_addresses, AccountId, InMemoryStorage}; + use crate::tests::{ + block_i, custom_block_i, genesis, signed_header, validator, validators_addresses, AccountId, InMemoryStorage, + }; + use crate::validators::{tests::validators_change_recept, ValidatorsSource}; + use crate::{kovan_aura_config, pool_configuration}; use parity_crypto::publickey::{sign, KeyPair}; - use primitives::{rlp_encode, H520}; + use primitives::{rlp_encode, TransactionOutcome, H520}; fn sealed_empty_step(validators: &[KeyPair], parent_hash: &H256, step: u64) -> SealedEmptyStep { let mut empty_step = SealedEmptyStep { @@ -191,6 +370,35 @@ mod tests { verify_with_config(&kovan_aura_config(), header) } + fn default_accept_into_pool( + mut make_header: impl FnMut(&mut InMemoryStorage, &[KeyPair]) -> (Header, Option>), + ) -> Result<(Vec>, Vec>), Error> { + let validators = vec![validator(0), validator(1), validator(2)]; + let mut storage = InMemoryStorage::new(genesis(), validators_addresses(3)); + let block1 = block_i(&storage, 1, &validators); + storage.insert(block1); + let block2 = block_i(&storage, 2, &validators); + let block2_hash = block2.hash(); + storage.insert(block2); + let block3 = block_i(&storage, 3, &validators); + let block3_hash = block3.hash(); + storage.insert(block3); + storage.set_finalized_block((2, block2_hash)); + storage.set_best_block((3, block3_hash)); + + let validators_config = + ValidatorsConfiguration::Single(ValidatorsSource::Contract(Default::default(), Vec::new())); + let (header, receipts) = make_header(&mut storage, &validators); + accept_aura_header_into_pool( + &storage, + &kovan_aura_config(), + &validators_config, + &pool_configuration(), + &header, + receipts.as_ref(), + ) + } + #[test] fn verifies_seal_count() { // when there are no seals at all @@ -433,4 +641,262 @@ mod tests { // when everything is OK assert_eq!(default_verify(&good_header).map(|_| ()), Ok(())); } + + #[test] + fn pool_verifies_known_blocks() { + // when header is known + assert_eq!( + default_accept_into_pool(|storage, validators| (block_i(storage, 3, validators), None)), + Err(Error::KnownHeader), + ); + } + + #[test] + fn pool_verifies_ancient_blocks() { + // when header number is less than finalized + assert_eq!( + default_accept_into_pool(|storage, validators| ( + custom_block_i(storage, 2, validators, |header| header.gas_limit += 1.into()), + None, + ),), + Err(Error::AncientHeader), + ); + } + + #[test] + fn pool_rejects_headers_without_required_receipts() { + assert_eq!( + default_accept_into_pool(|_, _| ( + Header { + number: 20_000_000, + seal: vec![vec![].into(), vec![].into()], + gas_limit: kovan_aura_config().min_gas_limit, + log_bloom: (&[0xff; 256]).into(), + ..Default::default() + }, + None, + ),), + Err(Error::MissingTransactionsReceipts), + ); + } + + #[test] + fn pool_rejects_headers_with_redundant_receipts() { + assert_eq!( + default_accept_into_pool(|storage, validators| ( + block_i(storage, 4, validators), + Some(vec![Receipt { + gas_used: 1.into(), + log_bloom: (&[0xff; 256]).into(), + logs: vec![], + outcome: TransactionOutcome::Unknown, + }]), + ),), + Err(Error::RedundantTransactionsReceipts), + ); + } + + #[test] + fn pool_verifies_future_block_number() { + // when header is too far from the future + assert_eq!( + default_accept_into_pool(|storage, validators| ( + custom_block_i(storage, 4, validators, |header| header.number = 100), + None, + ),), + Err(Error::UnsignedTooFarInTheFuture), + ); + } + + #[test] + fn pool_performs_full_verification_when_parent_is_known() { + // if parent is known, then we'll execute contextual_checks, which + // checks for DoubleVote + assert_eq!( + default_accept_into_pool(|storage, validators| ( + custom_block_i(storage, 4, validators, |header| header.seal[0] = + block_i(storage, 3, validators).seal[0].clone()), + None, + ),), + Err(Error::DoubleVote), + ); + } + + #[test] + fn pool_performs_validators_checks_when_parent_is_unknown() { + // if parent is unknown, then we still need to check if header has required signature + // (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: kovan_aura_config().min_gas_limit, + parent_hash: [42; 32].into(), + number: 8, + ..Default::default() + }, + 43 + ), + None, + )), + Err(Error::NotValidator), + ); + } + + #[test] + fn pool_verifies_header_with_known_parent() { + let mut hash = None; + assert_eq!( + default_accept_into_pool(|storage, validators| { + let header = block_i(&storage, 4, &validators); + hash = Some(header.hash()); + (header, None) + }), + Ok(( + // no tags are required + vec![], + // header provides two tags + vec![ + (4u64, validators_addresses(3)[1]).encode(), + (4u64, hash.unwrap()).encode(), + ], + )), + ); + } + + #[test] + fn pool_verifies_header_with_unknown_parent() { + let mut hash = 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: kovan_aura_config().min_gas_limit, + parent_hash: [42; 32].into(), + number: 5, + ..Default::default() + }, + 47, + ); + hash = Some(header.hash()); + (header, None) + }), + Ok(( + // parent tag required + vec![(4u64, [42u8; 32]).encode(),], + // header provides two tags + vec![ + (5u64, validators_addresses(3)[2]).encode(), + (5u64, hash.unwrap()).encode(), + ], + )), + ); + } + + #[test] + fn pool_uses_next_validators_set_when_finalized_fails() { + assert_eq!( + default_accept_into_pool(|storage, actual_validators| { + // change finalized set at parent header + storage.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: kovan_aura_config().min_gas_limit, + parent_hash: [42; 32].into(), + number: 5, + ..Default::default() + }, + 47, + ); + + (header, None) + }), + Err(Error::NotValidator), + ); + + let mut hash = None; + assert_eq!( + default_accept_into_pool(|storage, actual_validators| { + // change finalized set at parent header + signal valid set at parent block + storage.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: kovan_aura_config().min_gas_limit, + parent_hash: [42; 32].into(), + number: 5, + ..Default::default() + }, + 47, + ); + hash = Some(header.hash()); + + (header, None) + }), + Ok(( + // parent tag required + vec![(4u64, [42u8; 32]).encode(),], + // header provides two tags + vec![ + (5u64, validators_addresses(3)[2]).encode(), + (5u64, hash.unwrap()).encode(), + ], + )), + ); + } + + #[test] + fn pool_rejects_headers_with_invalid_receipts() { + assert_eq!( + default_accept_into_pool(|storage, validators| { + let header = custom_block_i(&storage, 4, &validators, |header| { + header.log_bloom = (&[0xff; 256]).into(); + }); + (header, Some(vec![validators_change_recept(Default::default())])) + }), + Err(Error::TransactionsReceiptsMismatch), + ); + } + + #[test] + fn pool_accepts_headers_with_valid_receipts() { + let mut hash = None; + assert_eq!( + default_accept_into_pool(|storage, validators| { + let header = custom_block_i(&storage, 4, &validators, |header| { + header.log_bloom = (&[0xff; 256]).into(); + header.receipts_root = "81ce88dc524403b796222046bf3daf543978329b87ffd50228f1d3987031dc45" + .parse() + .unwrap(); + }); + hash = Some(header.hash()); + (header, Some(vec![validators_change_recept(Default::default())])) + }), + Ok(( + // no tags are required + vec![], + // header provides two tags + vec![ + (4u64, validators_addresses(3)[1]).encode(), + (4u64, hash.unwrap()).encode(), + ], + )), + ); + } } diff --git a/bridges/relays/ethereum/src/cli.yml b/bridges/relays/ethereum/src/cli.yml index 7a3dcbe2b7ec..750d4f39274a 100644 --- a/bridges/relays/ethereum/src/cli.yml +++ b/bridges/relays/ethereum/src/cli.yml @@ -23,11 +23,22 @@ args: value_name: SUB_PORT help: Connect to Substrate node at given port. takes_value: true + - sub-tx-mode: + long: sub-tx-mode + value_name: MODE + help: Submit headers using signed (default) or unsigned transactions. Third mode - backup - submits signed transactions only when we believe that sync has stalled. + takes_value: true + possible_values: + - signed + - unsigned + - backup - sub-signer: long: sub-signer value_name: SUB_SIGNER help: The SURI of secret key to use when transactions are submitted to the Substrate node. + takes_value: true - sub-signer-password: long: sub-signer-password value_name: SUB_SIGNER_PASSWORD - help: The password for the SURI of secret key to use when transactions are submitted to the Substrate node. \ No newline at end of file + help: The password for the SURI of secret key to use when transactions are submitted to the Substrate node. + takes_value: true \ No newline at end of file diff --git a/bridges/relays/ethereum/src/ethereum_sync.rs b/bridges/relays/ethereum/src/ethereum_sync.rs index df5698f7e002..b174d3819e47 100644 --- a/bridges/relays/ethereum/src/ethereum_sync.rs +++ b/bridges/relays/ethereum/src/ethereum_sync.rs @@ -15,7 +15,7 @@ // along with Parity Bridges Common. If not, see . use crate::ethereum_headers::QueuedHeaders; -use crate::ethereum_sync_loop::EthereumSyncParams; +use crate::ethereum_sync_loop::{EthereumSyncParams, SubstrateTransactionMode}; use crate::ethereum_types::{HeaderId, HeaderStatus, QueuedHeader}; use crate::substrate_types::{into_substrate_ethereum_header, into_substrate_ethereum_receipts}; use codec::Encode; @@ -95,7 +95,12 @@ impl HeadersSync { } /// Select headers that need to be submitted to the Substrate node. - pub fn select_headers_to_submit(&self) -> Option> { + pub fn select_headers_to_submit(&self, stalled: bool) -> Option> { + // if we operate in backup mode, we only submit headers when sync has stalled + if self.params.sub_tx_mode == SubstrateTransactionMode::Backup && !stalled { + return None; + } + let headers_in_submit_status = self.headers.headers_in_status(HeaderStatus::Submitted); let headers_to_submit_count = self .params @@ -224,7 +229,7 @@ mod tests { assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeReceipts), Some(&header(101))); eth_sync.headers.maybe_receipts_response(&id(101), false); assert_eq!(eth_sync.headers.header(HeaderStatus::Ready), Some(&header(101))); - assert_eq!(eth_sync.select_headers_to_submit(), Some(vec![&header(101)])); + assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(101)])); // and header #102 is ready to be downloaded assert_eq!(eth_sync.select_new_header_to_download(), Some(102)); @@ -238,13 +243,13 @@ mod tests { assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeReceipts), Some(&header(102))); eth_sync.headers.maybe_receipts_response(&id(102), false); assert_eq!(eth_sync.headers.header(HeaderStatus::Ready), Some(&header(102))); - assert_eq!(eth_sync.select_headers_to_submit(), None); + assert_eq!(eth_sync.select_headers_to_submit(false), None); // substrate reports that it has imported block #101 eth_sync.substrate_best_header_response(id(101)); // and we are ready to submit #102 - assert_eq!(eth_sync.select_headers_to_submit(), Some(vec![&header(102)])); + assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(102)])); eth_sync.headers.headers_submitted(vec![id(102)]); // substrate reports that it has imported block #102 @@ -269,7 +274,7 @@ mod tests { eth_sync.headers.header_response(header(101).header().clone()); // we can't submit header #101, because its parent status is unknown - assert_eq!(eth_sync.select_headers_to_submit(), None); + assert_eq!(eth_sync.select_headers_to_submit(false), None); // instead we are trying to determine status of its parent (#100) assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeOrphan), Some(&header(101))); @@ -282,7 +287,7 @@ mod tests { eth_sync.headers.header_response(header(100).header().clone()); // we can't submit header #100, because its parent status is unknown - assert_eq!(eth_sync.select_headers_to_submit(), None); + assert_eq!(eth_sync.select_headers_to_submit(false), None); // instead we are trying to determine status of its parent (#99) assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeOrphan), Some(&header(100))); @@ -293,13 +298,13 @@ mod tests { // and we are ready to submit #100 assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeReceipts), Some(&header(100))); eth_sync.headers.maybe_receipts_response(&id(100), false); - assert_eq!(eth_sync.select_headers_to_submit(), Some(vec![&header(100)])); + assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(100)])); eth_sync.headers.headers_submitted(vec![id(100)]); // and we are ready to submit #101 assert_eq!(eth_sync.headers.header(HeaderStatus::MaybeReceipts), Some(&header(101))); eth_sync.headers.maybe_receipts_response(&id(101), false); - assert_eq!(eth_sync.select_headers_to_submit(), Some(vec![&header(101)])); + assert_eq!(eth_sync.select_headers_to_submit(false), Some(vec![&header(101)])); eth_sync.headers.headers_submitted(vec![id(101)]); } @@ -310,4 +315,26 @@ mod tests { eth_sync.substrate_best_header_response(id(100)); assert_eq!(eth_sync.headers.prune_border(), 50); } + + #[test] + fn only_submitting_headers_in_backup_mode_when_stalled() { + let mut eth_sync = HeadersSync::new(Default::default()); + eth_sync.params.sub_tx_mode = SubstrateTransactionMode::Backup; + + // ethereum reports best header #102 + eth_sync.ethereum_best_header_number_response(102); + + // substrate reports that it is at block #100 + eth_sync.substrate_best_header_response(id(100)); + + // block #101 is downloaded first + eth_sync.headers.header_response(header(101).header().clone()); + eth_sync.headers.maybe_receipts_response(&id(101), false); + + // ensure that headers are not submitted when sync is not stalled + assert_eq!(eth_sync.select_headers_to_submit(false), None); + + // ensure that headers are not submitted when sync is stalled + assert_eq!(eth_sync.select_headers_to_submit(true), Some(vec![&header(101)])); + } } diff --git a/bridges/relays/ethereum/src/ethereum_sync_loop.rs b/bridges/relays/ethereum/src/ethereum_sync_loop.rs index 5c91b275106c..b08d503a5fad 100644 --- a/bridges/relays/ethereum/src/ethereum_sync_loop.rs +++ b/bridges/relays/ethereum/src/ethereum_sync_loop.rs @@ -37,6 +37,9 @@ const SUBSTRATE_TICK_INTERVAL_MS: u64 = 5_000; /// the subscriber will receive every best header (2) reorg won't always lead to sync /// stall and restart is a heavy operation (we forget all in-memory headers). const STALL_SYNC_TIMEOUT_MS: u64 = 30_000; +/// Delay (in milliseconds) after we have seen update of best Ethereum header in Substrate, +/// for us to treat sync stalled. ONLY when relay operates in backup mode. +const BACKUP_STALL_SYNC_TIMEOUT_MS: u64 = 5 * 60_000; /// Delay (in milliseconds) after connection-related error happened before we'll try /// reconnection again. const CONNECTION_ERROR_DELAY_MS: u64 = 10_000; @@ -57,6 +60,8 @@ pub struct EthereumSyncParams { pub sub_host: String, /// Substrate RPC port. pub sub_port: u16, + /// Substrate transactions submission mode. + pub sub_tx_mode: SubstrateTransactionMode, /// Substrate transactions signer. pub sub_signer: sp_core::sr25519::Pair, /// Maximal number of ethereum headers to pre-download. @@ -72,6 +77,18 @@ pub struct EthereumSyncParams { pub prune_depth: u64, } +/// Substrate transaction mode. +#[derive(Debug, PartialEq)] +pub enum SubstrateTransactionMode { + /// Submit eth headers using signed substrate transactions. + Signed, + /// Submit eth headers using unsigned substrate transactions. + Unsigned, + /// Submit eth headers using signed substrate transactions, but only when we + /// believe that sync has stalled. + Backup, +} + impl std::fmt::Debug for EthereumSyncParams { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { f.debug_struct("EthereumSyncParams") @@ -79,6 +96,7 @@ impl std::fmt::Debug for EthereumSyncParams { .field("eth_port", &self.eth_port) .field("sub_host", &self.sub_port) .field("sub_port", &self.sub_port) + .field("sub_tx_mode", &self.sub_tx_mode) .field("max_future_headers_to_download", &self.max_future_headers_to_download) .field("max_headers_in_submitted_status", &self.max_headers_in_submitted_status) .field("max_headers_in_single_submit", &self.max_headers_in_single_submit) @@ -98,6 +116,7 @@ impl Default for EthereumSyncParams { eth_port: 8545, sub_host: "localhost".into(), sub_port: 9933, + sub_tx_mode: SubstrateTransactionMode::Signed, sub_signer: sp_keyring::AccountKeyring::Alice.pair(), max_future_headers_to_download: 128, max_headers_in_submitted_status: 128, @@ -112,6 +131,10 @@ impl Default for EthereumSyncParams { pub fn run(params: EthereumSyncParams) { let mut local_pool = futures::executor::LocalPool::new(); let mut progress_context = (std::time::Instant::now(), None, None); + let sign_sub_transactions = match params.sub_tx_mode { + SubstrateTransactionMode::Signed | SubstrateTransactionMode::Backup => true, + SubstrateTransactionMode::Unsigned => false, + }; local_pool.run_until(async move { let eth_uri = format!("http://{}:{}", params.eth_host, params.eth_port); @@ -120,6 +143,7 @@ pub fn run(params: EthereumSyncParams) { let mut eth_sync = crate::ethereum_sync::HeadersSync::new(params); let mut stall_countdown = None; + let mut last_update_time = std::time::Instant::now(); let mut eth_maybe_client = None; let mut eth_best_block_number_required = false; @@ -220,6 +244,9 @@ pub fn run(params: EthereumSyncParams) { sub_best_block, |sub_best_block| { let head_updated = eth_sync.substrate_best_header_response(sub_best_block); + if head_updated { + last_update_time = std::time::Instant::now(); + } match head_updated { // IF head is updated AND there are still our transactions: // => restart stall countdown timer @@ -336,7 +363,9 @@ pub fn run(params: EthereumSyncParams) { sub_existence_status_future .set(substrate_client::ethereum_header_known(sub_client, parent_id).fuse()); - } else if let Some(headers) = eth_sync.select_headers_to_submit() { + } else if let Some(headers) = eth_sync.select_headers_to_submit( + last_update_time.elapsed() > std::time::Duration::from_millis(BACKUP_STALL_SYNC_TIMEOUT_MS), + ) { let ids = match headers.len() { 1 => format!("{:?}", headers[0].id()), 2 => format!("[{:?}, {:?}]", headers[0].id(), headers[1].id()), @@ -350,7 +379,9 @@ pub fn run(params: EthereumSyncParams) { ); let headers = headers.into_iter().cloned().collect(); - sub_submit_header_future.set(substrate_client::submit_ethereum_headers(sub_client, headers).fuse()); + sub_submit_header_future.set( + substrate_client::submit_ethereum_headers(sub_client, headers, sign_sub_transactions).fuse(), + ); // remember that we have submitted some headers if stall_countdown.is_none() { diff --git a/bridges/relays/ethereum/src/main.rs b/bridges/relays/ethereum/src/main.rs index bf256d6fad01..fb276b480cff 100644 --- a/bridges/relays/ethereum/src/main.rs +++ b/bridges/relays/ethereum/src/main.rs @@ -99,5 +99,18 @@ fn ethereum_sync_params() -> Result eth_sync_params.sub_tx_mode = ethereum_sync_loop::SubstrateTransactionMode::Signed, + Some("unsigned") => { + eth_sync_params.sub_tx_mode = ethereum_sync_loop::SubstrateTransactionMode::Unsigned; + + // tx pool won't accept too much unsigned transactions + eth_sync_params.max_headers_in_submitted_status = 10; + } + Some("backup") => eth_sync_params.sub_tx_mode = ethereum_sync_loop::SubstrateTransactionMode::Backup, + Some(mode) => return Err(format!("Invalid sub-tx-mode: {}", mode)), + None => eth_sync_params.sub_tx_mode = ethereum_sync_loop::SubstrateTransactionMode::Signed, + } + Ok(eth_sync_params) } diff --git a/bridges/relays/ethereum/src/substrate_client.rs b/bridges/relays/ethereum/src/substrate_client.rs index 799bf6151ed8..51e2b1886e55 100644 --- a/bridges/relays/ethereum/src/substrate_client.rs +++ b/bridges/relays/ethereum/src/substrate_client.rs @@ -132,7 +132,19 @@ pub async fn ethereum_header_known( pub async fn submit_ethereum_headers( client: Client, headers: Vec, -) -> (Client, Result<(TransactionHash, Vec), Error>) { + sign_transactions: bool, +) -> (Client, Result<(Vec, Vec), Error>) { + match sign_transactions { + true => submit_signed_ethereum_headers(client, headers).await, + false => submit_unsigned_ethereum_headers(client, headers).await, + } +} + +/// Submits signed Ethereum header to Substrate runtime. +pub async fn submit_signed_ethereum_headers( + client: Client, + headers: Vec, +) -> (Client, Result<(Vec, Vec), Error>) { let ids = headers.iter().map(|header| header.id()).collect(); let (client, genesis_hash) = match client.genesis_hash { Some(genesis_hash) => (client, genesis_hash), @@ -152,7 +164,9 @@ pub async fn submit_ethereum_headers( Ok(nonce) => nonce, Err(err) => return (client, Err(err)), }; - let transaction = create_submit_transaction(headers, &client.signer, nonce, genesis_hash); + + let transaction = create_signed_submit_transaction(headers, &client.signer, nonce, genesis_hash); + let encoded_transaction = transaction.encode(); let (client, transaction_hash) = call_rpc( client, @@ -160,7 +174,39 @@ pub async fn submit_ethereum_headers( Params::Array(vec![to_value(Bytes(encoded_transaction)).unwrap()]), ) .await; - (client, transaction_hash.map(|transaction_hash| (transaction_hash, ids))) + + ( + client, + transaction_hash.map(|transaction_hash| (vec![transaction_hash], ids)), + ) +} + +/// Submits unsigned Ethereum header to Substrate runtime. +pub async fn submit_unsigned_ethereum_headers( + mut client: Client, + headers: Vec, +) -> (Client, Result<(Vec, Vec), Error>) { + let ids = headers.iter().map(|header| header.id()).collect(); + let mut transactions_hashes = Vec::new(); + for header in headers { + let transaction = create_unsigned_submit_transaction(header); + + let encoded_transaction = transaction.encode(); + let (used_client, transaction_hash) = call_rpc( + client, + "author_submitExtrinsic", + Params::Array(vec![to_value(Bytes(encoded_transaction)).unwrap()]), + ) + .await; + + client = used_client; + transactions_hashes.push(match transaction_hash { + Ok(transaction_hash) => transaction_hash, + Err(error) => return (client, Err(error)), + }); + } + + (client, Ok((transactions_hashes, ids))) } /// Get Substrate block hash by its number. @@ -236,25 +282,26 @@ async fn call_rpc_u64(mut client: Client, method: &'static str, params: Params) (client, result) } -/// Create Substrate transaction for submitting Ethereum header. -fn create_submit_transaction( +/// Create signed Substrate transaction for submitting Ethereum headers. +fn create_signed_submit_transaction( headers: Vec, signer: &sp_core::sr25519::Pair, index: node_primitives::Index, genesis_hash: H256, ) -> bridge_node_runtime::UncheckedExtrinsic { - let function = bridge_node_runtime::Call::BridgeEthPoA(bridge_node_runtime::BridgeEthPoACall::import_headers( - headers - .into_iter() - .map(|header| { - let (header, receipts) = header.extract(); - ( - into_substrate_ethereum_header(&header), - into_substrate_ethereum_receipts(&receipts), - ) - }) - .collect(), - )); + let function = + bridge_node_runtime::Call::BridgeEthPoA(bridge_node_runtime::BridgeEthPoACall::import_signed_headers( + headers + .into_iter() + .map(|header| { + let (header, receipts) = header.extract(); + ( + into_substrate_ethereum_header(&header), + into_substrate_ethereum_receipts(&receipts), + ) + }) + .collect(), + )); let extra = |i: node_primitives::Index, f: node_primitives::Balance| { ( @@ -284,3 +331,15 @@ fn create_submit_transaction( bridge_node_runtime::UncheckedExtrinsic::new_signed(function, signer.into_account().into(), signature.into(), extra) } + +/// Create unsigned Substrate transaction for submitting Ethereum header. +fn create_unsigned_submit_transaction(header: QueuedEthereumHeader) -> bridge_node_runtime::UncheckedExtrinsic { + let (header, receipts) = header.extract(); + let function = + bridge_node_runtime::Call::BridgeEthPoA(bridge_node_runtime::BridgeEthPoACall::import_unsigned_header( + into_substrate_ethereum_header(&header), + into_substrate_ethereum_receipts(&receipts), + )); + + bridge_node_runtime::UncheckedExtrinsic::new_unsigned(function) +}