diff --git a/bridges/bin/node/runtime/src/kovan.rs b/bridges/bin/node/runtime/src/kovan.rs
index 2592d8e057b2..09fb309c70e4 100644
--- a/bridges/bin/node/runtime/src/kovan.rs
+++ b/bridges/bin/node/runtime/src/kovan.rs
@@ -14,11 +14,16 @@
// You should have received a copy of the GNU General Public License
// along with Parity Bridges Common. If not, see .
+use frame_support::RuntimeDebug;
use hex_literal::hex;
-use pallet_bridge_eth_poa::{AuraConfiguration, ValidatorsConfiguration, ValidatorsSource};
+use pallet_bridge_eth_poa::{AuraConfiguration, PruningStrategy, ValidatorsConfiguration, ValidatorsSource};
use sp_bridge_eth_poa::{Address, Header, U256};
use sp_std::prelude::*;
+/// Max number of finalized headers to keep. It is equivalent of ~24 hours of
+/// finalized blocks on current Kovan chain.
+const FINALIZED_HEADERS_TO_KEEP: u64 = 20_000;
+
/// Aura engine configuration for Kovan chain.
pub fn kovan_aura_configuration() -> AuraConfiguration {
AuraConfiguration {
@@ -102,3 +107,45 @@ pub fn kovan_genesis_header() -> Header {
],
}
}
+
+/// Kovan headers pruning strategy.
+///
+/// We do not prune unfinalized headers because exchange module only accepts
+/// claims from finalized headers. And if we're pruning unfinalized headers, then
+/// some claims may never be accepted.
+#[derive(Default, RuntimeDebug)]
+pub struct KovanPruningStrategy;
+
+impl PruningStrategy for KovanPruningStrategy {
+ fn pruning_upper_bound(&mut self, _best_number: u64, best_finalized_number: u64) -> u64 {
+ best_finalized_number
+ .checked_sub(FINALIZED_HEADERS_TO_KEEP)
+ .unwrap_or(0)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn pruning_strategy_keeps_enough_headers() {
+ assert_eq!(
+ KovanPruningStrategy::default().pruning_upper_bound(100_000, 10_000),
+ 0,
+ "10_000 <= 20_000 => nothing should be pruned yet",
+ );
+
+ assert_eq!(
+ KovanPruningStrategy::default().pruning_upper_bound(100_000, 20_000),
+ 0,
+ "20_000 <= 20_000 => nothing should be pruned yet",
+ );
+
+ assert_eq!(
+ KovanPruningStrategy::default().pruning_upper_bound(100_000, 30_000),
+ 10_000,
+ "20_000 <= 30_000 => we're ready to prune first 10_000 headers",
+ );
+ }
+}
diff --git a/bridges/bin/node/runtime/src/lib.rs b/bridges/bin/node/runtime/src/lib.rs
index f92a3a11b792..fe1fc8c90587 100644
--- a/bridges/bin/node/runtime/src/lib.rs
+++ b/bridges/bin/node/runtime/src/lib.rs
@@ -225,6 +225,7 @@ impl pallet_bridge_eth_poa::Trait for Runtime {
type AuraConfiguration = KovanAuraConfiguration;
type FinalityVotesCachingInterval = FinalityVotesCachingInterval;
type ValidatorsConfiguration = KovanValidatorsConfiguration;
+ type PruningStrategy = kovan::KovanPruningStrategy;
type OnHeadersSubmitted = ();
}
diff --git a/bridges/modules/ethereum/src/import.rs b/bridges/modules/ethereum/src/import.rs
index a59bfc750b4a..9f8d146f881b 100644
--- a/bridges/modules/ethereum/src/import.rs
+++ b/bridges/modules/ethereum/src/import.rs
@@ -18,19 +18,10 @@ use crate::error::Error;
use crate::finality::finalize_blocks;
use crate::validators::{Validators, ValidatorsConfiguration};
use crate::verification::{is_importable_header, verify_aura_header};
-use crate::{AuraConfiguration, ChangeToEnact, Storage};
+use crate::{AuraConfiguration, ChangeToEnact, PruningStrategy, Storage};
use primitives::{Header, HeaderId, Receipt};
use sp_std::{collections::btree_map::BTreeMap, prelude::*};
-/// Maximal number of headers behind best blocks that we are aiming to store. When there
-/// are too many unfinalized headers, it slows down finalization tracking significantly.
-/// That's why we won't consider imports/reorganizations to blocks of PRUNE_DEPTH age.
-/// If there's more headers than that, we prune the oldest. The only exception is
-/// when unfinalized header schedules validators set change. We can't compute finality
-/// for pruned headers => we won't know when to enact validators set change. That's
-/// why we never prune headers with scheduled changes.
-pub(crate) const PRUNE_DEPTH: u64 = 4096;
-
/// Imports bunch of headers and updates blocks finality.
///
/// Transactions receipts must be provided if `header_import_requires_receipts()`
@@ -40,11 +31,11 @@ pub(crate) const PRUNE_DEPTH: u64 = 4096;
/// we have NOT imported.
/// Returns error if fatal error has occured during import. Some valid headers may be
/// imported in this case.
-pub fn import_headers(
+pub fn import_headers(
storage: &mut S,
+ pruning_strategy: &mut PS,
aura_config: &AuraConfiguration,
validators_config: &ValidatorsConfiguration,
- prune_depth: u64,
submitter: Option,
headers: Vec<(Header, Option>)>,
finalized_headers: &mut BTreeMap,
@@ -54,9 +45,9 @@ pub fn import_headers(
for (header, receipts) in headers {
let import_result = import_header(
storage,
+ pruning_strategy,
aura_config,
validators_config,
- prune_depth,
submitter.clone(),
header,
receipts,
@@ -85,11 +76,11 @@ pub fn import_headers(
/// has returned true.
///
/// Returns imported block id and list of all finalized headers.
-pub fn import_header(
+pub fn import_header(
storage: &mut S,
+ pruning_strategy: &mut PS,
aura_config: &AuraConfiguration,
validators_config: &ValidatorsConfiguration,
- prune_depth: u64,
submitter: Option,
header: Header,
receipts: Option>,
@@ -126,10 +117,9 @@ pub fn import_header(
// (because otherwise we'll have inconsistent storage if transaction will fail)
// and finally insert the block
- let (_, best_total_difficulty) = storage.best_block();
+ let (best_id, best_total_difficulty) = storage.best_block();
let total_difficulty = import_context.total_difficulty() + header.difficulty;
let is_best = total_difficulty > best_total_difficulty;
- let header_number = header.number;
storage.insert_header(import_context.into_import_header(
is_best,
header_id,
@@ -140,15 +130,19 @@ pub fn import_header(
finalized_blocks.votes,
));
- // now mark finalized headers && prune old headers
- storage.finalize_headers(
- finalized_blocks.finalized_headers.last().map(|(id, _)| *id),
- match is_best {
- true => header_number.checked_sub(prune_depth),
- false => None,
- },
+ // compute upper border of updated pruning range
+ let new_best_block_id = if is_best { header_id } else { best_id };
+ let new_best_finalized_block_id = finalized_blocks.finalized_headers.last().map(|(id, _)| *id);
+ let pruning_upper_bound = pruning_strategy.pruning_upper_bound(
+ new_best_block_id.number,
+ new_best_finalized_block_id
+ .map(|id| id.number)
+ .unwrap_or(finalized_id.number),
);
+ // now mark finalized headers && prune old headers
+ storage.finalize_and_prune_headers(new_best_finalized_block_id, pruning_upper_bound);
+
Ok((header_id, finalized_blocks.finalized_headers))
}
@@ -169,7 +163,7 @@ mod tests {
use super::*;
use crate::mock::{
block_i, custom_block_i, custom_test_ext, genesis, signed_header, test_aura_config, test_validators_config,
- validator, validators, validators_addresses, TestRuntime,
+ validator, validators, validators_addresses, KeepSomeHeadersBehindBest, TestRuntime, GENESIS_STEP,
};
use crate::validators::ValidatorsSource;
use crate::{BlocksToPrune, BridgeStorage, Headers, PruningRange};
@@ -179,19 +173,19 @@ mod tests {
fn rejects_finalized_block_competitors() {
custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| {
let mut storage = BridgeStorage::::new();
- storage.finalize_headers(
+ storage.finalize_and_prune_headers(
Some(HeaderId {
number: 100,
..Default::default()
}),
- None,
+ 0,
);
assert_eq!(
import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&test_validators_config(),
- PRUNE_DEPTH,
None,
Default::default(),
None,
@@ -210,9 +204,9 @@ mod tests {
assert_eq!(
import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&test_validators_config(),
- PRUNE_DEPTH,
None,
block.clone(),
None,
@@ -223,9 +217,9 @@ mod tests {
assert_eq!(
import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&test_validators_config(),
- PRUNE_DEPTH,
None,
block,
None,
@@ -250,9 +244,9 @@ mod tests {
assert_eq!(
import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&validators_config,
- PRUNE_DEPTH,
None,
header,
None
@@ -285,9 +279,9 @@ mod tests {
let header = block_i(i, &validators);
let (rolling_last_block_id, finalized_blocks) = import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&validators_config,
- 10,
Some(100),
header,
None,
@@ -316,9 +310,9 @@ mod tests {
});
let (rolling_last_block_id, finalized_blocks) = import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&validators_config,
- 10,
Some(101),
header11.clone(),
Some(vec![crate::validators::tests::validators_change_recept(
@@ -352,9 +346,9 @@ mod tests {
expected_blocks.push((header.compute_id(), Some(102)));
let (rolling_last_block_id, finalized_blocks) = import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&validators_config,
- 10,
Some(102),
header,
None,
@@ -387,9 +381,9 @@ mod tests {
let header = signed_header(&validators, header, step as _);
let (_, finalized_blocks) = import_header(
&mut storage,
+ &mut KeepSomeHeadersBehindBest::default(),
&test_aura_config(),
&validators_config,
- 10,
Some(103),
header,
None,
@@ -405,4 +399,76 @@ mod tests {
);
});
}
+
+ #[test]
+ fn import_of_non_best_block_may_finalize_blocks() {
+ const TOTAL_VALIDATORS: u8 = 3;
+ let validators_addresses = validators_addresses(TOTAL_VALIDATORS);
+ custom_test_ext(genesis(), validators_addresses.clone()).execute_with(move || {
+ let validators = validators(TOTAL_VALIDATORS);
+ let validators_config = ValidatorsConfiguration::Single(ValidatorsSource::Contract(
+ [0; 20].into(),
+ validators_addresses.clone(),
+ ));
+ let mut storage = BridgeStorage::::new();
+ let mut pruning_strategy = KeepSomeHeadersBehindBest::default();
+
+ // insert headers (H1, validator1), (H2, validator1), (H3, validator1)
+ // making H3 the best header, without finalizing anything (we need 2 signatures)
+ let mut expected_best_block = Default::default();
+ for i in 1..4 {
+ let step = GENESIS_STEP + i * TOTAL_VALIDATORS as u64;
+ let header = custom_block_i(i, &validators, |header| {
+ header.author = validators_addresses[0];
+ header.seal[0][0] = step as u8;
+ });
+ let header = signed_header(&validators, header, step);
+ expected_best_block = header.compute_id();
+ import_header(
+ &mut storage,
+ &mut pruning_strategy,
+ &test_aura_config(),
+ &validators_config,
+ None,
+ header,
+ None,
+ )
+ .unwrap();
+ }
+ let (best_block, best_difficulty) = storage.best_block();
+ assert_eq!(best_block, expected_best_block);
+ assert_eq!(storage.finalized_block(), genesis().compute_id());
+
+ // insert headers (H1', validator1), (H2', validator2), finalizing H2, even though H3
+ // has better difficulty than H2' (because there are more steps involved)
+ let mut expected_finalized_block = Default::default();
+ let mut parent_hash = genesis().compute_hash();
+ for i in 1..3 {
+ let header = custom_block_i(i, &validators, |header| {
+ header.gas_limit += 1.into();
+ header.parent_hash = parent_hash;
+ });
+ let header = signed_header(&validators, header, GENESIS_STEP + i);
+ parent_hash = header.compute_hash();
+ if i == 1 {
+ expected_finalized_block = header.compute_id();
+ }
+
+ import_header(
+ &mut storage,
+ &mut pruning_strategy,
+ &test_aura_config(),
+ &validators_config,
+ None,
+ header,
+ None,
+ )
+ .unwrap();
+ }
+ let (new_best_block, new_best_difficulty) = storage.best_block();
+ assert_eq!(new_best_block, expected_best_block);
+ assert_eq!(new_best_difficulty, best_difficulty);
+ assert_eq!(storage.finalized_block(), expected_finalized_block);
+ });
+ }
}
diff --git a/bridges/modules/ethereum/src/lib.rs b/bridges/modules/ethereum/src/lib.rs
index 8b927bd54ef2..ef7f7b95d514 100644
--- a/bridges/modules/ethereum/src/lib.rs
+++ b/bridges/modules/ethereum/src/lib.rs
@@ -286,15 +286,31 @@ pub trait Storage {
fn scheduled_change(&self, hash: &H256) -> Option;
/// Insert imported header.
fn insert_header(&mut self, header: HeaderToImport);
- /// Finalize given block and prune all headers with number < prune_end.
+ /// Finalize given block and schedules pruning of all headers
+ /// with number < prune_end.
+ ///
/// The headers in the pruning range could be either finalized, or not.
/// It is the storage duty to ensure that unfinalized headers that have
/// scheduled changes won't be pruned until they or their competitors
/// are finalized.
- fn finalize_headers(&mut self, finalized: Option, prune_end: Option);
+ fn finalize_and_prune_headers(&mut self, finalized: Option, prune_end: u64);
+}
+
+/// Headers pruning strategy.
+pub trait PruningStrategy: Default {
+ /// Return upper bound (exclusive) of headers pruning range.
+ ///
+ /// Every value that is returned from this function, must be greater or equal to the
+ /// previous value. Otherwise it will be ignored (we can't revert pruning).
+ ///
+ /// Module may prune both finalized and unfinalized blocks. But it can't give any
+ /// guarantees on when it will happen. Example: if some unfinalized block at height N
+ /// has scheduled validators set change, then the module won't prune any blocks with
+ /// number >= N even if strategy allows that.
+ fn pruning_upper_bound(&mut self, best_number: u64, best_finalized_number: u64) -> u64;
}
-/// Decides whether the session should be ended.
+/// Callbacks for header submission rewards/penalties.
pub trait OnHeadersSubmitted {
/// Called when valid headers have been submitted.
///
@@ -322,6 +338,9 @@ impl OnHeadersSubmitted for () {
pub trait Trait: frame_system::Trait {
/// Aura configuration.
type AuraConfiguration: Get;
+ /// Validators configuration.
+ type ValidatorsConfiguration: Get;
+
/// Interval (in blocks) for for finality votes caching.
/// If None, cache is disabled.
///
@@ -329,8 +348,9 @@ pub trait Trait: frame_system::Trait {
/// be any significant finalization delays), or something that is bit larger
/// than average finalization delay.
type FinalityVotesCachingInterval: Get