Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rkuris/fwdctl check #739

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
6 changes: 3 additions & 3 deletions firewood/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl<T: TrieReader> FusedStream for MerkleNodeStream<'_, T> {
impl<'a, T: TrieReader> MerkleNodeStream<'a, T> {
/// Returns a new iterator that will iterate over all the nodes in `merkle`
/// with keys greater than or equal to `key`.
pub(super) fn new(merkle: &'a T, key: Key) -> Self {
pub fn new(merkle: &'a T, key: Key) -> Self {
Self {
state: NodeStreamState::from(key),
merkle,
Expand All @@ -105,7 +105,7 @@ impl<T: TrieReader> Stream for MerkleNodeStream<'_, T> {

match state {
NodeStreamState::StartFromKey(key) => {
self.state = get_iterator_intial_state(*merkle, key)?;
self.state = get_iterator_initial_state(*merkle, key)?;
self.poll_next(_cx)
}
NodeStreamState::Iterating { iter_stack } => {
Expand Down Expand Up @@ -173,7 +173,7 @@ impl<T: TrieReader> Stream for MerkleNodeStream<'_, T> {
}

/// Returns the initial state for an iterator over the given `merkle` which starts at `key`.
fn get_iterator_intial_state<T: TrieReader>(
fn get_iterator_initial_state<T: TrieReader>(
merkle: &T,
key: &[u8],
) -> Result<NodeStreamState, api::Error> {
Expand Down
1 change: 1 addition & 0 deletions fwdctl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ edition = "2021"

[dependencies]
firewood = { version = "0.0.4", path = "../firewood" }
storage = { version = "0.0.4", path = "../storage" }
clap = { version = "4.5.0", features = ["cargo", "derive"] }
env_logger = "0.11.2"
log = "0.4.20"
Expand Down
129 changes: 129 additions & 0 deletions fwdctl/src/check.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (C) 2023, Ava Labs, Inc. All rights reserved.
// See the file LICENSE.md for licensing terms.

use clap::Args;
use log::warn;
use std::collections::BTreeMap;
use std::io::{Error, ErrorKind};
use std::ops::Bound;
use std::str;
use std::sync::Arc;

use firewood::db::{Db, DbConfig};
use firewood::v2::api::{self, Db as _};
use storage::{Committed, HashedNodeReader as _, LinearAddress, Node, NodeStore, ReadableStorage};

#[derive(Debug, Args)]
pub struct Options {
/// The database path. Defaults to firewood.
#[arg(
long,
required = false,
value_name = "DB_NAME",
default_value_t = String::from("firewood.db"),
help = "Name of the database"
)]
pub db: String,
}

pub(super) async fn run(opts: &Options) -> Result<(), api::Error> {
let cfg = DbConfig::builder().truncate(false);

let db = Db::new(opts.db.clone(), cfg.build()).await?;

let hash = db.root_hash().await?;

let Some(hash) = hash else {
println!("Database is empty");
return Ok(());
};

let rev = db.revision(hash).await?;

// walk the nodes

let addr = rev.root_address_and_hash()?.expect("was not empty").0;
let mut allocated = BTreeMap::new();

visitor(rev.clone(), addr, &mut allocated)?;

let mut expected = 2048;
for (addr, size) in allocated.iter() {
match addr.get().cmp(&expected) {
std::cmp::Ordering::Less => {
warn!(
"Node at {:?} is before the expected address {}",
addr, expected
);
}
std::cmp::Ordering::Greater => {
warn!("{} bytes missing at {}", addr.get() - expected, expected);
}
std::cmp::Ordering::Equal => {}
}
expected = addr.get() + rev.size_from_area_index(*size);
}

Ok(())
}

fn visitor<T: ReadableStorage>(
rev: Arc<NodeStore<Committed, T>>,
addr: LinearAddress,
allocated: &mut BTreeMap<LinearAddress, u8>,
) -> Result<(), Error> {
// find the node before this one, check if it overlaps
if let Some((found_addr, found_size)) = allocated
.range((Bound::Unbounded, Bound::Included(addr)))
.next_back()
{
match found_addr
.get()
.checked_add(rev.size_from_area_index(*found_size))
{
None => warn!("Node at {:?} overflows a u64", found_addr),
Some(end) => {
if end > addr.get() {
warn!(
"Node at {:?} overlaps with another node at {:?} (size: {})",
addr, found_addr, found_size
);
return Err(Error::new(ErrorKind::Other, "Overlapping nodes"));
}
}
}
}
if addr.get() > rev.header().size() {
warn!(
"Node at {:?} starts past the database high water mark",
addr
);
return Err(Error::new(ErrorKind::Other, "Node overflows database"));
}

let (node, size) = rev.uncached_read_node_and_size(addr)?;
if addr.get() + rev.size_from_area_index(size) > rev.header().size() {
warn!(
"Node at {:?} extends past the database high water mark",
addr
);
return Err(Error::new(ErrorKind::Other, "Node overflows database"));
}

allocated.insert(addr, size);

if let Node::Branch(branch) = node.as_ref() {
for child in branch.children.iter() {
match child {
None => {}
Some(child) => match child {
storage::Child::Node(_) => unreachable!(),
storage::Child::AddressWithHash(addr, _hash) => {
visitor(rev.clone(), *addr, allocated)?;
}
},
}
}
}
Ok(())
}
2 changes: 1 addition & 1 deletion fwdctl/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pub struct Options {
}

pub(super) async fn run(opts: &Options) -> Result<(), api::Error> {
log::debug!("dump database {:?}", opts);
log::debug!("graph database {:?}", opts);
let cfg = DbConfig::builder().truncate(false);

let db = Db::new(opts.db.clone(), cfg.build()).await?;
Expand Down
4 changes: 4 additions & 0 deletions fwdctl/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use clap::{Parser, Subcommand};
use firewood::v2::api;

pub mod check;
pub mod create;
pub mod delete;
pub mod dump;
Expand Down Expand Up @@ -45,6 +46,8 @@ enum Commands {
Root(root::Options),
/// Dump contents of key/value store
Dump(dump::Options),
/// Check a database
Check(check::Options),
/// Produce a dot file of the database
Graph(graph::Options),
}
Expand All @@ -65,6 +68,7 @@ async fn main() -> Result<(), api::Error> {
Commands::Delete(opts) => delete::run(opts).await,
Commands::Root(opts) => root::run(opts).await,
Commands::Dump(opts) => dump::run(opts).await,
Commands::Check(opts) => check::run(opts).await,
Commands::Graph(opts) => graph::run(opts).await,
}
}
41 changes: 33 additions & 8 deletions storage/src/nodestore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,7 @@ struct StoredArea<T> {
impl<T: ReadInMemoryNode, S: ReadableStorage> NodeStore<T, S> {
/// Returns (index, area_size) for the [StoredArea] at `addr`.
/// `index` is the index of `area_size` in [AREA_SIZES].
#[allow(dead_code)]
fn area_index_and_size(&self, addr: LinearAddress) -> Result<(AreaIndex, u64), Error> {
pub fn area_index_and_size(&self, addr: LinearAddress) -> Result<(AreaIndex, u64), Error> {
let mut area_stream = self.storage.stream_from(addr.get())?;

let index: AreaIndex = serializer()
Expand Down Expand Up @@ -240,6 +239,30 @@ impl<T: ReadInMemoryNode, S: ReadableStorage> NodeStore<T, S> {
}
Ok(node)
}

/// Read a [Node] from the provided [LinearAddress] and size.
/// This is an uncached read, primarily used by check utilities
pub fn uncached_read_node_and_size(
&self,
addr: LinearAddress,
) -> Result<(SharedNode, u8), Error> {
let mut area_stream = self.storage.stream_from(addr.get())?;
let mut size = [0u8];
area_stream.read_exact(&mut size)?;
self.storage.stream_from(addr.get() + 1)?;
let node: SharedNode = Node::from_reader(area_stream)?.into();
Ok((node, size[0]))
}

/// Get a reference to the header of this nodestore
pub fn header(&self) -> &NodeStoreHeader {
&self.header
}

/// Get the size of an area index (used by the checker)
pub fn size_from_area_index(&self, index: AreaIndex) -> u64 {
AREA_SIZES[index as usize]
}
}

impl<S: ReadableStorage> NodeStore<Committed, S> {
Expand Down Expand Up @@ -325,20 +348,17 @@ impl Parentable for Arc<ImmutableProposal> {
impl<S> NodeStore<Arc<ImmutableProposal>, S> {
/// When an immutable proposal commits, we need to reparent any proposal that
/// has the committed proposal as it's parent
pub fn commit_reparent(&self, other: &Arc<NodeStore<Arc<ImmutableProposal>, S>>) -> bool {
pub fn commit_reparent(&self, other: &Arc<NodeStore<Arc<ImmutableProposal>, S>>) {
match *other.kind.parent.load() {
NodeStoreParent::Proposed(ref parent) => {
if Arc::ptr_eq(&self.kind, parent) {
other
.kind
.parent
.store(NodeStoreParent::Committed(self.kind.root_hash()).into());
true
} else {
false
}
}
NodeStoreParent::Committed(_) => false,
NodeStoreParent::Committed(_) => {}
}
}
}
Expand Down Expand Up @@ -602,7 +622,7 @@ pub type FreeLists = [Option<LinearAddress>; NUM_AREA_SIZES];
/// The [NodeStoreHeader] is at the start of the ReadableStorage.
#[derive(Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Clone, NoUninit, AnyBitPattern)]
#[repr(C)]
struct NodeStoreHeader {
pub struct NodeStoreHeader {
/// Identifies the version of firewood used to create this [NodeStore].
version: Version,
/// always "1"; verifies endianness
Expand Down Expand Up @@ -634,6 +654,11 @@ impl NodeStoreHeader {
free_lists: Default::default(),
}
}

// return the size of this nodestore
pub fn size(&self) -> u64 {
self.size
}
}

/// A [FreeArea] is stored at the start of the area that contained a node that
Expand Down
Loading