Skip to content

Commit 424ee30

Browse files
committed
feat: build trie from Proofs
Key proofs are a sequence of trie nodes that follow a linear path through the trie. This means we can reconstruct a narrow view of the trie over that linear path. In this narrow view, each node can refer to another full node or just its hash. This resulting trie can (and will in an upcoming change) be hashed to generate the same root hash. While merging with the key-value tries from #1363 and #1365, we can iteratively verify the hash of each layer and detect early if any node is incomplete. The `Remote` edges can also point outside of the key range. We can use these remote edges to identify holes in our overall trie and continue synchronizing down those paths.
1 parent 2446fa7 commit 424ee30

File tree

6 files changed

+320
-13
lines changed

6 files changed

+320
-13
lines changed

firewood/src/proofs/tests.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
#![expect(clippy::unwrap_used, clippy::indexing_slicing)]
55

6+
use firewood_storage::{
7+
KeyProofTrieRoot, PackedPathRef, PathComponent, TrieNode, TriePath, TriePathFromPackedBytes,
8+
ValueDigest,
9+
};
610
use integer_encoding::VarInt;
711
use test_case::test_case;
812

@@ -224,3 +228,100 @@ fn test_empty_proof() {
224228
Err(err) => panic!("Expected valid empty proof, got error: {err}"),
225229
}
226230
}
231+
232+
#[test]
233+
fn test_proof_trie_construction() {
234+
let merkle = crate::merkle::tests::init_merkle((0u8..=10).map(|k| ([k], [k])));
235+
let proof = merkle
236+
.range_proof(Some(&[2u8]), Some(&[8u8]), std::num::NonZeroUsize::new(5))
237+
.unwrap();
238+
239+
let lower_trie = KeyProofTrieRoot::new(&**proof.start_proof())
240+
.unwrap()
241+
.unwrap();
242+
let upper_trie = KeyProofTrieRoot::new(&**proof.end_proof())
243+
.unwrap()
244+
.unwrap();
245+
246+
let mut iter = lower_trie.iter_path(PackedPathRef::path_from_packed_bytes(&[0x2_u8]));
247+
let (path, edge) = iter.next().unwrap();
248+
assert!(path.is_empty());
249+
assert!(edge.is_unhashed());
250+
let root = edge.node().unwrap();
251+
252+
#[cfg(feature = "branch_factor_256")]
253+
assert!(root.partial_path().is_empty());
254+
#[cfg(not(feature = "branch_factor_256"))]
255+
assert!(root.partial_path().path_eq(&[PathComponent::ALL[0]]));
256+
257+
assert_eq!(root.value(), None);
258+
assert!(root.child_hash(PathComponent::ALL[2]).is_some());
259+
assert!(root.child_hash(PathComponent::ALL[6]).is_some());
260+
assert!(root.child_hash(PathComponent::ALL[10]).is_some());
261+
assert!(root.child_hash(PathComponent::ALL[11]).is_none());
262+
assert!(root.child_node(PathComponent::ALL[6]).is_none());
263+
assert!(root.child_node(PathComponent::ALL[10]).is_none());
264+
assert!(root.child_node(PathComponent::ALL[11]).is_none());
265+
let child = root.child_node(PathComponent::ALL[2]).unwrap();
266+
assert!(child.partial_path().is_empty());
267+
assert_eq!(child.value(), Some(&ValueDigest::Value(&[2_u8][..])));
268+
269+
let (path, edge) = iter.next().unwrap();
270+
#[cfg(feature = "branch_factor_256")]
271+
assert!(path.path_eq(&[PathComponent::ALL[2]]));
272+
#[cfg(not(feature = "branch_factor_256"))]
273+
assert!(path.path_eq(&[PathComponent::ALL[0], PathComponent::ALL[2]]));
274+
275+
assert!(
276+
edge.is_local(),
277+
"edge from root to child has both hash and node"
278+
);
279+
let root = edge.node().unwrap();
280+
assert!(
281+
std::ptr::eq(root, child),
282+
"expected not just equal, but identical references to the same node"
283+
);
284+
assert!(root.partial_path().is_empty());
285+
assert_eq!(root.value(), Some(&ValueDigest::Value(&[2_u8][..])));
286+
287+
let mut iter = upper_trie.iter_path(PackedPathRef::path_from_packed_bytes(&[0x6_u8]));
288+
let (path, edge) = iter.next().unwrap();
289+
assert!(path.is_empty());
290+
assert!(edge.is_unhashed());
291+
let root = edge.node().unwrap();
292+
293+
#[cfg(feature = "branch_factor_256")]
294+
assert!(root.partial_path().is_empty());
295+
#[cfg(not(feature = "branch_factor_256"))]
296+
assert!(root.partial_path().path_eq(&[PathComponent::ALL[0]]));
297+
298+
assert_eq!(root.value(), None);
299+
assert!(root.child_hash(PathComponent::ALL[2]).is_some());
300+
assert!(root.child_hash(PathComponent::ALL[6]).is_some());
301+
assert!(root.child_hash(PathComponent::ALL[10]).is_some());
302+
assert!(root.child_hash(PathComponent::ALL[11]).is_none());
303+
assert!(root.child_node(PathComponent::ALL[2]).is_none());
304+
assert!(root.child_node(PathComponent::ALL[10]).is_none());
305+
assert!(root.child_node(PathComponent::ALL[11]).is_none());
306+
let child = root.child_node(PathComponent::ALL[6]).unwrap();
307+
assert!(child.partial_path().is_empty());
308+
assert_eq!(child.value(), Some(&ValueDigest::Value(&[6_u8][..])));
309+
310+
let (path, edge) = iter.next().unwrap();
311+
#[cfg(feature = "branch_factor_256")]
312+
assert!(path.path_eq(&[PathComponent::ALL[6]]));
313+
#[cfg(not(feature = "branch_factor_256"))]
314+
assert!(path.path_eq(&[PathComponent::ALL[0], PathComponent::ALL[6]]));
315+
316+
assert!(
317+
edge.is_local(),
318+
"edge from root to child has both hash and node"
319+
);
320+
let root = edge.node().unwrap();
321+
assert!(
322+
std::ptr::eq(root, child),
323+
"expected not just equal, but identical references to the same node"
324+
);
325+
assert!(root.partial_path().is_empty());
326+
assert_eq!(root.value(), Some(&ValueDigest::Value(&[6_u8][..])));
327+
}

