alloy_trie/proof/
retainer.rs

1use crate::{
2    Nibbles, TrieMask,
3    proof::{ProofNodes, added_removed_keys::AddedRemovedKeys},
4};
5use alloy_primitives::Bytes;
6use alloy_rlp::EMPTY_STRING_CODE;
7use tracing::trace;
8
9use alloc::vec::Vec;
10
11/// Tracks various datapoints related to already-seen proofs which may or may not have been
12/// retained in the outer [`ProofRetainer`]. Used to support retention of extra proofs which are
13/// required to support leaf additions/removals but aren't in the target set.
14///
15/// "target" refers those paths/proofs which are retained by the [`ProofRetainer`], as determined
16/// by its `target` field.
17#[derive(Default, Clone, Debug)]
18struct AddedRemovedKeysTracking {
19    /// Path of the last branch which was passed to `retain_branch_proof`, regardless of whether it
20    /// was a target or not.
21    last_branch_path: Nibbles,
22    /// Proof of the node at `last_branch_path`.
23    last_branch_proof: Vec<u8>,
24    /// Path of the last node which was not found in the `ProofRetainer`'s target set, and which is
25    /// not potentially removed as given by [`AddedRemovedKeys::get_removed_mask`]
26    last_nontarget_path: Nibbles,
27    /// Proof of the node at `last_nontarget_path`.
28    last_nontarget_proof: Vec<u8>,
29}
30
31impl AddedRemovedKeysTracking {
32    /// Stores the path and proof of the most recently seen path/proof which were not retained and
33    /// are not potentially removed.
34    fn track_nontarget(&mut self, path: &Nibbles, proof: &[u8]) {
35        trace!(target: "trie::proof_retainer", ?path, "Tracking non-target");
36        self.last_nontarget_path = *path;
37        self.last_nontarget_proof.clear();
38        self.last_nontarget_proof.extend(proof);
39    }
40
41    /// Stores the path and proof of the most recently seen branch.
42    fn track_branch(&mut self, path: &Nibbles, proof: &[u8]) {
43        trace!(target: "trie::proof_retainer", ?path, "Tracking branch");
44        self.last_branch_path = *path;
45        self.last_branch_proof.clear();
46        self.last_branch_proof.extend(proof);
47    }
48
49    /// Checks if an extension node is parent to the last tracked branch node.
50    fn extension_child_is_nontarget(&self, path: &Nibbles, short_key: &Nibbles) -> bool {
51        path.len() + short_key.len() == self.last_nontarget_path.len()
52            && self.last_nontarget_path.starts_with(path)
53            && self.last_nontarget_path.ends_with(short_key)
54    }
55
56    /// Checks if a branch node is parent to the last non-target node.
57    fn branch_child_is_nontarget(&self, path: &Nibbles, child_nibble: u8) -> bool {
58        path.len() + 1 == self.last_nontarget_path.len()
59            && self.last_nontarget_path.starts_with(path)
60            && self.last_nontarget_path.last().expect("path length >= 1") == child_nibble
61    }
62}
63
64/// Proof retainer is used to store proofs during merkle trie construction.
65/// It is intended to be used within the [`HashBuilder`](crate::HashBuilder).
66///
67/// When using the `retain_leaf_proof`, `retain_extension_proof`, and `retain_branch_proof`
68/// methods, it is required that the calls are ordered such that proofs of parent nodes are
69/// retained after their children.
70#[derive(Default, Clone, Debug)]
71pub struct ProofRetainer<K = AddedRemovedKeys> {
72    /// The nibbles of the target trie keys to retain proofs for.
73    targets: Vec<Nibbles>,
74    /// The map retained trie node keys to RLP serialized trie nodes.
75    proof_nodes: ProofNodes,
76    /// Provided by the user to give the necessary context to retain extra proofs.
77    added_removed_keys: Option<K>,
78    /// Tracks data related to previously seen proofs; required for certain edge-cases where we
79    /// want to keep proofs for nodes which aren't in the target set.
80    added_removed_tracking: AddedRemovedKeysTracking,
81}
82
83impl FromIterator<Nibbles> for ProofRetainer {
84    fn from_iter<T: IntoIterator<Item = Nibbles>>(iter: T) -> Self {
85        Self::new(FromIterator::from_iter(iter))
86    }
87}
88
89impl ProofRetainer {
90    /// Create new retainer with target nibbles.
91    pub fn new(targets: Vec<Nibbles>) -> Self {
92        Self { targets, ..Default::default() }
93    }
94}
95
96impl<K> ProofRetainer<K> {
97    /// Configures the retainer to retain proofs for certain nodes which would otherwise fall
98    /// outside the target set, when those nodes might be required to calculate the state root when
99    /// keys have been added or removed to the trie.
100    ///
101    /// If None is given then retention of extra proofs is disabled.
102    pub fn with_added_removed_keys<K2>(self, added_removed_keys: Option<K2>) -> ProofRetainer<K2> {
103        ProofRetainer {
104            targets: self.targets,
105            proof_nodes: self.proof_nodes,
106            added_removed_keys,
107            added_removed_tracking: self.added_removed_tracking,
108        }
109    }
110}
111
112impl<K: AsRef<AddedRemovedKeys>> ProofRetainer<K> {
113    /// Returns `true` if the given prefix matches the retainer target.
114    pub fn matches(&self, prefix: &Nibbles) -> bool {
115        prefix.is_empty() || self.targets.iter().any(|target| target.starts_with(prefix))
116    }
117
118    /// Returns all collected proofs.
119    pub fn into_proof_nodes(self) -> ProofNodes {
120        self.proof_nodes
121    }
122
123    /// Retain the proof if the key matches any of the targets.
124    ///
125    /// Usage of this method should be replaced with usage of the following methods, each dependent
126    /// on the node-type whose proof being retained:
127    /// - `retain_empty_root_proof`
128    /// - `retain_leaf_proof`
129    /// - `retain_extension_proof`
130    /// - `retain_branch_proof`
131    #[deprecated]
132    pub fn retain(&mut self, prefix: &Nibbles, proof: &[u8]) {
133        if self.matches(prefix) {
134            self.retain_unchecked(*prefix, Bytes::copy_from_slice(proof));
135        }
136    }
137
138    /// Retain the proof with no checks being performed.
139    fn retain_unchecked(&mut self, path: Nibbles, proof: Bytes) {
140        trace!(
141            target: "trie::proof_retainer",
142            path = ?path,
143            proof = alloy_primitives::hex::encode(&proof),
144            "Retaining proof",
145        );
146        self.proof_nodes.insert(path, proof);
147    }
148
149    /// Retains a proof for an empty root.
150    pub fn retain_empty_root_proof(&mut self) {
151        self.retain_unchecked(Nibbles::default(), Bytes::from_static(&[EMPTY_STRING_CODE]))
152    }
153
154    /// Tracks the proof in the [`AddedRemovedKeysTracking`] if:
155    /// - Tracking is enabled
156    /// - Path is not root
157    /// - The path is not a removed child as given by [`AddedRemovedKeys`]
158    ///
159    /// Non-target tracking is only used for retaining of extra branch children in cases where the
160    /// branch is getting deleted, hence why root node is not kept.
161    fn maybe_track_nontarget(&mut self, path: &Nibbles, proof: &[u8]) {
162        if let Some(added_removed_keys) = self.added_removed_keys.as_ref() {
163            if path.is_empty() {
164                return;
165            }
166
167            let branch_path = path.slice_unchecked(0, path.len() - 1);
168            let child_bit = path.get_unchecked(path.len() - 1);
169            let removed_mask = added_removed_keys.as_ref().get_removed_mask(&branch_path);
170            if !removed_mask.is_bit_set(child_bit) {
171                self.added_removed_tracking.track_nontarget(path, proof)
172            }
173        }
174    }
175
176    /// Retains a proof for a leaf node.
177    pub fn retain_leaf_proof(&mut self, path: &Nibbles, proof: &[u8]) {
178        if self.matches(path) {
179            self.retain_unchecked(*path, Bytes::copy_from_slice(proof));
180        } else {
181            self.maybe_track_nontarget(path, proof);
182        }
183    }
184
185    /// Retains a proof for an extension node.
186    pub fn retain_extension_proof(&mut self, path: &Nibbles, short_key: &Nibbles, proof: &[u8]) {
187        if self.matches(path) {
188            self.retain_unchecked(*path, Bytes::copy_from_slice(proof));
189
190            if let Some(added_removed_keys) = self.added_removed_keys.as_ref() {
191                // When a new leaf is being added to a trie, it can happen that an extension node's
192                // path is a prefix of the new leaf's, but the extension child's path is not. In
193                // this case a new branch node is created; the new leaf is one child, the previous
194                // branch node is its other, and the extension node is its parent.
195                //
196                //            Before │ After
197                //                   │
198                //   ┌───────────┐   │    ┌───────────┐
199                //   │ Extension │   │    │ Extension │
200                //   └─────┬─────┘   │    └─────┬─────┘
201                //         │         │          │
202                //         │         │    ┌─────┴──────┐
203                //         │         │    │ New Branch │
204                //         │         │    └─────┬───┬──┘
205                //         │         │          │   └─────┐
206                //   ┌─────┴────┐    │    ┌─────┴────┐  ┌─┴────────┐
207                //   │  Branch  │    │    │  Branch  │  │ New Leaf │
208                //   └──────────┘    │    └──────────┘  └──────────┘
209                //
210                // In this case the new leaf's proof will be retained, as will the extension's,
211                // because its path is a prefix of the leaf's. But the old branch's proof won't
212                // necessarily be retained, as its path is not a prefix of the leaf's.
213                //
214                // In order to support this case we can optimistically retain the proof for
215                // non-target children of target extensions.
216                //
217                let is_prefix_added = added_removed_keys.as_ref().is_prefix_added(path);
218                let extension_child_is_nontarget =
219                    self.added_removed_tracking.extension_child_is_nontarget(path, short_key);
220                trace!(
221                    target: "trie::proof_retainer",
222                    ?path,
223                    ?short_key,
224                    ?is_prefix_added,
225                    ?extension_child_is_nontarget,
226                    "Deciding to retain non-target extension child",
227                );
228                if is_prefix_added && extension_child_is_nontarget {
229                    let last_branch_path = self.added_removed_tracking.last_branch_path;
230                    let last_branch_proof =
231                        Bytes::copy_from_slice(&self.added_removed_tracking.last_branch_proof);
232                    self.retain_unchecked(last_branch_path, last_branch_proof);
233                }
234            }
235        } else {
236            self.maybe_track_nontarget(path, proof);
237        }
238    }
239
240    /// Retains a proof for a branch node.
241    pub fn retain_branch_proof(&mut self, path: &Nibbles, state_mask: TrieMask, proof: &[u8]) {
242        if self.matches(path) {
243            self.retain_unchecked(*path, Bytes::copy_from_slice(proof));
244
245            if let Some(added_removed_keys) = self.added_removed_keys.as_ref() {
246                // When we remove all but one child from a branch, that branch gets "collapsed"
247                // into its parent branch/extension, i.e. it is deleted and the remaining child is
248                // adopted by its grandparent.
249                //
250                //                           Before │ After
251                //                                  │
252                //   ┌───────────────┐              │    ┌───────────────┐
253                //   │  Grandparent  │              │    │  Grandparent  │
254                //   │  Branch       │              │    │  Branch       │
255                //   └──┬───┬───┬────┘              │    └──┬───┬───┬────┘
256                //      :   :   │                   │       :   :   │
257                //              │                   │               │
258                //   ┌──────────┴──────┐            │               │
259                //   │  Parent Branch  │            │               │
260                //   └──────────┬──┬───┘            │               │
261                //              │  └─────┐          │               │
262                //   ┌──────────┴─────┐ ┌┴─────┐    │    ┌──────────┴─────┐
263                //   │  Child Branch  │ │ Leaf │    │    │  Child Branch  │
264                //   └──┬───┬───┬─────┘ └──────┘    │    └──┬───┬───┬─────┘
265                //      :   :   :                   │       :   :   :
266                //
267                // The adopted child can also be a leaf or extension, which can affect what happens
268                // to the grandparent, so to perform a collapse we must retain a proof of the
269                // adopted child.
270                //
271                // The proofs of the removed children will be retained if they are in the `target`
272                // set, but it can happen that the remaining child will not be in the `target` set
273                // and so would not be retained.
274                //
275                // Using `removed_keys` we can discern if there is one remaining child in a branch
276                // which is not a target, and optimistically retain that child if so.
277                //
278                let removed_mask = added_removed_keys.as_ref().get_removed_mask(path);
279                let nonremoved_mask = !removed_mask & state_mask;
280                let branch_child_is_nontarget = self
281                    .added_removed_tracking
282                    .branch_child_is_nontarget(path, nonremoved_mask.trailing_zeros() as u8);
283
284                trace!(
285                    target: "trie::proof_retainer",
286                    ?path,
287                    ?removed_mask,
288                    ?state_mask,
289                    ?nonremoved_mask,
290                    ?branch_child_is_nontarget,
291                    "Deciding to retain non-target branch child",
292                );
293
294                if nonremoved_mask.count_ones() == 1 && branch_child_is_nontarget {
295                    let last_nontarget_path = self.added_removed_tracking.last_nontarget_path;
296                    let last_nontarget_proof =
297                        Bytes::copy_from_slice(&self.added_removed_tracking.last_nontarget_proof);
298                    self.retain_unchecked(last_nontarget_path, last_nontarget_proof);
299                }
300            }
301        } else {
302            self.maybe_track_nontarget(path, proof);
303        }
304
305        if self.added_removed_keys.is_some() {
306            self.added_removed_tracking.track_branch(path, proof)
307        }
308    }
309}