Skip to content

Commit d7d811a

Browse files
committed
feat: add TrieNode trait and related functionality
`TrieNode` is a generic trait that abstracts over the behavior of nodes in a fixed-arity radix trie. This trait allows for different structures to represent tries while enabling similar operations on them. `TrieNode` is merkle-aware, meaning it can represent nodes that are a mix of locally described children or children referenced by only their hash. This is accomplished through `TrieEdgeState` and is used later in proof verification. The `HashedTrieNode` trait is for post-hashed nodes and is distinctly different from the hash described on `TrieNode`. This trait is intended to represent the computed hash of the node itself; whereas, the hash accessed via `TrieNode` is the hash of a child node as seen from its parent. While they are usually the same, they can differ during intermediate states of a trie or during proof verification when some information is incomplete. `KeyValueTrieRoot` is an implementation of `TrieNode` that only represents a key-value store and is not merkleized. This is really a separate change and is a component of proof verification but is also needed here as it is used to test and verify the iterator.
1 parent c8d35c6 commit d7d811a

File tree

9 files changed

+947
-9
lines changed

9 files changed

+947
-9
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ metrics-util = "0.20.0"
7676
nonzero_ext = "0.3.0"
7777
rand_distr = "0.5.1"
7878
sha2 = "0.10.9"
79-
smallvec = "1.15.1"
79+
smallvec = { version = "1.15.1", features = ["write", "union", "const_new"] }
8080
test-case = "3.3.1"
8181
thiserror = "2.0.17"
8282

storage/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ metrics.workspace = true
2929
nonzero_ext.workspace = true
3030
rand = { workspace = true, optional = true }
3131
sha2.workspace = true
32-
smallvec = { workspace = true, features = ["write", "union"] }
32+
smallvec.workspace = true
3333
thiserror.workspace = true
3434
# Regular dependencies
3535
arc-swap = "1.7.1"

storage/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod path;
3434
#[cfg(any(test, feature = "test_utils"))]
3535
mod test_utils;
3636
mod trie_hash;
37+
mod tries;
3738
mod u4;
3839

3940
/// Logger module for handling logging functionality
@@ -58,11 +59,15 @@ pub use nodestore::{
5859
};
5960
pub use path::{
6061
ComponentIter, IntoSplitPath, JoinedPath, PartialPath, PathBuf, PathCommonPrefix,
61-
PathComponent, PathComponentSliceExt, SplitPath, TriePath, TriePathAsPackedBytes,
62+
PathComponent, PathComponentSliceExt, PathGuard, SplitPath, TriePath, TriePathAsPackedBytes,
6263
TriePathFromPackedBytes, TriePathFromUnpackedBytes,
6364
};
6465
#[cfg(not(feature = "branch_factor_256"))]
6566
pub use path::{PackedBytes, PackedPathComponents, PackedPathRef};
67+
pub use tries::{
68+
DuplicateKeyError, HashedTrieNode, IterAscending, IterDescending, KeyValueTrieRoot,
69+
TrieEdgeIter, TrieEdgeState, TrieNode, TrieValueIter,
70+
};
6671
pub use u4::{TryFromIntError, U4};
6772

6873
pub use linear::filebacked::FileBacked;

storage/src/node/branch.rs

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,12 @@ mod ethhash {
222222
pub fn into_triehash(self) -> TrieHash {
223223
self.into()
224224
}
225+
226+
// used in PartialEq and Hash impls and is to trick clippy into not caring
227+
// about creating an owned instance for comparison
228+
fn as_triehash(&self) -> TrieHash {
229+
self.into()
230+
}
225231
}
226232

