Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bin/citrea/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions bin/citrea/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 6 additions & 0 deletions bin/citrea/tests/common/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions crates/batch-prover/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
18 changes: 17 additions & 1 deletion crates/batch-prover/src/prover.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::<Da, _>(
partition.start_height,
partition.commitments,
Expand Down Expand Up @@ -633,6 +635,7 @@ where
last_l1_hash_witness,
previous_sequencer_commitment,
prev_hash_proof,
recovered_pubkeys,
})
}

Expand Down Expand Up @@ -900,6 +903,8 @@ pub(crate) struct CommitmentStateTransitionData {
committed_l2_blocks: VecDeque<Vec<L2Block>>,
/// 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<Vec<Secp256k1Pubkey>>,
}

/// This function retrieves the batch proof circuit input from the sequencer commitments
Expand Down Expand Up @@ -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::<Da, _>(
&committed_l2_blocks,
ledger_db,
Expand All @@ -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,
})
}

Expand Down Expand Up @@ -1038,7 +1045,8 @@ fn generate_cumulative_witness<Da: DaService, DB: BatchProverLedgerOps>(
VecDeque<Vec<(Witness, Witness)>>,
Vec<u64>,
VecDeque<Vec<u8>>,
Witness, // last hash witness
Witness, // last hash witness
VecDeque<Vec<Secp256k1Pubkey>>, // recovered pubkeys per commitment
)> {
let mut short_header_proofs: VecDeque<Vec<u8>> = VecDeque::new();

Expand All @@ -1062,6 +1070,8 @@ fn generate_cumulative_witness<Da: DaService, DB: BatchProverLedgerOps>(
.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());

Expand Down Expand Up @@ -1147,6 +1157,11 @@ fn generate_cumulative_witness<Da: DaService, DB: BatchProverLedgerOps>(
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);
}

Expand All @@ -1172,6 +1187,7 @@ fn generate_cumulative_witness<Da: DaService, DB: BatchProverLedgerOps>(
cache_prune_l2_heights,
short_header_proofs,
last_l1_hash_witness,
all_recovered_pubkeys,
))
}

Expand Down
2 changes: 2 additions & 0 deletions crates/citrea-stf/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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",
Expand Down
17 changes: 17 additions & 0 deletions crates/citrea-stf/src/verifier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/evm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
2 changes: 1 addition & 1 deletion crates/evm/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ impl<C: sov_modules_api::Context> Evm<C> {

let users_txs: Vec<Recovered<TransactionSigned>> = txs
.into_iter()
.map(|tx| tx.try_into())
.map(crate::evm::conversions::recover_raw_transaction)
.collect::<Result<Vec<_>, ConversionError>>()
.map_err(|_| L2BlockModuleCallError::EvmTxNotSerializable)?;

Expand Down
89 changes: 88 additions & 1 deletion crates/evm/src/evm/conversions.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -110,11 +111,97 @@ impl TryFrom<RlpEvmTransaction> for Recovered<TransactionSigned> {
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<TransactionSigned>.
///
/// 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<Recovered<TransactionSigned>, 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<TransactionSignedAndRecovered> for Recovered<TransactionSigned> {
fn from(value: TransactionSignedAndRecovered) -> Self {
Self::new_unchecked(value.signed_transaction, value.signer)
Expand Down
3 changes: 2 additions & 1 deletion crates/evm/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand Down
Loading
Loading