diff --git a/durable-storage/src/avl/node.rs b/durable-storage/src/avl/node.rs index 865e2f19d8..7cc665fadd 100644 --- a/durable-storage/src/avl/node.rs +++ b/durable-storage/src/avl/node.rs @@ -6,6 +6,7 @@ use std::borrow::Borrow; use std::cmp::Ordering; +use std::ops::Deref; use std::sync::OnceLock; use bincode::Decode; @@ -22,6 +23,8 @@ use octez_riscv_data::hash::HashFold; use octez_riscv_data::mode::Mode; use octez_riscv_data::mode::Normal; use octez_riscv_data::mode::Prove; +use octez_riscv_data::serialisation::deserialise; +use octez_riscv_data::serialisation::serialise; use perfect_derive::perfect_derive; use super::resolver::LazyTreeId; @@ -32,6 +35,10 @@ use crate::avl::resolver::AvlResolver; use crate::errors::Error; use crate::errors::OperationalError; use crate::key::Key; +use crate::storage::KeyValueStore; +use crate::storage::Loadable; +use crate::storage::Storable; +use crate::storage::StoreOptions; /// Metadata of a [`Node`] needed for accesses. #[derive(Clone, Default, Debug, Encode, Decode)] @@ -42,22 +49,12 @@ pub(crate) struct Meta { balance_factor: i64, } -/// A serialisable representation of [`Meta`]. +/// This type is a compact serialised form of a [`Node`] with metadata and child subtree hashes. #[derive(Encode, Decode)] -pub(super) struct MetaHashRepresentation> { - key: K, - balance_factor: i64, -} - -/// A serialisable representation of [`Node`]. -#[derive(Encode, Decode)] -pub(super) struct NodeHashRepresentation, H: Borrow> { - meta: MetaHashRepresentation, - data: Data, - // The hash of the left subtree. - left: H, - // The hash of the right subtree. - right: H, +struct StoredNode { + meta: Meta, + left: Hash, + right: Hash, } /// A node that supports rebalancing and Merklisation. @@ -75,25 +72,6 @@ pub struct Node { hash: OnceLock, } -impl From, Key, Hash>> - for Node -where - TreeId: From, -{ - fn from(node_repr: NodeHashRepresentation, Key, Hash>) -> Self { - Node { - meta: Atom::new(Meta { - key: node_repr.meta.key, - balance_factor: node_repr.meta.balance_factor, - }), - data: node_repr.data, - left: TreeId::from(node_repr.left), - right: TreeId::from(node_repr.right), - hash: OnceLock::new(), - } - } -} - impl Node { /// Converts the [`Node`] to [`Prove`] mode. pub fn into_proof(self) -> Node> { @@ -170,31 +148,12 @@ impl Node { } } - /// Converts the [`Node`] to an encoded, serialisable representation, - /// [`NodeHashRepresentation`], potentially re-hashing uncached [`Node`]s. - pub(crate) fn to_encode<'a>(&'a self) -> impl Encode + 'a - where - Bytes: Encode, - TreeId: Foldable, - { - NodeHashRepresentation { - meta: MetaHashRepresentation { - key: &self.meta.key, - balance_factor: self.meta.balance_factor, - }, - data: &self.data, - left: Hash::from_foldable(&self.left), - right: Hash::from_foldable(&self.right), - } - } - /// Returns the hash of this node. /// /// If the hash has been cached, the memo is returned. Otherwise, the hash is calculated and /// cached. pub(crate) fn hash(&self) -> &Hash where - TreeId: Foldable, Atom: Foldable, Bytes: Foldable, TreeId: Foldable, @@ -204,7 +163,7 @@ impl Node { #[inline] /// The difference in heights between child branches. - pub(super) fn balance_factor(&self) -> i64 { + pub(crate) fn balance_factor(&self) -> i64 { self.meta.balance_factor } @@ -216,10 +175,16 @@ impl Node { #[inline] /// The [`Key`] used for determining the [`Node`]. - pub(super) fn key(&self) -> &Key { + pub(crate) fn key(&self) -> &Key { &self.meta.key } + /// Retrieve the value associated with this node. + #[cfg(all(test, feature = "rocksdb"))] + pub(crate) fn value(&self) -> &Bytes { + &self.data + } + /// Rebalance the subtree of the [`Node`] so that the difference in height between child /// branches is in the range of -1..=1. /// @@ -832,6 +797,80 @@ impl Node { } } +impl Storable for Node { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + // The stored representation is more compact. We don't include the `data` field, as that + // should be written to the KV store separately. + let repr = StoredNode { + meta: self.meta.deref().clone(), + left: Hash::from_foldable(&self.left), + right: Hash::from_foldable(&self.right), + }; + + let &id = self.hash(); + let bytes = serialise(repr)?; + store.blob_set(id, bytes)?; + + // Are we in charge of writing the value data to the KV store? + if options.node_data() { + let key: &[u8] = self.meta.key.as_ref(); + let value: &[u8] = self.data.borrow(); + store.set(key, value)?; + } + + if options.deep() { + self.left.store(store, options)?; + self.right.store(store, options)?; + } + + Ok(()) + } +} + +impl Loadable for Node { + fn load(id: Hash, store: &impl KeyValueStore) -> Result { + let StoredNode { meta, left, right } = { + let bytes = + store + .blob_get(id) + .map_err(|error| OperationalError::CommitDataMissing { + root: id, + source: Box::new(error), + })?; + deserialise(bytes.as_ref())? + }; + + let meta = Atom::new(meta); + + // The stored representation does not include the `data` field, so we need to load it + // separately from the KV store. + let data = { + let bytes = store.get(meta.key.as_ref()).map_err(|error| { + OperationalError::CommitValueMissing { + key: meta.key.clone(), + source: Box::new(error), + } + })?; + Bytes::from(bytes.as_ref()) + }; + + let left = TreeId::load(left, store)?; + let right = TreeId::load(right, store)?; + + Ok(Self { + meta, + data, + left, + right, + hash: OnceLock::new(), + }) + } +} + impl Foldable for Node where F: Fold, diff --git a/durable-storage/src/avl/resolver.rs b/durable-storage/src/avl/resolver.rs index d005b3ad25..1a6e1e7a55 100644 --- a/durable-storage/src/avl/resolver.rs +++ b/durable-storage/src/avl/resolver.rs @@ -13,7 +13,7 @@ //! demand without changing the underlying storage format. //! //! # Lazy loading strategy -//! `LazyResolver` works with [`LazyId`] wrappers. A `LazyId` keeps a hash (`id`) and/or a loaded +//! `LazyResolver` works with [`LazyId`] wrappers. A `LazyId` keeps a hash and/or a loaded //! value (`inner`). Immutable resolution populates `inner` while keeping the hash available for //! later lookups. Mutable resolution clears the stored hash once the loaded value becomes the //! source of truth. This avoids loading the full tree upfront while preserving stable hash @@ -31,7 +31,6 @@ use std::rc::Rc; use std::sync::Arc; use std::sync::OnceLock; -use octez_riscv_data::components::bytes::Bytes; use octez_riscv_data::foldable::Fold; use octez_riscv_data::foldable::Foldable; use octez_riscv_data::hash::Hash; @@ -39,15 +38,15 @@ use octez_riscv_data::hash::HashFold; use octez_riscv_data::mode::Mode; use octez_riscv_data::mode::Normal; use octez_riscv_data::mode::Prove; -use octez_riscv_data::serialisation::deserialise; use trait_set::trait_set; use super::node::Node; use super::tree::Tree; -use crate::avl::node::NodeHashRepresentation; use crate::errors::OperationalError; -use crate::key::Key; use crate::storage::KeyValueStore; +use crate::storage::Loadable; +use crate::storage::Storable; +use crate::storage::StoreOptions; /// Trait for resolving identifiers to values. pub trait Resolver { @@ -80,6 +79,22 @@ impl Foldable for ArcNodeId { } } +impl Storable for ArcNodeId { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + self.0.store(store, options) + } +} + +impl Loadable for ArcNodeId { + fn load(id: Hash, store: &impl KeyValueStore) -> Result { + Arc::load(id, store).map(Self) + } +} + /// ID for a tree that is always present #[derive(Debug, Clone, derive_more::From, Default)] pub struct ArcTreeId(Tree); @@ -90,6 +105,22 @@ impl Foldable for ArcTreeId { } } +impl Storable for ArcTreeId { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + self.0.store(store, options) + } +} + +impl Loadable for ArcTreeId { + fn load(id: Hash, store: &impl KeyValueStore) -> Result { + Tree::load(id, store).map(Self) + } +} + /// Eager resolver that serves identifiers backed by in-memory [`Arc`] values. /// /// This resolver never touches persistent storage. It is useful for trees already in memory @@ -129,46 +160,62 @@ impl Resolver> for ArcResolver { /// Identifier wrapper used by lazy resolution. /// /// A [`LazyId`] may be in one of three states: -/// - hash-only, where `id` contains a hash and `inner` is empty, -/// - cached, where both `id` and `inner` are populated after immutable resolution, or -/// - owned, where `inner` is populated and `id` has been cleared after mutable resolution or +/// - hash-only, where `hash` contains a hash and `inner` is empty, +/// - cached, where both `hash` and `inner` are populated after immutable resolution, or +/// - owned, where `inner` is populated and `hash` has been cleared after mutable resolution or /// construction from an in-memory value. /// /// This representation lets hashes move through the AVL structure without forcing immediate loads, /// while caching loaded values for subsequent accesses. #[derive(Default, Debug, Clone)] -pub struct LazyId { +pub struct LazyId { inner: OnceLock, - id: Option, + hash: Option, } -impl LazyId { +impl LazyId { /// Create an identifier from an already loaded value. pub fn new(value: Value) -> Self { Self { inner: OnceLock::from(value), - id: None, + hash: None, } } - /// Return the identifier if available. - fn id(&self) -> Option<&Id> { - self.id.as_ref() + /// Return the hash if available. + fn hash(&self) -> Option<&Hash> { + self.hash.as_ref() + } + + /// Populate the inner value from the store. + /// + /// This does not check if the value is already loaded. + fn load(&self, store: &impl KeyValueStore) -> Result<(), OperationalError> + where + Value: Loadable, + { + let hash = self + .hash + .ok_or(OperationalError::ResolverInvariantViolated)?; + let node = Value::load(hash, store)?; + let _ = self.inner.set(node); + + Ok(()) } } -impl From for LazyId { +impl From for LazyId { fn from(hash: Hash) -> Self { Self { inner: OnceLock::new(), - id: Some(hash), + hash: Some(hash), } } } /// Identifier for an AVL node. #[derive(Debug, Clone)] -pub struct LazyNodeId(LazyId>>); +pub struct LazyNodeId(LazyId>>); impl LazyNodeId { /// Wrap this lazy node identifier in a prove-mode identifier. @@ -204,15 +251,37 @@ impl Foldable for LazyNodeId { } self.0 - .id() + .hash() .cloned() .expect("ID should be present when node is absent") } } +impl Storable for LazyNodeId { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + let Some(node) = self.0.inner.get() else { + // There is nothing to store. We assume that the node has been persisted already, as the + // alternative is loading and then storing the node again. + return Ok(()); + }; + + node.store(store, options) + } +} + +impl Loadable for LazyNodeId { + fn load(id: Hash, _store: &impl KeyValueStore) -> Result { + Ok(Self::from(id)) + } +} + /// Identifier for an AVL tree. #[derive(Debug, Clone)] -pub struct LazyTreeId(LazyId>); +pub struct LazyTreeId(LazyId>); impl LazyTreeId { /// Wrap this lazy tree identifier in a prove-mode identifier. @@ -238,7 +307,7 @@ impl Default for LazyTreeId { fn default() -> Self { Self(LazyId { inner: OnceLock::from(Tree::default()), - id: None, + hash: None, }) } } @@ -250,12 +319,34 @@ impl Foldable for LazyTreeId { } self.0 - .id() + .hash() .cloned() .expect("ID should be present when tree is absent") } } +impl Storable for LazyTreeId { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + let Some(tree) = self.0.inner.get() else { + // There is nothing to store. We assume that the tree has been persisted already, as the + // alternative is loading and then storing the tree again. + return Ok(()); + }; + + tree.store(store, options) + } +} + +impl Loadable for LazyTreeId { + fn load(id: Hash, _store: &impl KeyValueStore) -> Result { + Ok(Self::from(id)) + } +} + /// Resolver that lazily loads AVL nodes and trees from a [`KeyValueStore`]. /// /// In contrast to [`ArcResolver`], this resolver can start from hash-only identifiers and defer @@ -276,39 +367,6 @@ impl LazyResolver { } } -impl LazyResolver { - /// Load and decode a node by its content hash. - /// - /// This performs a `blob_get` lookup and deserialises the returned bytes into a node representation. - fn load_node(&self, hash: Hash) -> Result>, OperationalError> { - let bytes = self - .persistence_layer - .blob_get(hash) - .map_err(|error| error.into_resolver_op_error(hash))?; - let noderepr = - deserialise::, Key, Hash>>(bytes.as_ref())?; - Ok(Arc::new(Node::from(noderepr))) - } - - /// Load and decode a tree root reference by its content hash. - /// - /// The serialised payload is expected to be an optional root node hash, which is then wrapped - /// in a lazy node identifier. The canonical empty-tree hash resolves directly to default - /// [`Tree`] without hitting storage. - fn load_tree(&self, hash: Hash) -> Result, OperationalError> { - if hash == crate::merkle_layer::empty_tree_hash() { - return Ok(Tree::default()); - } - - let bytes = self - .persistence_layer - .blob_get(hash) - .map_err(|error| error.into_resolver_op_error(hash))?; - let tree_repr = deserialise::>(bytes.as_ref())?.map(LazyNodeId::from); - Ok(Tree::from(tree_repr)) - } -} - impl Resolver> for LazyResolver { fn resolve<'a>( &self, @@ -317,11 +375,9 @@ impl Resolver> for LazyR if let Some(value) = id.0.inner.get() { return Ok(value); } - let &hash = - id.0.id() - .ok_or(OperationalError::ResolverInvariantViolated)?; - let node = self.load_node(hash)?; - let _ = id.0.inner.set(node); + + id.0.load(self.persistence_layer.as_ref())?; + Ok(id.0.inner.wait().as_ref()) } @@ -340,12 +396,9 @@ impl Resolver> for LazyR return Ok(Arc::make_mut(unsafe { &mut *temp })); }; - let hash = - id.0.id() - .ok_or(OperationalError::ResolverInvariantViolated)?; - let _ = id.0.inner.set(self.load_node(*hash)?); + id.0.load(self.persistence_layer.as_ref())?; - id.0.id = None; + id.0.hash = None; id.0.inner .get_mut() .ok_or(OperationalError::ResolverInvariantViolated) @@ -358,11 +411,9 @@ impl Resolver> for LazyResolver< if let Some(value) = id.0.inner.get() { return Ok(value); } - let &hash = - id.0.id() - .ok_or(OperationalError::ResolverInvariantViolated)?; - let tree = self.load_tree(hash)?; - let _ = id.0.inner.set(tree); + + id.0.load(self.persistence_layer.as_ref())?; + Ok(id.0.inner.wait()) } @@ -371,13 +422,10 @@ impl Resolver> for LazyResolver< id: &'a mut LazyTreeId, ) -> Result<&'a mut Tree, OperationalError> { if id.0.inner.get().is_none() { - let hash = - id.0.id() - .ok_or(OperationalError::ResolverInvariantViolated)?; - let _ = id.0.inner.set(self.load_tree(*hash)?); + id.0.load(self.persistence_layer.as_ref())?; } - id.0.id = None; + id.0.hash = None; id.0.inner .get_mut() .ok_or(OperationalError::ResolverInvariantViolated) @@ -522,11 +570,8 @@ mod tests { use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; - use octez_riscv_data::foldable::Foldable; use octez_riscv_data::hash::Hash; - use octez_riscv_data::hash::HashFold; use octez_riscv_data::mode::Normal; - use octez_riscv_data::serialisation; use super::ArcNodeId; use super::ArcResolver; @@ -540,10 +585,11 @@ mod tests { use crate::avl::resolver::AvlResolver; use crate::avl::tree::Tree; use crate::errors::Error; - use crate::errors::InvalidArgumentError; use crate::errors::OperationalError; use crate::key::Key; use crate::storage::KeyValueStore; + use crate::storage::Storable; + use crate::storage::StoreOptions; use crate::storage::in_memory::InMemoryKeyValueStore; use crate::storage::in_memory::InMemoryRepo; @@ -622,20 +668,15 @@ mod tests { resolver: &Res, persistence_layer: &KV, ) where - NodeId: Foldable, - TreeId: Foldable, + NodeId: Storable, + TreeId: Storable, KV: KeyValueStore, Res: AvlResolver, { - // LazyTreeId resolves by loading a serialised optional root hash. - let tree_hash = tree.hash(); - let tree_repr: Option = tree.root().map(Hash::from_foldable); - let tree_bytes = - serialisation::serialise(tree_repr).expect("tree serialisation should succeed"); + let store_options = StoreOptions::default().with_shallow().with_node_data(); - persistence_layer - .blob_set(tree_hash, tree_bytes) - .expect("persisting trees should succeed"); + tree.store(persistence_layer, &store_options) + .expect("persisting tree should succeed"); let Some(node_id) = tree.root() else { return; @@ -645,14 +686,8 @@ mod tests { .resolve(node_id) .expect("resolving nodes should succeed"); - let node_hash = Hash::from_foldable(node_id); - let node_repr = node.to_encode(); - let node_bytes = - serialisation::serialise(node_repr).expect("node serialisation should succeed"); - - persistence_layer - .blob_set(node_hash, node_bytes) - .expect("persisting nodes should succeed"); + node.store(persistence_layer, &store_options) + .expect("persisting node should succeed"); persist_tree( node.left_ref(resolver) @@ -732,7 +767,7 @@ mod tests { let node_without_hash = LazyNodeId(LazyId { inner: OnceLock::new(), - id: None, + hash: None, }); let mut node_without_hash_mut = node_without_hash.clone(); @@ -747,7 +782,7 @@ mod tests { let tree_without_hash = LazyTreeId(LazyId { inner: OnceLock::new(), - id: None, + hash: None, }); let mut tree_without_hash_mut = tree_without_hash.clone(); @@ -762,7 +797,7 @@ mod tests { } #[test] - fn lazy_resolver_maps_missing_cas_entries_to_lookup_error() { + fn lazy_resolver_maps_missing_cas_entries_to_commit_data_missing() { let missing_hash = Hash::hash_bytes(b"missing"); let persistence_layer = Arc::new(InMemoryKeyValueStore::default()); let lazy_resolver = LazyResolver::new(persistence_layer); @@ -770,19 +805,13 @@ mod tests { let node_id = LazyNodeId::from(missing_hash); assert!(matches!( lazy_resolver.resolve(&node_id), - Err(OperationalError::ResolverCasLookup { - hash, - error: InvalidArgumentError::KeyNotFound - }) if hash == missing_hash + Err(OperationalError::CommitDataMissing { root, .. }) if root == missing_hash )); let tree_id = LazyTreeId::from(missing_hash); assert!(matches!( lazy_resolver.resolve(&tree_id), - Err(OperationalError::ResolverCasLookup { - hash, - error: InvalidArgumentError::KeyNotFound - }) if hash == missing_hash + Err(OperationalError::CommitDataMissing { root, .. }) if root == missing_hash )); } diff --git a/durable-storage/src/avl/tree.rs b/durable-storage/src/avl/tree.rs index 1b6d84ce48..a11eb1f7eb 100644 --- a/durable-storage/src/avl/tree.rs +++ b/durable-storage/src/avl/tree.rs @@ -5,12 +5,11 @@ //! Interface for an optional root [`Node`] of a Merklisable AVL tree use std::cmp::Ordering; +use std::sync::LazyLock; use bincode::Decode; use bincode::de::Decoder; use bincode::error::DecodeError; -#[cfg(test)] -use octez_riscv_data::components::atom::Atom; use octez_riscv_data::components::atom::AtomMode; use octez_riscv_data::components::bytes::Bytes; use octez_riscv_data::components::bytes::BytesMode; @@ -19,22 +18,23 @@ use octez_riscv_data::foldable::Foldable; use octez_riscv_data::foldable::NodeFold; use octez_riscv_data::hash::Hash; use octez_riscv_data::hash::HashFold; -use octez_riscv_data::mode::Mode; +use octez_riscv_data::serialisation::deserialise; +use octez_riscv_data::serialisation::serialise; use perfect_derive::perfect_derive; use super::node::Node; use super::resolver::ProveNodeId; -#[cfg(test)] -use crate::avl::node::Meta; use crate::avl::resolver::AvlResolver; use crate::avl::resolver::LazyNodeId; use crate::avl::resolver::NodeResolver; use crate::errors::Error; use crate::errors::InvalidArgumentError; use crate::errors::OperationalError; -#[cfg(test)] -use crate::key::KEY_MAX_SIZE; use crate::key::Key; +use crate::storage::KeyValueStore; +use crate::storage::Loadable; +use crate::storage::Storable; +use crate::storage::StoreOptions; /// A key-value store tree with left and right nodes that supports traversal and value retrieval. #[perfect_derive(Clone, Default, Debug)] @@ -150,26 +150,6 @@ impl Tree { Hash::from_foldable(self) } - /// Creates an in-order iterator for the [`Node`]s in the [`Tree`]. - /// - /// Each call to [`Iterator::next`] first descends as far left as possible from the current - /// subtree, pushing the visited node ids onto an explicit stack. Once it reaches an empty - /// subtree, it pops the next node from the stack, yields that node, and continues from that - /// node's right subtree on the next call. - /// - /// The iterator yields an error if resolving any intermediate node or subtree fails. - pub(crate) fn iter<'tree, 'res, TreeId, M: Mode, Res: AvlResolver>( - &'tree self, - resolver: &'res Res, - ) -> TreeIterator<'tree, 'res, NodeId, TreeId, M, Res> { - TreeIterator { - stack: vec![], - current: self, - resolver, - _marker: std::marker::PhantomData, - } - } - /// Take the root [`Node`] out of this tree, leaving the [`Tree`] empty. pub(crate) const fn take(&mut self) -> Option { self.0.take() @@ -355,187 +335,60 @@ impl> Foldable for Tree { } } -/// Used for iterating through the nodes of the [`Tree`] in-order (left-to-right). -/// -/// `current` tracks the subtree that still needs to be explored, while `stack` stores the path of -/// ancestor node ids whose left subtrees have already been explored. This lets the iterator do an -/// in-order traversal without recursion. -/// -/// Resolution failures are surfaced as iterator items of type `Err`. -pub(crate) struct TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> { - stack: Vec<&'tree NodeId>, - current: &'tree Tree, - resolver: &'res Resolver, - _marker: std::marker::PhantomData (TreeId, M)>, -} - -impl<'tree, 'res, NodeId, TreeId: 'tree, M: Mode + 'tree, Resolver: AvlResolver> - Iterator for TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> -{ - type Item = Result<&'tree Node, OperationalError>; +impl Storable for Tree { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + let repr = self.0.as_ref().map(Hash::from_foldable); - fn next(&mut self) -> Option { - if let Some(root_id) = self.current.root() { - if let Err(err) = self.advance_to_leftmost_in_subtree(root_id) { - return Some(Err(err)); - } + // We don't store empty trees. All leaf nodes contain two empty trees. Adding two more + // redundant writes to all leaves is not desirable. + // The empty tree can be recovered during loading, as the hash of the empty tree is known. + if repr.is_none() { + return Ok(()); } - match self.pop_and_prepare_right_subtree() { - Ok(Some(node)) => Some(Ok(node)), - Ok(None) => None, - Err(err) => Some(Err(err)), - } - } -} + let id = self.hash(); + let bytes = serialise(repr)?; + store.blob_set(id, bytes)?; -impl<'tree, 'res, NodeId, TreeId: 'tree, M: Mode + 'tree, Resolver: AvlResolver> - TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> -{ - /// Helper to descend to the leftmost node in the current subtree, pushing nodes onto the stack. - fn advance_to_leftmost_in_subtree( - &mut self, - mut node_id: &'tree NodeId, - ) -> Result<(), OperationalError> { - loop { - self.stack.push(node_id); - let resolved_node = self.resolver.resolve(node_id)?; - let left = resolved_node.left_ref(self.resolver)?; - match left.root() { - Some(left_id) => node_id = left_id, - None => { - self.current = left; - break; - } - } + if let Some(node) = &self.0 + && options.deep() + { + node.store(store, options)?; } - Ok(()) - } - - /// Helper to pop the next node from the stack and prepare to traverse its right subtree. - fn pop_and_prepare_right_subtree( - &mut self, - ) -> Result>, OperationalError> { - let node_id = match self.stack.pop() { - Some(id) => id, - None => return Ok(None), - }; - let resolved_node = self.resolver.resolve(node_id)?; - let right = resolved_node.right_ref(self.resolver)?; - self.current = right; - Ok(Some(resolved_node)) - } -} -#[cfg(test)] -impl Tree { - #[inline] - /// The data stored in a [`Node`] in the [`Tree`] with a given [`Key`]. - pub fn get<'a, TreeId: 'a, M: BytesMode + AtomMode>( - &'a self, - key: &Key, - resolver: &impl AvlResolver, - ) -> Result>, OperationalError> { - let Some(node) = self.root() else { - return Ok(None); - }; - Node::get(node, key, resolver) - } - - /// Asserts that the [`Tree`] is a valid AVL tree - pub(crate) fn check( - &self, - resolver: &impl AvlResolver, - ) -> Result<(), OperationalError> - where - NodeId: std::fmt::Debug, - TreeId: std::fmt::Debug, - Bytes: std::fmt::Debug, - Atom: std::fmt::Debug, - { - let inorder = self.is_inorder(resolver)?; - let is_balanced = self.is_balanced(resolver)?; - let has_correct_balance_factors = self.has_correct_balance_factors(resolver)?; - if !inorder || !is_balanced || !has_correct_balance_factors { - eprintln!("{self:?}"); - } - assert!(inorder, "AVL tree isn't in order"); - assert!(is_balanced, "AVL tree isn't balanced"); - assert!( - has_correct_balance_factors, - "AVL tree balance factors are incorrect" - ); Ok(()) } +} - /// Returns true if the [`Tree`] is in-order. - pub(crate) fn is_inorder( - &self, - resolver: &impl AvlResolver, - ) -> Result { - self.is_inorder_inner( - &Key::new(&[u8::MIN]).expect("Size less than KEY_MAX_SIZE"), - &Key::new(&[u8::MAX; KEY_MAX_SIZE]).expect("Size less than KEY_MAX_SIZE"), - resolver, - ) - } +impl Loadable for Tree { + fn load(id: Hash, store: &impl KeyValueStore) -> Result { + static EMPTY_HASH: LazyLock = + LazyLock::new(|| Hash::from_foldable(&Tree::(None))); - /// Returns true if the balance factors stored in any [`Node`]'s subtree are correct. - pub(super) fn has_correct_balance_factors( - &self, - resolver: &impl AvlResolver, - ) -> Result - where - NodeId: std::fmt::Debug, - TreeId: std::fmt::Debug, - Bytes: std::fmt::Debug, - Atom: std::fmt::Debug, - { - match self.root() { - None => Ok(true), - Some(node) => resolver - .resolve(node) - .map(|res| res.has_correct_balance_factors(resolver))?, + // Empty trees are not stored. We can short-circuit here, if we detect the requested hash + // corresponds to the hash of the empty tree. + if id == *EMPTY_HASH { + return Ok(Self(None)); } - } - /// Returns the height of the [`Tree`]. - pub(super) fn height( - &self, - resolver: &impl AvlResolver, - ) -> Result { - match self.root() { - None => Ok(0), - Some(node) => resolver.resolve(node).map(|res| res.height(resolver))?, - } - } - - /// Returns true if the [`Tree`] is balanced. - pub(super) fn is_balanced( - &self, - resolver: &impl AvlResolver, - ) -> Result { - match self.root() { - None => Ok(true), - Some(node) => resolver - .resolve(node) - .map(|res| res.is_balanced(resolver))?, - } - } + let repr: Option = { + let bytes = + store + .blob_get(id) + .map_err(|error| OperationalError::CommitDataMissing { + root: id, + source: Box::new(error), + })?; + deserialise(bytes.as_ref())? + }; - /// Returns true if the [`Tree`] is in-order and all values lie between the `min` and `max`. - pub(super) fn is_inorder_inner( - &self, - min: &Key, - max: &Key, - resolver: &impl AvlResolver, - ) -> Result { - match self.root() { - None => Ok(true), - Some(node) => resolver - .resolve(node) - .map(|res| res.is_inorder(min, max, resolver))?, - } + repr.map(|node_id| NodeId::load(node_id, store)) + .transpose() + .map(Self) } } @@ -545,11 +398,13 @@ mod tests { use std::io::prelude::*; use goldenfile::Mint; + use octez_riscv_data::components::atom::Atom; use octez_riscv_data::mode::Normal; use proptest::prelude::*; use proptest::test_runner::TestCaseError; use super::*; + use crate::avl::node::Meta; use crate::avl::resolver::ArcNodeId; use crate::avl::resolver::ArcResolver; use crate::avl::resolver::ArcTreeId; @@ -557,6 +412,227 @@ mod tests { use crate::key::KEY_MAX_SIZE; use crate::key::Key; + impl Tree { + #[inline] + /// The data stored in a [`Node`] in the [`Tree`] with a given [`Key`]. + pub fn get<'a, TreeId: 'a, M: BytesMode + AtomMode>( + &'a self, + key: &Key, + resolver: &impl AvlResolver, + ) -> Result>, OperationalError> { + let Some(node) = self.root() else { + return Ok(None); + }; + Node::get(node, key, resolver) + } + + /// Asserts that the [`Tree`] is a valid AVL tree + pub(crate) fn check( + &self, + resolver: &impl AvlResolver, + ) -> Result<(), OperationalError> + where + NodeId: std::fmt::Debug, + TreeId: std::fmt::Debug, + Bytes: std::fmt::Debug, + Atom: std::fmt::Debug, + { + let inorder = self.is_inorder(resolver)?; + let is_balanced = self.is_balanced(resolver)?; + let has_correct_balance_factors = self.has_correct_balance_factors(resolver)?; + if !inorder || !is_balanced || !has_correct_balance_factors { + eprintln!("{self:?}"); + } + assert!(inorder, "AVL tree isn't in order"); + assert!(is_balanced, "AVL tree isn't balanced"); + assert!( + has_correct_balance_factors, + "AVL tree balance factors are incorrect" + ); + Ok(()) + } + + /// Returns true if the [`Tree`] is in-order. + pub(crate) fn is_inorder( + &self, + resolver: &impl AvlResolver, + ) -> Result { + self.is_inorder_inner( + &Key::new(&[u8::MIN]).expect("Size less than KEY_MAX_SIZE"), + &Key::new(&[u8::MAX; KEY_MAX_SIZE]).expect("Size less than KEY_MAX_SIZE"), + resolver, + ) + } + + /// Returns true if the balance factors stored in any [`Node`]'s subtree are correct. + pub(crate) fn has_correct_balance_factors( + &self, + resolver: &impl AvlResolver, + ) -> Result + where + NodeId: std::fmt::Debug, + TreeId: std::fmt::Debug, + Bytes: std::fmt::Debug, + Atom: std::fmt::Debug, + { + match self.root() { + None => Ok(true), + Some(node) => resolver + .resolve(node) + .map(|res| res.has_correct_balance_factors(resolver))?, + } + } + + /// Returns the height of the [`Tree`]. + pub(crate) fn height( + &self, + resolver: &impl AvlResolver, + ) -> Result { + match self.root() { + None => Ok(0), + Some(node) => resolver.resolve(node).map(|res| res.height(resolver))?, + } + } + + /// Returns true if the [`Tree`] is balanced. + pub(crate) fn is_balanced( + &self, + resolver: &impl AvlResolver, + ) -> Result { + match self.root() { + None => Ok(true), + Some(node) => resolver + .resolve(node) + .map(|res| res.is_balanced(resolver))?, + } + } + + /// Returns true if the [`Tree`] is in-order and all values lie between the `min` and `max`. + pub(crate) fn is_inorder_inner( + &self, + min: &Key, + max: &Key, + resolver: &impl AvlResolver, + ) -> Result { + match self.root() { + None => Ok(true), + Some(node) => resolver + .resolve(node) + .map(|res| res.is_inorder(min, max, resolver))?, + } + } + + /// Creates an in-order iterator for the [`Node`]s in the [`Tree`]. + /// + /// Each call to [`Iterator::next`] first descends as far left as possible from the current + /// subtree, pushing the visited node ids onto an explicit stack. Once it reaches an empty + /// subtree, it pops the next node from the stack, yields that node, and continues from that + /// node's right subtree on the next call. + /// + /// The iterator yields an error if resolving any intermediate node or subtree fails. + pub(crate) fn iter< + 'tree, + 'res, + TreeId, + M: octez_riscv_data::mode::Mode, + Res: AvlResolver, + >( + &'tree self, + resolver: &'res Res, + ) -> TreeIterator<'tree, 'res, NodeId, TreeId, M, Res> { + TreeIterator { + stack: vec![], + current: self, + resolver, + _marker: std::marker::PhantomData, + } + } + } + + /// Used for iterating through the nodes of the [`Tree`] in-order (left-to-right). + /// + /// `current` tracks the subtree that still needs to be explored, while `stack` stores the path of + /// ancestor node ids whose left subtrees have already been explored. This lets the iterator do an + /// in-order traversal without recursion. + /// + /// Resolution failures are surfaced as iterator items of type `Err`. + pub(crate) struct TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> { + stack: Vec<&'tree NodeId>, + current: &'tree Tree, + resolver: &'res Resolver, + _marker: std::marker::PhantomData (TreeId, M)>, + } + + impl< + 'tree, + 'res, + NodeId, + TreeId: 'tree, + M: octez_riscv_data::mode::Mode + 'tree, + Resolver: AvlResolver, + > TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> + { + /// Helper to descend to the leftmost node in the current subtree, pushing nodes onto the stack. + fn advance_to_leftmost_in_subtree( + &mut self, + mut node_id: &'tree NodeId, + ) -> Result<(), OperationalError> { + loop { + self.stack.push(node_id); + let resolved_node = self.resolver.resolve(node_id)?; + let left = resolved_node.left_ref(self.resolver)?; + match left.root() { + Some(left_id) => node_id = left_id, + None => { + self.current = left; + break; + } + } + } + Ok(()) + } + + /// Helper to pop the next node from the stack and prepare to traverse its right subtree. + fn pop_and_prepare_right_subtree( + &mut self, + ) -> Result>, OperationalError> { + let node_id = match self.stack.pop() { + Some(id) => id, + None => return Ok(None), + }; + let resolved_node = self.resolver.resolve(node_id)?; + let right = resolved_node.right_ref(self.resolver)?; + self.current = right; + Ok(Some(resolved_node)) + } + } + + impl< + 'tree, + 'res, + NodeId, + TreeId: 'tree, + M: octez_riscv_data::mode::Mode + 'tree, + Resolver: AvlResolver, + > Iterator for TreeIterator<'tree, 'res, NodeId, TreeId, M, Resolver> + { + type Item = Result<&'tree Node, OperationalError>; + + fn next(&mut self) -> Option { + if let Some(root_id) = self.current.root() { + if let Err(err) = self.advance_to_leftmost_in_subtree(root_id) { + return Some(Err(err)); + } + } + + match self.pop_and_prepare_right_subtree() { + Ok(Some(node)) => Some(Ok(node)), + Ok(None) => None, + Err(err) => Some(Err(err)), + } + } + } + const GOLDEN_DIR: &str = "tests/expected"; #[derive(Debug, Clone)] diff --git a/durable-storage/src/database.rs b/durable-storage/src/database.rs index 8f88ab6b8a..79ed74a61b 100644 --- a/durable-storage/src/database.rs +++ b/durable-storage/src/database.rs @@ -32,6 +32,7 @@ use crate::merkle_worker::MerkleWorker; pub use crate::repo::DirectoryManager; use crate::storage::KeyValueStore; use crate::storage::PersistentKeyValueStore; +use crate::storage::StoreOptions; /// An isolated key-space, independent from other [`Database`]s, on which database operations can /// be performed, e.g. read, write, delete. @@ -111,7 +112,8 @@ impl Database { where KV: PersistentKeyValueStore, { - let commit_id = self.inner.merkle.commit()?; + let commit_options = StoreOptions::default().with_deep().without_node_data(); + let commit_id = self.inner.merkle.commit(commit_options)?; self.inner.persistent.commit(repo, &commit_id)?; Ok(commit_id) @@ -603,7 +605,7 @@ mod tests { assert!(matches!( Database::::checkout(handle, &repo, commit_id), - Err(Error::Operational(OperationalError::CommitDataMissing { root })) + Err(Error::Operational(OperationalError::CommitDataMissing { root, .. })) if root == *commit_id.as_hash() )); } diff --git a/durable-storage/src/errors.rs b/durable-storage/src/errors.rs index 27ffcc2f2a..fdac6c6782 100644 --- a/durable-storage/src/errors.rs +++ b/durable-storage/src/errors.rs @@ -8,6 +8,8 @@ use std::path::PathBuf; use octez_riscv_data::hash::Hash; +use crate::key::Key; + /// Errors that are the result of an operational failure /// /// These kinds of errors are fatal. When encountering such an error, there is no guarantee that the @@ -17,8 +19,11 @@ pub enum OperationalError { #[error("Unable to locate commitment on disk")] CommitNotFound, - #[error("Commit metadata is missing for root hash {root:?}")] - CommitDataMissing { root: Hash }, + #[error("Commit data is missing for a node or tree with hash {root:?}")] + CommitDataMissing { root: Hash, source: Box }, + + #[error("Commit is missing data for the value of key {key:?}")] + CommitValueMissing { key: Key, source: Box }, #[cfg(feature = "rocksdb")] #[error("Unable to create checkpoint: {error}")] @@ -108,19 +113,6 @@ pub enum OperationalError { #[error("Resolver invariant violated. Either the hash or the value of an ID must exist.")] ResolverInvariantViolated, - /// A content-addressed-storage (`blob_get`) lookup returned an invalid-argument error while - /// resolving a known hash. - /// - /// Resolver lookups are performed with a concrete hash that was already accepted by resolver - /// logic. Receiving an [`InvalidArgumentError`] at that point signals an unexpected storage - /// contract violation, so it is surfaced as an operational failure with the failing hash. - #[error("Resolver CAS lookup returned invalid argument for hash {hash:?}: {error}")] - ResolverCasLookup { - hash: Hash, - #[source] - error: InvalidArgumentError, - }, - #[error("Encountered a poisoned lock")] LockPoisoned, @@ -162,13 +154,3 @@ pub enum Error { #[error("Invalid argument error: {0}")] InvalidArgument(#[from] InvalidArgumentError), } - -impl Error { - pub(super) fn into_resolver_op_error(self, hash: Hash) -> OperationalError { - match self { - // See [`OperationalError::ResolverCasLookup`] - Error::InvalidArgument(error) => OperationalError::ResolverCasLookup { hash, error }, - Error::Operational(error) => error, - } - } -} diff --git a/durable-storage/src/merkle_layer.rs b/durable-storage/src/merkle_layer.rs index 90550fed7d..49d20b5225 100644 --- a/durable-storage/src/merkle_layer.rs +++ b/durable-storage/src/merkle_layer.rs @@ -18,14 +18,11 @@ use std::convert::Infallible; use std::marker::PhantomData; use std::sync::Arc; -use std::sync::OnceLock; use octez_riscv_data::hash::Hash; use octez_riscv_data::mode::Modal; use octez_riscv_data::mode::Mode; use octez_riscv_data::mode::Normal; -use octez_riscv_data::serialisation::deserialise; -use octez_riscv_data::serialisation::serialise; use perfect_derive::perfect_derive; use crate::avl::resolver::LazyNodeId; @@ -33,16 +30,13 @@ use crate::avl::resolver::LazyResolver; use crate::avl::tree::Tree; use crate::commit::CommitId; use crate::errors::Error; -use crate::errors::InvalidArgumentError; use crate::errors::OperationalError; use crate::key::Key; use crate::storage::KeyValueStore; +use crate::storage::Loadable; use crate::storage::PersistentKeyValueStore; - -pub(crate) fn empty_tree_hash() -> Hash { - static EMPTY_TREE_HASH: OnceLock = OnceLock::new(); - *EMPTY_TREE_HASH.get_or_init(|| Tree::from(None::).hash()) -} +use crate::storage::Storable; +use crate::storage::StoreOptions; /// A layer for transforming data into a Merkle-ised representation before commitment to a /// [`PersistentKeyValueStore`]. @@ -73,11 +67,11 @@ impl MerkleLayer { } /// Generates a commitment for the [MerkleLayer]. - pub fn commit(&mut self) -> Result + pub fn commit(&mut self, options: &StoreOptions) -> Result where KV: PersistentKeyValueStore, { - self.inner.commit() + self.inner.commit(options) } } @@ -269,22 +263,7 @@ impl NormalImpl { KV: KeyValueStore, { let resolver = LazyResolver::new(persistence.clone()); - let tree = if *root.as_hash() == empty_tree_hash() { - Tree::default() - } else { - let tree_data = persistence - .blob_get(*root.as_hash()) - .map_err(|error| match error { - Error::InvalidArgument(InvalidArgumentError::KeyNotFound) => { - OperationalError::CommitDataMissing { - root: *root.as_hash(), - } - .into() - } - other => other, - })?; - deserialise(tree_data.as_ref()).map_err(OperationalError::from)? - }; + let tree = Tree::load(*root.as_hash(), persistence.as_ref())?; Ok(Self { tree, @@ -294,25 +273,11 @@ impl NormalImpl { } /// Generates a commitment for the [MerkleLayer]. - fn commit(&mut self) -> Result + fn commit(&mut self, options: &StoreOptions) -> Result where KV: PersistentKeyValueStore, { - for node in self.tree.iter(&self.resolver) { - let node = node?; - - let node_hash = node.hash(); - let node_bytes = - serialise(node.to_encode()).expect("Serialisation of node data should not fail"); - - self.persistence.blob_set(node_hash, node_bytes)?; - - let tree_hash = Tree::from(Some(node_hash)).hash(); - let tree_bytes = serialise(Some(node_hash))?; - - self.persistence.blob_set(tree_hash, tree_bytes)?; - } - + self.tree.store(self.persistence.as_ref(), options)?; Ok(CommitId::from(self.hash())) } } @@ -1263,7 +1228,8 @@ mod tests { } let expected_hash = merkle_layer.hash().expect("Hash operation should succeed"); - let commit_id = merkle_layer.commit().expect("Commit operation should succeed"); + let commit_opts = crate::storage::StoreOptions::default().with_deep().with_node_data(); + let commit_id = merkle_layer.commit(&commit_opts).expect("Commit operation should succeed"); let mut lazy_loaded = MerkleLayer::checkout(persistence, commit_id) .expect("Lazy checkout should succeed"); @@ -1300,6 +1266,10 @@ mod tests { #[cfg(feature = "rocksdb")] #[test] fn test_merkle_layer_commit_persists_nodes() { + use crate::storage::Loadable; + use crate::storage::Storable; + use crate::storage::StoreOptions; + let (_keepalive, repo) = setup_repo(); let mut merkle_layer = new_merkle_layer(repo); @@ -1329,23 +1299,29 @@ mod tests { .expect("setting node should succeed"); } + let commit_opts = StoreOptions::default().with_deep().with_node_data(); let commit_id = merkle_layer - .commit() + .commit(&commit_opts) .expect("The commit operation should not fail"); for node in merkle_layer.inner.tree.iter(&merkle_layer.inner.resolver) { let node: &Node = node.expect("The node should be retrieved successfully"); - let node_repr = node.to_encode(); - let node_bytes = octez_riscv_data::serialisation::serialise(node_repr) - .expect("We should be able to serialise the node"); - let node_hash = *node.hash(); - let blob = merkle_layer - .inner - .persistence - .blob_get(node_hash) - .expect("The blob with the given key should be present"); - assert_eq!(node_bytes, blob.as_ref()); + + node.store( + merkle_layer.inner.persistence.as_ref(), + &StoreOptions::default().with_shallow().with_node_data(), + ) + .expect("Storing node should succeed"); + + let loaded_node: Node = + Node::load(*node.hash(), merkle_layer.inner.persistence.as_ref()) + .expect("Loading node should succeed"); + + assert_eq!(node.hash(), loaded_node.hash()); + assert_eq!(node.balance_factor(), loaded_node.balance_factor()); + assert_eq!(node.key(), loaded_node.key()); + assert_eq!(node.value(), loaded_node.value()); } let root_hash = merkle_layer.inner.tree.hash(); diff --git a/durable-storage/src/merkle_worker.rs b/durable-storage/src/merkle_worker.rs index db628f4fb2..8d70b1b212 100644 --- a/durable-storage/src/merkle_worker.rs +++ b/durable-storage/src/merkle_worker.rs @@ -25,6 +25,7 @@ use crate::key::Key; use crate::merkle_layer::MerkleLayer; use crate::storage::KeyValueStore; use crate::storage::PersistentKeyValueStore; +use crate::storage::StoreOptions; trait_set! { /// [`KeyValueStore`] that can be used in a background thread @@ -130,14 +131,16 @@ impl Command { } /// Construct a command that performs a [`MerkleLayer::commit`]. - fn new_commit() -> (impl FnOnce() -> Result, Self) + fn new_commit( + options: StoreOptions, + ) -> (impl FnOnce() -> Result, Self) where KV: PersistentKeyValueStore, { let (sender, receiver) = oneshot::channel(); let this = Self(Box::new(move |layer: &mut MerkleLayer| { - let result = layer.commit(); + let result = layer.commit(&options); let _ = sender.send(result); })); @@ -280,11 +283,11 @@ impl MerkleWorker { } /// See [`MerkleLayer::commit`]. - pub(crate) fn commit(&self) -> Result + pub(crate) fn commit(&self, options: StoreOptions) -> Result where KV: PersistentKeyValueStore, { - let (receive, command) = Command::new_commit(); + let (receive, command) = Command::new_commit(options); self.sender .send(command) .map_err(|_| OperationalError::WorkerThreadDied)?; @@ -379,8 +382,11 @@ mod tests { #[cfg(feature = "rocksdb")] Self::Commit => { - let commit1 = worker.commit().expect("Commit should succeed"); - let commit2 = layer.commit().expect("Commit should succeed"); + let options = crate::storage::StoreOptions::default() + .with_deep() + .with_node_data(); + let commit2 = layer.commit(&options).expect("Commit should succeed"); + let commit1 = worker.commit(options).expect("Commit should succeed"); assert_eq!(commit1, commit2); } diff --git a/durable-storage/src/storage.rs b/durable-storage/src/storage.rs index 9f9fc5786a..74ba4a0c48 100644 --- a/durable-storage/src/storage.rs +++ b/durable-storage/src/storage.rs @@ -7,7 +7,11 @@ pub mod in_memory; use std::path::Path; +use std::sync::Arc; +use octez_riscv_data::foldable::Foldable; +use octez_riscv_data::hash::Hash; +use octez_riscv_data::hash::HashFold; use tempfile::TempDir; use crate::commit::CommitId; @@ -143,3 +147,107 @@ cfg_if::cfg_if! { } } } + +/// Options for storing MAVL values in a [`KeyValueStore`] +#[derive(Debug, Clone)] +pub struct StoreOptions { + /// Persist the key-value pairs from MAVL nodes + node_data: bool, + + /// Persist nested items + deep: bool, +} + +impl StoreOptions { + /// When this is set, recursive child values will not be persisted. + pub fn with_shallow(self) -> Self { + Self { + node_data: self.node_data, + deep: false, + } + } + + /// Also stores nested nodes and trees. + pub fn with_deep(self) -> Self { + Self { + node_data: self.node_data, + deep: true, + } + } + + /// Persists the key-value data of nodes. + /// + /// Turning this option on ensures the nodes are persisted completely. When using the + /// [`crate::merkle_layer::MerkleLayer`] in isolation, this is necessary as there is no other + /// component that will be writing the key-value data to the store. + pub fn with_node_data(self) -> Self { + Self { + node_data: true, + deep: self.deep, + } + } + + /// Do not persist key-value data of nodes. + /// + /// This lets you avoid writing key-value data to the store when another component does this + /// already. This is, for example, the case in the [`crate::database::Database`] component which + /// mutates the store directly ahead of commitments. At commitment time, you only need to + /// persist the remaining tree and node structures. + pub fn without_node_data(self) -> Self { + Self { + node_data: false, + deep: self.deep, + } + } + + /// Returns whether node key-value data should be persisted. + pub fn node_data(&self) -> bool { + self.node_data + } + + /// Returns whether nested values should be persisted recursively. + pub fn deep(&self) -> bool { + self.deep + } +} + +impl Default for StoreOptions { + fn default() -> Self { + Self { + node_data: false, + deep: true, + } + } +} + +/// This trait marks values that can be persisted into a [`KeyValueStore`]. +pub trait Storable: Foldable { + /// Persist this value into `store` according to `options`. + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError>; +} + +impl Storable for Arc { + fn store( + &self, + store: &impl KeyValueStore, + options: &StoreOptions, + ) -> Result<(), OperationalError> { + T::store(self, store, options) + } +} + +/// This trait marks values that can be reconstructed from a [`KeyValueStore`] by content hash. +pub trait Loadable: Sized { + /// Load a value identified by `id` from `store`. + fn load(id: Hash, store: &impl KeyValueStore) -> Result; +} + +impl Loadable for Arc { + fn load(id: Hash, store: &impl KeyValueStore) -> Result { + T::load(id, store).map(Arc::new) + } +}