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