storage/src/lib.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,16 @@ pub use nodestore::{
5858
NodeReader, NodeStore, Parentable, RootReader, TrieReader,
5959
};
6060
pub use path::{
61-
ComponentIter, IntoSplitPath, JoinedPath, PartialPath, PathBuf, PathCommonPrefix,
62-
PathComponent, PathComponentSliceExt, PathGuard, SplitPath, TriePath, TriePathAsPackedBytes,
63-
TriePathFromPackedBytes, TriePathFromUnpackedBytes,
61+
ComponentIter, IntoSplitPath, JoinedPath, PackedPathRef, PartialPath, PathBuf,
62+
PathCommonPrefix, PathComponent, PathComponentSliceExt, PathGuard, SplitPath, TriePath,
63+
TriePathAsPackedBytes, TriePathFromPackedBytes, TriePathFromUnpackedBytes,
6464
};
6565
#[cfg(not(feature = "branch_factor_256"))]
66-
pub use path::{PackedBytes, PackedPathComponents, PackedPathRef};
66+
pub use path::{PackedBytes, PackedPathComponents};
6767
pub use tries::{
68-
DuplicateKeyError, HashedKeyValueTrieRoot, HashedTrieNode, IterAscending, IterDescending,
69-
KeyValueTrieRoot, TrieEdgeIter, TrieEdgeState, TrieNode, TriePathIter, TrieValueIter,
68+
DuplicateKeyError, FromKeyProofError, HashedKeyValueTrieRoot, HashedTrieNode, IterAscending,
69+
IterDescending, KeyProofTrieRoot, KeyValueTrieRoot, TrieEdgeIter, TrieEdgeState, TrieNode,
70+
TriePathIter, TrieValueIter,
7071
};
7172
pub use u4::{TryFromIntError, U4};
7273

storage/src/path/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ pub use self::joined::JoinedPath;
1515
pub use self::packed::{PackedBytes, PackedPathComponents, PackedPathRef};
1616
pub use self::split::{IntoSplitPath, PathCommonPrefix, SplitPath};
1717

18+
/// If the branch factor is 256, a packed path is just a slice of path components.
19+
#[cfg(feature = "branch_factor_256")]
20+
pub type PackedPathRef<'a> = &'a [PathComponent];
21+
1822
/// A trie path of components with different underlying representations.
1923
///
2024
/// The underlying representation does not need to be a contiguous array of

storage/src/tries/kvp.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
22
// See the file LICENSE.md for licensing terms.
33

4-
#[cfg(not(feature = "branch_factor_256"))]
5-
use crate::PackedPathRef;
64
use crate::{
7-
Children, HashType, Hashable, HashableShunt, HashedTrieNode, JoinedPath, PathBuf,
8-
PathComponent, PathGuard, SplitPath, TrieNode, TriePath, TriePathFromPackedBytes, ValueDigest,
5+
Children, HashType, Hashable, HashableShunt, HashedTrieNode, JoinedPath, PackedPathRef,
6+
PathBuf, PathComponent, PathGuard, SplitPath, TrieNode, TriePath, TriePathFromPackedBytes,
7+
ValueDigest,
98
};
109

