Repository: lambdaclass/ethrex
Pull request #2687: refactor(core): Refactor Patricia Merkle Trie (avoid compulsive hashing)
Author: login: azteca1998
Labels: name: performance
description: Block execution throughput and performance in general
Created: 2025-05-07T13:22:07Z | Merged: 2025-05-29T13:40:40Z

**Motivation**

<!-- Why does this pull request exist? What are its goals? -->

Refactor the Patricia Merkle Trie to avoid rehashing the affected part of the trie every time it is modified. Delay hashing until its hash is required.

<!-- A clear and concise general description of the changes this PR introduces -->

Replaces node hash references by either a `NodeHash` as before (for unbuffered nodes) or a node itself (for modified nodes).

<!-- Link to issues: Resolves #111, Resolves #222 -->

Closes #issue_number




Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62986f9fdd2..ed242f96c0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## Perf
 
+### 2025-05-27
+
+- Refactor Patricia Merkle Trie to avoid rehashing the entire path on every insert [2687](https://github.com/lambdaclass/ethrex/pull/2687)
+
 ### 2025-05-22
 
 - Add immutable cache to LEVM that stores in memory data read from the Database so that getting account doesn't need to consult the Database again. [2829](https://github.com/lambdaclass/ethrex/pull/2829)
diff --git a/crates/common/trie/node.rs b/crates/common/trie/node.rs
index 7a31e25f870..fbe762be332 100644
--- a/crates/common/trie/node.rs
+++ b/crates/common/trie/node.rs
@@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
+    /// The node is in the database, referenced by its hash.
+    Hash(NodeHash),
+}
+
+impl NodeRef {
+    pub const fn const_default() -> Self {
+        Self::Hash(NodeHash::const_default())
+    }
+
+    pub fn get_node(&self, db: &dyn TrieDB) -> Result<Option<Node>, TrieError> {
+        match *self {
+            NodeRef::Node(ref node, _) => Ok(Some(node.as_ref().clone())),
+            NodeRef::Hash(NodeHash::Inline((data, len))) => {
+                Ok(Some(Node::decode_raw(&data[..len as usize])?))
+            }
+            NodeRef::Hash(hash @ NodeHash::Hashed(_)) => db
+                .get(hash)?
+                .map(|rlp| Node::decode(&rlp).map_err(TrieError::RLPDecode))
+                .transpose(),
+        }
+    }
+
+    pub fn is_valid(&self) -> bool {
+        match self {
+            NodeRef::Node(_, _) => true,
+            NodeRef::Hash(hash) => hash.is_valid(),
+        }
+    }
+
+    pub fn commit(&mut self, acc: &mut Vec<(NodeHash, Vec<u8>)>) -> NodeHash {
+        match *self {
+            NodeRef::Node(ref mut node, ref mut hash) => {
+                match Arc::make_mut(node) {
+                    Node::Branch(node) => {
+                        for node in &mut node.choices {
+                            node.commit(acc);
+                        }
+                    }
+                    Node::Extension(node) => {
+                        node.child.commit(acc);
+                    }
+                    Node::Leaf(_) => {}
+                }
+
+                let hash = hash.get_or_init(|| node.compute_hash());
+                acc.push((*hash, node.encode_to_vec()));
+
+                let hash = *hash;
+                *self = hash.into();
+
+                hash
+            }
+            NodeRef::Hash(hash) => hash,
+        }
+    }
+
+    pub fn compute_hash(&self) -> NodeHash {
+        match self {
+            NodeRef::Node(node, hash) => *hash.get_or_init(|| node.compute_hash()),
+            NodeRef::Hash(hash) => *hash,
+        }
+    }
+}
+
+impl Default for NodeRef {
+    fn default() -> Self {
+        Self::const_default()
+    }
+}
+
+impl From<Node> for NodeRef {
+    fn from(value: Node) -> Self {
+        Self::Node(Arc::new(value), OnceLock::new())
+    }
+}
+
+impl From<NodeHash> for NodeRef {
+    fn from(value: NodeHash) -> Self {
+        Self::Hash(value)
+    }
+}
+
+impl PartialEq for NodeRef {
+    fn eq(&self, other: &Self) -> bool {
+        self.compute_hash() == other.compute_hash()
+    }
+}
+
+pub enum ValueOrHash {
+    Value(ValueRLP),
+    Hash(NodeHash),
+}
+
+impl From<ValueRLP> for ValueOrHash {
+    fn from(value: ValueRLP) -> Self {
+        Self::Value(value)
+    }
+}
+
+impl From<NodeHash> for ValueOrHash {
+    fn from(value: NodeHash) -> Self {
+        Self::Hash(value)
+    }
+}
 
 /// A Node in an Ethereum Compatible Patricia Merkle Trie
 #[derive(Debug, Clone, PartialEq)]
