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}