Espresso 2: derivation pipeline#445
Conversation
Adds the Espresso-introduced contracts and the minimum supporting changes
required for them to compile, test, and pass the contract checks.
New contracts and scripts:
- src/L1/BatchAuthenticator.sol and interfaces/L1/IBatchAuthenticator.sol
(upgradeable contract that authenticates batch transactions, with switching
between Espresso and fallback batchers)
- scripts/deploy/DeployBatchAuthenticator.s.sol and
scripts/deploy/DeployEspresso.s.sol
- test/L1/BatchAuthenticator.t.sol and test/mocks/MockEspressoTEEVerifiers.sol
- snapshots/{abi,storageLayout}/BatchAuthenticator.json
- snapshots/semver-lock.json entry for BatchAuthenticator
New submodules:
- lib/espresso-tee-contracts (interfaces required by BatchAuthenticator)
- lib/openzeppelin-contracts-upgradeable-v5 (OZ v5 used by BatchAuthenticator
via OwnableUpgradeable)
Supporting changes (Espresso-driven):
- foundry.toml: remappings for OZ v5 and espresso-tee-contracts; ignored
warning codes for vendored libs; OOM-safe jobs settings; via-ir profile.
- justfile: fix-proxy-artifact recipe to handle OZ v5 shadowing Proxy/ProxyAdmin
artifacts; build/coverage hooks.
- src/universal/Proxy.sol, src/universal/ProxyAdmin.sol: pin pragma to exact
0.8.15 so they stay in their own compilation group and never emit PUSH0.
- src/universal/ReinitializableBase.sol: loosen pragma to ^0.8.15 so
BatchAuthenticator (compiled with OZ v5) can import it.
- scripts/* and test/*: disambiguate Proxy artifact lookups to
src/universal/Proxy.sol:Proxy (avoids OZ v5 proxy/Proxy.sol shadow).
- scripts/checks: bypass interface checks for artifacts originating from lib/;
add Espresso-related contract names to exclude lists; pragma exclusions for
Proxy/ProxyAdmin/BatchAuthenticator.
- test/vendor/Initializable.t.sol: exclude BatchAuthenticator (deployed by a
separate Espresso script).
Co-authored-by: OpenCode <[email protected]>
Co-authored-by: piersy <[email protected]>
- strict-pragma: remove unneeded exclusions for src/universal/Proxy.sol and src/universal/ProxyAdmin.sol — both already use strict 'pragma solidity 0.8.15;', so the entries (and their misleading comment claiming '^') were dead. - interfaces: move the Espresso excludeContracts block out of the upstream-shared area and down next to the Celo block, with one entry per line to match the surrounding style. Localizes future rebase deltas. Co-authored-by: OpenCode <[email protected]>
|
Hi @QuentinI, wondering why there are these "CaffNode" changes:
I thought this was being handled by the espresso-rollup-node-proxy? |
|
@piersy you are right, I was out of date on how the proxy works. I'll strip the Caff node out of this PR, we are not going to need it. |
Inline the EspressoTEEVerifier deployment in DeployEspresso.s.sol so it
no longer imports lib/espresso-tee-contracts/scripts/DeployTEEVerifier.s.sol
or DeployNitroTEEVerifier.s.sol. The upstream scripts pulled OZ v5's
TransparentUpgradeableProxy (and its auto-deployed ProxyAdmin) into the
OP artifact tree, shadowing src/universal/ProxyAdmin.sol and forcing a
~90-line fix-proxy-artifact justfile recipe.
The TEEVerifier is now deployed behind src/universal/Proxy.sol +
src/universal/ProxyAdmin.sol, matching how BatchAuthenticator is
deployed in the same script. ERC-1967 slots are unchanged, so external
callers see no difference.
The raw vm.getCode("ProxyAdmin") lookups in the deploy scripts and
BatchAuthenticator tests are switched to the explicit artifact path
vm.getCode("forge-artifacts/ProxyAdmin.sol/ProxyAdmin.json") to
deterministically resolve the default compilation profile's bytecode
(the dispute profile transitively compiles ProxyAdmin at optimizer_runs=5000,
creating a second artifact that broke unqualified lookups).
The fix-proxy-artifact recipe and its 5 callsites are removed.
Cherry-picked from piersy's commit 5d0a803 on PR ethereum-optimism#443. Walks the dual-batcher state machine: Espresso path → switchBatcher → fallback path → switchBatcher → Espresso path. Asserts every transition emits the expected event, that signer registration survives the round-trip, and that re-issuing the same call after a mode flip changes the outcome (the previously-valid Espresso signature is no longer consulted on the fallback path). Co-authored-by: Piers Powlesland <[email protected]> Co-authored-by: OpenCode <[email protected]>
Adds derivation-pipeline support for the BatchAuthenticator contract introduced in the previous PR. Stacks on the contracts PR. Introduces an L2-timestamp hardfork (EspressoEnforcementTime) gating all post-fork derivation semantics. Pre-fork, derivation behaves exactly as upstream Optimism: batches are accepted based on the L1 transaction sender matching the SystemConfig batcher address. Post-fork, batches are authenticated via BatchInfoAuthenticated(bytes32) events emitted by the BatchAuthenticator contract, and sender-based authorization is rejected. Adds CollectAuthenticatedBatches which scans L1 receipts over a configurable lookback window (default 100 blocks) to build the set of authenticated batch commitment hashes for each L1 block being derived. Results are cached in two reorg-safe (block-hash-keyed) LRU caches: one for receipt-derived event sets, one for L1BlockRef resolution. For consecutive L1 blocks the lookback windows overlap by ~99 blocks, so only one new block's receipts need to be fetched on each call. Adds rollup.Config fields: EspressoEnforcementTime *uint64, BatchAuthenticatorAddress, BatchAuthLookbackWindow. Adds unit tests for batch authentication across calldata, blob, and altda data sources. Co-authored-by: OpenCode <[email protected]>
08a9056 to
51888e5
Compare
|
Rebased and dropped caff-node changes. |
Matches upstream Optimism hardfork naming convention (RegolithTime, EcotoneTime, IsthmusTime, ...). All hardforks enforce a new set of rules, so the "Enforcement" qualifier was redundant. Renames: EspressoEnforcementTime -> EspressoTime (rollup.Config field) IsEspressoEnforcement -> IsEspresso (rollup.Config method) espressoEnforcementTime -> espressoTime (DataSourceConfig field) isEspressoEnforcement -> isEspresso (DataSourceConfig method) espresso_enforcement_time -> espresso_time (JSON tag, forEachFork log key) "Espresso Enforcement" -> "Espresso" (forEachFork display name) Also rewords prose docstrings: "EspressoEnforcement" -> "Espresso", "Pre/Post-EspressoEnforcement" -> "Pre/Post-Espresso". Addresses PR feedback: celo-org#445 (comment) Co-authored-by: OpenCode <[email protected]>
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d55b56e12a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| IEspressoTEEVerifier(teeProxyAddr).setEspressoNitroTEEVerifier( | ||
| IEspressoNitroTEEVerifier(address(nitroVerifier)) | ||
| ); |
There was a problem hiding this comment.
Set the Nitro verifier before transferring ownership
When proxyAdminOwner is set to an address other than the broadcaster, production Espresso deployment initializes the TEE verifier owner to proxyAdminOwner (line 270) and then broadcasts setEspressoNitroTEEVerifier from msg.sender here, so this onlyOwner call reverts and the deployment cannot complete for the standard multisig-owner flow. Either perform this call as the configured owner or wire the Nitro verifier before ownership is no longer controlled by the broadcaster.
Useful? React with 👍 / 👎.
|
|
||
| // BatchAuthenticatorAddress is the L1 address of the BatchAuthenticator contract whose | ||
| // BatchInfoAuthenticated(bytes32) events the derivation pipeline scans post-Espresso. | ||
| BatchAuthenticatorAddress common.Address `json:"batch_authenticator_address,omitempty,omitzero"` |
There was a problem hiding this comment.
EspressoTime makes BatchInfoAuthenticated events mandatory post-fork, but the rollup config does not appear to require a non-zero BatchAuthenticatorAddress when the fork is configured.
There should be a check added that checks BatchAuthenticatorAddress is set when EspressoTime is non-nil.
Then test should be added:
- A rollup config validation test that fails when
EspressoTime != nilandBatchAuthenticatorAddress == common.Address{}. - A rollup config validation test that passes when both
EspressoTimeand a non-zeroBatchAuthenticatorAddressare configured.
There was a problem hiding this comment.
If we decide not to hardcode BatchAuthLookbackWindow then we probably would like to include that in the check as well.
|
@QuentinI Will the derivation changes for kona also be submitted in this PR or in a stacked one? |
| if authenticatedHashes[batchHash] { | ||
| return true | ||
| } |
There was a problem hiding this comment.
Hi @QuentinI, I realised that this opens up an attack vector in that batch transactions can be replayed once legitimately submitted. Any account can simply re-submit the same data and force the derivation of an invalid batch for the entire network. Because we use altDA it's actually pretty cheap to submit batches (about 10 cents currently). Is it reasonable to also check that the batch comes from the espresso batcher address here?
There was a problem hiding this comment.
It is quite hard to check the espresso batcher here, because we don't have access to contract storage in the derivation pipeline when running in FPVM and the espresso batcher address is mutable. The vanilla OP stack deals with this by embedding current batcher address (=fallback batcher address after our changes) in L2 state, but doing the same with espresso batcher address would require a very invasive change.
To be clear, do you worry about this as a potential DoS vector? My napkin math suggests that it would be at the absolute worst be about 1$/second to keep a node busy dropping replayed batches. If you consider this unacceptable I think the best way to work around the storage issue by making BatchAuthenticated event a pair of (hash, address) to let the derivation pipeline know who is supposed to post the authenticated batch.
There was a problem hiding this comment.
For reference, the estimate is from this benchmark I had Claude write that runs the derivation pipeline through the same batch with ~OP transaction numbers, ran on AWS c6i.2xlarge (Intel Xeon Platinum 8375C @ 2.9 GHz), bench is single-threaded (as is the op-node). OP number was 70ms/batch, which at 0.1$/batch comes to the 1$/second I referenced above.
There was a problem hiding this comment.
If we restrict who can submit batches to the authenticator contract like proposed in EspressoSystems#438, does this not solve the issue?
There was a problem hiding this comment.
@philippecamacho no, this is about replaying a batch already authenticated by the real batcher.
There was a problem hiding this comment.
This concerns submission to batch inbox, not batch authenticator contract. The flow is as follows:
- Batcher authenticates batch data
Dwith batch authentication contract - Batcher posts
Dto batch inbox - Attacker posts multiple copies of
Dverbatim to batch inbox - Derivation pipeline correctly derives batches from the first transaction containing
D - Subsequent copies of
Dare still authorized, so they pass the first authorization check and go through decompression before the derivation pipeline realizes they do not actually extend the chain and drops them
There was a problem hiding this comment.
Hi @QuentinI, thanks for that benchmark. The benchmark is still using stubbed L1 and L2 and also does not actually exercise the altda path. It looks promising but i'm not convinced that this is not a problem.
I think the best way to resolve this would be to actually test on the devnet and see what happens there. So we can leave this as is for now and come back to it if the devnet surfaces any problems.
There was a problem hiding this comment.
Hi @piersy!
After some internal discussion, our position is that preemptively fixing it the way I proposed earlier would be faster and easier than trying the attack on a devnet.
I'm referring to my suggestion to make BatchAuthenticated event a pair of (hash, address) to let the derivation pipeline know in advance who is supposed to post the authenticated batch. I don't think there are any downsides to this except implementation cost, WDYT?
| // Not exposed as a CLI flag; configured per-chain via rollup.json | ||
| // (Config.BatchAuthLookbackWindow) and consumed via | ||
| // rollup.Config.BatchAuthLookbackWindowOrDefault(). | ||
| const DefaultBatchAuthLookbackWindow uint64 = 100 |
There was a problem hiding this comment.
This should I think either be added to https://github.com/celo-org/superchain-registry so it can be fetched in the usual manner, or possibly hardcoded (and therefore not be configurable) since if this value differs between nodes on the network they can derive different chains.
There was a problem hiding this comment.
You can look at MaxSequencerDrift for an example of hardcoding.
There was a problem hiding this comment.
Adding this to the rollup config sounds like a lot of overhead for this, so I'm leaning towards hardcoding.
| // batchAuthCache is a global LRU cache mapping L1 block hash to the set of | ||
| // authenticated batch commitment hashes found in that block's receipts. | ||
| // Keyed by block hash so it is naturally reorg-safe: after a reorg the | ||
| // parent-hash traversal follows a different chain and stale entries are | ||
| // never hit. Thread-safe via lru.Cache's internal mutex. | ||
| batchAuthCache *lru.Cache[common.Hash, map[common.Hash]bool] | ||
| batchAuthCacheOnce sync.Once | ||
|
|
||
| // blockRefCache is a global LRU cache mapping L1 block hash to its L1BlockRef. | ||
| // This avoids redundant L1BlockRefByHash RPC calls during the lookback window | ||
| // traversal: consecutive L1 blocks share ~99 blocks in their lookback windows, | ||
| // so almost every parent-hash lookup hits the cache after the first full traversal. | ||
| // Keyed by block hash for natural reorg safety (same rationale as batchAuthCache). | ||
| blockRefCache *lru.Cache[common.Hash, eth.L1BlockRef] | ||
| blockRefCacheOnce sync.Once |
There was a problem hiding this comment.
I investigated if the global caches could be removed without much disruption and the change looks good to me (a slight reduction in lines of code), see what you think - 000060a
There was a problem hiding this comment.
Sure, this works! I think the idea behind the caches being global was to keep diff to existing files to absolute minimum, but if you prefer it as a config field I have no problem cherry-picking your commit.
There was a problem hiding this comment.
Yeah I don't think the existing diff changes much, all the places where changes were required I think have already been modified, so it's not really making that diff any worse.
Co-authored-by: OpenCode <[email protected]>
EspressoTime is a conceptually L2-timestamp fork activation time, but the derivation pipeline gates on it by comparing against the L1 origin time of the enclosing L1 block. Update the doc comments to reflect this, consistent with the existing blob_data_source.go/calldata_source.go comments. Co-authored-by: OpenCode <[email protected]>
| // IsEspresso returns true if the Espresso upgrade is active at or past the | ||
| // given timestamp. EspressoTime is conceptually an L2-timestamp fork activation | ||
| // time, but the derivation pipeline calls this with the L1 origin time of the | ||
| // enclosing L1 block (mirroring upstream's ecotoneTime treatment), so the fork | ||
| // is effectively gated per L2 epoch. When active, the derivation pipeline runs | ||
| // all Espresso-specific semantics (event-based batch authentication via the | ||
| // BatchAuthenticator contract). When inactive, the pipeline behaves exactly as | ||
| // upstream Optimism. | ||
| func (c *Config) IsEspresso(timestamp uint64) bool { | ||
| return c.EspressoTime != nil && timestamp >= *c.EspressoTime | ||
| } |
There was a problem hiding this comment.
Is this necessary, I can't see this being used anywhere, I can just see the datasource config's isEspresso being used.
There was a problem hiding this comment.
Hmm, actually I think I'm in favour of just holding the rollup config in the DataSourceConfig, since we are already copying three espresso fields out of it, if we did that then we could use this method and drop the implementation on the datasource.
There was a problem hiding this comment.
I made these changes in this commit 487ec10
I think its a slight improvement since we don't need to duplicate the description of those config fields and it makes the diff in the DataSourceConfig type 1 line instead of multiple. You can cherry pick if you agree.
There was a problem hiding this comment.
Makes total sense! Will cherry-pick it
Add the `celo-derive` crate implementing the Rust (kona) companion to the op-node Espresso batch-authentication changes (celo-org/optimism#445), relocated from the vendored-kona PR celo-org/optimism#449 to follow celo-kona's wrap/duplicate-don't-patch model. Post-fork, batches are authorized by scanning L1 receipts for `BatchInfoAuthenticated` events from the configured `BatchAuthenticator` contract within a lookback window, instead of by transaction sender. Pre-fork (or when the fork is not yet active) derivation is byte-identical to upstream OP Stack. - `CeloEthereumDataSource` / `CeloCalldataSource` / `CeloBlobSource`: duplicates of kona's data sources with the auth branch folded in (kona's `CalldataSource`/`BlobSource`/`BlobData` internals are `pub(crate)` and cannot be wrapped). - `BatchAuthConfig` bundles the authenticator address, fork time and lookback window so the "fork scheduled but no authenticator" state is unrepresentable; `CeloRollupConfig::batch_auth_params` validates it and hard-errors on misconfiguration (otherwise derivation would silently stall at the fork boundary). - `CeloRollupConfig` gains the Espresso settings (parsed from the same rollup.json) and `CeloBootInfo` carries them through boot. - Point the hokulea git dep at celo-org/hokulea fccc98c (celo-org/hokulea#2), which generalizes `EigenDADataSource` over `DataAvailabilityProvider` so the eigenda path can wrap `CeloEthereumDataSource`. Co-authored-by: OpenCode <[email protected]>
This PR introduces the op-node changes of Espresso integration: batch authentication and caff node. As I can't push branches to this PR I can't create a clean stacked PR; relevant changes over #443 are in commit 08a9056
Batch authentication: derivation pipeline now reads
BatchAuthenticatedevents emitted by theBatchAuthenticatorcontract introduced by #443, gated byEspressoEnforcementTimehardfork timestamp.Caff node: op-node gains Caff mode support. When a node is configured as a Caff node (
CaffNodeConfig.Enabled) and the parent L2 block has reached the configured caffeination height (and the Espresso enforcement hardfork is active), derivation pulls batches directly from the Espresso sequencer via Espresso StreamerThe PR includes unit tests for the batch-authenticator event scanner, Espresso batch RLP round-trip, and the new event-based authorization path across the calldata, blob, and altDA data sources. e2e testing involving actual Espresso instance is not part of this PR because it requires batcher changes as well.