agglayer_types/certificate/
mod.rs1use 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#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
47#[cfg_attr(feature = "testutils", derive(Eq, PartialEq))]
48pub struct Certificate {
49 pub network_id: NetworkId,
51 pub height: Height,
53 pub prev_local_exit_root: LocalExitRoot,
55 pub new_local_exit_root: LocalExitRoot,
57 pub bridge_exits: Vec<BridgeExit>,
59 pub imported_bridge_exits: Vec<ImportedBridgeExit>,
61 pub metadata: Metadata,
63 #[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 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 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 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}