diff --git a/deploy-config/local.json b/deploy-config/local.json index eec8c2b1f..c38098f96 100644 --- a/deploy-config/local.json +++ b/deploy-config/local.json @@ -17,11 +17,13 @@ "l2GenesisBlockGasLimit": "0x17D7840", "l2OutputOracleStartingBlockNumber": 1, "l2OutputOracleStartingTimestamp": 1, - "multiproofBlockInterval": 100, + "multiproofSlowBlockInterval": 100, "multiproofConfigHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "multiproofFastBlockInterval": 1000, + "multiproofFastIntermediateBlockInterval": 100, "multiproofGameType": 621, "multiproofGenesisBlockNumber": 0, - "multiproofIntermediateBlockInterval": 10, + "multiproofSlowIntermediateBlockInterval": 10, "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "nitroValidator": "0x0000000000000000000000000000000000000000", "operatorFeeVaultMinimumWithdrawalAmount": "0x8ac7230489e80000", diff --git a/interfaces/L1/proofs/IAggregateVerifier.sol b/interfaces/L1/proofs/IAggregateVerifier.sol index 9e3039953..1bb74be3a 100644 --- a/interfaces/L1/proofs/IAggregateVerifier.sol +++ b/interfaces/L1/proofs/IAggregateVerifier.sol @@ -29,8 +29,11 @@ interface IAggregateVerifier is IDisputeGame { function L2_GENESIS_BLOCK_NUMBER() external view returns (uint256); function L2_GENESIS_TIMESTAMP() external view returns (uint64); function L2_BLOCK_TIME() external view returns (uint64); - function BLOCK_INTERVAL() external view returns (uint256); - function INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); + function SLOW_BLOCK_INTERVAL() external view returns (uint256); + function SLOW_INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); + function FAST_BLOCK_INTERVAL() external view returns (uint256); + function FAST_INTERMEDIATE_BLOCK_INTERVAL() external view returns (uint256); + function intervalsForStartingBlock(uint256 startingBlock) external view returns (uint256, uint256); function startingOutputRoot() external view returns (Proposal memory); function bondRecipient() external view returns (address); diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index 975749864..42e1bbcc8 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -51,10 +51,12 @@ contract DeployConfig is Script { uint256 public l2GenesisTimestamp; uint256 public l2OutputOracleStartingBlockNumber; uint256 public l2OutputOracleStartingTimestamp; - uint256 public multiproofBlockInterval; + uint256 public multiproofSlowBlockInterval; + uint256 public multiproofFastBlockInterval; + uint256 public multiproofFastIntermediateBlockInterval; uint256 public multiproofGameType; uint256 public multiproofGenesisBlockNumber; - uint256 public multiproofIntermediateBlockInterval; + uint256 public multiproofSlowIntermediateBlockInterval; uint256 public operatorFeeVaultMinimumWithdrawalAmount; uint256 public operatorFeeVaultWithdrawalNetwork; uint256 public proofMaturityDelaySeconds; @@ -115,10 +117,12 @@ contract DeployConfig is Script { l2OutputOracleStartingTimestamp = _json.readUint("$.l2OutputOracleStartingTimestamp"); l2GenesisBlockNumber = _json.readUintOr("$.l2GenesisBlockNumber", 0); l2GenesisTimestamp = _json.readUintOr("$.l2GenesisTimestamp", 0); - multiproofBlockInterval = _json.readUintOr("$.multiproofBlockInterval", 100); + multiproofSlowBlockInterval = _json.readUintOr("$.multiproofSlowBlockInterval", 100); + multiproofFastBlockInterval = _json.readUintOr("$.multiproofFastBlockInterval", 1000); + multiproofFastIntermediateBlockInterval = _json.readUintOr("$.multiproofFastIntermediateBlockInterval", 100); multiproofGameType = _json.readUintOr("$.multiproofGameType", 621); multiproofGenesisBlockNumber = _json.readUintOr("$.multiproofGenesisBlockNumber", 0); - multiproofIntermediateBlockInterval = _json.readUintOr("$.multiproofIntermediateBlockInterval", 10); + multiproofSlowIntermediateBlockInterval = _json.readUintOr("$.multiproofSlowIntermediateBlockInterval", 10); operatorFeeVaultMinimumWithdrawalAmount = _json.readUint("$.operatorFeeVaultMinimumWithdrawalAmount"); operatorFeeVaultWithdrawalNetwork = _json.readUint("$.operatorFeeVaultWithdrawalNetwork"); proofMaturityDelaySeconds = _json.readUintOr("$.proofMaturityDelaySeconds", 0); diff --git a/scripts/deploy/SystemDeploy.s.sol b/scripts/deploy/SystemDeploy.s.sol index b196fc42f..0923c411c 100644 --- a/scripts/deploy/SystemDeploy.s.sol +++ b/scripts/deploy/SystemDeploy.s.sol @@ -79,8 +79,10 @@ contract SystemDeploy is Script { uint256 multiproofGameType; address nitroValidator; AggregateVerifier.ScheduleConfig scheduleConfig; - uint256 multiproofBlockInterval; - uint256 multiproofIntermediateBlockInterval; + uint256 multiproofSlowBlockInterval; + uint256 multiproofSlowIntermediateBlockInterval; + uint256 multiproofFastBlockInterval; + uint256 multiproofFastIntermediateBlockInterval; ISP1Verifier sp1Verifier; address teeProposer; address teeChallenger; @@ -128,8 +130,10 @@ contract SystemDeploy is Script { bytes32 multiproofConfigHash; uint256 l2ChainId; AggregateVerifier.ScheduleConfig scheduleConfig; - uint256 multiproofBlockInterval; - uint256 multiproofIntermediateBlockInterval; + uint256 multiproofSlowBlockInterval; + uint256 multiproofSlowIntermediateBlockInterval; + uint256 multiproofFastBlockInterval; + uint256 multiproofFastIntermediateBlockInterval; } struct MultiproofOutput { @@ -267,8 +271,10 @@ contract SystemDeploy is Script { multiproofGameType: cfg.multiproofGameType(), nitroValidator: cfg.nitroValidator(), scheduleConfig: _configuredScheduleConfig(), - multiproofBlockInterval: cfg.multiproofBlockInterval(), - multiproofIntermediateBlockInterval: cfg.multiproofIntermediateBlockInterval(), + multiproofSlowBlockInterval: cfg.multiproofSlowBlockInterval(), + multiproofSlowIntermediateBlockInterval: cfg.multiproofSlowIntermediateBlockInterval(), + multiproofFastBlockInterval: cfg.multiproofFastBlockInterval(), + multiproofFastIntermediateBlockInterval: cfg.multiproofFastIntermediateBlockInterval(), sp1Verifier: ISP1Verifier(cfg.sp1Verifier()), teeProposer: cfg.teeProposer(), teeChallenger: cfg.teeChallenger(), @@ -1055,8 +1061,10 @@ contract SystemDeploy is Script { multiproofConfigHash: _input.multiproofConfigHash, l2ChainId: _opChainInput.l2ChainId, scheduleConfig: scheduleConfig, - multiproofBlockInterval: _input.multiproofBlockInterval, - multiproofIntermediateBlockInterval: _input.multiproofIntermediateBlockInterval + multiproofSlowBlockInterval: _input.multiproofSlowBlockInterval, + multiproofSlowIntermediateBlockInterval: _input.multiproofSlowIntermediateBlockInterval, + multiproofFastBlockInterval: _input.multiproofFastBlockInterval, + multiproofFastIntermediateBlockInterval: _input.multiproofFastIntermediateBlockInterval }) ); @@ -1084,8 +1092,12 @@ contract SystemDeploy is Script { AggregateVerifier.ZkHashes(_input.zkRangeHash, _input.zkAggregationHash), _input.multiproofConfigHash, _input.l2ChainId, - _input.multiproofBlockInterval, - _input.multiproofIntermediateBlockInterval, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: _input.multiproofSlowBlockInterval, + slowIntermediateBlockInterval: _input.multiproofSlowIntermediateBlockInterval, + fastBlockInterval: _input.multiproofFastBlockInterval, + fastIntermediateBlockInterval: _input.multiproofFastIntermediateBlockInterval + }), _input.scheduleConfig ) ) @@ -1128,12 +1140,13 @@ contract SystemDeploy is Script { require(address(_input.sp1Verifier) != address(0), "SystemDeploy: sp1Verifier not set"); DeployUtils.assertValidContractAddress(_input.nitroValidator); DeployUtils.assertValidContractAddress(address(_input.sp1Verifier)); - require(_input.multiproofBlockInterval != 0, "SystemDeploy: multiproof block interval not set"); + require(_input.multiproofSlowBlockInterval != 0, "SystemDeploy: multiproof block interval not set"); require( - _input.multiproofIntermediateBlockInterval != 0, "SystemDeploy: multiproof intermediate interval not set" + _input.multiproofSlowIntermediateBlockInterval != 0, + "SystemDeploy: multiproof intermediate interval not set" ); require( - _input.multiproofBlockInterval % _input.multiproofIntermediateBlockInterval == 0, + _input.multiproofSlowBlockInterval % _input.multiproofSlowIntermediateBlockInterval == 0, "SystemDeploy: invalid multiproof block intervals" ); require(_input.teeProposer != address(0), "SystemDeploy: teeProposer not set"); diff --git a/scripts/multiproof/DeployDevBase.s.sol b/scripts/multiproof/DeployDevBase.s.sol index 5e2f89208..7b097f3c3 100644 --- a/scripts/multiproof/DeployDevBase.s.sol +++ b/scripts/multiproof/DeployDevBase.s.sol @@ -128,8 +128,7 @@ abstract contract DeployDevBase is Script { zkHashes, cfg.multiproofConfigHash(), cfg.l2ChainId(), - _blockInterval(), - _intermediateBlockInterval(), + _intervalConfig(), AggregateVerifier.ScheduleConfig({ protocolVersions: IProtocolVersions(address(protocolVersionsProxy)), genesisBlockNumber: cfg.l2GenesisBlockNumber(), @@ -159,8 +158,7 @@ abstract contract DeployDevBase is Script { console.log("Deployment saved to:", outPath); } - function _blockInterval() internal pure virtual returns (uint256); - function _intermediateBlockInterval() internal pure virtual returns (uint256); + function _intervalConfig() internal pure virtual returns (AggregateVerifier.IntervalConfig memory); function _initBond() internal pure virtual returns (uint256); function _outputSuffix() internal pure virtual returns (string memory); function _deployTEERegistryImpl() internal virtual returns (address); diff --git a/scripts/multiproof/DeployDevNoNitro.s.sol b/scripts/multiproof/DeployDevNoNitro.s.sol index 3eb209552..a84acf448 100644 --- a/scripts/multiproof/DeployDevNoNitro.s.sol +++ b/scripts/multiproof/DeployDevNoNitro.s.sol @@ -8,22 +8,27 @@ import { INitroValidator } from "interfaces/L1/proofs/tee/INitroValidator.sol"; import { DevTEEProverRegistry } from "test/mocks/MockDevTEEProverRegistry.sol"; import { MockNitroValidator } from "test/mocks/MockNitroValidator.sol"; +import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; + import { DeployDevBase } from "./DeployDevBase.s.sol"; /// @title DeployDevNoNitro /// @notice Development deployment using DevTEEProverRegistry, which bypasses AWS Nitro attestation /// validation. See scripts/multiproof/README.md for usage. Not for production. contract DeployDevNoNitro is DeployDevBase { - uint256 public constant BLOCK_INTERVAL = 100; - uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 public constant SLOW_BLOCK_INTERVAL = 100; + uint256 public constant SLOW_INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 public constant FAST_BLOCK_INTERVAL = 1000; + uint256 public constant FAST_INTERMEDIATE_BLOCK_INTERVAL = 100; uint256 public constant INIT_BOND = 0.001 ether; - function _blockInterval() internal pure override returns (uint256) { - return BLOCK_INTERVAL; - } - - function _intermediateBlockInterval() internal pure override returns (uint256) { - return INTERMEDIATE_BLOCK_INTERVAL; + function _intervalConfig() internal pure override returns (AggregateVerifier.IntervalConfig memory) { + return AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: FAST_INTERMEDIATE_BLOCK_INTERVAL + }); } function _initBond() internal pure override returns (uint256) { diff --git a/scripts/multiproof/DeployDevWithNitro.s.sol b/scripts/multiproof/DeployDevWithNitro.s.sol index 1ef4fe01f..3562dc0bf 100644 --- a/scripts/multiproof/DeployDevWithNitro.s.sol +++ b/scripts/multiproof/DeployDevWithNitro.s.sol @@ -7,6 +7,8 @@ import { IDisputeGameFactory } from "interfaces/L1/proofs/IDisputeGameFactory.so import { INitroValidator } from "interfaces/L1/proofs/tee/INitroValidator.sol"; import { TEEProverRegistry } from "src/L1/proofs/tee/TEEProverRegistry.sol"; +import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; + import { DeployDevBase } from "./DeployDevBase.s.sol"; /// @title DeployDevWithNitro @@ -17,18 +19,21 @@ import { DeployDevBase } from "./DeployDevBase.s.sol"; /// then set `nitroValidator` in the deploy config. AWS Nitro attestations are only valid /// for 60 minutes, and the certificate chain must be cached before registerSigner() is called. contract DeployDevWithNitro is DeployDevBase { - uint256 public constant BLOCK_INTERVAL = 600; - uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 30; + uint256 public constant SLOW_BLOCK_INTERVAL = 600; + uint256 public constant SLOW_INTERMEDIATE_BLOCK_INTERVAL = 30; + uint256 public constant FAST_BLOCK_INTERVAL = 6000; + uint256 public constant FAST_INTERMEDIATE_BLOCK_INTERVAL = 300; uint256 public constant INIT_BOND = 0.00001 ether; address public nitroValidatorAddr; - function _blockInterval() internal pure override returns (uint256) { - return BLOCK_INTERVAL; - } - - function _intermediateBlockInterval() internal pure override returns (uint256) { - return INTERMEDIATE_BLOCK_INTERVAL; + function _intervalConfig() internal pure override returns (AggregateVerifier.IntervalConfig memory) { + return AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: FAST_INTERMEDIATE_BLOCK_INTERVAL + }); } function _initBond() internal pure override returns (uint256) { diff --git a/scripts/multiproof/README.md b/scripts/multiproof/README.md index 786b7bc78..be8f1e6af 100644 --- a/scripts/multiproof/README.md +++ b/scripts/multiproof/README.md @@ -172,7 +172,9 @@ Games are created using `ProofType.ZK` with the `MockVerifier` (deployed by both ### Step 1: Set the anchor state -Pick an anchor block far enough behind the L2 tip to cover all the games you want to create. Each game covers `BLOCK_INTERVAL` (600) L2 blocks, so for 500 games you need 300,000 blocks of headroom. +Pick an anchor block far enough behind the L2 tip to cover all the games you want to create. Each game covers one block interval of L2 blocks — 600 on slow (pre-Cobalt) blocks, 6,000 on fast (post-Cobalt) ones — so for 500 slow-block games you need 300,000 blocks of headroom. + +`SeedGames.s.sol` reads the interval off the deployed `AggregateVerifier` and picks the side that matches the anchor block, so nothing needs changing here for Cobalt. The contract exposes the two pairs as `SLOW_BLOCK_INTERVAL` / `SLOW_INTERMEDIATE_BLOCK_INTERVAL` and `FAST_BLOCK_INTERVAL` / `FAST_INTERMEDIATE_BLOCK_INTERVAL`. `generate-roots.sh` cannot see the chain, so pass `BLOCK_INTERVAL=6000 INTERMEDIATE_BLOCK_INTERVAL=300` when seeding a devnet on which Cobalt is already active; seeding will abort on a mismatched roots file rather than create bad games. ```bash # Calculate an anchor block 300,000 blocks behind the L2 tip diff --git a/scripts/multiproof/SeedGames.s.sol b/scripts/multiproof/SeedGames.s.sol index e04121dbd..9081e4626 100644 --- a/scripts/multiproof/SeedGames.s.sol +++ b/scripts/multiproof/SeedGames.s.sol @@ -26,10 +26,6 @@ import { MockAnchorStateRegistry } from "./mocks/MockAnchorStateRegistry.sol"; /// All transactions must confirm within the 256-block blockhash window of the /// L1 origin captured at simulation time. For large counts, use --slow. contract SeedGames is Script { - /// @notice Must match the AggregateVerifier deployment constants from DeployDevWithNitro/NoNitro. - uint256 public constant BLOCK_INTERVAL = 600; - uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 30; - uint256 public constant INTERMEDIATE_ROOTS_COUNT = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL; uint32 public constant GAME_TYPE_ID = 621; uint256 public constant PROGRESS_LOG_INTERVAL = 100; @@ -38,6 +34,8 @@ contract SeedGames is Script { GameType gameType; uint256 initBond; uint256 anchorBlock; + uint256 slowBlockInterval; + uint256 intermediateRootsCount; bytes32[] roots; bytes proof; } @@ -56,6 +54,8 @@ contract SeedGames is Script { console.log("Roots file:", rootsPath); console.log("Game count:", gameCount); console.log("Game type:", uint256(GAME_TYPE_ID)); + console.log("Block interval:", ctx.slowBlockInterval); + console.log("Intermediate roots per game:", ctx.intermediateRootsCount); console.log("Init bond per game:", ctx.initBond); console.log("Anchor block:", ctx.anchorBlock); console.log("Total ETH required:", ctx.initBond * gameCount); @@ -64,8 +64,8 @@ contract SeedGames is Script { (address firstGame, address lastGame) = _createGames(ctx, asrAddr); vm.stopBroadcast(); - uint256 l2Start = ctx.anchorBlock + BLOCK_INTERVAL; - uint256 l2End = ctx.anchorBlock + BLOCK_INTERVAL * gameCount; + uint256 l2Start = ctx.anchorBlock + ctx.slowBlockInterval; + uint256 l2End = ctx.anchorBlock + ctx.slowBlockInterval * gameCount; console.log(""); console.log("=== Seeding Complete ==="); @@ -93,10 +93,20 @@ contract SeedGames is Script { ctx.initBond = ctx.factory.initBonds(ctx.gameType); (, ctx.anchorBlock) = MockAnchorStateRegistry(asrAddr).getAnchorRoot(); + // Read the proposal geometry off the deployment rather than restating it. AggregateVerifier + // carries both the slow- and fast-block pairs and picks per game, so a hardcoded pair here + // would silently seed unopenable games on a devnet with Cobalt scheduled. + // ponytail: resolved once from the anchor, so a chain seeded straight across the activation + // would be wrong for its later games. Seed before scheduling Cobalt, or seed each side + // separately; per-game resolution only matters if a devnet ever needs a straddling chain. + AggregateVerifier gameImpl = AggregateVerifier(address(ctx.factory.gameImpls(ctx.gameType))); + (ctx.slowBlockInterval,) = gameImpl.intervalsForStartingBlock(ctx.anchorBlock); + ctx.intermediateRootsCount = gameImpl.intermediateOutputRootsCount(); + string memory rootsJson = vm.readFile(rootsPath); ctx.roots = abi.decode(vm.parseJson(rootsJson, ".roots"), (bytes32[])); - uint256 expectedRoots = gameCount * INTERMEDIATE_ROOTS_COUNT; + uint256 expectedRoots = gameCount * ctx.intermediateRootsCount; require( ctx.roots.length == expectedRoots, string.concat( @@ -104,7 +114,9 @@ contract SeedGames is Script { vm.toString(ctx.roots.length), ", expected ", vm.toString(expectedRoots), - ". Re-run generate-roots.sh with matching game count." + ". Re-run generate-roots.sh with matching game count and SLOW_BLOCK_INTERVAL=", + vm.toString(ctx.slowBlockInterval), + "." ) ); @@ -115,7 +127,7 @@ contract SeedGames is Script { } function _createGames(SeedCtx memory ctx, address asrAddr) internal returns (address firstGame, address lastGame) { - uint256 count = ctx.roots.length / INTERMEDIATE_ROOTS_COUNT; + uint256 count = ctx.roots.length / ctx.intermediateRootsCount; address parentAddr = asrAddr; for (uint256 i = 0; i < count; i++) { @@ -133,11 +145,12 @@ contract SeedGames is Script { } function _createSingleGame(SeedCtx memory ctx, uint256 index, address parentAddr) internal returns (address) { - uint256 l2Block = ctx.anchorBlock + BLOCK_INTERVAL * (index + 1); - uint256 rootsOffset = index * INTERMEDIATE_ROOTS_COUNT; - bytes32 rootClaimHash = ctx.roots[rootsOffset + INTERMEDIATE_ROOTS_COUNT - 1]; + uint256 l2Block = ctx.anchorBlock + ctx.slowBlockInterval * (index + 1); + uint256 rootsOffset = index * ctx.intermediateRootsCount; + bytes32 rootClaimHash = ctx.roots[rootsOffset + ctx.intermediateRootsCount - 1]; - bytes memory extraData = abi.encodePacked(l2Block, parentAddr, _sliceRoots(ctx.roots, rootsOffset)); + bytes memory extraData = + abi.encodePacked(l2Block, parentAddr, _sliceRoots(ctx.roots, rootsOffset, ctx.intermediateRootsCount)); IDisputeGame created = ctx.factory.createWithInitData{ value: ctx.initBond }( ctx.gameType, Claim.wrap(rootClaimHash), extraData, ctx.proof @@ -145,9 +158,9 @@ contract SeedGames is Script { return address(created); } - function _sliceRoots(bytes32[] memory all, uint256 offset) internal pure returns (bytes memory) { - bytes32[] memory slice = new bytes32[](INTERMEDIATE_ROOTS_COUNT); - for (uint256 j = 0; j < INTERMEDIATE_ROOTS_COUNT; j++) { + function _sliceRoots(bytes32[] memory all, uint256 offset, uint256 count) internal pure returns (bytes memory) { + bytes32[] memory slice = new bytes32[](count); + for (uint256 j = 0; j < count; j++) { slice[j] = all[offset + j]; } return abi.encodePacked(slice); diff --git a/scripts/multiproof/generate-roots.sh b/scripts/multiproof/generate-roots.sh index 50b43f811..f34145fd6 100755 --- a/scripts/multiproof/generate-roots.sh +++ b/scripts/multiproof/generate-roots.sh @@ -32,9 +32,12 @@ GAME_COUNT="${3:-500}" PARALLELISM="${4:-20}" OUTPUT_FILE="${5:-roots.json}" -# Must match AggregateVerifier / SeedGames constants -BLOCK_INTERVAL=600 -INTERMEDIATE_BLOCK_INTERVAL=30 +# Must match the intervals the deployed AggregateVerifier selects for these games. The contract +# carries both the slow- and fast-block pairs, so override these when seeding a devnet on which Cobalt +# is already active: BLOCK_INTERVAL=6000 INTERMEDIATE_BLOCK_INTERVAL=300 ./generate-roots.sh ... +# SeedGames.s.sol reads the live values off the deployment and will refuse a mismatched roots file. +BLOCK_INTERVAL="${BLOCK_INTERVAL:-600}" +INTERMEDIATE_BLOCK_INTERVAL="${INTERMEDIATE_BLOCK_INTERVAL:-30}" ROOTS_PER_GAME=$((BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL)) TOTAL_ROOTS=$((GAME_COUNT * ROOTS_PER_GAME)) @@ -43,6 +46,7 @@ LAST_BLOCK=$((ANCHOR_BLOCK + GAME_COUNT * BLOCK_INTERVAL)) echo "=== Generating Output Roots ===" echo "Anchor block: $ANCHOR_BLOCK" echo "Game count: $GAME_COUNT" +echo "Block interval: $BLOCK_INTERVAL" echo "Roots per game: $ROOTS_PER_GAME" echo "Total roots: $TOTAL_ROOTS" echo "L2 block range: [$((ANCHOR_BLOCK + INTERMEDIATE_BLOCK_INTERVAL)), $LAST_BLOCK]" diff --git a/snapshots/abi/AggregateVerifier.json b/snapshots/abi/AggregateVerifier.json index ee3b801a1..618a657f6 100644 --- a/snapshots/abi/AggregateVerifier.json +++ b/snapshots/abi/AggregateVerifier.json @@ -59,14 +59,31 @@ "type": "uint256" }, { - "internalType": "uint256", - "name": "blockInterval", - "type": "uint256" - }, - { - "internalType": "uint256", - "name": "intermediateBlockInterval", - "type": "uint256" + "components": [ + { + "internalType": "uint256", + "name": "slowBlockInterval", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "slowIntermediateBlockInterval", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fastBlockInterval", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fastIntermediateBlockInterval", + "type": "uint256" + } + ], + "internalType": "struct AggregateVerifier.IntervalConfig", + "name": "intervalConfig", + "type": "tuple" }, { "components": [ @@ -112,19 +129,6 @@ "stateMutability": "view", "type": "function" }, - { - "inputs": [], - "name": "BLOCK_INTERVAL", - "outputs": [ - { - "internalType": "uint256", - "name": "", - "type": "uint256" - } - ], - "stateMutability": "view", - "type": "function" - }, { "inputs": [], "name": "CONFIG_HASH", @@ -190,6 +194,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "FAST_BLOCK_INTERVAL", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "FAST_FINALIZATION_DELAY", @@ -205,7 +222,7 @@ }, { "inputs": [], - "name": "INTERMEDIATE_BLOCK_INTERVAL", + "name": "FAST_INTERMEDIATE_BLOCK_INTERVAL", "outputs": [ { "internalType": "uint256", @@ -294,6 +311,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "SLOW_BLOCK_INTERVAL", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "SLOW_FINALIZATION_DELAY", @@ -307,6 +337,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "SLOW_INTERMEDIATE_BLOCK_INTERVAL", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "TEE_IMAGE_HASH", @@ -646,6 +689,30 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "startingBlock", + "type": "uint256" + } + ], + "name": "intervalsForStartingBlock", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "l1Head", @@ -1071,12 +1138,12 @@ "inputs": [ { "internalType": "uint256", - "name": "blockInterval", + "name": "slowBlockInterval", "type": "uint256" }, { "internalType": "uint256", - "name": "intermediateBlockInterval", + "name": "slowIntermediateBlockInterval", "type": "uint256" } ], @@ -1204,6 +1271,22 @@ "name": "L2TimestampOverflow", "type": "error" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "slowCount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "fastCount", + "type": "uint256" + } + ], + "name": "MismatchedIntermediateRootCount", + "type": "error" + }, { "inputs": [ { diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 3d8078e45..8f10fe974 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -28,8 +28,8 @@ "sourceCodeHash": "0x780ff372493ba9010bc0d13100ac896f2bf75730a9b17d4bb63aaf694dc3c634" }, "src/L1/proofs/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0xc14998e269a6ad7e5c2b5d5beee0eabb68a68c3758e666a76822905c4f16575f", - "sourceCodeHash": "0x04921678f197a1dbe20b04696822c02c22e528112900616aeeb78fd9649b28ef" + "initCodeHash": "0x981207fb011a16372054d383ee5a90cf65774aaa67d9a3fad9a8f9b80e1c25b5", + "sourceCodeHash": "0x0e54893d50a31bf65e72290de1fbde37b30e949f5419176a8b359cd3aa52271c" }, "src/L1/proofs/AnchorStateRegistry.sol:AnchorStateRegistry": { "initCodeHash": "0x6f3afd2d0ef97a82ca3111976322b99343a270e54cd4a405028f2f29c75f7fb1", diff --git a/src/L1/proofs/AggregateVerifier.sol b/src/L1/proofs/AggregateVerifier.sol index 7ef9d4c28..44b9ca218 100644 --- a/src/L1/proofs/AggregateVerifier.sol +++ b/src/L1/proofs/AggregateVerifier.sol @@ -53,6 +53,16 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { uint64 blockTime; } + /// @notice Proposal block intervals for each side of the block-speedup activation. + /// @dev Both pairs must yield the same intermediate root count, so the CWIA `extraData` layout + /// and `INITIALIZE_CALLDATA_SIZE` are identical on both sides of the fork. + struct IntervalConfig { + uint256 slowBlockInterval; + uint256 slowIntermediateBlockInterval; + uint256 fastBlockInterval; + uint256 fastIntermediateBlockInterval; + } + //////////////////////////////////////////////////////////////// // Constants // //////////////////////////////////////////////////////////////// @@ -76,11 +86,15 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The minimum number of proofs required to resolve the game. uint256 public constant PROOF_THRESHOLD = 1; - /// @notice The ProtocolVersions upgrade index for Denim. - uint256 private constant DENIM_UPGRADE_INDEX = 13; + /// @notice The ProtocolVersions upgrade index at which L2 blocks switch to the fast cadence. + /// @dev This is the one place the contract is tied to a specific hardfork: index 12 is Cobalt, + /// which drops the L2 block time from 2s to 200ms. Everything downstream is expressed as + /// slow-vs-fast blocks, so a later cadence change is a new index and new interval pair + /// rather than new machinery. + uint256 private constant FAST_BLOCK_UPGRADE_INDEX = 12; - /// @notice The number of whole Denim blocks produced per second. - uint256 private constant DENIM_BLOCKS_PER_SECOND = 5; + /// @notice The number of whole fast-cadence L2 blocks produced per second. + uint256 private constant FAST_BLOCKS_PER_SECOND = 5; //////////////////////////////////////////////////////////////// // Immutables // //////////////////////////////////////////////////////////////// @@ -123,13 +137,21 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The legacy number of seconds between consecutive L2 blocks. uint64 public immutable L2_BLOCK_TIME; - /// @notice The block interval between each proposal. - /// @dev The parent's block number + BLOCK_INTERVAL = this proposal's block number. - uint256 public immutable BLOCK_INTERVAL; + /// @notice The block interval between each proposal, for games starting on slow blocks. + /// @dev The parent's block number + SLOW_BLOCK_INTERVAL = this proposal's block number. + uint256 public immutable SLOW_BLOCK_INTERVAL; + + /// @notice The block interval for intermediate proposals, for games starting on slow blocks. + /// @dev SLOW_BLOCK_INTERVAL must be divisible by SLOW_INTERMEDIATE_BLOCK_INTERVAL. + uint256 public immutable SLOW_INTERMEDIATE_BLOCK_INTERVAL; + + /// @notice The block interval between each proposal, for games starting on fast blocks. + uint256 public immutable FAST_BLOCK_INTERVAL; - /// @notice The block interval for intermediate proposals. - /// @dev BLOCK_INTERVAL must be divisible by INTERMEDIATE_BLOCK_INTERVAL. - uint256 public immutable INTERMEDIATE_BLOCK_INTERVAL; + /// @notice The block interval for intermediate proposals, for games starting on fast blocks. + /// @dev FAST_BLOCK_INTERVAL must be divisible by FAST_INTERMEDIATE_BLOCK_INTERVAL, and their + /// ratio must equal the slow-block one. + uint256 public immutable FAST_INTERMEDIATE_BLOCK_INTERVAL; /// @notice The size of the initialize call data. uint256 internal immutable INITIALIZE_CALLDATA_SIZE; @@ -228,7 +250,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Errors // //////////////////////////////////////////////////////////////// /// @notice When the block interval or intermediate block interval is invalid. - error InvalidBlockInterval(uint256 blockInterval, uint256 intermediateBlockInterval); + error InvalidBlockInterval(uint256 slowBlockInterval, uint256 slowIntermediateBlockInterval); + + /// @notice When the slow- and fast-block intervals do not yield the same intermediate root count. + error MismatchedIntermediateRootCount(uint256 slowCount, uint256 fastCount); /// @notice When the block number is unexpected. error UnexpectedBlockNumber(uint256 expectedBlockNumber, uint256 actualBlockNumber); @@ -302,8 +327,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @param zkHashes The hashes of the ZK range and aggregate programs. /// @param configHash The hash of the rollup configuration. /// @param l2ChainId The chain ID of the L2 network. - /// @param blockInterval The block interval. - /// @param intermediateBlockInterval The intermediate block interval. + /// @param intervalConfig The slow- and fast-block proposal intervals. /// @param scheduleConfig Upgrade registry and deterministic L2 timestamp configuration. constructor( GameType gameType_, @@ -315,14 +339,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { ZkHashes memory zkHashes, bytes32 configHash, uint256 l2ChainId, - uint256 blockInterval, - uint256 intermediateBlockInterval, + IntervalConfig memory intervalConfig, ScheduleConfig memory scheduleConfig ) { - // Block interval and intermediate block interval must be positive and divisible. - if (blockInterval == 0 || intermediateBlockInterval == 0 || blockInterval % intermediateBlockInterval != 0) { - revert InvalidBlockInterval(blockInterval, intermediateBlockInterval); - } + _validateIntervals(intervalConfig); if (scheduleConfig.blockTime == 0) revert InvalidL2BlockTime(); // Set up initial game state. @@ -340,8 +360,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { L2_GENESIS_BLOCK_NUMBER = scheduleConfig.genesisBlockNumber; L2_GENESIS_TIMESTAMP = scheduleConfig.genesisTimestamp; L2_BLOCK_TIME = scheduleConfig.blockTime; - BLOCK_INTERVAL = blockInterval; - INTERMEDIATE_BLOCK_INTERVAL = intermediateBlockInterval; + SLOW_BLOCK_INTERVAL = intervalConfig.slowBlockInterval; + SLOW_INTERMEDIATE_BLOCK_INTERVAL = intervalConfig.slowIntermediateBlockInterval; + FAST_BLOCK_INTERVAL = intervalConfig.fastBlockInterval; + FAST_INTERMEDIATE_BLOCK_INTERVAL = intervalConfig.fastIntermediateBlockInterval; PROTOCOL_VERSIONS = scheduleConfig.protocolVersions; INITIALIZE_CALLDATA_SIZE = 0x8E + 0x20 * intermediateOutputRootsCount(); @@ -369,8 +391,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // - 0x20 l1 head (CWIA data offset: 0x34) // - 0x20 extraData (l2BlockNumber) (CWIA data offset: 0x54) // - 0x14 extraData (parent game address) (CWIA data offset: 0x74) - // - 0x20 x (BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL) extraData (intermediate roots) (CWIA data offset: - // 0x88) + // - 0x20 x (SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL) extraData (intermediate roots) (CWIA data + // offset: 0x88) // - 0x02 CWIA bytes // - 0x20 proof length location @@ -409,9 +431,15 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { startingOutputRoot = ANCHOR_STATE_REGISTRY.getStartingAnchorRoot(); } - // The block number must be BLOCK_INTERVAL blocks after the starting block number. - if (l2SequenceNumber() != startingOutputRoot.l2SequenceNumber + BLOCK_INTERVAL) { - revert UnexpectedBlockNumber(startingOutputRoot.l2SequenceNumber + BLOCK_INTERVAL, l2SequenceNumber()); + // Resolved once and threaded through both fork-sensitive decisions below, which would + // otherwise re-read the schedule from `PROTOCOL_VERSIONS` a second time. + uint256 firstFastBlock = _firstFastBlock(); + + // The block number must be one block interval after the starting block number. The interval + // is selected on the starting block so the game chain stays contiguous across the speedup. + (uint256 slowBlockInterval,) = _intervalsAt(startingOutputRoot.l2SequenceNumber, firstFastBlock); + if (l2SequenceNumber() != startingOutputRoot.l2SequenceNumber + slowBlockInterval) { + revert UnexpectedBlockNumber(startingOutputRoot.l2SequenceNumber + slowBlockInterval, l2SequenceNumber()); } // Set the game as initialized. @@ -419,12 +447,17 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Pinning the upgrades active at the ending L2 block makes the schedule independent of both // the proof's L1 head and the L1 block in which this game is created. + // + // Note that this anchors on the *ending* block while the interval selection above anchors on + // the *starting* block. That is deliberate, and it is what makes the straddling game work: it + // spans the slow-block interval its start selects, while pinning the post-speedup schedule + // its end falls under, so the prover knows the fast cadence is active past the boundary. uint256 claimBlock = l2SequenceNumber(); if (claimBlock < L2_GENESIS_BLOCK_NUMBER) { revert L2BlockBeforeGenesis(claimBlock, L2_GENESIS_BLOCK_NUMBER); } - uint64 claimTimestamp = _l2Timestamp(claimBlock); + uint64 claimTimestamp = _l2Timestamp(claimBlock, firstFastBlock); // `ProtocolVersions` freezes mutations to an activation `FREEZE_WINDOW` before it takes // effect. Requiring the claim timestamp to have been reached on L1 additionally ensures the @@ -767,10 +800,27 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { return expectedResolution.raw() <= block.timestamp; } + /// @notice Returns the proposal intervals that govern a game starting at `startingBlock`. + /// @dev Offchain proposers and challengers must resolve intervals per game through this getter + /// rather than reading `SLOW_BLOCK_INTERVAL` / `SLOW_INTERMEDIATE_BLOCK_INTERVAL`, which describe + /// only games starting on slow blocks. + /// @dev Selection is on the starting block rather than the ending block so the game chain stays + /// contiguous across the activation: every game satisfies + /// `end == parent.end + slowBlockInterval` for the interval its own start selects. Exactly one + /// game straddles the activation and is proven under the slow-block interval, which is + /// correct because provers apply fork rules per block by timestamp. + /// @param startingBlock The starting L2 block number of the game. + /// @return The block interval and the intermediate block interval governing that game. + function intervalsForStartingBlock(uint256 startingBlock) public view returns (uint256, uint256) { + return _intervalsAt(startingBlock, _firstFastBlock()); + } + /// @notice The number of intermediate output roots. /// @dev At least one as the proposal's root claim is considered an intermediate root. + /// The constructor requires both interval pairs to yield the same count, so this is constant + /// across the speedup activation and the CWIA `extraData` layout never changes. function intermediateOutputRootsCount() public view returns (uint256) { - return (BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL); + return (SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL); } /// @notice The intermediate output roots of the game. @@ -1101,43 +1151,99 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { bytes32 startingRoot = intermediateRootIndex == 0 ? startingOutputRoot.root.raw() : intermediateOutputRoot(intermediateRootIndex - 1); + (, uint256 slowIntermediateBlockInterval) = intervalsForStartingBlock(startingOutputRoot.l2SequenceNumber); uint64 startingL2SequenceNumber = - uint64(startingOutputRoot.l2SequenceNumber + intermediateRootIndex * INTERMEDIATE_BLOCK_INTERVAL); - uint64 endingL2SequenceNumber = startingL2SequenceNumber + uint64(INTERMEDIATE_BLOCK_INTERVAL); + uint64(startingOutputRoot.l2SequenceNumber + intermediateRootIndex * slowIntermediateBlockInterval); + uint64 endingL2SequenceNumber = startingL2SequenceNumber + uint64(slowIntermediateBlockInterval); return (startingRoot, startingL2SequenceNumber, endingL2SequenceNumber); } /// @notice Semantic version. - /// @custom:semver 0.1.0 + /// @custom:semver 0.2.0 function version() public pure virtual returns (string memory) { - return "0.1.0"; + return "0.2.0"; + } + + /// @notice Derives an L2 block timestamp: the slow cadence before the speedup, whole-second groups after it. + /// @param claimBlock The L2 block number to derive a timestamp for. + /// @param firstFastBlock The speedup activation block, as returned by `_firstFastBlock()`. + function _l2Timestamp(uint256 claimBlock, uint256 firstFastBlock) private view returns (uint64) { + if (claimBlock < firstFastBlock) return _slowL2Timestamp(claimBlock); + + uint256 claimTimestamp = + uint256(_slowL2Timestamp(firstFastBlock)) + (claimBlock - firstFastBlock) / FAST_BLOCKS_PER_SECOND; + if (claimTimestamp > type(uint64).max) revert L2TimestampOverflow(claimBlock); + return uint64(claimTimestamp); } - /// @notice Derives an L2 block timestamp using the legacy cadence before Denim and whole-second groups after it. - function _l2Timestamp(uint256 claimBlock) private view returns (uint64) { + /// @notice Reverts unless both interval pairs are positive, divisible, and yield the same + /// intermediate root count. + function _validateIntervals(IntervalConfig memory intervalConfig) private pure { + uint256 slowBlockInterval = intervalConfig.slowBlockInterval; + uint256 slowIntermediateBlockInterval = intervalConfig.slowIntermediateBlockInterval; + uint256 fastBlockInterval = intervalConfig.fastBlockInterval; + uint256 fastIntermediateBlockInterval = intervalConfig.fastIntermediateBlockInterval; + + if ( + slowBlockInterval == 0 || slowIntermediateBlockInterval == 0 + || slowBlockInterval % slowIntermediateBlockInterval != 0 + ) { + revert InvalidBlockInterval(slowBlockInterval, slowIntermediateBlockInterval); + } + if ( + fastBlockInterval == 0 || fastIntermediateBlockInterval == 0 + || fastBlockInterval % fastIntermediateBlockInterval != 0 + ) { + revert InvalidBlockInterval(fastBlockInterval, fastIntermediateBlockInterval); + } + + // The intermediate root count fixes the CWIA `extraData` layout, which is frozen for the + // lifetime of this implementation. Both interval pairs must therefore agree on it. + uint256 slowCount = slowBlockInterval / slowIntermediateBlockInterval; + uint256 fastCount = fastBlockInterval / fastIntermediateBlockInterval; + if (slowCount != fastCount) revert MismatchedIntermediateRootCount(slowCount, fastCount); + } + + /// @notice Returns the first L2 block number produced at the fast cadence, or `type(uint256).max` + /// when the speedup is not scheduled. + /// @dev Reading the live schedule is safe despite it being mutable, in both directions: + /// + /// - A game that selected the fast-block intervals has a starting block at or past the + /// activation, so the activation is in the past. `ProtocolVersions._assertNotFrozen` + /// rejects every mutation of a passed activation, from `setTimestamp` and + /// `delayTimestamp` alike, so that game's selection can never be revoked. + /// - A game that selected the slow-block intervals cannot be pulled across the boundary + /// either, including by the owner moving the activation *earlier* rather than later. + /// `initializeWithInitData` rejects a game whose ending L2 timestamp L1 has not yet + /// reached, so every initialized game satisfies `startingTimestamp < endingTimestamp <= + /// block.timestamp`, while any new activation must clear `block.timestamp + MIN_NOTICE`. + /// The activation therefore always lands after the game's starting block. + function _firstFastBlock() private view returns (uint256) { uint64[] memory schedule = PROTOCOL_VERSIONS.getSchedule(); - if (schedule.length <= DENIM_UPGRADE_INDEX) return _legacyL2Timestamp(claimBlock); + if (schedule.length <= FAST_BLOCK_UPGRADE_INDEX) return type(uint256).max; - uint64 denimActivationTimestamp = schedule[DENIM_UPGRADE_INDEX]; - if (denimActivationTimestamp == 0) return _legacyL2Timestamp(claimBlock); + uint64 fastActivationTimestamp = schedule[FAST_BLOCK_UPGRADE_INDEX]; + if (fastActivationTimestamp == 0) return type(uint256).max; - uint256 blocksUntilDenim; - if (denimActivationTimestamp > L2_GENESIS_TIMESTAMP) { - blocksUntilDenim = FixedPointMathLib.divUp(denimActivationTimestamp - L2_GENESIS_TIMESTAMP, L2_BLOCK_TIME); + if (fastActivationTimestamp <= L2_GENESIS_TIMESTAMP) { + return L2_GENESIS_BLOCK_NUMBER; } - uint256 blocksSinceGenesis = claimBlock - L2_GENESIS_BLOCK_NUMBER; - if (blocksSinceGenesis < blocksUntilDenim) return _legacyL2Timestamp(claimBlock); + return L2_GENESIS_BLOCK_NUMBER + + FixedPointMathLib.divUp(fastActivationTimestamp - L2_GENESIS_TIMESTAMP, L2_BLOCK_TIME); + } - uint256 denimBlock = L2_GENESIS_BLOCK_NUMBER + blocksUntilDenim; - uint256 claimTimestamp = - uint256(_legacyL2Timestamp(denimBlock)) + (claimBlock - denimBlock) / DENIM_BLOCKS_PER_SECOND; - if (claimTimestamp > type(uint64).max) revert L2TimestampOverflow(claimBlock); - return uint64(claimTimestamp); + /// @notice Selects the proposal intervals governing a game, from its starting block number and an + /// already-resolved speedup activation block. + /// @dev Takes `firstFastBlock` rather than resolving it so a caller making more than one + /// fork-sensitive decision pays for `PROTOCOL_VERSIONS.getSchedule()` once. + function _intervalsAt(uint256 startingBlock, uint256 firstFastBlock) private view returns (uint256, uint256) { + if (startingBlock < firstFastBlock) return (SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + return (FAST_BLOCK_INTERVAL, FAST_INTERMEDIATE_BLOCK_INTERVAL); } - /// @notice Derives an L2 block timestamp using the pre-Denim block cadence. - function _legacyL2Timestamp(uint256 claimBlock) private view returns (uint64) { + /// @notice Derives an L2 block timestamp using the slow block cadence. + function _slowL2Timestamp(uint256 claimBlock) private view returns (uint64) { uint256 blocksSinceGenesis = claimBlock - L2_GENESIS_BLOCK_NUMBER; uint256 maxBlocks = (type(uint64).max - L2_GENESIS_TIMESTAMP) / L2_BLOCK_TIME; if (blocksSinceGenesis > maxBlocks) revert L2TimestampOverflow(claimBlock); diff --git a/test/L1/OptimismPortal2.t.sol b/test/L1/OptimismPortal2.t.sol index afa597ae1..ff44eb887 100644 --- a/test/L1/OptimismPortal2.t.sol +++ b/test/L1/OptimismPortal2.t.sol @@ -89,8 +89,12 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { AggregateVerifier.ZkHashes(bytes32(uint256(2)), bytes32(uint256(3))), bytes32(uint256(4)), deploy.cfg().l2ChainId(), - 100, - 10, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: 100, + slowIntermediateBlockInterval: 10, + fastBlockInterval: 1000, + fastIntermediateBlockInterval: 100 + }), AggregateVerifier.ScheduleConfig({ protocolVersions: protocolVersions, genesisBlockNumber: 0, genesisTimestamp: 1, blockTime: 2 }) @@ -99,7 +103,8 @@ abstract contract OptimismPortal2_TestInit is DisputeGameFactory_TestInit { disputeGameFactory.setInitBond(respectedGameType, 0); Proposal memory startingRoot = anchorStateRegistry.getStartingAnchorRoot(); - _proposedBlockNumber = startingRoot.l2SequenceNumber + gameImpl.BLOCK_INTERVAL(); + (uint256 slowBlockInterval,) = gameImpl.intervalsForStartingBlock(startingRoot.l2SequenceNumber); + _proposedBlockNumber = startingRoot.l2SequenceNumber + slowBlockInterval; depositor = makeAddr("depositor"); diff --git a/test/L1/proofs/AggregateVerifier.t.sol b/test/L1/proofs/AggregateVerifier.t.sol index eba05dc11..2a7f76c76 100644 --- a/test/L1/proofs/AggregateVerifier.t.sol +++ b/test/L1/proofs/AggregateVerifier.t.sol @@ -20,8 +20,8 @@ import { BaseTest } from "./BaseTest.t.sol"; contract AggregateVerifierTest is BaseTest { using LibClone for address; - uint256 private constant DENIM_UPGRADE_INDEX = 13; - uint256 private constant DENIM_BLOCKS_PER_SECOND = 5; + uint256 private constant FAST_BLOCK_UPGRADE_INDEX = 12; + uint256 private constant FAST_BLOCKS_PER_SECOND = 5; AggregateVerifier private aggregateVerifierImpl; @@ -43,8 +43,8 @@ contract AggregateVerifierTest is BaseTest { /// @notice Initialization pins the upgrades active at the claimed L2 block, independently of /// the L1 game-creation timestamp, and later schedule changes cannot alter the pin. function test_initialize_pinsScheduleId_succeeds() public { - uint64 firstGameTimestamp = _l2Timestamp(BLOCK_INTERVAL); - uint64 secondActivationTimestamp = _l2Timestamp(BLOCK_INTERVAL + BLOCK_INTERVAL / 2); + uint64 firstGameTimestamp = _l2Timestamp(SLOW_BLOCK_INTERVAL); + uint64 secondActivationTimestamp = _l2Timestamp(SLOW_BLOCK_INTERVAL + SLOW_BLOCK_INTERVAL / 2); // The first upgrade is active at the first game's L2 timestamp; the second is not. uint64[] memory schedule = new uint64[](2); @@ -80,7 +80,7 @@ contract AggregateVerifierTest is BaseTest { /// @notice Consecutive games on opposite sides of an L2 activation boundary pin different /// schedule commitments. function test_initialize_scheduleChangesAtL2ActivationBoundary_succeeds() public { - uint64 activationTimestamp = _l2Timestamp(BLOCK_INTERVAL + BLOCK_INTERVAL / 2); + uint64 activationTimestamp = _l2Timestamp(SLOW_BLOCK_INTERVAL + SLOW_BLOCK_INTERVAL / 2); uint64[] memory schedule = new uint64[](1); schedule[0] = activationTimestamp; _importProtocolVersionsSchedule(schedule); @@ -106,36 +106,36 @@ contract AggregateVerifierTest is BaseTest { assertEq(secondGame.scheduleId(), protocolVersions.activatedScheduleId(_l2Timestamp(currentL2BlockNumber))); } - function test_initialize_denimBlockTimestamps_succeeds() public { - uint64 denimActivationTimestamp = L2_GENESIS_TIMESTAMP + 1; - uint64 denimBlockTimestamp = L2_GENESIS_TIMESTAMP + L2_BLOCK_TIME; - uint64[] memory schedule = new uint64[](DENIM_UPGRADE_INDEX + 2); - for (uint256 i; i < DENIM_UPGRADE_INDEX; i++) { + function test_initialize_fastBlockTimestamps_succeeds() public { + uint64 fastActivationTimestamp = L2_GENESIS_TIMESTAMP + 1; + uint64 firstFastBlockTimestamp = L2_GENESIS_TIMESTAMP + L2_BLOCK_TIME; + uint64[] memory schedule = new uint64[](FAST_BLOCK_UPGRADE_INDEX + 2); + for (uint256 i; i < FAST_BLOCK_UPGRADE_INDEX; i++) { schedule[i] = L2_GENESIS_TIMESTAMP; } - schedule[DENIM_UPGRADE_INDEX] = denimActivationTimestamp; - schedule[DENIM_UPGRADE_INDEX + 1] = denimBlockTimestamp + 1; + schedule[FAST_BLOCK_UPGRADE_INDEX] = fastActivationTimestamp; + schedule[FAST_BLOCK_UPGRADE_INDEX + 1] = firstFastBlockTimestamp + 1; _importProtocolVersionsSchedule(schedule); _setSingleBlockAggregateVerifier(L2_GENESIS_TIMESTAMP); - bytes32 denimScheduleId = protocolVersions.scheduleId(DENIM_UPGRADE_INDEX); - bytes32 postDenimScheduleId = protocolVersions.scheduleId(DENIM_UPGRADE_INDEX + 1); + bytes32 speedupScheduleId = protocolVersions.scheduleId(FAST_BLOCK_UPGRADE_INDEX); + bytes32 postSpeedupScheduleId = protocolVersions.scheduleId(FAST_BLOCK_UPGRADE_INDEX + 1); address parent = address(anchorStateRegistry); - for (uint256 l2BlockNumber = 1; l2BlockNumber <= DENIM_BLOCKS_PER_SECOND + 1; l2BlockNumber++) { - vm.warp(denimBlockTimestamp + (l2BlockNumber - 1) / DENIM_BLOCKS_PER_SECOND); + for (uint256 l2BlockNumber = 1; l2BlockNumber <= FAST_BLOCKS_PER_SECOND + 1; l2BlockNumber++) { + vm.warp(firstFastBlockTimestamp + (l2BlockNumber - 1) / FAST_BLOCKS_PER_SECOND); AggregateVerifier game = _createSingleBlockGame(l2BlockNumber, parent); assertEq( - game.scheduleId(), l2BlockNumber <= DENIM_BLOCKS_PER_SECOND ? denimScheduleId : postDenimScheduleId + game.scheduleId(), l2BlockNumber <= FAST_BLOCKS_PER_SECOND ? speedupScheduleId : postSpeedupScheduleId ); parent = address(game); } } - function test_initialize_unscheduledDenimUsesLegacyTimestamp_succeeds() public { - uint64[] memory schedule = new uint64[](DENIM_UPGRADE_INDEX + 1); - for (uint256 i; i < DENIM_UPGRADE_INDEX; i++) { + function test_initialize_unscheduledSpeedupUsesSlowTimestamp_succeeds() public { + uint64[] memory schedule = new uint64[](FAST_BLOCK_UPGRADE_INDEX + 1); + for (uint256 i; i < FAST_BLOCK_UPGRADE_INDEX; i++) { schedule[i] = L2_GENESIS_TIMESTAMP; } _importProtocolVersionsSchedule(schedule); @@ -152,7 +152,103 @@ contract AggregateVerifierTest is BaseTest { vm.warp(legacyBlockTimestamp); AggregateVerifier game = _createSingleBlockGame(1, address(anchorStateRegistry)); - assertEq(game.scheduleId(), protocolVersions.scheduleId(DENIM_UPGRADE_INDEX - 1)); + assertEq(game.scheduleId(), protocolVersions.scheduleId(FAST_BLOCK_UPGRADE_INDEX - 1)); + } + + /// @notice Intervals are selected on the game's starting block relative to the first fast block, + /// so the chain of games stays contiguous across the fork. + function test_intervalsForStartingBlock_selectsOnFirstFastBlock_succeeds() public { + // divUp(86500 - L2_GENESIS_TIMESTAMP, L2_BLOCK_TIME) == 50. + _importSpeedupSchedule(L2_GENESIS_TIMESTAMP + 100); + uint256 firstFastBlock = 50; + + _assertIntervals(firstFastBlock - 1, SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + _assertIntervals(firstFastBlock, FAST_BLOCK_INTERVAL, FAST_INTERMEDIATE_BLOCK_INTERVAL); + _assertIntervals(firstFastBlock + 1, FAST_BLOCK_INTERVAL, FAST_INTERMEDIATE_BLOCK_INTERVAL); + } + + function test_intervalsForStartingBlock_speedupUnscheduled_succeeds() public view { + _assertIntervals(0, SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + _assertIntervals(type(uint64).max, SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + } + + /// @notice A game starting at or after the first fast block must span FAST_BLOCK_INTERVAL. + function test_initialize_fastIntervals_succeeds() public { + // The activation timestamp equals genesis, so every block including the anchor is fast. + _importSpeedupSchedule(L2_GENESIS_TIMESTAMP); + _assertIntervals(0, FAST_BLOCK_INTERVAL, FAST_INTERMEDIATE_BLOCK_INTERVAL); + + vm.expectRevert( + abi.encodeWithSelector( + AggregateVerifier.UnexpectedBlockNumber.selector, FAST_BLOCK_INTERVAL, SLOW_BLOCK_INTERVAL + ) + ); + _createGameEndingAt(SLOW_BLOCK_INTERVAL); + + AggregateVerifier game = _createGameEndingAt(FAST_BLOCK_INTERVAL); + assertEq(game.l2SequenceNumber(), FAST_BLOCK_INTERVAL); + } + + /// @notice The one game whose range contains the activation block starts before it, so it is + /// proven under the slow-block interval. + function test_initialize_straddlingGame_usesSlowInterval_succeeds() public { + // Activation block 50 falls inside the first game's [0, 100) range. + _importSpeedupSchedule(L2_GENESIS_TIMESTAMP + 100); + _assertIntervals(0, SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + + vm.expectRevert( + abi.encodeWithSelector( + AggregateVerifier.UnexpectedBlockNumber.selector, SLOW_BLOCK_INTERVAL, FAST_BLOCK_INTERVAL + ) + ); + _createGameEndingAt(FAST_BLOCK_INTERVAL); + + AggregateVerifier game = _createGameEndingAt(SLOW_BLOCK_INTERVAL); + assertEq(game.l2SequenceNumber(), SLOW_BLOCK_INTERVAL); + } + + /// @notice `challenge` derives the intermediate sub-range from the interval the game's starting + /// block selects. This is the second fork-sensitive call site, and unlike the block + /// number check its output goes into the journal the prover signs, so a stale interval + /// here makes a valid challenge unconstructable rather than reverting loudly. + function test_challenge_fastIntermediateInterval_succeeds() public { + _importSpeedupSchedule(L2_GENESIS_TIMESTAMP); + _assertChallengeIntermediateRange(FAST_BLOCK_INTERVAL, FAST_INTERMEDIATE_BLOCK_INTERVAL); + } + + /// @notice The straddling game's sub-ranges stay slow-block sized, matching the interval its own + /// starting block selects. + function test_challenge_straddlingGameUsesSlowIntermediateInterval_succeeds() public { + // Activation block 50 falls inside the first game's [0, 100) range. + _importSpeedupSchedule(L2_GENESIS_TIMESTAMP + 100); + _assertChallengeIntermediateRange(SLOW_BLOCK_INTERVAL, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + } + + function test_constructor_mismatchedIntermediateRootCount_reverts() public { + // 100 / 10 == 10 intermediate roots, but 1000 / 200 == 5. + vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.MismatchedIntermediateRootCount.selector, 10, 5)); + _deployAggregateVerifier( + AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: 200 + }), + _defaultScheduleConfig() + ); + } + + function test_constructor_invalidFastBlockIntervals_reverts() public { + vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.InvalidBlockInterval.selector, 1000, 0)); + _deployAggregateVerifier( + AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: 0 + }), + _defaultScheduleConfig() + ); } /// @notice A claim whose L2 timestamp L1 has not yet reached cannot open a game, so a game can @@ -160,7 +256,7 @@ contract AggregateVerifierTest is BaseTest { function test_initialize_l2TimestampInFuture_reverts() public { // Scheduling the upgrade at the first game's deterministic L2 timestamp and leaving the L1 // clock short of it is the window the finding exploits. - uint64 activationTimestamp = _l2Timestamp(BLOCK_INTERVAL); + uint64 activationTimestamp = _l2Timestamp(SLOW_BLOCK_INTERVAL); uint64[] memory schedule = new uint64[](1); schedule[0] = activationTimestamp; _importProtocolVersionsSchedule(schedule); @@ -209,7 +305,7 @@ contract AggregateVerifierTest is BaseTest { function test_initialize_l2BlockBeforeGenesis_reverts() public { AggregateVerifier.ScheduleConfig memory scheduleConfig = AggregateVerifier.ScheduleConfig({ protocolVersions: IProtocolVersions(address(protocolVersions)), - genesisBlockNumber: BLOCK_INTERVAL + 1, + genesisBlockNumber: SLOW_BLOCK_INTERVAL + 1, genesisTimestamp: 0, blockTime: L2_BLOCK_TIME }); @@ -219,7 +315,7 @@ contract AggregateVerifierTest is BaseTest { Claim rootClaim = _advanceL2BlockAndClaim(); vm.expectRevert( abi.encodeWithSelector( - AggregateVerifier.L2BlockBeforeGenesis.selector, currentL2BlockNumber, BLOCK_INTERVAL + 1 + AggregateVerifier.L2BlockBeforeGenesis.selector, currentL2BlockNumber, SLOW_BLOCK_INTERVAL + 1 ) ); _createAggregateVerifierGame( @@ -252,23 +348,23 @@ contract AggregateVerifierTest is BaseTest { ); } - function test_initialize_denimTimestampOverflow_reverts() public { + function test_initialize_fastTimestampOverflow_reverts() public { uint64 genesisTimestamp = type(uint64).max - L2_BLOCK_TIME; - uint64[] memory schedule = new uint64[](DENIM_UPGRADE_INDEX + 1); - for (uint256 i; i < DENIM_UPGRADE_INDEX; i++) { + uint64[] memory schedule = new uint64[](FAST_BLOCK_UPGRADE_INDEX + 1); + for (uint256 i; i < FAST_BLOCK_UPGRADE_INDEX; i++) { schedule[i] = genesisTimestamp; } - schedule[DENIM_UPGRADE_INDEX] = genesisTimestamp + 1; + schedule[FAST_BLOCK_UPGRADE_INDEX] = genesisTimestamp + 1; _importProtocolVersionsSchedule(schedule); _setSingleBlockAggregateVerifier(genesisTimestamp); vm.warp(uint256(type(uint64).max) + 3); address parent = address(anchorStateRegistry); - for (uint256 l2BlockNumber = 1; l2BlockNumber <= DENIM_BLOCKS_PER_SECOND; l2BlockNumber++) { + for (uint256 l2BlockNumber = 1; l2BlockNumber <= FAST_BLOCKS_PER_SECOND; l2BlockNumber++) { parent = address(_createSingleBlockGame(l2BlockNumber, parent)); } - uint256 overflowingBlock = DENIM_BLOCKS_PER_SECOND + 1; + uint256 overflowingBlock = FAST_BLOCKS_PER_SECOND + 1; vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.L2TimestampOverflow.selector, overflowingBlock)); _createSingleBlockGame(overflowingBlock, parent); } @@ -393,7 +489,7 @@ contract AggregateVerifierTest is BaseTest { /// @dev Parent is a real `AggregateVerifier` clone initialized like a factory game, but deployed without /// `_finalizeGameCreation`, so the factory UUID mapping has no entry. function testInitializeFailsIfParentGameNotFactoryRegistered() public { - currentL2BlockNumber += BLOCK_INTERVAL; + currentL2BlockNumber += SLOW_BLOCK_INTERVAL; Claim parentRootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "parent"))); AggregateVerifier unregisteredParent = _deployAggregateVerifierCloneWithoutFactoryRegistration( @@ -404,7 +500,7 @@ contract AggregateVerifierTest is BaseTest { _generateProof("parent-tee", AggregateVerifier.ProofType.TEE) ); - currentL2BlockNumber += BLOCK_INTERVAL; + currentL2BlockNumber += SLOW_BLOCK_INTERVAL; Claim childRootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "child"))); vm.expectRevert(AggregateVerifier.InvalidParentGame.selector); @@ -496,13 +592,13 @@ contract AggregateVerifierTest is BaseTest { } function testDeployWithInvalidBlockIntervals() public { - _expectDeployWithInvalidBlockIntervalsReverts(0, INTERMEDIATE_BLOCK_INTERVAL); - _expectDeployWithInvalidBlockIntervalsReverts(BLOCK_INTERVAL, 0); + _expectDeployWithInvalidBlockIntervalsReverts(0, SLOW_INTERMEDIATE_BLOCK_INTERVAL); + _expectDeployWithInvalidBlockIntervalsReverts(SLOW_BLOCK_INTERVAL, 0); _expectDeployWithInvalidBlockIntervalsReverts(3, 2); } function _advanceL2BlockAndClaim() private returns (Claim rootClaim) { - currentL2BlockNumber += BLOCK_INTERVAL; + currentL2BlockNumber += SLOW_BLOCK_INTERVAL; return Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); } @@ -524,8 +620,12 @@ contract AggregateVerifierTest is BaseTest { function _setSingleBlockAggregateVerifier(uint64 genesisTimestamp) private { AggregateVerifier implementation = _deployAggregateVerifier( - 1, - 1, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: 1, + slowIntermediateBlockInterval: 1, + fastBlockInterval: 1, + fastIntermediateBlockInterval: 1 + }), AggregateVerifier.ScheduleConfig({ protocolVersions: IProtocolVersions(address(protocolVersions)), genesisBlockNumber: L2_GENESIS_BLOCK_NUMBER, @@ -630,17 +730,17 @@ contract AggregateVerifierTest is BaseTest { } function _expectDeployWithInvalidBlockIntervalsReverts( - uint256 blockInterval, - uint256 intermediateBlockInterval + uint256 slowBlockInterval, + uint256 slowIntermediateBlockInterval ) private { vm.expectRevert( abi.encodeWithSelector( - AggregateVerifier.InvalidBlockInterval.selector, blockInterval, intermediateBlockInterval + AggregateVerifier.InvalidBlockInterval.selector, slowBlockInterval, slowIntermediateBlockInterval ) ); - _deployAggregateVerifierWithIntervals(blockInterval, intermediateBlockInterval); + _deployAggregateVerifierWithIntervals(slowBlockInterval, slowIntermediateBlockInterval); } /// @notice Clones the implementation like the factory, but skips `_finalizeGameCreation`. @@ -669,15 +769,19 @@ contract AggregateVerifierTest is BaseTest { } function _deployAggregateVerifierWithIntervals( - uint256 blockInterval, - uint256 intermediateBlockInterval + uint256 slowBlockInterval, + uint256 slowIntermediateBlockInterval ) private returns (AggregateVerifier) { return _deployAggregateVerifier( - blockInterval, - intermediateBlockInterval, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: slowBlockInterval, + slowIntermediateBlockInterval: slowIntermediateBlockInterval, + fastBlockInterval: slowBlockInterval, + fastIntermediateBlockInterval: slowIntermediateBlockInterval + }), AggregateVerifier.ScheduleConfig({ protocolVersions: IProtocolVersions(address(protocolVersions)), genesisBlockNumber: L2_GENESIS_BLOCK_NUMBER, @@ -691,12 +795,118 @@ contract AggregateVerifierTest is BaseTest { private returns (AggregateVerifier) { - return _deployAggregateVerifier(BLOCK_INTERVAL, INTERMEDIATE_BLOCK_INTERVAL, scheduleConfig); + return _deployAggregateVerifier(_defaultIntervalConfig(), scheduleConfig); + } + + /// @dev Registers a schedule whose only meaningful entry is the speedup activation, and rebinds + /// the implementation to it. + function _importSpeedupSchedule(uint64 fastActivationTimestamp) private { + uint64[] memory schedule = new uint64[](FAST_BLOCK_UPGRADE_INDEX + 1); + for (uint256 i; i < FAST_BLOCK_UPGRADE_INDEX; i++) { + schedule[i] = L2_GENESIS_TIMESTAMP; + } + schedule[FAST_BLOCK_UPGRADE_INDEX] = fastActivationTimestamp; + _importProtocolVersionsSchedule(schedule); + aggregateVerifierImpl = AggregateVerifier(address(factory.gameImpls(GameTypes.AGGREGATE_VERIFIER))); + } + + function _assertIntervals( + uint256 startingBlock, + uint256 expectedBlockInterval, + uint256 expectedIntermediateBlockInterval + ) + private + view + { + (uint256 slowBlockInterval, uint256 slowIntermediateBlockInterval) = + aggregateVerifierImpl.intervalsForStartingBlock(startingBlock); + assertEq(slowBlockInterval, expectedBlockInterval); + assertEq(slowIntermediateBlockInterval, expectedIntermediateBlockInterval); + } + + /// @dev Challenges intermediate root 0 of a game ending at `endingBlock` and asserts the range + /// handed to the ZK verifier is `[0, expectedIntermediateBlockInterval)`. The mock verifier + /// accepts anything, so the journal is checked by matching the call rather than by reverting. + function _assertChallengeIntermediateRange(uint256 endingBlock, uint256 expectedIntermediateBlockInterval) private { + AggregateVerifier game = _createGameEndingAt(endingBlock); + (Hash startingRoot,) = game.startingOutputRoot(); + + bytes32 counterRoot = keccak256("counter"); + bytes memory zkProof = abi.encodePacked(uint8(AggregateVerifier.ProofType.ZK), bytes1(0)); + + bytes32 expectedJournal = keccak256( + abi.encodePacked( + ZK_PROVER, + game.l1Head().raw(), + startingRoot.raw(), + uint64(0), + counterRoot, + uint64(expectedIntermediateBlockInterval), + abi.encodePacked(counterRoot), + CONFIG_HASH, + ZK_RANGE_HASH, + game.scheduleId() + ) + ); + + // `zkProof[1:]` is the single trailing zero byte. + vm.expectCall( + address(zkVerifier), abi.encodeCall(IVerifier.verify, (hex"00", ZK_AGGREGATE_HASH, expectedJournal)) + ); + vm.prank(ZK_PROVER); + game.challenge(zkProof, 0, counterRoot); + } + + /// @dev Creates a game off the anchor root. Intermediate root values are unchecked by the mock + /// verifiers, so only their count has to match the implementation. + function _createGameEndingAt(uint256 endingBlock) private returns (AggregateVerifier) { + Claim rootClaim = Claim.wrap(keccak256(abi.encode(endingBlock))); + // Both interval pairs are constructor-checked to yield the same count, so the slow-block ratio + // is the count on either side. Reading it off the implementation instead would put a + // staticcall between a caller's `vm.expectRevert` and the call it is meant to apply to. + uint256 count = SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL; + bytes32[] memory intermediateRoots = new bytes32[](count); + for (uint256 i; i < count - 1; i++) { + intermediateRoots[i] = keccak256(abi.encode(endingBlock, i)); + } + intermediateRoots[count - 1] = rootClaim.raw(); + + bytes memory extraData = + abi.encodePacked(endingBlock, address(anchorStateRegistry), abi.encodePacked(intermediateRoots)); + bytes memory proof = _generateProof(abi.encode(endingBlock), AggregateVerifier.ProofType.TEE); + + _warpToL2Timestamp(endingBlock); + vm.deal(TEE_PROVER, INIT_BOND); + vm.prank(TEE_PROVER); + return AggregateVerifier( + address( + factory.createWithInitData{ value: INIT_BOND }( + GameTypes.AGGREGATE_VERIFIER, rootClaim, extraData, proof + ) + ) + ); + } + + function _defaultScheduleConfig() private view returns (AggregateVerifier.ScheduleConfig memory) { + return AggregateVerifier.ScheduleConfig({ + protocolVersions: IProtocolVersions(address(protocolVersions)), + genesisBlockNumber: L2_GENESIS_BLOCK_NUMBER, + genesisTimestamp: L2_GENESIS_TIMESTAMP, + blockTime: L2_BLOCK_TIME + }); + } + + function _defaultIntervalConfig() private pure returns (AggregateVerifier.IntervalConfig memory) { + return AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: FAST_INTERMEDIATE_BLOCK_INTERVAL + }); } function _deployAggregateVerifier( - uint256 blockInterval, - uint256 intermediateBlockInterval, + AggregateVerifier.IntervalConfig memory intervalConfig, AggregateVerifier.ScheduleConfig memory scheduleConfig ) private @@ -712,8 +922,7 @@ contract AggregateVerifierTest is BaseTest { AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), CONFIG_HASH, L2_CHAIN_ID, - blockInterval, - intermediateBlockInterval, + intervalConfig, scheduleConfig ); } diff --git a/test/L1/proofs/AnchorStateRegistry.t.sol b/test/L1/proofs/AnchorStateRegistry.t.sol index 0f794f967..b97f63c1c 100644 --- a/test/L1/proofs/AnchorStateRegistry.t.sol +++ b/test/L1/proofs/AnchorStateRegistry.t.sol @@ -50,7 +50,7 @@ abstract contract AnchorStateRegistry_TestInit is BaseTest { // Get the actual anchor roots (, uint256 l2BlockNumber) = anchorStateRegistry.getAnchorRoot(); - validL2BlockNumber = l2BlockNumber + BLOCK_INTERVAL; + validL2BlockNumber = l2BlockNumber + SLOW_BLOCK_INTERVAL; Claim rootClaim = Claim.wrap(keccak256(abi.encode(validL2BlockNumber))); bytes memory proof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); gameProxy = IDisputeGame( diff --git a/test/L1/proofs/BaseTest.t.sol b/test/L1/proofs/BaseTest.t.sol index 123af8425..1f75627da 100644 --- a/test/L1/proofs/BaseTest.t.sol +++ b/test/L1/proofs/BaseTest.t.sol @@ -31,9 +31,11 @@ contract BaseTest is Test { uint64 internal constant L2_BLOCK_TIME = 2; // AggregateVerifier expects evenly spaced intermediate roots. - uint256 internal constant BLOCK_INTERVAL = 100; - uint256 internal constant INTERMEDIATE_BLOCK_INTERVAL = 10; - uint256 private constant INTERMEDIATE_ROOTS_COUNT = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL; + uint256 internal constant SLOW_BLOCK_INTERVAL = 100; + uint256 internal constant SLOW_INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 internal constant FAST_BLOCK_INTERVAL = 1000; + uint256 internal constant FAST_INTERMEDIATE_BLOCK_INTERVAL = 100; + uint256 private constant INTERMEDIATE_ROOTS_COUNT = SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL; uint256 internal constant INIT_BOND = 1 ether; uint256 internal constant DELAYED_WETH_DELAY = 1 days; @@ -136,8 +138,12 @@ contract BaseTest is Test { AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), CONFIG_HASH, L2_CHAIN_ID, - BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: FAST_INTERMEDIATE_BLOCK_INTERVAL + }), AggregateVerifier.ScheduleConfig({ protocolVersions: IProtocolVersions(address(protocolVersions)), genesisBlockNumber: L2_GENESIS_BLOCK_NUMBER, @@ -220,9 +226,10 @@ contract BaseTest is Test { function _generateIntermediateRoots(uint256 l2BlockNumber, Claim rootClaim) internal pure returns (bytes memory) { bytes32[] memory intermediateRoots = new bytes32[](INTERMEDIATE_ROOTS_COUNT); - uint256 startingL2BlockNumber = l2BlockNumber - BLOCK_INTERVAL; + uint256 startingL2BlockNumber = l2BlockNumber - SLOW_BLOCK_INTERVAL; for (uint256 i = 1; i < INTERMEDIATE_ROOTS_COUNT; i++) { - intermediateRoots[i - 1] = keccak256(abi.encode(startingL2BlockNumber + INTERMEDIATE_BLOCK_INTERVAL * i)); + intermediateRoots[i - 1] = + keccak256(abi.encode(startingL2BlockNumber + SLOW_INTERMEDIATE_BLOCK_INTERVAL * i)); } intermediateRoots[INTERMEDIATE_ROOTS_COUNT - 1] = rootClaim.raw(); diff --git a/test/L1/proofs/Challenge.t.sol b/test/L1/proofs/Challenge.t.sol index 0aca7e4d2..3e425684c 100644 --- a/test/L1/proofs/Challenge.t.sol +++ b/test/L1/proofs/Challenge.t.sol @@ -12,7 +12,7 @@ import { Verifier } from "src/L1/proofs/Verifier.sol"; import { BaseTest } from "./BaseTest.t.sol"; contract ChallengeTest is BaseTest { - uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL - 1; function testChallengeTEEProofWithZKProof() public { AggregateVerifier game = @@ -158,7 +158,7 @@ contract ChallengeTest is BaseTest { private returns (AggregateVerifier) { - currentL2BlockNumber += BLOCK_INTERVAL; + currentL2BlockNumber += SLOW_BLOCK_INTERVAL; Claim rootClaim = _claim(claimSalt); bytes memory proof = _generateProof(proofSalt, proofType); return _createAggregateVerifierGame(prover, rootClaim, currentL2BlockNumber, parent, proof); diff --git a/test/L1/proofs/DisputeGameFactory.t.sol b/test/L1/proofs/DisputeGameFactory.t.sol index 367ecd2a7..a09931e65 100644 --- a/test/L1/proofs/DisputeGameFactory.t.sol +++ b/test/L1/proofs/DisputeGameFactory.t.sol @@ -45,8 +45,10 @@ contract DisputeGameFactory_FakeClone_Harness { abstract contract DisputeGameFactory_TestInit is CommonTest { uint256 internal constant DEFAULT_INIT_BOND = 0.08 ether; uint256 internal constant L2_CHAIN_ID = 111; - uint256 internal constant AGGREGATE_BLOCK_INTERVAL = 100; - uint256 internal constant AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 internal constant AGGREGATE_SLOW_BLOCK_INTERVAL = 100; + uint256 internal constant AGGREGATE_SLOW_INTERMEDIATE_BLOCK_INTERVAL = 10; + uint256 internal constant AGGREGATE_FAST_BLOCK_INTERVAL = 1000; + uint256 internal constant AGGREGATE_FAST_INTERMEDIATE_BLOCK_INTERVAL = 100; uint32 internal constant MAX_GAME_TYPE = 8; address internal constant NON_OWNER = address(0xBEEF); @@ -229,8 +231,12 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { AggregateVerifier.ZkHashes(bytes32(uint256(2)), bytes32(uint256(3))), bytes32(uint256(4)), L2_CHAIN_ID, - AGGREGATE_BLOCK_INTERVAL, - AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL, + AggregateVerifier.IntervalConfig({ + slowBlockInterval: AGGREGATE_SLOW_BLOCK_INTERVAL, + slowIntermediateBlockInterval: AGGREGATE_SLOW_INTERMEDIATE_BLOCK_INTERVAL, + fastBlockInterval: AGGREGATE_FAST_BLOCK_INTERVAL, + fastIntermediateBlockInterval: AGGREGATE_FAST_INTERMEDIATE_BLOCK_INTERVAL + }), AggregateVerifier.ScheduleConfig({ protocolVersions: protocolVersions, genesisBlockNumber: 0, genesisTimestamp: 0, blockTime: 2 }) @@ -244,12 +250,14 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { for (uint256 i = 1; i < intermediateRootsCount; i++) { intermediateRoots = abi.encodePacked( intermediateRoots, - keccak256(abi.encode(startingRoot.l2SequenceNumber + AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL * i)) + keccak256(abi.encode(startingRoot.l2SequenceNumber + AGGREGATE_SLOW_INTERMEDIATE_BLOCK_INTERVAL * i)) ); } intermediateRoots = abi.encodePacked(intermediateRoots, rootClaim.raw()); bytes memory extraData = abi.encodePacked( - startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL, address(anchorStateRegistry), intermediateRoots + startingRoot.l2SequenceNumber + AGGREGATE_SLOW_BLOCK_INTERVAL, + address(anchorStateRegistry), + intermediateRoots ); bytes memory proof = abi.encodePacked( uint8(AggregateVerifier.ProofType.TEE), blockhash(block.number - 1), block.number - 1, bytes32(0) @@ -260,7 +268,7 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { // The claimed L2 block's deterministic timestamp (block number x blockTime) must have // already passed on L1 for the game to be creatable. - vm.warp((startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL) * 2); + vm.warp((startingRoot.l2SequenceNumber + AGGREGATE_SLOW_BLOCK_INTERVAL) * 2); uint256 gameCountBefore = disputeGameFactory.gameCount(); IDisputeGame proxy = disputeGameFactory.createWithInitData{ value: bondAmount }( @@ -275,13 +283,13 @@ contract DisputeGameFactory_Create_Test is DisputeGameFactory_TestInit { assertEq(gameV2.extraData(), extraData); assertEq(gameV2.L2_CHAIN_ID(), L2_CHAIN_ID); assertEq(address(gameV2.gameCreator()), address(this)); - assertEq(gameV2.l2SequenceNumber(), startingRoot.l2SequenceNumber + AGGREGATE_BLOCK_INTERVAL); + assertEq(gameV2.l2SequenceNumber(), startingRoot.l2SequenceNumber + AGGREGATE_SLOW_BLOCK_INTERVAL); assertEq(gameV2.parentAddress(), address(anchorStateRegistry)); assertEq(address(gameV2.DELAYED_WETH()), address(delayedWeth)); assertEq(address(gameV2.anchorStateRegistry()), address(anchorStateRegistry)); assertEq(GameType.unwrap(gameV2.gameType()), GameType.unwrap(GameTypes.AGGREGATE_VERIFIER)); - assertEq(gameV2.BLOCK_INTERVAL(), AGGREGATE_BLOCK_INTERVAL); - assertEq(gameV2.INTERMEDIATE_BLOCK_INTERVAL(), AGGREGATE_INTERMEDIATE_BLOCK_INTERVAL); + assertEq(gameV2.SLOW_BLOCK_INTERVAL(), AGGREGATE_SLOW_BLOCK_INTERVAL); + assertEq(gameV2.SLOW_INTERMEDIATE_BLOCK_INTERVAL(), AGGREGATE_SLOW_INTERMEDIATE_BLOCK_INTERVAL); } } diff --git a/test/L1/proofs/Nullify.t.sol b/test/L1/proofs/Nullify.t.sol index 841bc8cca..f9303dfcf 100644 --- a/test/L1/proofs/Nullify.t.sol +++ b/test/L1/proofs/Nullify.t.sol @@ -10,7 +10,7 @@ import { AggregateVerifier } from "src/L1/proofs/AggregateVerifier.sol"; import { BaseTest } from "./BaseTest.t.sol"; contract NullifyTest is BaseTest { - uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + uint256 private constant LAST_INTERMEDIATE_ROOT_INDEX = SLOW_BLOCK_INTERVAL / SLOW_INTERMEDIATE_BLOCK_INTERVAL - 1; uint256 private constant NO_PROOF_CREDIT_CLAIM_DELAY = 14 days; function testNullifyWithTEEProof() public { @@ -142,7 +142,7 @@ contract NullifyTest is BaseTest { private returns (AggregateVerifier) { - currentL2BlockNumber += BLOCK_INTERVAL; + currentL2BlockNumber += SLOW_BLOCK_INTERVAL; return _createAggregateVerifierGame( prover, _claim(claimSalt), currentL2BlockNumber, parent, _generateProof(proofSalt, proofType) ); diff --git a/test/deploy/SystemDeploy.t.sol b/test/deploy/SystemDeploy.t.sol index 832813952..7f39e14b0 100644 --- a/test/deploy/SystemDeploy.t.sol +++ b/test/deploy/SystemDeploy.t.sol @@ -460,8 +460,10 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { genesisTimestamp: 1, blockTime: 2 }), - multiproofBlockInterval: 100, - multiproofIntermediateBlockInterval: 10, + multiproofSlowBlockInterval: 100, + multiproofSlowIntermediateBlockInterval: 10, + multiproofFastBlockInterval: 1000, + multiproofFastIntermediateBlockInterval: 100, sp1Verifier: ISP1Verifier(address(sp1Verifier)), teeProposer: proposer, teeChallenger: challenger, @@ -571,8 +573,12 @@ contract SystemDeploy_Test is Test, SystemDeployAssertions { l2GenesisBlockNumber: _input.implementationsInput.scheduleConfig.genesisBlockNumber, l2GenesisTimestamp: _input.implementationsInput.scheduleConfig.genesisTimestamp, l2BlockTime: _input.implementationsInput.scheduleConfig.blockTime, - multiproofBlockInterval: _input.implementationsInput.multiproofBlockInterval, - multiproofIntermediateBlockInterval: _input.implementationsInput.multiproofIntermediateBlockInterval, + multiproofSlowBlockInterval: _input.implementationsInput.multiproofSlowBlockInterval, + multiproofSlowIntermediateBlockInterval: _input.implementationsInput + .multiproofSlowIntermediateBlockInterval, + multiproofFastBlockInterval: _input.implementationsInput.multiproofFastBlockInterval, + multiproofFastIntermediateBlockInterval: _input.implementationsInput + .multiproofFastIntermediateBlockInterval, withdrawalDelaySeconds: _input.implementationsInput.withdrawalDelaySeconds }); } diff --git a/test/deploy/SystemDeployAssertions.sol b/test/deploy/SystemDeployAssertions.sol index 2d8d3130b..749ed5dcb 100644 --- a/test/deploy/SystemDeployAssertions.sol +++ b/test/deploy/SystemDeployAssertions.sol @@ -44,8 +44,10 @@ abstract contract SystemDeployAssertions is Test { uint256 l2GenesisBlockNumber; uint64 l2GenesisTimestamp; uint64 l2BlockTime; - uint256 multiproofBlockInterval; - uint256 multiproofIntermediateBlockInterval; + uint256 multiproofSlowBlockInterval; + uint256 multiproofSlowIntermediateBlockInterval; + uint256 multiproofFastBlockInterval; + uint256 multiproofFastIntermediateBlockInterval; uint256 withdrawalDelaySeconds; } @@ -257,9 +259,17 @@ abstract contract SystemDeployAssertions is Test { assertEq(_aggregateVerifier.L2_GENESIS_BLOCK_NUMBER(), _expected.l2GenesisBlockNumber, "AV-132"); assertEq(_aggregateVerifier.L2_GENESIS_TIMESTAMP(), _expected.l2GenesisTimestamp, "AV-134"); assertEq(_aggregateVerifier.L2_BLOCK_TIME(), _expected.l2BlockTime, "AV-136"); - assertEq(_aggregateVerifier.BLOCK_INTERVAL(), _expected.multiproofBlockInterval, "AV-140"); + assertEq(_aggregateVerifier.SLOW_BLOCK_INTERVAL(), _expected.multiproofSlowBlockInterval, "AV-140"); assertEq( - _aggregateVerifier.INTERMEDIATE_BLOCK_INTERVAL(), _expected.multiproofIntermediateBlockInterval, "AV-150" + _aggregateVerifier.SLOW_INTERMEDIATE_BLOCK_INTERVAL(), + _expected.multiproofSlowIntermediateBlockInterval, + "AV-150" + ); + assertEq(_aggregateVerifier.FAST_BLOCK_INTERVAL(), _expected.multiproofFastBlockInterval, "AV-160"); + assertEq( + _aggregateVerifier.FAST_INTERMEDIATE_BLOCK_INTERVAL(), + _expected.multiproofFastIntermediateBlockInterval, + "AV-170" ); }