reth_trie/proof_v2/target.rs
1use reth_trie_common::{Nibbles, ProofV2Target};
2
3// A helper function for getting the largest prefix of the sub-trie which contains a particular
4// target, based on its `min_len`.
5//
6// A target will only match nodes which share the target's prefix, where the target's prefix is
7// the first `min_len` nibbles of its key. E.g. a target with `key` 0xabcd and `min_len` 2 will
8// only match nodes with prefix 0xab.
9//
10// In general the target will only match within the sub-trie whose prefix is identical to the
11// target's. However there is an exception:
12//
13// Given a trie with a node at 0xabc, there must be a branch at 0xab. A target with prefix 0xabc
14// needs to match that node, but the branch at 0xab must be constructed order to know the node
15// is at that path. Therefore the sub-trie prefix is the target prefix with a nibble truncated.
16//
17// For a target with an empty prefix (`min_len` of 0) we still use an empty sub-trie prefix;
18// this will still construct the branch at the root node (if there is one). Targets with
19// `min_len` of both 0 and 1 will therefore construct the root node, but only those with
20// `min_len` of 0 will retain it.
21#[inline]
22pub(crate) fn sub_trie_prefix(target: &ProofV2Target) -> Nibbles {
23 let mut sub_trie_prefix = target.key_nibbles;
24 sub_trie_prefix.truncate(target.min_len.saturating_sub(1) as usize);
25 sub_trie_prefix
26}
27
28// A helper function which returns the first path following a sub-trie in lexicographical order.
29#[inline]
30fn sub_trie_upper_bound(sub_trie_prefix: &Nibbles) -> Option<Nibbles> {
31 sub_trie_prefix.next_without_prefix()
32}
33
34/// Describes a set of targets which all apply to a single sub-trie, ie a section of the overall
35/// trie whose nodes all share a prefix.
36pub(crate) struct SubTrieTargets<'a> {
37 /// The prefix which all nodes in the sub-trie share. This is also the first node in the trie
38 /// in lexicographic order.
39 pub(crate) prefix: Nibbles,
40 /// The targets belonging to this sub-trie. These will be sorted by their `key` field,
41 /// lexicographically.
42 pub(crate) targets: &'a [ProofV2Target],
43 /// Will be true if at least one target in the set has a zero `min_len`.
44 ///
45 /// If this is true then `prefix.is_empty()`, though not necessarily vice-versa.
46 pub(crate) retain_root: bool,
47}
48
49impl<'a> SubTrieTargets<'a> {
50 // A helper function which returns the first path following a sub-trie in lexicographical order.
51 #[inline]
52 pub(crate) fn upper_bound(&self) -> Option<Nibbles> {
53 sub_trie_upper_bound(&self.prefix)
54 }
55}
56
57/// Given a set of [`ProofV2Target`]s, returns an iterator over those same [`ProofV2Target`]s
58/// chunked by the sub-tries they apply to within the overall trie.
59pub(crate) fn iter_sub_trie_targets(
60 targets: &mut [ProofV2Target],
61) -> impl Iterator<Item = SubTrieTargets<'_>> {
62 // First sort by the sub-trie prefix of each target, falling back to the `min_len` in cases
63 // where the sub-trie prefixes are equal (to differentiate targets which match the root node and
64 // those which don't).
65 targets.sort_unstable_by(|a, b| {
66 sub_trie_prefix(a).cmp(&sub_trie_prefix(b)).then_with(|| a.min_len.cmp(&b.min_len))
67 });
68
69 // We now chunk targets, such that each chunk contains all targets belonging to the same
70 // sub-trie. We are taking advantage of the following properties:
71 //
72 // - The first target in the chunk has the shortest sub-trie prefix (see previous sorting step).
73 //
74 // - The upper bound of the first target in the chunk's sub-trie will therefore be the upper
75 // bound of the whole chunk.
76 // - For example, given a chunk with sub-trie prefixes [0x2, 0x2f, 0x2fa], the upper bounds
77 // will be [0x3, 0x3, 0x2fb]. Note that no target could match a trie node with path equal
78 // to or greater than 0x3.
79 //
80 // - If a target's sub-trie's prefix does not lie within the bounds of the current chunk, then
81 // that target must be the first target of the next chunk, lying in a separate sub-trie.
82 // - Example: given sub-trie prefixes of [0x2, 0x2fa, 0x4c, 0x4ce, 0x4e], we would end up
83 // with the following chunks:
84 // - [0x2, 0x2fa] w/ upper bound 0x3
85 // - [0x4c 0x4ce] w/ upper bound 0x4d
86 // - [0x4e] w/ upper bound 0x4f
87 let mut upper_bound = targets.first().and_then(|t| sub_trie_upper_bound(&sub_trie_prefix(t)));
88 let target_chunks = targets.chunk_by_mut(move |_, next| {
89 if let Some(some_upper_bound) = upper_bound {
90 let prefix = sub_trie_prefix(next);
91 let same_chunk = prefix < some_upper_bound;
92 if !same_chunk {
93 upper_bound = sub_trie_upper_bound(&prefix);
94 }
95 same_chunk
96 } else {
97 true
98 }
99 });
100
101 // Map the chunks to the return type. Within each chunk we want targets to be sorted by their
102 // key, as that will be the order they are checked by the `ProofCalculator`.
103 target_chunks.map(move |targets| {
104 let prefix = sub_trie_prefix(&targets[0]);
105 let retain_root = targets[0].min_len == 0;
106 targets.sort_unstable_by_key(|target| target.key_nibbles);
107 SubTrieTargets { prefix, targets, retain_root }
108 })
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use alloy_primitives::B256;
115
116 #[test]
117 fn test_iter_sub_trie_targets() {
118 // Helper to create nibbles from hex string (each character is a nibble)
119 let nibbles = |hex: &str| -> Nibbles {
120 if hex.is_empty() {
121 return Nibbles::new();
122 }
123 format!("0x{}", hex).parse().expect("valid nibbles hex string")
124 };
125
126 // Test cases: (input_targets, expected_output)
127 // Expected output format: Vec<(exp_prefix_hex, Vec<key_hex>)>
128 let test_cases = vec![
129 // Case 1: Empty targets
130 (vec![], vec![]),
131 // Case 2: Single target without min_len
132 (
133 vec![ProofV2Target::new(B256::repeat_byte(0x20))],
134 vec![(
135 "",
136 vec!["2020202020202020202020202020202020202020202020202020202020202020"],
137 )],
138 ),
139 // Case 3: Multiple targets in same sub-trie (no min_len)
140 (
141 vec![
142 ProofV2Target::new(B256::repeat_byte(0x20)),
143 ProofV2Target::new(B256::repeat_byte(0x21)),
144 ],
145 vec![(
146 "",
147 vec![
148 "2020202020202020202020202020202020202020202020202020202020202020",
149 "2121212121212121212121212121212121212121212121212121212121212121",
150 ],
151 )],
152 ),
153 // Case 4: Multiple targets in different sub-tries
154 (
155 vec![
156 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(2),
157 ProofV2Target::new(B256::repeat_byte(0x40)).with_min_len(2),
158 ],
159 vec![
160 ("2", vec!["2020202020202020202020202020202020202020202020202020202020202020"]),
161 ("4", vec!["4040404040404040404040404040404040404040404040404040404040404040"]),
162 ],
163 ),
164 // Case 5: Three targets, two in same sub-trie, one separate
165 (
166 vec![
167 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(2),
168 ProofV2Target::new(B256::repeat_byte(0x2f)).with_min_len(2),
169 ProofV2Target::new(B256::repeat_byte(0x40)).with_min_len(2),
170 ],
171 vec![
172 (
173 "2",
174 vec![
175 "2020202020202020202020202020202020202020202020202020202020202020",
176 "2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f",
177 ],
178 ),
179 ("4", vec!["4040404040404040404040404040404040404040404040404040404040404040"]),
180 ],
181 ),
182 // Case 6: Targets with different min_len values in same sub-trie
183 (
184 vec![
185 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(2),
186 ProofV2Target::new(B256::repeat_byte(0x2f)).with_min_len(3),
187 ],
188 vec![(
189 "2",
190 vec![
191 "2020202020202020202020202020202020202020202020202020202020202020",
192 "2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f",
193 ],
194 )],
195 ),
196 // Case 7: More complex chunking with multiple sub-tries
197 (
198 vec![
199 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(2),
200 ProofV2Target::new(B256::repeat_byte(0x2f)).with_min_len(4),
201 ProofV2Target::new(B256::repeat_byte(0x4c)).with_min_len(3),
202 ProofV2Target::new(B256::repeat_byte(0x4c)).with_min_len(4),
203 ProofV2Target::new(B256::repeat_byte(0x4e)).with_min_len(3),
204 ],
205 vec![
206 (
207 "2",
208 vec![
209 "2020202020202020202020202020202020202020202020202020202020202020",
210 "2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f2f",
211 ],
212 ),
213 (
214 "4c",
215 vec![
216 "4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c",
217 "4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c",
218 ],
219 ),
220 (
221 "4e",
222 vec!["4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e4e"],
223 ),
224 ],
225 ),
226 // Case 8: Min-len 1 should result in zero-length sub-trie prefix
227 (
228 vec![
229 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(1),
230 ProofV2Target::new(B256::repeat_byte(0x40)).with_min_len(1),
231 ],
232 vec![(
233 "",
234 vec![
235 "2020202020202020202020202020202020202020202020202020202020202020",
236 "4040404040404040404040404040404040404040404040404040404040404040",
237 ],
238 )],
239 ),
240 // Case 9: Second target's sub-trie prefix is root
241 (
242 vec![
243 ProofV2Target::new(B256::repeat_byte(0x20)).with_min_len(2),
244 ProofV2Target::new(B256::repeat_byte(0x40)).with_min_len(1),
245 ],
246 vec![(
247 "",
248 vec![
249 "2020202020202020202020202020202020202020202020202020202020202020",
250 "4040404040404040404040404040404040404040404040404040404040404040",
251 ],
252 )],
253 ),
254 ];
255
256 for (i, (mut input_targets, expected)) in test_cases.into_iter().enumerate() {
257 let test_case = i + 1;
258 let sub_tries: Vec<_> = iter_sub_trie_targets(&mut input_targets).collect();
259
260 assert_eq!(
261 sub_tries.len(),
262 expected.len(),
263 "Test case {} failed: expected {} sub-tries, got {}",
264 test_case,
265 expected.len(),
266 sub_tries.len()
267 );
268
269 for (j, (sub_trie, (exp_prefix_hex, exp_keys))) in
270 sub_tries.iter().zip(expected.iter()).enumerate()
271 {
272 let exp_prefix = nibbles(exp_prefix_hex);
273
274 assert_eq!(
275 sub_trie.prefix, exp_prefix,
276 "Test case {} sub-trie {}: prefix mismatch",
277 test_case, j
278 );
279 assert_eq!(
280 sub_trie.targets.len(),
281 exp_keys.len(),
282 "Test case {} sub-trie {}: expected {} targets, got {}",
283 test_case,
284 j,
285 exp_keys.len(),
286 sub_trie.targets.len()
287 );
288
289 for (k, (target, exp_key_hex)) in
290 sub_trie.targets.iter().zip(exp_keys.iter()).enumerate()
291 {
292 let exp_key = nibbles(exp_key_hex);
293 assert_eq!(
294 target.key_nibbles, exp_key,
295 "Test case {} sub-trie {} target {}: key mismatch",
296 test_case, j, k
297 );
298 }
299 }
300 }
301 }
302}