Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions client/state-db/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,24 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

//! State database maintenance. Handles canonicalization and pruning in the database. The input to
//! this module is a `ChangeSet` which is basically a list of key-value pairs (trie nodes) that
//! were added or deleted during block execution.
//! State database maintenance. Handles canonicalization and pruning in the database.
//!
//! # Canonicalization.
//! Canonicalization window tracks a tree of blocks identified by header hash. The in-memory
//! overlay allows to get any node that was inserted in any of the blocks within the window.
//! The tree is journaled to the backing database and rebuilt on startup.
//! overlay allows to get any trie node that was inserted in any of the blocks within the window.
//! The overlay is journaled to the backing database and rebuilt on startup.
//! There's a limit of 32 blocks that may have the same block number in the canonicalization window.
//!
//! Canonicalization function selects one root from the top of the tree and discards all other roots
//! and their subtrees.
//! and their subtrees. Upon canonicalization all trie nodes that were inserted in the block are added to
//! the backing DB and block tracking is moved to the pruning window, where no forks are allowed.
//!
//! # Canonicalization vs Finality
//! Database engine uses a notion of canonicality, rather then finality. A canonical block may not be yet finalized
//! from the perspective of the consensus engine, but it still can't be reverted in the database. Most of the time
//! during normal operation last canonical block is the same as last finalized. However if finality stall for a
//! long duration for some reason, there's only a certain number of blocks that can fit in the non-canonical overlay,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So if we actually could not re-org on long non-finalized forks?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, If the fork is longer than 4096 blocks. That's why we recommend running with archive mode on validators.

//! so canonicalization of an unfinalized block may be forced.
//!
//! # Pruning.
//! See `RefWindow` for pruning algorithm details. `StateDb` prunes on each canonicalization until
Expand Down Expand Up @@ -89,6 +97,8 @@ pub enum Error<E: fmt::Debug> {
InvalidParent,
/// Invalid pruning mode specified. Contains expected mode.
InvalidPruningMode(String),
/// Too many unfinalized sibling blocks inserted.
TooManySiblingBlocks,
}

/// Pinning error type.
Expand All @@ -112,6 +122,7 @@ impl<E: fmt::Debug> fmt::Debug for Error<E> {
Error::InvalidBlockNumber => write!(f, "Trying to insert block with invalid number"),
Error::InvalidParent => write!(f, "Trying to insert block with unknown parent"),
Error::InvalidPruningMode(e) => write!(f, "Expected pruning mode: {}", e),
Error::TooManySiblingBlocks => write!(f, "Too many sibling blocks inserted"),
}
}
}
Expand Down
88 changes: 66 additions & 22 deletions client/state-db/src/noncanonical.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use log::trace;

const NON_CANONICAL_JOURNAL: &[u8] = b"noncanonical_journal";
const LAST_CANONICAL: &[u8] = b"last_canonical";
const MAX_BLOCKS_PER_LEVEL: u64 = 32;

