agglayer_types/certificate/
testutils.rs

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        // The LET depth can't be inferred to be the default of 32 due to the
19        // limitations of the Rust compiler's type inference, so we specify it here.
20        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        // The LET depth can't be inferred to be the default of 32 due to the
95        // limitations of the Rust compiler's type inference, so we specify it here.
96        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, // No bridge exits for basic test certificates
103            AggchainDataType::Ecdsa,
104            version,
105        )
106    }
107
108    /// Generate a certificate with random bridge exits and custom parameters.
109    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        // Use a constant seed for deterministic, repeatable tests
119        // Seed is derived from network_id and height for variety while maintaining
120        // determinism
121        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        // Remaining bytes stay as 0 for determinism
125        let mut rng = rand::rngs::StdRng::from_seed(seed);
126
127        // Generate deterministic bridge exits
128        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        // Calculate new_local_exit_root based on bridge_exits
147        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                // Generic variant with proof, aggchain_params, and signature
167                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                // Generate multisig with specified number of signers
178                let threshold = num_signers.div_ceil(2); // Majority threshold
179                let signatures: Vec<Option<Signature>> = (0..num_signers)
180                    .map(|i| {
181                        if i < threshold {
182                            Some(signature) // Reuse the same signature for
183                                            // simplicity
184                        } 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                // Generate both multisig and aggchain proof
196                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
235/// Enum to specify which AggchainData variant to use in test certificates
236pub enum AggchainDataType {
237    /// Legacy ECDSA signature
238    Ecdsa,
239    /// Generic proof with aggchain params
240    Generic,
241    /// Multisig only with specified number of signers
242    MultisigOnly { num_signers: usize },
243    /// Multisig and aggchain proof with specified number of signers
244    MultisigAndAggchainProof { num_signers: usize },
245}
246
247/// Empty ELF file for testing purposes.
248/// This is a minimal ELF that can be used to create dummy SP1 proofs in tests.
249pub const EMPTY_ELF: &[u8] = include_bytes!("tests/empty.elf");
250
251/// Create a dummy STARK proof for testing purposes.
252/// This creates a minimal SP1 proof that can be used in tests.
253fn 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    /// Retrieve the signer from the certificate signature.
280    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        // Can retrieve the correct signer address from the signature
324        assert_eq!(
325            certificate.retrieve_signer(version).unwrap(),
326            expected_signer
327        );
328
329        // Check that the signature is valid
330        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}