@@ -41,10 +159,10 @@ impl From<LeafNode> for Node {
 
 impl Node {
     /// Retrieves a value from the subtrie originating from this node given its path
-    pub fn get(&self, state: &TrieState, path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
+    pub fn get(&self, db: &dyn TrieDB, path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
         match self {
-            Node::Branch(n) => n.get(state, path),
-            Node::Extension(n) => n.get(state, path),
+            Node::Branch(n) => n.get(db, path),
+            Node::Extension(n) => n.get(db, path),
             Node::Leaf(n) => n.get(path),
         }
     }
@@ -52,14 +170,14 @@ impl Node {
     /// Inserts a value into the subtrie originating from this node and returns the new root of the subtrie
     pub fn insert(
         self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         path: Nibbles,
-        value: ValueRLP,
+        value: impl Into<ValueOrHash>,
     ) -> Result<Node, TrieError> {
         match self {
-            Node::Branch(n) => n.insert(state, path, value),
-            Node::Extension(n) => n.insert(state, path, value),
-            Node::Leaf(n) => n.insert(state, path, value),
+            Node::Branch(n) => n.insert(db, path, value.into()),
+            Node::Extension(n) => n.insert(db, path, value.into()),
+            Node::Leaf(n) => n.insert(path, value.into()),
         }
     }
 
@@ -67,12 +185,12 @@ impl Node {
     /// Returns the new root of the subtrie (if any) and the removed value if it existed in the subtrie
     pub fn remove(
         self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         path: Nibbles,
     ) -> Result<(Option<Node>, Option<ValueRLP>), TrieError> {
         match self {
-            Node::Branch(n) => n.remove(state, path),
-            Node::Extension(n) => n.remove(state, path),
+            Node::Branch(n) => n.remove(db, path),
+            Node::Extension(n) => n.remove(db, path),
             Node::Leaf(n) => n.remove(path),
         }
     }
@@ -82,25 +200,17 @@ impl Node {
     /// Only nodes with encoded len over or equal to 32 bytes are included
     pub fn get_path(
         &self,
-        state: &TrieState,
+        db: &dyn TrieDB,
         path: Nibbles,
         node_path: &mut Vec<Vec<u8>>,
     ) -> Result<(), TrieError> {
         match self {
-            Node::Branch(n) => n.get_path(state, path, node_path),
-            Node::Extension(n) => n.get_path(state, path, node_path),
+            Node::Branch(n) => n.get_path(db, path, node_path),
+            Node::Extension(n) => n.get_path(db, path, node_path),
             Node::Leaf(n) => n.get_path(node_path),
         }
     }
 
-    pub fn insert_self(self, state: &mut TrieState) -> Result<NodeHash, TrieError> {
-        match self {
-            Node::Branch(n) => n.insert_self(state),
-            Node::Extension(n) => n.insert_self(state),
-            Node::Leaf(n) => n.insert_self(state),
-        }
-    }
-
     /// Encodes the node
     pub fn encode_raw(&self) -> Vec<u8> {
         match self {
@@ -142,17 +252,17 @@ impl Node {
                     // Decode as Extension
                     ExtensionNode {
                         prefix: path,
-                        child: decode_child(&rlp_items[1]),
+                        child: decode_child(&rlp_items[1]).into(),
                     }
                     .into()
                 }
             }
             // Branch Node
             17 => {
-                let choices = array::from_fn(|i| decode_child(&rlp_items[i]));
+                let choices = array::from_fn(|i| decode_child(&rlp_items[i]).into());
                 let (value, _) = decode_bytes(&rlp_items[16])?;
                 BranchNode {
-                    choices: Box::new(choices),
+                    choices,
                     value: value.to_vec(),
                 }
                 .into()
diff --git a/crates/common/trie/node/branch.rs b/crates/common/trie/node/branch.rs
index 9760b0c9bff..21cda82938d 100644
--- a/crates/common/trie/node/branch.rs
+++ b/crates/common/trie/node/branch.rs
@@ -1,41 +1,40 @@
 use ethrex_rlp::structs::Encoder;
 
-use crate::{error::TrieError, nibbles::Nibbles, node_hash::NodeHash, state::TrieState, ValueRLP};
+use crate::{error::TrieError, nibbles::Nibbles, node_hash::NodeHash, TrieDB, ValueRLP};
 
-use super::{ExtensionNode, LeafNode, Node};
+use super::{ExtensionNode, LeafNode, Node, NodeRef, ValueOrHash};
 
 /// Branch Node of an an Ethereum Compatible Patricia Merkle Trie
 /// Contains the node's value and the hash of its children nodes
 #[derive(Debug, Clone, PartialEq)]
 pub struct BranchNode {
-    // TODO: check if switching to hashmap is a better solution
-    pub choices: Box<[NodeHash; 16]>,
+    pub choices: [NodeRef; 16],
     pub value: ValueRLP,
 }
 
 impl BranchNode {
     /// Empty choice array for more convenient node-building
-    pub const EMPTY_CHOICES: [NodeHash; 16] = [
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
-        NodeHash::const_default(),
+    pub const EMPTY_CHOICES: [NodeRef; 16] = [
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
+        NodeRef::const_default(),
     ];
 
     /// Creates a new branch node given its children, without any stored value
-    pub fn new(choices: Box<[NodeHash; 16]>) -> Self {
+    pub fn new(choices: [NodeRef; 16]) -> Self {
         Self {
             choices,
             value: Default::default(),
@@ -43,7 +42,7 @@ impl BranchNode {
     }
 
     /// Creates a new branch node given its children and value
-    pub const fn new_with_value(choices: Box<[NodeHash; 16]>, value: ValueRLP) -> Self {
+    pub const fn new_with_value(choices: [NodeRef; 16], value: ValueRLP) -> Self {
         Self { choices, value }
     }
 
@@ -53,17 +52,15 @@ impl BranchNode {
     }
 
     /// Retrieves a value from the subtrie originating from this node given its path
-    pub fn get(&self, state: &TrieState, mut path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
+    pub fn get(&self, db: &dyn TrieDB, mut path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
         // If path is at the end, return to its own value if present.
         // Otherwise, check the corresponding choice and delegate accordingly if present.
         if let Some(choice) = path.next_choice() {
             // Delegate to children if present
-            let child_hash = &self.choices[choice];
-            if child_hash.is_valid() {
-                let child_node = state
-                    .get_node(*child_hash)?
-                    .ok_or(TrieError::InconsistentTree)?;
-                child_node.get(state, path)
+            let child_ref = &self.choices[choice];
+            if child_ref.is_valid() {
+                let child_node = child_ref.get_node(db)?.ok_or(TrieError::InconsistentTree)?;
+                child_node.get(db, path)
             } else {
                 Ok(None)
             }
@@ -76,33 +73,49 @@ impl BranchNode {
     /// Inserts a value into the subtrie originating from this node and returns the new root of the subtrie
     pub fn insert(
         mut self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
-        value: ValueRLP,
+        value: ValueOrHash,
     ) -> Result<Node, TrieError> {
         // If path is at the end, insert or replace its own value.
         // Otherwise, check the corresponding choice and insert or delegate accordingly.
         if let Some(choice) = path.next_choice() {
-            match &mut self.choices[choice] {
+            match (&mut self.choices[choice], value) {
                 // Create new child (leaf node)
-                choice_hash if !choice_hash.is_valid() => {
+                (choice_ref, ValueOrHash::Value(value)) if !choice_ref.is_valid() => {
                     let new_leaf = LeafNode::new(path, value);
-                    let child_hash = new_leaf.insert_self(state)?;
-                    *choice_hash = child_hash;
+                    *choice_ref = Node::from(new_leaf).into();
                 }
                 // Insert into existing child and then update it
-                choice_hash => {
-                    let child_node = state
-                        .get_node(*choice_hash)?
+                (choice_ref, ValueOrHash::Value(value)) => {
+                    let child_node = choice_ref
+                        .get_node(db)?
                         .ok_or(TrieError::InconsistentTree)?;
 
-                    let child_node = child_node.insert(state, path, value)?;
-                    *choice_hash = child_node.insert_self(state)?;
+                    *choice_ref = child_node.insert(db, path, value)?.into();
+                }
+                // Insert external node hash if there are no overrides.
+                (choice_ref, value @ ValueOrHash::Hash(hash)) => {
+                    if !choice_ref.is_valid() {
+                        *choice_ref = hash.into();
+                    } else if path.is_empty() {
+                        return Err(TrieError::Verify(
+                            "attempt to override proof node with external hash".to_string(),
+                        ));
+                    } else {
+                        *choice_ref = choice_ref
+                            .get_node(db)?
+                            .ok_or(TrieError::InconsistentTree)?
+                            .insert(db, path, value)?
+                            .into();
+                    }
                 }
             }
-        } else {
+        } else if let ValueOrHash::Value(value) = value {
             // Insert into self
             self.update(value);
+        } else {
+            todo!("handle override case (error?)")
         }
 
         Ok(self.into())
@@ -112,7 +125,7 @@ impl BranchNode {
     /// Returns the new root of the subtrie (if any) and the removed value if it existed in the subtrie
     pub fn remove(
         mut self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
     ) -> Result<(Option<Node>, Option<ValueRLP>), TrieError> {
         /* Possible flow paths:
@@ -137,17 +150,17 @@ impl BranchNode {
         // Check if the value is located in a child subtrie
         let value = if let Some(choice_index) = path.next_choice() {
             if self.choices[choice_index].is_valid() {
-                let child_node = state
-                    .get_node(self.choices[choice_index])?
+                let child_node = self.choices[choice_index]
+                    .get_node(db)?
                     .ok_or(TrieError::InconsistentTree)?;
                 // Remove value from child node
-                let (child_node, old_value) = child_node.remove(state, path.clone())?;
+                let (child_node, old_value) = child_node.remove(db, path.clone())?;
                 if let Some(child_node) = child_node {
                     // Update child node
-                    self.choices[choice_index] = child_node.insert_self(state)?;
+                    self.choices[choice_index] = child_node.into();
                 } else {
                     // Remove child hash if the child subtrie was removed in the process
-                    self.choices[choice_index] = NodeHash::default();
+                    self.choices[choice_index] = NodeHash::default().into();
                 }
                 old_value
             } else {
@@ -177,16 +190,15 @@ impl BranchNode {
             (0, true) => LeafNode::new(Nibbles::from_hex(vec![16]), self.value).into(),
             // If this node doesn't have a value and has only one child, replace it with its child node
             (1, false) => {
-                let (choice_index, child_hash) = children[0];
-                let child = state
-                    .get_node(*child_hash)?
-                    .ok_or(TrieError::InconsistentTree)?;
+                let (choice_index, child_ref) = children[0];
+                let child = child_ref.get_node(db)?.ok_or(TrieError::InconsistentTree)?;
                 match child {
                     // Replace self with an extension node leading to the child
-                    Node::Branch(_) => {
-                        ExtensionNode::new(Nibbles::from_hex(vec![choice_index as u8]), *child_hash)
-                            .into()
-                    }
+                    Node::Branch(_) => ExtensionNode::new(
+                        Nibbles::from_hex(vec![choice_index as u8]),
+                        child_ref.clone(),
+                    )
+                    .into(),
                     // Replace self with the child extension node, updating its path in the process
                     Node::Extension(mut extension_node) => {
                         extension_node.prefix.prepend(choice_index as u8);
@@ -214,9 +226,11 @@ impl BranchNode {
         let mut buf = vec![];
         let mut encoder = Encoder::new(&mut buf);
         for child in self.choices.iter() {
-            match child {
+            match child.compute_hash() {
                 NodeHash::Hashed(hash) => encoder = encoder.encode_bytes(&hash.0),
-                NodeHash::Inline(raw) if raw.1 != 0 => encoder = encoder.encode_raw(child.as_ref()),
+                child @ NodeHash::Inline(raw) if raw.1 != 0 => {
+                    encoder = encoder.encode_raw(child.as_ref())
+                }
                 _ => encoder = encoder.encode_bytes(&[]),
             }
         }
@@ -225,19 +239,12 @@ impl BranchNode {
         buf
     }
 
-    /// Inserts the node into the state and returns its hash
-    pub fn insert_self(self, state: &mut TrieState) -> Result<NodeHash, TrieError> {
-        let hash = self.compute_hash();
-        state.insert_node(self.into(), hash);
-        Ok(hash)
-    }
-
     /// Traverses own subtrie until reaching the node containing `path`
     /// Appends all encoded nodes traversed to `node_path` (including self)
     /// Only nodes with encoded len over or equal to 32 bytes are included
     pub fn get_path(
         &self,
-        state: &TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
         node_path: &mut Vec<Vec<u8>>,
     ) -> Result<(), TrieError> {
@@ -249,12 +256,10 @@ impl BranchNode {
         // Check the corresponding choice and delegate accordingly if present.
         if let Some(choice) = path.next_choice() {
             // Continue to child
-            let child_hash = &self.choices[choice];
-            if child_hash.is_valid() {
-                let child_node = state
-                    .get_node(*child_hash)?
-                    .ok_or(TrieError::InconsistentTree)?;
-                child_node.get_path(state, path, node_path)?;
+            let child_ref = &self.choices[choice];
+            if child_ref.is_valid() {
+                let child_node = child_ref.get_node(db)?.ok_or(TrieError::InconsistentTree)?;
+                child_node.get_path(db, path, node_path)?;
             }
         }
         Ok(())
@@ -274,21 +279,21 @@ mod test {
         let node = BranchNode::new({
             let mut choices = BranchNode::EMPTY_CHOICES;
 
-            choices[2] = NodeHash::Hashed(H256([2; 32]));
-            choices[5] = NodeHash::Hashed(H256([5; 32]));
+            choices[2] = NodeHash::Hashed(H256([2; 32])).into();
+            choices[5] = NodeHash::Hashed(H256([5; 32])).into();
 
-            Box::new(choices)
+            choices
         });
 
         assert_eq!(
-            *node.choices,
+            node.choices,
             [
                 Default::default(),
                 Default::default(),
-                NodeHash::Hashed(H256([2; 32])),
+                NodeHash::Hashed(H256([2; 32])).into(),
                 Default::default(),
                 Default::default(),
-                NodeHash::Hashed(H256([5; 32])),
+                NodeHash::Hashed(H256([5; 32])).into(),
                 Default::default(),
                 Default::default(),
                 Default::default(),
@@ -305,7 +310,7 @@ mod test {
 
     #[test]
     fn get_some() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0,16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -314,18 +319,20 @@ mod test {
         };
 
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x00])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
+                .unwrap(),
             Some(vec![0x12, 0x34, 0x56, 0x78]),
         );
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x10])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x10]))
+                .unwrap(),
             Some(vec![0x34, 0x56, 0x78, 0x9A]),
         );
     }
 
     #[test]
     fn get_none() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0,16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -334,14 +341,15 @@ mod test {
         };
 
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x20])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x20]))
+                .unwrap(),
             None,
         );
     }
 
     #[test]
     fn insert_self() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -352,16 +360,16 @@ mod test {
         let value = vec![0x3];
 
         let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
+            .insert(trie.db.as_ref(), path.clone(), value.clone().into())
             .unwrap();
 
         assert!(matches!(node, Node::Branch(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_choice() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -373,16 +381,16 @@ mod test {
         let value = vec![0x21];
 
         let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
+            .insert(trie.db.as_ref(), path.clone(), value.clone().into())
             .unwrap();
 
         assert!(matches!(node, Node::Branch(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_passthrough() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -396,7 +404,7 @@ mod test {
 
         let new_node = node
             .clone()
-            .insert(&mut trie.state, path.clone(), value.clone())
+            .insert(trie.db.as_ref(), path.clone(), value.clone().into())
             .unwrap();
 
         let new_node = match new_node {
@@ -410,7 +418,7 @@ mod test {
 
     #[test]
     fn remove_choice_into_inner() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -419,7 +427,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x00]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Leaf(_))));
@@ -428,7 +436,7 @@ mod test {
 
     #[test]
     fn remove_choice() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -438,7 +446,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x00]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Branch(_))));
@@ -447,7 +455,7 @@ mod test {
 
     #[test]
     fn remove_choice_into_value() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -455,7 +463,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x00]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Leaf(_))));
@@ -464,7 +472,7 @@ mod test {
 
     #[test]
     fn remove_value_into_inner() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -472,7 +480,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Leaf(_))));
@@ -481,7 +489,7 @@ mod test {
 
     #[test]
     fn remove_value() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -490,7 +498,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Branch(_))));
@@ -499,7 +507,6 @@ mod test {
 
     #[test]
     fn compute_hash_two_choices() {
-        let mut trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 2 => leaf { vec![0, 16] => vec![0x20] },
@@ -518,7 +525,6 @@ mod test {
 
     #[test]
     fn compute_hash_all_choices() {
-        let mut trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0x0 => leaf { vec![0, 16] => vec![0x00] },
@@ -552,7 +558,6 @@ mod test {
 
     #[test]
     fn compute_hash_one_choice_with_value() {
-        let mut trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 2 => leaf { vec![0, 16] => vec![0x20] },
@@ -571,7 +576,6 @@ mod test {
 
     #[test]
     fn compute_hash_all_choices_with_value() {
-        let mut trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             branch {
                 0x0 => leaf { vec![0, 16] => vec![0x00] },
@@ -605,7 +609,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_a() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0,16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -618,7 +621,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_b() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             branch {
                 0 => leaf { vec![0, 16] => vec![0x00] },
@@ -635,7 +637,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_c() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             branch {
                 0x0 => leaf { vec![0, 16] => vec![0x00] },
diff --git a/crates/common/trie/node/extension.rs b/crates/common/trie/node/extension.rs
index 24833fb32c7..fdbcb6d5842 100644
--- a/crates/common/trie/node/extension.rs
+++ b/crates/common/trie/node/extension.rs
@@ -1,37 +1,37 @@
 use ethrex_rlp::structs::Encoder;
 
-use crate::error::TrieError;
 use crate::nibbles::Nibbles;
 use crate::node_hash::NodeHash;
-use crate::state::TrieState;
 use crate::ValueRLP;
+use crate::{error::TrieError, TrieDB};
 
-use super::{BranchNode, Node};
+use super::{BranchNode, Node, NodeRef, ValueOrHash};
 
 /// Extension Node of an an Ethereum Compatible Patricia Merkle Trie
 /// Contains the node's prefix and a its child node hash, doesn't store any value
 #[derive(Debug, Clone, PartialEq)]
 pub struct ExtensionNode {
     pub prefix: Nibbles,
-    pub child: NodeHash,
+    pub child: NodeRef,
 }
 
 impl ExtensionNode {
     /// Creates a new extension node given its child hash and prefix
-    pub(crate) const fn new(prefix: Nibbles, child: NodeHash) -> Self {
+    pub(crate) const fn new(prefix: Nibbles, child: NodeRef) -> Self {
         Self { prefix, child }
     }
 
     /// Retrieves a value from the subtrie originating from this node given its path
-    pub fn get(&self, state: &TrieState, mut path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
+    pub fn get(&self, db: &dyn TrieDB, mut path: Nibbles) -> Result<Option<ValueRLP>, TrieError> {
         // If the path is prefixed by this node's prefix, delegate to its child.
         // Otherwise, no value is present.
         if path.skip_prefix(&self.prefix) {
-            let child_node = state
-                .get_node(self.child)?
+            let child_node = self
+                .child
+                .get_node(db)?
                 .ok_or(TrieError::InconsistentTree)?;
 
-            child_node.get(state, path)
+            child_node.get(db, path)
         } else {
             Ok(None)
         }
@@ -40,9 +40,9 @@ impl ExtensionNode {
     /// Inserts a value into the subtrie originating from this node and returns the new root of the subtrie
     pub fn insert(
         mut self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         path: Nibbles,
-        value: ValueRLP,
+        value: ValueOrHash,
     ) -> Result<Node, TrieError> {
         /* Possible flow paths:
             * Prefix fully matches path
@@ -57,43 +57,42 @@ impl ExtensionNode {
         let match_index = path.count_prefix(&self.prefix);
         if match_index == self.prefix.len() {
             // Insert into child node
-            let child_node = state
-                .get_node(self.child)?
+            let child_node = self
+                .child
+                .get_node(db)?
                 .ok_or(TrieError::InconsistentTree)?;
-            let new_child_node = child_node.insert(state, path.offset(match_index), value)?;
-            self.child = new_child_node.insert_self(state)?;
+            let new_child_node = child_node.insert(db, path.offset(match_index), value)?;
+            self.child = new_child_node.into();
             Ok(self.into())
         } else if match_index == 0 {
             let new_node = if self.prefix.len() == 1 {
                 self.child
             } else {
-                ExtensionNode::new(self.prefix.offset(1), self.child).insert_self(state)?
+                Node::from(ExtensionNode::new(self.prefix.offset(1), self.child)).into()
             };
             let mut choices = BranchNode::EMPTY_CHOICES;
             let branch_node = if self.prefix.at(0) == 16 {
-                match state.get_node(new_node)? {
-                    Some(Node::Leaf(leaf)) => {
-                        BranchNode::new_with_value(Box::new(choices), leaf.value)
-                    }
+                match new_node.get_node(db)? {
+                    Some(Node::Leaf(leaf)) => BranchNode::new_with_value(choices, leaf.value),
                     _ => return Err(TrieError::InconsistentTree),
                 }
             } else {
                 choices[self.prefix.at(0)] = new_node;
-                BranchNode::new(Box::new(choices))
+                BranchNode::new(choices)
             };
-            return branch_node.insert(state, path, value);
+            branch_node.insert(db, path, value)
         } else {
             let new_extension = ExtensionNode::new(self.prefix.offset(match_index), self.child);
-            let new_node = new_extension.insert(state, path.offset(match_index), value)?;
+            let new_node = new_extension.insert(db, path.offset(match_index), value)?;
             self.prefix = self.prefix.slice(0, match_index);
-            self.child = new_node.insert_self(state)?;
+            self.child = new_node.into();
             Ok(self.into())
         }
     }
 
     pub fn remove(
         mut self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
     ) -> Result<(Option<Node>, Option<ValueRLP>), TrieError> {
         /* Possible flow paths:
@@ -106,11 +105,12 @@ impl ExtensionNode {
 
         // Check if the value is part of the child subtrie according to the prefix
         if path.skip_prefix(&self.prefix) {
-            let child_node = state
-                .get_node(self.child)?
+            let child_node = self
+                .child
+                .get_node(db)?
                 .ok_or(TrieError::InconsistentTree)?;
             // Remove value from child subtrie
-            let (child_node, old_value) = child_node.remove(state, path)?;
+            let (child_node, old_value) = child_node.remove(db, path)?;
             // Restructure node based on removal
             let node = match child_node {
                 // If there is no subtrie remove the node
@@ -118,7 +118,7 @@ impl ExtensionNode {
                 Some(node) => Some(match node {
                     // If it is a branch node set it as self's child
                     Node::Branch(branch_node) => {
-                        self.child = branch_node.insert_self(state)?;
+                        self.child = Node::from(branch_node).into();
                         self.into()
                     }
                     // If it is an extension replace self with it after updating its prefix
@@ -151,24 +151,17 @@ impl ExtensionNode {
     pub fn encode_raw(&self) -> Vec<u8> {
         let mut buf = vec![];
         let mut encoder = Encoder::new(&mut buf).encode_bytes(&self.prefix.encode_compact());
-        encoder = self.child.encode(encoder);
+        encoder = self.child.compute_hash().encode(encoder);
         encoder.finish();
         buf
     }
 
-    /// Inserts the node into the state and returns its hash
-    pub fn insert_self(self, state: &mut TrieState) -> Result<NodeHash, TrieError> {
-        let hash = self.compute_hash();
-        state.insert_node(self.into(), hash);
-        Ok(hash)
-    }
-
     /// Traverses own subtrie until reaching the node containing `path`
     /// Appends all encoded nodes traversed to `node_path` (including self)
     /// Only nodes with encoded len over or equal to 32 bytes are included
     pub fn get_path(
         &self,
-        state: &TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
         node_path: &mut Vec<Vec<u8>>,
     ) -> Result<(), TrieError> {
@@ -179,10 +172,11 @@ impl ExtensionNode {
         };
         // Continue to child
         if path.skip_prefix(&self.prefix) {
-            let child_node = state
-                .get_node(self.child)?
+            let child_node = self
+                .child
+                .get_node(db)?
                 .ok_or(TrieError::InconsistentTree)?;
-            child_node.get_path(state, path, node_path)?;
+            child_node.get_path(db, path, node_path)?;
         }
         Ok(())
     }
@@ -203,7 +197,7 @@ mod test {
 
     #[test]
     fn get_some() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -212,18 +206,20 @@ mod test {
         };
 
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x00])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
+                .unwrap(),
             Some(vec![0x12, 0x34, 0x56, 0x78]),
         );
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x01])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x01]))
+                .unwrap(),
             Some(vec![0x34, 0x56, 0x78, 0x9A]),
         );
     }
 
     #[test]
     fn get_none() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -232,14 +228,15 @@ mod test {
         };
 
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x02])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x02]))
+                .unwrap(),
             None,
         );
     }
 
     #[test]
     fn insert_passthrough() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -248,7 +245,11 @@ mod test {
         };
 
         let node = node
-            .insert(&mut trie.state, Nibbles::from_bytes(&[0x02]), vec![])
+            .insert(
+                trie.db.as_ref(),
+                Nibbles::from_bytes(&[0x02]),
+                Vec::new().into(),
+            )
             .unwrap();
         let node = match node {
             Node::Extension(x) => x,
@@ -259,7 +260,7 @@ mod test {
 
     #[test]
     fn insert_branch() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -268,21 +269,26 @@ mod test {
         };
 
         let node = node
-            .insert(&mut trie.state, Nibbles::from_bytes(&[0x10]), vec![0x20])
+            .insert(
+                trie.db.as_ref(),
+                Nibbles::from_bytes(&[0x10]),
+                vec![0x20].into(),
+            )
             .unwrap();
         let node = match node {
             Node::Branch(x) => x,
             _ => panic!("expected a branch node"),
         };
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x10])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x10]))
+                .unwrap(),
             Some(vec![0x20])
         );
     }
 
     #[test]
     fn insert_branch_extension() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0, 0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -291,21 +297,26 @@ mod test {
         };
 
         let node = node
-            .insert(&mut trie.state, Nibbles::from_bytes(&[0x10]), vec![0x20])
+            .insert(
+                trie.db.as_ref(),
+                Nibbles::from_bytes(&[0x10]),
+                vec![0x20].into(),
+            )
             .unwrap();
         let node = match node {
             Node::Branch(x) => x,
             _ => panic!("expected a branch node"),
         };
         assert_eq!(
-            node.get(&trie.state, Nibbles::from_bytes(&[0x10])).unwrap(),
+            node.get(trie.db.as_ref(), Nibbles::from_bytes(&[0x10]))
+                .unwrap(),
             Some(vec![0x20])
         );
     }
 
     #[test]
     fn insert_extension_branch() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0, 0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -317,16 +328,16 @@ mod test {
         let value = vec![0x02];
 
         let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
+            .insert(trie.db.as_ref(), path.clone(), value.clone().into())
             .unwrap();
 
         assert!(matches!(node, Node::Extension(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_extension_branch_extension() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0, 0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -338,16 +349,16 @@ mod test {
         let value = vec![0x04];
 
         let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
+            .insert(trie.db.as_ref(), path.clone(), value.clone().into())
             .unwrap();
 
         assert!(matches!(node, Node::Extension(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn remove_none() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x00] },
@@ -356,7 +367,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x02]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x02]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Extension(_))));
@@ -365,7 +376,7 @@ mod test {
 
     #[test]
     fn remove_into_leaf() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x00] },
@@ -374,7 +385,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x01]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x01]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Leaf(_))));
@@ -383,7 +394,7 @@ mod test {
 
     #[test]
     fn remove_into_extension() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x00] },
@@ -395,7 +406,7 @@ mod test {
         };
 
         let (node, value) = node
-            .remove(&mut trie.state, Nibbles::from_bytes(&[0x00]))
+            .remove(trie.db.as_ref(), Nibbles::from_bytes(&[0x00]))
             .unwrap();
 
         assert!(matches!(node, Some(Node::Extension(_))));
@@ -416,10 +427,13 @@ mod test {
         let leaf_node_a = LeafNode::new(Nibbles::from_hex(vec![0, 16]), vec![0x12, 0x34]);
         let leaf_node_b = LeafNode::new(Nibbles::from_hex(vec![0, 16]), vec![0x56, 0x78]);
         let mut choices = BranchNode::EMPTY_CHOICES;
-        choices[0] = leaf_node_a.compute_hash();
-        choices[1] = leaf_node_b.compute_hash();
-        let branch_node = BranchNode::new(Box::new(choices));
-        let node = ExtensionNode::new(Nibbles::from_hex(vec![0, 0]), branch_node.compute_hash());
+        choices[0] = leaf_node_a.compute_hash().into();
+        choices[1] = leaf_node_b.compute_hash().into();
+        let branch_node = BranchNode::new(choices);
+        let node = ExtensionNode::new(
+            Nibbles::from_hex(vec![0, 0]),
+            branch_node.compute_hash().into(),
+        );
 
         assert_eq!(
             node.compute_hash().as_ref(),
@@ -451,10 +465,13 @@ mod test {
             vec![0x34, 0x56, 0x78, 0x9A, 0xBC],
         );
         let mut choices = BranchNode::EMPTY_CHOICES;
-        choices[0] = leaf_node_a.compute_hash();
-        choices[1] = leaf_node_b.compute_hash();
-        let branch_node = BranchNode::new(Box::new(choices));
-        let node = ExtensionNode::new(Nibbles::from_hex(vec![0, 0]), branch_node.compute_hash());
+        choices[0] = leaf_node_a.compute_hash().into();
+        choices[1] = leaf_node_b.compute_hash().into();
+        let branch_node = BranchNode::new(choices);
+        let node = ExtensionNode::new(
+            Nibbles::from_hex(vec![0, 0]),
+            branch_node.compute_hash().into(),
+        );
 
         assert_eq!(
             node.compute_hash().as_ref(),
@@ -468,7 +485,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_a() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x12, 0x34, 0x56, 0x78] },
@@ -481,7 +497,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_b() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x00] },
@@ -498,7 +513,6 @@ mod test {
 
     #[test]
     fn symmetric_encoding_c() {
-        let mut trie = Trie::new_temp();
         let node: Node = pmt_node! { @(trie)
             extension { [0], branch {
                 0 => leaf { vec![16] => vec![0x00] },
diff --git a/crates/common/trie/node/leaf.rs b/crates/common/trie/node/leaf.rs
index bcac4c42220..6f731eecf4d 100644
--- a/crates/common/trie/node/leaf.rs
+++ b/crates/common/trie/node/leaf.rs
@@ -1,11 +1,8 @@
 use ethrex_rlp::structs::Encoder;
 
-use crate::{
-    error::TrieError, nibbles::Nibbles, node::BranchNode, node_hash::NodeHash, state::TrieState,
-    ValueRLP,
-};
+use crate::{error::TrieError, nibbles::Nibbles, node::BranchNode, node_hash::NodeHash, ValueRLP};
 
-use super::{ExtensionNode, Node};
+use super::{ExtensionNode, Node, ValueOrHash};
 /// Leaf Node of an an Ethereum Compatible Patricia Merkle Trie
 /// Contains the node's hash, value & path
 #[derive(Debug, Clone, Default, PartialEq)]
@@ -30,12 +27,7 @@ impl LeafNode {
     }
 
     /// Stores the received value and returns the new root of the subtrie previously consisting of self
-    pub fn insert(
-        mut self,
-        state: &mut TrieState,
-        path: Nibbles,
-        value: ValueRLP,
-    ) -> Result<Node, TrieError> {
+    pub fn insert(mut self, path: Nibbles, value: ValueOrHash) -> Result<Node, TrieError> {
         /* Possible flow paths:
             Leaf { SelfValue } -> Leaf { Value }
             Leaf { SelfValue } -> Extension { Branch { [Self,...] Value } }
@@ -44,7 +36,14 @@ impl LeafNode {
         */
         // If the path matches the stored path, update the value and return self
         if self.partial == path {
-            self.value = value;
+            match value {
+                ValueOrHash::Value(value) => self.value = value,
+                ValueOrHash::Hash(_) => {
+                    return Err(TrieError::Verify(
+                        "attempt to override proof node with external hash".to_string(),
+                    ));
+                }
+            }
             Ok(self.into())
         } else {
             let match_index = path.count_prefix(&self.partial);
@@ -56,25 +55,39 @@ impl LeafNode {
                 // Create a new leaf node and store the value in it
                 // Create a new branch node with the leaf as a child and store self's value
                 // Branch { [ Leaf { Value } , ... ], SelfValue}
-                let new_leaf = LeafNode::new(path.offset(match_index + 1), value);
                 let mut choices = BranchNode::EMPTY_CHOICES;
-                choices[new_leaf_choice_idx] = new_leaf.insert_self(state)?;
-                BranchNode::new_with_value(Box::new(choices), self.value)
+                choices[new_leaf_choice_idx] = match value {
+                    ValueOrHash::Value(value) => {
+                        Node::from(LeafNode::new(path.offset(match_index + 1), value)).into()
+                    }
+                    ValueOrHash::Hash(hash) => hash.into(),
+                };
+                BranchNode::new_with_value(choices, self.value)
             } else if new_leaf_choice_idx == 16 {
                 // Create a branch node with self as a child and store the value in the branch node
                 // Branch { [Self,...], Value }
                 let mut choices = BranchNode::EMPTY_CHOICES;
-                choices[self_choice_idx] = self.insert_self(state)?;
-                BranchNode::new_with_value(Box::new(choices), value)
+                choices[self_choice_idx] = Node::from(self).into();
+                BranchNode::new_with_value(
+                    choices,
+                    match value {
+                        ValueOrHash::Value(value) => value,
+                        ValueOrHash::Hash(_) => todo!("handle override case (error?)"),
+                    },
+                )
             } else {
                 // Create a new leaf node and store the path and value in it
                 // Create a new branch node with the leaf and self as children
                 // Branch { [ Leaf { Path, Value }, Self, ... ], None, None}
-                let new_leaf = LeafNode::new(path.offset(match_index + 1), value);
                 let mut choices = BranchNode::EMPTY_CHOICES;
-                choices[new_leaf_choice_idx] = new_leaf.insert_self(state)?;
-                choices[self_choice_idx] = self.insert_self(state)?;
-                BranchNode::new(Box::new(choices))
+                choices[new_leaf_choice_idx] = match value {
+                    ValueOrHash::Value(value) => {
+                        Node::from(LeafNode::new(path.offset(match_index + 1), value)).into()
+                    }
+                    ValueOrHash::Hash(hash) => hash.into(),
+                };
+                choices[self_choice_idx] = Node::from(self).into();
+                BranchNode::new(choices)
             };
 
             let final_node = if match_index == 0 {
@@ -82,7 +95,7 @@ impl LeafNode {
             } else {
                 // Create an extension node with the branch node as child
                 // Extension { BranchNode }
-                ExtensionNode::new(path.slice(0, match_index), branch_node.insert_self(state)?)
+                ExtensionNode::new(path.slice(0, match_index), Node::from(branch_node).into())
                     .into()
             };
 
@@ -114,14 +127,6 @@ impl LeafNode {
         buf
     }
 
-    /// Inserts the node into the state and returns its hash
-    /// Receives the offset that needs to be traversed to reach the leaf node from the canonical root, used to compute the node hash
-    pub fn insert_self(self, state: &mut TrieState) -> Result<NodeHash, TrieError> {
-        let hash = self.compute_hash();
-        state.insert_node(self.into(), hash);
-        Ok(hash)
-    }
-
     /// Encodes the node and appends it to `node_path` if the encoded node is 32 or more bytes long
     pub fn get_path(&self, node_path: &mut Vec<Vec<u8>>) -> Result<(), TrieError> {
         let encoded = self.encode_raw();
@@ -166,13 +171,12 @@ mod test {
 
     #[test]
     fn insert_replace() {
-        let mut trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             leaf { vec![1,2,16] => vec![0x12, 0x34, 0x56, 0x78] }
         };
 
         let node = node
-            .insert(&mut trie.state, Nibbles::from_bytes(&[0x12]), vec![0x13])
+            .insert(Nibbles::from_bytes(&[0x12]), vec![0x13].into())
             .unwrap();
         let node = match node {
             Node::Leaf(x) => x,
@@ -184,25 +188,23 @@ mod test {
 
     #[test]
     fn insert_branch() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             leaf { vec![1,2,16] => vec![0x12, 0x34, 0x56, 0x78] }
         };
         let path = Nibbles::from_bytes(&[0x22]);
         let value = vec![0x23];
-        let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
-            .unwrap();
+        let node = node.insert(path.clone(), value.clone().into()).unwrap();
         let node = match node {
             Node::Branch(x) => x,
             _ => panic!("expected a branch node"),
         };
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_extension_branch() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             leaf { vec![1,2,16] => vec![0x12, 0x34, 0x56, 0x78] }
         };
@@ -210,17 +212,15 @@ mod test {
         let path = Nibbles::from_bytes(&[0x13]);
         let value = vec![0x15];
 
-        let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
-            .unwrap();
+        let node = node.insert(path.clone(), value.clone().into()).unwrap();
 
         assert!(matches!(node, Node::Extension(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_extension_branch_value_self() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             leaf { vec![1,2,16] => vec![0x12, 0x34, 0x56, 0x78] }
         };
@@ -228,17 +228,15 @@ mod test {
         let path = Nibbles::from_bytes(&[0x12, 0x34]);
         let value = vec![0x17];
 
-        let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
-            .unwrap();
+        let node = node.insert(path.clone(), value.clone().into()).unwrap();
 
         assert!(matches!(node, Node::Extension(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     #[test]
     fn insert_extension_branch_value_other() {
-        let mut trie = Trie::new_temp();
+        let trie = Trie::new_temp();
         let node = pmt_node! { @(trie)
             leaf { vec![1, 2, 3, 4, 16] => vec![0x12, 0x34, 0x56, 0x78] }
         };
@@ -246,12 +244,10 @@ mod test {
         let path = Nibbles::from_bytes(&[0x12]);
         let value = vec![0x17];
 
-        let node = node
-            .insert(&mut trie.state, path.clone(), value.clone())
-            .unwrap();
+        let node = node.insert(path.clone(), value.clone().into()).unwrap();
 
         assert!(matches!(node, Node::Extension(_)));
-        assert_eq!(node.get(&trie.state, path).unwrap(), Some(value));
+        assert_eq!(node.get(trie.db.as_ref(), path).unwrap(), Some(value));
     }
 
     // An insertion that returns branch [value=(x)] -> leaf (y) is not possible because of the path
diff --git a/crates/common/trie/rlp.rs b/crates/common/trie/rlp.rs
index c9c1be9fd2c..7757706da41 100644
--- a/crates/common/trie/rlp.rs
+++ b/crates/common/trie/rlp.rs
@@ -8,6 +8,7 @@ use ethrex_rlp::{
 };
 
 use super::node::{BranchNode, ExtensionNode, LeafNode, Node};
+use crate::{node::NodeRef, NodeHash};
 
 enum NodeType {
     Branch = 0,
@@ -30,7 +31,13 @@ impl RLPEncode for BranchNode {
     fn encode(&self, buf: &mut dyn bytes::BufMut) {
         // TODO: choices encoded as vec due to conflicting trait impls for [T;N] & [u8;N], check if we can fix this later
         Encoder::new(buf)
-            .encode_field(&self.choices.to_vec())
+            .encode_field(
+                &self
+                    .choices
+                    .iter()
+                    .map(|x| x.compute_hash())
+                    .collect::<Vec<_>>(),
+            )
             .encode_field(&self.value)
             .finish()
     }
@@ -40,7 +47,7 @@ impl RLPEncode for ExtensionNode {
     fn encode(&self, buf: &mut dyn bytes::BufMut) {
         Encoder::new(buf)
             .encode_field(&self.prefix)
-            .encode_field(&self.child)
+            .encode_field(&self.child.compute_hash())
             .finish()
     }
 }
@@ -59,7 +66,8 @@ impl RLPDecode for BranchNode {
         const CHOICES_LEN_ERROR_MSG: &str =
             "Error decoding field 'choices' of type [H256;16]: Invalid Length";
         let decoder = Decoder::new(rlp)?;
-        let (choices, decoder) = decoder.decode_field::<Vec<_>>("choices")?;
+        let (choices, decoder) = decoder.decode_field::<Vec<NodeHash>>("choices")?;
+        let choices = choices.into_iter().map(NodeRef::Hash).collect::<Vec<_>>();
         let choices = choices
             .try_into()
             .map_err(|_| RLPDecodeError::Custom(CHOICES_LEN_ERROR_MSG.to_string()))?;
@@ -73,7 +81,13 @@ impl RLPDecode for ExtensionNode {
         let decoder = Decoder::new(rlp)?;
         let (prefix, decoder) = decoder.decode_field("prefix")?;
         let (child, decoder) = decoder.decode_field("child")?;
-        Ok((Self { prefix, child }, decoder.finish()?))
+        Ok((
+            Self {
+                prefix,
+                child: NodeRef::Hash(child),
+            },
+            decoder.finish()?,
+        ))
     }
 }
 
diff --git a/crates/common/trie/state.rs b/crates/common/trie/state.rs
deleted file mode 100644
index be779d1fb8f..00000000000
--- a/crates/common/trie/state.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-use std::collections::HashMap;
-
-use crate::error::TrieError;
-use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode};
-
-use super::db::TrieDB;
-
-/// Database representing the trie state
-/// It contains a table mapping node hashes to rlp encoded nodes
-/// All nodes are stored in the DB and no node is ever removed
-use super::{node::Node, node_hash::NodeHash};
-pub struct TrieState {
-    db: Box<dyn TrieDB>,
-    cache: HashMap<NodeHash, Node>,
-}
-
-impl TrieState {
-    /// Creates a TrieState referring to a db.
-    pub fn new(db: Box<dyn TrieDB>) -> TrieState {
-        TrieState {
-            db,
-            cache: Default::default(),
-        }
-    }
-
-    /// Retrieves a node based on its hash
-    pub fn get_node(&self, hash: NodeHash) -> Result<Option<Node>, TrieError> {
-        // Decode the node if it is inlined
-        if let NodeHash::Inline(_) = hash {
-            return Ok(Some(Node::decode_raw(hash.as_ref())?));
-        }
-        if let Some(node) = self.cache.get(&hash) {
-            return Ok(Some(node.clone()));
-        };
-        self.db
-            .get(hash)?
-            .map(|rlp| Node::decode(&rlp).map_err(TrieError::RLPDecode))
-            .transpose()
-    }
-
-    /// Inserts a node
-    pub fn insert_node(&mut self, node: Node, hash: NodeHash) {
-        // Don't insert the node if it is already inlined on the parent
-        if matches!(hash, NodeHash::Hashed(_)) {
-            self.cache.insert(hash, node);
-        }
-    }
-
-    /// Commits cache changes to DB and clears it
-    /// Only writes nodes that follow the root's canonical trie
-    pub fn commit(&mut self, root: &NodeHash) -> Result<(), TrieError> {
-        self.commit_node(root)?;
-        self.cache.clear();
-        Ok(())
-    }
-
-    // Writes a node and its children into the DB
-    fn commit_node(&mut self, node_hash: &NodeHash) -> Result<(), TrieError> {
-        let mut to_commit = vec![];
-        self.commit_node_tail_recursive(node_hash, &mut to_commit)?;
-
-        self.db.put_batch(to_commit)?;
-
-        Ok(())
-    }
-
-    // Writes a node and its children into the DB
-    fn commit_node_tail_recursive(
-        &mut self,
-        node_hash: &NodeHash,
-        acc: &mut Vec<(NodeHash, Vec<u8>)>,
-    ) -> Result<(), TrieError> {
-        let Some(node) = self.cache.remove(node_hash) else {
-            // If the node is not in the cache then it means it is already stored in the DB
-            return Ok(());
-        };
-        // Commit children (if any)
-        match &node {
-            Node::Branch(n) => {
-                for child in n.choices.iter() {
-                    if child.is_valid() {
-                        self.commit_node_tail_recursive(child, acc)?;
-                    }
-                }
-            }
-            Node::Extension(n) => self.commit_node_tail_recursive(&n.child, acc)?,
-            Node::Leaf(_) => {}
-        }
-        // Commit self
-        acc.push((*node_hash, node.encode_to_vec()));
-
-        Ok(())
-    }
-
-    /// Writes a node directly to the DB bypassing the cache
-    pub fn write_node(&mut self, node: Node, hash: NodeHash) -> Result<(), TrieError> {
-        // Don't insert the node if it is already inlined on the parent
-        if matches!(hash, NodeHash::Hashed(_)) {
-            self.db.put(hash, node.encode_to_vec())?;
-        }
-        Ok(())
-    }
-
-    /// Writes a node batch directly to the DB bypassing the cache
-    pub fn write_node_batch(&mut self, nodes: &[Node]) -> Result<(), TrieError> {
-        // Don't insert the node if it is already inlined on the parent
-        let key_values = nodes
-            .iter()
-            .filter_map(|node| {
-                let hash = node.compute_hash();
-                matches!(hash, NodeHash::Hashed(_)).then(|| (hash, node.encode_to_vec()))
-            })
-            .collect();
-        self.db.put_batch(key_values)?;
-        Ok(())
-    }
-}
diff --git a/crates/common/trie/test_utils.rs b/crates/common/trie/test_utils.rs
index 369a9e2b8d5..2094a13cf75 100644
--- a/crates/common/trie/test_utils.rs
+++ b/crates/common/trie/test_utils.rs
@@ -16,9 +16,9 @@ macro_rules! pmt_node {
                     $child_type { $( $child_tokens )* }
                     offset offset
                 }.into();
-                choices[$choice as usize] = child_node.insert_self(&mut $trie.state).unwrap();
+                choices[$choice as usize] = child_node.into();
             )*
-            Box::new(choices)
+            choices
         })
     };
     (
@@ -36,9 +36,9 @@ macro_rules! pmt_node {
                     pmt_node! { @($trie)
                         $child_type { $( $child_tokens )* }
                         offset offset
-                    }).insert_self(&mut $trie.state).unwrap();
+                    }).into();
             )*
-            Box::new(choices)
+            choices
         }, $value)
     }};
 
@@ -56,7 +56,7 @@ macro_rules! pmt_node {
                 let child_node = $crate::node::Node::from(pmt_node! { @($trie)
                     $child_type { $( $child_tokens )* }
                 });
-                child_node.insert_self(&mut $trie.state).unwrap()
+                child_node.into()
             }
         )
     }};
diff --git a/crates/common/trie/trie.rs b/crates/common/trie/trie.rs
index aefa56d9c05..5f9add2f96e 100644
--- a/crates/common/trie/trie.rs
+++ b/crates/common/trie/trie.rs
@@ -4,7 +4,6 @@ mod nibbles;
 mod node;
 mod node_hash;
 mod rlp;
-mod state;
 #[cfg(test)]
 mod test_utils;
 mod trie_iter;
@@ -12,16 +11,20 @@ mod verify_range;
 use ethereum_types::H256;
 use ethrex_rlp::constants::RLP_NULL;
 use sha3::{Digest, Keccak256};
-use std::collections::HashSet;
+use std::collections::{HashMap, HashSet};
 
 pub use self::db::{InMemoryTrieDB, TrieDB};
 pub use self::nibbles::Nibbles;
 pub use self::verify_range::verify_range;
-pub use self::{node::Node, node_hash::NodeHash, state::TrieState};
+pub use self::{
+    node::{Node, NodeRef},
+    node_hash::NodeHash,
+};
 
 pub use self::error::TrieError;
 use self::{node::LeafNode, trie_iter::TrieIterator};
 
+use ethrex_rlp::decode::RLPDecode;
 use lazy_static::lazy_static;
 
 lazy_static! {
@@ -43,80 +46,87 @@ pub type NodeRLP = Vec<u8>;
 
 /// Libmdx-based Ethereum Compatible Merkle Patricia Trie
 pub struct Trie {
-    /// Hash of the current node
-    root: Option<NodeHash>,
-    /// Contains the trie's nodes
-    pub(crate) state: TrieState,
+    db: Box<dyn TrieDB>,
+    root: NodeRef,
 }
 
 impl Trie {
     /// Creates a new Trie from a clean DB
     pub fn new(db: Box<dyn TrieDB>) -> Self {
         Self {
-            state: TrieState::new(db),
-            root: None,
+            db,
+            root: NodeRef::default(),
         }
     }
 
     /// Creates a trie from an already-initialized DB and sets root as the root node of the trie
     pub fn open(db: Box<dyn TrieDB>, root: H256) -> Self {
-        let root = (root != *EMPTY_TRIE_HASH).then_some(root.into());
         Self {
-            state: TrieState::new(db),
-            root,
+            db,
+            root: (root != *EMPTY_TRIE_HASH)
+                .then_some(NodeHash::from(root).into())
+                .unwrap_or_default(),
         }
     }
 
+    /// Return a reference to the internal database.
+    ///
+    /// Warning: All changes made to the db will bypass the trie and may cause the trie to suddenly
+    ///   become inconsistent.
+    pub fn db(&self) -> &dyn TrieDB {
+        self.db.as_ref()
+    }
+
     /// Retrieve an RLP-encoded value from the trie given its RLP-encoded path.
     pub fn get(&self, path: &PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        if let Some(root) = &self.root {
-            let root_node = self
-                .state
-                .get_node(*root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            root_node.get(&self.state, Nibbles::from_bytes(path))
-        } else {
-            Ok(None)
-        }
+        Ok(match self.root {
+            NodeRef::Node(ref node, _) => node.get(self.db.as_ref(), Nibbles::from_bytes(path))?,
+            NodeRef::Hash(hash) if hash.is_valid() => {
+                Node::decode(&self.db.get(hash)?.ok_or(TrieError::InconsistentTree)?)
+                    .map_err(TrieError::RLPDecode)?
+                    .get(self.db.as_ref(), Nibbles::from_bytes(path))?
+            }
+            _ => None,
+        })
     }
 
     /// Insert an RLP-encoded value into the trie.
     pub fn insert(&mut self, path: PathRLP, value: ValueRLP) -> Result<(), TrieError> {
-        let root = self.root.take();
-        if let Some(root_node) = root
-            .map(|root| self.state.get_node(root))
-            .transpose()?
-            .flatten()
-        {
-            // If the trie is not empty, call the root node's insertion logic
-            let root_node = root_node.insert(&mut self.state, Nibbles::from_bytes(&path), value)?;
-            self.root = Some(root_node.insert_self(&mut self.state)?)
+        let path = Nibbles::from_bytes(&path);
+
+        self.root = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.db.as_ref(), path, value)?
+                .into()
         } else {
             // If the trie is empty, just add a leaf.
-            let new_leaf = Node::from(LeafNode::new(Nibbles::from_bytes(&path), value));
-            self.root = Some(new_leaf.insert_self(&mut self.state)?)
-        }
+            Node::from(LeafNode::new(path, value)).into()
+        };
+
         Ok(())
     }
 
     /// Remove a value from the trie given its RLP-encoded path.
     /// Returns the value if it was succesfully removed or None if it wasn't part of the trie
     pub fn remove(&mut self, path: PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        let root = self.root.take();
-        if let Some(root) = root {
-            let root_node = self
-                .state
-                .get_node(root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            let (root_node, old_value) =
-                root_node.remove(&mut self.state, Nibbles::from_bytes(&path))?;
-            self.root = root_node
-                .map(|root| root.insert_self(&mut self.state))
-                .transpose()?;
-            Ok(old_value)
+        let value;
+        (self.root, value) = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's removal logic.
+            let (node, value) = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .remove(self.db.as_ref(), Nibbles::from_bytes(&path))?;
+
+            (node.map(Into::into).unwrap_or_default(), value)
         } else {
-            Ok(None)
-        }
+            (NodeRef::default(), None)
+        };
+
+        Ok(value)
     }
 
     /// Return the hash of the trie's root node.
@@ -124,46 +134,59 @@ impl Trie {
     /// Also commits changes to the DB
     pub fn hash(&mut self) -> Result<H256, TrieError> {
         self.commit()?;
-        Ok(self
-            .root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH))
+        Ok(self.hash_no_commit())
     }
 
     /// Return the hash of the trie's root node.
     /// Returns keccak(RLP_NULL) if the trie is empty
     pub fn hash_no_commit(&self) -> H256 {
-        self.root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH)
+        if self.root.is_valid() {
+            self.root.compute_hash().finalize()
+        } else {
+            *EMPTY_TRIE_HASH
+        }
     }
 
+    /// Compute the hash of the root node and flush any changes into the database.
+    ///
+    /// This method will also compute the hash of all internal nodes indirectly. It will not clear
+    /// the cached nodes.
     pub fn commit(&mut self) -> Result<(), TrieError> {
-        if let Some(ref root) = self.root {
-            self.state.commit(root)?;
+        if self.root.is_valid() {
+            let mut acc = Vec::new();
+            self.root.commit(&mut acc);
+            self.db.put_batch(acc)?;
         }
+
         Ok(())
     }
 
     /// Obtain a merkle proof for the given path.
     /// The proof will contain all the encoded nodes traversed until reaching the node where the path is stored (including this last node).
     /// The proof will still be constructed even if the path is not stored in the trie, proving its absence.
+    ///
+    /// Note: This method has a different behavior in regard to non-existent trie root nodes. Normal
+    ///   behavior is to return `Err(InconsistentTrie)`, but this method will return
+    ///   `Ok(Vec::new())` instead.
     pub fn get_proof(&self, path: &PathRLP) -> Result<Vec<NodeRLP>, TrieError> {
-        // Will store all the encoded nodes traversed until reaching the node containing the path
-        let mut node_path = Vec::new();
-        let Some(root) = &self.root else {
-            return Ok(node_path);
-        };
-        // If the root is inlined, add it to the node_path
-        if let NodeHash::Inline(_) = root {
-            node_path.push(root.as_ref().to_vec());
-        }
-        if let Some(root_node) = self.state.get_node(*root)? {
-            root_node.get_path(&self.state, Nibbles::from_bytes(path), &mut node_path)?;
+        if self.root.is_valid() {
+            let hash = self.root.compute_hash();
+
+            let mut node_path = Vec::new();
+            if let NodeHash::Inline((data, len)) = hash {
+                node_path.push(data[..len as usize].to_vec());
+            }
+
+            let root = match self.root.get_node(self.db.as_ref())? {
+                Some(x) => x,
+                None => return Ok(Vec::new()),
+            };
+            root.get_path(self.db.as_ref(), Nibbles::from_bytes(path), &mut node_path)?;
+
+            Ok(node_path)
+        } else {
+            Ok(Vec::new())
         }
-        Ok(node_path)
     }
 
     /// Obtains all encoded nodes traversed until reaching the node where every path is stored.
@@ -173,48 +196,88 @@ impl Trie {
         &self,
         paths: &[PathRLP],
     ) -> Result<(Option<NodeRLP>, Vec<NodeRLP>), TrieError> {
-        let Some(root_node) = self
-            .root
-            .as_ref()
-            .map(|root| self.state.get_node(*root))
-            .transpose()?
-            .flatten()
-        else {
-            return Ok((None, Vec::new()));
-        };
+        if self.root.is_valid() {
+            let encoded_root = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .encode_raw();
+
+            let mut node_path = HashSet::new();
+            for path in paths {
+                let mut nodes = self.get_proof(path)?;
+                nodes.swap_remove(0);
+                node_path.extend(nodes);
+            }
 
-        let mut node_path = Vec::new();
-        for path in paths {
-            let mut nodes = self.get_proof(path)?;
-            nodes.swap_remove(0);
-            node_path.extend(nodes); // skip root node
+            Ok((Some(encoded_root), node_path.into_iter().collect()))
+        } else {
+            Ok((None, Vec::new()))
         }
-
-        // dedup
-        // TODO: really inefficient, by making the traversing smarter we can avoid having
-        // duplicates
-        let node_path: HashSet<_> = node_path.into_iter().collect();
-        let node_path = Vec::from_iter(node_path);
-        Ok((Some(root_node.encode_raw()), node_path))
     }
 
-    /// Creates a cached Trie (with [NullTrieDB]) from a list of encoded nodes.
-    /// Generally used in conjuction with [Trie::get_proofs].
-    pub fn from_nodes(
-        root_node: Option<&NodeRLP>,
-        other_nodes: &[NodeRLP],
-    ) -> Result<Self, TrieError> {
-        let mut trie = Trie::stateless();
+    /// Builds a trie from a set of nodes.
+    ///
+    /// Note: This method will not ensure that all node references are valid. Invalid references
+    ///   will cause other methods (including, but not limited to `Trie::get`, `Trie::insert` and
+    ///   `Trie::remove`) to return `Err(InconsistentTrie)`.
+    /// Note: This method will ignore any dangling nodes. All nodes that are not accessible from the
+    ///   root node are considered dangling.
+    pub fn from_nodes(root: Option<&NodeRLP>, nodes: &[NodeRLP]) -> Result<Self, TrieError> {
+        let Some(root) = root else {
+            return Ok(Trie::stateless());
+        };
 
-        if let Some(root_node) = root_node {
-            let root_node = Node::decode_raw(root_node)?;
-            trie.root = Some(root_node.insert_self(&mut trie.state)?);
-        }
+        let mut storage = nodes
+            .iter()
+            .map(|node| {
+                (
+                    NodeHash::from_slice(&Keccak256::new_with_prefix(node).finalize()),
+                    node,
+                )
+            })
+            .collect::<HashMap<_, _>>();
+
+        fn inner(
+            storage: &mut HashMap<NodeHash, &Vec<u8>>,
+            node: &NodeRLP,
+        ) -> Result<Node, TrieError> {
+            Ok(match Node::decode_raw(node)? {
+                Node::Branch(mut node) => {
+                    for choice in &mut node.choices {
+                        let NodeRef::Hash(hash) = *choice else {
+                            unreachable!()
+                        };
+
+                        if hash.is_valid() {
+                            *choice = match storage.remove(&hash) {
+                                Some(rlp) => inner(storage, rlp)?.into(),
+                                None => hash.into(),
+                            };
+                        }
+                    }
+
+                    node.into()
+                }
+                Node::Extension(mut node) => {
+                    let NodeRef::Hash(hash) = node.child else {
+                        unreachable!()
+                    };
+
+                    node.child = match storage.remove(&hash) {
+                        Some(rlp) => inner(storage, rlp)?.into(),
+                        None => hash.into(),
+                    };
 
-        for node in other_nodes.iter().map(|node| Node::decode_raw(node)) {
-            node?.insert_self(&mut trie.state)?;
+                    node.into()
+                }
+                Node::Leaf(node) => node.into(),
+            })
         }
 
+        let mut trie = Trie::stateless();
+        trie.root = inner(&mut storage, root)?.into();
+
         Ok(trie)
     }
 
@@ -227,10 +290,8 @@ impl Trie {
             // Unwraping here won't panic as our in_memory trie DB won't fail
             trie.insert(path, value).unwrap();
         }
-        trie.root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH)
+
+        trie.hash_no_commit()
     }
 
     /// Creates a new stateless trie. This trie won't be able to store any nodes so all data will be lost after calculating the hash
@@ -269,67 +330,58 @@ impl Trie {
             _ => return Ok(vec![]),
         };
 
-        // Fetch node
-        let Some(root_node) = self
-            .root
-            .as_ref()
-            .map(|root| self.state.get_node(*root))
-            .transpose()?
-            .flatten()
-        else {
-            return Ok(vec![]);
-        };
-        self.get_node_inner(root_node, partial_path)
-    }
-
-    fn get_node_inner(&self, node: Node, mut partial_path: Nibbles) -> Result<Vec<u8>, TrieError> {
-        // If we reached the end of the partial path, return the current node
-        if partial_path.is_empty() {
-            return Ok(node.encode_raw());
-        }
-        match node {
-            Node::Branch(branch_node) => match partial_path.next_choice() {
-                Some(idx) => {
-                    let child_hash = &branch_node.choices[idx];
-                    if child_hash.is_valid() {
-                        let child_node = self
-                            .state
-                            .get_node(*child_hash)?
+        fn get_node_inner(
+            db: &dyn TrieDB,
+            node: Node,
+            mut partial_path: Nibbles,
+        ) -> Result<Vec<u8>, TrieError> {
+            // If we reached the end of the partial path, return the current node
+            if partial_path.is_empty() {
+                return Ok(node.encode_raw());
+            }
+            match node {
+                Node::Branch(branch_node) => match partial_path.next_choice() {
+                    Some(idx) => {
+                        let child_ref = &branch_node.choices[idx];
+                        if child_ref.is_valid() {
+                            let child_node =
+                                child_ref.get_node(db)?.ok_or(TrieError::InconsistentTree)?;
+                            get_node_inner(db, child_node, partial_path)
+                        } else {
+                            Ok(vec![])
+                        }
+                    }
+                    _ => Ok(vec![]),
+                },
+                Node::Extension(extension_node) => {
+                    if partial_path.skip_prefix(&extension_node.prefix)
+                        && extension_node.child.is_valid()
+                    {
+                        let child_node = extension_node
+                            .child
+                            .get_node(db)?
                             .ok_or(TrieError::InconsistentTree)?;
-                        self.get_node_inner(child_node, partial_path)
+                        get_node_inner(db, child_node, partial_path)
                     } else {
                         Ok(vec![])
                     }
                 }
-                _ => Ok(vec![]),
-            },
-            Node::Extension(extension_node) => {
-                if partial_path.skip_prefix(&extension_node.prefix)
-                    && extension_node.child.is_valid()
-                {
-                    let child_node = self
-                        .state
-                        .get_node(extension_node.child)?
-                        .ok_or(TrieError::InconsistentTree)?;
-                    self.get_node_inner(child_node, partial_path)
-                } else {
-                    Ok(vec![])
-                }
+                Node::Leaf(_) => Ok(vec![]),
             }
-            Node::Leaf(_) => Ok(vec![]),
         }
-    }
-
-    /// Returns a mutable reference to the trie's internal node state
-    /// [WARNING] This will allow directly manipulating the trie's state and
-    /// may lead to inconsistent trie structures if not used resposibly
-    pub fn state_mut(&mut self) -> &mut TrieState {
-        &mut self.state
-    }
 
-    /// Returns a reference to the trie's internal node state
-    pub fn state(&mut self) -> &TrieState {
-        &self.state
+        // Fetch node
+        if self.root.is_valid() {
+            get_node_inner(
+                self.db.as_ref(),
+                self.root
+                    .get_node(self.db.as_ref())?
+                    .ok_or(TrieError::InconsistentTree)?,
+                partial_path,
+            )
+        } else {
+            Ok(Vec::new())
+        }
     }
 
     #[cfg(test)]
@@ -356,6 +408,40 @@ impl IntoIterator for Trie {
     }
 }
 
+pub struct ProofTrie(Trie);
+
+impl ProofTrie {
+    pub fn insert(
+        &mut self,
+        partial_path: Nibbles,
+        external_ref: NodeHash,
+    ) -> Result<(), TrieError> {
+        self.0.root = if self.0.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.0
+                .root
+                .get_node(self.0.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.0.db.as_ref(), partial_path, external_ref)?
+                .into()
+        } else {
+            external_ref.into()
+        };
+
+        Ok(())
+    }
+
+    pub fn hash(&self) -> H256 {
+        self.0.hash_no_commit()
+    }
+}
+
+impl From<Trie> for ProofTrie {
+    fn from(value: Trie) -> Self {
+        Self(value)
+    }
+}
+
 #[cfg(test)]
 mod test {
     use cita_trie::{MemoryDB as CitaMemoryDB, PatriciaTrie as CitaTrie, Trie as CitaTrieTrait};
@@ -744,7 +830,6 @@ mod test {
             }
         }
 
-
         #[test]
         fn proptest_compare_hash(data in btree_set(vec(any::<u8>(), 1..100), 1..100)) {
             let mut trie = Trie::new_temp();
@@ -873,7 +958,6 @@ mod test {
             }
         }
 
-
         #[test]
         // The previous test needs to sort the input values in order to get rid of duplicate entries, leading to ordered insertions
         // This check has a fixed way of determining whether a value should be removed but doesn't require ordered insertions
diff --git a/crates/common/trie/trie_iter.rs b/crates/common/trie/trie_iter.rs
index 5526b62b2ba..60d45aa104f 100644
--- a/crates/common/trie/trie_iter.rs
+++ b/crates/common/trie/trie_iter.rs
@@ -1,19 +1,23 @@
-use crate::{nibbles::Nibbles, node::Node, node_hash::NodeHash, PathRLP, Trie, ValueRLP};
+use crate::{
+    nibbles::Nibbles,
+    node::{Node, NodeRef},
+    PathRLP, Trie, TrieDB, ValueRLP,
+};
 
 pub struct TrieIterator {
-    trie: Trie,
+    db: Box<dyn TrieDB>,
     // The stack contains the current traversed path and the next node to be traversed
-    stack: Vec<(Nibbles, NodeHash)>,
+    stack: Vec<(Nibbles, NodeRef)>,
 }
 
 impl TrieIterator {
     pub(crate) fn new(trie: Trie) -> Self {
-        let stack = if let Some(root) = &trie.root {
-            vec![(Nibbles::default(), *root)]
-        } else {
-            vec![]
-        };
-        Self { trie, stack }
+        let mut stack = Vec::new();
+        if trie.root.is_valid() {
+            stack.push((Nibbles::default(), trie.root));
+        }
+
+        Self { db: trie.db, stack }
     }
 }
 
@@ -25,8 +29,8 @@ impl Iterator for TrieIterator {
             return None;
         };
         // Fetch the last node in the stack
-        let (mut path, next_node_hash) = self.stack.pop()?;
-        let next_node = self.trie.state.get_node(next_node_hash).ok()??;
+        let (mut path, next_node_ref) = self.stack.pop()?;
+        let next_node = next_node_ref.get_node(self.db.as_ref()).ok().flatten()?;
         match &next_node {
             Node::Branch(branch_node) => {
                 // Add all children to the stack (in reverse order so we process first child frist)
@@ -34,7 +38,7 @@ impl Iterator for TrieIterator {
                     if child.is_valid() {
                         let mut child_path = path.clone();
                         child_path.append(choice as u8);
-                        self.stack.push((child_path, *child))
+                        self.stack.push((child_path, child.clone()))
                     }
                 }
             }
@@ -42,7 +46,8 @@ impl Iterator for TrieIterator {
                 // Update path
                 path.extend(&extension_node.prefix);
                 // Add child to the stack
-                self.stack.push((path.clone(), extension_node.child));
+                self.stack
+                    .push((path.clone(), extension_node.child.clone()));
             }
             Node::Leaf(leaf) => {
                 path.extend(&leaf.partial);
diff --git a/crates/common/trie/verify_range.rs b/crates/common/trie/verify_range.rs
index 358bdf422e6..6e98641fd73 100644
--- a/crates/common/trie/verify_range.rs
+++ b/crates/common/trie/verify_range.rs
@@ -1,10 +1,16 @@
-use std::{cmp::Ordering, collections::HashMap};
+use std::{
+    cmp::Ordering,
+    collections::{HashMap, VecDeque},
+};
 
 use ethereum_types::H256;
 use sha3::{Digest, Keccak256};
 
 use crate::{
-    nibbles::Nibbles, node::Node, node_hash::NodeHash, state::TrieState, Trie, TrieError, ValueRLP,
+    nibbles::Nibbles,
+    node::{Node, NodeRef},
+    node_hash::NodeHash,
+    ProofTrie, Trie, TrieError, ValueRLP,
 };
 
 /// Verifies that the key value range belongs to the trie with the given root given the edge proofs for the range
@@ -16,8 +22,6 @@ pub fn verify_range(
     values: &[ValueRLP],
     proof: &[Vec<u8>],
 ) -> Result<bool, TrieError> {
-    // Store proof nodes by hash
-    let proof_nodes = ProofNodeStorage::from_proof(proof);
     // Validate range
     if keys.len() != values.len() {
         return Err(TrieError::Verify(format!(
@@ -41,7 +45,6 @@ pub fn verify_range(
         )));
     }
 
-    // Verify ranges depending on the given proof
     let mut trie = Trie::stateless();
 
     // Special Case: No proofs given, the range is expected to be the full set of leaves
@@ -64,15 +67,15 @@ pub fn verify_range(
     if keys.is_empty() {
         // We need to check that the proof confirms the non-existance of the first key
         // and that there are no more elements to the right of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-        let has_right_element = has_right_element(root, first_key.as_bytes(), &trie.state)?;
-        if has_right_element || !value.is_empty() {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, None))?;
+        if num_right_refs > 0 || !left_value.is_empty() {
             return Err(TrieError::Verify(
                 "no keys returned but more are available on the trie".to_string(),
             ));
         } else {
             return Ok(false);
-        }
+        };
     }
 
     let last_key = keys.last().unwrap();
@@ -80,368 +83,178 @@ pub fn verify_range(
     // Special Case: There is only one element and the two edge keys are the same
     if keys.len() == 1 && first_key == last_key {
         // We need to check that the proof confirms the existance of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
         if first_key != &keys[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid key".to_string(),
             ));
         }
-        if value != values[0] {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+        if left_value != values[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid data".to_string(),
             ));
         }
-        return has_right_element(root, first_key.as_bytes(), &trie.state);
+        return Ok(num_right_refs > 0);
     }
 
     // Regular Case: Two edge proofs
     if first_key >= last_key {
         return Err(TrieError::Verify("invalid edge keys".to_string()));
     }
-    // Fill up the state with the nodes from the proof
-    fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-    fill_state(&mut trie.state, root, last_key, &proof_nodes)?;
-    // Remove all references to the internal nodes that belong to the range so they can be reconstructed
-    let empty = remove_internal_references(root, first_key, last_key, &mut trie.state)?;
-    if !empty {
-        trie.root = Some(NodeHash::from(root));
-    }
+
+    // Process proofs to check if they are valid.
+    let (external_refs, _, num_right_refs) =
+        process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+
     // Reconstruct the internal nodes by inserting the elements on the range
     for (key, value) in keys.iter().zip(values.iter()) {
         trie.insert(key.0.to_vec(), value.clone())?;
     }
-    // Check for elements to the right of the range before we wipe the sate
-    let has_right_element = has_right_element(root, last_key.as_bytes(), &trie.state)?;
+
+    // Fill up the state with the nodes from the proof
+    let mut trie = ProofTrie::from(trie);
+    for (partial_path, external_ref) in external_refs {
+        trie.insert(partial_path, external_ref)?;
+    }
+
     // Check that the hash is the one we expected (aka the trie was properly reconstructed from the edge proofs and the range)
-    let hash = trie.hash()?;
+    let hash = trie.hash();
     if hash != root {
         return Err(TrieError::Verify(format!(
             "invalid proof, expected root hash {}, got  {}",
             root, hash
         )));
     }
-    Ok(has_right_element)
+    Ok(num_right_refs > 0)
 }
 
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_state(
-    trie_state: &mut TrieState,
-    root_hash: H256,
-    first_key: &H256,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let mut path = Nibbles::from_bytes(&first_key.0);
-    fill_node(
-        &mut path,
-        &NodeHash::from(root_hash),
-        trie_state,
-        proof_nodes,
-    )
-}
-
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_node(
-    path: &mut Nibbles,
-    node_hash: &NodeHash,
-    trie_state: &mut TrieState,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let node = proof_nodes.get_node(node_hash)?;
-    let child_hash = get_child(path, &node);
-    if let Some(ref child_hash) = child_hash {
-        trie_state.insert_node(node, *node_hash);
-        fill_node(path, child_hash, trie_state, proof_nodes)
-    } else {
-        let value = match &node {
-            Node::Branch(n) => n.value.clone(),
-            Node::Extension(_) => vec![],
-            Node::Leaf(n) => (*path == n.partial)
-                .then_some(n.value.clone())
-                .unwrap_or_default(),
-        };
-        trie_state.insert_node(node, *node_hash);
-        Ok(value)
+/// Iterate over all provided proofs starting from the root and generate a set of hashes that fall
+/// outside the verification bounds.
+///
+/// For example, calling this function with the proofs for the range `(hash_a, hash_b)` will return
+/// all node references contained within those proofs except the ones that are contained between
+/// `hash_a` and `hash_b` lexicographically.
+///
+/// Also returns the number of references strictly to the right of the bounds. If the right bound
+/// is unbounded (aka. not provided), all nodes to the right (inclusive) of the left bound will
+/// be counted. Leaf nodes are not counted (the leaf nodes within the proof do not count).
+type ProcessProofNodesResult = (Vec<(Nibbles, NodeHash)>, (Vec<u8>, Vec<u8>), usize);
+fn process_proof_nodes(
+    proof: &[Vec<u8>],
+    root: NodeHash,
+    bounds: (H256, Option<H256>),
+) -> Result<ProcessProofNodesResult, TrieError> {
+    // Convert `H256` bounds into `Nibble` bounds for convenience.
+    let bounds = (
+        Nibbles::from_bytes(&bounds.0 .0),
+        bounds.1.map(|x| Nibbles::from_bytes(&x.0)),
+    );
+
+    // Generate a map of node hashes to node data for obtaining proof nodes given their hashes.
+    let proof = proof
+        .iter()
+        .map(|node| {
+            (
+                H256::from_slice(&Keccak256::new_with_prefix(node).finalize()),
+                node.as_slice(),
+            )
+        })
+        .collect::<HashMap<_, _>>();
+    fn get_node(proof: &HashMap<H256, &[u8]>, hash: NodeHash) -> Result<Option<Node>, TrieError> {
+        Ok(Some(Node::decode_raw(match hash {
+            NodeHash::Hashed(hash) => match proof.get(&hash) {
+                Some(x) => x,
+                None => return Ok(None),
+            },
+            NodeHash::Inline(_) => hash.as_ref(),
+        })?))
     }
-}
 
-/// Returns the node hash of the node's child (if any) following the given path
-fn get_child<'a>(path: &'a mut Nibbles, node: &'a Node) -> Option<NodeHash> {
-    match node {
-        Node::Branch(n) => {
-            if let Some(choice) = path.next_choice() {
-                if n.choices[choice].is_valid() {
-                    return Some(n.choices[choice]);
-                }
-            }
-            None
-        }
-        Node::Extension(n) => path.skip_prefix(&n.prefix).then_some(n.child),
-        Node::Leaf(_) => None,
-    }
-}
+    // Initialize the external refs container.
+    let mut external_refs = Vec::new();
+    let (mut left_value, mut right_value) = (Vec::new(), Vec::new());
+    let mut num_right_refs = 0;
+
+    // Iterate over the proofs tree.
+    //
+    // The children are processed as follows:
+    //   1. Nodes that fall within bounds will be filtered out.
+    //   2. Nodes for which we have the proof will push themselves into the queue.
+    //   3. Nodes for which we do not have the proof are treated as external references.
+    let mut process_child =
+        |stack: &mut VecDeque<_>, mut partial_path: Nibbles, child| -> Result<(), TrieError> {
+            let cmp_l = bounds.0.compare_prefix(&partial_path);
+            let cmp_r = bounds.1.as_ref().map(|x| x.compare_prefix(&partial_path));
+
+            if cmp_l != Ordering::Less || cmp_r.is_none_or(|x| x != Ordering::Greater) {
+                let NodeRef::Hash(hash) = child else {
+                    // This is unreachable because the nodes have just been decoded, therefore only
+                    // having hash references.
+                    unreachable!()
+                };
 
-/// Returns true if the trie contains elements to the right of the given key
-/// (Aka if the given key is not the edge key of the trie)
-fn has_right_element(
-    root_hash: H256,
-    key: &[u8],
-    trie_state: &TrieState,
-) -> Result<bool, TrieError> {
-    let path = Nibbles::from_bytes(key);
-    has_right_element_inner(root_hash.into(), path, trie_state)
-}
+                match get_node(&proof, hash)? {
+                    Some(node) => {
+                        // Append implicit leaf extension when pushing leaves.
+                        if let Node::Leaf(node) = &node {
+                            partial_path.extend(&node.partial);
+                        }
+
+                        stack.push_back((partial_path, node));
+                    }
+                    None => {
+                        if cmp_l == Ordering::Equal || cmp_r.is_some_and(|x| x == Ordering::Equal) {
+                            return Err(TrieError::Verify(format!("proof node missing: {hash:?}")));
+                        }
+
+                        external_refs.push((partial_path, hash));
+                    }
+                }
 
-/// Returns true if the node's subtrie contains elements to the right of the given key
-/// (Aka if the given key is not the edge key of the subtrie)
-fn has_right_element_inner(
-    node_hash: NodeHash,
-    mut path: Nibbles,
-    trie_state: &TrieState,
-) -> Result<bool, TrieError> {
-    let Ok(Some(node)) = trie_state.get_node(node_hash) else {
-        return Ok(false);
-    };
-    match node {
-        Node::Branch(ref n) => {
-            // Check if there are children to the right side
-            if let Some(choice) = path.next_choice() {
-                if n.choices[choice + 1..].iter().any(|child| child.is_valid()) {
-                    return Ok(true);
-                } else if n.choices[choice].is_valid() {
-                    return has_right_element_inner(n.choices[choice], path, trie_state);
+                // Increment right-reference counter.
+                if cmp_l == Ordering::Less && cmp_r.is_none_or(|x| x == Ordering::Less) {
+                    num_right_refs += 1;
                 }
             }
-        }
-        Node::Extension(n) => {
-            if path.skip_prefix(&n.prefix) {
-                return has_right_element_inner(n.child, path, trie_state);
-            } else {
-                return Ok(n.prefix.as_ref() > path.as_ref());
-            }
-        }
-        // We reached the end of the path
-        Node::Leaf(_) => {}
-    }
-    Ok(false)
-}
-
-/// Removes references to internal nodes between the left and right key
-/// These nodes should be entirely reconstructed when inserting the elements between left and right key (the proven range)
-/// Returns true if the trie is left empty (rootless) as a result of this process
-/// Asumes that left_key & right_key are not equal and of same length
-fn remove_internal_references(
-    root_hash: H256,
-    left_key: &H256,
-    right_key: &H256,
-    trie_state: &mut TrieState,
-) -> Result<bool, TrieError> {
-    // First find the node at which the left and right path differ
-    let left_path = Nibbles::from_bytes(&left_key.0);
-    let right_path = Nibbles::from_bytes(&right_key.0);
 
-    remove_internal_references_inner(NodeHash::from(root_hash), left_path, right_path, trie_state)
-}
+            Ok(())
+        };
 
-/// Traverses the left and right path starting from the given node until the paths diverge
-/// Once the paths diverge, removes the nodes between the left and right path
-/// Returns true if the given node was completely removed as a result of this process
-/// In which case the caller should remove the reference to this node from its parent node
-/// Asumes that left_key & right_key are not equal and of same length
-fn remove_internal_references_inner(
-    node_hash: NodeHash,
-    mut left_path: Nibbles,
-    mut right_path: Nibbles,
-    trie_state: &mut TrieState,
-) -> Result<bool, TrieError> {
-    if !node_hash.is_valid() {
-        return Ok(true);
-    }
-    // We already looked up the nodes when filling the state so this shouldn't fail
-    let node = trie_state.get_node(node_hash)?.unwrap();
-    match node {
-        Node::Branch(mut n) => {
-            // If none of the paths have a next choice nibble then it means that this is the end of the path
-            // which would mean that both paths are equal, which we already checked before
-            // If only one path doesn't have a next choice then it would mean that the paths have different lengths,
-            // which we also checked before calling this function
-            // Therefore we can safely unwrap here
-            let left_choice = left_path.next_choice().unwrap();
-            let right_choice = right_path.next_choice().unwrap();
-
-            if left_choice == right_choice && n.choices[left_choice].is_valid() {
-                // Keep going
-                // Check if the child extension node should be removed as a result of this process
-                let should_remove = remove_internal_references_inner(
-                    n.choices[left_choice],
-                    left_path,
-                    right_path,
-                    trie_state,
-                )?;
-                if should_remove {
-                    // Remove child node
-                    n.choices[left_choice] = NodeHash::default();
-                    // Update node in the state
-                    trie_state.insert_node(n.into(), node_hash);
-                }
-            } else {
-                // We found our fork node, now we can remove the internal references
-                // Remove all child nodes between the left and right child nodes
-                for choice in &mut n.choices[left_choice + 1..right_choice] {
-                    *choice = NodeHash::default()
+    let mut stack = VecDeque::from_iter([(
+        Nibbles::default(),
+        get_node(&proof, root)?
+            .ok_or(TrieError::Verify(format!("proof node missing: {root:?}")))?,
+    )]);
+    while let Some((mut current_path, current_node)) = stack.pop_front() {
+        let value = match current_node {
+            Node::Branch(node) => {
+                for (index, choice) in node.choices.into_iter().enumerate() {
+                    if choice.is_valid() {
+                        process_child(&mut stack, current_path.append_new(index as u8), choice)?;
+                    }
                 }
-                // Remove nodes on the left and right choice's subtries
-                let should_remove_left =
-                    remove_node(n.choices[left_choice], left_path, false, trie_state);
-                let should_remove_right =
-                    remove_node(n.choices[right_choice], right_path, true, trie_state);
-                // Remove left and right child nodes if their subtries where wiped in the process
-                if should_remove_left {
-                    n.choices[left_choice] = NodeHash::default();
-                }
-                if should_remove_right {
-                    n.choices[right_choice] = NodeHash::default();
-                }
-                // Update node in the state
-                trie_state.insert_node(n.into(), node_hash);
+                node.value
             }
-        }
-        Node::Extension(n) => {
-            // Compare left and right paths against prefix
-
-            let left_fork = left_path.compare_prefix(&n.prefix);
-            let right_fork = right_path.compare_prefix(&n.prefix);
-
-            match (left_fork, right_fork) {
-                // If both paths contain the same prefix as the extension node, keep going
-                (Ordering::Equal, Ordering::Equal) => {
-                    return remove_internal_references_inner(
-                        n.child,
-                        left_path.offset(n.prefix.len()),
-                        right_path.offset(n.prefix.len()),
-                        trie_state,
-                    );
-                }
-                // If both paths are greater or lesser than the node's prefix then the range is empty
-                (Ordering::Greater, Ordering::Greater) | (Ordering::Less, Ordering::Less) => {
-                    return Err(TrieError::Verify("empty range".to_string()))
-                }
-                // None of the paths fit the prefix, remove the entire subtrie
-                (left, right) if left.is_ne() && right.is_ne() => {
-                    // Return true so that the parent node removes this node
-                    return Ok(true);
-                }
-                // One path fits the prefix, the other one doesn't
-                (left, right) => {
-                    // Remove the nodes from the child's subtrie
-                    let path = if left.is_eq() { left_path } else { right_path };
-                    // Propagate the response so that this node will be removed too if the child's subtrie is wiped
-                    return Ok(remove_node(
-                        node_hash,
-                        path.offset(n.prefix.len()),
-                        right.is_eq(),
-                        trie_state,
-                    ));
-                }
+            Node::Extension(node) => {
+                current_path.extend(&node.prefix);
+                process_child(&mut stack, current_path.clone(), node.child)?;
+                Vec::new()
             }
-        }
-        // This case should be unreachable as we checked that left_path != right_path
-        // before calling this function
-        Node::Leaf(_) => {}
-    }
-    Ok(false)
-}
+            Node::Leaf(node) => node.value,
+        };
 
-// Removes all nodes in the node's subtrie to the left or right of the path (given by the `remove_left` flag)
-// If the whole subtrie is removed in the process this function will return true, in which case
-// the caller must remove the reference to this node from it's parent node
-fn remove_node(
-    node_hash: NodeHash,
-    mut path: Nibbles,
-    remove_left: bool,
-    trie_state: &mut TrieState,
-) -> bool {
-    // Node doesn't exist already, no need to remove it
-    if !node_hash.is_valid() {
-        return false;
-    }
-    // We already looked up the nodes when filling the state so this shouldn't fail
-    let node = trie_state.get_node(node_hash).unwrap().unwrap();
-    match node {
-        Node::Branch(mut n) => {
-            // Remove child nodes
-            let Some(choice) = path.next_choice() else {
-                // Path ends in the branch node
-                return true;
-            };
-            if remove_left {
-                for child in &mut n.choices[..choice] {
-                    *child = NodeHash::default()
-                }
-            } else {
-                for child in &mut n.choices[choice + 1..] {
-                    *child = NodeHash::default()
-                }
-            }
-            // Remove nodes to the left/right of the choice's subtrie
-            let should_remove = remove_node(n.choices[choice], path, remove_left, trie_state);
-            if should_remove {
-                n.choices[choice] = NodeHash::default();
+        if !value.is_empty() {
+            if current_path == bounds.0 {
+                left_value = value.clone();
             }
-            // Update node in the state
-            trie_state.insert_node(n.into(), node_hash);
-        }
-        Node::Extension(n) => {
-            // If no child subtrie would result from this process remove the node entirely
-            // (Such as removing the left side of a trie with no right side)
-            if !path.skip_prefix(&n.prefix) {
-                if (remove_left && path.compare_prefix(&n.prefix).is_gt())
-                    || !remove_left && path.compare_prefix(&n.prefix).is_lt()
-                {
-                    return true;
-                }
-            } else {
-                // Remove left/right side of the child subtrie
-                return remove_node(n.child, path, remove_left, trie_state);
+            if bounds.1.as_ref().is_some_and(|x| &current_path == x) {
+                right_value = value.clone();
             }
         }
-        Node::Leaf(_) => return true,
-    }
-    false
-}
-
-/// An intermediate storage for proof nodes, containing encoded nodes indexed by hash
-struct ProofNodeStorage<'a> {
-    nodes: HashMap<Vec<u8>, &'a Vec<u8>>,
-}
-
-impl<'a> ProofNodeStorage<'a> {
-    // Construct a ProofNodeStorage for a proof
-    fn from_proof(proof: &'a [Vec<u8>]) -> Self {
-        Self {
-            nodes: proof
-                .iter()
-                .map(|node| (Keccak256::new_with_prefix(node).finalize().to_vec(), node))
-                .collect::<HashMap<_, _>>(),
-        }
     }
-    // Fetch a node by its hash, return an error if the node is not present or badly encoded
-    fn get_node(&self, hash: &NodeHash) -> Result<Node, TrieError> {
-        let encoded = match hash {
-            NodeHash::Hashed(hash) => {
-                let Some(encoded) = self.nodes.get(hash.as_bytes()) else {
-                    return Err(TrieError::Verify(format!("proof node missing: {hash}")));
-                };
-                encoded.as_slice()
-            }
 
-            NodeHash::Inline(_) => hash.as_ref(),
-        };
-        Ok(Node::decode_raw(encoded)?)
-    }
+    Ok((external_refs, (left_value, right_value), num_right_refs))
 }
 
 #[cfg(test)]
@@ -628,7 +441,7 @@ mod tests {
                 if first_key == data[start -1] {
                     // Skip test
                     return Ok(());
-            }
+                }
             }
             // Generate proofs
             let mut proof = trie.get_proof(&first_key).unwrap();
diff --git a/crates/networking/p2p/sync.rs b/crates/networking/p2p/sync.rs
index a316f2d8b8f..0e3d9a614d5 100644
--- a/crates/networking/p2p/sync.rs
+++ b/crates/networking/p2p/sync.rs
@@ -14,7 +14,7 @@ use ethrex_common::{
 };
 use ethrex_rlp::error::RLPDecodeError;
 use ethrex_storage::{error::StoreError, EngineType, Store, STATE_TRIE_SEGMENTS};
-use ethrex_trie::{Nibbles, Node, TrieError, TrieState};
+use ethrex_trie::{Nibbles, Node, TrieDB, TrieError};
 use state_healing::heal_state_trie;
 use state_sync::state_sync;
 use std::{
@@ -667,19 +667,19 @@ impl Syncer {
 fn node_missing_children(
     node: &Node,
     parent_path: &Nibbles,
-    trie_state: &TrieState,
+    trie_state: &dyn TrieDB,
 ) -> Result<Vec<Nibbles>, TrieError> {
     let mut paths = Vec::new();
     match &node {
         Node::Branch(node) => {
             for (index, child) in node.choices.iter().enumerate() {
-                if child.is_valid() && trie_state.get_node(*child)?.is_none() {
+                if child.is_valid() && child.get_node(trie_state)?.is_none() {
                     paths.push(parent_path.append_new(index as u8));
                 }
             }
         }
         Node::Extension(node) => {
-            if node.child.is_valid() && trie_state.get_node(node.child)?.is_none() {
+            if node.child.is_valid() && node.child.get_node(trie_state)?.is_none() {
                 paths.push(parent_path.concat(node.prefix.clone()));
             }
         }
diff --git a/crates/networking/p2p/sync/state_healing.rs b/crates/networking/p2p/sync/state_healing.rs
index 9ea8e081ecf..9414a9621c7 100644
--- a/crates/networking/p2p/sync/state_healing.rs
+++ b/crates/networking/p2p/sync/state_healing.rs
@@ -14,9 +14,9 @@ use ethrex_common::{
     types::{AccountState, EMPTY_KECCACK_HASH},
     H256,
 };
-use ethrex_rlp::decode::RLPDecode;
+use ethrex_rlp::{decode::RLPDecode, encode::RLPEncode};
 use ethrex_storage::Store;
-use ethrex_trie::{Nibbles, Node, EMPTY_TRIE_HASH};
+use ethrex_trie::{Nibbles, Node, NodeHash, EMPTY_TRIE_HASH};
 use tokio::sync::mpsc::{channel, Sender};
 use tracing::{debug, info};
 
@@ -115,10 +115,10 @@ async fn heal_state_batch(
         // - If it is a leaf, request its bytecode & storage
         // - If it is a leaf, add its path & value to the trie
         {
-            let mut trie = store.open_state_trie(*EMPTY_TRIE_HASH);
+            let trie = store.open_state_trie(*EMPTY_TRIE_HASH);
             for node in nodes.iter() {
                 let path = batch.remove(0);
-                batch.extend(node_missing_children(node, &path, trie.state())?);
+                batch.extend(node_missing_children(node, &path, trie.db())?);
                 if let Node::Leaf(node) = &node {
                     // Fetch bytecode & storage
                     let account = AccountState::decode(&node.value)?;
@@ -142,7 +142,15 @@ async fn heal_state_batch(
                 }
             }
             // Write nodes to trie
-            trie.state_mut().write_node_batch(&nodes)?;
+            trie.db().put_batch(
+                nodes
+                    .into_iter()
+                    .filter_map(|node| match node.compute_hash() {
+                        hash @ NodeHash::Hashed(_) => Some((hash, node.encode_to_vec())),
+                        NodeHash::Inline(_) => None,
+                    })
+                    .collect(),
+            )?;
         }
         // Send storage & bytecode requests
         if !hashed_addresses.is_empty() {
diff --git a/crates/networking/p2p/sync/storage_healing.rs b/crates/networking/p2p/sync/storage_healing.rs
index 135957e9849..5e04684512b 100644
--- a/crates/networking/p2p/sync/storage_healing.rs
+++ b/crates/networking/p2p/sync/storage_healing.rs
@@ -15,7 +15,7 @@ use std::{
 
 use ethrex_common::H256;
 use ethrex_storage::Store;
-use ethrex_trie::{Nibbles, EMPTY_TRIE_HASH};
+use ethrex_trie::{Nibbles, NodeHash, EMPTY_TRIE_HASH};
 use tokio::time::Instant;
 use tokio_util::sync::CancellationToken;
 use tracing::{debug, info};
@@ -27,6 +27,7 @@ use crate::{peer_handler::PeerHandler, sync::node_missing_children};
 const MINUMUM_STORAGES_IN_QUEUE: usize = 400;
 
 use super::{SyncError, MAX_PARALLEL_FETCHES, NODE_BATCH_SIZE, SHOW_PROGRESS_INTERVAL_DURATION};
+use ethrex_rlp::encode::RLPEncode;
 
 /// Waits for incoming hashed addresses from the receiver channel endpoint and queues the associated root nodes for state retrieval
 /// Also retrieves their children nodes until we have the full storage trie stored
@@ -118,7 +119,7 @@ async fn heal_storage_batch(
         debug!("Received {} storage nodes", nodes.len());
         // Process the nodes for each account path
         for (acc_path, paths) in batch.iter_mut() {
-            let mut trie = store.open_storage_trie(*acc_path, *EMPTY_TRIE_HASH);
+            let trie = store.open_storage_trie(*acc_path, *EMPTY_TRIE_HASH);
             // Get the corresponding nodes
             let trie_nodes: Vec<ethrex_trie::Node> =
                 nodes.drain(..paths.len().min(nodes.len())).collect();
@@ -126,11 +127,19 @@ async fn heal_storage_batch(
             let children = trie_nodes
                 .iter()
                 .zip(paths.drain(..trie_nodes.len()))
-                .map(|(node, path)| node_missing_children(node, &path, trie.state()))
+                .map(|(node, path)| node_missing_children(node, &path, trie.db()))
                 .collect::<Result<Vec<_>, _>>()?;
             paths.extend(children.into_iter().flatten());
             // Write nodes to trie
-            trie.state_mut().write_node_batch(&nodes)?;
+            trie.db().put_batch(
+                nodes
+                    .iter()
+                    .filter_map(|node| match node.compute_hash() {
+                        hash @ NodeHash::Hashed(_) => Some((hash, node.encode_to_vec())),
+                        NodeHash::Inline(_) => None,
+                    })
+                    .collect(),
+            )?;
             if nodes.is_empty() {
                 break;
             }
diff --git a/crates/storage/store.rs b/crates/storage/store.rs
index f79cb9f924a..072cda6e95d 100644
--- a/crates/storage/store.rs
+++ b/crates/storage/store.rs
@@ -893,8 +893,8 @@ impl Store {
         // Root is irrelevant, we only care about the internal state
         Ok(self
             .open_state_trie(*EMPTY_TRIE_HASH)
-            .state()
-            .get_node(node_hash.into())?
+            .db()
+            .get(node_hash.into())?
             .is_some())
     }
 
@@ -907,8 +907,8 @@ impl Store {
         // Root is irrelevant, we only care about the internal state
         Ok(self
             .open_storage_trie(hashed_address, *EMPTY_TRIE_HASH)
-            .state()
-            .get_node(node_hash.into())?
+            .db()
+            .get(node_hash.into())?
             .is_some())
     }
 


Reviews:
user: login: Arkenan
body: Thanks for the PR! A lot of great changes. Left some comments.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T17:17:40Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:34:20Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:37:40Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:54:49Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:56:22Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T17:58:37Z
user: login: Arkenan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T18:04:14Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-23T18:07:42Z
user: login: Arkenan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T18:08:55Z
user: login: Arkenan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-23T18:18:18Z
user: login: Arkenan
body: LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-27T13:08:07Z
user: login: mpaulucci
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-27T14:46:36Z
user: login: Oppen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T14:52:16Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T15:02:15Z
user: login: Oppen
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T16:28:15Z
user: login: azteca1998
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-28T16:52:22Z
user: login: Arkenan
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-28T16:55:06Z

Review comments:
diff_hunk: @@ -43,127 +46,139 @@ pub type NodeRLP = Vec<u8>;
 
 /// Libmdx-based Ethereum Compatible Merkle Patricia Trie
 pub struct Trie {
-    /// Hash of the current node
-    root: Option<NodeHash>,
-    /// Contains the trie's nodes
-    pub(crate) state: TrieState,
+    db: Box<dyn TrieDB>,
+    root: NodeRef,
 }
 
 impl Trie {
     /// Creates a new Trie from a clean DB
     pub fn new(db: Box<dyn TrieDB>) -> Self {
         Self {
-            state: TrieState::new(db),
-            root: None,
+            db,
+            root: NodeRef::default(),
         }
     }
 
     /// Creates a trie from an already-initialized DB and sets root as the root node of the trie
     pub fn open(db: Box<dyn TrieDB>, root: H256) -> Self {
-        let root = (root != *EMPTY_TRIE_HASH).then_some(root.into());
         Self {
-            state: TrieState::new(db),
-            root,
+            db,
+            root: (root != *EMPTY_TRIE_HASH)
+                .then_some(NodeHash::from(root).into())
+                .unwrap_or_default(),
         }
     }
 
+    pub fn db(&self) -> &dyn TrieDB {
path: crates/common/trie/trie.rs
user: login: Arkenan
body: Add a comment here. e.g. "Gets a reference to the storage that the trie uses. Any queries done to that storage will bypass the memory cache".
created_at: 2025-05-23T12:00:32Z
author_association: COLLABORATOR
diff_hunk: @@ -43,127 +46,139 @@ pub type NodeRLP = Vec<u8>;
 
 /// Libmdx-based Ethereum Compatible Merkle Patricia Trie
 pub struct Trie {
-    /// Hash of the current node
-    root: Option<NodeHash>,
-    /// Contains the trie's nodes
-    pub(crate) state: TrieState,
+    db: Box<dyn TrieDB>,
+    root: NodeRef,
 }
 
 impl Trie {
     /// Creates a new Trie from a clean DB
     pub fn new(db: Box<dyn TrieDB>) -> Self {
         Self {
-            state: TrieState::new(db),
-            root: None,
+            db,
+            root: NodeRef::default(),
         }
     }
 
     /// Creates a trie from an already-initialized DB and sets root as the root node of the trie
     pub fn open(db: Box<dyn TrieDB>, root: H256) -> Self {
-        let root = (root != *EMPTY_TRIE_HASH).then_some(root.into());
         Self {
-            state: TrieState::new(db),
-            root,
+            db,
+            root: (root != *EMPTY_TRIE_HASH)
+                .then_some(NodeHash::from(root).into())
+                .unwrap_or_default(),
         }
     }
 
+    pub fn db(&self) -> &dyn TrieDB {
+        self.db.as_ref()
+    }
+
     /// Retrieve an RLP-encoded value from the trie given its RLP-encoded path.
     pub fn get(&self, path: &PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        if let Some(root) = &self.root {
-            let root_node = self
-                .state
-                .get_node(*root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            root_node.get(&self.state, Nibbles::from_bytes(path))
-        } else {
-            Ok(None)
-        }
+        Ok(match self.root {
+            NodeRef::Node(ref node, _) => node.get(self.db.as_ref(), Nibbles::from_bytes(path))?,
+            NodeRef::Hash(hash) if hash.is_valid() => {
+                Node::decode(&self.db.get(hash)?.ok_or(TrieError::InconsistentTree)?)
+                    .map_err(TrieError::RLPDecode)?
+                    .get(self.db.as_ref(), Nibbles::from_bytes(path))?
+            }
+            _ => None,
+        })
     }
 
     /// Insert an RLP-encoded value into the trie.
     pub fn insert(&mut self, path: PathRLP, value: ValueRLP) -> Result<(), TrieError> {
-        let root = self.root.take();
-        if let Some(root_node) = root
-            .map(|root| self.state.get_node(root))
-            .transpose()?
-            .flatten()
-        {
-            // If the trie is not empty, call the root node's insertion logic
-            let root_node = root_node.insert(&mut self.state, Nibbles::from_bytes(&path), value)?;
-            self.root = Some(root_node.insert_self(&mut self.state)?)
+        let path = Nibbles::from_bytes(&path);
+
+        self.root = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.db.as_ref(), path, value)?
+                .into()
         } else {
             // If the trie is empty, just add a leaf.
-            let new_leaf = Node::from(LeafNode::new(Nibbles::from_bytes(&path), value));
-            self.root = Some(new_leaf.insert_self(&mut self.state)?)
-        }
+            Node::from(LeafNode::new(path, value)).into()
+        };
+
         Ok(())
     }
 
     /// Remove a value from the trie given its RLP-encoded path.
     /// Returns the value if it was succesfully removed or None if it wasn't part of the trie
     pub fn remove(&mut self, path: PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        let root = self.root.take();
-        if let Some(root) = root {
-            let root_node = self
-                .state
-                .get_node(root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            let (root_node, old_value) =
-                root_node.remove(&mut self.state, Nibbles::from_bytes(&path))?;
-            self.root = root_node
-                .map(|root| root.insert_self(&mut self.state))
-                .transpose()?;
-            Ok(old_value)
+        let value;
+        (self.root, value) = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's removal logic.
+            let (node, value) = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .remove(self.db.as_ref(), Nibbles::from_bytes(&path))?;
+
+            (node.map(Into::into).unwrap_or_default(), value)
         } else {
-            Ok(None)
-        }
+            (NodeRef::default(), None)
+        };
+
+        Ok(value)
     }
 
     /// Return the hash of the trie's root node.
     /// Returns keccak(RLP_NULL) if the trie is empty
     /// Also commits changes to the DB
     pub fn hash(&mut self) -> Result<H256, TrieError> {
         self.commit()?;
-        Ok(self
-            .root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH))
+        Ok(self.hash_no_commit())
     }
 
     /// Return the hash of the trie's root node.
     /// Returns keccak(RLP_NULL) if the trie is empty
     pub fn hash_no_commit(&self) -> H256 {
-        self.root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH)
+        if self.root.is_valid() {
+            self.root.compute_hash().finalize()
+        } else {
+            *EMPTY_TRIE_HASH
+        }
     }
 
     pub fn commit(&mut self) -> Result<(), TrieError> {
-        if let Some(ref root) = self.root {
-            self.state.commit(root)?;
+        if self.root.is_valid() {
+            let mut acc = Vec::new();
+            self.root.commit(&mut acc);
+            self.db.put_batch(acc)?;
         }
+
         Ok(())
     }
 
     /// Obtain a merkle proof for the given path.
     /// The proof will contain all the encoded nodes traversed until reaching the node where the path is stored (including this last node).
     /// The proof will still be constructed even if the path is not stored in the trie, proving its absence.
+    ///
+    /// Note: This method has a different behavior in regard to non-existent trie root nodes. Normal
+    ///   behavior is to return `Err(InconsistentTrie)`, but this method will return
+    ///   `Ok(Vec::new())` instead.
     pub fn get_proof(&self, path: &PathRLP) -> Result<Vec<NodeRLP>, TrieError> {
-        // Will store all the encoded nodes traversed until reaching the node containing the path
-        let mut node_path = Vec::new();
-        let Some(root) = &self.root else {
-            return Ok(node_path);
-        };
-        // If the root is inlined, add it to the node_path
-        if let NodeHash::Inline(_) = root {
-            node_path.push(root.as_ref().to_vec());
-        }
-        if let Some(root_node) = self.state.get_node(*root)? {
-            root_node.get_path(&self.state, Nibbles::from_bytes(path), &mut node_path)?;
+        if self.root.is_valid() {
+            let hash = self.root.compute_hash();
+
+            let mut node_path = Vec::new();
path: crates/common/trie/trie.rs
user: login: Arkenan
body: nit: as we won't have paths of more elements than the amount of nibbles in the path, is it worth it to start this with that amount as capacity?
created_at: 2025-05-23T12:39:10Z
author_association: COLLABORATOR
diff_hunk: @@ -173,48 +188,86 @@ impl Trie {
         &self,
         paths: &[PathRLP],
     ) -> Result<(Option<NodeRLP>, Vec<NodeRLP>), TrieError> {
-        let Some(root_node) = self
-            .root
-            .as_ref()
-            .map(|root| self.state.get_node(*root))
-            .transpose()?
-            .flatten()
-        else {
-            return Ok((None, Vec::new()));
-        };
+        if self.root.is_valid() {
+            let encoded_root = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .encode_raw();
+
+            let mut node_path = HashSet::new();
+            for path in paths {
+                let mut nodes = self.get_proof(path)?;
+                nodes.swap_remove(0);
+                node_path.extend(nodes);
+            }
 
-        let mut node_path = Vec::new();
-        for path in paths {
-            let mut nodes = self.get_proof(path)?;
-            nodes.swap_remove(0);
-            node_path.extend(nodes); // skip root node
+            Ok((Some(encoded_root), node_path.into_iter().collect()))
+        } else {
+            Ok((None, Vec::new()))
         }
-
-        // dedup
-        // TODO: really inefficient, by making the traversing smarter we can avoid having
-        // duplicates
-        let node_path: HashSet<_> = node_path.into_iter().collect();
-        let node_path = Vec::from_iter(node_path);
-        Ok((Some(root_node.encode_raw()), node_path))
     }
 
-    /// Creates a cached Trie (with [NullTrieDB]) from a list of encoded nodes.
-    /// Generally used in conjuction with [Trie::get_proofs].
-    pub fn from_nodes(
-        root_node: Option<&NodeRLP>,
-        other_nodes: &[NodeRLP],
-    ) -> Result<Self, TrieError> {
-        let mut trie = Trie::stateless();
+    /// Builds a trie from a set of nodes.
+    ///
+    /// Note: This method will not ensure neither that all node references are valid nor that there
+    ///   are no dangling nodes. Invalid references will be reflected in the final trie by
+    ///   `InconsistentTree` errors at runtime, whereas dangling nodes will be ignored.
path: crates/common/trie/trie.rs
user: login: Arkenan
body: Comment is great, although something like this might be a bit clearer:

```suggestion
    /// Note: the returned trie may invalid external references. This means that `get`s over this trie may result 
    /// in `InconsistentTrie` errors.
    /// Note 2: nodes of the second argument that aren't the root and don't have any parent in the list will be
    /// considered dangling and ignored.
```
created_at: 2025-05-23T12:45:40Z
author_association: COLLABORATOR
diff_hunk: @@ -356,6 +399,40 @@ impl IntoIterator for Trie {
     }
 }
 
+pub struct ProofTrie(Trie);
+
+impl ProofTrie {
+    pub fn insert(
+        &mut self,
+        partial_path: Nibbles,
+        external_ref: NodeHash,
+    ) -> Result<(), TrieError> {
+        self.0.root = if self.0.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.0
+                .root
+                .get_node(self.0.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.0.db.as_ref(), partial_path, external_ref)?
+                .into()
+        } else {
+            todo!()
path: crates/common/trie/trie.rs
user: login: Arkenan
body: What does this mean here?
created_at: 2025-05-23T15:33:46Z
author_association: COLLABORATOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
path: crates/common/trie/node.rs
user: login: Arkenan
body: Why is this an arc?
created_at: 2025-05-23T15:40:28Z
author_association: COLLABORATOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
+    /// The node is in the database, referenced by its hash.
+    Hash(NodeHash),
+}
+
+impl NodeRef {
+    pub const fn const_default() -> Self {
+        Self::Hash(NodeHash::const_default())
+    }
+
+    pub fn get_node(&self, db: &dyn TrieDB) -> Result<Option<Node>, TrieError> {
+        match *self {
+            NodeRef::Node(ref node, _) => Ok(Some(node.as_ref().clone())),
+            NodeRef::Hash(NodeHash::Inline((data, len))) => {
path: crates/common/trie/node.rs
user: login: Arkenan
body: Not to tackle on this PR, but I find the "Inline" type useless now. It's only important when serializing, but in runtime should be a regular NodeRref::Node.
created_at: 2025-05-23T15:45:17Z
author_association: COLLABORATOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
+    /// The node is in the database, referenced by its hash.
+    Hash(NodeHash),
+}
+
+impl NodeRef {
+    pub const fn const_default() -> Self {
+        Self::Hash(NodeHash::const_default())
+    }
+
+    pub fn get_node(&self, db: &dyn TrieDB) -> Result<Option<Node>, TrieError> {
+        match *self {
+            NodeRef::Node(ref node, _) => Ok(Some(node.as_ref().clone())),
+            NodeRef::Hash(NodeHash::Inline((data, len))) => {
+                Ok(Some(Node::decode_raw(&data[..len as usize])?))
+            }
+            NodeRef::Hash(hash @ NodeHash::Hashed(_)) => db
+                .get(hash)?
+                .map(|rlp| Node::decode(&rlp).map_err(TrieError::RLPDecode))
+                .transpose(),
+        }
+    }
+
+    pub fn is_valid(&self) -> bool {
+        match self {
+            NodeRef::Node(_, _) => true,
+            NodeRef::Hash(hash) => hash.is_valid(),
+        }
+    }
+
+    pub fn commit(&mut self, acc: &mut Vec<(NodeHash, Vec<u8>)>) -> NodeHash {
path: crates/common/trie/node.rs
user: login: Arkenan
body: Please add a comment on this function. All public functions should be documented. In this case, something like "commits all in-memory nodes to the db and returns the hash of the reference".

created_at: 2025-05-23T15:49:28Z
author_association: COLLABORATOR
diff_hunk: @@ -76,33 +75,49 @@ impl BranchNode {
     /// Inserts a value into the subtrie originating from this node and returns the new root of the subtrie
     pub fn insert(
         mut self,
-        state: &mut TrieState,
+        db: &dyn TrieDB,
         mut path: Nibbles,
-        value: ValueRLP,
+        value: ValueOrHash,
     ) -> Result<Node, TrieError> {
         // If path is at the end, insert or replace its own value.
         // Otherwise, check the corresponding choice and insert or delegate accordingly.
         if let Some(choice) = path.next_choice() {
-            match &mut self.choices[choice] {
+            match (&mut self.choices[choice], value) {
                 // Create new child (leaf node)
-                choice_hash if !choice_hash.is_valid() => {
+                (choice_hash, ValueOrHash::Value(value)) if !choice_hash.is_valid() => {
path: crates/common/trie/node/branch.rs
user: login: Arkenan
body: I would rename this from `choice_hash` to `choice_ref` as hash is a bit confusing.
created_at: 2025-05-23T16:05:58Z
author_association: COLLABORATOR
diff_hunk: @@ -178,15 +193,16 @@ impl BranchNode {
             // If this node doesn't have a value and has only one child, replace it with its child node
             (1, false) => {
                 let (choice_index, child_hash) = children[0];
-                let child = state
-                    .get_node(*child_hash)?
+                let child = child_hash
path: crates/common/trie/node/branch.rs
user: login: Arkenan
body: Let's rename child_hash with child_ref here as well. 
created_at: 2025-05-23T16:22:27Z
author_association: COLLABORATOR
diff_hunk: @@ -64,384 +67,194 @@ pub fn verify_range(
     if keys.is_empty() {
         // We need to check that the proof confirms the non-existance of the first key
         // and that there are no more elements to the right of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-        let has_right_element = has_right_element(root, first_key.as_bytes(), &trie.state)?;
-        if has_right_element || !value.is_empty() {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, None))?;
+        if num_right_refs > 0 || !left_value.is_empty() {
             return Err(TrieError::Verify(
                 "no keys returned but more are available on the trie".to_string(),
             ));
         } else {
             return Ok(false);
-        }
+        };
     }
 
     let last_key = keys.last().unwrap();
 
     // Special Case: There is only one element and the two edge keys are the same
     if keys.len() == 1 && first_key == last_key {
         // We need to check that the proof confirms the existance of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
         if first_key != &keys[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid key".to_string(),
             ));
         }
-        if value != values[0] {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+        if left_value != values[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid data".to_string(),
             ));
         }
-        return has_right_element(root, first_key.as_bytes(), &trie.state);
+        return Ok(num_right_refs > 0);
     }
 
     // Regular Case: Two edge proofs
     if first_key >= last_key {
         return Err(TrieError::Verify("invalid edge keys".to_string()));
     }
-    // Fill up the state with the nodes from the proof
-    fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-    fill_state(&mut trie.state, root, last_key, &proof_nodes)?;
-    // Remove all references to the internal nodes that belong to the range so they can be reconstructed
-    let empty = remove_internal_references(root, first_key, last_key, &mut trie.state)?;
-    if !empty {
-        trie.root = Some(NodeHash::from(root));
-    }
+
+    // Process proofs to check if they are valid.
+    let (external_refs, _, num_right_refs) =
+        process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+
     // Reconstruct the internal nodes by inserting the elements on the range
     for (key, value) in keys.iter().zip(values.iter()) {
         trie.insert(key.0.to_vec(), value.clone())?;
     }
-    // Check for elements to the right of the range before we wipe the sate
-    let has_right_element = has_right_element(root, last_key.as_bytes(), &trie.state)?;
+
+    // Fill up the state with the nodes from the proof
+    let mut trie = ProofTrie::from(trie);
+    for (partial_path, external_ref) in external_refs {
+        trie.insert(partial_path, external_ref)?;
+    }
+
     // Check that the hash is the one we expected (aka the trie was properly reconstructed from the edge proofs and the range)
-    let hash = trie.hash()?;
+    let hash = trie.hash();
     if hash != root {
         return Err(TrieError::Verify(format!(
             "invalid proof, expected root hash {}, got  {}",
             root, hash
         )));
     }
-    Ok(has_right_element)
+    Ok(num_right_refs > 0)
 }
 
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_state(
-    trie_state: &mut TrieState,
-    root_hash: H256,
-    first_key: &H256,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let mut path = Nibbles::from_bytes(&first_key.0);
-    fill_node(
-        &mut path,
-        &NodeHash::from(root_hash),
-        trie_state,
-        proof_nodes,
-    )
-}
-
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_node(
-    path: &mut Nibbles,
-    node_hash: &NodeHash,
-    trie_state: &mut TrieState,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let node = proof_nodes.get_node(node_hash)?;
-    let child_hash = get_child(path, &node);
-    if let Some(ref child_hash) = child_hash {
-        trie_state.insert_node(node, *node_hash);
-        fill_node(path, child_hash, trie_state, proof_nodes)
-    } else {
-        let value = match &node {
-            Node::Branch(n) => n.value.clone(),
-            Node::Extension(_) => vec![],
-            Node::Leaf(n) => (*path == n.partial)
-                .then_some(n.value.clone())
-                .unwrap_or_default(),
-        };
-        trie_state.insert_node(node, *node_hash);
-        Ok(value)
+/// Iterate over all provided proofs starting from the root and generate a set of hashes that fall
+/// outside the verification bounds.
+///
+/// For example, calling this function with the proofs for the range `(hash_a, hash_b)` will return
+/// all node references contained within those proofs except the ones that are contained between
+/// `hash_a` and `hash_b` lexicographically.
+///
+/// Also returns the number of references strictly to the right of the bounds. If the right bound
+/// is unbounded (aka. not provided), all nodes to the right (inclusive) of the left bound will
+/// be counted. Leaf nodes are not counted (the leaf nodes within the proof do not count).
+type ProcessProofNodesResult = (Vec<(Nibbles, NodeHash)>, (Vec<u8>, Vec<u8>), usize);
+fn process_proof_nodes(
path: crates/common/trie/verify_range.rs
user: login: Arkenan
body: Before I start this comment, congrats on reducing this file to a fraction of what it was!

Now for my comment, is it possible to change the flow to something even simpler? Maybe something like follows:
1. call `from_nodes(proof)`. Get the initial trie with only the proofs.
2. `from_nodes` should return `true` if all nodes where inserted, `false` if not. We can use the `false` value to filter out malformed proofs (e.g. missing links).
3. insert the key-value pairs. This is already done up in the verifying function. These inserts should now override the hash, right? That means we don't need the removal of internal references as we previously did to recalculate the hash.
4. Calculate if there's any right references. We may need to revive the `has_right_element` function, but the complexity of that one is smaller than the complexity of this one.

By doing this we also could be able to remove the new `ValueOrHash` abstractions and all its appearances in the node types, which would simplify this PR even more.
created_at: 2025-05-23T17:16:05Z
author_association: COLLABORATOR
diff_hunk: @@ -505,14 +506,54 @@ impl LevmDatabase for RpcDB {
 /// calculate all posible child nodes.
 fn get_potential_child_nodes(proof: &[NodeRLP], key: &PathRLP) -> Option<Vec<Node>> {
     // TODO: Perhaps it's possible to calculate the child nodes instead of storing all possible ones.
-    let trie = Trie::from_nodes(
-        proof.first(),
-        &proof.iter().skip(1).cloned().collect::<Vec<_>>(),
-    )
-    .unwrap();
+    let proof_missing_key = match proof.split_first() {
path: crates/l2/prover/bench/src/rpc/db.rs
user: login: Arkenan
body: Can we not use from_nodes here as well?
created_at: 2025-05-23T17:17:16Z
author_association: COLLABORATOR
diff_hunk: @@ -43,127 +46,139 @@ pub type NodeRLP = Vec<u8>;
 
 /// Libmdx-based Ethereum Compatible Merkle Patricia Trie
 pub struct Trie {
-    /// Hash of the current node
-    root: Option<NodeHash>,
-    /// Contains the trie's nodes
-    pub(crate) state: TrieState,
+    db: Box<dyn TrieDB>,
+    root: NodeRef,
 }
 
 impl Trie {
     /// Creates a new Trie from a clean DB
     pub fn new(db: Box<dyn TrieDB>) -> Self {
         Self {
-            state: TrieState::new(db),
-            root: None,
+            db,
+            root: NodeRef::default(),
         }
     }
 
     /// Creates a trie from an already-initialized DB and sets root as the root node of the trie
     pub fn open(db: Box<dyn TrieDB>, root: H256) -> Self {
-        let root = (root != *EMPTY_TRIE_HASH).then_some(root.into());
         Self {
-            state: TrieState::new(db),
-            root,
+            db,
+            root: (root != *EMPTY_TRIE_HASH)
+                .then_some(NodeHash::from(root).into())
+                .unwrap_or_default(),
         }
     }
 
+    pub fn db(&self) -> &dyn TrieDB {
+        self.db.as_ref()
+    }
+
     /// Retrieve an RLP-encoded value from the trie given its RLP-encoded path.
     pub fn get(&self, path: &PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        if let Some(root) = &self.root {
-            let root_node = self
-                .state
-                .get_node(*root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            root_node.get(&self.state, Nibbles::from_bytes(path))
-        } else {
-            Ok(None)
-        }
+        Ok(match self.root {
+            NodeRef::Node(ref node, _) => node.get(self.db.as_ref(), Nibbles::from_bytes(path))?,
+            NodeRef::Hash(hash) if hash.is_valid() => {
+                Node::decode(&self.db.get(hash)?.ok_or(TrieError::InconsistentTree)?)
+                    .map_err(TrieError::RLPDecode)?
+                    .get(self.db.as_ref(), Nibbles::from_bytes(path))?
+            }
+            _ => None,
+        })
     }
 
     /// Insert an RLP-encoded value into the trie.
     pub fn insert(&mut self, path: PathRLP, value: ValueRLP) -> Result<(), TrieError> {
-        let root = self.root.take();
-        if let Some(root_node) = root
-            .map(|root| self.state.get_node(root))
-            .transpose()?
-            .flatten()
-        {
-            // If the trie is not empty, call the root node's insertion logic
-            let root_node = root_node.insert(&mut self.state, Nibbles::from_bytes(&path), value)?;
-            self.root = Some(root_node.insert_self(&mut self.state)?)
+        let path = Nibbles::from_bytes(&path);
+
+        self.root = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.db.as_ref(), path, value)?
+                .into()
         } else {
             // If the trie is empty, just add a leaf.
-            let new_leaf = Node::from(LeafNode::new(Nibbles::from_bytes(&path), value));
-            self.root = Some(new_leaf.insert_self(&mut self.state)?)
-        }
+            Node::from(LeafNode::new(path, value)).into()
+        };
+
         Ok(())
     }
 
     /// Remove a value from the trie given its RLP-encoded path.
     /// Returns the value if it was succesfully removed or None if it wasn't part of the trie
     pub fn remove(&mut self, path: PathRLP) -> Result<Option<ValueRLP>, TrieError> {
-        let root = self.root.take();
-        if let Some(root) = root {
-            let root_node = self
-                .state
-                .get_node(root)?
-                .ok_or(TrieError::InconsistentTree)?;
-            let (root_node, old_value) =
-                root_node.remove(&mut self.state, Nibbles::from_bytes(&path))?;
-            self.root = root_node
-                .map(|root| root.insert_self(&mut self.state))
-                .transpose()?;
-            Ok(old_value)
+        let value;
+        (self.root, value) = if self.root.is_valid() {
+            // If the trie is not empty, call the root node's removal logic.
+            let (node, value) = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .remove(self.db.as_ref(), Nibbles::from_bytes(&path))?;
+
+            (node.map(Into::into).unwrap_or_default(), value)
         } else {
-            Ok(None)
-        }
+            (NodeRef::default(), None)
+        };
+
+        Ok(value)
     }
 
     /// Return the hash of the trie's root node.
     /// Returns keccak(RLP_NULL) if the trie is empty
     /// Also commits changes to the DB
     pub fn hash(&mut self) -> Result<H256, TrieError> {
         self.commit()?;
-        Ok(self
-            .root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH))
+        Ok(self.hash_no_commit())
     }
 
     /// Return the hash of the trie's root node.
     /// Returns keccak(RLP_NULL) if the trie is empty
     pub fn hash_no_commit(&self) -> H256 {
-        self.root
-            .as_ref()
-            .map(|root| root.finalize())
-            .unwrap_or(*EMPTY_TRIE_HASH)
+        if self.root.is_valid() {
+            self.root.compute_hash().finalize()
+        } else {
+            *EMPTY_TRIE_HASH
+        }
     }
 
     pub fn commit(&mut self) -> Result<(), TrieError> {
-        if let Some(ref root) = self.root {
-            self.state.commit(root)?;
+        if self.root.is_valid() {
+            let mut acc = Vec::new();
+            self.root.commit(&mut acc);
+            self.db.put_batch(acc)?;
         }
+
         Ok(())
     }
 
     /// Obtain a merkle proof for the given path.
     /// The proof will contain all the encoded nodes traversed until reaching the node where the path is stored (including this last node).
     /// The proof will still be constructed even if the path is not stored in the trie, proving its absence.
+    ///
+    /// Note: This method has a different behavior in regard to non-existent trie root nodes. Normal
+    ///   behavior is to return `Err(InconsistentTrie)`, but this method will return
+    ///   `Ok(Vec::new())` instead.
     pub fn get_proof(&self, path: &PathRLP) -> Result<Vec<NodeRLP>, TrieError> {
-        // Will store all the encoded nodes traversed until reaching the node containing the path
-        let mut node_path = Vec::new();
-        let Some(root) = &self.root else {
-            return Ok(node_path);
-        };
-        // If the root is inlined, add it to the node_path
-        if let NodeHash::Inline(_) = root {
-            node_path.push(root.as_ref().to_vec());
-        }
-        if let Some(root_node) = self.state.get_node(*root)? {
-            root_node.get_path(&self.state, Nibbles::from_bytes(path), &mut node_path)?;
+        if self.root.is_valid() {
+            let hash = self.root.compute_hash();
+
+            let mut node_path = Vec::new();
path: crates/common/trie/trie.rs
user: login: azteca1998
body: Performance-wise there are a lot of clones of `Vec<T>` that could be removed before this change makes a significant change. It could be done but unless we [`shrink_to_fit`](https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#method.shrink_to_fit) after returning, we'll be using substantially more capacity than actually needed. Maybe it'd make sense if most of the nibbles ended up in branches, which is probably not the case for most paths given the growth factor of these trees.

`Vec::<T>::push` uses an [amortized growth strategy](https://doc.rust-lang.org/stable/std/vec/struct.Vec.html#method.push) of `O(1)`. This most likely means that it has quadratic growth (up to a limit): from capacity 0 may grow to 8, then 16, then 32...
created_at: 2025-05-23T17:34:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
path: crates/common/trie/node.rs
user: login: azteca1998
body: Because there are clones literally everywhere. Try running the `process.py` I sent you among the load test logs with `output-box.log`, then `output-arc+hash.log` (which is the fastest), the latter should be about 50% faster just by avoiding cloning `Box<Node>`.
created_at: 2025-05-23T17:37:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -356,6 +399,40 @@ impl IntoIterator for Trie {
     }
 }
 
+pub struct ProofTrie(Trie);
+
+impl ProofTrie {
+    pub fn insert(
+        &mut self,
+        partial_path: Nibbles,
+        external_ref: NodeHash,
+    ) -> Result<(), TrieError> {
+        self.0.root = if self.0.root.is_valid() {
+            // If the trie is not empty, call the root node's insertion logic.
+            self.0
+                .root
+                .get_node(self.0.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .insert(self.0.db.as_ref(), partial_path, external_ref)?
+                .into()
+        } else {
+            todo!()
path: crates/common/trie/trie.rs
user: login: azteca1998
body: It means I forgot something. Fixed.
created_at: 2025-05-23T17:54:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,48 +188,86 @@ impl Trie {
         &self,
         paths: &[PathRLP],
     ) -> Result<(Option<NodeRLP>, Vec<NodeRLP>), TrieError> {
-        let Some(root_node) = self
-            .root
-            .as_ref()
-            .map(|root| self.state.get_node(*root))
-            .transpose()?
-            .flatten()
-        else {
-            return Ok((None, Vec::new()));
-        };
+        if self.root.is_valid() {
+            let encoded_root = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .encode_raw();
+
+            let mut node_path = HashSet::new();
+            for path in paths {
+                let mut nodes = self.get_proof(path)?;
+                nodes.swap_remove(0);
+                node_path.extend(nodes);
+            }
 
-        let mut node_path = Vec::new();
-        for path in paths {
-            let mut nodes = self.get_proof(path)?;
-            nodes.swap_remove(0);
-            node_path.extend(nodes); // skip root node
+            Ok((Some(encoded_root), node_path.into_iter().collect()))
+        } else {
+            Ok((None, Vec::new()))
         }
-
-        // dedup
-        // TODO: really inefficient, by making the traversing smarter we can avoid having
-        // duplicates
-        let node_path: HashSet<_> = node_path.into_iter().collect();
-        let node_path = Vec::from_iter(node_path);
-        Ok((Some(root_node.encode_raw()), node_path))
     }
 
-    /// Creates a cached Trie (with [NullTrieDB]) from a list of encoded nodes.
-    /// Generally used in conjuction with [Trie::get_proofs].
-    pub fn from_nodes(
-        root_node: Option<&NodeRLP>,
-        other_nodes: &[NodeRLP],
-    ) -> Result<Self, TrieError> {
-        let mut trie = Trie::stateless();
+    /// Builds a trie from a set of nodes.
+    ///
+    /// Note: This method will not ensure neither that all node references are valid nor that there
+    ///   are no dangling nodes. Invalid references will be reflected in the final trie by
+    ///   `InconsistentTree` errors at runtime, whereas dangling nodes will be ignored.
path: crates/common/trie/trie.rs
user: login: azteca1998
body: I tried improving it a bit. Is it better now?
created_at: 2025-05-23T17:56:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -505,14 +506,54 @@ impl LevmDatabase for RpcDB {
 /// calculate all posible child nodes.
 fn get_potential_child_nodes(proof: &[NodeRLP], key: &PathRLP) -> Option<Vec<Node>> {
     // TODO: Perhaps it's possible to calculate the child nodes instead of storing all possible ones.
-    let trie = Trie::from_nodes(
-        proof.first(),
-        &proof.iter().skip(1).cloned().collect::<Vec<_>>(),
-    )
-    .unwrap();
+    let proof_missing_key = match proof.split_first() {
path: crates/l2/prover/bench/src/rpc/db.rs
user: login: azteca1998
body: This was made before the `from_nodes` function accepted inconsistent tries. Should I change back both `get_potential_child_nodes` functions?
created_at: 2025-05-23T17:58:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -173,48 +188,86 @@ impl Trie {
         &self,
         paths: &[PathRLP],
     ) -> Result<(Option<NodeRLP>, Vec<NodeRLP>), TrieError> {
-        let Some(root_node) = self
-            .root
-            .as_ref()
-            .map(|root| self.state.get_node(*root))
-            .transpose()?
-            .flatten()
-        else {
-            return Ok((None, Vec::new()));
-        };
+        if self.root.is_valid() {
+            let encoded_root = self
+                .root
+                .get_node(self.db.as_ref())?
+                .ok_or(TrieError::InconsistentTree)?
+                .encode_raw();
+
+            let mut node_path = HashSet::new();
+            for path in paths {
+                let mut nodes = self.get_proof(path)?;
+                nodes.swap_remove(0);
+                node_path.extend(nodes);
+            }
 
-        let mut node_path = Vec::new();
-        for path in paths {
-            let mut nodes = self.get_proof(path)?;
-            nodes.swap_remove(0);
-            node_path.extend(nodes); // skip root node
+            Ok((Some(encoded_root), node_path.into_iter().collect()))
+        } else {
+            Ok((None, Vec::new()))
         }
-
-        // dedup
-        // TODO: really inefficient, by making the traversing smarter we can avoid having
-        // duplicates
-        let node_path: HashSet<_> = node_path.into_iter().collect();
-        let node_path = Vec::from_iter(node_path);
-        Ok((Some(root_node.encode_raw()), node_path))
     }
 
-    /// Creates a cached Trie (with [NullTrieDB]) from a list of encoded nodes.
-    /// Generally used in conjuction with [Trie::get_proofs].
-    pub fn from_nodes(
-        root_node: Option<&NodeRLP>,
-        other_nodes: &[NodeRLP],
-    ) -> Result<Self, TrieError> {
-        let mut trie = Trie::stateless();
+    /// Builds a trie from a set of nodes.
+    ///
+    /// Note: This method will not ensure neither that all node references are valid nor that there
+    ///   are no dangling nodes. Invalid references will be reflected in the final trie by
+    ///   `InconsistentTree` errors at runtime, whereas dangling nodes will be ignored.
path: crates/common/trie/trie.rs
user: login: Arkenan
body: Looks great
created_at: 2025-05-23T18:04:14Z
author_association: COLLABORATOR
diff_hunk: @@ -64,384 +67,194 @@ pub fn verify_range(
     if keys.is_empty() {
         // We need to check that the proof confirms the non-existance of the first key
         // and that there are no more elements to the right of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-        let has_right_element = has_right_element(root, first_key.as_bytes(), &trie.state)?;
-        if has_right_element || !value.is_empty() {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, None))?;
+        if num_right_refs > 0 || !left_value.is_empty() {
             return Err(TrieError::Verify(
                 "no keys returned but more are available on the trie".to_string(),
             ));
         } else {
             return Ok(false);
-        }
+        };
     }
 
     let last_key = keys.last().unwrap();
 
     // Special Case: There is only one element and the two edge keys are the same
     if keys.len() == 1 && first_key == last_key {
         // We need to check that the proof confirms the existance of the first key
-        let value = fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
         if first_key != &keys[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid key".to_string(),
             ));
         }
-        if value != values[0] {
+        let (_, (left_value, _), num_right_refs) =
+            process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+        if left_value != values[0] {
             return Err(TrieError::Verify(
                 "correct proof but invalid data".to_string(),
             ));
         }
-        return has_right_element(root, first_key.as_bytes(), &trie.state);
+        return Ok(num_right_refs > 0);
     }
 
     // Regular Case: Two edge proofs
     if first_key >= last_key {
         return Err(TrieError::Verify("invalid edge keys".to_string()));
     }
-    // Fill up the state with the nodes from the proof
-    fill_state(&mut trie.state, root, first_key, &proof_nodes)?;
-    fill_state(&mut trie.state, root, last_key, &proof_nodes)?;
-    // Remove all references to the internal nodes that belong to the range so they can be reconstructed
-    let empty = remove_internal_references(root, first_key, last_key, &mut trie.state)?;
-    if !empty {
-        trie.root = Some(NodeHash::from(root));
-    }
+
+    // Process proofs to check if they are valid.
+    let (external_refs, _, num_right_refs) =
+        process_proof_nodes(proof, root.into(), (*first_key, Some(*last_key)))?;
+
     // Reconstruct the internal nodes by inserting the elements on the range
     for (key, value) in keys.iter().zip(values.iter()) {
         trie.insert(key.0.to_vec(), value.clone())?;
     }
-    // Check for elements to the right of the range before we wipe the sate
-    let has_right_element = has_right_element(root, last_key.as_bytes(), &trie.state)?;
+
+    // Fill up the state with the nodes from the proof
+    let mut trie = ProofTrie::from(trie);
+    for (partial_path, external_ref) in external_refs {
+        trie.insert(partial_path, external_ref)?;
+    }
+
     // Check that the hash is the one we expected (aka the trie was properly reconstructed from the edge proofs and the range)
-    let hash = trie.hash()?;
+    let hash = trie.hash();
     if hash != root {
         return Err(TrieError::Verify(format!(
             "invalid proof, expected root hash {}, got  {}",
             root, hash
         )));
     }
-    Ok(has_right_element)
+    Ok(num_right_refs > 0)
 }
 
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_state(
-    trie_state: &mut TrieState,
-    root_hash: H256,
-    first_key: &H256,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let mut path = Nibbles::from_bytes(&first_key.0);
-    fill_node(
-        &mut path,
-        &NodeHash::from(root_hash),
-        trie_state,
-        proof_nodes,
-    )
-}
-
-/// Fills up the TrieState with nodes from the proof traversing the path given by first_key
-/// Returns an error if there are gaps in the proof node path
-/// Also returns the value if it is part of the proof
-fn fill_node(
-    path: &mut Nibbles,
-    node_hash: &NodeHash,
-    trie_state: &mut TrieState,
-    proof_nodes: &ProofNodeStorage,
-) -> Result<Vec<u8>, TrieError> {
-    let node = proof_nodes.get_node(node_hash)?;
-    let child_hash = get_child(path, &node);
-    if let Some(ref child_hash) = child_hash {
-        trie_state.insert_node(node, *node_hash);
-        fill_node(path, child_hash, trie_state, proof_nodes)
-    } else {
-        let value = match &node {
-            Node::Branch(n) => n.value.clone(),
-            Node::Extension(_) => vec![],
-            Node::Leaf(n) => (*path == n.partial)
-                .then_some(n.value.clone())
-                .unwrap_or_default(),
-        };
-        trie_state.insert_node(node, *node_hash);
-        Ok(value)
+/// Iterate over all provided proofs starting from the root and generate a set of hashes that fall
+/// outside the verification bounds.
+///
+/// For example, calling this function with the proofs for the range `(hash_a, hash_b)` will return
+/// all node references contained within those proofs except the ones that are contained between
+/// `hash_a` and `hash_b` lexicographically.
+///
+/// Also returns the number of references strictly to the right of the bounds. If the right bound
+/// is unbounded (aka. not provided), all nodes to the right (inclusive) of the left bound will
+/// be counted. Leaf nodes are not counted (the leaf nodes within the proof do not count).
+type ProcessProofNodesResult = (Vec<(Nibbles, NodeHash)>, (Vec<u8>, Vec<u8>), usize);
+fn process_proof_nodes(
path: crates/common/trie/verify_range.rs
user: login: azteca1998
body: > These inserts should now override the hash, right?
No, they will cause an `InconsistentTrie` error. Internal references should have already been removed.

I think this is the correct behavior because the external reference will have its own (unknown) insertion logic. By overriding the external reference we're assuming that the external reference is only to a leaf with exactly the same partial path at the insertion point.
created_at: 2025-05-23T18:07:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -505,14 +506,54 @@ impl LevmDatabase for RpcDB {
 /// calculate all posible child nodes.
 fn get_potential_child_nodes(proof: &[NodeRLP], key: &PathRLP) -> Option<Vec<Node>> {
     // TODO: Perhaps it's possible to calculate the child nodes instead of storing all possible ones.
-    let trie = Trie::from_nodes(
-        proof.first(),
-        &proof.iter().skip(1).cloned().collect::<Vec<_>>(),
-    )
-    .unwrap();
+    let proof_missing_key = match proof.split_first() {
path: crates/l2/prover/bench/src/rpc/db.rs
user: login: Arkenan
body: Unless we have a big reason against it, yes. I would reuse that function.
created_at: 2025-05-23T18:08:55Z
author_association: COLLABORATOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
path: crates/common/trie/node.rs
user: login: Arkenan
body: Awesome
created_at: 2025-05-23T18:18:18Z
author_association: COLLABORATOR
diff_hunk: @@ -142,7 +142,15 @@ async fn heal_state_batch(
                 }
             }
             // Write nodes to trie
-            trie.state_mut().write_node_batch(&nodes)?;
+            trie.db().put_batch(
+                nodes
+                    .into_iter()
+                    .filter_map(|node| match node.compute_hash() {
+                        hash @ NodeHash::Hashed(_) => Some((hash, node.encode_to_vec())),
+                        NodeHash::Inline(_) => None,
+                    })
path: crates/networking/p2p/sync/state_healing.rs
user: login: Oppen
body: Why is this needed now but not before?
created_at: 2025-05-27T19:02:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
+    /// The node is in the database, referenced by its hash.
+    Hash(NodeHash),
+}
+
+impl NodeRef {
+    pub const fn const_default() -> Self {
+        Self::Hash(NodeHash::const_default())
+    }
+
+    pub fn get_node(&self, db: &dyn TrieDB) -> Result<Option<Node>, TrieError> {
+        match *self {
+            NodeRef::Node(ref node, _) => Ok(Some(node.as_ref().clone())),
+            NodeRef::Hash(NodeHash::Inline((data, len))) => {
path: crates/common/trie/node.rs
user: login: Oppen
body: It's also only really usable on branch nodes, so maybe making it a part of the branch node instead might reduce cognitive load everywhere else.
created_at: 2025-05-27T19:08:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -142,7 +142,15 @@ async fn heal_state_batch(
                 }
             }
             // Write nodes to trie
-            trie.state_mut().write_node_batch(&nodes)?;
+            trie.db().put_batch(
+                nodes
+                    .into_iter()
+                    .filter_map(|node| match node.compute_hash() {
+                        hash @ NodeHash::Hashed(_) => Some((hash, node.encode_to_vec())),
+                        NodeHash::Inline(_) => None,
+                    })
path: crates/networking/p2p/sync/state_healing.rs
user: login: azteca1998
body: It's what the `write_node_batch` function did internally.
created_at: 2025-05-28T15:02:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
path: crates/common/trie/node.rs
user: login: Oppen
body: And why the `OnceLock`?
created_at: 2025-05-28T16:28:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
path: crates/common/trie/node.rs
user: login: azteca1998
body: Due to the requirements it has to be a threadsafe container, we could have a `Mutex<Option<T>>` but `OnceLock<T>` works perfect in this case since it has the `.get_or_init()` method and also support reset using `.take()`.

If you're referring about why an inner mutability container, the `Node::compute_hash()` accepts an immutable self and is part of the public API. Having the OnceLock avoids changing the public API.
created_at: 2025-05-28T16:52:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,16 +2,134 @@ mod branch;
 mod extension;
 mod leaf;
 
-use std::array;
+use std::{
+    array,
+    sync::{Arc, OnceLock},
+};
 
 pub use branch::BranchNode;
-use ethrex_rlp::{decode::decode_bytes, error::RLPDecodeError, structs::Decoder};
+use ethrex_rlp::{
+    decode::{decode_bytes, RLPDecode},
+    encode::RLPEncode,
+    error::RLPDecodeError,
+    structs::Decoder,
+};
 pub use extension::ExtensionNode;
 pub use leaf::LeafNode;
 
-use crate::{error::TrieError, nibbles::Nibbles};
+use crate::{error::TrieError, nibbles::Nibbles, TrieDB};
 
-use super::{node_hash::NodeHash, state::TrieState, ValueRLP};
+use super::{node_hash::NodeHash, ValueRLP};
+
+/// A reference to a node.
+#[derive(Clone, Debug)]
+pub enum NodeRef {
+    /// The node is embedded within the reference.
+    Node(Arc<Node>, OnceLock<NodeHash>),
+    /// The node is in the database, referenced by its hash.
+    Hash(NodeHash),
+}
+
+impl NodeRef {
+    pub const fn const_default() -> Self {
+        Self::Hash(NodeHash::const_default())
+    }
+
+    pub fn get_node(&self, db: &dyn TrieDB) -> Result<Option<Node>, TrieError> {
+        match *self {
+            NodeRef::Node(ref node, _) => Ok(Some(node.as_ref().clone())),
+            NodeRef::Hash(NodeHash::Inline((data, len))) => {
path: crates/common/trie/node.rs
user: login: Arkenan
body: We opened issue #2915 for this. It will be tackled in a separate PR
created_at: 2025-05-28T16:55:06Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-07T13:24:14Z
body: <h2>Lines of code report</h2>

Total lines added: `206`
Total lines removed: `147`
Total lines changed: `353`

<details>
<summary>Detailed view</summary>

```
+------------------------------------------------------+-------+------+
| File                                                 | Lines | Diff |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/node.rs                    | 241   | +92  |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/node/branch.rs             | 538   | +3   |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/node/extension.rs          | 430   | +16  |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/node/leaf.rs               | 264   | -1   |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/rlp.rs                     | 121   | +14  |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/trie.rs                    | 870   | +60  |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/trie_iter.rs               | 94    | +4   |
+------------------------------------------------------+-------+------+
| ethrex/crates/common/trie/verify_range.rs            | 469   | -146 |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/state_healing.rs   | 136   | +8   |
+------------------------------------------------------+-------+------+
| ethrex/crates/networking/p2p/sync/storage_healing.rs | 115   | +9   |
+------------------------------------------------------+-------+------+

```
</details>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-07T13:27:05Z
body: ## Benchmark for ffa87eb
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 40.6±2.67ms | **38.1±1.49ms** | **-6.16%** |
| Trie/cita-trie insert 1k | **3.7±0.03ms** | 3.8±0.05ms | **+2.70%** |
| Trie/ethrex-trie insert 10k | **132.8±2.12ms** | 159.1±2.13ms | **+19.80%** |
| Trie/ethrex-trie insert 1k | **11.1±0.15ms** | 13.7±0.40ms | **+23.42%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-14T18:37:50Z
body: ## Benchmark for 1c71d11
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 40.6±4.43ms | 39.1±4.34ms | -3.69% |
| Trie/cita-trie insert 1k | 3.7±0.07ms | 3.7±0.08ms | 0.00% |
| Trie/ethrex-trie insert 10k | **135.1±5.03ms** | 20.6±0.98s | **+15147.96%** |
| Trie/ethrex-trie insert 1k | **11.1±0.28ms** | 160.5±3.48ms | **+1345.95%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-14T22:51:03Z
body: ## Benchmark for f2fefcb
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 34.0±0.37ms | **31.1±0.43ms** | **-8.53%** |
| Trie/cita-trie insert 1k | 3.4±0.05ms | **3.2±0.03ms** | **-5.88%** |
| Trie/ethrex-trie insert 10k | **114.6±1.28ms** | 16.3±0.44s | **+14123.39%** |
| Trie/ethrex-trie insert 1k | **10.1±0.11ms** | 156.0±3.87ms | **+1444.55%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T13:28:05Z
body: ## Benchmark for eb0b028
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 37.0±1.10ms | 36.8±1.95ms | -0.54% |
| Trie/cita-trie insert 1k | 3.7±0.04ms | **3.6±0.02ms** | **-2.70%** |
| Trie/ethrex-trie insert 10k | **132.3±2.41ms** | 18.4±0.60s | **+13807.79%** |
| Trie/ethrex-trie insert 1k | **11.3±0.38ms** | 164.7±3.50ms | **+1357.52%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-15T16:39:01Z
body: ## Benchmark for 3db09e5
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 35.6±0.76ms | 35.6±0.51ms | 0.00% |
| Trie/cita-trie insert 1k | 3.7±0.04ms | 3.7±0.04ms | 0.00% |
| Trie/ethrex-trie insert 10k | **128.1±2.26ms** | 16.8±0.43s | **+13014.75%** |
| Trie/ethrex-trie insert 1k | **11.1±0.18ms** | 158.2±2.57ms | **+1325.23%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T11:48:41Z
body: ## Benchmark for 80e45a7
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 35.9±0.68ms | 36.5±1.44ms | +1.67% |
| Trie/cita-trie insert 1k | 3.7±0.04ms | 3.7±0.03ms | 0.00% |
| Trie/ethrex-trie insert 10k | **128.9±1.16ms** | 18.7±0.75s | **+14407.37%** |
| Trie/ethrex-trie insert 1k | **11.1±0.21ms** | 163.2±1.26ms | **+1370.27%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T15:25:19Z
body: ## Benchmark for 48f8dd6
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | **35.1±0.87ms** | 36.7±1.93ms | **+4.56%** |
| Trie/cita-trie insert 1k | 3.6±0.09ms | 3.6±0.06ms | 0.00% |
| Trie/ethrex-trie insert 10k | **124.2±4.20ms** | 18.8±1.75s | **+15036.88%** |
| Trie/ethrex-trie insert 1k | **11.1±0.22ms** | 157.1±2.09ms | **+1315.32%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T15:40:42Z
body: ## Benchmark for e5cd2a7
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 37.7±0.98ms | **35.8±1.01ms** | **-5.04%** |
| Trie/cita-trie insert 1k | 3.7±0.06ms | **3.6±0.03ms** | **-2.70%** |
| Trie/ethrex-trie insert 10k | **138.2±1.63ms** | 27.7±2.37s | **+19943.42%** |
| Trie/ethrex-trie insert 1k | **11.4±0.07ms** | 159.2±2.45ms | **+1296.49%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T18:47:58Z
body: ## Benchmark for 1b8ad33
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 35.9±0.86ms | 35.9±0.80ms | 0.00% |
| Trie/cita-trie insert 1k | 3.7±0.05ms | 3.7±0.13ms | 0.00% |
| Trie/ethrex-trie insert 10k | 128.4±2.36ms | **56.7±0.67ms** | **-55.84%** |
| Trie/ethrex-trie insert 1k | 11.1±0.07ms | **6.8±0.26ms** | **-38.74%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-16T20:31:19Z
body: ## Benchmark Block Execution Results Comparison Against Main

| Command | Mean [s] | Min [s] | Max [s] | Relative |
|:---|---:|---:|---:|---:|
| `base` | 203.264 ± 0.939 | 201.749 | 204.545 | 1.06 ± 0.01 |
| `head` | 192.460 ± 1.014 | 191.037 | 193.843 | 1.00 |

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-19T14:00:48Z
body: ## Benchmark for 677a2e8
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 41.4±1.91ms | **33.6±2.03ms** | **-18.84%** |
| Trie/cita-trie insert 1k | 3.7±0.03ms | **3.6±0.05ms** | **-2.70%** |
| Trie/ethrex-trie insert 10k | 137.9±3.20ms | **63.2±1.54ms** | **-54.17%** |
| Trie/ethrex-trie insert 1k | 11.5±0.17ms | **6.8±0.31ms** | **-40.87%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-21T17:17:10Z
body: ## Benchmark for 24ddab1
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 36.0±1.59ms | **34.8±0.59ms** | **-3.33%** |
| Trie/cita-trie insert 1k | 3.7±0.05ms | 3.7±0.13ms | 0.00% |
| Trie/ethrex-trie insert 10k | 127.5±2.66ms | **57.7±0.73ms** | **-54.75%** |
| Trie/ethrex-trie insert 1k | 11.1±0.10ms | **6.7±0.03ms** | **-39.64%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T12:23:17Z
body: ## Benchmark for 5b7127c
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 36.7±1.51ms | 38.2±2.47ms | +4.09% |
| Trie/cita-trie insert 1k | 3.6±0.01ms | 3.6±0.05ms | 0.00% |
| Trie/ethrex-trie insert 10k | 132.3±1.65ms | **59.2±1.09ms** | **-55.25%** |
| Trie/ethrex-trie insert 1k | 11.1±0.13ms | **6.7±0.21ms** | **-39.64%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T12:49:30Z
body: ## Benchmark for 4fec24e
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | **32.1±1.26ms** | 39.2±1.96ms | **+22.12%** |
| Trie/cita-trie insert 1k | 3.7±0.03ms | 3.7±0.18ms | 0.00% |
| Trie/ethrex-trie insert 10k | 135.4±3.14ms | **61.1±1.39ms** | **-54.87%** |
| Trie/ethrex-trie insert 1k | 11.2±0.20ms | **6.6±0.06ms** | **-41.07%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T13:01:02Z
body: ## Benchmark for 87221b4
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 36.5±1.49ms | 36.2±1.87ms | -0.82% |
| Trie/cita-trie insert 1k | 3.7±0.04ms | 3.7±0.13ms | 0.00% |
| Trie/ethrex-trie insert 10k | 130.7±2.36ms | **59.4±1.18ms** | **-54.55%** |
| Trie/ethrex-trie insert 1k | 11.1±0.06ms | **6.6±0.03ms** | **-40.54%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T15:28:05Z
body: ## Benchmark for 13158c5
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 40.3±2.78ms | **34.1±2.33ms** | **-15.38%** |
| Trie/cita-trie insert 1k | 3.7±0.05ms | 3.7±0.16ms | 0.00% |
| Trie/ethrex-trie insert 10k | 136.2±1.94ms | **61.5±2.49ms** | **-54.85%** |
| Trie/ethrex-trie insert 1k | 11.2±0.15ms | **6.7±0.10ms** | **-40.18%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T15:35:18Z
body: ## Benchmark for 37a4d2b
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | **34.9±0.34ms** | 40.3±1.47ms | **+15.47%** |
| Trie/cita-trie insert 1k | 3.6±0.06ms | 3.6±0.06ms | 0.00% |
| Trie/ethrex-trie insert 10k | 126.3±2.33ms | **60.8±1.18ms** | **-51.86%** |
| Trie/ethrex-trie insert 1k | 11.1±0.13ms | **6.7±0.32ms** | **-39.64%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T16:16:59Z
body: ## Benchmark for 4ff614d
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 38.9±1.87ms | 38.8±2.24ms | -0.26% |
| Trie/cita-trie insert 1k | 3.7±0.02ms | 3.7±0.14ms | 0.00% |
| Trie/ethrex-trie insert 10k | 131.6±2.97ms | **59.8±0.89ms** | **-54.56%** |
| Trie/ethrex-trie insert 1k | 11.2±0.62ms | **6.7±0.10ms** | **-40.18%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-22T16:19:04Z
body: ## Benchmark for 614171d
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 36.4±1.39ms | **34.5±1.51ms** | **-5.22%** |
| Trie/cita-trie insert 1k | 3.7±0.02ms | **3.6±0.08ms** | **-2.70%** |
| Trie/ethrex-trie insert 10k | 124.4±2.40ms | **56.2±0.38ms** | **-54.82%** |
| Trie/ethrex-trie insert 1k | 11.1±0.06ms | **6.5±0.03ms** | **-41.44%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-23T17:49:47Z
body: ## Benchmark for c0d893f
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 35.5±0.56ms | **34.7±0.45ms** | **-2.25%** |
| Trie/cita-trie insert 1k | 3.7±0.04ms | **3.6±0.16ms** | **-2.70%** |
| Trie/ethrex-trie insert 10k | 125.6±2.01ms | **57.2±1.40ms** | **-54.46%** |
| Trie/ethrex-trie insert 1k | 11.1±0.13ms | **6.5±0.08ms** | **-41.44%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-23T17:57:33Z
body: ## Benchmark for ac86e9e
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | 36.3±1.64ms | 36.2±1.52ms | -0.28% |
| Trie/cita-trie insert 1k | 3.6±0.05ms | 3.6±0.17ms | 0.00% |
| Trie/ethrex-trie insert 10k | 129.3±2.72ms | **57.1±0.61ms** | **-55.84%** |
| Trie/ethrex-trie insert 1k | 11.0±0.05ms | **6.6±0.05ms** | **-40.00%** |

  </details>
  
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-27T12:19:32Z
body: ## Benchmark for 7908baf
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Trie/cita-trie insert 10k | **35.2±0.90ms** | 36.6±1.68ms | **+3.98%** |
| Trie/cita-trie insert 1k | **3.6±0.03ms** | 3.7±0.13ms | **+2.78%** |
| Trie/ethrex-trie insert 10k | 134.8±1.70ms | **58.6±2.67ms** | **-56.53%** |
| Trie/ethrex-trie insert 1k | 11.1±0.22ms | **6.6±0.04ms** | **-40.54%** |

  </details>
  
author_association: NONE