Upgrades
Every release — register the new verification key
Applies to any deployment that verifies real proofs (--sp1 true at deploy
time), whether or not the release changes a contract.
A batch is committed under the git sha of the binary that committed it, and
commitBatch rejects a commit hash the deployment holds no key for. Your
deployment only holds the key of the version that deployed it, so an upgraded
sequencer commits nothing until the new key is registered — every commit
reverts with MissingVerificationKeyForCommit() (0xf6b9798e), surfacing as:
Failed to send commitment for batch N ... execution reverted: 0xf6b9798e
The L2 keeps producing blocks throughout, so it reads as a stuck committer rather than a missing key. Nothing is lost: register the key and the pending batch commits on the next attempt.
rex l2 register-vk --commit <GIT_SHA> --vk <VERIFICATION_KEY> \
--on-chain-proposer <OCP> --timelock <TIMELOCK> --private-key <SECURITY_COUNCIL_PK>
The sha is the one the new binary prints after HEAD- in ethrex --version; the
key is the ethrex-riscv32im-succinct-zkvm-vk-bn254 file from the new release’s
ethrex-contracts.tar.gz. “Registering a new verification key” in
docs/l2/fundamentals/upgrades.md explains what the command does and why the
Timelock is in the path.
Keys are stored per commit hash, not per version, so the old key stays valid — which is what lets a rollback keep verifying.
From v7 to v8
Database migration (local node)
This migration applies to the L2 node database only (SQL-backed store). It does not change any on-chain contracts.
The messages table was renamed to l1_messages. Copy the data and then remove the old table:
INSERT INTO l1_messages
SELECT *
FROM messages;
Then delete the messages table.
From v8 to v9
Timelock upgrade (L1 contracts)
From ethrex v9 onwards, the Timelock contract manages access to the OnChainProposer (OCP). The OCP owner becomes the Timelock, and the deprecated authorizedSequencerAddresses mapping is replaced by Timelock roles.
What changes
- Sequencer permissions move to Timelock roles (
SEQUENCER). - Commit and verify transactions must target the Timelock, not the OCP.
- Governance and Security Council accounts control upgrades and emergency actions via the Timelock.
1) Deploy Timelock (proxy + implementation)
Deploy a Timelock proxy and implementation using your standard UUPS deployment flow. Record the proxy address; that is the address you will initialize and use later.
2) Initialize Timelock
Call the Timelock initializer on the proxy:
initialize(uint256 minDelay,address[] sequencers,address governance,address securityCouncil,address onChainProposer)
sequencersshould include the L1 committer and proof sender addresses (and any other accounts that should commit or verify).securityCouncilis typically the current OCP owner (ideally a multisig).governanceis the account that will schedule and execute timelocked upgrades.
3) Transfer OCP ownership to Timelock
From the current OCP owner, transfer ownership with transferOwnership(address).
Then accept ownership from the Timelock:
- Normal path: schedule and execute
acceptOwnership()through the Timelock (respectsminDelay). - Emergency path: the Security Council can call
emergencyExecuteon the Timelock with calldata foracceptOwnership()to accept immediately.
Note: acceptOwnership() must be executed by the Timelock (the pending owner), so it cannot be called directly from an EOA.
4) Configure the L2 node to use Timelock
This is required because the sequencer can no longer call the OCP directly once the Timelock is the owner. Set the Timelock address so commit and verify calls target the Timelock:
- CLI flag:
--l1.timelock-address <TIMELOCK_PROXY_ADDRESS> - Env var:
ETHREX_TIMELOCK_ADDRESS=<TIMELOCK_PROXY_ADDRESS>
The committer requires this address for non-based deployments, and the proof sender/verifier will use it when present.
Do this before restarting the sequencer after the ownership transfer. If the node keeps targeting the OCP after the transfer, commit/verify calls will revert (onlyOwner). If you point to the Timelock before the transfer, the Timelock will forward but the OCP will still reject it because the Timelock is not the owner yet.
5) Verify the migration
- OCP
owner()returns the Timelock address. - Sequencer addresses return
trueforhasRole(SEQUENCER, <addr>)on the Timelock. - The L2 node logs show commit/verify txs sent to the Timelock.
Database migration (local node)
This migration applies to the L2 node database only (SQL-backed store). It does not change any on-chain contracts.
The balance_diffs table added a new value_per_token column of type BLOB:
ALTER TABLE balance_diffs
ADD COLUMN value_per_token BLOB;
From v9 to v10
CommonBridge: L2 gas limit stored on-chain
From v10 onwards, the L2 block gas limit is stored in the CommonBridge contract as l2GasLimit. The sequencer fetches this value on startup instead of using a CLI flag.
Upgrade requirement
On existing deployments, l2GasLimit will default to 0 because initialize() has already run. This means _sendToL2 will revert for any non-zero gas limit, bricking the bridge until the owner calls setL2GasLimit().
After upgrading (with the contract paused), call setL2GasLimit before unpausing (see Updating the gas limit below). The contract must not be unpaused until l2GasLimit is set to a valid value. Otherwise all deposits and privileged transactions will revert.
CLI flag removed
The --block-producer.block-gas-limit flag has been removed. The sequencer now reads the gas limit from the CommonBridge contract on startup. Update any scripts or deployment configurations that use this flag.
Viewing the current gas limit
rex call <BRIDGE_ADDRESS> "l2GasLimit()" --rpc-url <L1_RPC_URL>
Updating the gas limit
Only the bridge owner can update the gas limit:
rex send <BRIDGE_ADDRESS> "setL2GasLimit(uint256)" <NEW_GAS_LIMIT> \
--private-key <OWNER_PRIVATE_KEY> \
--rpc-url <L1_RPC_URL>
After updating the on-chain value, restart the sequencer for it to take effect. Until the restart, the sequencer continues using the previous gas limit, which may cause a temporary mismatch with the on-chain value.
From v29 to v30
SP1 6 (OnChainProposer and SP1 verifier)
The SP1 prover moves from SP1 5 to SP1 6.9.0. Its proofs need the v6.1.0 SP1VerifierGroth16, and the OnChainProposer gains upgradeSP1Verifier(address) to point at it. Deployments that require SP1 proofs need both: the SP1 5 verifier they were deployed with rejects SP1 6 proofs whatever verification key is registered, because SP1 6 wraps its proofs in a different Groth16 circuit. The OnChainProposer has not changed since v25, so deployments from v25 onwards follow the same steps.
The OnChainProposer reads a single verifier address, so the contract steps go between two points: after every batch committed by the old version is verified, since its proofs only pass the old verifier, and before the new version submits its first proof. The v30 OnChainProposer enforces the first point: the call in step 2 reverts with UnverifiedBatchesPending while a batch is unverified. The nodes can move either without downtime or with a stop.
Contract steps
-
Deploy
SP1VerifierGroth16from sp1-contractsv6.1.0and the v30 OnChainProposer implementation. -
Through the Timelock, upgrade the OnChainProposer and point it at the new verifier in a single call,
upgradeToAndCall(<new implementation>, <calldata of upgradeSP1Verifier(<new verifier>)>). Usescheduleandexecute, oremergencyExecutefrom the Security Council. For example, with the Security Council key:UPGRADE=$(cast calldata 'upgradeToAndCall(address,bytes)' <NEW_IMPLEMENTATION> \ $(cast calldata 'upgradeSP1Verifier(address)' <NEW_VERIFIER>)) cast send <TIMELOCK_ADDRESS> 'emergencyExecute(address,uint256,bytes)' \ <ON_CHAIN_PROPOSER_ADDRESS> 0 "$UPGRADE" \ --private-key <SECURITY_COUNCIL_PK> --rpc-url <L1_RPC_URL> -
Through the Timelock, call
upgradeSP1VerificationKey(bytes32,bytes32)with the keccak of the v30 commit and theethrex-riscv32im-succinct-zkvm-vk-bn254key from the v30 release. The v30 sequencer cannot commit a batch before its key is registered. -
Check
SP1_VERIFIER_ADDRESS()andverificationKeys(<commit hash>, 1)on the OnChainProposer.
Without downtime
The v30 sequencer runs next to the old one, follows it over p2p, and takes over block production at a chosen block. Only one prover version can serve a batch, and a sequencer that stops sequencing also stops sending proofs, so the old version’s batches are verified before the handover:
-
Stop the old sequencer’s committer with
GET /committer/stopon its admin server. It keeps producing blocks and sending the old prover’s proofs. -
Wait until
lastVerifiedBatchequalslastCommittedBatch, then stop the old prover. -
Run the contract steps.
-
Start the v30 sequencer with its own data directory, ports and proof coordinator, and:
--bootnodes <enode of the old sequencer>, which the old sequencer logs at startup asLocal node initialized enode=...(its RPC only servesadmin_nodeInfowhenadminis in--http.api);--admin.start-at <N>, a block far enough ahead for it to catch up;--admin.l2-head-check-rpc-url <old sequencer RPC>.
Start the v30 SP1 prover against the v30 sequencer’s proof coordinator.
-
Send
POST /state-updater/stop-at/<N>to the old sequencer’s admin server. It produces blockNand stops; the v30 sequencer produces the next block. -
Move RPC traffic to the v30 sequencer, since the old one stays at block
N, then stop the old sequencer.
Block production does not pause. No new batch is committed from step 1 until the v30 sequencer commits its first one, which holds the blocks the old sequencer produced after step 1; the batches already committed keep being verified meanwhile.
With a stop
- Stop the committer and wait until
lastVerifiedBatchequalslastCommittedBatch, then stop the old sequencer and prover. - Run the contract steps.
- Start the v30 sequencer and prover with the existing data directories; the node migrates its database at startup.
With Succinct’s SP1 verifier gateway
If SP1_VERIFIER_ADDRESS() is Succinct’s SP1VerifierGateway, the gateway routes SP1 6 proofs to its v6.1.0 verifier, so contract steps 1 and 2 are not needed: register the v30 key (step 3) and move the nodes. The gateway froze its v5.0.0 route, though, so batches committed with SP1 5 and not verified before the freeze can no longer be verified. They are reverted and committed again by v30:
- Stop the old committer and the old prover. Note
lastVerifiedBatchasVandlastCommittedBatchasC; if they are equal, go to step 4. - Check that
batchWithdrawalLogsMerkleRoots(n)on the CommonBridge is zero for every batch fromV + 1toC.CommonBridgerefuses to publish withdrawals twice for one batch number andrevertBatchdoes not clear them, so a batch that published withdrawals cannot be committed again. If any root is non-zero, stop here and do not revert: there is no way to recover that batch until the contracts clear the roots on revert (#7414). - Through the Timelock, call
pause(),revertBatch(V + 1)andunpause()on the OnChainProposer. - Register the v30 key (contract step 3).
- Move the nodes:
- With a stop: stop the old sequencer, run
ethrex l2 revert-batch <V> <ON_CHAIN_PROPOSER_ADDRESS> --datadir <L2 datadir>, which keeps the batch it is given and deletes the later ones, delete thecheckpoint_batch_<n>directories fornaboveVin the data directory, and start v30 on it. - Without downtime: hand over as above, from step 4. The v30 sequencer commits the batches the old one sealed again, generating its own prover input for each.
- With a stop: stop the old sequencer, run
The SP1 version upgrade test runs both.
Prover
The SP1 GPU prover now runs on a local sp1-gpu-server that the SDK downloads, so --sp1-server is ignored; see the SP1 prover guide for GPU persistence mode and host memory.
Aligned mode is not available in v30: the Aligned SDK still verifies SP1 5 proofs.
From v30 to v31
L2→L1 intents (CommonBridge and CommonBridgeL2)
v31 adds value-free L2→L1 intents: an L2 contract calls sendIntentToL1(payloadHash, consumerOnL1) on CommonBridgeL2, and once the batch holding it is verified, consumerOnL1 consumes it on L1 with verifyAndConsume on CommonBridge. New deployments have them from genesis. Existing deployments get them by upgrading both bridges to their v31 implementations. Neither upgrade adds storage or needs an initialization call, and there is no database migration.
The bridge upgrades do not depend on the node upgrade: they can run before or after the nodes move to v31, with or without downtime. The L2 one goes through a privileged transaction, so a sequencer has to be running to include it.
-
CommonBridge (L1). Deploy the v31
CommonBridgeimplementation, then upgrade the proxy from the bridge owner:rex send <BRIDGE_ADDRESS> 'upgradeToAndCall(address,bytes)' <NEW_BRIDGE_IMPLEMENTATION> 0x \ --private-key <BRIDGE_OWNER_PK> --rpc-url <L1_RPC_URL> -
CommonBridgeL2 (L2 proxy at
0x000000000000000000000000000000000000ffff). Deploy the v31CommonBridgeL2implementation on L2, then have the L1 bridge upgrade the L2 proxy:rex send <BRIDGE_ADDRESS> 'upgradeL2Contract(address,address,uint256,bytes)' \ 0x000000000000000000000000000000000000ffff <NEW_BRIDGE_L2_IMPLEMENTATION> 1000000 0x \ --private-key <BRIDGE_OWNER_PK> --rpc-url <L1_RPC_URL> -
Check that each proxy’s ERC-1967 implementation slot (
0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc) holds the new implementation, on L1 forCommonBridgeand on L2 for0x…ffff, and thatINTENT_DOMAIN()returns the same value on both.
As in every release, also register the v31 verification key.