From faecf86ba4ae892a2bf61a469ff2a03dfe5beb85 Mon Sep 17 00:00:00 2001 From: PelleKrab Date: Wed, 8 Jul 2026 13:09:20 -0700 Subject: [PATCH] feat(L1): bind aggregate proofs to ProtocolVersions schedule Co-authored-by: Codex --- interfaces/L1/proofs/IAggregateVerifier.sol | 3 ++ scripts/deploy/SystemDeploy.s.sol | 7 +++-- scripts/multiproof/DeployDevBase.s.sol | 11 ++++++- snapshots/abi/AggregateVerifier.json | 31 +++++++++++++++++++ snapshots/semver-lock.json | 4 +-- .../storageLayout/AggregateVerifier.json | 7 +++++ src/L1/proofs/AggregateVerifier.sol | 24 ++++++++++++-- test/L1/OptimismPortal2.t.sol | 3 +- test/L1/proofs/AggregateVerifier.t.sol | 29 ++++++++++++++++- test/L1/proofs/BaseTest.t.sol | 10 +++++- test/L1/proofs/DisputeGameFactory.t.sol | 3 +- 11 files changed, 120 insertions(+), 12 deletions(-) diff --git a/interfaces/L1/proofs/IAggregateVerifier.sol b/interfaces/L1/proofs/IAggregateVerifier.sol index dd588e4ec..125fc3aa4 100644 --- a/interfaces/L1/proofs/IAggregateVerifier.sol +++ b/interfaces/L1/proofs/IAggregateVerifier.sol @@ -5,6 +5,7 @@ import { IDisputeGame } from "./IDisputeGame.sol"; import { IDisputeGameFactory } from "./IDisputeGameFactory.sol"; import { IDelayedWETH } from "./IDelayedWETH.sol"; import { IVerifier } from "./IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { Proposal, Hash } from "src/libraries/bridge/Types.sol"; import { Timestamp } from "src/libraries/bridge/LibUDT.sol"; @@ -23,6 +24,7 @@ interface IAggregateVerifier is IDisputeGame { function ZK_RANGE_HASH() external view returns (bytes32); function ZK_AGGREGATE_HASH() external view returns (bytes32); function CONFIG_HASH() external view returns (bytes32); + function PROTOCOL_VERSIONS() external view returns (IProtocolVersions); function L2_CHAIN_ID() external view returns (uint256); function BLOCK_INTERVAL() external view returns (uint256); function INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); @@ -35,6 +37,7 @@ interface IAggregateVerifier is IDisputeGame { function counteredByIntermediateRootIndexPlusOne() external view returns (uint256); function expectedResolution() external view returns (Timestamp); function proofCount() external view returns (uint8); + function scheduleId() external view returns (bytes32); function initializeWithInitData(bytes calldata proof) external payable; function verifyProposalProof(bytes calldata proofBytes) external; diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index d12aa6c08..e8e1a19cf 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -129,6 +129,7 @@ contract SystemDeploy is Script { uint256 l2ChainId; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; + IProtocolVersions protocolVersions; } struct MultiproofOutput { @@ -1048,7 +1049,8 @@ contract SystemDeploy is Script { multiproofConfigHash: _input.multiproofConfigHash, l2ChainId: _opChainInput.l2ChainId, multiproofBlockInterval: _input.multiproofBlockInterval, - multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval + multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval, + protocolVersions: _output.protocolVersionsProxy }) ); @@ -1077,7 +1079,8 @@ contract SystemDeploy is Script { _input.multiproofConfigHash, _input.l2ChainId, _input.multiproofBlockInterval, - _input.multiproofIntermediateBlockInterval + _input.multiproofIntermediateBlockInterval, + _input.protocolVersions ) ) ); diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index 31d009270..dd4cc4a06 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -16,6 +16,8 @@ import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; import { TEEVerifier } from "src/L1/proofs/tee/TEEVerifier.sol"; @@ -104,6 +106,12 @@ abstract contract DeployDevBase is Script { AggregateVerifier.ZkHashes memory zkHashes = AggregateVerifier.ZkHashes({ rangeHash: cfg.zkRangeHash(), aggregateHash: cfg.zkAggregationHash() }); + Proxy protocolVersionsProxy = new Proxy(msg.sender); + protocolVersionsProxy.upgradeToAndCall( + address(new ProtocolVersions()), abi.encodeCall(IProtocolVersions.initialize, (address(0))) + ); + protocolVersionsProxy.changeAdmin(address(0xdead)); + aggregateVerifier = address( new AggregateVerifier( gameType, @@ -116,7 +124,8 @@ abstract contract DeployDevBase is Script { cfg.multiproofConfigHash(), cfg.l2ChainId(), _blockInterval(), - _intermediateBlockInterval() + _intermediateBlockInterval(), + IProtocolVersions(address(protocolVersionsProxy)) ) ); diff --git a/snapshots/abi/AggregateVerifier.json b/snapshots/abi/AggregateVerifier.json index d8ee465e9..6b5d6357c 100644 --- a/snapshots/abi/AggregateVerifier.json +++ b/snapshots/abi/AggregateVerifier.json @@ -67,6 +67,11 @@ "internalType": "uint256", "name": "intermediateBlockInterval", "type": "uint256" + }, + { + "internalType": "contract IProtocolVersions", + "name": "protocolVersions", + "type": "address" } ], "stateMutability": "nonpayable", @@ -215,6 +220,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "PROTOCOL_VERSIONS", + "outputs": [ + { + "internalType": "contract IProtocolVersions", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "SLOW_FINALIZATION_DELAY", @@ -681,6 +699,19 @@ "stateMutability": "pure", "type": "function" }, + { + "inputs": [], + "name": "scheduleId", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "startingBlockNumber", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index c61a63c25..65173a13f 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -32,8 +32,8 @@ "sourceCodeHash": "0xf122a50487efe9bd5a620262ba20ef4adbca14eeec2af7fd32e6e16739001596" }, "src/L1/proofs/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x90629c679e821a70553d899a68e818bcddb672a004a0d2e09078d17028a2f082", - "sourceCodeHash": "0x4835d3de998b2951898f0e427c61b0c330ac3feb5f7a8dc9fd0deb78912a9b7a" + "initCodeHash": "0x6f3c53245830b9c72c8199695e93959d5ed3b0cb8352e5b50b3dfa28183ac787", + "sourceCodeHash": "0x1f2e2b07ebc4359fa31ea7400ace9bab0e4f6099d8ff3ff7b6a2b14cccfb89ca" }, "src/L1/proofs/AnchorStateRegistry.sol:AnchorStateRegistry": { "initCodeHash": "0x6f3afd2d0ef97a82ca3111976322b99343a270e54cd4a405028f2f29c75f7fb1", diff --git a/snapshots/storageLayout/AggregateVerifier.json b/snapshots/storageLayout/AggregateVerifier.json index 08fd716cb..1226af645 100644 --- a/snapshots/storageLayout/AggregateVerifier.json +++ b/snapshots/storageLayout/AggregateVerifier.json @@ -96,5 +96,12 @@ "offset": 8, "slot": "7", "type": "uint8" + }, + { + "bytes": "32", + "label": "scheduleId", + "offset": 0, + "slot": "8", + "type": "bytes32" } ] \ No newline at end of file diff --git a/src/L1/proofs/AggregateVerifier.sol b/src/L1/proofs/AggregateVerifier.sol index a468dc058..fd447fe6a 100644 --- a/src/L1/proofs/AggregateVerifier.sol +++ b/src/L1/proofs/AggregateVerifier.sol @@ -26,6 +26,7 @@ import { ReentrancyGuard } from "lib/solady/src/utils/ReentrancyGuard.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { //////////////////////////////////////////////////////////////// @@ -113,6 +114,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The game type ID. GameType internal immutable GAME_TYPE; + /// @notice The ProtocolVersions upgrade schedule contract. + IProtocolVersions public immutable PROTOCOL_VERSIONS; + //////////////////////////////////////////////////////////////// // State Vars // //////////////////////////////////////////////////////////////// @@ -162,6 +166,11 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The number of proofs provided. uint8 public proofCount; + /// @notice The ProtocolVersions scheduleId snapshotted at game initialization. + /// @dev Pinned once at init; every proof in this game commits to this value, binding it to the + /// upgrade schedule in effect when the game was created. + bytes32 public scheduleId; + //////////////////////////////////////////////////////////////// // Events // //////////////////////////////////////////////////////////////// @@ -259,6 +268,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @param l2ChainId The chain ID of the L2 network. /// @param blockInterval The block interval. /// @param intermediateBlockInterval The intermediate block interval. + /// @param protocolVersions The ProtocolVersions upgrade schedule contract. constructor( GameType gameType_, IAnchorStateRegistry anchorStateRegistry_, @@ -270,7 +280,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { bytes32 configHash, uint256 l2ChainId, uint256 blockInterval, - uint256 intermediateBlockInterval + uint256 intermediateBlockInterval, + IProtocolVersions protocolVersions ) { // Block interval and intermediate block interval must be positive and divisible. if (blockInterval == 0 || intermediateBlockInterval == 0 || blockInterval % intermediateBlockInterval != 0) { @@ -291,6 +302,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { L2_CHAIN_ID = l2ChainId; BLOCK_INTERVAL = blockInterval; INTERMEDIATE_BLOCK_INTERVAL = intermediateBlockInterval; + PROTOCOL_VERSIONS = protocolVersions; INITIALIZE_CALLDATA_SIZE = 0x8E + 0x20 * intermediateOutputRootsCount(); } @@ -364,6 +376,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Set the game as initialized. initialized = true; + // Snapshot the scheduleId from ProtocolVersions. Every proof in this game commits to this + // value, pinning them to the upgrade schedule in effect at the game's creation block. + scheduleId = PROTOCOL_VERSIONS.scheduleId(); + // Set the game's starting timestamp. createdAt = Timestamp.wrap(uint64(block.timestamp)); @@ -899,7 +915,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - TEE_IMAGE_HASH + TEE_IMAGE_HASH, + scheduleId ) ); @@ -933,7 +950,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { endingL2SequenceNumber, intermediateRoots, CONFIG_HASH, - ZK_RANGE_HASH + ZK_RANGE_HASH, + scheduleId ) ); diff --git a/test/L1/OptimismPortal2.t.sol b/test/L1/OptimismPortal2.t.sol index bf93e25f4..04e8d1df5 100644 --- a/test/L1/OptimismPortal2.t.sol +++ b/test/L1/OptimismPortal2.t.sol @@ -90,7 +90,8 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { bytes32(uint256(4)), deploy.cfg().l2ChainId(), 100, - 10 + 10, + protocolVersions ); disputeGameFactory.setImplementation(respectedGameType, IDisputeGame(address(gameImpl))); disputeGameFactory.setInitBond(respectedGameType, 0); diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index 30907b20e..a6d34f7fb 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -11,6 +11,7 @@ import { Claim, Timestamp } from "src/libraries/bridge/LibUDT.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { LibClone } from "lib/solady/src/utils/LibClone.sol"; @@ -36,6 +37,31 @@ contract AggregateVerifierTest is BaseTest { _createAndAssertInitializedGame("zk-proof", AggregateVerifier.ProofType.ZK, ZK_PROVER, address(0), ZK_PROVER); } + /// @notice init snapshots the live scheduleId into storage and pins it: a later schedule change + /// must not alter the game's snapshot (proves it is stored once, not read live). + /// @dev Only covers the stored getter. That the snapshot flows into the proof journal is not + /// asserted here — MockVerifier ignores the journal, so journal binding is untestable. + function test_initialize_pinsScheduleId_succeeds() public { + // Register an upgrade so the schedule commitment is non-trivial (non-zero). + protocolVersions.registerUpgrade(uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 100, 1); + bytes32 pinned = protocolVersions.scheduleId(); + assertTrue(pinned != bytes32(0)); + + Claim rootClaim = _advanceL2BlockAndClaim(); + bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); + + AggregateVerifier game = _createAggregateVerifierGame( + TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proof + ); + + assertEq(game.scheduleId(), pinned); + + // Move the live schedule after creation; the game's snapshot must stay pinned. + protocolVersions.registerUpgrade(uint64(block.timestamp) + protocolVersions.MIN_NOTICE() + 200, 2); + assertNotEq(protocolVersions.scheduleId(), pinned); + assertEq(game.scheduleId(), pinned); + } + function testInitializeFailsIfInvalidCallDataSize() public { Claim rootClaim = _advanceL2BlockAndClaim(); @@ -417,7 +443,8 @@ contract AggregateVerifierTest is BaseTest { CONFIG_HASH, L2_CHAIN_ID, blockInterval, - intermediateBlockInterval + intermediateBlockInterval, + IProtocolVersions(address(protocolVersions)) ); } } diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index 2016de959..87e6c96f5 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -19,6 +19,8 @@ import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { IVerifier } from "interfaces/L1/proofs/IVerifier.sol"; +import { ProtocolVersions } from "src/L1/ProtocolVersions.sol"; +import { IProtocolVersions } from "interfaces/L1/IProtocolVersions.sol"; import { MockVerifier } from "test/mocks/MockVerifier.sol"; @@ -54,6 +56,7 @@ contract BaseTest is Test { MockVerifier internal teeVerifier; MockVerifier internal zkVerifier; + ProtocolVersions internal protocolVersions; function setUp() public virtual { _deployContractsAndProxies(); @@ -78,9 +81,12 @@ contract BaseTest is Test { proxyAdmin = new ProxyAdmin(address(this)); + ProtocolVersions _protocolVersions = new ProtocolVersions(); + anchorStateRegistry = AnchorStateRegistry(_deployProxy(address(_anchorStateRegistry))); factory = DisputeGameFactory(_deployProxy(address(_factory))); delayedWETH = DelayedWETH(payable(_deployProxy(address(_delayedWETH)))); + protocolVersions = ProtocolVersions(_deployProxy(address(_protocolVersions))); teeVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); zkVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); @@ -103,6 +109,7 @@ contract BaseTest is Test { ); factory.initialize(address(this)); delayedWETH.initialize(systemConfig); + protocolVersions.initialize(address(0)); } function _deployAndSetAggregateVerifier() internal { @@ -117,7 +124,8 @@ contract BaseTest is Test { CONFIG_HASH, L2_CHAIN_ID, BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL + INTERMEDIATE_BLOCK_INTERVAL, + IProtocolVersions(address(protocolVersions)) ); factory.setImplementation(GameTypes.AGGREGATE_VERIFIER, IDisputeGame(address(aggregateVerifierImpl))); diff --git a/test/L1/proofs/DisputeGameFactory.t.sol b/test/L1/proofs/DisputeGameFactory.t.sol index 6cab78c26..768fff7ce 100644 --- a/test/L1/proofs/DisputeGameFactory.t.sol +++ b/test/L1/proofs/DisputeGameFactory.t.sol @@ -230,7 +230,8 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { bytes32(uint256(4)), L2_CHAIN_ID, AGGREGATE_BLOCK_INTERVAL, - AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL + AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL, + protocolVersions ); _setGame(address(gameImpl), GameTypes.AGGREGATE_VERIFIER);