SequencerRegistry
SequencerRegistry system contract — dual-role registry for system address and sequencer, with change scheduling and history.
This page specifies the SequencerRegistry system contract. It tracks two independent roles: the system address (Oracle/system-tx authority) and the sequencer (mini-block signing key). Each role has its own change lifecycle.
Motivation
MegaETH needs to decouple the Oracle/system-transaction sender from the mini-block signing key so that each can be changed independently without affecting the other. The SequencerRegistry provides a canonical on-chain source of truth for both roles, enabling:
On-chain verification of mini-block signatures via
currentSequencer()andsequencerAt().Dynamic Oracle authority via
currentSystemAddress(), replacing the hardcodedMEGA_SYSTEM_ADDRESS.Independent change of each role without redeploying contracts.
Specification
Address
0x6342000000000000000000000000000000000006Bytecode
A node MUST deploy the bytecode version corresponding to the active spec.
Version 1.0.0
Since: Rex5
Code hash: 0x63cd411a379be1c198613ef1d15c3058e7b0db4a5d07d4bcf07014af90040315
Deployed bytecode: 0x608060405234801561000f57... (full bytecode: SequencerRegistry-1.0.0.json, deployedBytecode field).
To verify the code hash, from the repository root:
A node MUST deploy the contract via raw state patch with initial storage seeded at deploy time. A node MUST NOT execute a constructor during deployment.
Version 2.0.0
Since: Rex6
Code hash: 0xabd7e8f1c8f0f9ca0346df585b65d9791a7e9ba5c431cc4eda5d2c2f1f5e9c43
Version 2.0.0 hardens sequencer rotation: scheduleNextSequencerChange gains a third parameter carrying the new sequencer key's EIP-712 possession proof, enforces a minimum scheduling-to-activation delay, and exposes the minRotationDelay() and rotationDigest() views. This is a breaking ABI change for the sequencer-role scheduling entry point (hence the major version bump); every other method keeps its version 1.0.0 ABI and behavior, with version() reporting 2.0.0.
At the Rex6 activation block, a node MUST upgrade an existing version 1.0.0 registry in place: swap the bytecode to version 2.0.0, write the new _minRotationDelay slot, and preserve every other storage slot (roles, pending changes, histories). A pending sequencer change scheduled under version 1.0.0 MUST still activate normally under version 2.0.0.
Deployed bytecode: 0x608060405234801561000f57... (full bytecode: SequencerRegistry-2.0.0.json, deployedBytecode field).
To verify the code hash, from the repository root:
Storage Layout
The storage layout is consensus-critical. Rust slot constants in mega-system-contracts MUST match this layout.
0
_currentSystemAddress
address
1
_currentSequencer
address
2
_admin
address
3
_pendingAdmin
address
4
_initialSystemAddress
address
5
_initialSequencer
address
6
_initialFromBlock
uint256
7
_pendingSystemAddress
address
8
_systemAddressActivationBlock
uint256
9
_pendingSequencer
address
10
_sequencerActivationBlock
uint256
11
_systemAddressHistory
ChangeRecord[]
12
_sequencerHistory
ChangeRecord[]
Each ChangeRecord MUST be packed as uint96 fromBlock followed by address addr in one 32-byte slot.
Version 2.0.0 appends one slot:
13
_minRotationDelay
uint256
Slot 13 exists only in version 2.0.0 and later; slots 0–12 are identical across versions.
Future versions of SequencerRegistry MUST only append new storage slots after slot 13. Future versions MUST NOT reorder existing slots or insert new slots before slot 14 once the contract is in use, because dynamic-array element keys are derived from keccak256(slot) and any slot change orphans the existing data.
Interface
Version 2.0.0 replaces the sequencer scheduling entry point and adds two views and two errors:
Every other method keeps its version 1.0.0 signature, and version() reports 2.0.0.
Read Methods
currentSystemAddress() MUST return the value in _currentSystemAddress. currentSequencer() MUST return the value in _currentSequencer. Both values MUST be seeded at deploy time and MUST be updated only by applyPendingChanges().
systemAddressAt(blockNumber) and sequencerAt(blockNumber) MUST return the role address active at the given block. They MUST revert with FutureBlock if blockNumber > block.number and BeforeInitialBlock if blockNumber < _initialFromBlock. Both roles MUST share the same _initialFromBlock.
Change Scheduling
Each role MUST have an independent schedule*Change(newAddress, activationBlock) method. To cancel a pending change, the caller MUST pass activationBlock = type(uint256).max and newAddress = address(0); the call MUST be handled as a special cancellation case before address and uint96 validation. For all other values, activationBlock MUST be strictly greater than block.number; otherwise the call MUST revert with InvalidActivationBlock(). activationBlock MUST also fit in uint96; if it exceeds type(uint96).max, the call MUST revert with ActivationBlockTooLarge(). At most one pending change per role MUST exist at a time; a new schedule MUST overwrite the previous one.
Sequencer Rotation Hardening
From version 2.0.0, the sequencer role additionally enforces the rules below; the system-address role keeps the two-parameter scheduleNextSystemAddressChange(newSystemAddress, activationBlock) signature and only the rules above.
Scheduling a sequencer change with an address whose private key nobody holds creates an unrecoverable liveness failure: at the activation block, mini-block production requires the new key's signature, and the admin transaction that could fix the registry itself requires block production. Version 2.0.0 closes this hole at the contract entry point with two independent guards on scheduleNextSequencerChange.
Minimum rotation delay. The activation block MUST satisfy activationBlock >= block.number + minRotationDelay(); otherwise the call MUST revert with RotationDelayTooShort(). This guarantees a reaction window between scheduling and activation in which a bad registration can be detected and cancelled. The delay value is read from the _minRotationDelay storage slot, which is seeded by the node at deploy/upgrade time and has no on-chain setter; changing it requires a bytecode upgrade.
Possession proof. newSequencerSignature MUST be a 65-byte r || s || v signature by the newSequencer key over the EIP-712 digest returned by rotationDigest(newSequencer, activationBlock); otherwise the call MUST revert with InvalidRotationProof(). The digest is computed per EIP-712 with:
Domain:
name = "MegaETH SequencerRegistry",version = "1",chainId = block.chainid,verifyingContract = address(this). The domain version is independent of the contract's semantic version and only changes if the signing scheme itself changes.Domain type hash:
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")=0x8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f.Struct type hash:
keccak256("SequencerRotation(address newSequencer,uint256 activationBlock)")=0xaebf84f20bcc14afc14200ed16cfb9314ef205d4d1d8fbcc0a256c39a3e79aa9.Digest:
keccak256(0x1901 || domainSeparator || keccak256(abi.encode(structTypeHash, newSequencer, activationBlock))).
The domain separator MUST be computed at call time (not cached at construction), because the contract is installed via raw state patch and no constructor runs.
Signature validation MUST reject, with InvalidRotationProof():
a signature whose length is not exactly 65 bytes;
a malleable signature with
s > secp256k1n / 2;a
vvalue outside{27, 28};a failed
ecrecover(recovered address zero);a recovered address different from
newSequencer.
Validation order. Checks MUST run cheapest-first: InvalidActivationBlock, the cancellation special case, ActivationBlockTooLarge, ZeroAddress, RotationDelayTooShort, and the possession proof last, so ecrecover only runs on otherwise-valid inputs.
Cancellation is exempt. The cancellation case (newSequencer = address(0), activationBlock = type(uint256).max) MUST NOT require a signature, so a rotation to a key that turns out to be lost can always be cancelled.
Replay protection. The signature commits to (chainId, verifyingContract, newSequencer, activationBlock) and needs no nonce:
Only the admin can call
scheduleNextSequencerChange, so a third party cannot replay a captured signature.A signature cannot be reused for a different
newSequenceroractivationBlock— the digest would differ.Replaying the identical
(newSequencer, activationBlock)pair merely reinstates the same intended rotation, which is harmless by construction.A stale signature naturally expires: once
block.number + minRotationDelay()exceeds itsactivationBlock, no schedule call can accept it.
Pre-Block Apply
applyPendingChanges() MUST be permissionless and MUST apply both roles atomically. The execution layer MUST call applyPendingChanges() as a pre-block system call when a pre-check confirms any role change is due. For each role, if a change is pending and due, applyPendingChanges() MUST update the current address, append to the change history, and clear pending state.
The system call MUST be issued with gas_limit = max(block.gas_limit, 30_000_000) instead of revm's upstream-fixed 30M default, matching the EIP-2935 / EIP-4788 pre-block calls. This gas floor is necessary because the slot-rotation cost scales with REX dynamic storage gas (SALT bucket capacity), and a fixed 30M is no longer guaranteed to be sufficient on activation blocks.
Apply/Deploy Commit Order
Pre-block outcomes commit in execution order, and the apply call always executes against the not-yet-committed pre-block state, so its recorded outcome carries the registry account info as of the start of the block. The execution layer MUST run the applyPendingChanges() pre-block call (including its due-check) before the registry deploy step, so the deploy outcome commits last. At an activation block where a version 1.0.0 registry exists, the deploy performs the in-place version 1.0.0 → 2.0.0 upgrade; an apply outcome committed after it would overwrite the upgraded account info with the stale pre-upgrade bytecode. On a chain whose registry never existed, the deploy installs version 2.0.0 directly and the order is not observable, but the same order MUST be used. The applyPendingChanges() logic is identical in versions 1.0.0 and 2.0.0 — the version 2.0.0 changes touch only rotation scheduling — so its semantics do not depend on which side of the deploy it executes.
Two-Step Admin Transfer
Admin handoff is a two-step process to prevent permanent loss of admin authority through a single mistyped, phished, or clipboard-substituted address.
The current admin calls
transferAdmin(newAdmin). The call MUST set_pendingAdmin = newAdminand emitAdminTransferStarted(currentAdmin, newAdmin). The current admin MUST remain in effect;admin()and all admin-only operations MUST be unaffected until step 2. Passingaddress(0)MUST cancel any previously pending transfer. Re-callingtransferAdminMUST overwrite_pendingAdmin.The pending admin calls
acceptAdmin(). This MUST be the only way_adminis updated. It MUST set_admin = msg.sender, clear_pendingAdmin, and emitAdminTransferred(oldAdmin, newAdmin). Any caller other than the current_pendingAdminMUST revert withNotPendingAdmin.
The acceptance step proves the new admin's keys are live and controlled. Until acceptance, the old admin MUST retain full authority and MAY re-target or cancel the pending transfer.
Interception
SequencerRegistry does NOT use call interception. All methods MUST run as normal on-chain bytecode.
Deploy-Time Seeding
A node MUST write 6 flat storage slots at first deploy: _currentSystemAddress, _currentSequencer, _admin, _initialSystemAddress, _initialSequencer, _initialFromBlock. The sequencer and admin addresses MUST come from SequencerRegistryConfig on the chain's hardfork configuration.
The initial system address is fixed. Both _currentSystemAddress and _initialSystemAddress MUST be seeded with the legacy MEGA_SYSTEM_ADDRESS constant. The genesis value MUST NOT be configurable on SequencerRegistryConfig. Until the registry is active, the payload executor, txpool, and replay paths all assume the system-transaction sender equals MEGA_SYSTEM_ADDRESS, so the initial value is seeded at the activation boundary rather than being freely configurable, to avoid silently breaking those invariants at chain bootstrap. Once the registry is active, the system address MAY be rotated via scheduleNextSystemAddressChange and applyPendingChanges.
Validation constraints: Both configurable address fields — rex5_initial_sequencer and rex5_initial_admin — MUST be non-zero. The chain configuration MUST reject either zero address before the first block using this registry is executed. A zero rex5_initial_admin would permanently lock all admin-only registry operations. A zero rex5_initial_sequencer produces an invalid initial sequencer state.
The rex5_ prefix on these field names is deliberate: the values seed the registry at the activation boundary of the spec that introduces it, and blocks before that boundary ignore them entirely.
The node MUST also seed the _minRotationDelay slot from SequencerRegistryRex6Config.rex6_min_rotation_delay on the chain's hardfork configuration:
On a fresh deploy of version 2.0.0 (registry never existed), the node MUST write all 6 bootstrap slots plus
_minRotationDelayand install the version 2.0.0 bytecode directly.On the in-place version 1.0.0 → 2.0.0 upgrade, the node MUST write only
_minRotationDelayand MUST leave every other slot untouched.
rex6_min_rotation_delay MUST be non-zero; the chain configuration MUST reject a zero value at load time, because a zero delay disables the reaction window the field exists to guarantee.
Constants
SEQUENCER_REGISTRY_ADDRESS
0x6342000000000000000000000000000000000006
Contract address
SEQUENCER_ROTATION_TYPEHASH
0xaebf84f20bcc14afc14200ed16cfb9314ef205d4d1d8fbcc0a256c39a3e79aa9
EIP-712 struct type hash for rotation proofs
EIP-712 domain name
MegaETH SequencerRegistry
Rotation-proof signing domain
EIP-712 domain version
1
Independent of the contract semantic version
Rationale
Why two roles instead of one? The Oracle/system-tx sender and the mini-block signing key are different operational concerns. Coupling them means changing one silently revokes the other, which would break Oracle authority on the first sequencer key change.
Why a pre-block system call for role changes? Applying a role change as a regular transaction would change role addresses mid-block, breaking block-stability.
Why deploy-time storage seeding instead of constant bootstrap? The _initialFromBlock depends on the Rex5 activation block number, which is not known at compile time. Seeding all initial values at deploy time keeps the bootstrap mechanism uniform.
Why require a possession proof at scheduling time? A sequencer rotation to a typo'd or otherwise unheld address deadlocks the chain at the activation block, and no on-chain recovery exists once block production halts. Requiring the new key to sign the exact rotation it authorizes moves the failure to the scheduling transaction, where it reverts harmlessly.
Why is _minRotationDelay config-seeded instead of a Solidity constant? A constant baked into the bytecode would force every network — including devnets and e2e test harnesses — to wait the same real wall-clock delay for a live rotation test. A seeded storage value keeps one canonical version 2.0.0 code hash across all networks while letting each network choose its own delay, exactly like the existing role seeding.
Why no nonce in the rotation proof? Scheduling is admin-gated, the signature binds the exact (chainId, contract, newSequencer, activationBlock) tuple, replaying the identical tuple is idempotent, and the minimum-delay check naturally expires stale signatures. A nonce would add a storage slot and a tooling burden without excluding any additional attack.
Spec History
Rex5 introduced the
SequencerRegistrycontract with dual roles, at version 1.0.0.Rex6 upgraded the contract to version 2.0.0 in place: sequencer rotation requires the new key's EIP-712 possession proof and a minimum scheduling-to-activation delay (
_minRotationDelay, slot 13), and the pre-block apply call now runs before the deploy step so the upgraded account info commits last.
Last updated