Skip to content

fix(L1): freeze schedule changes within sequencer drift of activation - #409

Merged
jackchuma merged 4 commits into
mainfrom
jack/freeze-schedule-before-activation
Aug 20, 2026
Merged

fix(L1): freeze schedule changes within sequencer drift of activation#409
jackchuma merged 4 commits into
mainfrom
jack/freeze-schedule-before-activation

Conversation

@jackchuma

@jackchuma jackchuma commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What changed? Why?

  • Freeze changes to an existing ProtocolVersions activation during the 30-minute post-Fjord sequencer-drift window. setTimestamp and delayTimestamp now reject mutations at or after activationTimestamp - FREEZE_WINDOW, preventing L1 from changing the schedule after an L2 block may already have activated it.
  • Apply the existing one-hour MIN_NOTICE floor to non-zero registerUpgrade timestamps, so appending an upgrade cannot change the activated schedule for L2 timestamps that are already reachable.
  • Allow ProtocolVersions to import a chain's existing upgrade schedule only during initialization. Standard and multiproof deployments source the optional protocolVersionsInitialSchedule deploy-config field and pass it into the initializer; omitted config retains an empty schedule.
  • Update the ProtocolVersions ABI and semver snapshots for the initializer, freeze-window getter, and new frozen-activation error.

Notes to reviewers

  • Initial schedule import preserves the existing schedule-id hash chain and ordering checks, while keeping past activation timestamps unavailable after deployment.
  • Base mainnet schedule-id golden tests now exercise the import path.

How has it been tested?

  • ProtocolVersions unit tests cover the exact freeze boundary, both mutation paths, registration notice enforcement, imported schedules, and the Base mainnet schedule-id golden values.
  • DeployConfig and SystemDeploy tests cover parsing, defaulting, validation, and deployment with an initial schedule.
  • Full suite passed locally (1,220 tests); ABI and semver-lock snapshots were regenerated.

`ProtocolVersions` allowed the owner to clear or move an activation, and the
incident responder to delay one, right up until L1 time reached the activation
timestamp. Post-Fjord batch validation lets an L2 block carry a timestamp up to
1800s ahead of its L1 origin, so for the final 30 minutes before an activation
the sequencer can already have produced the block that activates the upgrade
while L1 still allows that activation to be cancelled or moved.

Adds FREEZE_WINDOW = 30 minutes and requires a preexisting activation to still
be more than that away before its timestamp can change, in both `setTimestamp`
and `delayTimestamp`. This enforces onchain the operator rule that an upgrade
timestamp is not touched close to activation.

Co-authored-by: Cursor <cursoragent@cursor.com>
@cb-heimdall

cb-heimdall commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

✅ Heimdall Review Status

Requirement Status More Info
Reviews 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

jackchuma and others added 3 commits August 19, 2026 18:22
The freeze added in the previous commit only covered the paths that change an
activation already on the schedule. `registerUpgrade` writes its first timestamp
directly, so appending an upgrade activating inside the sequencer-drift horizon
still moved `activatedScheduleId` for L2 timestamps the sequencer may already
have produced, and registration accepted timestamps arbitrarily far in the past.

Registration now clears the same MIN_NOTICE floor `setTimestamp` applies, which
exceeds FREEZE_WINDOW and so covers the drift horizon. Registering without a
timestamp stays unconstrained, since `activatedScheduleId` skips zero entries.

Importing a schedule that is already in the past is a real need for a chain with
hardfork history, so `initialize` now takes the initial schedule. That confines
unconstrained writes to deployment, before any proof game can have pinned a
commitment from the registry, and the Base mainnet goldens confirm the import
builds the same hash chain the equivalent registrations did.

Co-authored-by: Cursor <cursoragent@cursor.com>
The initializer's schedule import was unreachable from the deploy scripts,
which always passed an empty array. Since activations already in the past
cannot be registered afterwards and the AggregateVerifier binds the registry
immutably, a chain with existing hardfork history would have been stuck with
an empty registry short of redeploying the proof stack.

Take the schedule as a deploy input, sourced from an optional
`protocolVersionsInitialSchedule` config key.

Co-authored-by: Cursor <cursoragent@cursor.com>
The zero-timestamp exemption from MIN_NOTICE is already exercised by every
test that registers an unscheduled upgrade, so the standalone case added no
coverage beyond assertions made elsewhere.

Co-authored-by: Cursor <cursoragent@cursor.com>
@jackchuma
jackchuma merged commit 4f7acda into main Aug 20, 2026
6 checks passed
@jackchuma
jackchuma deleted the jack/freeze-schedule-before-activation branch August 20, 2026 13:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants