diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4bac99322e..c7a6f2f29e 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -518,7 +518,7 @@ jobs: proving-stats: name: Proving stats - runs-on: ubicloud-standard-2 + runs-on: ubicloud-standard-30 needs: [build, docker-setup] if: github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'guest-code') steps: diff --git a/Cargo.lock b/Cargo.lock index c49891b643..6f1ceed45b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3613,6 +3613,7 @@ dependencies = [ "metrics-util", "prover-services", "rand 0.8.5", + "recovered-pubkey-provider", "regex", "reqwest", "reth-primitives", @@ -3658,6 +3659,7 @@ dependencies = [ "bincode", "borsh", "citrea-common", + "citrea-evm", "citrea-primitives", "citrea-stf", "faster-hex", @@ -3670,6 +3672,7 @@ dependencies = [ "prover-services", "rand 0.8.5", "rayon", + "recovered-pubkey-provider", "reth-tasks", "risc0-zkvm", "rs_merkle", @@ -3809,6 +3812,7 @@ dependencies = [ "metrics-derive", "rand 0.8.5", "rayon", + "recovered-pubkey-provider", "reth-chainspec", "reth-db", "reth-errors", @@ -4040,6 +4044,7 @@ dependencies = [ "citrea-stf", "jsonrpsee", "l2-block-rule-enforcer", + "recovered-pubkey-provider", "rs_merkle", "serde", "serde_json", @@ -8918,6 +8923,14 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "recovered-pubkey-provider" +version = "1.1.0" +dependencies = [ + "parking_lot", + "thiserror 2.0.12", +] + [[package]] name = "redox_syscall" version = "0.5.9" diff --git a/Cargo.toml b/Cargo.toml index 51751eed4a..aea4275033 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "crates/risc0", "crates/sequencer", "crates/short-header-proof-provider", + "crates/recovered-pubkey-provider", "crates/l2-block-rule-enforcer", # "crates/sp1", # Sovereign sdk diff --git a/bin/citrea/Cargo.toml b/bin/citrea/Cargo.toml index 0568ff8694..b124ae721e 100644 --- a/bin/citrea/Cargo.toml +++ b/bin/citrea/Cargo.toml @@ -28,6 +28,7 @@ citrea-stf = { path = "../../crates/citrea-stf", features = ["native"] } citrea-storage-ops = { path = "../../crates/storage-ops" } ethereum-rpc = { path = "../../crates/ethereum-rpc" } prover-services = { path = "../../crates/prover-services" } +recovered-pubkey-provider = { path = "../../crates/recovered-pubkey-provider", features = ["native"] } short-header-proof-provider = { path = "../../crates/short-header-proof-provider", features = ["native"] } # Sovereign-SDK deps diff --git a/bin/citrea/src/main.rs b/bin/citrea/src/main.rs index cc9f2cdc79..a6a110c4c2 100644 --- a/bin/citrea/src/main.rs +++ b/bin/citrea/src/main.rs @@ -19,6 +19,7 @@ use citrea_stf::genesis_config::GenesisPaths; use citrea_stf::runtime::{CitreaRuntime, DefaultContext}; use clap::Parser; use metrics_exporter_prometheus::PrometheusBuilder; +use recovered_pubkey_provider::{RecoveredPubkeyProvider, RECOVERED_PUBKEY_PROVIDER}; use reth_tasks::TaskManager; use short_header_proof_provider::{ NativeShortHeaderProofProviderService, SHORT_HEADER_PROOF_PROVIDER, @@ -229,6 +230,11 @@ where Err(_) => tracing::error!("Short header proof provider already set"), } + match RECOVERED_PUBKEY_PROVIDER.set(RecoveredPubkeyProvider::new()) { + Ok(_) => {} + Err(_) => panic!("recovered pubkey provider already initialized"), + } + let rpc_storage = storage_manager.create_final_view_storage(); let mut rpc_module = rollup_blueprint.create_rpc_methods( (&node_type).into(), diff --git a/bin/citrea/tests/common/helpers.rs b/bin/citrea/tests/common/helpers.rs index dbc53e84d5..fecaf6fcb4 100644 --- a/bin/citrea/tests/common/helpers.rs +++ b/bin/citrea/tests/common/helpers.rs @@ -16,6 +16,7 @@ use citrea_common::{ use citrea_light_client_prover::da_block_handler::StartVariant; use citrea_primitives::TEST_PRIVATE_KEY; use citrea_stf::genesis_config::GenesisPaths; +use recovered_pubkey_provider::{RecoveredPubkeyProvider, RECOVERED_PUBKEY_PROVIDER}; use reth_tasks::TaskManager; use short_header_proof_provider::{ NativeShortHeaderProofProviderService, SHORT_HEADER_PROOF_PROVIDER, @@ -182,6 +183,11 @@ pub async fn start_rollup( Err(_) => tracing::error!("Short header proof provider already set"), } + match RECOVERED_PUBKEY_PROVIDER.set(RecoveredPubkeyProvider::new()) { + Ok(_) => tracing::debug!("ecrecover address provider set"), + Err(_) => tracing::error!("ecrecover address provider already set"), + } + let task_executor = task_manager.executor(); // I am sorry diff --git a/crates/batch-prover/Cargo.toml b/crates/batch-prover/Cargo.toml index cc57e58962..2cd02ea4d7 100644 --- a/crates/batch-prover/Cargo.toml +++ b/crates/batch-prover/Cargo.toml @@ -11,9 +11,11 @@ repository.workspace = true [dependencies] # Citrea Deps citrea-common = { path = "../common" } +citrea-evm = { path = "../evm" } citrea-primitives = { path = "../primitives" } citrea-stf = { path = "../citrea-stf" } prover-services = { path = "../prover-services" } +recovered-pubkey-provider = { path = "../recovered-pubkey-provider", features = ["native"] } short-header-proof-provider = { path = "../short-header-proof-provider", features = ["native"] } # Sov SDK deps sov-db = { path = "../sovereign-sdk/full-node/db/sov-db" } diff --git a/crates/batch-prover/src/prover.rs b/crates/batch-prover/src/prover.rs index 0c57eadfda..d0315b754f 100644 --- a/crates/batch-prover/src/prover.rs +++ b/crates/batch-prover/src/prover.rs @@ -17,6 +17,7 @@ use futures::stream::FuturesUnordered; use futures::StreamExt; use prover_services::{ParallelProverService, ProofData, ProofWithDuration}; use rand::Rng; +use recovered_pubkey_provider::{Secp256k1Pubkey, RECOVERED_PUBKEY_PROVIDER}; use reth_tasks::shutdown::GracefulShutdown; use rs_merkle::algorithms::Sha256; use rs_merkle::MerkleTree; @@ -593,6 +594,7 @@ where cache_prune_l2_heights, committed_l2_blocks, last_l1_hash_witness, + recovered_pubkeys, } = get_batch_proof_circuit_input_from_commitments::( partition.start_height, partition.commitments, @@ -633,6 +635,7 @@ where last_l1_hash_witness, previous_sequencer_commitment, prev_hash_proof, + recovered_pubkeys, }) } @@ -900,6 +903,8 @@ pub(crate) struct CommitmentStateTransitionData { committed_l2_blocks: VecDeque>, /// Witness needed to get the last Bitcoin hash on Bitcoin Light Client contract last_l1_hash_witness: Witness, + /// Pre-computed ecrecovered pubkeys + recovered_pubkeys: VecDeque>, } /// This function retrieves the batch proof circuit input from the sequencer commitments @@ -984,6 +989,7 @@ pub(crate) fn get_batch_proof_circuit_input_from_commitments< cache_prune_l2_heights, short_header_proofs, last_l1_hash_witness, + recovered_pubkeys, ) = generate_cumulative_witness::( &committed_l2_blocks, ledger_db, @@ -1005,6 +1011,7 @@ pub(crate) fn get_batch_proof_circuit_input_from_commitments< cache_prune_l2_heights, committed_l2_blocks, last_l1_hash_witness, + recovered_pubkeys, }) } @@ -1038,7 +1045,8 @@ fn generate_cumulative_witness( VecDeque>, Vec, VecDeque>, - Witness, // last hash witness + Witness, // last hash witness + VecDeque>, // recovered pubkeys per commitment )> { let mut short_header_proofs: VecDeque> = VecDeque::new(); @@ -1062,6 +1070,8 @@ fn generate_cumulative_witness( .expect("must have at least one l2 block") .height(); + let mut all_recovered_pubkeys = VecDeque::new(); + for l2_blocks_in_commitment in committed_l2_blocks { let mut witnesses = Vec::with_capacity(l2_blocks_in_commitment.len()); @@ -1147,6 +1157,11 @@ fn generate_cumulative_witness( short_header_proofs.push_back(serialized_shp); } + // Extract recoverdd pubkeys for this commitment. + // These pubkeys were collected during transaction recovery in recover_raw_transaction() + let pubkeys = RECOVERED_PUBKEY_PROVIDER.get().unwrap().take_pubkeys()?; + + all_recovered_pubkeys.push_back(pubkeys); state_transition_witnesses.push_back(witnesses); } @@ -1172,6 +1187,7 @@ fn generate_cumulative_witness( cache_prune_l2_heights, short_header_proofs, last_l1_hash_witness, + all_recovered_pubkeys, )) } diff --git a/crates/citrea-stf/Cargo.toml b/crates/citrea-stf/Cargo.toml index 2f32aeb148..015bc15517 100644 --- a/crates/citrea-stf/Cargo.toml +++ b/crates/citrea-stf/Cargo.toml @@ -31,6 +31,7 @@ sov-state = { path = "../sovereign-sdk/module-system/sov-state" } citrea-evm = { path = "../evm" } l2-block-rule-enforcer = { path = "../l2-block-rule-enforcer" } +recovered-pubkey-provider = { path = "../recovered-pubkey-provider", default-features = false } short-header-proof-provider = { path = "../short-header-proof-provider" } [dev-dependencies] @@ -50,6 +51,7 @@ sov-state = { path = "../sovereign-sdk/module-system/sov-state", features = ["na default = [] native = [ "short-header-proof-provider/native", + "recovered-pubkey-provider/native", "dep:sov-db", "sov-accounts/native", "sov-modules-api/native", diff --git a/crates/citrea-stf/src/verifier.rs b/crates/citrea-stf/src/verifier.rs index 57b43470c3..9bc61e269d 100644 --- a/crates/citrea-stf/src/verifier.rs +++ b/crates/citrea-stf/src/verifier.rs @@ -60,6 +60,23 @@ where panic!("Short header proof provider already set"); } + // Initialize pubkey provider with pre-computed pubkeys from input + #[cfg(not(feature = "native"))] + { + let mut flat_pubkeys = std::vec::Vec::new(); + for commitment_addresses in data.recovered_pubkeys { + flat_pubkeys.extend(commitment_addresses); + } + let recovered_pubkey_provider = + recovered_pubkey_provider::RecoveredPubkeyProvider::new(flat_pubkeys); + if recovered_pubkey_provider::RECOVERED_PUBKEY_PROVIDER + .set(recovered_pubkey_provider) + .is_err() + { + panic!("Recovered pubkey provider already set"); + } + } + println!("going into apply_l2_blocks_from_sequencer_commitments"); let ApplySequencerCommitmentsOutput { diff --git a/crates/evm/Cargo.toml b/crates/evm/Cargo.toml index b574387010..b7ab6721ac 100644 --- a/crates/evm/Cargo.toml +++ b/crates/evm/Cargo.toml @@ -12,6 +12,7 @@ publish = false readme = "README.md" [dependencies] +recovered-pubkey-provider = { path = "../recovered-pubkey-provider", default-features = false } short-header-proof-provider = { path = "../short-header-proof-provider", default-features = false } sov-db = { path = "../sovereign-sdk/full-node/db/sov-db", optional = true } sov-modules-api = { path = "../sovereign-sdk/module-system/sov-modules-api", default-features = false, features = ["macros"] } diff --git a/crates/evm/src/call.rs b/crates/evm/src/call.rs index 617f19dd51..a61ec23007 100644 --- a/crates/evm/src/call.rs +++ b/crates/evm/src/call.rs @@ -43,7 +43,7 @@ impl Evm { let users_txs: Vec> = txs .into_iter() - .map(|tx| tx.try_into()) + .map(crate::evm::conversions::recover_raw_transaction) .collect::, ConversionError>>() .map_err(|_| L2BlockModuleCallError::EvmTxNotSerializable)?; diff --git a/crates/evm/src/evm/conversions.rs b/crates/evm/src/evm/conversions.rs index c66bfa8545..b769cf0963 100644 --- a/crates/evm/src/evm/conversions.rs +++ b/crates/evm/src/evm/conversions.rs @@ -1,9 +1,10 @@ use alloy_consensus::constants::KECCAK_EMPTY; -use alloy_consensus::Transaction; +use alloy_consensus::{SignableTransaction, Transaction}; use alloy_eips::eip2718::Decodable2718; use alloy_primitives::Bytes as RethBytes; #[cfg(feature = "native")] use alloy_primitives::U256; +use recovered_pubkey_provider::RECOVERED_PUBKEY_PROVIDER; use reth_primitives::{Recovered, TransactionSigned}; use reth_primitives_traits::SignedTransaction; use revm::context::{TransactTo, TxEnv}; @@ -110,11 +111,97 @@ impl TryFrom for Recovered { if tx.signature() == &SYSTEM_SIGNATURE { return Ok(Self::new_unchecked(tx, SYSTEM_SIGNER)); } + tx.try_into_recovered() .map_err(|_| ConversionError::InvalidSignature) } } +/// Convert RlpEvmTransaction to Recovered. +/// +/// This function implements the ecrecover optimization pattern: +/// - On native: Performs actual ecrecover and records the pubkey to be added to input +/// - In non native: Uses pre-computed pubkeys from input to verify and derive address +/// +/// Pubkeys are recorded/consumed in deterministic order (block by block, tx by tx). +pub fn recover_raw_transaction( + evm_tx: RlpEvmTransaction, +) -> Result, ConversionError> { + let tx = TransactionSigned::try_from(evm_tx)?; + if tx.signature() == &SYSTEM_SIGNATURE { + return Ok(Recovered::new_unchecked(tx, SYSTEM_SIGNER)); + } + + #[cfg(not(feature = "native"))] + { + use alloy_primitives::{keccak256, Address}; + use k256::ecdsa::signature::hazmat::PrehashVerifier; + use k256::ecdsa::VerifyingKey; + use k256::elliptic_curve::sec1::ToEncodedPoint; + + // Use pre-computed pubkey from provider + let pubkey_bytes = RECOVERED_PUBKEY_PROVIDER + .get() + .expect("Ecrecover pubkey provider not initialized") + .get_next() + .expect("Missing ecrecover pubkey in witness"); + + let verifying_key = VerifyingKey::from_sec1_bytes(&pubkey_bytes) + .map_err(|_| ConversionError::InvalidSignature)?; + + let sig = *tx.signature(); + let prehash = tx.signature_hash(); + + let normalized_sig = sig.normalized_s(); + let k256_sig = normalized_sig + .to_k256() + .map_err(|_| ConversionError::InvalidSignature)?; + + verifying_key + .verify_prehash(prehash.as_slice(), &k256_sig) + .map_err(|_| ConversionError::InvalidSignature)?; + + // Compute address from pubkey + let affine = verifying_key.as_ref(); + let encoded = affine.to_encoded_point(false); + let digest = keccak256(&encoded.as_bytes()[1..]); + let address = Address::from_slice(&digest[12..]); + + return Ok(Recovered::new_unchecked(tx, Address::from(address))); + } + + #[cfg(feature = "native")] + { + let sig = *tx.signature(); + let prehash = *tx.signature_hash(); + + let recovered = tx + .try_into_recovered() + .map_err(|_| ConversionError::InvalidSignature)?; + + let normalized_sig = sig.normalized_s(); + let verifying_key = k256::ecdsa::VerifyingKey::recover_from_prehash( + prehash.as_slice(), + &normalized_sig.to_k256().unwrap(), + normalized_sig.recid(), + ) + .map_err(|_| ConversionError::InvalidSignature)?; + + let encoded = verifying_key.to_encoded_point(false).as_bytes().to_vec(); + + let pubkey: [u8; 65] = encoded + .try_into() + .expect("secp256k1 uncompressed pubkey must be 65 bytes"); + + // Record the pubkey + if let Some(provider) = RECOVERED_PUBKEY_PROVIDER.get() { + provider.record(pubkey); + } + + Ok(recovered) + } +} + impl From for Recovered { fn from(value: TransactionSignedAndRecovered) -> Self { Self::new_unchecked(value.signed_transaction, value.signer) diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 2a2c381ef1..839b7b86cf 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -1,7 +1,8 @@ #![deny(missing_docs)] #![doc = include_str!("../README.md")] mod call; -mod evm; +/// EVM handler and transaction processing +pub mod evm; mod genesis; mod hooks; #[cfg(feature = "native")] diff --git a/crates/recovered-pubkey-provider/Cargo.toml b/crates/recovered-pubkey-provider/Cargo.toml new file mode 100644 index 0000000000..ae14ad6866 --- /dev/null +++ b/crates/recovered-pubkey-provider/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "recovered-pubkey-provider" +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +version = { workspace = true } +publish = false +resolver = "2" + +[dependencies] +parking_lot = { workspace = true, optional = true } +thiserror = { workspace = true } + +[features] +native = [ + "dep:parking_lot", +] diff --git a/crates/recovered-pubkey-provider/src/lib.rs b/crates/recovered-pubkey-provider/src/lib.rs new file mode 100644 index 0000000000..253898c836 --- /dev/null +++ b/crates/recovered-pubkey-provider/src/lib.rs @@ -0,0 +1,24 @@ +//! Ecrecover Address Provider +use std::sync::OnceLock; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum EcrecoverProviderError { + #[error("No more addresses available")] + NoMoreAddresses, +} + +#[cfg(feature = "native")] +mod native; +#[cfg(feature = "native")] +pub use native::RecoveredPubkeyProvider; + +#[cfg(not(feature = "native"))] +mod zk; +#[cfg(not(feature = "native"))] +pub use zk::RecoveredPubkeyProvider; + +pub type Secp256k1Pubkey = [u8; 65]; + +pub static RECOVERED_PUBKEY_PROVIDER: OnceLock = OnceLock::new(); diff --git a/crates/recovered-pubkey-provider/src/native.rs b/crates/recovered-pubkey-provider/src/native.rs new file mode 100644 index 0000000000..5dfdfd4951 --- /dev/null +++ b/crates/recovered-pubkey-provider/src/native.rs @@ -0,0 +1,42 @@ +use parking_lot::Mutex; + +use super::EcrecoverProviderError; +use crate::Secp256k1Pubkey; + +/// Collects ecrecover pubkeys during native execution in deterministic order. +/// +/// Addresses are recorded during transaction recovery and later extracted to be +/// included inside the input +pub struct RecoveredPubkeyProvider { + pubkeys: Mutex>, +} + +impl RecoveredPubkeyProvider { + pub fn new() -> Self { + Self { + pubkeys: Mutex::new(Vec::new()), + } + } + + /// Record a recovered pubkey bytes in deterministic order + pub fn record(&self, pubkey_bytes: Secp256k1Pubkey) { + self.pubkeys.lock().push(pubkey_bytes); + } + + /// Clear all recorded pubkeys + pub fn clear(&self) { + self.pubkeys.lock().clear(); + } + + /// Take all recorded pubkeys and clear the internal buffer + pub fn take_pubkeys(&self) -> Result, EcrecoverProviderError> { + let mut vec = self.pubkeys.lock(); + Ok(std::mem::take(&mut *vec)) + } +} + +impl Default for RecoveredPubkeyProvider { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/recovered-pubkey-provider/src/zk.rs b/crates/recovered-pubkey-provider/src/zk.rs new file mode 100644 index 0000000000..7f5fc9e71a --- /dev/null +++ b/crates/recovered-pubkey-provider/src/zk.rs @@ -0,0 +1,29 @@ +use std::cell::RefCell; + +use super::EcrecoverProviderError; +use crate::Secp256k1Pubkey; + +/// Provides pre-computed pubkeys zk context +pub struct RecoveredPubkeyProvider { + pubkeys: RefCell>, +} + +impl RecoveredPubkeyProvider { + pub fn new(pubkeys: Vec) -> Self { + Self { + pubkeys: RefCell::new(pubkeys.into_iter()), + } + } + + /// Get the next address + pub fn get_next(&self) -> Result { + self.pubkeys + .borrow_mut() + .next() + .ok_or(EcrecoverProviderError::NoMoreAddresses) + } +} + +// This is safe to do because zk environment is single-threaded +unsafe impl Send for RecoveredPubkeyProvider {} +unsafe impl Sync for RecoveredPubkeyProvider {} diff --git a/crates/sovereign-sdk/rollup-interface/src/state_machine/zk/batch_proof/input/v3.rs b/crates/sovereign-sdk/rollup-interface/src/state_machine/zk/batch_proof/input/v3.rs index 9abacfa10d..f2e99ad72e 100644 --- a/crates/sovereign-sdk/rollup-interface/src/state_machine/zk/batch_proof/input/v3.rs +++ b/crates/sovereign-sdk/rollup-interface/src/state_machine/zk/batch_proof/input/v3.rs @@ -40,6 +40,8 @@ pub struct BatchProofCircuitInputV3Part1 { pub cache_prune_l2_heights: Vec, /// The witness needed to access the last L1 hash on the bitcoin light client contract pub last_l1_hash_witness: Witness, + /// Pre-computed recovered pubkeys + pub recovered_pubkeys: VecDeque>, } #[derive(BorshDeserialize, BorshSerialize)] @@ -73,6 +75,8 @@ pub struct BatchProofCircuitInputV3 { /// To verify the first `prev_hash`, we need a merkle proof for the last header in the previous /// sequencer commitment. pub prev_hash_proof: Option, + /// Pre-computed recovered pubkeys + pub recovered_pubkeys: VecDeque>, } impl BatchProofCircuitInputV3 { @@ -108,6 +112,7 @@ impl BatchProofCircuitInputV3 { last_l1_hash_witness: self.last_l1_hash_witness, previous_sequencer_commitment: self.previous_sequencer_commitment, prev_hash_proof: self.prev_hash_proof, + recovered_pubkeys: self.recovered_pubkeys, }, BatchProofCircuitInputV3Part2(x), ) diff --git a/guests/risc0/batch-proof/bitcoin/Cargo.lock b/guests/risc0/batch-proof/bitcoin/Cargo.lock index e0c3b7b9b1..f7630d3ddc 100644 --- a/guests/risc0/batch-proof/bitcoin/Cargo.lock +++ b/guests/risc0/batch-proof/bitcoin/Cargo.lock @@ -1209,6 +1209,7 @@ dependencies = [ "borsh", "citrea-primitives", "k256", + "recovered-pubkey-provider", "reth-primitives", "reth-primitives-traits", "revm", @@ -1254,6 +1255,7 @@ dependencies = [ "borsh", "citrea-evm", "l2-block-rule-enforcer", + "recovered-pubkey-provider", "serde", "short-header-proof-provider", "sov-accounts", @@ -2798,6 +2800,13 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "recovered-pubkey-provider" +version = "1.1.0" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "regex-syntax" version = "0.8.5" diff --git a/guests/risc0/batch-proof/mock/Cargo.lock b/guests/risc0/batch-proof/mock/Cargo.lock index 366ad6361e..cad401763c 100644 --- a/guests/risc0/batch-proof/mock/Cargo.lock +++ b/guests/risc0/batch-proof/mock/Cargo.lock @@ -1148,6 +1148,7 @@ dependencies = [ "borsh", "citrea-primitives", "k256", + "recovered-pubkey-provider", "reth-primitives", "reth-primitives-traits", "revm", @@ -1193,6 +1194,7 @@ dependencies = [ "borsh", "citrea-evm", "l2-block-rule-enforcer", + "recovered-pubkey-provider", "serde", "short-header-proof-provider", "sov-accounts", @@ -2731,6 +2733,13 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "recovered-pubkey-provider" +version = "1.1.0" +dependencies = [ + "thiserror 2.0.12", +] + [[package]] name = "regex-syntax" version = "0.8.5"