Skip to content

feat(L1): bind aggregate proofs to ProtocolVersions schedule#359

Open
PelleKrab wants to merge 20 commits into
mainfrom
proof-schedule-binding
Open

feat(L1): bind aggregate proofs to ProtocolVersions schedule#359
PelleKrab wants to merge 20 commits into
mainfrom
proof-schedule-binding

Conversation

@PelleKrab

Copy link
Copy Markdown
Contributor

Summary

Builds on #353.

Pins each AggregateVerifier game to the ProtocolVersions.scheduleId() that is active when the game
is initialized, and includes that pinned value in the TEE and ZK proof journals.

  • scheduleId snapshot: games read PROTOCOL_VERSIONS.scheduleId() once during initialization and
    store it as scheduleId
  • proof binding: TEE and ZK journal hashes now commit to the pinned schedule ID, so proofs are
    bound to the upgrade schedule that existed when the game was created
  • deployment wiring: standard and dev multiproof deployment flows now pass the ProtocolVersions
    proxy into AggregateVerifier
  • interface/tests: IAggregateVerifier exposes PROTOCOL_VERSIONS and scheduleId; tests cover
    that later schedule changes do not alter an initialized game's pinned value

Test plan

  • forge test --match-path test/L1/proofs/AggregateVerifier.t.sol
  • forge test --match-path test/L1/proofs/DisputeGameFactory.t.sol
  • forge test --match-path test/L1/OptimismPortal2.t.sol
  • forge test --match-path 'test/L1/proofs/*.t.sol'
  • forge fmt --check

PelleKrab and others added 15 commits June 25, 2026 18:10
Add the Security Council-controlled upgrade activation schedule contract,
its interface, and tests. Maintains an ordered registry of upgrades and their
L2 activation timestamps, committed via scheduleId.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…o conventions

Replace the history-dependent rolling hash with a per-upgrade cumulative hash
chain reproducible from (l2ChainId, address, ordered keys, current timestamps).
When a timestamp changes, only the affected suffix is recomputed (O(n-j) bubble-up)
rather than hashing the full state on every mutation.

- Add MIN_NOTICE (1 hour) guard on setTimestamp; expose it in the interface
- Require non-zero protocolVersion; use _protocolVersions as existence sentinel,
  removing the separate _upgradeIndex mapping
- Move all errors and events into IProtocolVersions; add MIN_NOTICE to interface
- Replace assembly in _keyFromUpgradeId with bytes32(raw) Solidity cast
- Rename constructor params to leading-underscore convention
- Restructure test file into grouped contracts with snake_case naming,
  external visibility, and @notice on each test function per repo style

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Returns the full ordered upgrade schedule in a single eth_call — name,
activation timestamp, protocol version, and cumulative scheduleId per entry.
Name is recovered from the bytes32 key at read time via _nameFromKey,
avoiding a separate storage mapping.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Replace the per-call keccak256(abi.encode(l2ChainId, address(this)))
in _refreshScheduleId with a cached immutable _seed set once in the
constructor, saving a hash + encode on every schedule mutation.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Reflects bytecode change from caching hash chain seed as immutable.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…s control

- Replace Ownable with ProxyAdminOwnedBase; drop explicit _owner constructor
  param and ProtocolVersions_ZeroOwner error
- Replace per-upgrade protocolVersion mapping with a single latestProtocolVersion
  uint256 updated on each registerUpgrade call
- Remove redundant storage vars: lastUpdatedAtBlock, l2ChainId immutable,
  _scheduleId (scheduleId() now reads the tip of _upgradeScheduleId directly)
- Remove redundant view functions: upgradeCount, upgradeIdAt, upgradeIds,
  getProtocolVersion (getSchedule() covers all registry reads)
- Inline _registerUpgrade into registerUpgrade and _applyTimestamp into
  setTimestamp; inline onlyChainTeam guard into delayTimestamp
- Drop ScheduleIdUpdated old-scheduleId param; enforce MIN_NOTICE in delayTimestamp

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…y pattern

Replace the string-based upgrade registry (bytes32 key from packed upgradeId)
with a purely numeric scheme: each upgrade gets an ascending uint256 id equal
to its registration index. Human-readable names are kept off-chain; the id is
permanent and stable because the registry is strictly append-only.

Convert the constructor to an initializer so the contract can be deployed
behind an OP proxy. _seed is now stored (not immutable) so it can be set
inside initialize() against the proxy's address(this), binding the schedule
commitment to the proxy address that callers interact with.

Replace the mapping-based storage (_registered, _timestamps, _upgradeScheduleId
keyed by bytes32) with parallel arrays indexed by id, simplifying the hash
chain and eliminating the three-mapping structure. Drop getTimestamp(string)
and the duplicate-registration guard; add setLatestProtocolVersion() so the
informational field can be updated independently of registrations.

Add LatestProtocolVersionUpdated and ScheduleIdUpdated events; update the
interface to inherit IProxyAdminOwnedBase and IReinitializableBase.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Seven findings from a multi-angle review; fixes applied:

* emit LatestProtocolVersionUpdated in registerUpgrade — previously only
  setLatestProtocolVersion fired the event, so indexers missed every version
  bump that came through registration.

