perf(l1): hold the rollup config in immutables instead of storage - #25314
Open
spalladino wants to merge 5 commits into
Open
perf(l1): hold the rollup config in immutables instead of storage#25314spalladino wants to merge 5 commits into
spalladino wants to merge 5 commits into
Conversation
spalladino
force-pushed
the
spl/rollup-config-immutable
branch
from
August 25, 2026 19:56
e23f0c0 to
5dd1645
Compare
spalladino
force-pushed
the
spl/rollup-config-immutable
branch
from
August 25, 2026 21:55
b792779 to
08920ae
Compare
This was referenced Aug 25, 2026
Every field of `RollupConfig` is written exactly once, in the Rollup's constructor, and has no setter. Move all eight into immutables and drop `config` from `RollupStore`. Libraries cannot read a contract's immutables -- and the ExtLibs are external, so they cannot either -- so the values are assembled into a memory `RollupConfig` and threaded down: the full struct into the epoch proof path, the Inbox into propose (bundled with `checkBlob` into a `ProposeConfig` to stay under the stack limit), and the fee asset into the reward claims. `config` was the last member of `RollupStore`, so `tips`, `archives` and `tempCheckpointLogs` keep their slots and the cheatcode offsets that depend on them are unaffected. The TS client read `vkTreeRoot` and `protocolContractsHash` out of raw storage; it now uses the contract getters. Gas: propose -1,893, submitEpochRootProof -10,807 (avg, no-validators benchmark).
`Rollup.validateHeaderWithAttestations` decoded seven parameters, built a `ValidateHeaderArgs` (which embeds a full `ProposedHeader`) in memory, resolved the mana min fee through two delegatecall hops, then re-encoded four arguments for the ExtLib call. All of that lived in the Rollup's own runtime code. Forward the parameters straight through and assemble the struct in `RollupOperationsExtLib` instead. Frees 630 bytes of Rollup runtime bytecode, bringing the EIP-170 margin back to 725 from the 96 the immutables change left it at. The mana min fee is computed identically: `RewardExtLib.summedMinFee` and `.getManaMinFeeComponentsAt` are themselves one-line forwarders to `FeeLib.summedMinFee` and `ProposeLib.getManaMinFeeComponentsAt`.
Mirrors the benchmark refresh. The config getters drop their cold SLOAD (getInbox 2,543 -> 878, getVersion 1,448 -> 852, getOutbox 2,521 -> 856), propose falls 267,116 -> 265,187 and submitEpochRootProof 386,659 -> 376,201, matching the gas_benchmark deltas. A handful of unrelated views move by 22-44 gas as the Rollup's selector dispatch shifts. Call counts on some entries drop by 27 because the three tests that only fail under FORGE_GAS_REPORT (testExtraBlobs, testRevertInvalidCoinbase, testRevertInvalidTimestamp, all failing identically on the base commit) abort at a slightly different point. Per-call gas for those entries is unchanged.
solhint's no-unused-import is an error in .solhint.json, and two imports went stale: `ValidateHeaderArgs` in Rollup.sol, once assembling that struct moved into RollupOperationsExtLib, and `RollupStore` in RollupCore.sol, once `config` left the struct and RollupCore stopped touching storage directly. RollupCore had been re-exporting `RollupStore` to its importers, so Rollup.sol and RollupWithPreheating.sol now take it straight from IRollup.sol instead.
spalladino
force-pushed
the
spl/rollup-config-immutable
branch
from
August 28, 2026 18:51
08920ae to
807803a
Compare
spalladino
changed the base branch from
spl/a-1390-inbox-ring-overwrite-protection
to
project/fast-inbox
August 28, 2026 18:51
Collaborator
Flakey Tests🤖 says: This CI run detected 1 tests that failed, but were tolerated due to a .test_patterns.yml entry. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the bottom of the Fast Inbox stack: it targets
project/fast-inboxdirectly and is independently mergeable — nothing in it depends on the inbox work above it.Context
Every field of
RollupConfig—vkTreeRoot,protocolContractsHash,version,feeAsset,feeAssetPortal,epochProofVerifier,inbox,outbox— is written exactly once in the Rollup's constructor and has no setter anywhere insrc/. They were nonetheless kept in storage, so every read paid a coldSLOAD.proposepaid one,submitEpochRootProofpaid six.Approach
Move all eight into immutables and drop
configfromRollupStore.Libraries cannot read a contract's immutables, and the
*ExtLiblibraries here areexternal(delegatecalled), so they cannot either. The values are assembled into a memoryRollupConfigbyRollupCore._getRollupConfig()and threaded down as parameters: the full struct into the epoch-proof path, theIInboxinto propose, and the fee asset into the reward claims. Propose needed itsIInboxbundled with the existingcheckBlobflag into aProposeConfigstruct — a seventh scalar parameter pushedProposeLib.proposeover the stack limit, and a memory struct costs one slot instead of two.configwas the last member ofRollupStore, sotips,archivesandtempCheckpointLogskeep slots 0–2 and every raw-slot consumer ofkeccak256("aztec.stf.storage")is unaffected — all of them use offsets ≤ +2. The two that used+3/+4wereRollupContract.getVkTreeRootandgetProtocolContractsHashin the TS client, which now call the contract getters thatIRolluphas exposed since #22563.The second commit is bytecode budget, not gas. Each immutable read inlines a 32-byte push, which grew
Rollup's runtime code to within 148 bytes of the EIP-170 limit.Rollup.validateHeaderWithAttestationswas decoding seven parameters, building aValidateHeaderArgs(which embeds a fullProposedHeader) in memory, resolving the mana min fee through two delegatecall hops, and re-encoding four arguments — all in the Rollup's own runtime code. Forwarding the parameters straight through and assembling the struct inRollupOperationsExtLibfrees 629 bytes. The fee value is unchanged:RewardExtLib.summedMinFeeand.getManaMinFeeComponentsAtare one-line forwarders to theFeeLib/ProposeLibfunctions the ExtLib now calls directly.Gas
propose(no validators)submitEpochRootProof(no validators)propose(100 validators)submitEpochRootProof(100 validators)aggregate3(100 validators)The config getters lose their cold
SLOADoutright —getInbox2,543 → 878,getVersion1,447 → 852,getOutbox2,521 → 856. A handful of unrelated views move by 22–44 gas as the Rollup's selector dispatch shifts. Deployment also drops eightSSTOREs.Rollupruntime bytecode ends at 23,799 against the 24,576 limit — 777 bytes of margin, against 886 before this change.Note on the regenerated
gas_report.json: call counts on a few entries drop by 36 because the three tests that fail only underFORGE_GAS_REPORT(testExtraBlobs,testRevertInvalidCoinbase,testRevertInvalidTimestamp— all failing identically on the base commit) abort at a slightly different point. Per-call gas for those entries is unchanged.Node compatibility
Node binaries already in the field read
vkTreeRootandprotocolContractsHashfrom raw slots+3/+4. Against a rollup deployed from this branch those slots are zero, so such a node'swaitForCompatibleRollupreports a VK mismatch and sits in standby. #25313 lands the getter-based read on the v5 line so binaries cut from it work against a rollup deployed from either version; it does nothing for binaries already released, so this needs sequencing against any node rollout.