Limit number of headers that are pruned within single import Call (#105)

* removeInMemoryStorage + extract Kovan stuff to runtime

* removed comment from the future

* limit number of headers that are pruned within single import Call

* verify that pruning range upper bottom is always-increasing

* Fix typo

Co-authored-by: Hernando Castano <HCastano@users.noreply.github.com>
This commit is contained in:
Svyatoslav Nikolsky
2020-06-03 01:17:16 +03:00
committed by Bastian Köcher
parent 3ebaf0548d
commit 944859319e
2 changed files with 295 additions and 43 deletions
+16 -4
View File
@@ -171,7 +171,7 @@ mod tests {
validator, validators, validators_addresses, TestRuntime, validator, validators, validators_addresses, TestRuntime,
}; };
use crate::validators::ValidatorsSource; use crate::validators::ValidatorsSource;
use crate::{BridgeStorage, Headers, OldestUnprunedBlock}; use crate::{BlocksToPrune, BridgeStorage, Headers, PruningRange};
use frame_support::{StorageMap, StorageValue}; use frame_support::{StorageMap, StorageValue};
#[test] #[test]
@@ -264,7 +264,7 @@ mod tests {
} }
#[test] #[test]
fn headers_are_pruned() { fn headers_are_pruned_during_import() {
custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| { custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| {
let validators_config = let validators_config =
ValidatorsConfiguration::Single(ValidatorsSource::Contract([3; 20].into(), validators_addresses(3))); ValidatorsConfiguration::Single(ValidatorsSource::Contract([3; 20].into(), validators_addresses(3)));
@@ -354,7 +354,13 @@ mod tests {
latest_block_hash = rolling_last_block_hash; latest_block_hash = rolling_last_block_hash;
step += 3; step += 3;
} }
assert_eq!(OldestUnprunedBlock::get(), 11); assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 11,
oldest_block_to_keep: 14,
},
);
// now let's insert block signed by validator 1 // now let's insert block signed by validator 1
// => blocks 11..24 are finalized and blocks 11..14 are pruned // => blocks 11..24 are finalized and blocks 11..14 are pruned
@@ -380,7 +386,13 @@ mod tests {
) )
.unwrap(); .unwrap();
assert_eq!(finalized_blocks, expected_blocks); assert_eq!(finalized_blocks, expected_blocks);
assert_eq!(OldestUnprunedBlock::get(), 15); assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 15,
oldest_block_to_keep: 15,
},
);
}); });
} }
} }
+279 -39
View File
@@ -39,6 +39,9 @@ mod verification;
#[cfg(test)] #[cfg(test)]
mod mock; mod mock;
/// Maximal number of blocks we're pruning in single import call.
const MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT: u64 = 8;
/// Authority round engine configuration parameters. /// Authority round engine configuration parameters.
#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug)] #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug)]
pub struct AuraConfiguration { pub struct AuraConfiguration {
@@ -147,6 +150,19 @@ pub struct ChangeToEnact {
pub validators: Vec<Address>, pub validators: Vec<Address>,
} }
/// Blocks range that we want to prune.
#[derive(Encode, Decode, Default, RuntimeDebug, Clone, PartialEq)]
struct PruningRange {
/// Number of the oldest unpruned block(s). This might be the block that we do not
/// want to prune now (then it is equal to `oldest_block_to_keep`), or block that we
/// were unable to prune for whatever reason (i.e. if it isn't finalized yet and has
/// scheduled validators set change).
pub oldest_unpruned_block: u64,
/// Number of oldest block(s) that we want to keep. We want to prune blocks in range
/// [`oldest_unpruned_block`; `oldest_block_to_keep`).
pub oldest_block_to_keep: u64,
}
/// Header import context. /// Header import context.
/// ///
/// The import context contains information needed by the header verification /// The import context contains information needed by the header verification
@@ -364,8 +380,8 @@ decl_storage! {
BestBlock: (u64, H256, U256); BestBlock: (u64, H256, U256);
/// Best finalized block. /// Best finalized block.
FinalizedBlock: (u64, H256); FinalizedBlock: (u64, H256);
/// Oldest unpruned block(s) number. /// Range of blocks that we want to prune.
OldestUnprunedBlock: u64; BlocksToPrune: PruningRange;
/// Map of imported headers by hash. /// Map of imported headers by hash.
Headers: map hasher(identity) H256 => Option<StoredHeader<T::AccountId>>; Headers: map hasher(identity) H256 => Option<StoredHeader<T::AccountId>>;
/// Map of imported header hashes by number. /// Map of imported header hashes by number.
@@ -399,7 +415,10 @@ decl_storage! {
let initial_hash = config.initial_header.hash(); let initial_hash = config.initial_header.hash();
BestBlock::put((config.initial_header.number, initial_hash, config.initial_difficulty)); BestBlock::put((config.initial_header.number, initial_hash, config.initial_difficulty));
FinalizedBlock::put((config.initial_header.number, initial_hash)); FinalizedBlock::put((config.initial_header.number, initial_hash));
OldestUnprunedBlock::put(config.initial_header.number); BlocksToPrune::put(PruningRange {
oldest_unpruned_block: config.initial_header.number,
oldest_block_to_keep: config.initial_header.number,
});
HeadersByNumber::insert(config.initial_header.number, vec![initial_hash]); HeadersByNumber::insert(config.initial_header.number, vec![initial_hash]);
Headers::<T>::insert(initial_hash, StoredHeader { Headers::<T>::insert(initial_hash, StoredHeader {
submitter: None, submitter: None,
@@ -480,10 +499,96 @@ impl<T: Trait> frame_support::unsigned::ValidateUnsigned for Module<T> {
#[derive(Default)] #[derive(Default)]
struct BridgeStorage<T>(sp_std::marker::PhantomData<T>); struct BridgeStorage<T>(sp_std::marker::PhantomData<T>);
impl<T> BridgeStorage<T> { impl<T: Trait> BridgeStorage<T> {
/// Create new BridgeStorage.
pub fn new() -> Self { pub fn new() -> Self {
BridgeStorage(sp_std::marker::PhantomData::<T>::default()) BridgeStorage(sp_std::marker::PhantomData::<T>::default())
} }
/// Prune old blocks.
fn prune_blocks(&self, mut max_blocks_to_prune: u64, finalized_number: u64, prune_end: Option<u64>) {
let pruning_range = BlocksToPrune::get();
let mut new_pruning_range = pruning_range.clone();
// update oldest block we want to keep
if let Some(prune_end) = prune_end {
if prune_end > new_pruning_range.oldest_block_to_keep {
new_pruning_range.oldest_block_to_keep = prune_end;
}
}
// start pruning blocks
let begin = new_pruning_range.oldest_unpruned_block;
let end = new_pruning_range.oldest_block_to_keep;
for number in begin..end {
// if we can't prune anything => break
if max_blocks_to_prune == 0 {
break;
}
// read hashes of blocks with given number and try to prune these blocks
let blocks_at_number = HeadersByNumber::take(number);
if let Some(mut blocks_at_number) = blocks_at_number {
self.prune_blocks_by_hashes(
&mut max_blocks_to_prune,
finalized_number,
number,
&mut blocks_at_number,
);
// if we haven't pruned all blocks, remember unpruned
if !blocks_at_number.is_empty() {
HeadersByNumber::insert(number, blocks_at_number);
break;
}
}
// we have pruned all headers at number
new_pruning_range.oldest_unpruned_block = number + 1;
}
// update pruning range in storage
if pruning_range != new_pruning_range {
BlocksToPrune::put(new_pruning_range);
}
}
/// Prune old blocks with given hashes.
fn prune_blocks_by_hashes(
&self,
max_blocks_to_prune: &mut u64,
finalized_number: u64,
number: u64,
blocks_at_number: &mut Vec<H256>,
) {
// ensure that unfinalized headers we want to prune do not have scheduled changes
if number > finalized_number {
if blocks_at_number
.iter()
.any(|block| ScheduledChanges::contains_key(block))
{
return;
}
}
// physically remove headers and (probably) obsolete validators sets
while let Some(hash) = blocks_at_number.pop() {
let header = Headers::<T>::take(&hash);
ScheduledChanges::remove(hash);
if let Some(header) = header {
ValidatorsSetsRc::mutate(header.next_validators_set_id, |rc| match *rc {
Some(rc) if rc > 1 => Some(rc - 1),
_ => None,
});
}
// check if we have already pruned too much headers in this call
*max_blocks_to_prune -= 1;
if *max_blocks_to_prune == 0 {
return;
}
}
}
} }
impl<T: Trait> Storage for BridgeStorage<T> { impl<T: Trait> Storage for BridgeStorage<T> {
@@ -591,41 +696,8 @@ impl<T: Trait> Storage for BridgeStorage<T> {
FinalizedBlock::put(finalized); FinalizedBlock::put(finalized);
} }
if let Some(prune_end) = prune_end { // and now prune headers if we need to
let prune_begin = OldestUnprunedBlock::get(); self.prune_blocks(MAX_BLOCKS_TO_PRUNE_IN_SINGLE_IMPORT, finalized_number, prune_end);
for number in prune_begin..prune_end {
let blocks_at_number = HeadersByNumber::take(number);
// ensure that unfinalized headers we want to prune do not have scheduled changes
if number > finalized_number {
if let Some(ref blocks_at_number) = blocks_at_number {
if blocks_at_number
.iter()
.any(|block| ScheduledChanges::contains_key(block))
{
HeadersByNumber::insert(number, blocks_at_number);
OldestUnprunedBlock::put(number);
return;
}
}
}
// physically remove headers and (probably) obsolete validators sets
for hash in blocks_at_number.into_iter().flat_map(|x| x) {
let header = Headers::<T>::take(&hash);
ScheduledChanges::remove(hash);
if let Some(header) = header {
ValidatorsSetsRc::mutate(header.next_validators_set_id, |rc| match *rc {
Some(rc) if rc > 1 => Some(rc - 1),
_ => None,
});
}
}
}
OldestUnprunedBlock::put(prune_end);
}
} }
} }
@@ -635,3 +707,171 @@ fn pool_configuration() -> PoolConfiguration {
max_future_number_difference: 10, max_future_number_difference: 10,
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{custom_block_i, custom_test_ext, genesis, validators, validators_addresses, TestRuntime};
fn with_headers_to_prune<T>(f: impl Fn(BridgeStorage<TestRuntime>) -> T) -> T {
custom_test_ext(genesis(), validators_addresses(3)).execute_with(|| {
let validators = validators(3);
for i in 1..10 {
let mut headers_by_number = Vec::with_capacity(5);
for j in 0..5 {
let header = custom_block_i(i, &validators, |header| {
header.gas_limit = header.gas_limit + U256::from(j);
});
let hash = header.hash();
headers_by_number.push(hash);
Headers::<TestRuntime>::insert(
hash,
StoredHeader {
submitter: None,
header: header,
total_difficulty: 0.into(),
next_validators_set_id: 0,
last_signal_block: None,
},
);
if i == 7 && j == 1 {
ScheduledChanges::insert(
hash,
ScheduledChange {
validators: validators_addresses(5),
prev_signal_block: None,
},
);
}
}
HeadersByNumber::insert(i, headers_by_number);
}
f(BridgeStorage::new())
})
}
#[test]
fn blocks_are_not_pruned_if_range_is_empty() {
with_headers_to_prune(|storage| {
BlocksToPrune::put(PruningRange {
oldest_unpruned_block: 5,
oldest_block_to_keep: 5,
});
// try to prune blocks [5; 10)
storage.prune_blocks(0xFFFF, 10, Some(5));
assert_eq!(HeadersByNumber::get(&5).unwrap().len(), 5);
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 5,
oldest_block_to_keep: 5,
},
);
});
}
#[test]
fn blocks_to_prune_never_shrinks_from_the_end() {
with_headers_to_prune(|storage| {
BlocksToPrune::put(PruningRange {
oldest_unpruned_block: 0,
oldest_block_to_keep: 5,
});
// try to prune blocks [5; 10)
storage.prune_blocks(0xFFFF, 10, Some(3));
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 5,
oldest_block_to_keep: 5,
},
);
});
}
#[test]
fn blocks_are_not_pruned_if_limit_is_zero() {
with_headers_to_prune(|storage| {
// try to prune blocks [0; 10)
storage.prune_blocks(0, 10, Some(10));
assert!(HeadersByNumber::get(&0).is_some());
assert!(HeadersByNumber::get(&1).is_some());
assert!(HeadersByNumber::get(&2).is_some());
assert!(HeadersByNumber::get(&3).is_some());
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 0,
oldest_block_to_keep: 10,
},
);
});
}
#[test]
fn blocks_are_pruned_if_limit_is_non_zero() {
with_headers_to_prune(|storage| {
// try to prune blocks [0; 10)
storage.prune_blocks(7, 10, Some(10));
// 1 headers with number = 0 is pruned (1 total)
assert!(HeadersByNumber::get(&0).is_none());
// 5 headers with number = 1 are pruned (6 total)
assert!(HeadersByNumber::get(&1).is_none());
// 1 header with number = 2 are pruned (7 total)
assert_eq!(HeadersByNumber::get(&2).unwrap().len(), 4);
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 2,
oldest_block_to_keep: 10,
},
);
// try to prune blocks [2; 10)
storage.prune_blocks(11, 10, Some(10));
// 4 headers with number = 2 are pruned (4 total)
assert!(HeadersByNumber::get(&2).is_none());
// 5 headers with number = 3 are pruned (9 total)
assert!(HeadersByNumber::get(&3).is_none());
// 2 headers with number = 4 are pruned (11 total)
assert_eq!(HeadersByNumber::get(&4).unwrap().len(), 3);
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 4,
oldest_block_to_keep: 10,
},
);
});
}
#[test]
fn pruning_stops_on_unfainalized_block_with_scheduled_change() {
with_headers_to_prune(|storage| {
// try to prune blocks [0; 10)
// last finalized block is 5
// and one of blocks#7 has scheduled change
// => we won't prune any block#7 at all
storage.prune_blocks(0xFFFF, 5, Some(10));
assert!(HeadersByNumber::get(&0).is_none());
assert!(HeadersByNumber::get(&1).is_none());
assert!(HeadersByNumber::get(&2).is_none());
assert!(HeadersByNumber::get(&3).is_none());
assert!(HeadersByNumber::get(&4).is_none());
assert!(HeadersByNumber::get(&5).is_none());
assert!(HeadersByNumber::get(&6).is_none());
assert_eq!(HeadersByNumber::get(&7).unwrap().len(), 5);
assert_eq!(
BlocksToPrune::get(),
PruningRange {
oldest_unpruned_block: 7,
oldest_block_to_keep: 10,
},
);
});
}
}