diff --git a/crates/kona/derive/src/batch_auth.rs b/crates/kona/derive/src/batch_auth.rs index 85efb1eb..7d5428ec 100644 --- a/crates/kona/derive/src/batch_auth.rs +++ b/crates/kona/derive/src/batch_auth.rs @@ -11,16 +11,20 @@ //! on the L1 origin time of the block being scanned — mirroring the upstream `ecotoneTime` //! precedent. //! -//! - **Pre-Espresso (or Espresso unset):** the pipeline runs vanilla OP Stack semantics. A batch is -//! authorized iff its sender matches `batcher_address`. The `BatchAuthenticator` event lookback -//! is bypassed entirely. -//! - **Post-Espresso:** a batch is authorized iff its commitment hash was authenticated by a -//! `BatchInfoAuthenticated(bytes32 commitment, address indexed caller)` event emitted by the -//! configured `BatchAuthenticator` contract within the lookback window AND the batch -//! transaction's recovered L1 sender equals the `caller` that emitted that event. This -//! caller-binding prevents one batcher from replaying a batch authenticated by another. On -//! duplicate authentication of the same commitment within the lookback window, the newest event's -//! caller wins. +//! Enforcement is delayed by a grace period after activation +//! ([`celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS`] — see its docs for the rationale and +//! sizing), giving two regimes by L1 origin time: +//! +//! - **Before enforcement (pre-fork, or within the grace window):** the pipeline runs vanilla OP +//! Stack semantics. A batch is authorized iff its sender matches `batcher_address`. The +//! `BatchAuthenticator` event lookback is bypassed entirely. +//! - **Enforced (origin time `>= espresso_time + BATCH_AUTH_ENFORCEMENT_DELAY_SECS`):** a batch is +//! authorized iff its commitment hash was authenticated by a `BatchInfoAuthenticated(bytes32 +//! commitment, address indexed caller)` event emitted by the configured `BatchAuthenticator` +//! contract within the lookback window AND the batch transaction's recovered L1 sender equals the +//! `caller` that emitted that event. This caller-binding prevents one batcher from replaying a +//! batch authenticated by another. On duplicate authentication of the same commitment within the +//! lookback window, the newest event's caller wins. Sender-based fallback is rejected. //! //! The authorization semantics must stay in lockstep with the op-node verifier (the Go batcher //! emits the `BatchInfoAuthenticated` events that this module consumes). @@ -32,7 +36,7 @@ use alloc::{collections::BTreeMap, vec::Vec}; use alloy_consensus::{Receipt, TxEnvelope, TxReceipt, transaction::SignerRecoverable}; use alloy_primitives::{Address, B256, b256, keccak256}; -use celo_genesis::BATCH_AUTH_LOOKBACK_WINDOW; +use celo_genesis::{BATCH_AUTH_ENFORCEMENT_DELAY_SECS, BATCH_AUTH_LOOKBACK_WINDOW}; use kona_derive::ChainProvider; use kona_protocol::BlockInfo; use lru::LruCache; @@ -58,18 +62,20 @@ pub struct BatchAuthConfig { /// /// The fork is conceptually an L2-timestamp hardfork, but the per-L1-block decision made at /// the data source layer is gated on the L1 origin time of the block being scanned (see - /// [`Self::is_active`]) — mirroring the upstream `ecotoneTime` precedent. + /// [`Self::is_enforced`]) — mirroring the upstream `ecotoneTime` precedent. pub espresso_time: u64, } impl BatchAuthConfig { - /// Returns true when Espresso event-only batch authorization is active at the given L1 origin - /// time, i.e. `l1_origin_time >= espresso_time`. + /// Returns true once event-based batch authentication is EXCLUSIVELY enforced at the given L1 + /// origin time: `l1_origin_time >= espresso_time +` + /// [`BATCH_AUTH_ENFORCEMENT_DELAY_SECS`]. + /// Before that — pre-fork or within the grace window — derivation keeps accepting + /// sender-authenticated batches. /// - /// Mirrors [`celo_genesis::CeloRollupConfig::is_espresso_active`] for the case where a - /// `BatchAuthenticator` is configured. - pub const fn is_active(&self, l1_origin_time: u64) -> bool { - l1_origin_time >= self.espresso_time + /// Mirrors op-node's `derive.isEspressoAuthEnforced`; must stay in lockstep with it. + pub const fn is_enforced(&self, l1_origin_time: u64) -> bool { + l1_origin_time >= self.espresso_time + BATCH_AUTH_ENFORCEMENT_DELAY_SECS } } @@ -254,15 +260,17 @@ impl BatchAuthCache { /// Behaviour is gated by `auth_config` together with `l1_origin_time` (the L1 origin time of the /// block being scanned): /// -/// - **Pre-Espresso / vanilla OP Stack** — `auth_config` is `None`, or it is `Some` but the fork is -/// not yet active at `l1_origin_time` ([`BatchAuthConfig::is_active`] is false): authorized iff -/// the transaction sender matches `batcher_address`. `authenticated_hashes` is ignored. -/// - **Post-Espresso** — `auth_config` is `Some` and active: authorized iff `batch_hash` is present -/// in `authenticated_hashes` AND the transaction's recovered L1 sender equals the `caller` that -/// authenticated that commitment. This caller-binding prevents one batcher from replaying a batch -/// authenticated by another. +/// - **Before enforcement / vanilla OP Stack** — `auth_config` is `None`, or it is `Some` but +/// enforcement has not begun at `l1_origin_time` ([`BatchAuthConfig::is_enforced`] is false, i.e. +/// pre-fork or within the grace window): authorized iff the transaction sender matches +/// `batcher_address`. `authenticated_hashes` is ignored. +/// - **Enforced** — `auth_config` is `Some` and [`BatchAuthConfig::is_enforced`] is true: +/// authorized iff `batch_hash` is present in `authenticated_hashes` AND the transaction's +/// recovered L1 sender equals the `caller` that authenticated that commitment. This +/// caller-binding prevents one batcher from replaying a batch authenticated by another. +/// Sender-based fallback is rejected. /// -/// Because the fork time lives inside [`BatchAuthConfig`], the post-Espresso (caller-bound) branch +/// Because the fork time lives inside [`BatchAuthConfig`], the enforced (caller-bound) branch /// is only reachable when an authenticator is configured — the "fork active but no authenticator" /// state is unrepresentable. /// @@ -278,9 +286,9 @@ pub fn is_batch_authorized( l1_origin_time: u64, ) -> bool { if let Some(config) = auth_config && - config.is_active(l1_origin_time) + config.is_enforced(l1_origin_time) { - // Post-Espresso: the commitment must be authenticated AND the recovered batch tx sender + // Enforced: the commitment must be authenticated AND the recovered batch tx sender // must equal the `caller` that emitted the authenticating event. Sender-based // fallback is rejected. let Some(&caller) = authenticated_hashes.get(&batch_hash) else { @@ -288,9 +296,10 @@ pub fn is_batch_authorized( }; return tx.recover_signer().map(|sender| sender == caller).unwrap_or(false); } - // Pre-Espresso (or Espresso not yet active): vanilla OP Stack sender verification. Kept - // byte-identical to upstream kona's `BlobSource::extract_blob_data`, which substitutes - // `Address::ZERO` on signature-recovery failure (`unwrap_or_default`) rather than rejecting. + // Before enforcement (pre-fork or within the grace window): vanilla OP Stack sender + // verification. Kept byte-identical to upstream kona's `BlobSource::extract_blob_data`, which + // substitutes `Address::ZERO` on signature-recovery failure (`unwrap_or_default`) rather than + // rejecting. tx.recover_signer().unwrap_or_default() == batcher_address } @@ -439,11 +448,12 @@ mod tests { BatchAuthConfig { authenticator_address, espresso_time: 0 } } - // L1 origin time used as "post-Espresso" for an `active_config` (espresso_time = 0). - const POST_ESPRESSO_TIME: u64 = 0; + // L1 origin time at which event-based auth is enforced for an `active_config` + // (espresso_time = 0): the first second past the grace period. + const ENFORCED_TIME: u64 = BATCH_AUTH_ENFORCEMENT_DELAY_SECS; #[test] - fn test_is_batch_authorized_post_espresso_matching_caller_accepted() { + fn test_is_batch_authorized_enforced_matching_caller_accepted() { let auth_addr = address!("1234567890123456789012345678901234567890"); let config = active_config(auth_addr); let batch_hash = b256!("abcdef0000000000000000000000000000000000000000000000000000000000"); @@ -454,7 +464,7 @@ mod tests { // The commitment was authenticated by the tx's own sender. authenticated.insert(batch_hash, sender); - // Post-Espresso, commitment authenticated and tx sender == authenticating caller: + // Auth enforced, commitment authenticated and tx sender == authenticating caller: // authorized. assert!(is_batch_authorized( &tx, @@ -462,12 +472,12 @@ mod tests { Some(&config), &authenticated, Address::ZERO, - POST_ESPRESSO_TIME, + ENFORCED_TIME, )); } #[test] - fn test_is_batch_authorized_post_espresso_caller_mismatch_rejected() { + fn test_is_batch_authorized_enforced_caller_mismatch_rejected() { let auth_addr = address!("1234567890123456789012345678901234567890"); let config = active_config(auth_addr); let batch_hash = b256!("abcdef0000000000000000000000000000000000000000000000000000000000"); @@ -478,40 +488,40 @@ mod tests { // The commitment is authenticated, but by a different caller than the tx sender. authenticated.insert(batch_hash, other_caller); - // Post-Espresso, commitment authenticated but tx sender != authenticating caller: rejected. + // Auth enforced, commitment authenticated but tx sender != authenticating caller: rejected. assert!(!is_batch_authorized( &tx, batch_hash, Some(&config), &authenticated, Address::ZERO, - POST_ESPRESSO_TIME, + ENFORCED_TIME, )); } #[test] - fn test_is_batch_authorized_post_espresso_no_event_rejected() { + fn test_is_batch_authorized_enforced_no_event_rejected() { let auth_addr = address!("1234567890123456789012345678901234567890"); let config = active_config(auth_addr); let batch_hash = b256!("abcdef0000000000000000000000000000000000000000000000000000000000"); let authenticated = BTreeMap::new(); // empty let tx = test_legacy_tx(Address::ZERO); - // Post-Espresso, commitment absent: rejected even for an empty map. + // Auth enforced, commitment absent: rejected even for an empty map. assert!(!is_batch_authorized( &tx, batch_hash, Some(&config), &authenticated, Address::ZERO, - POST_ESPRESSO_TIME, + ENFORCED_TIME, )); } #[test] - fn test_is_batch_authorized_post_espresso_sender_fallback_rejected() { - // Even when sender matches batcher_address, post-Espresso requires an authenticating event - // for the commitment. + fn test_is_batch_authorized_enforced_sender_fallback_rejected() { + // Even when sender matches batcher_address, the enforced regime requires an authenticating + // event for the commitment. let batch_hash = B256::ZERO; let authenticated = BTreeMap::new(); @@ -526,7 +536,7 @@ mod tests { Some(&config), &authenticated, sender, - POST_ESPRESSO_TIME, + ENFORCED_TIME, )); } @@ -585,6 +595,44 @@ mod tests { )); } + #[test] + fn test_is_batch_authorized_grace_window_uses_sender_path() { + // Fork active but still within the grace window (espresso_time <= t < espresso_time + + // BATCH_AUTH_ENFORCEMENT_DELAY_SECS): the event path is gated off and only the sender check + // is honored, even if a matching auth event exists. This is what lets a batcher switch to + // authenticated submission at activation without a configured lead time. + let auth_addr = address!("1234567890123456789012345678901234567890"); + let espresso_time = 1_000; + let config = BatchAuthConfig { authenticator_address: auth_addr, espresso_time }; + let batch_hash = b256!("abcdef0000000000000000000000000000000000000000000000000000000000"); + let mut authenticated = BTreeMap::new(); + + let tx = test_legacy_tx(Address::ZERO); + let sender = tx.recover_signer().unwrap(); + authenticated.insert(batch_hash, sender); + // In the grace window: fork active, but before espresso_time + delay. + let grace_time = espresso_time + BATCH_AUTH_ENFORCEMENT_DELAY_SECS - 1; + + // Sender mismatch: rejected even though a matching event exists (event path gated off). + assert!(!is_batch_authorized( + &tx, + batch_hash, + Some(&config), + &authenticated, + address!("0000000000000000000000000000000000000001"), + grace_time, + )); + // Sender match: authorized via the sender path. + assert!(is_batch_authorized( + &tx, + batch_hash, + Some(&config), + &authenticated, + sender, + grace_time, + )); + } + #[test] fn test_batch_info_authenticated_topic_is_correct() { assert_eq!( diff --git a/crates/kona/derive/src/blobs.rs b/crates/kona/derive/src/blobs.rs index 6d91203e..958e56aa 100644 --- a/crates/kona/derive/src/blobs.rs +++ b/crates/kona/derive/src/blobs.rs @@ -39,9 +39,10 @@ where pub data: Vec, /// Whether the source is open. pub open: bool, - /// Espresso batch-authentication configuration. When `Some` and active for the L1 origin time + /// Espresso batch-authentication configuration. When `Some` and enforced at the L1 origin time /// of the block being scanned, event-based batch authentication is used. Otherwise (no config, - /// or fork not yet active) the source falls back to vanilla OP Stack sender verification. + /// or not yet enforced — pre-fork or within the grace window) the source falls back to vanilla + /// OP Stack sender verification. pub(crate) batch_auth_config: Option, /// LRU caches for batch auth lookback window traversal (receipts + headers). Present iff /// [`Self::batch_auth_config`] is set. @@ -74,11 +75,11 @@ where /// Extracts blob data and indexed blob hashes from the given transactions. /// - /// When the Espresso fork is active at `l1_origin_time`, each transaction is authorized via - /// the `authenticated_hashes` map (commitment → authenticating `caller`); otherwise vanilla - /// OP Stack sender verification against `batcher_address` is used. The gating decision is made - /// per-transaction by [`is_batch_authorized`] from [`Self::batch_auth_config`] + - /// `l1_origin_time`. + /// When event-based authentication is enforced at `l1_origin_time`, each transaction is + /// authorized via the `authenticated_hashes` map (commitment → authenticating `caller`); + /// otherwise vanilla OP Stack sender verification against `batcher_address` is used. The gating + /// decision is made per-transaction by [`is_batch_authorized`] from + /// [`Self::batch_auth_config`] + `l1_origin_time`. fn extract_blob_data( &self, txs: Vec, @@ -175,14 +176,15 @@ where .await .map_err(Into::into)?; - // Only scan for authenticating events when Espresso is active. Pre-Espresso (or Espresso - // not yet active) the lookback walk is bypassed entirely so derivation is - // byte-identical to upstream OP Stack (the BatchAuthenticator events are still - // emitted on L1 but ignored). - let espresso_active = - self.batch_auth_config.is_some_and(|c| c.is_active(block_ref.timestamp)); - let authenticated_hashes: Option> = if espresso_active { - let config = self.batch_auth_config.expect("config present when espresso active"); + // Only scan for authenticating events once event-based authentication is enforced (fork + // active plus the grace period). Before that — pre-fork or within the grace window — the + // lookback walk is bypassed entirely and sender-based authorization is used, so derivation + // is byte-identical to upstream OP Stack (the BatchAuthenticator events are still emitted + // on L1 but ignored). + let auth_enforced = + self.batch_auth_config.is_some_and(|c| c.is_enforced(block_ref.timestamp)); + let authenticated_hashes: Option> = if auth_enforced { + let config = self.batch_auth_config.expect("config present when auth enforced"); let cache = self.auth_cache.as_mut().expect("cache present when config present"); Some( collect_authenticated_batches( @@ -497,7 +499,7 @@ mod tests { } #[tokio::test] - async fn test_post_espresso_event_path() { + async fn test_auth_enforced_event_path() { let batch_inbox = address!("0123456789012345678901234567890123456789"); let auth_addr = address!("00000000000000000000000000000000000000aa"); let tx = test_legacy_tx(batch_inbox); @@ -514,7 +516,10 @@ mod tests { batch_inbox, Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; // The commitment is the unindexed data word; the authenticating `caller` (= the batch tx // sender) is the indexed topic. let log = Log { @@ -529,13 +534,13 @@ mod tests { source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx]); source.chain_provider.insert_receipts(block_info.hash, vec![receipt]); - // Post-Espresso: commitment authenticated and tx sender matches authenticating caller. + // Auth enforced: commitment authenticated and tx sender matches authenticating caller. source.load_blobs(&block_info, Address::ZERO).await.unwrap(); assert_eq!(source.data.len(), 1); } #[tokio::test] - async fn test_post_espresso_caller_mismatch_rejected() { + async fn test_auth_enforced_caller_mismatch_rejected() { let batch_inbox = address!("0123456789012345678901234567890123456789"); let auth_addr = address!("00000000000000000000000000000000000000aa"); let tx = test_legacy_tx(batch_inbox); @@ -551,7 +556,10 @@ mod tests { batch_inbox, Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; // The commitment is authenticated, but by a different caller than the batch tx sender. let other_caller = address!("00000000000000000000000000000000000000bb"); let log = Log { @@ -566,13 +574,13 @@ mod tests { source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx]); source.chain_provider.insert_receipts(block_info.hash, vec![receipt]); - // Post-Espresso: commitment authenticated but tx sender != authenticating caller: rejected. + // Auth enforced: commitment authenticated but tx sender != authenticating caller: rejected. source.load_blobs(&block_info, Address::ZERO).await.unwrap(); assert!(source.data.is_empty()); } #[tokio::test] - async fn test_post_espresso_no_event_rejected() { + async fn test_auth_enforced_no_event_rejected() { let batch_inbox = address!("0123456789012345678901234567890123456789"); let auth_addr = address!("00000000000000000000000000000000000000aa"); let tx = test_legacy_tx(batch_inbox); @@ -583,7 +591,10 @@ mod tests { batch_inbox, Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); source.chain_provider.insert_receipts(block_info.hash, Vec::new()); @@ -612,6 +623,31 @@ mod tests { assert_eq!(source.data.len(), 1); } + #[tokio::test] + async fn test_grace_window_uses_sender_path() { + // Fork active but within the grace window (timestamp < espresso_time + + // BATCH_AUTH_ENFORCEMENT_DELAY_SECS): a sender-matching batch is accepted without an + // authenticating event. No receipts are inserted, so this also proves the lookback scan + // is bypassed — the enforced path could not even fetch them. + let batch_inbox = address!("0123456789012345678901234567890123456789"); + let auth_addr = address!("00000000000000000000000000000000000000aa"); + let mut source = CeloBlobSource::new( + TestChainProvider::default(), + TestBlobProvider::default(), + batch_inbox, + Some(auth_config(auth_addr)), + ); + let tx = test_legacy_tx(batch_inbox); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS - 1, + ..Default::default() + }; + source.chain_provider.insert_block_with_transactions(0, block_info, vec![tx.clone()]); + + source.load_blobs(&block_info, tx.recover_signer().unwrap()).await.unwrap(); + assert_eq!(source.data.len(), 1); + } + // --- Tests ported (adapted to the 4-arg constructor) from kona's `BlobSource`, exercising the // blob-fetch and reset/EOF routing that is unchanged from upstream. --- @@ -685,12 +721,12 @@ mod tests { assert_eq!(source.data.len(), blob_tx_hashes().len()); } - /// Post-Espresso: a 4844 blob tx whose blob batch commitment was authenticated by an event + /// Auth enforced: a 4844 blob tx whose blob batch commitment was authenticated by an event /// emitted by the batch tx's own sender is accepted; its blob versioned hashes are filled. /// Direct analogue of the Go "authenticated blob tx accepted" sub-test (commitment = /// `ComputeBlobBatchHash(blobHashes)`, auth caller = batcher). #[tokio::test] - async fn test_post_espresso_4844_blob_event_path() { + async fn test_auth_enforced_4844_blob_event_path() { let auth_addr = address!("00000000000000000000000000000000000000aa"); let mut source = CeloBlobSource::new( TestChainProvider::default(), @@ -698,7 +734,10 @@ mod tests { BLOB_TX_SENDER, // batcher_address gates the tx `to` Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; // The blob batch commitment is keccak256(concat(blob_versioned_hashes)); authenticated by // the batch tx's recovered signer. let commitment = compute_blob_batch_hash(&blob_tx_hashes()); @@ -723,11 +762,11 @@ mod tests { assert_eq!(source.data.len(), blob_tx_hashes().len()); } - /// Post-Espresso: a 4844 blob tx whose blob batch commitment is authenticated, but by a + /// Auth enforced: a 4844 blob tx whose blob batch commitment is authenticated, but by a /// different caller than the batch tx sender, is rejected (caller-binding). Mirrors the Go /// "authenticated tx rejected when sender differs from auth caller" sub-test, on the blob path. #[tokio::test] - async fn test_post_espresso_4844_blob_caller_mismatch_rejected() { + async fn test_auth_enforced_4844_blob_caller_mismatch_rejected() { let auth_addr = address!("00000000000000000000000000000000000000aa"); let other_caller = address!("00000000000000000000000000000000000000bb"); let mut source = CeloBlobSource::new( @@ -736,7 +775,10 @@ mod tests { BLOB_TX_SENDER, Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; let commitment = compute_blob_batch_hash(&blob_tx_hashes()); // Commitment authenticated, but by `other_caller`, not the batch tx sender BLOB_TX_BATCHER. let log = Log { @@ -756,11 +798,11 @@ mod tests { assert!(source.data.is_empty()); } - /// Post-Espresso: a 4844 blob tx from the batcher with no authenticating event is rejected — + /// Auth enforced: a 4844 blob tx from the batcher with no authenticating event is rejected — /// the sender-based fallback is gone once Espresso is active. Mirrors the Go "fallback batcher /// without auth event rejected" sub-test, on the blob path. #[tokio::test] - async fn test_post_espresso_4844_blob_no_event_rejected() { + async fn test_auth_enforced_4844_blob_no_event_rejected() { let auth_addr = address!("00000000000000000000000000000000000000aa"); let mut source = CeloBlobSource::new( TestChainProvider::default(), @@ -768,7 +810,10 @@ mod tests { BLOB_TX_SENDER, Some(auth_config(auth_addr)), ); - let block_info = BlockInfo::default(); + let block_info = BlockInfo { + timestamp: celo_genesis::BATCH_AUTH_ENFORCEMENT_DELAY_SECS, + ..Default::default() + }; source.chain_provider.insert_block_with_transactions(1, block_info, valid_blob_txs()); // No auth event for the blob batch commitment. source.chain_provider.insert_receipts(block_info.hash, Vec::new()); diff --git a/crates/kona/genesis/src/lib.rs b/crates/kona/genesis/src/lib.rs index a2a13bff..d50ea0ca 100644 --- a/crates/kona/genesis/src/lib.rs +++ b/crates/kona/genesis/src/lib.rs @@ -7,5 +7,6 @@ extern crate alloc; mod rollup; pub use rollup::{ - BATCH_AUTH_LOOKBACK_WINDOW, CeloEspressoConfig, CeloEspressoConfigError, CeloRollupConfig, + BATCH_AUTH_ENFORCEMENT_DELAY_SECS, BATCH_AUTH_LOOKBACK_WINDOW, CeloEspressoConfig, + CeloEspressoConfigError, CeloRollupConfig, }; diff --git a/crates/kona/genesis/src/rollup.rs b/crates/kona/genesis/src/rollup.rs index 20f3f38b..05513d07 100644 --- a/crates/kona/genesis/src/rollup.rs +++ b/crates/kona/genesis/src/rollup.rs @@ -14,6 +14,19 @@ use kona_genesis::RollupConfig; /// celo-kona wraps upstream kona instead of patching it at source. pub const BATCH_AUTH_LOOKBACK_WINDOW: u64 = 100; +/// Grace period, in seconds, after `espresso_time` during which derivation still accepts +/// sender-authenticated batches. Event-based authentication is enforced only once an L1 block's +/// origin time reaches `espresso_time + BATCH_AUTH_ENFORCEMENT_DELAY_SECS`. +/// +/// This lets a batcher switch to authenticated submission right at activation: a batch decided +/// pre-fork (so without an auth event) that lands in a post-activation L1 block is still accepted +/// under sender authorization, as long as it is included within the grace period. Sized to one +/// full [`BATCH_AUTH_LOOKBACK_WINDOW`] at the nominal 12s L1 slot time (~20 minutes). +/// +/// This is a consensus constant: it MUST match op-node's `derive.BatchAuthEnforcementDelaySecs`, +/// or fault-proof derivation and the op-node verifier will disagree at the fork boundary. +pub const BATCH_AUTH_ENFORCEMENT_DELAY_SECS: u64 = BATCH_AUTH_LOOKBACK_WINDOW * 12; + /// Celo-specific Espresso batch-authentication configuration. /// /// These fields live on [`CeloRollupConfig`] rather than on upstream @@ -25,9 +38,11 @@ pub const BATCH_AUTH_LOOKBACK_WINDOW: u64 = 100; pub struct CeloEspressoConfig { /// Activation timestamp (L2) for the Espresso event-only batch authorization hardfork. /// - /// Pre-fork the derivation pipeline runs vanilla OP Stack semantics (sender-based - /// authorization, no `BatchAuthenticator` event lookup). Post-fork batches must be - /// authenticated by `BatchInfoAuthenticated` events; sender-based fallback is rejected. + /// Pre-fork — and within the [`BATCH_AUTH_ENFORCEMENT_DELAY_SECS`] grace window after + /// activation — the derivation pipeline runs vanilla OP Stack semantics (sender-based + /// authorization, no `BatchAuthenticator` event lookup). Once an L1 block's origin time + /// reaches `espresso_time + BATCH_AUTH_ENFORCEMENT_DELAY_SECS`, batches must be authenticated + /// by `BatchInfoAuthenticated` events; sender-based fallback is rejected. /// /// The per-L1-block decision in the data source is gated on the L1 origin time of the block /// being scanned, mirroring the upstream `ecotoneTime` precedent. @@ -78,20 +93,6 @@ impl CeloRollupConfig { self.espresso.batch_authenticator_address.is_some_and(|addr| !addr.is_zero()) } - /// Returns true if Espresso event-only batch authorization is active at the given L1 origin - /// timestamp. - /// - /// Pre-fork the derivation pipeline runs vanilla OP Stack semantics (sender-based - /// authorization, no `BatchAuthenticator` event lookup). Post-fork batches must be - /// authenticated by `BatchInfoAuthenticated` events; sender-based fallback is rejected. - /// - /// This is intentionally orthogonal to the chained OP Stack hardforks and to - /// [`Self::is_batch_auth_enabled`] (which only signals that a `BatchAuthenticator` contract - /// address is configured). - pub fn is_espresso_active(&self, timestamp: u64) -> bool { - self.espresso.espresso_time.is_some_and(|t| timestamp >= t) - } - /// Resolves the Espresso batch-authentication parameters into a validated bundle suitable for /// the derivation data sources. /// @@ -373,20 +374,6 @@ mod tests { // No Espresso fields in this config => defaults (disabled). assert_eq!(deserialized.espresso, CeloEspressoConfig::default()); assert!(!deserialized.is_batch_auth_enabled()); - assert!(!deserialized.is_espresso_active(u64::MAX)); - } - - #[test] - fn test_is_espresso_active() { - let mut cfg = CeloRollupConfig::new(RollupConfig::default()); - // Unset: never active. - assert!(!cfg.is_espresso_active(0)); - assert!(!cfg.is_espresso_active(u64::MAX)); - // Set: boundary semantics match the OP Stack forks. - cfg.espresso.espresso_time = Some(100); - assert!(!cfg.is_espresso_active(99)); - assert!(cfg.is_espresso_active(100)); - assert!(cfg.is_espresso_active(101)); } #[test] @@ -545,8 +532,6 @@ mod tests { Some(address!("00000000000000000000000000000000000000aa")) ); assert!(cfg.is_batch_auth_enabled()); - assert!(cfg.is_espresso_active(1234)); - assert!(!cfg.is_espresso_active(1233)); } /// The derived `Serialize` must emit the same flat shape the custom `Deserialize` consumes, so