Skip to content

Commit 2164262

Browse files
authored
feat: add TrieNode trait and related functionality (#1363)
`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 2164262

File tree

9 files changed

+958
-9
lines changed

9 files changed

+958
-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: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,79 @@ 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+
///
134+
/// The guard exposes append-only methods to add components to the path buffer
135+
/// but not remove them without dropping the guard. This ensures that the guard
136+
/// will always restore the path buffer to its original state when dropped.
137+
#[must_use]
138+
pub struct PathGuard<'a> {
139+
buf: &'a mut PathBuf,
140+
len: usize,
141+
}
142+
143+
impl std::fmt::Debug for PathGuard<'_> {
144+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145+
self.buf.display().fmt(f)
146+
}
147+
}
148+
149+
impl Drop for PathGuard<'_> {
150+
fn drop(&mut self) {
151+
self.buf.truncate(self.len);
152+
}
153+
}
154+
155+
impl<'a> PathGuard<'a> {
156+
/// Creates a new guard that will reset the provided buffer to its current
157+
/// length when dropped.
158+
pub fn new(buf: &'a mut PathBuf) -> Self {
159+
Self {
160+
len: buf.len(),
161+
buf,
162+
}
163+
}
164+
165+
/// Creates a new guard that will reset this guard's buffer to its current
166+
/// length when dropped.
167+
///
168+
/// This allows for nested guards that can be used in recursive algorithms.
169+
pub fn fork(&mut self) -> PathGuard<'_> {
170+
PathGuard::new(self.buf)
171+
}
172+
173+
/// Fork this guard and append the given segment to the path buffer.
174+
///
175+
/// This is a convenience method that combines `fork` then `extend` in a
176+
/// single operation for ergonomic one-liners.
177+
///
178+
/// The returned guard will reset the path buffer to its original length,
179+
/// before appending the given segment, when dropped.
180+
pub fn fork_append(&mut self, path: impl TriePath) -> PathGuard<'_> {
181+
let mut fork = self.fork();
182+
fork.extend(path.components());
183+
fork
184+
}
185+
186+
/// Appends the given component to the path buffer.
187+
///
188+
/// This component will be removed when the guard is dropped.
189+
pub fn push(&mut self, component: PathComponent) {
190+
self.buf.push(component);
191+
}
192+
}
193+
194+
impl std::ops::Deref for PathGuard<'_> {
195+
type Target = PathBuf;
196+
197+
fn deref(&self) -> &Self::Target {
198+
self.buf
199+
}
200+
}
201+
202+
impl Extend<PathComponent> for PathGuard<'_> {
203+
fn extend<T: IntoIterator<Item = PathComponent>>(&mut self, iter: T) {
204+
self.buf.extend(iter);
205+
}
206+
}

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)