/// See module documentation.
#[derive(parity_util_mem_derive::MallocSizeOf)]
Expand Down Expand Up @@ -162,28 +163,30 @@ impl<BlockHash: Hash, Key: Hash> NonCanonicalOverlay<BlockHash, Key> {
let mut total: u64 = 0;
block += 1;
loop {
let mut index: u64 = 0;
let mut level = Vec::new();
loop {
for index in 0 .. MAX_BLOCKS_PER_LEVEL {
let journal_key = to_journal_key(block, index);
match db.get_meta(&journal_key).map_err(|e| Error::Db(e))? {
Some(record) => {
let record: JournalRecord<BlockHash, Key> = Decode::decode(&mut record.as_slice())?;
let inserted = record.inserted.iter().map(|(k, _)| k.clone()).collect();
let overlay = BlockOverlay {
hash: record.hash.clone(),
journal_key,
inserted: inserted,
deleted: record.deleted,
};
insert_values(&mut values, record.inserted);
trace!(target: "state-db", "Uncanonicalized journal entry {}.{} ({} inserted, {} deleted)", block, index, overlay.inserted.len(), overlay.deleted.len());
level.push(overlay);
parents.insert(record.hash, record.parent_hash);
index += 1;
total += 1;
},
None => break,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, so this was the bug? There was some gap and we did not loaded the block(s) after the gap?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right

if let Some(record) = db.get_meta(&journal_key).map_err(|e| Error::Db(e))? {
let record: JournalRecord<BlockHash, Key> = Decode::decode(&mut record.as_slice())?;
let inserted = record.inserted.iter().map(|(k, _)| k.clone()).collect();
let overlay = BlockOverlay {
hash: record.hash.clone(),
journal_key,
inserted: inserted,
deleted: record.deleted,
};
insert_values(&mut values, record.inserted);
trace!(
target: "state-db",
"Uncanonicalized journal entry {}.{} ({} inserted, {} deleted)",
block,
index,
overlay.inserted.len(),
overlay.deleted.len()
);
level.push(overlay);
parents.insert(record.hash, record.parent_hash);
total += 1;
}
}
if level.is_empty() {
Expand Down Expand Up @@ -241,6 +244,10 @@ impl<BlockHash: Hash, Key: Hash> NonCanonicalOverlay<BlockHash, Key> {
.expect("number is [front_block_number .. front_block_number + levels.len()) is asserted in precondition; qed")
};

if level.len() >= MAX_BLOCKS_PER_LEVEL as usize {
return Err(Error::TooManySiblingBlocks);
}

let index = level.len() as u64;
let journal_key = to_journal_key(number, index);

Expand Down Expand Up @@ -513,7 +520,7 @@ mod tests {
use std::io;
use sp_core::H256;
use super::{NonCanonicalOverlay, to_journal_key};
use crate::{ChangeSet, CommitSet};
use crate::{ChangeSet, CommitSet, MetaDb};
use crate::test::{make_db, make_changeset};

fn contains(overlay: &NonCanonicalOverlay<H256, H256>, key: u64) -> bool {
Expand Down Expand Up @@ -716,7 +723,6 @@ mod tests {

#[test]
fn complex_tree() {
use crate::MetaDb;
let mut db = make_db(&[]);

// - 1 - 1_1 - 1_1_1
Expand Down Expand Up @@ -958,4 +964,42 @@ mod tests {
assert!(!contains(&overlay, 1));
assert!(overlay.pinned.is_empty());
}

#[test]
fn restore_from_journal_after_canonicalize_no_first() {
// This test discards a branch that is journaled under a non-zero index on level 1,
// making sure all journals are loaded for each level even if some of them are missing.
let root = H256::random();
let h1 = H256::random();
let h2 = H256::random();
let h11 = H256::random();
let h21 = H256::random();
let mut db = make_db(&[]);
let mut overlay = NonCanonicalOverlay::<H256, H256>::new(&db).unwrap();
db.commit(&overlay.insert::<io::Error>(&root, 10, &H256::default(), make_changeset(&[], &[])).unwrap());
db.commit(&overlay.insert::<io::Error>(&h1, 11, &root, make_changeset(&[1], &[])).unwrap());
db.commit(&overlay.insert::<io::Error>(&h2, 11, &root, make_changeset(&[2], &[])).unwrap());
db.commit(&overlay.insert::<io::Error>(&h11, 12, &h1, make_changeset(&[11], &[])).unwrap());
db.commit(&overlay.insert::<io::Error>(&h21, 12, &h2, make_changeset(&[21], &[])).unwrap());
let mut commit = CommitSet::default();
overlay.canonicalize::<io::Error>(&root, &mut commit).unwrap();
overlay.canonicalize::<io::Error>(&h2, &mut commit).unwrap(); // h11 should stay in the DB
db.commit(&commit);
overlay.apply_pending();
assert_eq!(overlay.levels.len(), 1);
assert!(contains(&overlay, 21));
assert!(!contains(&overlay, 11));
assert!(db.get_meta(&to_journal_key(12, 1)).unwrap().is_some());
assert!(db.get_meta(&to_journal_key(12, 0)).unwrap().is_none());

// Restore into a new overlay and check that journaled value exists.
let mut overlay = NonCanonicalOverlay::<H256, H256>::new(&db).unwrap();
assert!(contains(&overlay, 21));

let mut commit = CommitSet::default();
overlay.canonicalize::<io::Error>(&h21, &mut commit).unwrap(); // h11 should stay in the DB
db.commit(&commit);
overlay.apply_pending();
assert!(!contains(&overlay, 21));
}
}