mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-09 19:01:08 +00:00
Sync ethereum headers using unsigned (substrate) transactions (#45)
* 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 <HCastano@users.noreply.github.com> * Update modules/ethereum/src/verification.rs Co-Authored-By: Hernando Castano <HCastano@users.noreply.github.com> * 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 <HCastano@users.noreply.github.com> * 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 <tomusdrw@users.noreply.github.com> * Update modules/ethereum/src/lib.rs Co-Authored-By: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * return err from unsigned_import_header * get header once * Update relays/ethereum/src/ethereum_sync.rs Co-Authored-By: Tomasz Drwięga <tomusdrw@users.noreply.github.com> * fix * UnsignedTooFarInTheFuture -> Custom(err.code()) * updated ImportContext::last_signal_block * cargo fmt --all * rename runtime calls Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com> Co-authored-by: Tomasz Drwięga <tomusdrw@users.noreply.github.com>
This commit is contained in:
committed by
Bastian Köcher
parent
b055027161
commit
c6c46462ab
@@ -247,7 +247,7 @@ construct_runtime!(
|
||||
Balances: pallet_balances::{Module, Call, Storage, Config<T>, Event<T>},
|
||||
TransactionPayment: pallet_transaction_payment::{Module, Storage},
|
||||
Sudo: pallet_sudo::{Module, Call, Config<T>, Storage, Event<T>},
|
||||
BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage},
|
||||
BridgeEthPoA: pallet_bridge_eth_poa::{Module, Call, Config, Storage, ValidateUnsigned},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,15 +15,18 @@
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Item = (H256, Header, Option<S::Submitter>)> + '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::*;
|
||||
|
||||
@@ -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<S: Storage>(
|
||||
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<S: Storage>(
|
||||
.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<S: Storage>(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::*;
|
||||
|
||||
@@ -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<Submitter> {
|
||||
@@ -67,6 +85,32 @@ pub struct StoredHeader<Submitter> {
|
||||
/// 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<H256>,
|
||||
}
|
||||
|
||||
/// 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<Address>,
|
||||
/// Hash of the block where this set has been signalled. None if this is the first set.
|
||||
pub signal_block: Option<H256>,
|
||||
/// 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<Address>,
|
||||
/// Hash of the block which has emitted previous validators change signal.
|
||||
pub prev_signal_block: Option<H256>,
|
||||
}
|
||||
|
||||
/// Header that we're importing.
|
||||
@@ -83,41 +127,44 @@ pub struct HeaderToImport<Submitter> {
|
||||
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<Vec<Address>>,
|
||||
/// 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<ChangeToEnact>,
|
||||
/// Validators set scheduled change, if happened at the header.
|
||||
pub scheduled_change: Option<Vec<Address>>,
|
||||
}
|
||||
|
||||
/// 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<H256>,
|
||||
/// Validators set that is enacted.
|
||||
pub validators: Vec<Address>,
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
submitter: Option<Submitter>,
|
||||
parent_hash: H256,
|
||||
parent_header: Header,
|
||||
parent_total_difficulty: U256,
|
||||
next_validators_set_id: u64,
|
||||
next_validators_set: (H256, Vec<Address>),
|
||||
parent_scheduled_change: Option<ScheduledChange>,
|
||||
validators_set_id: u64,
|
||||
validators_set: ValidatorsSet,
|
||||
last_signal_block: Option<H256>,
|
||||
}
|
||||
|
||||
impl<Submitter> ImportContext<Submitter> {
|
||||
/// Create import context using passing parameters;
|
||||
pub fn new(
|
||||
submitter: Option<Submitter>,
|
||||
parent_header: Header,
|
||||
parent_total_difficulty: U256,
|
||||
next_validators_set_id: u64,
|
||||
next_validators_set: (H256, Vec<Address>),
|
||||
) -> 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<Submitter> ImportContext<Submitter> {
|
||||
&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<Submitter> ImportContext<Submitter> {
|
||||
hash: H256,
|
||||
header: Header,
|
||||
total_difficulty: U256,
|
||||
enacted_change: Option<Vec<Address>>,
|
||||
enacted_change: Option<ChangeToEnact>,
|
||||
scheduled_change: Option<Vec<Address>>,
|
||||
) -> HeaderToImport<Submitter> {
|
||||
HeaderToImport {
|
||||
@@ -191,8 +247,9 @@ pub trait Storage {
|
||||
submitter: Option<Self::Submitter>,
|
||||
parent_hash: &H256,
|
||||
) -> Option<ImportContext<Self::Submitter>>;
|
||||
/// Get new validators that are scheduled by given header.
|
||||
fn scheduled_change(&self, hash: &H256) -> Option<Vec<Address>>;
|
||||
/// 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<ScheduledChange>;
|
||||
/// Insert imported header.
|
||||
fn insert_header(&mut self, header: HeaderToImport<Self::Submitter>);
|
||||
/// 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<T: Trait> 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<Vec<Receipt>>) {
|
||||
frame_system::ensure_none(origin)?;
|
||||
|
||||
import_header(
|
||||
&mut BridgeStorage::<T>::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<Vec<Receipt>>)>) {
|
||||
pub fn import_signed_headers(origin, headers_with_receipts: Vec<(Header, Option<Vec<Receipt>>)>) {
|
||||
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<Address>)>;
|
||||
ValidatorsSets: map hasher(blake2_256) u64 => Option<ValidatorsSet>;
|
||||
/// 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<u64>;
|
||||
/// Map of validators set changes scheduled by given header.
|
||||
ScheduledChanges: map hasher(blake2_256) H256 => Option<Vec<Address>>;
|
||||
ScheduledChanges: map hasher(blake2_256) H256 => Option<ScheduledChange>;
|
||||
}
|
||||
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<T: Trait> Module<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Trait> frame_support::unsigned::ValidateUnsigned for Module<T> {
|
||||
type Call = Call<T>;
|
||||
|
||||
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::<T>::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<T>(sp_std::marker::PhantomData<T>);
|
||||
@@ -385,20 +499,23 @@ impl<T: Trait> Storage for BridgeStorage<T> {
|
||||
parent_hash: &H256,
|
||||
) -> Option<ImportContext<Self::Submitter>> {
|
||||
Headers::<T>::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<Vec<Address>> {
|
||||
fn scheduled_change(&self, hash: &H256) -> Option<ScheduledChange> {
|
||||
ScheduledChanges::get(hash)
|
||||
}
|
||||
|
||||
@@ -407,7 +524,13 @@ impl<T: Trait> Storage for BridgeStorage<T> {
|
||||
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<T: Trait> Storage for BridgeStorage<T> {
|
||||
*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::<T>::insert(
|
||||
&header.hash,
|
||||
@@ -437,6 +568,7 @@ impl<T: Trait> Storage for BridgeStorage<T> {
|
||||
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<Item = (H256, Header, Option<S::Submitter>)> + '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<H256, StoredHeader<AccountId>>,
|
||||
headers_by_number: HashMap<u64, Vec<H256>>,
|
||||
next_validators_set_id: u64,
|
||||
validators_sets: HashMap<u64, (H256, Vec<Address>)>,
|
||||
validators_sets: HashMap<u64, ValidatorsSet>,
|
||||
validators_sets_rc: HashMap<u64, u64>,
|
||||
scheduled_changes: HashMap<H256, Vec<Address>>,
|
||||
scheduled_changes: HashMap<H256, ScheduledChange>,
|
||||
}
|
||||
|
||||
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<Address>,
|
||||
signalled_set: Option<Vec<Address>>,
|
||||
) {
|
||||
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<ImportContext<Self::Submitter>> {
|
||||
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<Vec<Address>> {
|
||||
fn scheduled_change(&self, hash: &H256) -> Option<ScheduledChange> {
|
||||
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,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<S::Submitter>)],
|
||||
) -> Option<Vec<Address>> {
|
||||
) -> Option<ChangeToEnact> {
|
||||
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
|
||||
|
||||
@@ -15,39 +15,205 @@
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<S: Storage>(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<S: Storage>(
|
||||
storage: &S,
|
||||
config: &AuraConfiguration,
|
||||
validators_config: &ValidatorsConfiguration,
|
||||
pool_config: &PoolConfiguration,
|
||||
header: &Header,
|
||||
receipts: Option<&Vec<Receipt>>,
|
||||
) -> Result<(Vec<Vec<u8>>, Vec<Vec<u8>>), 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<S: Storage>(
|
||||
storage: &S,
|
||||
params: &AuraConfiguration,
|
||||
config: &AuraConfiguration,
|
||||
submitter: Option<S::Submitter>,
|
||||
header: &Header,
|
||||
) -> Result<ImportContext<S::Submitter>, 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<Submitter>(
|
||||
config: &AuraConfiguration,
|
||||
context: &ImportContext<Submitter>,
|
||||
validators_override: Option<&[Address]>,
|
||||
header: &Header,
|
||||
) -> Result<u64, Error> {
|
||||
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<S: Storage>(
|
||||
};
|
||||
|
||||
// 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<S: Storage>(
|
||||
|
||||
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<S: Storage>(storage: &S, context: &ImportContext<S::Submitter>) -> Option<Vec<Address>> {
|
||||
// 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<ScheduledChange> = 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<Vec<Receipt>>),
|
||||
) -> Result<(Vec<Vec<u8>>, Vec<Vec<u8>>), 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(),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
help: The password for the SURI of secret key to use when transactions are submitted to the Substrate node.
|
||||
takes_value: true
|
||||
@@ -15,7 +15,7 @@
|
||||
// along with Parity Bridges Common. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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<Vec<&QueuedHeader>> {
|
||||
pub fn select_headers_to_submit(&self, stalled: bool) -> Option<Vec<&QueuedHeader>> {
|
||||
// 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)]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -99,5 +99,18 @@ fn ethereum_sync_params() -> Result<ethereum_sync_loop::EthereumSyncParams, Stri
|
||||
sp_core::sr25519::Pair::from_string(sub_signer, sub_signer_password).map_err(|e| format!("{:?}", e))?;
|
||||
}
|
||||
|
||||
match matches.value_of("sub-tx-mode") {
|
||||
Some("signed") => 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)
|
||||
}
|
||||
|
||||
@@ -132,7 +132,19 @@ pub async fn ethereum_header_known(
|
||||
pub async fn submit_ethereum_headers(
|
||||
client: Client,
|
||||
headers: Vec<QueuedEthereumHeader>,
|
||||
) -> (Client, Result<(TransactionHash, Vec<EthereumHeaderId>), Error>) {
|
||||
sign_transactions: bool,
|
||||
) -> (Client, Result<(Vec<TransactionHash>, Vec<EthereumHeaderId>), 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<QueuedEthereumHeader>,
|
||||
) -> (Client, Result<(Vec<TransactionHash>, Vec<EthereumHeaderId>), 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<QueuedEthereumHeader>,
|
||||
) -> (Client, Result<(Vec<TransactionHash>, Vec<EthereumHeaderId>), 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<QueuedEthereumHeader>,
|
||||
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user