agglayer_types/certificate/
mod.rs

1use agglayer_interop_types::{aggchain_proof::AggchainData, LocalExitRoot};
2use agglayer_primitives::{Address, Hashable, Signature};
3use pessimistic_proof::{
4    core::commitment::{SignatureCommitmentValues, SignatureCommitmentVersion},
5    keccak::keccak256_combine,
6};
7use unified_bridge::{
8    BridgeExit, ImportedBridgeExit, ImportedBridgeExitCommitmentValues, NetworkId,
9};
10
11use crate::{
12    aggchain_data::{MultisigCtx, MultisigPayload, PayloadWithCtx},
13    Digest, Error, SignerError,
14};
15
16mod header;
17mod height;
18mod id;
19mod index;
20mod metadata;
21#[cfg(feature = "testutils")]
22mod testutils;
23
24pub use header::{CertificateHeader, CertificateStatus, SettlementTxHash};
25pub use height::Height;
26pub use id::CertificateId;
27pub use index::CertificateIndex;
28pub use metadata::Metadata;
29#[cfg(feature = "testutils")]
30pub use testutils::{compute_signature_info, EMPTY_ELF};
31
32/// Represents the data submitted by the chains to the AggLayer.
33///
34/// The bridge exits plus the imported bridge exits define
35/// the state transition, resp. the amount that goes out and the amount that
36/// comes in.
37///
38/// The bridge exits refer to the [`BridgeExit`] emitted by
39/// the origin network of the [`Certificate`].
40///
41/// The imported bridge exits refer to the [`BridgeExit`] received and imported
42/// by the origin network of the [`Certificate`].
43///
44/// Note: be mindful to update the [`Self::hash`] method accordingly
45/// upon modifying the fields of this structure.
46#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
47#[cfg_attr(feature = "testutils", derive(Eq, PartialEq))]
48pub struct Certificate {
49    /// NetworkID of the origin network.
50    pub network_id: NetworkId,
51    /// Simple increment to count the Certificate per network.
52    pub height: Height,
53    /// Previous local exit root.
54    pub prev_local_exit_root: LocalExitRoot,
55    /// New local exit root.
56    pub new_local_exit_root: LocalExitRoot,
57    /// List of bridge exits included in this state transition.
58    pub bridge_exits: Vec<BridgeExit>,
59    /// List of imported bridge exits included in this state transition.
60    pub imported_bridge_exits: Vec<ImportedBridgeExit>,
61    /// Fixed size field of arbitrary data for the chain needs.
62    pub metadata: Metadata,
63    /// Aggchain data for the certificate.
64    #[serde(flatten)]
65    pub aggchain_data: AggchainData,
66    #[serde(default)]
67    pub custom_chain_data: Vec<u8>,
68    #[serde(default)]
69    pub l1_info_tree_leaf_count: Option<u32>,
70}
71
72impl Certificate {
73    pub fn hash(&self) -> CertificateId {
74        let commit_bridge_exits =
75            keccak256_combine(self.bridge_exits.iter().map(|exit| exit.hash()));
76        let commit_imported_bridge_exits =
77            keccak256_combine(self.imported_bridge_exits.iter().map(|exit| exit.hash()));
78
79        CertificateId::new(keccak256_combine([
80            self.network_id.to_be_bytes().as_slice(),
81            self.height.as_u64().to_be_bytes().as_slice(),
82            self.prev_local_exit_root.as_ref(),
83            self.new_local_exit_root.as_ref(),
84            commit_bridge_exits.as_slice(),
85            commit_imported_bridge_exits.as_slice(),
86            self.metadata.0.as_slice(),
87        ]))
88    }
89
90    /// Returns the L1 Info Tree leaf count considered for this [`Certificate`].
91    /// Corresponds to the highest L1 Info Tree leaf index considered by the
92    /// imported bridge exits.
93    pub fn l1_info_tree_leaf_count(&self) -> Option<u32> {
94        self.l1_info_tree_leaf_count.or_else(|| {
95            self.imported_bridge_exits
96                .iter()
97                .map(|i| i.l1_leaf_index() + 1)
98                .max()
99        })
100    }
101
102    /// Returns the L1 Info Root considered for this [`Certificate`].
103    /// Fails if multiple L1 Info Root are considered among the inclusion proofs
104    /// of the imported bridge exits.
105    pub fn l1_info_root(&self) -> Result<Option<Digest>, Error> {
106        let Some(l1_info_root) = self
107            .imported_bridge_exits
108            .first()
109            .map(|imported_bridge_exit| imported_bridge_exit.l1_info_root())
110        else {
111            return Ok(None);
112        };
113
114        if self
115            .imported_bridge_exits
116            .iter()
117            .all(|exit| exit.l1_info_root() == l1_info_root)
118        {
119            Ok(Some(l1_info_root))
120        } else {
121            Err(Error::MultipleL1InfoRoot)
122        }
123    }
124
125    pub fn signature_commitment_values(&self) -> SignatureCommitmentValues {
126        SignatureCommitmentValues::from(self)
127    }
128
129    pub fn verify_legacy_ecdsa(
130        &self,
131        expected_signer: Address,
132        signature: &Signature,
133    ) -> Result<(), SignerError> {
134        let signature_commitment_values = self.signature_commitment_values();
135
136        let recovered_expected_signer = [
137            SignatureCommitmentVersion::V5,
138            SignatureCommitmentVersion::V3,
139            SignatureCommitmentVersion::V2,
140        ]
141        .iter()
142        .any(|version| {
143            let commitment = signature_commitment_values.commitment(*version);
144            match signature.recover_address_from_prehash(&commitment) {
145                Ok(recovered) => recovered == expected_signer,
146                Err(_) => false,
147            }
148        });
149
150        recovered_expected_signer
151            .then_some(())
152            .ok_or(SignerError::InvalidPessimisticProofSignature { expected_signer })
153    }
154
155    pub fn verify_aggchain_proof_signature(
156        &self,
157        expected_signer: Address,
158        signature: &Option<Box<Signature>>,
159    ) -> Result<(), SignerError> {
160        let signature_commitment_values = self.signature_commitment_values();
161
162        let signature = signature.as_ref().ok_or(SignerError::Missing)?;
163        let commitment = signature_commitment_values.commitment(SignatureCommitmentVersion::V5);
164        let recovered = signature
165            .recover_address_from_prehash(&commitment)
166            .map_err(SignerError::Recovery)?;
167
168        if recovered != expected_signer {
169            return Err(SignerError::InvalidPessimisticProofSignature { expected_signer });
170        }
171
172        Ok(())
173    }
174
175    pub fn verify_multisig(
176        &self,
177        signatures: MultisigPayload,
178        ctx: MultisigCtx,
179    ) -> Result<(), SignerError> {
180        // Verify the multisig from the chain payload and the L1 context
181        let prehash = ctx.prehash;
182        let multisig_with_ctx = PayloadWithCtx(signatures, ctx);
183        let witness_data: pessimistic_proof::core::MultiSignature = multisig_with_ctx.into();
184        witness_data
185            .verify(prehash)
186            .map_err(SignerError::InvalidMultisig)?;
187
188        Ok(())
189    }
190
191    pub fn aggchain_params(&self) -> Option<Digest> {
192        match &self.aggchain_data {
193            AggchainData::ECDSA { .. } => None,
194            AggchainData::Generic {
195                aggchain_params, ..
196            } => Some(*aggchain_params),
197            AggchainData::MultisigOnly { .. } => None,
198            AggchainData::MultisigAndAggchainProof {
199                aggchain_proof:
200                    agglayer_interop_types::aggchain_proof::AggchainProof {
201                        aggchain_params, ..
202                    },
203                ..
204            } => Some(*aggchain_params),
205        }
206    }
207}
208
209impl From<&Certificate> for SignatureCommitmentValues {
210    fn from(certificate: &Certificate) -> Self {
211        Self {
212            new_local_exit_root: certificate.new_local_exit_root,
213            commit_imported_bridge_exits: ImportedBridgeExitCommitmentValues {
214                claims: certificate
215                    .imported_bridge_exits
216                    .iter()
217                    .map(|exit| exit.to_indexed_exit_hash())
218                    .collect(),
219            },
220            height: certificate.height.as_u64(),
221            aggchain_params: certificate.aggchain_params(),
222            certificate_id: certificate.hash().into(),
223        }
224    }
225}