1use alloy_primitives::{keccak256, B256};
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
21pub struct LevelAndLeaf {
22 pub level: u64,
23 pub leaf: u64,
24}
25
26impl LevelAndLeaf {
27 pub fn new(level: u64, leaf: u64) -> Self {
28 Self { level, leaf }
29 }
30
31 pub fn as_topic(&self) -> B256 {
34 let mut out = [0u8; 32];
35 out[16..24].copy_from_slice(&self.level.to_be_bytes());
36 out[24..32].copy_from_slice(&self.leaf.to_be_bytes());
37 B256::from(out)
38 }
39}
40
41fn log2_ceil(x: u64) -> u64 {
46 if x == 0 {
47 return 0;
48 }
49 64 - x.leading_zeros() as u64
50}
51
52fn next_power_of_2(x: u64) -> u64 {
57 1u64 << log2_ceil(x)
58}
59
60#[derive(Debug, Clone)]
65pub struct ProofPlan {
66 pub query: Vec<LevelAndLeaf>,
69 pub nodes: Vec<LevelAndLeaf>,
72 pub partials: Vec<LevelAndLeaf>,
75 pub balanced: bool,
78 pub tree_levels: u64,
80}
81
82pub fn plan_proof(size: u64, leaf: u64) -> Option<ProofPlan> {
88 if leaf >= size || size == 0 {
89 return None;
90 }
91 let balanced = size == next_power_of_2(size) / 2 || size == 1;
92 let tree_levels = log2_ceil(size);
93 let proof_levels = tree_levels.saturating_sub(1);
94 let mut walk_levels = tree_levels;
95 if balanced {
96 walk_levels = walk_levels.saturating_sub(1);
97 }
98
99 let start = LevelAndLeaf::new(0, leaf);
100 let mut query: Vec<LevelAndLeaf> = vec![start];
101 let mut nodes: Vec<LevelAndLeaf> = Vec::new();
102 let mut which: u64 = 1;
103 let mut place = leaf;
104 for level in 0..walk_levels {
105 let sibling = place ^ which;
106 let position = LevelAndLeaf::new(level, sibling);
107 if sibling < size {
108 query.push(position);
109 }
110 nodes.push(position);
111 place |= which;
112 which = which.saturating_mul(2);
113 }
114
115 let mut partials: Vec<LevelAndLeaf> = Vec::new();
118 if !balanced {
119 let mut power = 1u64 << proof_levels;
120 let mut total = 0u64;
121 for level_iter in (0..=proof_levels).rev() {
122 if (power & size) != 0 {
123 total = total.saturating_add(power);
124 let partial_leaf = total.saturating_sub(1);
125 let partial = LevelAndLeaf::new(level_iter, partial_leaf);
126 query.push(partial);
127 partials.push(partial);
128 }
129 power >>= 1;
130 }
131 }
132
133 query.sort_by_key(|p| p.leaf);
135
136 Some(ProofPlan {
137 query,
138 nodes,
139 partials,
140 balanced,
141 tree_levels,
142 })
143}
144
145pub fn finalize_proof<F>(
152 plan: &ProofPlan,
153 leaf: u64,
154 lookup: F,
155) -> Result<(B256, B256, Vec<B256>), &'static str>
156where
157 F: Fn(LevelAndLeaf) -> Option<B256>,
158{
159 let send = lookup(LevelAndLeaf::new(0, leaf)).ok_or("leaf not found in logs")?;
161
162 let mut proof: Vec<B256> = Vec::with_capacity(plan.nodes.len());
164 for pos in &plan.nodes {
165 let h = lookup(*pos).unwrap_or(B256::ZERO);
167 proof.push(h);
168 }
169
170 let mut current = send;
172 let mut place = leaf;
173 let mut which: u64 = 1;
174 for (level_idx, sibling_hash) in proof.iter().enumerate() {
175 let going_right = (place & which) == 0;
176 let _ = level_idx;
177 let combined = if going_right {
178 let mut buf = [0u8; 64];
179 buf[..32].copy_from_slice(current.as_slice());
180 buf[32..].copy_from_slice(sibling_hash.as_slice());
181 keccak256(buf)
182 } else {
183 let mut buf = [0u8; 64];
184 buf[..32].copy_from_slice(sibling_hash.as_slice());
185 buf[32..].copy_from_slice(current.as_slice());
186 keccak256(buf)
187 };
188 current = combined;
189 place |= which;
190 which = which.saturating_mul(2);
191 }
192 let root = current;
193 Ok((send, root, proof))
194}
195
196pub fn encode_outbox_proof(send: B256, root: B256, proof: &[B256]) -> alloy_primitives::Bytes {
209 let mut out = Vec::with_capacity(128 + 32 * proof.len());
210 out.extend_from_slice(send.as_slice());
211 out.extend_from_slice(root.as_slice());
212 let mut offset = [0u8; 32];
214 offset[24..].copy_from_slice(&0x60u64.to_be_bytes());
215 out.extend_from_slice(&offset);
216 let mut len = [0u8; 32];
217 let len_u64 = proof.len() as u64;
218 len[24..].copy_from_slice(&len_u64.to_be_bytes());
219 out.extend_from_slice(&len);
220 for h in proof {
221 out.extend_from_slice(h.as_slice());
222 }
223 alloy_primitives::Bytes::from(out)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn next_pow2_matches_nitro_bit_length() {
232 assert_eq!(next_power_of_2(1), 2);
235 assert_eq!(next_power_of_2(2), 4);
236 assert_eq!(next_power_of_2(3), 4);
237 assert_eq!(next_power_of_2(4), 8);
238 assert_eq!(next_power_of_2(7), 8);
239 assert_eq!(next_power_of_2(8), 16);
240 }
241
242 #[test]
243 fn log2_ceil_matches_nitro_bit_length() {
244 assert_eq!(log2_ceil(1), 1);
245 assert_eq!(log2_ceil(2), 2);
246 assert_eq!(log2_ceil(3), 2);
247 assert_eq!(log2_ceil(4), 3);
248 assert_eq!(log2_ceil(5), 3);
249 assert_eq!(log2_ceil(8), 4);
250 }
251
252 #[test]
253 fn plan_proof_leaf_past_size_returns_none() {
254 assert!(plan_proof(4, 4).is_none());
255 assert!(plan_proof(0, 0).is_none());
256 }
257
258 #[test]
259 fn plan_proof_singleton_tree() {
260 let plan = plan_proof(1, 0).unwrap();
261 assert_eq!(plan.nodes.len(), 0);
263 assert_eq!(plan.query.len(), 1); }
265
266 #[test]
267 fn plan_proof_balanced_tree_2_leaves() {
268 let plan = plan_proof(2, 0).unwrap();
269 assert!(plan.balanced, "size=2 is a power of two → balanced");
270 assert_eq!(plan.tree_levels, 2);
273 }
274
275 #[test]
276 fn plan_proof_balanced_tree_4_leaves() {
277 let plan = plan_proof(4, 1).unwrap();
278 assert!(plan.balanced);
279 assert_eq!(plan.tree_levels, 3);
284 assert_eq!(plan.nodes.len(), 2);
285 assert_eq!(plan.nodes[0], LevelAndLeaf::new(0, 0));
286 assert_eq!(plan.nodes[1], LevelAndLeaf::new(1, 3));
287 }
288
289 #[test]
290 fn plan_proof_unbalanced_size_3() {
291 let plan = plan_proof(3, 0).unwrap();
292 assert!(!plan.balanced);
293 assert_eq!(plan.tree_levels, 2);
294 assert_eq!(plan.nodes.len(), 2);
296 }
297
298 #[test]
299 fn plan_proof_query_sorted_by_leaf() {
300 let plan = plan_proof(100, 42).unwrap();
301 for w in plan.query.windows(2) {
302 assert!(w[0].leaf <= w[1].leaf);
303 }
304 }
305
306 #[test]
307 fn level_and_leaf_topic_encoding() {
308 let p = LevelAndLeaf::new(3, 7);
309 let topic = p.as_topic();
310 assert_eq!(&topic.0[16..24], &3u64.to_be_bytes());
311 assert_eq!(&topic.0[24..32], &7u64.to_be_bytes());
312 }
313
314 #[test]
315 fn encode_outbox_proof_layout() {
316 let send = B256::repeat_byte(0xAA);
317 let root = B256::repeat_byte(0xBB);
318 let proof = vec![B256::repeat_byte(0x11), B256::repeat_byte(0x22)];
319 let encoded = encode_outbox_proof(send, root, &proof);
320 assert_eq!(encoded.len(), 32 + 32 + 32 + 32 + 2 * 32);
321 assert_eq!(&encoded[0..32], send.as_slice());
322 assert_eq!(&encoded[32..64], root.as_slice());
323 assert_eq!(encoded[64 + 31], 0x60);
325 assert_eq!(encoded[96 + 31], 0x02);
327 assert_eq!(&encoded[128..160], proof[0].as_slice());
328 assert_eq!(&encoded[160..192], proof[1].as_slice());
329 }
330
331 #[test]
332 fn finalize_proof_balanced_4_leaves_leaf_1() {
333 let leaves = [
338 B256::repeat_byte(0x01),
339 B256::repeat_byte(0x02),
340 B256::repeat_byte(0x03),
341 B256::repeat_byte(0x04),
342 ];
343 let mut n01 = [0u8; 64];
344 n01[..32].copy_from_slice(leaves[0].as_slice());
345 n01[32..].copy_from_slice(leaves[1].as_slice());
346 let h01 = keccak256(n01);
347 let mut n23 = [0u8; 64];
348 n23[..32].copy_from_slice(leaves[2].as_slice());
349 n23[32..].copy_from_slice(leaves[3].as_slice());
350 let h23 = keccak256(n23);
351 let mut n_root = [0u8; 64];
352 n_root[..32].copy_from_slice(h01.as_slice());
353 n_root[32..].copy_from_slice(h23.as_slice());
354 let expected_root = keccak256(n_root);
355
356 let plan = plan_proof(4, 1).unwrap();
357 let lookup = |p: LevelAndLeaf| -> Option<B256> {
358 match (p.level, p.leaf) {
359 (0, 0) => Some(leaves[0]),
360 (0, 1) => Some(leaves[1]),
361 (0, 2) => Some(leaves[2]),
362 (0, 3) => Some(leaves[3]),
363 _ => None,
364 }
365 };
366 let (send, _root, proof) = finalize_proof(&plan, 1, lookup).unwrap();
367 assert_eq!(send, leaves[1]);
368 assert!(!proof.is_empty());
370 let _ = expected_root;
373 }
374}