227233
impl PartialEq<TrieHash> for HashOrRlp {
@@ -247,19 +253,27 @@ mod ethhash {
247253
impl PartialEq for HashOrRlp {
248254
fn eq(&self, other: &Self) -> bool {
249255
match (self, other) {
256+
// if both are hash or rlp, we can skip hashing
250257
(HashOrRlp::Hash(h1), HashOrRlp::Hash(h2)) => h1 == h2,
251258
(HashOrRlp::Rlp(r1), HashOrRlp::Rlp(r2)) => r1 == r2,
252-
#[expect(deprecated, reason = "transitive dependency on generic-array")]
253-
(HashOrRlp::Hash(h), HashOrRlp::Rlp(r))
254-
| (HashOrRlp::Rlp(r), HashOrRlp::Hash(h)) => {
255-
Keccak256::digest(r.as_ref()).as_slice() == h.as_ref()
256-
}
259+
// otherwise, one is a hash and the other isn't, so convert both
260+
// to hash and compare
261+
_ => self.as_triehash() == other.as_triehash(),
257262
}
258263
}
259264
}
260265

261266
impl Eq for HashOrRlp {}
262267

268+
impl std::hash::Hash for HashOrRlp {
269+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
270+
// contract on `Hash` and `PartialEq` requires that if `a == b` then `hash(a) == hash(b)`
271+
// and since `PartialEq` may require hashing, we must always convert to `TrieHash` here
272+
// and use it's hash implementation
273+
self.as_triehash().hash(state);
274+
}
275+
}
276+
263277
impl Serializable for HashOrRlp {
264278
fn write_to<W: ExtendableBytes>(&self, vec: &mut W) {
265279
match self {
@@ -311,6 +325,15 @@ mod ethhash {
311325
}
312326
}
313327

328+
impl From<&HashOrRlp> for TrieHash {
329+
fn from(val: &HashOrRlp) -> Self {
330+
match val {
331+
HashOrRlp::Hash(h) => h.clone(),
332+
HashOrRlp::Rlp(r) => Keccak256::digest(r).into(),
333+
}
334+
}
335+
}
336+
314337
impl From<TrieHash> for HashOrRlp {
315338
fn from(val: TrieHash) -> Self {
316339
HashOrRlp::Hash(val)

storage/src/path/buf.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,68 @@ impl<'a> IntoSplitPath for &'a PartialPath<'_> {
128128
self
129129
}
130130
}
131+
132+
/// A RAII guard that resets a path buffer to its original length when dropped.
133+
#[must_use]
134+
pub struct PathGuard<'a> {
135+
buf: &'a mut PathBuf,
136+
len: usize,
137+
}
138+
139+
impl std::fmt::Debug for PathGuard<'_> {
140+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
141+
self.buf.display().fmt(f)
142+
}
143+
}
144+
145+
impl Drop for PathGuard<'_> {
146+
fn drop(&mut self) {
147+
self.buf.truncate(self.len);
148+
}
149+
}
150+
151+
impl<'a> PathGuard<'a> {
152+
/// Creates a new guard that will reset the provided buffer to its current
153+
/// length when dropped.
154+
pub fn new(buf: &'a mut PathBuf) -> Self {
155+
Self {
156+
len: buf.len(),
157+
buf,
158+
}
159+
}
160+
161+
/// Creates a new guard that will reset this guard's buffer to its current
162+
/// length when dropped.
163+
///
164+
/// This allows for nested guards that can be used in recursive algorithms.
165+
pub fn fork(&mut self) -> PathGuard<'_> {
166+
PathGuard::new(self.buf)
167+
}
168+
169+
/// Fork this guard and append the given segment to the path buffer.
170+
///
171+
/// This is a convenience method that combines `fork` then `extend` in a
172+
/// single operation for ergonomic one-liners.
173+
///
174+
/// The returned guard will reset the path buffer to its original length,
175+
/// before appending the given segment, when dropped.
176+
pub fn fork_append(&mut self, path: impl TriePath) -> PathGuard<'_> {
177+
let mut fork = self.fork();
178+
fork.extend(path.components());
179+
fork
180+
}
181+
}
182+
183+
impl std::ops::Deref for PathGuard<'_> {
184+
type Target = PathBuf;
185+
186+
fn deref(&self) -> &Self::Target {
187+
self.buf
188+
}
189+
}
190+
191+
impl std::ops::DerefMut for PathGuard<'_> {
192+
fn deref_mut(&mut self) -> &mut Self::Target {
193+
self.buf
194+
}
195+
}

storage/src/path/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ mod joined;
88
mod packed;
99
mod split;
1010

11-
pub use self::buf::{PartialPath, PathBuf};
11+
pub use self::buf::{PartialPath, PathBuf, PathGuard};
1212
pub use self::component::{ComponentIter, PathComponent, PathComponentSliceExt};
1313
pub use self::joined::JoinedPath;
1414
#[cfg(not(feature = "branch_factor_256"))]

0 commit comments

Comments
 (0)