1use agglayer_interop_types::{aggchain_proof::AggchainData, LocalExitRoot};
2use agglayer_primitives::{Address, Digest, Hashable, Signature, B256};
3use pessimistic_proof::{
4 core::commitment::{SignatureCommitmentValues, SignatureCommitmentVersion},
5 keccak::keccak256_combine,
6 unified_bridge::{BridgeExit, LeafType, TokenInfo},
7};
8use unified_bridge::{
9 ImportedBridgeExit, ImportedBridgeExitCommitmentValues, LocalExitTree, NetworkId,
10};
11
12use crate::{Certificate, Height, SignerError, U256};
13
14impl Default for Certificate {
15 fn default() -> Self {
16 let network_id = NetworkId::ETH_L1;
17 let wallet = Self::wallet_for_test(network_id);
18 let local_exit_root = LocalExitTree::<32>::default().get_root().into();
21 let height = Height::ZERO;
22 let (_new_local_exit_root, signature, _signer) = compute_signature_info(
23 local_exit_root,
24 &[],
25 &wallet,
26 height,
27 SignatureCommitmentVersion::V2,
28 );
29 Self {
30 network_id,
31 height,
32 prev_local_exit_root: local_exit_root,
33 new_local_exit_root: local_exit_root,
34 bridge_exits: Default::default(),
35 imported_bridge_exits: Default::default(),
36 aggchain_data: AggchainData::ECDSA { signature },
37 metadata: Default::default(),
38 custom_chain_data: vec![],
39 l1_info_tree_leaf_count: None,
40 }
41 }
42}
43
44pub fn compute_signature_info(
45 new_local_exit_root: LocalExitRoot,
46 imported_bridge_exits: &[ImportedBridgeExit],
47 wallet: &alloy::signers::local::PrivateKeySigner,
48 height: Height,
49 version: SignatureCommitmentVersion,
50) -> (B256, Signature, Address) {
51 use alloy::signers::SignerSync;
52 let combined_hash = SignatureCommitmentValues {
53 new_local_exit_root,
54 commit_imported_bridge_exits: ImportedBridgeExitCommitmentValues {
55 claims: imported_bridge_exits
56 .iter()
57 .map(|exit| exit.to_indexed_exit_hash())
58 .collect(),
59 },
60 height: height.as_u64(),
61 aggchain_params: None,
62 certificate_id: Digest::default(),
63 }
64 .commitment(version);
65
66 let signature = wallet
67 .sign_hash_sync(&combined_hash)
68 .expect("valid signature");
69 let signature = Signature::new(signature.r(), signature.s(), signature.v());
70
71 (combined_hash, signature, wallet.address().into())
72}
73
74impl Certificate {
75 pub fn wallet_for_test(network_id: NetworkId) -> alloy::signers::local::PrivateKeySigner {
76 let fake_priv_key = keccak256_combine([b"FAKEKEY:", network_id.to_be_bytes().as_slice()]);
77 alloy::signers::local::PrivateKeySigner::from_slice(fake_priv_key.as_bytes())
78 .expect("valid fake private key")
79 }
80
81 pub fn get_signer(&self) -> Address {
82 Self::wallet_for_test(self.network_id).address().into()
83 }
84
85 pub fn new_for_test(network_id: NetworkId, height: Height) -> Self {
86 Self::new_for_test_with_version(network_id, height, SignatureCommitmentVersion::V2)
87 }
88
89 pub fn new_for_test_with_version(
90 network_id: NetworkId,
91 height: Height,
92 version: SignatureCommitmentVersion,
93 ) -> Self {
94 let local_exit_root = LocalExitTree::<32>::default().get_root().into();
97
98 Self::new_for_test_custom(
99 network_id,
100 height,
101 local_exit_root,
102 0, AggchainDataType::Ecdsa,
104 version,
105 )
106 }
107
108 pub fn new_for_test_custom(
110 network_id: NetworkId,
111 height: Height,
112 prev_local_exit_root: LocalExitRoot,
113 num_bridge_exits: usize,
114 aggchain_data_type: AggchainDataType,
115 version: SignatureCommitmentVersion,
116 ) -> Self {
117 use rand::{Rng, SeedableRng};
118 let mut seed = [0u8; 32];
122 seed[0..4].copy_from_slice(&network_id.to_u32().to_le_bytes());
123 seed[4..12].copy_from_slice(&height.as_u64().to_le_bytes());
124 let mut rng = rand::rngs::StdRng::from_seed(seed);
126
127 let bridge_exits: Vec<BridgeExit> = (0..num_bridge_exits)
129 .map(|_| {
130 let origin_network = NetworkId::new(rng.random_range(0..100));
131 let token_address = Address::from(rng.random::<[u8; 20]>());
132 BridgeExit {
133 leaf_type: LeafType::Transfer,
134 token_info: TokenInfo {
135 origin_network,
136 origin_token_address: token_address,
137 },
138 dest_network: NetworkId::new(rng.random_range(0..100)),
139 dest_address: Address::from(rng.random::<[u8; 20]>()),
140 amount: U256::from(rng.random::<u128>()),
141 metadata: Some(Digest::from(rng.random::<[u8; 32]>())),
142 }
143 })
144 .collect();
145
146 let new_local_exit_root = if bridge_exits.is_empty() {
148 prev_local_exit_root
149 } else {
150 let mut local_exit_tree = LocalExitTree::<32>::default();
151 for exit in &bridge_exits {
152 local_exit_tree
153 .add_leaf(exit.hash())
154 .expect("Failed to add leaf");
155 }
156 local_exit_tree.get_root().into()
157 };
158
159 let wallet = Self::wallet_for_test(network_id);
160 let (_, signature, _signer) =
161 compute_signature_info(new_local_exit_root, &[], &wallet, height, version);
162
163 let aggchain_data = match aggchain_data_type {
164 AggchainDataType::Ecdsa => AggchainData::ECDSA { signature },
165 AggchainDataType::Generic => {
166 let aggchain_params = Digest::from(rng.random::<[u8; 32]>());
168 let proof = create_dummy_stark_proof();
169 AggchainData::Generic {
170 proof,
171 aggchain_params,
172 signature: Some(Box::new(signature)),
173 public_values: None,
174 }
175 }
176 AggchainDataType::MultisigOnly { num_signers } => {
177 let threshold = num_signers.div_ceil(2); let signatures: Vec<Option<Signature>> = (0..num_signers)
180 .map(|i| {
181 if i < threshold {
182 Some(signature) } else {
185 None
186 }
187 })
188 .collect();
189
190 AggchainData::MultisigOnly {
191 multisig: agglayer_interop_types::aggchain_proof::MultisigPayload(signatures),
192 }
193 }
194 AggchainDataType::MultisigAndAggchainProof { num_signers } => {
195 let threshold = num_signers.div_ceil(2);
197 let signatures: Vec<Option<Signature>> = (0..num_signers)
198 .map(|i| if i < threshold { Some(signature) } else { None })
199 .collect();
200
201 let aggchain_params = Digest::from(rng.random::<[u8; 32]>());
202 let proof = create_dummy_stark_proof();
203
204 AggchainData::MultisigAndAggchainProof {
205 multisig: agglayer_interop_types::aggchain_proof::MultisigPayload(signatures),
206 aggchain_proof: agglayer_interop_types::aggchain_proof::AggchainProof {
207 proof,
208 aggchain_params,
209 public_values: None,
210 },
211 }
212 }
213 };
214
215 Self {
216 network_id,
217 height,
218 prev_local_exit_root,
219 new_local_exit_root,
220 bridge_exits,
221 imported_bridge_exits: Default::default(),
222 aggchain_data,
223 metadata: Default::default(),
224 custom_chain_data: vec![],
225 l1_info_tree_leaf_count: None,
226 }
227 }
228
229 pub fn with_new_local_exit_root(mut self, new_local_exit_root: LocalExitRoot) -> Self {
230 self.new_local_exit_root = new_local_exit_root;
231 self
232 }
233}
234
235pub enum AggchainDataType {
237 Ecdsa,
239 Generic,
241 MultisigOnly { num_signers: usize },
243 MultisigAndAggchainProof { num_signers: usize },
245}
246
247pub const EMPTY_ELF: &[u8] = include_bytes!("tests/empty.elf");
250
251fn create_dummy_stark_proof() -> agglayer_interop_types::aggchain_proof::Proof {
254 use sp1_sdk::Prover;
255
256 let (proof, vkey) = {
257 let client = sp1_sdk::ProverClient::builder().mock().build();
258 let (proving_key, verif_key) = client.setup(EMPTY_ELF);
259 let dummy_proof = sp1_sdk::SP1ProofWithPublicValues::create_mock_proof(
260 &proving_key,
261 sp1_sdk::SP1PublicValues::new(),
262 sp1_sdk::SP1ProofMode::Compressed,
263 sp1_sdk::SP1_CIRCUIT_VERSION,
264 );
265 let proof = dummy_proof.proof.try_as_compressed().unwrap();
266 (proof, verif_key)
267 };
268
269 agglayer_interop_types::aggchain_proof::Proof::SP1Stark(
270 agglayer_interop_types::aggchain_proof::SP1StarkWithContext {
271 proof,
272 vkey,
273 version: "test".to_string(),
274 },
275 )
276}
277
278impl Certificate {
279 pub fn retrieve_signer(
281 &self,
282 version: SignatureCommitmentVersion,
283 ) -> Result<Address, SignerError> {
284 let (signature, commitment) = match &self.aggchain_data {
285 AggchainData::ECDSA { signature } => {
286 let commitment = SignatureCommitmentValues::from(self).commitment(version);
287 (signature, commitment)
288 }
289 AggchainData::Generic { signature, .. } => {
290 let signature = signature.as_ref().ok_or(SignerError::Missing)?;
291 let commitment = SignatureCommitmentValues::from(self)
292 .commitment(SignatureCommitmentVersion::V4);
293 (signature.as_ref(), commitment)
294 }
295 AggchainData::MultisigOnly { .. } => unimplemented!("adapt tests for multisig"),
296 AggchainData::MultisigAndAggchainProof { .. } => {
297 unimplemented!("adapt tests for multisig")
298 }
299 };
300
301 signature
302 .recover_address_from_prehash(&commitment)
303 .map_err(SignerError::Recovery)
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use agglayer_interop_types::aggchain_proof::AggchainData;
310 use pessimistic_proof::core::commitment::SignatureCommitmentVersion;
311 use rstest::rstest;
312
313 use crate::{Certificate, Height};
314
315 #[rstest]
316 fn can_retrieve_correct_signer(
317 #[values(SignatureCommitmentVersion::V2, SignatureCommitmentVersion::V3)]
318 version: SignatureCommitmentVersion,
319 ) {
320 let certificate = Certificate::new_for_test_with_version(2.into(), 1.into(), version);
321 let expected_signer = certificate.get_signer();
322
323 assert_eq!(
325 certificate.retrieve_signer(version).unwrap(),
326 expected_signer
327 );
328
329 let agglayer_types::aggchain_proof::AggchainData::ECDSA { signature } =
331 certificate.aggchain_data
332 else {
333 panic!("inconsistent test data")
334 };
335
336 assert!(certificate
337 .verify_legacy_ecdsa(expected_signer, &signature)
338 .is_ok())
339 }
340
341 #[test]
342 fn test_new_for_test_custom_ecdsa() {
343 use unified_bridge::LocalExitTree;
344
345 use crate::certificate::testutils::AggchainDataType;
346
347 let cert = Certificate::new_for_test_custom(
348 1.into(),
349 Height::ZERO,
350 LocalExitTree::<32>::default().get_root().into(),
351 5,
352 AggchainDataType::Ecdsa,
353 SignatureCommitmentVersion::V2,
354 );
355
356 assert_eq!(cert.network_id, 1.into());
357 assert_eq!(cert.height, Height::ZERO);
358 assert_eq!(cert.bridge_exits.len(), 5);
359 assert!(matches!(cert.aggchain_data, AggchainData::ECDSA { .. }));
360 }
361
362 #[test]
363 fn test_new_for_test_custom_generic() {
364 use unified_bridge::LocalExitTree;
365
366 use crate::certificate::testutils::AggchainDataType;
367
368 let cert = Certificate::new_for_test_custom(
369 2.into(),
370 Height::ZERO,
371 LocalExitTree::<32>::default().get_root().into(),
372 3,
373 AggchainDataType::Generic,
374 SignatureCommitmentVersion::V2,
375 );
376
377 assert_eq!(cert.bridge_exits.len(), 3);
378 assert!(matches!(cert.aggchain_data, AggchainData::Generic { .. }));
379 }
380
381 #[test]
382 fn test_new_for_test_custom_multisig_only() {
383 use unified_bridge::LocalExitTree;
384
385 use crate::certificate::testutils::AggchainDataType;
386
387 let cert = Certificate::new_for_test_custom(
388 3.into(),
389 Height::ZERO,
390 LocalExitTree::<32>::default().get_root().into(),
391 2,
392 AggchainDataType::MultisigOnly { num_signers: 5 },
393 SignatureCommitmentVersion::V2,
394 );
395
396 assert_eq!(cert.bridge_exits.len(), 2);
397 assert!(matches!(
398 cert.aggchain_data,
399 AggchainData::MultisigOnly { .. }
400 ));
401 }
402
403 #[test]
404 fn test_new_for_test_custom_multisig_and_aggchain_proof() {
405 use unified_bridge::LocalExitTree;
406
407 use crate::certificate::testutils::AggchainDataType;
408
409 let cert = Certificate::new_for_test_custom(
410 4.into(),
411 Height::ZERO,
412 LocalExitTree::<32>::default().get_root().into(),
413 10,
414 AggchainDataType::MultisigAndAggchainProof { num_signers: 3 },
415 SignatureCommitmentVersion::V3,
416 );
417
418 assert_eq!(cert.bridge_exits.len(), 10);
419 assert!(matches!(
420 cert.aggchain_data,
421 AggchainData::MultisigAndAggchainProof { .. }
422 ));
423 }
424}