11-
#[cfg(feature = "branch_factor_256")]
12-
type PackedPathRef<'a> = &'a [PathComponent];
13-
1410
/// A duplicate key error when merging two key-value tries.
1511
#[non_exhaustive]
1612
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]

storage/src/tries/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33

44
mod iter;
55
mod kvp;
6+
mod proof;
67

78
use crate::{HashType, IntoSplitPath, PathComponent, SplitPath};
89

910
pub use self::iter::{IterAscending, IterDescending, TrieEdgeIter, TriePathIter, TrieValueIter};
1011
pub use self::kvp::{DuplicateKeyError, HashedKeyValueTrieRoot, KeyValueTrieRoot};
12+
pub use self::proof::{FromKeyProofError, KeyProofTrieRoot};
1113

1214
/// The state of an edge from a parent node to a child node in a trie.
1315
#[derive(Debug, PartialEq, Eq, Hash)]

storage/src/tries/proof.rs

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
// Copyright (C) 2025, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE.md for licensing terms.
3+
4+
use crate::{
5+
Children, HashType, Hashable, IntoSplitPath, PathBuf, PathComponent, SplitPath, TrieEdgeState,
6+
TrieNode, TriePath, ValueDigest,
7+
};
8+
9+
/// An error indicating that a slice of proof nodes is invalid.
10+
#[derive(Debug, Clone, PartialEq, Eq, Hash, thiserror::Error)]
11+
pub enum FromKeyProofError {
12+
/// The parent node's path is not a strict prefix the node that follows it.
13+
#[error(
14+
"parent node {parent_path} precedes child node {child_path} but is not a strict prefix of it",
15+
parent_path = parent_path.display(),
16+
child_path = child_path.display(),
17+
)]
18+
InvalidChildPath {
19+
/// The path of the parent node.
20+
parent_path: PathBuf,
21+
/// The path of the following child node.
22+
child_path: PathBuf,
23+
},
24+
/// The parent node does not reference the child node at the path component
25+
/// leading to the child node.
26+
#[error(
27+
"child node {child_path} is not reachable from parent node {parent_path}",
28+
parent_path = parent_path.display(),
29+
child_path = child_path.display(),
30+
)]
31+
MissingChild {
32+
/// The path of the parent node.
33+
parent_path: PathBuf,
34+
/// The path of the following child node.
35+
child_path: PathBuf,
36+
},
37+
}
38+
39+
/// A root node in a trie formed from a [`ProofNode`].
40+
///
41+
/// A proof trie follows a linear path from the root to a terminal node, and
42+
/// includes the necessary information to calculate the hash of each node along
43+
/// that path.
44+
///
45+
/// In the proof, each node will include the value or value digest at that node,
46+
/// depending on what is required by the hasher. Additionally, the hashes of each
47+
/// child node that branches off the node along the path are included.
48+
#[derive(Debug)]
49+
pub struct KeyProofTrieRoot<'a, P> {
50+
partial_path: P,
51+
value_digest: Option<ValueDigest<&'a [u8]>>,
52+
children: Children<Option<KeyProofTrieNode<'a, P>>>,
53+
}
54+
55+
#[derive(Debug)]
56+
enum KeyProofTrieNode<'a, P> {
57+
/// Described nodes are proof nodes where we have the data necessary to
58+
/// reconstruct the hash. The value digest may be a value or a digest. We can
59+
/// verify the hash of theses nodes using the value or digest, but may not
60+
/// have the full value.
61+
Described {
62+
node: Box<KeyProofTrieRoot<'a, P>>,
63+
hash: HashType,
64+
},
65+
/// Remote nodes are the nodes where we only know the ID, as discovered
66+
/// from a proof node. If we only have the child, we can't infer anything
67+
/// else about the node.
68+
Remote { hash: HashType },
69+
}
70+
71+
impl<'a, P: SplitPath> KeyProofTrieRoot<'a, P> {
72+
/// Constructs a trie root from a slice of proof nodes.
73+
///
74+
/// Each node in the slice must be a strict prefix of the following node. And,
75+
/// each child node must be referenced by its parent (i.e., the parent must
76+
/// indicate a child at the path component leading to the child). The hash
77+
/// is not verified here.
78+
///
79+
/// # Errors
80+
///
81+
/// - [`FromKeyProofError::InvalidChildPath`] if any node's path is not a strict
82+
/// prefix of the following node's path.
83+
/// - [`FromKeyProofError::MissingChild`] if any parent node does not reference
84+
/// the following child node at the path component leading to the child.
85+
pub fn new<T, N>(proof: &'a T) -> Result<Option<Box<Self>>, FromKeyProofError>
86+
where
87+
T: AsRef<[N]> + ?Sized,
88+
N: Hashable<FullPath<'a>: IntoSplitPath<Path = P>> + 'a,
89+
{
90+
proof
91+
.as_ref()
92+
.iter()
93+
.rev()
94+
.try_fold(None::<Box<Self>>, |parent, node| match parent {
95+
None => Ok(Some(Self::new_tail_node(node))),
96+
Some(p) => p.new_parent_node(node).map(Some),
97+
})
98+
}
99+
100+
/// Creates a new trie root from the tail node of a proof.
101+
fn new_tail_node<N>(node: &'a N) -> Box<Self>
102+
where
103+
N: Hashable<FullPath<'a>: IntoSplitPath<Path = P>>,
104+
{
105+
Box::new(Self {
106+
partial_path: node.full_path().into_split_path(),
107+
value_digest: node.value_digest(),
108+
children: node
109+
.children()
110+
.map(|_, child| child.map(|hash| KeyProofTrieNode::Remote { hash })),
111+
})
112+
}
113+
114+
/// Creates a new trie root by making this node a child of the given parent.
115+
///
116+
/// The parent key must be a strict prefix of this node's key, and the parent
117+
/// must reference this node in its children by hash (the hash is not verified
118+
/// here).
119+
fn new_parent_node<N>(
120+
mut self: Box<Self>,
121+
parent: &'a N,
122+
) -> Result<Box<Self>, FromKeyProofError>
123+
where
124+
N: Hashable<FullPath<'a>: IntoSplitPath<Path = P>>,
125+
{
126+
match parent
127+
.full_path()
128+
.into_split_path()
129+
.longest_common_prefix(self.partial_path)
130+
.split_first_parts()
131+
{
132+
(None, Some((pc, child_path)), parent_path) => {
133+
let mut parent = Self::new_tail_node(parent);
134+
if let Some(KeyProofTrieNode::Remote { hash }) = parent.children.take(pc) {
135+
self.partial_path = child_path;
136+
parent.partial_path = parent_path;
137+
parent.children[pc] = Some(KeyProofTrieNode::Described { node: self, hash });
138+
Ok(parent)
139+
} else {
140+
Err(FromKeyProofError::MissingChild {
141+
parent_path: parent.partial_path.as_component_slice().into_owned(),
142+
child_path: self.partial_path.as_component_slice().into_owned(),
143+
})
144+
}
145+
}
146+
_ => Err(FromKeyProofError::InvalidChildPath {
147+
parent_path: parent.full_path().as_component_slice().into_owned(),
148+
child_path: self.partial_path.as_component_slice().into_owned(),
149+
}),
150+
}
151+
}
152+
}
153+
154+
impl<'a, P: IntoSplitPath + 'a> KeyProofTrieNode<'a, P> {
155+
const fn hash(&self) -> &HashType {
156+
match self {
157+
KeyProofTrieNode::Described { hash, .. } | KeyProofTrieNode::Remote { hash } => hash,
158+
}
159+
}
160+
161+
const fn node(&self) -> Option<&KeyProofTrieRoot<'a, P>> {
162+
match self {
163+
KeyProofTrieNode::Described { node, .. } => Some(node),
164+
KeyProofTrieNode::Remote { .. } => None,
165+
}
166+
}
167+
168+
const fn as_edge_state(&self) -> TrieEdgeState<'_, KeyProofTrieRoot<'a, P>> {
169+
match self {
170+
KeyProofTrieNode::Described { node, hash } => TrieEdgeState::LocalChild { node, hash },
171+
KeyProofTrieNode::Remote { hash } => TrieEdgeState::RemoteChild { hash },
172+
}
173+
}
174+
}
175+
176+
impl<'a, P: SplitPath + 'a> TrieNode<ValueDigest<&'a [u8]>> for KeyProofTrieRoot<'a, P> {
177+
type PartialPath<'b>
178+
= P
179+
where
180+
Self: 'b;
181+
182+
fn partial_path(&self) -> Self::PartialPath<'_> {
183+
self.partial_path
184+
}
185+
186+
fn value(&self) -> Option<&ValueDigest<&'a [u8]>> {
187+
self.value_digest.as_ref()
188+
}
189+
190+
fn child_hash(&self, pc: PathComponent) -> Option<&HashType> {
191+
self.children[pc].as_ref().map(KeyProofTrieNode::hash)
192+
}
193+
194+
fn child_node(&self, pc: PathComponent) -> Option<&Self> {
195+
self.children[pc].as_ref().and_then(KeyProofTrieNode::node)
196+
}
197+
198+
fn child_state(&self, pc: PathComponent) -> Option<super::TrieEdgeState<'_, Self>> {
199+
self.children[pc]
200+
.as_ref()
201+
.map(KeyProofTrieNode::as_edge_state)
202+
}
203+
}

0 commit comments

Comments
 (0)