1use crate::config::protocol::ProtocolParamset;
4use crate::database::DatabaseTransaction;
5use crate::errors::BridgeError;
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 eyre::Context;
32use jsonrpsee::http_client::{HttpClient, HttpClientBuilder};
33use jsonrpsee::proc_macros::rpc;
34use risc0_zkvm::{InnerReceipt, Receipt};
35use std::{fmt::Debug, time::Duration};
36use tonic::async_trait;
37
38pub const LIGHT_CLIENT_ADDRESS: &str = "0x3100000000000000000000000000000000000001";
39pub const BRIDGE_CONTRACT_ADDRESS: &str = "0x3100000000000000000000000000000000000002";
40pub const SATS_TO_WEI_MULTIPLIER: u64 = 10_000_000_000;
41const UTXOS_STORAGE_INDEX: [u8; 32] =
42 hex_literal::hex!("0000000000000000000000000000000000000000000000000000000000000007");
43const DEPOSIT_STORAGE_INDEX: [u8; 32] =
44 hex_literal::hex!("0000000000000000000000000000000000000000000000000000000000000008");
45
46sol!(
48 #[allow(missing_docs)]
49 #[sol(rpc)]
50 #[derive(Debug)]
51 BRIDGE_CONTRACT,
52 "../scripts/Bridge.json"
53);
54
55#[async_trait]
56pub trait CitreaClientT: Send + Sync + Debug + Clone + 'static {
57 async fn new(
66 citrea_rpc_url: String,
67 light_client_prover_url: String,
68 chain_id: u32,
69 secret_key: Option<PrivateKeySigner>,
70 timeout: Option<Duration>,
71 ) -> Result<Self, BridgeError>;
72
73 async fn collect_deposit_move_txids(
80 &self,
81 last_deposit_idx: Option<u32>,
82 to_height: u64,
83 ) -> Result<Vec<(u64, Txid)>, BridgeError>;
84
85 async fn collect_withdrawal_utxos(
92 &self,
93 last_withdrawal_idx: Option<u32>,
94 to_height: u64,
95 ) -> Result<Vec<(u64, OutPoint)>, BridgeError>;
96
97 async fn get_light_client_proof(
109 &self,
110 l1_height: u64,
111 paramset: &'static ProtocolParamset,
112 ) -> Result<Option<(LightClientProof, Receipt, u64)>, BridgeError>;
113
114 async fn get_citrea_l2_height_range(
128 &self,
129 block_height: u64,
130 timeout: Duration,
131 paramset: &'static ProtocolParamset,
132 ) -> Result<(u64, u64), BridgeError>;
133
134 async fn get_replacement_deposit_move_txids(
148 &self,
149 from_height: u64,
150 to_height: u64,
151 ) -> Result<Vec<(u32, Txid)>, BridgeError>;
152
153 async fn check_nofn_correctness(
154 &self,
155 nofn_xonly_pk: XOnlyPublicKey,
156 ) -> Result<(), BridgeError>;
157
158 async fn get_storage_proof(
159 &self,
160 l2_height: u64,
161 deposit_index: u32,
162 ) -> Result<StorageProof, BridgeError>;
163
164 async fn fetch_validate_and_store_lcp(
165 &self,
166 payout_block_height: u64,
167 deposit_index: u32,
168 db: &Database,
169 dbtx: Option<DatabaseTransaction<'_, '_>>,
170 paramset: &'static ProtocolParamset,
171 ) -> Result<Receipt, BridgeError>;
172}
173
174#[derive(Clone, Debug)]
177pub struct CitreaClient {
178 pub client: HttpClient,
179 pub light_client_prover_client: HttpClient,
180 pub wallet_address: alloy::primitives::Address,
181 pub contract: CitreaContract,
182}
183
184impl CitreaClient {
185 async fn get_logs(
188 &self,
189 filter: Filter,
190 from_height: u64,
191 to_height: u64,
192 ) -> Result<Vec<Log>, BridgeError> {
193 let mut logs = vec![];
194
195 let mut from_height = from_height;
196 while from_height <= to_height {
197 let to_height = std::cmp::min(from_height + 999, to_height);
199 tracing::debug!("Fetching logs from {} to {}", from_height, to_height);
200
201 let filter = filter.clone();
203 let filter = filter.from_block(BlockNumberOrTag::Number(from_height));
204 let filter = filter.to_block(BlockNumberOrTag::Number(to_height));
205
206 let logs_chunk = self
207 .contract
208 .provider()
209 .get_logs(&filter)
210 .await
211 .wrap_err("Failed to get logs")?;
212 logs.extend(logs_chunk);
213
214 from_height = to_height + 1;
215 }
216
217 Ok(logs)
218 }
219}
220
221#[async_trait]
222impl CitreaClientT for CitreaClient {
223 async fn get_storage_proof(
235 &self,
236 l2_height: u64,
237 deposit_index: u32,
238 ) -> Result<StorageProof, BridgeError> {
239 let ind = deposit_index;
240 let tx_index: u32 = ind * 2;
241
242 let storage_address_wd_utxo_bytes = keccak256(UTXOS_STORAGE_INDEX);
243 let storage_address_wd_utxo: U256 = U256::from_be_bytes(
244 <[u8; 32]>::try_from(&storage_address_wd_utxo_bytes[..])
245 .wrap_err("Storage address wd utxo bytes slice with incorrect length")?,
246 );
247
248 let storage_key_wd_utxo: U256 = storage_address_wd_utxo + U256::from(tx_index);
250 let storage_key_wd_utxo_hex =
251 format!("0x{}", hex::encode(storage_key_wd_utxo.to_be_bytes::<32>()));
252
253 let storage_key_vout: U256 = storage_address_wd_utxo + U256::from(tx_index + 1);
255 let storage_key_vout_hex =
256 format!("0x{}", hex::encode(storage_key_vout.to_be_bytes::<32>()));
257
258 let storage_address_deposit_bytes = keccak256(DEPOSIT_STORAGE_INDEX);
260 let storage_address_deposit: U256 = U256::from_be_bytes(
261 <[u8; 32]>::try_from(&storage_address_deposit_bytes[..])
262 .wrap_err("Storage address deposit bytes slice with incorrect length")?,
263 );
264
265 let storage_key_deposit: U256 = storage_address_deposit + U256::from(deposit_index);
266 let storage_key_deposit_hex = hex::encode(storage_key_deposit.to_be_bytes::<32>());
267 let storage_key_deposit_hex = format!("0x{storage_key_deposit_hex}");
268
269 let response: serde_json::Value = self
270 .client
271 .get_proof(
272 BRIDGE_CONTRACT_ADDRESS,
273 vec![
274 storage_key_wd_utxo_hex,
275 storage_key_vout_hex,
276 storage_key_deposit_hex,
277 ],
278 format!("0x{l2_height:x}"),
279 )
280 .await
281 .wrap_err("Failed to get storage proof from rpc")?;
282
283 let response: EIP1186AccountProofResponse = serde_json::from_value(response)
284 .wrap_err("Failed to deserialize EIP1186AccountProofResponse")?;
285
286 if response.storage_proof.len() < 3 {
289 return Err(eyre::eyre!(
290 "Expected at least 3 storage proofs, got {}",
291 response.storage_proof.len()
292 )
293 .into());
294 }
295
296 let serialized_utxo = serde_json::to_string(&response.storage_proof[0])
297 .wrap_err("Failed to serialize storage proof utxo")?;
298
299 let serialized_vout = serde_json::to_string(&response.storage_proof[1])
300 .wrap_err("Failed to serialize storage proof vout")?;
301
302 let serialized_deposit = serde_json::to_string(&response.storage_proof[2])
303 .wrap_err("Failed to serialize storage proof deposit")?;
304
305 Ok(StorageProof {
306 storage_proof_utxo: serialized_utxo,
307 storage_proof_vout: serialized_vout,
308 storage_proof_deposit_txid: serialized_deposit,
309 index: ind,
310 })
311 }
312
313 async fn fetch_validate_and_store_lcp(
314 &self,
315 payout_block_height: u64,
316 deposit_index: u32,
317 db: &Database,
318 mut dbtx: Option<DatabaseTransaction<'_, '_>>,
319 paramset: &'static ProtocolParamset,
320 ) -> Result<Receipt, BridgeError> {
321 let saved_data = db
322 .get_lcp_for_assert(dbtx.as_deref_mut(), deposit_index)
323 .await?;
324 if let Some(lcp) = saved_data {
325 return Ok(lcp);
327 };
328
329 let lcp_result = self
330 .get_light_client_proof(payout_block_height, paramset)
331 .await?;
332 let (_lcp, lcp_receipt, _l2_height) = match lcp_result {
333 Some(lcp) => lcp,
334 None => {
335 return Err(eyre::eyre!(
336 "Light client proof could not be fetched found for block height {}",
337 payout_block_height
338 )
339 .into())
340 }
341 };
342
343 db.insert_lcp_for_assert(dbtx, deposit_index, lcp_receipt.clone())
345 .await?;
346
347 Ok(lcp_receipt)
348 }
349
350 async fn new(
351 citrea_rpc_url: String,
352 light_client_prover_url: String,
353 chain_id: u32,
354 secret_key: Option<PrivateKeySigner>,
355 timeout: Option<Duration>,
356 ) -> Result<Self, BridgeError> {
357 let citrea_rpc_url = Url::parse(&citrea_rpc_url).wrap_err("Can't parse Citrea RPC URL")?;
358 let light_client_prover_url =
359 Url::parse(&light_client_prover_url).wrap_err("Can't parse Citrea LCP RPC URL")?;
360 let secret_key = secret_key.unwrap_or(PrivateKeySigner::random());
361
362 let key = secret_key.with_chain_id(Some(chain_id.into()));
363 let wallet_address = key.address();
364
365 tracing::info!("Wallet address: {}", wallet_address);
366
367 let provider = ProviderBuilder::new()
368 .wallet(EthereumWallet::from(key))
369 .on_http(citrea_rpc_url.clone());
370
371 tracing::info!("Provider created");
372
373 let contract = BRIDGE_CONTRACT::new(
374 BRIDGE_CONTRACT_ADDRESS
375 .parse()
376 .expect("Correct contract address"),
377 provider,
378 );
379
380 tracing::info!("Contract created");
381
382 let client = HttpClientBuilder::default()
383 .request_timeout(timeout.unwrap_or(Duration::from_secs(60)))
384 .build(citrea_rpc_url)
385 .wrap_err("Failed to create Citrea RPC client")?;
386
387 tracing::info!("Citrea RPC client created");
388
389 let light_client_prover_client = HttpClientBuilder::default()
390 .request_timeout(timeout.unwrap_or(Duration::from_secs(60)))
391 .build(light_client_prover_url)
392 .wrap_err("Failed to create Citrea LCP RPC client")?;
393
394 tracing::info!("Citrea LCP RPC client created");
395
396 Ok(CitreaClient {
397 client,
398 light_client_prover_client,
399 wallet_address,
400 contract,
401 })
402 }
403
404 async fn collect_deposit_move_txids(
405 &self,
406 last_deposit_idx: Option<u32>,
407 to_height: u64,
408 ) -> Result<Vec<(u64, Txid)>, BridgeError> {
409 let mut move_txids = vec![];
410
411 let mut start_idx = match last_deposit_idx {
412 Some(idx) => idx + 1,
413 None => 0,
414 };
415
416 loop {
417 let deposit_txid = self
418 .contract
419 .depositTxIds(U256::from(start_idx))
420 .block(BlockId::Number(BlockNumberOrTag::Number(to_height)))
421 .call()
422 .await;
423 match deposit_txid {
424 Err(e) if e.to_string().contains("execution reverted") => {
425 tracing::trace!("Deposit txid not found for index, error: {:?}", e);
426 break;
427 }
428 Err(e) => return Err(e.into()),
429 Ok(_) => {}
430 }
431 tracing::info!("Deposit txid found for index: {:?}", deposit_txid);
432
433 let deposit_txid = deposit_txid.expect("Failed to get deposit txid");
434 let move_txid = Txid::from_slice(deposit_txid._0.as_ref())
435 .wrap_err("Failed to convert move txid to Txid")?;
436 move_txids.push((start_idx as u64, move_txid));
437 start_idx += 1;
438 }
439 Ok(move_txids)
440 }
441
442 async fn collect_withdrawal_utxos(
443 &self,
444 last_withdrawal_idx: Option<u32>,
445 to_height: u64,
446 ) -> Result<Vec<(u64, OutPoint)>, BridgeError> {
447 let mut utxos = vec![];
448
449 let mut start_idx = match last_withdrawal_idx {
450 Some(idx) => idx + 1,
451 None => 0,
452 };
453
454 loop {
455 let withdrawal_utxo = self
456 .contract
457 .withdrawalUTXOs(U256::from(start_idx))
458 .block(BlockId::Number(BlockNumberOrTag::Number(to_height)))
459 .call()
460 .await;
461 match withdrawal_utxo {
462 Err(e) if e.to_string().contains("execution reverted") => {
463 tracing::trace!("Withdrawal utxo not found for index, error: {:?}", e);
464 break;
465 }
466 Err(e) => return Err(e.into()),
467 Ok(_) => {}
468 }
469 let withdrawal_utxo = withdrawal_utxo.expect("Failed to get withdrawal UTXO");
470 let txid = withdrawal_utxo.txId.0;
471 let txid =
472 Txid::from_slice(txid.as_ref()).wrap_err("Failed to convert txid to Txid")?;
473 let vout = withdrawal_utxo.outputId.0;
474 let vout = u32::from_le_bytes(vout);
475 let utxo = OutPoint { txid, vout };
476 utxos.push((start_idx as u64, utxo));
477 start_idx += 1;
478 }
479 Ok(utxos)
480 }
481
482 async fn get_light_client_proof(
483 &self,
484 l1_height: u64,
485 paramset: &'static ProtocolParamset,
486 ) -> Result<Option<(LightClientProof, Receipt, u64)>, BridgeError> {
487 let proof_result = self
488 .light_client_prover_client
489 .get_light_client_proof_by_l1_height(l1_height)
490 .await
491 .wrap_err("Failed to get light client proof")?;
492 tracing::debug!(
493 "Light client proof result {}: {:?}",
494 l1_height,
495 proof_result
496 );
497
498 let ret = if let Some(proof_result) = proof_result {
499 let decoded: InnerReceipt = bincode::deserialize(&proof_result.proof)
500 .wrap_err("Failed to deserialize light client proof from citrea lcp")?;
501 let receipt = receipt_from_inner(decoded)
502 .wrap_err("Failed to create receipt from light client proof")?;
503
504 let l2_height = u64::try_from(proof_result.light_client_proof_output.last_l2_height)
505 .wrap_err("Failed to convert l2 height to u64")?;
506
507 let lc_image_id = paramset.get_lcp_image_id()?;
508
509 let proof_output: LightClientCircuitOutput = borsh::from_slice(&receipt.journal.bytes)
510 .wrap_err("Failed to deserialize light client circuit output")?;
511
512 if !paramset.is_regtest() {
513 receipt
514 .verify(lc_image_id)
515 .map_err(|_| eyre::eyre!("Light client proof verification failed"))?;
516
517 if !check_method_id(&proof_output, lc_image_id) {
518 return Err(eyre::eyre!(
519 "Current light client proof method ID does not match the expected LC image ID"
520 )
521 .into());
522 }
523 }
524
525 Some((
526 LightClientProof {
527 lc_journal: receipt.journal.bytes.clone(),
528 },
529 receipt,
530 l2_height,
531 ))
532 } else {
533 None
534 };
535
536 Ok(ret)
537 }
538
539 async fn get_citrea_l2_height_range(
540 &self,
541 block_height: u64,
542 timeout: Duration,
543 paramset: &'static ProtocolParamset,
544 ) -> Result<(u64, u64), BridgeError> {
545 let start = std::time::Instant::now();
546 let proof_current = loop {
547 if let Some(proof) = self.get_light_client_proof(block_height, paramset).await? {
548 break proof;
549 }
550
551 if start.elapsed() > timeout {
552 return Err(eyre::eyre!(
553 "Light client proof not found for block height {} after {} seconds",
554 block_height,
555 timeout.as_secs()
556 )
557 .into());
558 }
559
560 tokio::time::sleep(Duration::from_secs(1)).await;
561 };
562
563 let proof_previous = self
564 .get_light_client_proof(block_height - 1, paramset)
565 .await?
566 .ok_or(eyre::eyre!(
567 "Light client proof not found for block height: {}",
568 block_height - 1
569 ))?;
570
571 let l2_height_end: u64 = proof_current.2;
572 let l2_height_start: u64 = proof_previous.2;
573
574 Ok((l2_height_start, l2_height_end))
575 }
576
577 async fn get_replacement_deposit_move_txids(
578 &self,
579 from_height: u64,
580 to_height: u64,
581 ) -> Result<Vec<(u32, Txid)>, BridgeError> {
582 let mut replacement_move_txids = vec![];
583
584 let filter = self.contract.event_filter::<DepositReplaced>().filter;
586 let logs = self.get_logs(filter, from_height, to_height).await?;
587
588 for log in logs {
589 let replacement_raw_data = &log.data().data;
590
591 let idx = DepositReplaced::abi_decode_data(replacement_raw_data, false)
592 .wrap_err("Failed to decode replacement deposit data")?
593 .0;
594 let new_move_txid = DepositReplaced::abi_decode_data(replacement_raw_data, false)
595 .wrap_err("Failed to decode replacement deposit data")?
596 .2;
597
598 let idx = u32::try_from(idx).wrap_err("Failed to convert idx to u32")?;
599 let new_move_txid = Txid::from_slice(new_move_txid.as_ref())
600 .wrap_err("Failed to convert new move txid to Txid")?;
601
602 replacement_move_txids.push((idx, new_move_txid));
603 }
604
605 Ok(replacement_move_txids)
606 }
607
608 async fn check_nofn_correctness(
609 &self,
610 nofn_xonly_pk: XOnlyPublicKey,
611 ) -> Result<(), BridgeError> {
612 if std::env::var("DISABLE_NOFN_CHECK").is_ok() {
613 return Ok(());
614 }
615
616 let contract_nofn_xonly_pk = self
617 .contract
618 .getAggregatedKey()
619 .call()
620 .await
621 .wrap_err("Failed to get script prefix")?
622 ._0;
623
624 let contract_nofn_xonly_pk = XOnlyPublicKey::from_slice(contract_nofn_xonly_pk.as_ref())
625 .wrap_err("Failed to convert citrea contract script nofn bytes to xonly pk")?;
626 if contract_nofn_xonly_pk != nofn_xonly_pk {
627 return Err(eyre::eyre!("Nofn of deposit does not match with citrea contract").into());
628 }
629 Ok(())
630 }
631}
632
633#[rpc(client, namespace = "lightClientProver")]
634trait LightClientProverRpc {
635 #[method(name = "getLightClientProofByL1Height")]
637 async fn get_light_client_proof_by_l1_height(
638 &self,
639 l1_height: u64,
640 ) -> RpcResult<Option<citrea_sov_rollup_interface::rpc::LightClientProofResponse>>;
641}
642
643#[rpc(client, namespace = "eth")]
644pub trait CitreaRpc {
645 #[method(name = "getProof")]
646 async fn get_proof(
647 &self,
648 address: &str,
649 storage_keys: Vec<String>,
650 block: String,
651 ) -> RpcResult<serde_json::Value>;
652}
653
654type CitreaContract = BRIDGE_CONTRACT::BRIDGE_CONTRACTInstance<
656 (),
657 FillProvider<
658 JoinFill<
659 JoinFill<
660 alloy::providers::Identity,
661 JoinFill<GasFiller, JoinFill<BlobGasFiller, JoinFill<NonceFiller, ChainIdFiller>>>,
662 >,
663 WalletFiller<EthereumWallet>,
664 >,
665 RootProvider,
666 >,
667>;