Support block revert operation in blockchain cache (#3401)

* support block revert operation in cache

* #[cfg(test)] -> fn unused_sink()

* swap conditions

* post-merge fix
This commit is contained in:
Svyatoslav Nikolsky
2019-10-18 18:32:13 +03:00
committed by Gavin Wood
parent b618d8f0b2
commit 35f8cd19cb
4 changed files with 175 additions and 1 deletions
+140 -1
View File
@@ -39,7 +39,7 @@
//! Finalized entry E1 is pruned when block B is finalized so that:
//! EntryAt(B.number - prune_depth).points_to(E1)
use std::collections::BTreeSet;
use std::collections::{BTreeSet, BTreeMap};
use log::warn;
@@ -89,6 +89,9 @@ pub enum CommitOperation<Block: BlockT, T: CacheItemT> {
/// - new entry is finalized AND/OR
/// - some forks are destroyed
BlockFinalized(ComplexBlockId<Block>, Option<Entry<Block, T>>, BTreeSet<usize>),
/// When best block is reverted - contains the forks that have to be updated
/// (they're either destroyed, or their best entry is updated to earlier block).
BlockReverted(BTreeMap<usize, Option<Fork<Block, T>>>),
}
/// Single fork of list-based cache.
@@ -333,6 +336,44 @@ impl<Block: BlockT, T: CacheItemT, S: Storage<Block, T>> ListCache<Block, T, S>
Ok(Some(operation))
}
/// When block is reverted.
pub fn on_block_revert<Tx: StorageTransaction<Block, T>>(
&self,
tx: &mut Tx,
reverted_block: &ComplexBlockId<Block>,
) -> ClientResult<CommitOperation<Block, T>> {
// can't revert finalized blocks
debug_assert!(self.best_finalized_block.number < reverted_block.number);
// iterate all unfinalized forks and truncate/destroy if required
let mut updated = BTreeMap::new();
for (index, fork) in self.unfinalized.iter().enumerate() {
// we only need to truncate fork if its head is ancestor of truncated block
if fork.head.valid_from.number < reverted_block.number {
continue;
}
// we only need to truncate fork if its head is connected to truncated block
if !chain::is_connected_to_block(&self.storage, reverted_block, &fork.head.valid_from)? {
continue;
}
let updated_fork = fork.truncate(
&self.storage,
tx,
reverted_block.number,
self.best_finalized_block.number,
)?;
updated.insert(index, updated_fork);
}
// schedule commit operation and update meta
let operation = CommitOperation::BlockReverted(updated);
tx.update_meta(self.best_finalized_entry.as_ref(), &self.unfinalized, &operation);
Ok(operation)
}
/// When transaction is committed.
pub fn on_transaction_commit(&mut self, op: CommitOperation<Block, T>) {
match op {
@@ -366,6 +407,14 @@ impl<Block: BlockT, T: CacheItemT, S: Storage<Block, T>> ListCache<Block, T, S>
self.unfinalized.remove(*fork_index);
}
},
CommitOperation::BlockReverted(forks) => {
for (fork_index, updated_fork) in forks.into_iter().rev() {
match updated_fork {
Some(updated_fork) => self.unfinalized[fork_index] = updated_fork,
None => { self.unfinalized.remove(fork_index); },
}
}
},
}
}
@@ -533,6 +582,43 @@ impl<Block: BlockT, T: CacheItemT> Fork<Block, T> {
best_finalized_block,
)
}
/// Truncate fork by deleting all entries that are descendants of given block.
pub fn truncate<S: Storage<Block, T>, Tx: StorageTransaction<Block, T>>(
&self,
storage: &S,
tx: &mut Tx,
reverting_block: NumberFor<Block>,
best_finalized_block: NumberFor<Block>,
) -> ClientResult<Option<Fork<Block, T>>> {
let mut current = self.head.valid_from.clone();
loop {
// read pointer to previous entry
let entry = storage.require_entry(&current)?;
// truncation stops when we have reached the ancestor of truncated block
if current.number < reverting_block {
// if we have reached finalized block => destroy fork
if chain::is_finalized_block(storage, &current, best_finalized_block)? {
return Ok(None);
}
// else fork needs to be updated
return Ok(Some(Fork {
best_block: None,
head: entry.into_entry(current),
}));
}
tx.remove_storage_entry(&current);
// truncation also stops when there are no more entries in the list
current = match entry.prev_valid_from {
Some(prev_valid_from) => prev_valid_from,
None => return Ok(None),
};
}
}
}
/// Destroy fork by deleting all unfinalized entries.
@@ -1400,4 +1486,57 @@ pub mod tests {
do_test(PruningStrategy::ByDepth(10));
do_test(PruningStrategy::NeverPrune)
}
#[test]
fn revert_block_works() {
// 1 -> (2) -> 3 -> 4 -> 5
// \
// -> 5''
// \
// -> (3') -> 4' -> 5'
let mut cache = ListCache::new(
DummyStorage::new()
.with_meta(Some(correct_id(1)), vec![correct_id(5), fork_id(1, 2, 5), fork_id(2, 4, 5)])
.with_id(1, correct_id(1).hash)
.with_entry(correct_id(1), StorageEntry { prev_valid_from: None, value: 1 })
.with_entry(correct_id(3), StorageEntry { prev_valid_from: Some(correct_id(1)), value: 3 })
.with_entry(correct_id(4), StorageEntry { prev_valid_from: Some(correct_id(3)), value: 4 })
.with_entry(correct_id(5), StorageEntry { prev_valid_from: Some(correct_id(4)), value: 5 })
.with_entry(fork_id(1, 2, 4), StorageEntry { prev_valid_from: Some(correct_id(1)), value: 14 })
.with_entry(fork_id(1, 2, 5), StorageEntry { prev_valid_from: Some(fork_id(1, 2, 4)), value: 15 })
.with_entry(fork_id(2, 4, 5), StorageEntry { prev_valid_from: Some(correct_id(4)), value: 25 })
.with_header(test_header(1))
.with_header(test_header(2))
.with_header(test_header(3))
.with_header(test_header(4))
.with_header(test_header(5))
.with_header(fork_header(1, 2, 3))
.with_header(fork_header(1, 2, 4))
.with_header(fork_header(1, 2, 5))
.with_header(fork_header(2, 4, 5)),
PruningStrategy::ByDepth(1024), correct_id(1)
);
// when 5 is reverted: entry 5 is truncated
let op = cache.on_block_revert(&mut DummyTransaction::new(), &correct_id(5)).unwrap();
assert_eq!(op, CommitOperation::BlockReverted(vec![
(0, Some(Fork { best_block: None, head: Entry { valid_from: correct_id(4), value: 4 } })),
].into_iter().collect()));
cache.on_transaction_commit(op);
// when 3 is reverted: entries 4+5' are truncated
let op = cache.on_block_revert(&mut DummyTransaction::new(), &correct_id(3)).unwrap();
assert_eq!(op, CommitOperation::BlockReverted(vec![
(0, None),
(2, None),
].into_iter().collect()));
cache.on_transaction_commit(op);
// when 2 is reverted: entries 4'+5' are truncated
let op = cache.on_block_revert(&mut DummyTransaction::new(), &correct_id(2)).unwrap();
assert_eq!(op, CommitOperation::BlockReverted(vec![
(0, None),
].into_iter().collect()));
cache.on_transaction_commit(op);
}
}
+8
View File
@@ -227,6 +227,14 @@ mod meta {
unfinalized.remove(*fork_index);
}
},
CommitOperation::BlockReverted(ref forks) => {
for (fork_index, updated_fork) in forks.iter().rev() {
match updated_fork {
Some(updated_fork) => unfinalized[*fork_index] = &updated_fork.head().valid_from,
None => { unfinalized.remove(*fork_index); },
}
}
},
}
(finalized, unfinalized).encode()
+21
View File
@@ -268,6 +268,27 @@ impl<'a, Block: BlockT> DbCacheTransaction<'a, Block> {
Ok(self)
}
/// When block is reverted.
pub fn on_block_revert(
mut self,
reverted_block: &ComplexBlockId<Block>,
) -> ClientResult<Self> {
for (name, cache) in self.cache.cache_at.iter() {
let op = cache.on_block_revert(
&mut self::list_storage::DbStorageTransaction::new(
cache.storage(),
&mut self.tx
),
reverted_block,
)?;
assert!(!self.cache_at_op.contains_key(name));
self.cache_at_op.insert(name.to_owned(), op);
}
Ok(self)
}
}
/// Synchronous implementation of database-backed blockchain data cache.
+6
View File
@@ -1518,6 +1518,12 @@ impl<Block> client::backend::Backend<Block, Blake2Hasher> for Backend<Block> whe
impl<Block> client::backend::LocalBackend<Block, Blake2Hasher> for Backend<Block>
where Block: BlockT<Hash=H256> {}
/// TODO: remove me in #3201
pub fn unused_sink<Block: BlockT>(cache_tx: crate::cache::DbCacheTransaction<Block>) {
cache_tx.on_block_revert(&crate::cache::ComplexBlockId::new(Default::default(), 0.into())).unwrap();
unimplemented!()
}
#[cfg(test)]
mod tests {
use hash_db::{HashDB, EMPTY_PREFIX};