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:
Svyatoslav Nikolsky
2020-04-07 15:53:59 +03:00
committed by Bastian Köcher
parent b055027161
commit c6c46462ab
12 changed files with 1043 additions and 211 deletions
+1 -1
View File
@@ -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},
}
);
+31 -20
View File
@@ -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
}
}
+27 -6
View File
@@ -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::*;
+10 -22
View File
@@ -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::*;
+286 -84
View File
@@ -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,
},
);
}
+6 -3
View File
@@ -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
+512 -46
View File
@@ -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(&current_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(&current_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(),
],
)),
);
}
}
+12 -1
View File
@@ -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
+36 -9
View File
@@ -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() {
+13
View File
@@ -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)
}
+76 -17
View File
@@ -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)
}