Skip to main content

unified_bridge/
imported_bridge_exit.rs

1use agglayer_primitives::{
2    keccak::{keccak256, keccak256_combine},
3    Digest, Hashable, U256,
4};
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::{
9    bridge_exit::BridgeExit, global_index::GlobalIndex, local_exit_tree::proof::LETMerkleProof,
10    ImportedBridgeExitCommitmentVersion, RollupIndex,
11};
12
13impl Hashable for MerkleProof {
14    #[inline]
15    fn hash(&self) -> Digest {
16        keccak256_combine([
17            self.root.as_slice(),
18            self.proof
19                .siblings
20                .iter()
21                .flat_map(|v| v.0)
22                .collect::<Vec<_>>()
23                .as_slice(),
24        ])
25    }
26}
27impl Hashable for Claim {
28    #[inline]
29    fn hash(&self) -> Digest {
30        match self {
31            Claim::Mainnet(claim_from_mainnet) => claim_from_mainnet.hash(),
32            Claim::Rollup(claim_from_rollup) => claim_from_rollup.hash(),
33        }
34    }
35}
36
37impl Hashable for ClaimFromMainnet {
38    #[inline]
39    fn hash(&self) -> Digest {
40        keccak256_combine([
41            self.proof_leaf_mer.hash(),
42            self.proof_ger_l1root.hash(),
43            self.l1_leaf.hash(),
44        ])
45    }
46}
47
48impl Hashable for ClaimFromRollup {
49    #[inline]
50    fn hash(&self) -> Digest {
51        keccak256_combine([
52            self.proof_leaf_ler.hash(),
53            self.proof_ler_rer.hash(),
54            self.proof_ger_l1root.hash(),
55            self.l1_leaf.hash(),
56        ])
57    }
58}
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
60#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
61pub struct L1InfoTreeLeafInner {
62    pub global_exit_root: Digest,
63    pub block_hash: Digest,
64    pub timestamp: u64,
65}
66
67impl L1InfoTreeLeafInner {
68    #[inline]
69    pub fn hash(&self, global_exit_root: Digest) -> Digest {
70        keccak256_combine([
71            global_exit_root.as_slice(),
72            self.block_hash.as_slice(),
73            &self.timestamp.to_be_bytes(),
74        ])
75    }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
79#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
80pub struct L1InfoTreeLeaf {
81    pub l1_info_tree_index: u32,
82    pub rer: Digest,
83    pub mer: Digest,
84    pub inner: L1InfoTreeLeafInner,
85}
86
87impl L1InfoTreeLeaf {
88    #[inline]
89    pub fn ger(&self) -> Digest {
90        keccak256_combine([self.mer, self.rer])
91    }
92
93    #[inline]
94    pub fn hash(&self) -> Digest {
95        self.inner.hash(self.ger())
96    }
97}
98
99#[derive(Clone, Debug, Error, Serialize, Deserialize, PartialEq, Eq)]
100#[serde(rename = "unified_bridge::Error")]
101pub enum Error {
102    /// The global index and the inclusion proof do not both correspond to the
103    /// same network type: mainnet or rollup.
104    #[error("Mismatch between the global index and the inclusion proof.")]
105    MismatchGlobalIndexInclusionProof,
106
107    /// The provided L1 info root does not match the one provided in the
108    /// inclusion proof.
109    #[error("Mismatch between the provided L1 root and the inclusion proof.")]
110    MismatchL1Root,
111
112    /// The provided L1 info leaf does not refer to the same MER as the
113    /// inclusion proof.
114    #[error("Mismatch on the MER between the L1 leaf and the inclusion proof.")]
115    MismatchMER,
116
117    /// The provided L1 info leaf does not refer to the same RER as the
118    /// inclusion proof.
119    #[error("Mismatch on the RER between the L1 leaf and the inclusion proof.")]
120    MismatchRER,
121
122    /// The inclusion proof from the leaf to the LER is invalid.
123    #[error("Invalid merkle path from the leaf to the LER.")]
124    InvalidMerklePathLeafToLER,
125
126    /// The inclusion proof from the LER to the RER is invalid.
127    #[error("Invalid merkle path from the LER to the RER.")]
128    InvalidMerklePathLERToRER,
129
130    /// The inclusion proof from the GER to the L1 info Root is invalid.
131    #[error("Invalid merkle path from the GER to the L1 Info Root.")]
132    InvalidMerklePathGERToL1Root,
133
134    /// The provided imported bridge exit does not target the right destination
135    /// network.
136    #[error("Invalid imported bridge exit destination network.")]
137    InvalidExitNetwork,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
141#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
142pub struct MerkleProof {
143    pub proof: LETMerkleProof,
144    pub root: Digest,
145}
146
147impl MerkleProof {
148    #[inline]
149    pub fn new(root: Digest, siblings: [Digest; 32]) -> Self {
150        Self {
151            proof: LETMerkleProof { siblings },
152            root,
153        }
154    }
155
156    #[inline]
157    pub fn verify(&self, leaf: Digest, leaf_index: u32) -> bool {
158        self.proof.verify(leaf, leaf_index, self.root)
159    }
160
161    #[inline]
162    pub fn siblings(&self) -> &[Digest; 32] {
163        &self.proof.siblings
164    }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
168#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
169pub enum Claim {
170    Mainnet(Box<ClaimFromMainnet>),
171    Rollup(Box<ClaimFromRollup>),
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
175#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
176pub struct ClaimFromMainnet {
177    /// Proof from bridge exit leaf to MER
178    pub proof_leaf_mer: MerkleProof,
179
180    /// Proof from GER to L1Root
181    pub proof_ger_l1root: MerkleProof,
182
183    /// L1InfoTree leaf
184    pub l1_leaf: L1InfoTreeLeaf,
185}
186
187impl ClaimFromMainnet {
188    #[inline]
189    pub fn verify(&self, leaf: Digest, leaf_index: u32, l1root: Digest) -> Result<(), Error> {
190        // Check the consistency on the l1 root
191        if l1root != self.proof_ger_l1root.root {
192            return Err(Error::MismatchL1Root);
193        }
194
195        // Check the consistency on the declared MER
196        if self.proof_leaf_mer.root != self.l1_leaf.mer {
197            return Err(Error::MismatchMER);
198        }
199
200        // Check the inclusion proof of the leaf to the LER (here LER is the MER)
201        if !self.proof_leaf_mer.verify(leaf, leaf_index) {
202            return Err(Error::InvalidMerklePathLeafToLER);
203        }
204
205        // Check the inclusion proof of the L1 leaf to L1Root
206        if !self
207            .proof_ger_l1root
208            .verify(self.l1_leaf.hash(), self.l1_leaf.l1_info_tree_index)
209        {
210            return Err(Error::InvalidMerklePathGERToL1Root);
211        }
212
213        Ok(())
214    }
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
218#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
219pub struct ClaimFromRollup {
220    /// Proof from bridge exit leaf to LER
221    pub proof_leaf_ler: MerkleProof,
222    /// Proof from LER to RER
223    pub proof_ler_rer: MerkleProof,
224    /// Proof from GER to L1Root
225    pub proof_ger_l1root: MerkleProof,
226    /// L1InfoTree leaf
227    pub l1_leaf: L1InfoTreeLeaf,
228}
229
230impl ClaimFromRollup {
231    #[inline]
232    pub fn verify(
233        &self,
234        leaf: Digest,
235        leaf_index: u32,
236        rollup_index: RollupIndex,
237        l1root: Digest,
238    ) -> Result<(), Error> {
239        // Check the consistency on the l1 root
240        if l1root != self.proof_ger_l1root.root {
241            return Err(Error::MismatchL1Root);
242        }
243
244        // Check the consistency on the declared RER
245        if self.proof_ler_rer.root != self.l1_leaf.rer {
246            return Err(Error::MismatchRER);
247        }
248
249        // Check the inclusion proof of the leaf to the LER
250        if !self.proof_leaf_ler.verify(leaf, leaf_index) {
251            return Err(Error::InvalidMerklePathLeafToLER);
252        }
253
254        // Check the inclusion proof of the LER to the RER
255        if !self
256            .proof_ler_rer
257            .verify(self.proof_leaf_ler.root, rollup_index.to_u32())
258        {
259            return Err(Error::InvalidMerklePathLERToRER);
260        }
261
262        // Check the inclusion proof of the L1 leaf to L1Root
263        if !self
264            .proof_ger_l1root
265            .verify(self.l1_leaf.hash(), self.l1_leaf.l1_info_tree_index)
266        {
267            return Err(Error::InvalidMerklePathGERToL1Root);
268        }
269
270        Ok(())
271    }
272}
273
274/// Represents a token bridge exit originating on another network but claimed on
275/// the current network.
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
277#[cfg_attr(feature = "testutils", derive(arbitrary::Arbitrary))]
278pub struct ImportedBridgeExit {
279    /// The bridge exit initiated on another network, called the "sending"
280    /// network. Need to verify that the destination network matches the
281    /// current network, and that the bridge exit is included in an imported
282    /// LER
283    pub bridge_exit: BridgeExit,
284
285    /// The claim data
286    pub claim_data: Claim,
287
288    /// The global index of the imported bridge exit.
289    pub global_index: GlobalIndex,
290}
291
292impl ImportedBridgeExit {
293    /// Verifies that the provided inclusion path is valid and consistent with
294    /// the provided LER
295    #[inline]
296    pub fn verify_path(&self, l1root: Digest) -> Result<(), Error> {
297        // Check that the inclusion proof and the global index both refer to mainnet or
298        // rollup
299        if self.global_index.is_mainnet() != matches!(self.claim_data, Claim::Mainnet(_)) {
300            return Err(Error::MismatchGlobalIndexInclusionProof);
301        }
302
303        match &self.claim_data {
304            Claim::Mainnet(claim) => claim.verify(
305                self.bridge_exit.hash(),
306                self.global_index.leaf_index(),
307                l1root,
308            ),
309            Claim::Rollup(claim) => claim.verify(
310                self.bridge_exit.hash(),
311                self.global_index.leaf_index(),
312                self.global_index.rollup_index().unwrap(), // Checked just above
313                l1root,
314            ),
315        }
316    }
317}
318
319#[cfg(not(feature = "zkvm"))]
320impl ImportedBridgeExit {
321    /// Creates a new [`ImportedBridgeExit`].
322    #[inline]
323    pub fn new(bridge_exit: BridgeExit, claim_data: Claim, global_index: GlobalIndex) -> Self {
324        Self {
325            bridge_exit,
326            global_index,
327            claim_data,
328        }
329    }
330    /// Returns the considered L1 Info Root against which the claim is done.
331    #[inline]
332    pub fn l1_info_root(&self) -> Digest {
333        match &self.claim_data {
334            Claim::Mainnet(claim) => claim.proof_ger_l1root.root,
335            Claim::Rollup(claim) => claim.proof_ger_l1root.root,
336        }
337    }
338
339    /// Returns the considered L1 Info Tree leaf index against which the claim
340    /// is done.
341    #[inline]
342    pub fn l1_leaf_index(&self) -> u32 {
343        match &self.claim_data {
344            Claim::Mainnet(claim) => claim.l1_leaf.l1_info_tree_index,
345            Claim::Rollup(claim) => claim.l1_leaf.l1_info_tree_index,
346        }
347    }
348
349    /// Hash the entire data structure.
350    #[inline]
351    pub fn hash(&self) -> Digest {
352        keccak256_combine([
353            self.bridge_exit.hash(),
354            self.claim_data.hash(),
355            self.global_index.hash(),
356        ])
357    }
358}
359
360impl ImportedBridgeExit {
361    pub fn valid_claim(&self) -> bool {
362        match &self.claim_data {
363            Claim::Mainnet(claim) => {
364                claim.l1_leaf.inner.global_exit_root
365                    == keccak256_combine([claim.l1_leaf.mer, claim.l1_leaf.rer])
366            }
367            Claim::Rollup(claim) => {
368                claim.l1_leaf.inner.global_exit_root
369                    == keccak256_combine([claim.l1_leaf.mer, claim.l1_leaf.rer])
370            }
371        }
372    }
373
374    /// Returns the global index and the underlying bridge exit leaf hash.
375    pub fn to_indexed_exit_hash(&self) -> GlobalIndexWithLeafHash {
376        GlobalIndexWithLeafHash {
377            global_index: self.global_index.into(),
378            bridge_exit_hash: self.bridge_exit.hash(),
379        }
380    }
381}
382
383/// Refers to one claim as per its global index and the hash of the underlying
384/// bridge exit.
385#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
386pub struct GlobalIndexWithLeafHash {
387    /// Global index of the claimed bridge exit.
388    pub global_index: U256,
389
390    /// Hash of the claimed bridge exit.
391    pub bridge_exit_hash: Digest,
392}
393
394impl GlobalIndexWithLeafHash {
395    /// Compute the bridge exit commitment used for the hash chain.
396    pub fn commitment(&self) -> Digest {
397        keccak256_combine([self.global_index.to_be_bytes(), self.bridge_exit_hash.0])
398    }
399}
400
401/// The values which compose the commitment on the imported bridge exits.
402#[derive(Debug, Clone)]
403pub struct ImportedBridgeExitCommitmentValues {
404    pub claims: Vec<GlobalIndexWithLeafHash>,
405}
406
407impl ImportedBridgeExitCommitmentValues {
408    /// Returns the expected signed commitment for the provided version.
409    #[inline]
410    pub fn commitment(&self, version: ImportedBridgeExitCommitmentVersion) -> Digest {
411        match version {
412            ImportedBridgeExitCommitmentVersion::V2 => {
413                // Commits solely to the global index of each imported bridge exit. Designed
414                // prior to having any notion of aggchain proof.
415                keccak256_combine(
416                    self.claims
417                        .iter()
418                        .map(|ibe| keccak256(ibe.global_index.as_le_slice())),
419                )
420            }
421            ImportedBridgeExitCommitmentVersion::V3 => {
422                // Adds the bridge exit hashes in the commitment to ensure that the aggchain
423                // proof and PP talk about the exact same set of imported bridge exits.
424                keccak256_combine(self.claims.iter().map(|ibe| {
425                    [
426                        ibe.global_index.as_le_slice(),
427                        ibe.bridge_exit_hash.as_slice(),
428                    ]
429                    .concat()
430                }))
431            }
432        }
433    }
434}