Skip to content

feat(l1): derive rollup version on-chain from chain id and address - #25537

Draft
AztecBot wants to merge 3 commits into
nextfrom
cb/rollup-version-from-address
Draft

AztecBot wants to merge 3 commits into
nextfrom
cb/rollup-version-from-address

Conversation

@AztecBot

@AztecBot AztecBot commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Refs AztecProtocol/aztec-claude#2043

Problem

The rollup version identifies a rollup instance. It is used in four places:

  • the tx_context signed into every transaction;
  • Inbox / Outbox message scoping (recipient.version, sender.version);
  • the Registry.versionToRollup key;
  • the epoch (root rollup) proof public inputs.

But the rollup never computed it. RollupCore took it as a constructor argument (VERSION = _config.version), and the deploy script filled it in as uint32(bytes4(keccak256(abi.encode(config, genesisState)))). As a result:

  • two rollups deployed on the same L1 with the same config and genesis state got the same version;
  • anyone deploying a Rollup could pass any version.

The only uniqueness check was Registry.addRollup rejecting a duplicate, and that only covers rollups in the same registry.

Change

As its first statement, RollupCore's constructor computes:

VERSION = uint32(bytes4(keccak256(abi.encode(block.chainid, address(this), _config, _genesisState))));

The preimage keeps the config and genesis state the deploy script already hashed, and adds the chain id and the rollup's own address.

It sits at the top of the constructor rather than where VERSION used to be assigned. _initializeRewards writes the freshly deployed booster address into _config.rewardConfig.booster in memory when it was supplied as zero, which is the default. Hashing after that point would cover the mutated config instead of the one supplied.

version is removed from RollupConfigInput, and Inbox / Outbox are constructed with VERSION.

Full extent of the change

File Change
src/core/RollupCore.sol compute VERSION at the top of the constructor from chain id, address(this), config and genesis state; pass VERSION to Inbox/Outbox
src/core/interfaces/IRollup.sol drop uint32 version from RollupConfigInput (ABI change to the constructor struct)
src/governance/Registry.sol doc comment describes the derivation
script/deploy/RollupConfiguration.sol delete _computeConfigVersion and the lines assigning config.version
test/harnesses/TestConstants.sol delete the lines assigning config.version (and the TMNT-139 TODO about computing it at deployment)
test/builder/RollupBuilder.sol delete the line copying version
test/staking/initiateWithdrawByAttester.t.sol delete the line assigning config.version
test/RollupVersion.t.sol (new) version equals the formula over chain id, address, config and genesis, and matches Inbox/Outbox; two identically configured rollups sharing one Registry get distinct versions and can both be registered

Every removal is a read or write of the deleted version field, or the helper that computed it. No other code is touched.

No circuit, constants or TS changes are needed. aztec-node reads the version back with rollup.getVersion() after the forge deploy (DeployRollupLib serialises rollupVersion from the deployed contract), and passes config to forge via env vars rather than ABI-encoding RollupConfigInput.

Knock-on changes outside this PR

  • v6 deploy script (aztec-packages#25496). DeployRollupForUpgradeV6.s.sol ends _config() with config.version = uint32(bytes4(keccak256(abi.encode(config, _genesisState(_c)))));. That assignment and its comment block have to be deleted, or that branch won't compile once version leaves the struct. Its verify() checks (inbox.VERSION() == rollup.getVersion() and so on) still hold as written.
  • aztec-node e2e. end-to-end/src/multi-node/governance/add_rollup.test.ts has a comment saying the new rollup's genesis archive root must differ, or the version collides. That's no longer true. The test still passes, but the comment should be updated.

Impact and what this does not fix

This is hygiene, not a fix for an exploitable issue on the canonical rollup. L1 consumers resolve the rollup by address or through the Registry, which rejects duplicate versions. A proof or tx replayed onto a clone only affects the clone's own state.

The version also stays 32 bits (uint32 on L1, a JS number in the node), so a deployer grinding about 2³² CREATE2 salts can still land on another rollup's version. Closing that needs either a wider version or also binding the full rollup address in the root rollup public inputs. That's why this PR uses Refs, not Closes, for the issue.

Testing

Run locally from l1-contracts, with ConstantsGen.sol generated and a local stub generated/HonkVerifier.sol used only so the deploy scripts compile (it is gitignored and not committed):

  • forge build of src + test: compiles.
  • forge fmt --check on the touched files: clean.
  • forge test on the new RollupVersion.t.sol plus the staking, DeployRollupForUpgrade, registry, AddRollup, portals, fee portal, Inbox, Outbox and Rollup suites: all pass.

The full suite, gas snapshots and e2e are left to CI.


Created by claudebox · group: slackbot · requested by Mike (@iAmMichaelConnor) · Slack thread

The rollup version was a constructor argument, computed by the deploy script
as a hash of the config and genesis state. Two rollups deployed on the same
chain with the same config therefore shared a version, and any deployer could
pass an arbitrary one.

RollupCore now computes it in the constructor as the first 4 bytes of
keccak256(abi.encode("aztec_rollup_version", block.chainid, address(this))),
and `version` is removed from RollupConfigInput. The version already feeds the
tx context, Inbox/Outbox message scoping, the Registry key and the epoch proof
public inputs, so each of those becomes deployment-specific.
@AztecBot AztecBot added ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claudebox Owned by claudebox. it can push to this PR. labels Sep 24, 2026
The version now hashes the chain id, the rollup address, the config and the
genesis state, computed at the top of the constructor so it covers the config
as supplied.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-draft Run CI on draft PRs. ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure claudebox Owned by claudebox. it can push to this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant