Espresso 3a: Fallback batcher (re-hosted) - #458
Conversation
a587307 to
34c9d0d
Compare
f840eee to
12b46a6
Compare
34c9d0d to
60f92ee
Compare
0f8d63c to
9330de1
Compare
8a45116 to
266fe6f
Compare
9330de1 to
1616378
Compare
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 <noreply@opencode.ai>
Co-authored-by: piersy <pierspowlesland@gmail.com>
- 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 <noreply@opencode.ai>
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 #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 <pierspowlesland@gmail.com> Co-authored-by: OpenCode <noreply@opencode.ai>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 25c5086252
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ide enforcement delay (#468) * address comment * Update op-node/rollup/derive/batch_authenticator.go Co-authored-by: piersy <pierspowlesland@gmail.com> * lint * fix documentation --------- Co-authored-by: piersy <pierspowlesland@gmail.com>
Fixes the M3 nonce-gap hazard raised in the audit review: #458 (comment) A fallback batch is two L1 transactions that must land in order — an `authenticateBatchInfo` call followed by the batch-data tx. Previously both were submitted back-to-back at consecutive nonces. If the auth tx failed asynchronously, txmgr reset its cached nonce, the in-flight batch tx was left sitting on a gapped nonce, and the next pair collided with it, forcing channel rewinds. The fix keeps the two txs paired but makes a failed pair leave no gap — without serializing the hot path: - Calldata pairs (today's fallback DA) pipeline through a new txmgr primitive, `SendPair`. Both legs are signed synchronously at consecutive nonces and broadcast back-to-back under a single `MaxPendingTransactions` slot, so throughput is unchanged and `max-pending-tx` stays meaningful. If the auth leg fails permanently, the batch leg's nonce is consumed by a fee-bumped no-op at that exact nonce (`cancelPairNonce`) instead of a global nonce reset — no gap, no orphaned batch tx, and higher pending nonces are left undisturbed. - Blob pairs can't share one slot (geth won't hold a calldata auth tx and a blob batch tx reserved for the same account at the same time), so they serialize: the auth tx is sent and confirmed first, then the batch tx (`sendFallbackAuthSerialized`). Also in this PR: - Auth-tx failures are reported under a calldata-typed `txRef`, so an `ErrAlreadyReserved` receipt can't make `cancelBlockingTx` cancel the wrong pool and wedge the blobpool. - On parent-context cancellation (shutdown, or a sibling pair failing the queue's error group) the pair resets the cached nonce and leaves in-flight legs to resolve in the pool, so the next send refills any gap. - New metric `fallback_auth_window_exceeded_total`, incremented when a batch tx lands more than `BatchAuthLookbackWindow` blocks after its auth tx. - Startup check that `NumConfirmations` leaves enough headroom inside `BatchAuthLookbackWindow` for the batch to land, applied only when the BatchAuthenticator is configured and the DA type can serialize (blobs or auto). The receipt-watcher goroutine and `authGroup` are retained — they drive the pipelined calldata path (the earlier plan to delete them applied only to the fully-serialized design). --------- Co-authored-by: piersy <pierspowlesland@gmail.com>
The interface doc claimed a failed pair never resets the nonce, but the pair code resets it in three cases: a leg failing during preparation, a cancelled parent context (the repair branch calls resetNonce), and a failed no-op repair falling back to resetNonce. Reword to state that only in-flight leg failures are repaired with a no-op and no reset, and that preparation failures, parent-ctx cancellation, and failed repairs fall back to a nonce reset.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 229eea2f1f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8957155392
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
155ea47 to
7c1715c
Compare
leg's methods were one-line wrappers, legResult duplicated SendResponse, and pairSend existed only to hold sendPair/repair's arguments. Replace all three with plain SimpleTxManager methods taking explicit args, matching the SendAsync idiom. No behavior change.
7c1715c to
1cc52fc
Compare
|
Hi @lukeiannucci, I pushed a commit just simplifying the pair and leg code just drops around 50 LOC, makes it a bit easier to follow. |
Match the espresso_txmgr.go rename.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1da0b588b1
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Thanks @piersy just took a look, and it looks nice and clean to me! |
| // sendPair drives the pair to resolution on its own goroutine: it broadcasts | ||
| // both legs concurrently, cancels the second leg if the first fails permanently, | ||
| // and delivers exactly one response per channel. A second leg cancelled this way | ||
| // has its error wrapped in ErrPairLegCancelled so callers can tell it did not | ||
| // fail on its own. | ||
| // | ||
| // Before any response is delivered, the pair repairs the nonce of each leg that | ||
| // resolved with an error (repairPair): the nonce is consumed on chain by a | ||
| // fee-bumped no-op, and sendPair blocks until that settles. Why: an errored leg | ||
| // is in an unknown state — possibly never broadcast, possibly in the pool at | ||
| // heavily bumped fees, possibly even mined unseen. Stock recovery (resetNonce, | ||
| // then re-signing the same nonces) recovers by collision with those orphans, | ||
| // which is safe for self-contained txs (a stale winner is at worst a duplicate) | ||
| // but not for pairs: a failure makes the batcher rewind and re-pack, so resent | ||
| // legs carry a different commitment than the orphans they contest. A stale leg | ||
| // winning one nonce while a fresh leg wins the adjacent one mines a torn pair — | ||
| // auth and batch with mismatched commitments, ignored by derivation — and the | ||
| // recovery round has itself half-failed, leaving fresh orphans behind. | ||
| // | ||
| // Repairing before responding keeps the failure self-contained: the error | ||
| // cannot cancel the queue's error group, and so cannot trigger resubmission, | ||
| // until this pair's nonces are already consumed — the resubmission's nonce | ||
| // query starts above everything the pair left behind. | ||
| // | ||
| // Aborted siblings get no such guarantee. A pair cancelled by the group abort | ||
| // (parent context already dead) only resets the nonce — see repairPair's | ||
| // parent-context branch — leaving its legs wherever cancellation caught them: | ||
| // That spoilage is self-limiting: a stale leg can only cause damage by | ||
| // contesting a slot, and a contested slot that fails a round becomes an | ||
| // errored leg of that round's pair — so repair settles the very slots the | ||
| // damage occurred at, and each abort's leftovers are cleaned up by the first | ||
| // round they hurt. It is accepted because settling every aborted sibling | ||
| // eagerly would stall the queue's drain barrier a block per abort, scaling | ||
| // with the pipeline depth. |
There was a problem hiding this comment.
Hi @lukeiannucci, it took me a while to get my head around how repair and recovery work. I've extended the comment for sendPair to include my understanding. I'd appreciate you checking it to make sure it aligns with your understanding.
I think it's Ok to go ahead with this as it is, the alternatives I see to this approach would be:
- To serialize (we already tried this and it reduced the potential throughput too much, although if instead of waiting for 10 confirmations on the auth we just waited for inclusion then the throughput could be aceptable for now (up to 36MiB/h, but might struggle with future load)
- Use a single tx for auth and batch, I think this was ruled out initially by espresso's investigations, but might be worth revisiting if the current approach does not work well
- Use 2 accounts for sending auth and batch txs, this would allow reusing plain txmgr and queue constructs but feels like it would introduce quite a lot of changes elsewhere.
There was a problem hiding this comment.
Hey @piersy,
Thanks for cleaning up the doc comment it does look good to me now.
Regarding your bullet points:
- Serialize: We could also have support for this way (might get a bit messy), and enable it if the
SendPairAsyncproves problematic. - Single tx: Yea, it may be worth revisiting, however I would imagine the cancellation logic would not get triggered that frequently (at least i hope)
- Two accounts: Yes this might be quite a large overhaul, as we also use this key currently to sign espresso transactions. So I would need to then rethink the system and which keys to use when.
With a BatchAuthenticator address configured but no EspressoTime scheduled, fallback auth is unreachable, so the NumConfirmations headroom bound does not apply and must not block startup. A scheduled future activation still enforces it, since activation switches the send path mid-run. Addresses #458 (comment)
|
@piersy Seems I cannot merge this, do you mind doing it if all looks good? |
Relatively small change based on #445 - makes fallback batcher authenticate its transactions.
This is #448, re-hosted from an in-repo branch (now properly stacked).