clementine_core/
citrea.rs

1//! # Citrea Related Utilities
2
3use crate::config::protocol::ProtocolParamset;
4use crate::config::protocol::ProtocolParamsetExt;
5use crate::database::DatabaseTransaction;
6use crate::{citrea::BRIDGE_CONTRACT::DepositReplaced, database::Database};
7use alloy::{
8    eips::{BlockId, BlockNumberOrTag},
9    network::EthereumWallet,
10    primitives::{keccak256, U256},
11    providers::{
12        fillers::{
13            BlobGasFiller, ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller,
14            WalletFiller,
15        },
16        Provider, ProviderBuilder, RootProvider,
17    },
18    rpc::types::{EIP1186AccountProofResponse, Filter, Log},
19    signers::{local::PrivateKeySigner, Signer},
20    sol,
21    sol_types::SolEvent,
22    transports::http::reqwest::Url,
23};
24use bitcoin::{hashes::Hash, OutPoint, Txid, XOnlyPublicKey};
25use bridge_circuit_host::receipt_from_inner;
26use circuits_lib::bridge_circuit::{
27    lc_proof::check_method_id,
28    structs::{LightClientProof, StorageProof},
29};
30use citrea_sov_rollup_interface::zk::light_client_proof::output::LightClientCircuitOutput;
31use clementine_errors::BridgeError;
32use eyre::Context;
33use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
34use jsonrpsee::proc_macros::rpc;
35use risc0_zkvm::{InnerReceipt, Receipt};
36use std::{fmt::Debug, time::Duration};
37use tonic::async_trait;
38
39#[cfg(test)]
40pub const SATS_TO_WEI_MULTIPLIER: u64 = 10_000_000_000;
41pub const BRIDGE_CONTRACT_ADDRESS: &str = "0x3100000000000000000000000000000000000002";
42
43#[cfg(test)]
44pub const LIGHT_CLIENT_ADDRESS: &str = "0x3100000000000000000000000000000000000001";
45
46const UTXOS_STORAGE_INDEX: [u8; 32] =
47    hex_literal::hex!("0000000000000000000000000000000000000000000000000000000000000007");
48const DEPOSIT_STORAGE_INDEX: [u8; 32] =
49    hex_literal::hex!("0000000000000000000000000000000000000000000000000000000000000008");
50
51// Codegen from ABI file to interact with the contract.
52sol!(
53    #[allow(missing_docs)]
54    #[sol(rpc)]
55    #[derive(Debug)]
56    BRIDGE_CONTRACT,
57    "../scripts/Bridge.json"
58);
59
60#[async_trait]
61pub trait CitreaClientT: Send + Sync + Debug + Clone + 'static {
62    /// # Parameters
63    ///
64    /// - `citrea_rpc_url`: URL of the Citrea RPC.
65    /// - `light_client_prover_url`: URL of the Citrea light client prover RPC.
66    /// - `chain_id`: Citrea's EVM chain id.
67    /// - `secret_key`: EVM secret key of the EVM user. If not given, random
68    ///   secret key is used (wallet is not required). This is given mostly for
69    ///   testing purposes.
70    async fn new(
71        citrea_rpc_url: String,
72        light_client_prover_url: String,
73        chain_id: u32,
74        secret_key: Option<PrivateKeySigner>,
75        timeout: Option<Duration>,
76    ) -> Result<Self, BridgeError>;
77
78    /// Returns deposit move txids, starting from the last deposit index.
79    ///
80    /// # Parameters
81    ///
82    /// - `last_deposit_idx`: Last deposit index. None if no deposit
83    /// - `to_height`: End block height (inclusive)
84    async fn collect_deposit_move_txids(
85        &self,
86        last_deposit_idx: Option<u32>,
87        to_height: u64,
88    ) -> Result<Vec<(u64, Txid)>, BridgeError>;
89
90    /// Returns withdrawal utxos, starting from the last withdrawal index.
91    ///
92    /// # Parameters
93    ///
94    /// - `last_withdrawal_idx`: Last withdrawal index. None if no withdrawal
95    /// - `to_height`: End block height (inclusive)
96    async fn collect_withdrawal_utxos(
97        &self,
98        last_withdrawal_idx: Option<u32>,
99        to_height: u64,
100    ) -> Result<Vec<(u64, OutPoint)>, BridgeError>;
101
102    /// Returns the light client proof and its L2 height for the given L1 block
103    /// height.
104    ///
105    /// # Returns
106    ///
107    /// A tuple, wrapped around a [`Some`] if present:
108    ///
109    /// - [`u64`]: Last L2 block height.
110    /// - [`ProtocolParamset`]: Protocol paramset.
111    ///
112    /// If not present, [`None`] is returned.
113    async fn get_light_client_proof(
114        &self,
115        l1_height: u64,
116        paramset: &'static ProtocolParamset,
117    ) -> Result<Option<(LightClientProof, Receipt, u64)>, BridgeError>;
118
119    /// Returns the L2 block height range for the given L1 block height.
120    ///
121    /// # Parameters
122    ///
123    /// - `block_height`: L1 block height.
124    /// - `timeout`: Timeout duration.
125    ///
126    /// # Returns
127    ///
128    /// A tuple of:
129    ///
130    /// - [`u64`]: Start of the L2 block height (not inclusive)
131    /// - [`u64`]: End of the L2 block height (inclusive)
132    async fn get_citrea_l2_height_range(
133        &self,
134        block_height: u64,
135        timeout: Duration,
136        paramset: &'static ProtocolParamset,
137    ) -> Result<(u64, u64), BridgeError>;
138
139    /// Returns the replacement deposit move txids for the given range of blocks.
140    ///
141    /// # Parameters
142    ///
143    /// - `from_height`: Start block height (inclusive)
144    /// - `to_height`: End block height (inclusive)
145    ///
146    /// # Returns
147    ///
148    /// A vector of tuples, each containing:
149    ///
150    /// - [`Txid`]: The original move txid.
151    /// - [`Txid`]: The replacement move txid.
152    async fn get_replacement_deposit_move_txids(
153        &self,
154        from_height: u64,
155        to_height: u64,
156    ) -> Result<Vec<(u32, Txid)>, BridgeError>;
157
158    async fn check_nofn_correctness(
159        &self,
160        nofn_xonly_pk: XOnlyPublicKey,
161    ) -> Result<(), BridgeError>;
162
163    async fn get_storage_proof(
164        &self,
165        l2_height: u64,
166        deposit_index: u32,
167    ) -> Result<StorageProof, BridgeError>;
168
169    async fn fetch_validate_and_store_lcp(
170        &self,
171        payout_block_height: u64,
172        deposit_index: u32,
173        db: &Database,
174        dbtx: Option<DatabaseTransaction<'_>>,
175        paramset: &'static ProtocolParamset,
176    ) -> Result<Receipt, BridgeError>;
177
178    /// Returns the current L2 block height from Citrea.
179    ///
180    /// # Returns
181    ///
182    /// - [`Result<u32, BridgeError>`]: Current L2 block height, or an error if the request fails or the value doesn't fit in u32.
183    async fn get_current_l2_block_height(&self) -> Result<u32, BridgeError>;
184}
185
186/// Citrea client is responsible for interacting with the Citrea EVM and Citrea
187/// RPC.
188#[derive(Clone, Debug)]
189pub struct CitreaClient {
190    pub client: HttpClient,
191    pub light_client_prover_client: HttpClient,
192    #[cfg(test)]
193    pub wallet_address: alloy::primitives::Address,
194    pub contract: CitreaContract,
195}
196
197impl CitreaClient {
198    /// Returns all logs for the given filter and block range while considering
199    /// about the 1000 block limit.
200    async fn get_logs(
201        &self,
202        filter: Filter,
203        from_height: u64,
204        to_height: u64,
205    ) -> Result<Vec<Log>, BridgeError> {
206        let mut logs = vec![];
207
208        let mut from_height = from_height;
209        while from_height <= to_height {
210            // Block num is 999 because limits are inclusive.
211            let to_height = std::cmp::min(from_height + 999, to_height);
212            tracing::debug!("Fetching logs from {} to {}", from_height, to_height);
213
214            // Update filter with the new range.
215            let filter = filter.clone();
216            let filter = filter.from_block(BlockNumberOrTag::Number(from_height));
217            let filter = filter.to_block(BlockNumberOrTag::Number(to_height));
218
219            let logs_chunk = self
220                .contract
221                .provider()
222                .get_logs(&filter)
223                .await
224                .wrap_err("Failed to get logs")?;
225            logs.extend(logs_chunk);
226
227            from_height = to_height + 1;
228        }
229
230        Ok(logs)
231    }
232}
233
234#[async_trait]
235impl CitreaClientT for CitreaClient {
236    /// Fetches the storage proof for a given deposit index and transaction ID.
237    ///
238    /// This function interacts with an Citrea RPC endpoint to retrieve a storage proof,
239    /// which includes proof details for both the UTXO and the deposit index.
240    ///
241    /// # Arguments
242    /// * `l2_height` - A `u64` representing the L2 block height.
243    /// * `deposit_index` - A `u32` representing the deposit index.
244    ///
245    /// # Returns
246    /// Returns a `StorageProof` struct containing serialized storage proofs for the UTXO and deposit index.
247    async fn get_storage_proof(
248        &self,
249        l2_height: u64,
250        deposit_index: u32,
251    ) -> Result<StorageProof, BridgeError> {
252        let ind = deposit_index;
253        let tx_index: u32 = ind * 2;
254
255        let storage_address_wd_utxo_bytes = keccak256(UTXOS_STORAGE_INDEX);
256        let storage_address_wd_utxo: U256 = U256::from_be_bytes(
257            <[u8; 32]>::try_from(&storage_address_wd_utxo_bytes[..])
258                .wrap_err("Storage address wd utxo bytes slice with incorrect length")?,
259        );
260
261        // Storage key address calculation UTXO
262        let storage_key_wd_utxo: U256 = storage_address_wd_utxo + U256::from(tx_index);
263        let storage_key_wd_utxo_hex =
264            format!("0x{}", hex::encode(storage_key_wd_utxo.to_be_bytes::<32>()));
265
266        // Storage key address calculation Vout
267        let storage_key_vout: U256 = storage_address_wd_utxo + U256::from(tx_index + 1);
268        let storage_key_vout_hex =
269            format!("0x{}", hex::encode(storage_key_vout.to_be_bytes::<32>()));
270
271        // Storage key address calculation Deposit
272        let storage_address_deposit_bytes = keccak256(DEPOSIT_STORAGE_INDEX);
273        let storage_address_deposit: U256 = U256::from_be_bytes(
274            <[u8; 32]>::try_from(&storage_address_deposit_bytes[..])
275                .wrap_err("Storage address deposit bytes slice with incorrect length")?,
276        );
277
278        let storage_key_deposit: U256 = storage_address_deposit + U256::from(deposit_index);
279        let storage_key_deposit_hex = hex::encode(storage_key_deposit.to_be_bytes::<32>());
280        let storage_key_deposit_hex = format!("0x{storage_key_deposit_hex}");
281
282        let response: serde_json::Value = self
283            .client
284            .get_proof(
285                BRIDGE_CONTRACT_ADDRESS,
286                vec![
287                    storage_key_wd_utxo_hex,
288                    storage_key_vout_hex,
289                    storage_key_deposit_hex,
290                ],
291                format!("0x{l2_height:x}"),
292            )
293            .await
294            .wrap_err("Failed to get storage proof from rpc")?;
295
296        let response: EIP1186AccountProofResponse = serde_json::from_value(response)
297            .wrap_err("Failed to deserialize EIP1186AccountProofResponse")?;
298
299        // It does not seem possible to get a storage proof with less than 3 items. But still
300        // we check it to avoid panics.
301        if response.storage_proof.len() < 3 {
302            return Err(eyre::eyre!(
303                "Expected at least 3 storage proofs, got {}",
304                response.storage_proof.len()
305            )
306            .into());
307        }
308
309        let serialized_utxo = serde_json::to_string(&response.storage_proof[0])
310            .wrap_err("Failed to serialize storage proof utxo")?;
311
312        let serialized_vout = serde_json::to_string(&response.storage_proof[1])
313            .wrap_err("Failed to serialize storage proof vout")?;
314
315        let serialized_deposit = serde_json::to_string(&response.storage_proof[2])
316            .wrap_err("Failed to serialize storage proof deposit")?;
317
318        Ok(StorageProof {
319            storage_proof_utxo: serialized_utxo,
320            storage_proof_vout: serialized_vout,
321            storage_proof_deposit_txid: serialized_deposit,
322            index: ind,
323        })
324    }
325
326    async fn fetch_validate_and_store_lcp(
327        &self,
328        payout_block_height: u64,
329        deposit_index: u32,
330        db: &Database,
331        mut dbtx: Option<DatabaseTransaction<'_>>,
332        paramset: &'static ProtocolParamset,
333    ) -> Result<Receipt, BridgeError> {
334        let saved_data = db
335            .get_lcp_for_assert(dbtx.as_deref_mut(), deposit_index)
336            .await?;
337        if let Some(lcp) = saved_data {
338            // if already saved, do nothing
339            return Ok(lcp);
340        };
341
342        let lcp_result = self
343            .get_light_client_proof(payout_block_height, paramset)
344            .await?;
345        let (_lcp, lcp_receipt, _l2_height) = match lcp_result {
346            Some(lcp) => lcp,
347            None => {
348                return Err(eyre::eyre!(
349                    "Light client proof could not be fetched found for block height {}",
350                    payout_block_height
351                )
352                .into())
353            }
354        };
355
356        // save the LCP for assert
357        db.insert_lcp_for_assert(dbtx, deposit_index, lcp_receipt.clone())
358            .await?;
359
360        Ok(lcp_receipt)
361    }
362
363    async fn new(
364        citrea_rpc_url: String,
365        light_client_prover_url: String,
366        chain_id: u32,
367        secret_key: Option<PrivateKeySigner>,
368        timeout: Option<Duration>,
369    ) -> Result<Self, BridgeError> {
370        let citrea_rpc_url = Url::parse(&citrea_rpc_url).wrap_err("Can't parse Citrea RPC URL")?;
371        let light_client_prover_url =
372            Url::parse(&light_client_prover_url).wrap_err("Can't parse Citrea LCP RPC URL")?;
373        let secret_key = secret_key.unwrap_or(PrivateKeySigner::random());
374
375        let key = secret_key.with_chain_id(Some(chain_id.into()));
376
377        #[cfg(test)]
378        let wallet_address = key.address();
379
380        tracing::info!("Wallet address: {}", key.address());
381
382        let provider = ProviderBuilder::new()
383            .wallet(EthereumWallet::from(key))
384            .on_http(citrea_rpc_url.clone());
385
386        tracing::info!("Provider created");
387
388        let contract = BRIDGE_CONTRACT::new(
389            BRIDGE_CONTRACT_ADDRESS
390                .parse()
391                .expect("Correct contract address"),
392            provider,
393        );
394
395        tracing::info!("Contract created");
396
397        let client = HttpClientBuilder::default()
398            .request_timeout(timeout.unwrap_or(Duration::from_secs(60)))
399            .build(citrea_rpc_url)
400            .wrap_err("Failed to create Citrea RPC client")?;
401
402        tracing::info!("Citrea RPC client created");
403
404        let light_client_prover_client = HttpClientBuilder::default()
405            .request_timeout(timeout.unwrap_or(Duration::from_secs(60)))
406            .build(light_client_prover_url)
407            .wrap_err("Failed to create Citrea LCP RPC client")?;
408
409        tracing::info!("Citrea LCP RPC client created");
410
411        Ok(CitreaClient {
412            client,
413            light_client_prover_client,
414            #[cfg(test)]
415            wallet_address,
416            contract,
417        })
418    }
419
420    async fn collect_deposit_move_txids(
421        &self,
422        last_deposit_idx: Option<u32>,
423        to_height: u64,
424    ) -> Result<Vec<(u64, Txid)>, BridgeError> {
425        let mut move_txids = vec![];
426
427        let mut start_idx = match last_deposit_idx {
428            Some(idx) => idx + 1,
429            None => 0,
430        };
431
432        loop {
433            let deposit_txid = self
434                .contract
435                .depositTxIds(U256::from(start_idx))
436                .block(BlockId::Number(BlockNumberOrTag::Number(to_height)))
437                .call()
438                .await;
439            match deposit_txid {
440                Err(e) if e.to_string().contains("execution reverted") => {
441                    tracing::trace!("Deposit txid not found for index, error: {:?}", e);
442                    break;
443                }
444                Err(e) => return Err(e.into()),
445                Ok(_) => {}
446            }
447            tracing::info!("Deposit txid found for index: {:?}", deposit_txid);
448
449            let deposit_txid = deposit_txid.expect("Failed to get deposit txid");
450            let move_txid = Txid::from_slice(deposit_txid._0.as_ref())
451                .wrap_err("Failed to convert move txid to Txid")?;
452            move_txids.push((start_idx as u64, move_txid));
453            start_idx += 1;
454        }
455        Ok(move_txids)
456    }
457
458    async fn collect_withdrawal_utxos(
459        &self,
460        last_withdrawal_idx: Option<u32>,
461        to_height: u64,
462    ) -> Result<Vec<(u64, OutPoint)>, BridgeError> {
463        let mut utxos = vec![];
464
465        let mut start_idx = match last_withdrawal_idx {
466            Some(idx) => idx + 1,
467            None => 0,
468        };
469
470        loop {
471            let withdrawal_utxo = self
472                .contract
473                .withdrawalUTXOs(U256::from(start_idx))
474                .block(BlockId::Number(BlockNumberOrTag::Number(to_height)))
475                .call()
476                .await;
477            match withdrawal_utxo {
478                Err(e) if e.to_string().contains("execution reverted") => {
479                    tracing::trace!("Withdrawal utxo not found for index, error: {:?}", e);
480                    break;
481                }
482                Err(e) => return Err(e.into()),
483                Ok(_) => {}
484            }
485            let withdrawal_utxo = withdrawal_utxo.expect("Failed to get withdrawal UTXO");
486            let txid = withdrawal_utxo.txId.0;
487            let txid =
488                Txid::from_slice(txid.as_ref()).wrap_err("Failed to convert txid to Txid")?;
489            let vout = withdrawal_utxo.outputId.0;
490            let vout = u32::from_le_bytes(vout);
491            let utxo = OutPoint { txid, vout };
492            utxos.push((start_idx as u64, utxo));
493            start_idx += 1;
494        }
495        Ok(utxos)
496    }
497
498    async fn get_light_client_proof(
499        &self,
500        l1_height: u64,
501        paramset: &'static ProtocolParamset,
502    ) -> Result<Option<(LightClientProof, Receipt, u64)>, BridgeError> {
503        let proof_result = self
504            .light_client_prover_client
505            .get_light_client_proof_by_l1_height(l1_height)
506            .await
507            .wrap_err("Failed to get light client proof")?;
508        tracing::debug!(
509            "Light client proof result {}: {:?}",
510            l1_height,
511            proof_result
512        );
513
514        let ret = if let Some(proof_result) = proof_result {
515            let decoded: InnerReceipt = bincode::deserialize(&proof_result.proof)
516                .wrap_err("Failed to deserialize light client proof from citrea lcp")?;
517            let receipt = receipt_from_inner(decoded)
518                .wrap_err("Failed to create receipt from light client proof")?;
519
520            let l2_height = u64::try_from(proof_result.light_client_proof_output.last_l2_height)
521                .wrap_err("Failed to convert l2 height to u64")?;
522
523            let lc_image_id = paramset.get_lcp_image_id()?;
524
525            let proof_output: LightClientCircuitOutput = borsh::from_slice(&receipt.journal.bytes)
526                .wrap_err("Failed to deserialize light client circuit output")?;
527
528            if !paramset.is_regtest() {
529                receipt
530                    .verify(lc_image_id)
531                    .map_err(|_| eyre::eyre!("Light client proof verification failed"))?;
532
533                if !check_method_id(&proof_output, lc_image_id) {
534                    return Err(eyre::eyre!(
535                    "Current light client proof method ID does not match the expected LC image ID"
536                )
537                    .into());
538                }
539            }
540
541            Some((
542                LightClientProof {
543                    lc_journal: receipt.journal.bytes.clone(),
544                },
545                receipt,
546                l2_height,
547            ))
548        } else {
549            None
550        };
551
552        Ok(ret)
553    }
554
555    async fn get_citrea_l2_height_range(
556        &self,
557        block_height: u64,
558        timeout: Duration,
559        paramset: &'static ProtocolParamset,
560    ) -> Result<(u64, u64), BridgeError> {
561        let start = std::time::Instant::now();
562        let proof_current = loop {
563            if let Some(proof) = self.get_light_client_proof(block_height, paramset).await? {
564                break proof;
565            }
566
567            if start.elapsed() > timeout {
568                return Err(eyre::eyre!(
569                    "Light client proof not found for block height {} after {} seconds",
570                    block_height,
571                    timeout.as_secs()
572                )
573                .into());
574            }
575
576            tokio::time::sleep(Duration::from_secs(1)).await;
577        };
578
579        let proof_previous = self
580            .get_light_client_proof(block_height - 1, paramset)
581            .await?
582            .ok_or(eyre::eyre!(
583                "Light client proof not found for block height: {}",
584                block_height - 1
585            ))?;
586
587        let l2_height_end: u64 = proof_current.2;
588        let l2_height_start: u64 = proof_previous.2;
589
590        Ok((l2_height_start, l2_height_end))
591    }
592
593    async fn get_replacement_deposit_move_txids(
594        &self,
595        from_height: u64,
596        to_height: u64,
597    ) -> Result<Vec<(u32, Txid)>, BridgeError> {
598        let mut replacement_move_txids = vec![];
599
600        // get logs
601        let filter = self.contract.event_filter::<DepositReplaced>().filter;
602        let logs = self.get_logs(filter, from_height, to_height).await?;
603
604        for log in logs {
605            let replacement_raw_data = &log.data().data;
606
607            let idx = DepositReplaced::abi_decode_data(replacement_raw_data, false)
608                .wrap_err("Failed to decode replacement deposit data")?
609                .0;
610            let new_move_txid = DepositReplaced::abi_decode_data(replacement_raw_data, false)
611                .wrap_err("Failed to decode replacement deposit data")?
612                .2;
613
614            let idx = u32::try_from(idx).wrap_err("Failed to convert idx to u32")?;
615            let new_move_txid = Txid::from_slice(new_move_txid.as_ref())
616                .wrap_err("Failed to convert new move txid to Txid")?;
617
618            replacement_move_txids.push((idx, new_move_txid));
619        }
620
621        Ok(replacement_move_txids)
622    }
623
624    async fn check_nofn_correctness(
625        &self,
626        nofn_xonly_pk: XOnlyPublicKey,
627    ) -> Result<(), BridgeError> {
628        if std::env::var("DISABLE_NOFN_CHECK").is_ok() {
629            return Ok(());
630        }
631
632        let contract_nofn_xonly_pk = self
633            .contract
634            .getAggregatedKey()
635            .call()
636            .await
637            .wrap_err("Failed to get script prefix")?
638            ._0;
639
640        let contract_nofn_xonly_pk = XOnlyPublicKey::from_slice(contract_nofn_xonly_pk.as_ref())
641            .wrap_err("Failed to convert citrea contract script nofn bytes to xonly pk")?;
642        if contract_nofn_xonly_pk != nofn_xonly_pk {
643            return Err(eyre::eyre!("Nofn of deposit does not match with citrea contract").into());
644        }
645        Ok(())
646    }
647
648    async fn get_current_l2_block_height(&self) -> Result<u32, BridgeError> {
649        // Query Citrea RPC to get the current L2 block number
650        // U256 is automatically deserialized from hex string by jsonrpsee
651        let block_number_u256 = self
652            .client
653            .block_number()
654            .await
655            .wrap_err("Failed to get L2 block height from Citrea RPC")?;
656
657        // Convert U256 to u32, return error if it doesn't fit
658        let block_number: u32 = block_number_u256
659            .try_into()
660            .map_err(|_| eyre::eyre!("L2 block height {} exceeds u32::MAX", block_number_u256))
661            .wrap_err("Failed to convert L2 block height to u32")?;
662        Ok(block_number)
663    }
664}
665
666#[rpc(client, namespace = "lightClientProver")]
667trait LightClientProverRpc {
668    /// Generate state transition data for the given L1 block height, and return the data as a borsh serialized hex string.
669    #[method(name = "getLightClientProofByL1Height")]
670    async fn get_light_client_proof_by_l1_height(
671        &self,
672        l1_height: u64,
673    ) -> RpcResult<Option<citrea_sov_rollup_interface::rpc::LightClientProofResponse>>;
674}
675
676#[rpc(client, namespace = "eth")]
677pub trait CitreaRpc {
678    #[method(name = "getProof")]
679    async fn get_proof(
680        &self,
681        address: &str,
682        storage_keys: Vec<String>,
683        block: String,
684    ) -> RpcResult<serde_json::Value>;
685
686    #[method(name = "blockNumber")]
687    async fn block_number(&self) -> RpcResult<U256>;
688}
689
690// Ugly typedefs.
691type CitreaContract = BRIDGE_CONTRACT::BRIDGE_CONTRACTInstance<
692    (),
693    FillProvider<
694        JoinFill<
695            JoinFill<
696                alloy::providers::Identity,
697                JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
698            >,
699            WalletFiller<EthereumWallet>,
700        >,
701        RootProvider,
702    >,
703>;