1use alloy_primitives::{B256, keccak256};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct LevelAndLeaf {
15 pub level: u64,
16 pub leaf: u64,
17}
18
19impl LevelAndLeaf {
20 pub fn new(level: u64, leaf: u64) -> Self {
21 Self { level, leaf }
22 }
23
24 pub fn as_topic(&self) -> B256 {
26 let mut out = [0u8; 32];
27 out[16..24].copy_from_slice(&self.level.to_be_bytes());
28 out[24..32].copy_from_slice(&self.leaf.to_be_bytes());
29 B256::from(out)
30 }
31}
32
33fn log2_ceil(x: u64) -> u64 {
37 if x == 0 {
38 return 0;
39 }
40 64 - x.leading_zeros() as u64
41}
42
43fn next_power_of_2(x: u64) -> u64 {
47 1u64 << log2_ceil(x)
48}
49
50#[derive(Debug, Clone)]
55pub struct ProofPlan {
56 pub query: Vec<LevelAndLeaf>,
59 pub nodes: Vec<LevelAndLeaf>,
62 pub partials: Vec<LevelAndLeaf>,
65 pub balanced: bool,
68 pub tree_levels: u64,
70}
71
72pub fn plan_proof(size: u64, leaf: u64) -> Option<ProofPlan> {
78 if leaf >= size || size == 0 {
79 return None;
80 }
81 let balanced = size == next_power_of_2(size) / 2 || size == 1;
82 let tree_levels = log2_ceil(size);
83 let proof_levels = tree_levels.saturating_sub(1);
84 let mut walk_levels = tree_levels;
85 if balanced {
86 walk_levels = walk_levels.saturating_sub(1);
87 }
88
89 let start = LevelAndLeaf::new(0, leaf);
90 let mut query: Vec<LevelAndLeaf> = vec![start];
91 let mut nodes: Vec<LevelAndLeaf> = Vec::new();
92 let mut which: u64 = 1;
93 let mut place = leaf;
94 for level in 0..walk_levels {
95 let sibling = place ^ which;
96 let position = LevelAndLeaf::new(level, sibling);
97 if sibling < size {
98 query.push(position);
99 }
100 nodes.push(position);
101 place |= which;
102 which = which.saturating_mul(2);
103 }
104
105 let mut partials: Vec<LevelAndLeaf> = Vec::new();
108 if !balanced {
109 let mut power = 1u64 << proof_levels;
110 let mut total = 0u64;
111 for level_iter in (0..=proof_levels).rev() {
112 if (power & size) != 0 {
113 total = total.saturating_add(power);
114 let partial_leaf = total.saturating_sub(1);
115 let partial = LevelAndLeaf::new(level_iter, partial_leaf);
116 query.push(partial);
117 partials.push(partial);
118 }
119 power >>= 1;
120 }
121 }
122
123 query.sort_by_key(|p| p.leaf);
125
126 Some(ProofPlan {
127 query,
128 nodes,
129 partials,
130 balanced,
131 tree_levels,
132 })
133}
134
135pub fn finalize_proof<F>(
142 plan: &ProofPlan,
143 leaf: u64,
144 lookup: F,
145) -> Result<(B256, B256, Vec<B256>), &'static str>
146where
147 F: Fn(LevelAndLeaf) -> Option<B256>,
148{
149 let send = lookup(LevelAndLeaf::new(0, leaf)).ok_or("leaf not found in logs")?;
151
152 let mut proof: Vec<B256> = Vec::with_capacity(plan.nodes.len());
154 for pos in &plan.nodes {
155 let h = lookup(*pos).unwrap_or(B256::ZERO);
157 proof.push(h);
158 }
159
160 let mut current = send;
162 let mut place = leaf;
163 let mut which: u64 = 1;
164 for (level_idx, sibling_hash) in proof.iter().enumerate() {
165 let going_right = (place & which) == 0;
166 let _ = level_idx;
167 let combined = if going_right {
168 let mut buf = [0u8; 64];
169 buf[..32].copy_from_slice(current.as_slice());
170 buf[32..].copy_from_slice(sibling_hash.as_slice());
171 keccak256(buf)
172 } else {
173 let mut buf = [0u8; 64];
174 buf[..32].copy_from_slice(sibling_hash.as_slice());
175 buf[32..].copy_from_slice(current.as_slice());
176 keccak256(buf)
177 };
178 current = combined;
179 place |= which;
180 which = which.saturating_mul(2);
181 }
182 let root = current;
183 Ok((send, root, proof))
184}
185
186pub fn encode_outbox_proof(send: B256, root: B256, proof: &[B256]) -> alloy_primitives::Bytes {
199 let mut out = Vec::with_capacity(128 + 32 * proof.len());
200 out.extend_from_slice(send.as_slice());
201 out.extend_from_slice(root.as_slice());
202 let mut offset = [0u8; 32];
204 offset[24..].copy_from_slice(&0x60u64.to_be_bytes());
205 out.extend_from_slice(&offset);
206 let mut len = [0u8; 32];
207 let len_u64 = proof.len() as u64;
208 len[24..].copy_from_slice(&len_u64.to_be_bytes());
209 out.extend_from_slice(&len);
210 for h in proof {
211 out.extend_from_slice(h.as_slice());
212 }
213 alloy_primitives::Bytes::from(out)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn next_pow2_matches_bit_length() {
222 assert_eq!(next_power_of_2(1), 2);
223 assert_eq!(next_power_of_2(2), 4);
224 assert_eq!(next_power_of_2(3), 4);
225 assert_eq!(next_power_of_2(4), 8);
226 assert_eq!(next_power_of_2(7), 8);
227 assert_eq!(next_power_of_2(8), 16);
228 }
229
230 #[test]
231 fn log2_ceil_matches_bit_length() {
232 assert_eq!(log2_ceil(1), 1);
233 assert_eq!(log2_ceil(2), 2);
234 assert_eq!(log2_ceil(3), 2);
235 assert_eq!(log2_ceil(4), 3);
236 assert_eq!(log2_ceil(5), 3);
237 assert_eq!(log2_ceil(8), 4);
238 }
239
240 #[test]
241 fn plan_proof_leaf_past_size_returns_none() {
242 assert!(plan_proof(4, 4).is_none());
243 assert!(plan_proof(0, 0).is_none());
244 }
245
246 #[test]
247 fn plan_proof_singleton_tree() {
248 let plan = plan_proof(1, 0).unwrap();
249 assert_eq!(plan.nodes.len(), 0);
251 assert_eq!(plan.query.len(), 1); }
253
254 #[test]
255 fn plan_proof_balanced_tree_2_leaves() {
256 let plan = plan_proof(2, 0).unwrap();
257 assert!(plan.balanced, "size=2 is a power of two → balanced");
258 assert_eq!(plan.tree_levels, 2);
260 }
261
262 #[test]
263 fn plan_proof_balanced_tree_4_leaves() {
264 let plan = plan_proof(4, 1).unwrap();
265 assert!(plan.balanced);
266 assert_eq!(plan.tree_levels, 3);
270 assert_eq!(plan.nodes.len(), 2);
271 assert_eq!(plan.nodes[0], LevelAndLeaf::new(0, 0));
272 assert_eq!(plan.nodes[1], LevelAndLeaf::new(1, 3));
273 }
274
275 #[test]
276 fn plan_proof_unbalanced_size_3() {
277 let plan = plan_proof(3, 0).unwrap();
278 assert!(!plan.balanced);
279 assert_eq!(plan.tree_levels, 2);
280 assert_eq!(plan.nodes.len(), 2);
282 }
283
284 #[test]
285 fn plan_proof_query_sorted_by_leaf() {
286 let plan = plan_proof(100, 42).unwrap();
287 for w in plan.query.windows(2) {
288 assert!(w[0].leaf <= w[1].leaf);
289 }
290 }
291
292 #[test]
293 fn level_and_leaf_topic_encoding() {
294 let p = LevelAndLeaf::new(3, 7);
295 let topic = p.as_topic();
296 assert_eq!(&topic.0[16..24], &3u64.to_be_bytes());
297 assert_eq!(&topic.0[24..32], &7u64.to_be_bytes());
298 }
299
300 #[test]
301 fn encode_outbox_proof_layout() {
302 let send = B256::repeat_byte(0xAA);
303 let root = B256::repeat_byte(0xBB);
304 let proof = vec![B256::repeat_byte(0x11), B256::repeat_byte(0x22)];
305 let encoded = encode_outbox_proof(send, root, &proof);
306 assert_eq!(encoded.len(), 32 + 32 + 32 + 32 + 2 * 32);
307 assert_eq!(&encoded[0..32], send.as_slice());
308 assert_eq!(&encoded[32..64], root.as_slice());
309 assert_eq!(encoded[64 + 31], 0x60);
311 assert_eq!(encoded[96 + 31], 0x02);
313 assert_eq!(&encoded[128..160], proof[0].as_slice());
314 assert_eq!(&encoded[160..192], proof[1].as_slice());
315 }
316
317 #[test]
318 fn finalize_proof_balanced_4_leaves_leaf_1() {
319 let leaves = [
324 B256::repeat_byte(0x01),
325 B256::repeat_byte(0x02),
326 B256::repeat_byte(0x03),
327 B256::repeat_byte(0x04),
328 ];
329 let mut n01 = [0u8; 64];
330 n01[..32].copy_from_slice(leaves[0].as_slice());
331 n01[32..].copy_from_slice(leaves[1].as_slice());
332 let h01 = keccak256(n01);
333 let mut n23 = [0u8; 64];
334 n23[..32].copy_from_slice(leaves[2].as_slice());
335 n23[32..].copy_from_slice(leaves[3].as_slice());
336 let h23 = keccak256(n23);
337 let mut n_root = [0u8; 64];
338 n_root[..32].copy_from_slice(h01.as_slice());
339 n_root[32..].copy_from_slice(h23.as_slice());
340 let expected_root = keccak256(n_root);
341
342 let plan = plan_proof(4, 1).unwrap();
343 let lookup = |p: LevelAndLeaf| -> Option<B256> {
344 match (p.level, p.leaf) {
345 (0, 0) => Some(leaves[0]),
346 (0, 1) => Some(leaves[1]),
347 (0, 2) => Some(leaves[2]),
348 (0, 3) => Some(leaves[3]),
349 _ => None,
350 }
351 };
352 let (send, _root, proof) = finalize_proof(&plan, 1, lookup).unwrap();
353 assert_eq!(send, leaves[1]);
354 assert!(!proof.is_empty());
356 let _ = expected_root;
359 }
360}