mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-04-27 17:28:00 +00:00
Prune leaves on finality (#1871)
* support multiple leaves at same height in leaves set * prune leaves on finalization * test leaves behavior in client-db * fix no-std compilation
This commit is contained in:
committed by
GitHub
parent
72a8927ea3
commit
64685c0536
@@ -14,68 +14,63 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Substrate. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
use std::cmp::{Ord, Ordering};
|
||||
//! Helper for managing the set of available leaves in the chain for DB implementations.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::cmp::Reverse;
|
||||
use kvdb::{KeyValueDB, DBTransaction};
|
||||
use runtime_primitives::traits::SimpleArithmetic;
|
||||
use parity_codec::{Encode, Decode};
|
||||
use crate::error;
|
||||
|
||||
/// helper wrapper type to keep a list of block hashes ordered
|
||||
/// by `number` descending in a `BTreeSet` which allows faster and simpler
|
||||
/// insertion and removal than keeping them in a list.
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
struct LeafSetItem<H, N> {
|
||||
hash: H,
|
||||
number: N,
|
||||
number: Reverse<N>,
|
||||
}
|
||||
|
||||
impl<H, N> Ord for LeafSetItem<H, N> where N: Ord {
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
// reverse (descending) order
|
||||
other.number.cmp(&self.number)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> PartialOrd for LeafSetItem<H, N> where N: PartialOrd {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
// reverse (descending) order
|
||||
other.number.partial_cmp(&self.number)
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> PartialEq for LeafSetItem<H, N> where N: PartialEq {
|
||||
fn eq(&self, other: &LeafSetItem<H, N>) -> bool {
|
||||
self.number == other.number
|
||||
}
|
||||
}
|
||||
|
||||
impl<H, N> Eq for LeafSetItem<H, N> where N: PartialEq {}
|
||||
|
||||
/// A displaced leaf after import.
|
||||
pub struct DisplacedLeaf<H, N> {
|
||||
#[must_use = "Displaced items from the leaf set must be handled."]
|
||||
pub struct ImportDisplaced<H, N> {
|
||||
new_hash: H,
|
||||
displaced: LeafSetItem<H, N>,
|
||||
}
|
||||
|
||||
/// Displaced leaves after finalization.
|
||||
#[must_use = "Displaced items from the leaf set must be handled."]
|
||||
pub struct FinalizationDisplaced<H, N> {
|
||||
leaves: BTreeMap<Reverse<N>, Vec<H>>,
|
||||
}
|
||||
|
||||
impl<H, N: Ord> FinalizationDisplaced<H, N> {
|
||||
/// Merge with another. This should only be used for displaced items that
|
||||
/// are produced within one transaction of each other.
|
||||
pub fn merge(&mut self, mut other: Self) {
|
||||
// this will ignore keys that are in duplicate, however
|
||||
// if these are actually produced correctly via the leaf-set within
|
||||
// one transaction, then there will be no overlap in the keys.
|
||||
self.leaves.append(&mut other.leaves);
|
||||
}
|
||||
}
|
||||
|
||||
/// list of leaf hashes ordered by number (descending).
|
||||
/// stored in memory for fast access.
|
||||
/// this allows very fast checking and modification of active leaves.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LeafSet<H, N> {
|
||||
storage: BTreeSet<LeafSetItem<H, N>>,
|
||||
storage: BTreeMap<Reverse<N>, Vec<H>>,
|
||||
pending_added: Vec<LeafSetItem<H, N>>,
|
||||
pending_removed: Vec<H>,
|
||||
}
|
||||
|
||||
impl<H, N> LeafSet<H, N> where
|
||||
H: Clone + Decode + Encode,
|
||||
N: Clone + SimpleArithmetic + Decode + Encode,
|
||||
H: Clone + PartialEq + Decode + Encode,
|
||||
N: std::fmt::Debug + Clone + SimpleArithmetic + Decode + Encode,
|
||||
{
|
||||
/// Construct a new, blank leaf set.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
storage: BTreeSet::new(),
|
||||
storage: BTreeMap::new(),
|
||||
pending_added: Vec::new(),
|
||||
pending_removed: Vec::new(),
|
||||
}
|
||||
@@ -83,7 +78,8 @@ impl<H, N> LeafSet<H, N> where
|
||||
|
||||
/// Read the leaf list from the DB, using given prefix for keys.
|
||||
pub fn read_from_db(db: &KeyValueDB, column: Option<u32>, prefix: &[u8]) -> error::Result<Self> {
|
||||
let mut storage = BTreeSet::new();
|
||||
let mut storage = BTreeMap::new();
|
||||
|
||||
for (key, value) in db.iter_from_prefix(column, prefix) {
|
||||
if !key.starts_with(prefix) { break }
|
||||
let raw_hash = &mut &key[prefix.len()..];
|
||||
@@ -95,7 +91,7 @@ impl<H, N> LeafSet<H, N> where
|
||||
Some(number) => number,
|
||||
None => return Err(error::ErrorKind::Backend("Error decoding number".into()).into()),
|
||||
};
|
||||
storage.insert(LeafSetItem { hash, number });
|
||||
storage.entry(Reverse(number)).or_insert_with(Vec::new).push(hash);
|
||||
}
|
||||
Ok(Self {
|
||||
storage,
|
||||
@@ -105,20 +101,20 @@ impl<H, N> LeafSet<H, N> where
|
||||
}
|
||||
|
||||
/// update the leaf list on import. returns a displaced leaf if there was one.
|
||||
pub fn import(&mut self, hash: H, number: N, parent_hash: H) -> Option<DisplacedLeaf<H, N>> {
|
||||
pub fn import(&mut self, hash: H, number: N, parent_hash: H) -> Option<ImportDisplaced<H, N>> {
|
||||
// avoid underflow for genesis.
|
||||
let displaced = if number != N::zero() {
|
||||
let displaced = LeafSetItem {
|
||||
hash: parent_hash.clone(),
|
||||
number: number.clone() - N::one(),
|
||||
};
|
||||
let was_displaced = self.storage.remove(&displaced);
|
||||
let new_number = Reverse(number.clone() - N::one());
|
||||
let was_displaced = self.remove_leaf(&new_number, &parent_hash);
|
||||
|
||||
if was_displaced {
|
||||
self.pending_removed.push(parent_hash);
|
||||
Some(DisplacedLeaf {
|
||||
self.pending_removed.push(parent_hash.clone());
|
||||
Some(ImportDisplaced {
|
||||
new_hash: hash.clone(),
|
||||
displaced,
|
||||
displaced: LeafSetItem {
|
||||
hash: parent_hash,
|
||||
number: new_number,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -127,36 +123,53 @@ impl<H, N> LeafSet<H, N> where
|
||||
None
|
||||
};
|
||||
|
||||
let item = LeafSetItem { hash, number };
|
||||
self.storage.insert(item.clone());
|
||||
self.pending_added.push(item);
|
||||
self.insert_leaf(Reverse(number.clone()), hash.clone());
|
||||
self.pending_added.push(LeafSetItem { hash, number: Reverse(number) });
|
||||
displaced
|
||||
}
|
||||
|
||||
/// Undo an import operation, with a displaced leaf.
|
||||
pub fn undo(&mut self, displaced: DisplacedLeaf<H, N>) {
|
||||
let new_number = displaced.displaced.number.clone() + N::one();
|
||||
self.storage.remove(&LeafSetItem { hash: displaced.new_hash, number: new_number });
|
||||
self.storage.insert(displaced.displaced);
|
||||
self.pending_added.clear();
|
||||
self.pending_removed.clear();
|
||||
/// Note a block height finalized, displacing all leaves with number less than the finalized block's.
|
||||
///
|
||||
/// Although it would be more technically correct to also prune out leaves at the
|
||||
/// same number as the finalized block, but with different hashes, the current behavior
|
||||
/// is simpler and our assumptions about how finalization works means that those leaves
|
||||
/// will be pruned soon afterwards anyway.
|
||||
pub fn finalize_height(&mut self, number: N) -> FinalizationDisplaced<H, N> {
|
||||
let boundary = if number == N::zero() {
|
||||
return FinalizationDisplaced { leaves: BTreeMap::new() };
|
||||
} else {
|
||||
number - N::one()
|
||||
};
|
||||
|
||||
let below_boundary = self.storage.split_off(&Reverse(boundary));
|
||||
self.pending_removed.extend(below_boundary.values().flat_map(|h| h.iter()).cloned());
|
||||
FinalizationDisplaced {
|
||||
leaves: below_boundary,
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo all pending operations.
|
||||
///
|
||||
/// This returns an `Undo` struct, where any
|
||||
/// `Displaced` objects that have returned by previous method calls
|
||||
/// should be passed to via the appropriate methods. Otherwise,
|
||||
/// the on-disk state may get out of sync with in-memory state.
|
||||
pub fn undo(&mut self) -> Undo<H, N> {
|
||||
Undo { inner: self }
|
||||
}
|
||||
|
||||
/// currently since revert only affects the canonical chain
|
||||
/// we assume that parent has no further children
|
||||
/// and we add it as leaf again
|
||||
pub fn revert(&mut self, hash: H, number: N, parent_hash: H) {
|
||||
self.storage.insert(LeafSetItem {
|
||||
hash: parent_hash,
|
||||
number: number.clone() - N::one(),
|
||||
});
|
||||
self.storage.remove(&LeafSetItem { hash, number });
|
||||
self.insert_leaf(Reverse(number.clone() - N::one()), parent_hash);
|
||||
self.remove_leaf(&Reverse(number), &hash);
|
||||
}
|
||||
|
||||
/// returns an iterator over all hashes in the leaf set
|
||||
/// ordered by their block number descending.
|
||||
pub fn hashes(&self) -> Vec<H> {
|
||||
self.storage.iter().map(|item| item.hash.clone()).collect()
|
||||
self.storage.iter().flat_map(|(_, hashes)| hashes.iter()).cloned().collect()
|
||||
}
|
||||
|
||||
/// Write the leaf list to the database transaction.
|
||||
@@ -164,7 +177,7 @@ impl<H, N> LeafSet<H, N> where
|
||||
let mut buf = prefix.to_vec();
|
||||
for LeafSetItem { hash, number } in self.pending_added.drain(..) {
|
||||
hash.using_encoded(|s| buf.extend(s));
|
||||
tx.put_vec(column, &buf[..], number.encode());
|
||||
tx.put_vec(column, &buf[..], number.0.encode());
|
||||
buf.truncate(prefix.len()); // reuse allocation.
|
||||
}
|
||||
for hash in self.pending_removed.drain(..) {
|
||||
@@ -173,6 +186,68 @@ impl<H, N> LeafSet<H, N> where
|
||||
buf.truncate(prefix.len()); // reuse allocation.
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn contains(&self, number: N, hash: H) -> bool {
|
||||
self.storage.get(&Reverse(number)).map_or(false, |hashes| hashes.contains(&hash))
|
||||
}
|
||||
|
||||
fn insert_leaf(&mut self, number: Reverse<N>, hash: H) {
|
||||
self.storage.entry(number).or_insert_with(Vec::new).push(hash);
|
||||
}
|
||||
|
||||
// returns true if this leaf was contained, false otherwise.
|
||||
fn remove_leaf(&mut self, number: &Reverse<N>, hash: &H) -> bool {
|
||||
let mut empty = false;
|
||||
let removed = self.storage.get_mut(number).map_or(false, |leaves| {
|
||||
let mut found = false;
|
||||
leaves.retain(|h| if h == hash {
|
||||
found = true;
|
||||
false
|
||||
} else {
|
||||
true
|
||||
});
|
||||
|
||||
if leaves.is_empty() { empty = true }
|
||||
|
||||
found
|
||||
});
|
||||
|
||||
if removed && empty {
|
||||
self.storage.remove(number);
|
||||
}
|
||||
|
||||
removed
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper for undoing operations.
|
||||
pub struct Undo<'a, H: 'a, N: 'a> {
|
||||
inner: &'a mut LeafSet<H, N>,
|
||||
}
|
||||
|
||||
impl<'a, H: 'a, N: 'a> Undo<'a, H, N> where
|
||||
H: Clone + PartialEq + Decode + Encode,
|
||||
N: std::fmt::Debug + Clone + SimpleArithmetic + Decode + Encode,
|
||||
{
|
||||
/// Undo an imported block by providing the displaced leaf.
|
||||
pub fn undo_import(&mut self, displaced: ImportDisplaced<H, N>) {
|
||||
let new_number = Reverse(displaced.displaced.number.0.clone() + N::one());
|
||||
self.inner.remove_leaf(&new_number, &displaced.new_hash);
|
||||
self.inner.insert_leaf(new_number, displaced.displaced.hash);
|
||||
}
|
||||
|
||||
/// Undo a finalization operation by providing the displaced leaves.
|
||||
pub fn undo_finalization(&mut self, mut displaced: FinalizationDisplaced<H, N>) {
|
||||
self.inner.storage.append(&mut displaced.leaves);
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, H: 'a, N: 'a> Drop for Undo<'a, H, N> {
|
||||
fn drop(&mut self) {
|
||||
self.inner.pending_added.clear();
|
||||
self.inner.pending_removed.clear();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -188,15 +263,15 @@ mod tests {
|
||||
set.import(2_1, 2, 1_1);
|
||||
set.import(3_1, 3, 2_1);
|
||||
|
||||
assert!(set.storage.contains(&LeafSetItem { hash: 3_1, number: 3 }));
|
||||
assert!(!set.storage.contains(&LeafSetItem { hash: 2_1, number: 2 }));
|
||||
assert!(!set.storage.contains(&LeafSetItem { hash: 1_1, number: 1 }));
|
||||
assert!(!set.storage.contains(&LeafSetItem { hash: 0, number: 0 }));
|
||||
assert!(set.contains(3, 3_1));
|
||||
assert!(!set.contains(2, 2_1));
|
||||
assert!(!set.contains(1, 1_1));
|
||||
assert!(!set.contains(0, 0));
|
||||
|
||||
set.import(2_2, 2, 1_1);
|
||||
|
||||
assert!(set.storage.contains(&LeafSetItem { hash: 3_1, number: 3 }));
|
||||
assert!(set.storage.contains(&LeafSetItem { hash: 2_2, number: 2 }));
|
||||
assert!(set.contains(3, 3_1));
|
||||
assert!(set.contains(2, 2_2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -219,4 +294,63 @@ mod tests {
|
||||
let set2 = LeafSet::read_from_db(&db, None, PREFIX).unwrap();
|
||||
assert_eq!(set, set2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_leaves_same_height_can_be_included() {
|
||||
let mut set = LeafSet::new();
|
||||
|
||||
set.import(1_1u32, 10u32,0u32);
|
||||
set.import(1_2, 10, 0);
|
||||
|
||||
assert!(set.storage.contains_key(&Reverse(10)));
|
||||
assert!(set.contains(10, 1_1));
|
||||
assert!(set.contains(10, 1_2));
|
||||
assert!(!set.contains(10, 1_3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalization_consistent_with_disk() {
|
||||
const PREFIX: &[u8] = b"prefix";
|
||||
let db = ::kvdb_memorydb::create(0);
|
||||
|
||||
let mut set = LeafSet::new();
|
||||
set.import(10_1u32, 10u32, 0u32);
|
||||
set.import(11_1, 11, 10_2);
|
||||
set.import(11_2, 11, 10_2);
|
||||
set.import(12_1, 12, 11_123);
|
||||
|
||||
assert!(set.contains(10, 10_1));
|
||||
|
||||
let mut tx = DBTransaction::new();
|
||||
set.prepare_transaction(&mut tx, None, PREFIX);
|
||||
db.write(tx).unwrap();
|
||||
|
||||
let _ = set.finalize_height(11);
|
||||
let mut tx = DBTransaction::new();
|
||||
set.prepare_transaction(&mut tx, None, PREFIX);
|
||||
db.write(tx).unwrap();
|
||||
|
||||
assert!(set.contains(11, 11_1));
|
||||
assert!(set.contains(11, 11_2));
|
||||
assert!(set.contains(12, 12_1));
|
||||
assert!(!set.contains(10, 10_1));
|
||||
|
||||
let set2 = LeafSet::read_from_db(&db, None, PREFIX).unwrap();
|
||||
assert_eq!(set, set2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn undo_finalization() {
|
||||
let mut set = LeafSet::new();
|
||||
set.import(10_1u32, 10u32, 0u32);
|
||||
set.import(11_1, 11, 10_2);
|
||||
set.import(11_2, 11, 10_2);
|
||||
set.import(12_1, 12, 11_123);
|
||||
|
||||
let displaced = set.finalize_height(11);
|
||||
assert!(!set.contains(10, 10_1));
|
||||
|
||||
set.undo().undo_finalization(displaced);
|
||||
assert!(set.contains(10, 10_1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,9 @@ pub mod block_builder;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod light;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod children;
|
||||
pub mod leaves;
|
||||
#[cfg(feature = "std")]
|
||||
mod leaves;
|
||||
pub mod children;
|
||||
#[cfg(feature = "std")]
|
||||
mod call_executor;
|
||||
#[cfg(feature = "std")]
|
||||
|
||||
Reference in New Issue
Block a user