* Guard scheduleId() before initialization — returns bytes32(0) while _seed
  is unset, which could be picked up as a valid schedule commitment; now
  reverts with ProtocolVersions_NotInitialized.

* Dedicate ProtocolVersions_InsufficientNotice for setTimestamp — the MIN_NOTICE
  check in setTimestamp reused DelayMustBeLater with current=0 as the floor,
  which was misleading; a new error carries only the offending timestamp.

* Split delayTimestamp compound condition — the combined || condition reported
  current as the floor even when only the MIN_NOTICE leg fired; each constraint
  now reverts independently with the correct floor value.

* Extract _setTimestamp internal — setTimestamp and delayTimestamp duplicated
  the activation-already-passed check and the write+emit+refresh triple;
  _setTimestamp holds those shared invariants and each public function keeps
  only its distinct preconditions.

* Drop redundant zero-write in registerUpgrade — push(bytes32(0)) wrote zero
  to a slot that _refreshScheduleId immediately overwrites; replaced with
  push() (no argument) to save one SSTORE.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
…elper, init guard

Rename latestProtocolVersion → minimumProtocolVersion throughout (state variable,
event, setter, interface, tests). The field represents the minimum version nodes
must run, not an informational latest; the old name implied recommendation rather
than a floor.

Remove the protocolVersion parameter from registerUpgrade. Upgrade registration is
purely a scheduling concern (assign id, extend hash chain); the minimum version is
set independently via setMinimumProtocolVersion. Drops the InvalidProtocolVersion
check from registerUpgrade and removes the protocolVersion field from the
UpgradeRegistered event.

Extract _writeTimestamp(id, newTs) as the shared write helper for setTimestamp and
delayTimestamp. Each public function owns its complete validation sequence;
_writeTimestamp is a pure write (store + emit TimestampSet + refreshScheduleId).
Eliminates the mixed-responsibility _setTimestamp that owned some validation but
not all, which caused the ActivationAlreadyPassed check to appear in two places.

Guard registerUpgrade against pre-initialization calls. _seed is zero between
upgradeTo and initialize; any registerUpgrade call in that window would compute
hash chain links from a zero seed, permanently desynchronizing scheduleId from its
reproducible derivation (l2ChainId, address(this)). Revert with NotInitialized
when _seed == 0, consistent with the existing guard in scheduleId().

Regenerate ABI, storage layout, and semver-lock snapshots.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
Deploy the ProtocolVersions impl + per-chain proxy as part of SystemDeploy
and initialize it via initialize(l2ChainId), rather than excluding it from
initializer tracking. Adds it to Types.Implementations/DeployOutput, artifact
saves, getImplementations, and _assertValidImplementations. Tags the contract
@Custom:proxied true so the initializer test verifies both impl and proxy, and
wires it into Setup (via getAddress for fork safety). Updates the ProtocolVersions
sourceCodeHash in semver-lock accordingly.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
@PelleKrab PelleKrab force-pushed the proof-schedule-binding branch from 641b312 to 0964d31 Compare July 9, 2026 17:07
@PelleKrab PelleKrab force-pushed the dynamic-hardfork-protocol-id branch from 0570814 to 7cb1c5d Compare July 9, 2026 17:18
@PelleKrab PelleKrab force-pushed the proof-schedule-binding branch from 0964d31 to 9c49406 Compare July 9, 2026 17:20
…no-op setTimestamp

Apply @jackchuma review nits on ProtocolVersions:
- Rename the chainTeam role to incidentResponder; delaying a bad hardfork
  activation is an incident-response action. NatSpec scopes the power so the
  shared name doesn't imply SuperchainConfig's pause authority.
- Drop the redundant chainTeam deploy config key and source the role from
  superchainConfigIncidentResponder (same entity).
- setTimestamp: short-circuit no-op writes ahead of the change-validation
  guards so idempotent resubmissions succeed.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
@PelleKrab PelleKrab force-pushed the dynamic-hardfork-protocol-id branch from 7cb1c5d to e1ebc49 Compare July 9, 2026 17:39
@PelleKrab PelleKrab force-pushed the proof-schedule-binding branch from 9c49406 to a09ecb3 Compare July 9, 2026 17:39
Run ProtocolVersions tests through CommonTest so they use the SystemDeploy-created proxy and implementation. Reorder ProtocolVersions members to match the Solidity style guide and restrict implementation internals to private.

Co-authored-by: Codex <codex-noreply@coinbase.com>
@PelleKrab PelleKrab force-pushed the proof-schedule-binding branch from a09ecb3 to a3fbbb6 Compare July 9, 2026 21:41
PelleKrab and others added 3 commits July 10, 2026 10:51
Events before errors, and mutating externals before view getters, mirroring
the ProtocolVersions contract body and the repo style-guide ordering. ABI is
generator-sorted, so no snapshot or semver-lock impact.

Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Codex <codex-noreply@coinbase.com>
@PelleKrab PelleKrab force-pushed the proof-schedule-binding branch from a3fbbb6 to 90bf7d3 Compare July 10, 2026 20:09
Base automatically changed from dynamic-hardfork-protocol-id to main July 10, 2026 23:41
@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/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 1
Sum 2

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.

2 participants