diff --git a/deploy-config/hardhat.json b/deploy-config/hardhat.json index 8b79cf118..b696a1481 100644 --- a/deploy-config/hardhat.json +++ b/deploy-config/hardhat.json @@ -84,6 +84,5 @@ "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "multiproofGenesisBlockNumber": 0, "multiproofBlockInterval": 100, - "multiproofIntermediateBlockInterval": 10, - "multiproofProofThreshold": 1 + "multiproofIntermediateBlockInterval": 10 } diff --git a/deploy-config/sepolia.json b/deploy-config/sepolia.json index 52c30a043..85731182f 100644 --- a/deploy-config/sepolia.json +++ b/deploy-config/sepolia.json @@ -77,7 +77,6 @@ "multiproofGenesisBlockNumber": 37223829, "multiproofBlockInterval": 600, "multiproofIntermediateBlockInterval": 30, - "multiproofProofThreshold": 1, "risc0VerifierRouter": "0xb121b667dd2cf27f95f9f5107137696f56f188f6", "risc0SetBuilderImageId": "0x70909b25db0db00f1d4b4016aeb876f53568a3e5a8e6397cb562d79947a02cc9" } diff --git a/interfaces/multiproof/IVerifier.sol b/interfaces/multiproof/IVerifier.sol index 4f5fbd38b..14510de9f 100644 --- a/interfaces/multiproof/IVerifier.sol +++ b/interfaces/multiproof/IVerifier.sol @@ -13,4 +13,7 @@ interface IVerifier { /// @dev Should only occur if a soundness issue is found. /// @dev Should only be callable by a proper dispute game. function nullify() external; + + /// @notice Whether this verifier has been nullified. + function nullified() external view returns (bool); } diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index cc91cad7a..76f53555a 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -290,7 +290,6 @@ contract Deploy is Deployer { l2ChainID: cfg.l2ChainID(), multiproofBlockInterval: cfg.multiproofBlockInterval(), multiproofIntermediateBlockInterval: cfg.multiproofIntermediateBlockInterval(), - multiproofProofThreshold: cfg.multiproofProofThreshold(), protocolVersionsProxy: IProtocolVersions(artifacts.mustGetAddress("ProtocolVersionsProxy")), superchainConfigProxy: superchainConfigProxy, superchainProxyAdmin: superchainProxyAdmin, diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index ddf13fadd..f4522c365 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -106,7 +106,6 @@ contract DeployConfig is Script { uint256 public multiproofGenesisBlockNumber; uint256 public multiproofBlockInterval; uint256 public multiproofIntermediateBlockInterval; - uint256 public multiproofProofThreshold; // RISC Zero / NitroEnclaveVerifier Configuration (reference values; DeployRiscZeroStack takes these as CLI args) bytes32 public risc0SetBuilderImageId; @@ -227,7 +226,6 @@ contract DeployConfig is Script { multiproofGenesisBlockNumber = _readOr(_json, "$.multiproofGenesisBlockNumber", 0); multiproofBlockInterval = _readOr(_json, "$.multiproofBlockInterval", 100); multiproofIntermediateBlockInterval = _readOr(_json, "$.multiproofIntermediateBlockInterval", 10); - multiproofProofThreshold = _readOr(_json, "$.multiproofProofThreshold", 1); risc0SetBuilderImageId = bytes32(_readOr(_json, "$.risc0SetBuilderImageId", 0)); nitroRootCert = bytes32(_readOr(_json, "$.nitroRootCert", 0)); diff --git a/scripts/deploy/DeployImplementations.s.sol b/scripts/deploy/DeployImplementations.s.sol index a77e97296..cc73baa74 100644 --- a/scripts/deploy/DeployImplementations.s.sol +++ b/scripts/deploy/DeployImplementations.s.sol @@ -73,7 +73,6 @@ contract DeployImplementations is Script { uint256 l2ChainID; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; - uint256 multiproofProofThreshold; // Outputs from DeploySuperchain.s.sol. ISuperchainConfig superchainConfigProxy; IProtocolVersions protocolVersionsProxy; @@ -740,8 +739,7 @@ contract DeployImplementations is Script { _input.multiproofConfigHash, _input.l2ChainID, _input.multiproofBlockInterval, - _input.multiproofIntermediateBlockInterval, - _input.multiproofProofThreshold + _input.multiproofIntermediateBlockInterval ) ) ); diff --git a/scripts/multiproof/DeployDevNoNitro.s.sol b/scripts/multiproof/DeployDevNoNitro.s.sol index 7eaa5dea3..13cfb48a0 100644 --- a/scripts/multiproof/DeployDevNoNitro.s.sol +++ b/scripts/multiproof/DeployDevNoNitro.s.sol @@ -84,7 +84,6 @@ import { MockDelayedWETH } from "./mocks/MockDelayedWETH.sol"; contract DeployDevNoNitro is Script { uint256 public constant BLOCK_INTERVAL = 100; uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 10; - uint256 public constant PROOF_THRESHOLD = 1; uint256 public constant INIT_BOND = 0.001 ether; DeployConfig public constant cfg = @@ -183,8 +182,7 @@ contract DeployDevNoNitro is Script { cfg.multiproofConfigHash(), cfg.l2ChainID(), BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, - PROOF_THRESHOLD + INTERMEDIATE_BLOCK_INTERVAL ) ); diff --git a/scripts/multiproof/DeployDevWithNitro.s.sol b/scripts/multiproof/DeployDevWithNitro.s.sol index 3eaa966c6..ed34a3f5e 100644 --- a/scripts/multiproof/DeployDevWithNitro.s.sol +++ b/scripts/multiproof/DeployDevWithNitro.s.sol @@ -68,7 +68,6 @@ import { MockDelayedWETH } from "./mocks/MockDelayedWETH.sol"; contract DeployDevWithNitro is Script { uint256 public constant BLOCK_INTERVAL = 600; uint256 public constant INTERMEDIATE_BLOCK_INTERVAL = 30; - uint256 public constant PROOF_THRESHOLD = 1; uint256 public constant INIT_BOND = 0.00001 ether; DeployConfig public constant cfg = @@ -178,8 +177,7 @@ contract DeployDevWithNitro is Script { cfg.multiproofConfigHash(), cfg.l2ChainID(), BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, - PROOF_THRESHOLD + INTERMEDIATE_BLOCK_INTERVAL ) ); diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index c7ce7ce69..625403be7 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -232,24 +232,24 @@ "sourceCodeHash": "0x955bd0c9b47e43219865e4e92abf28d916c96de20cbdf2f94c8ab14d02083759" }, "src/multiproof/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x54168f8e087c104e6bcc532d2d323327205a14b4ad3597d14d6cadcc1924543b", - "sourceCodeHash": "0x36342bdca5dd7c41f408216377b4b0e1a6d076df6396c77a6350d5d67e499ebe" + "initCodeHash": "0x10bb6a60f21de103d1da9ff310a38f571c53a8113d04a029087648debf3f0341", + "sourceCodeHash": "0xcd4c508223b9b461ec230bde03c1630fe68cd00b82ee84c39b93d9f3c0d71d37" }, "src/multiproof/tee/NitroEnclaveVerifier.sol:NitroEnclaveVerifier": { "initCodeHash": "0x100364f9b0c63a61538386ba91e73dcffba22d3f6dfe8efdbbf5ff347b6fce47", - "sourceCodeHash": "0xed5d92ee1f32f2fde5e89ba278f3142c8113c774f6ce7bd831781a8abe7bddf3" + "sourceCodeHash": "0xbedb66738a0ab18600b2ed21de51d552a210c4cc71b2c971440e325791dc3ce7" }, "src/multiproof/tee/TEEProverRegistry.sol:TEEProverRegistry": { "initCodeHash": "0xfd1942e1c2f59b0aa72b33d698a948a53b6e4cf1040106f173fb5d89f63f57b0", "sourceCodeHash": "0xf1ec1f02f540da659a204b26acf986fdce7d7d63bba67a87923f52453fb92ccb" }, "src/multiproof/tee/TEEVerifier.sol:TEEVerifier": { - "initCodeHash": "0x9f65f2d6e2c43512594f7a08fc0ea475d06f48d230690c3c85cc85dee3a9679a", + "initCodeHash": "0x655576cc21cc5c603d55fb4dd2a2f0ef57b11deeaabd3e539b0a70a5f7e2c9af", "sourceCodeHash": "0xd5e01b4f4e69313e56e51f2f46b7bbe699ef8dc24b8a6385b8ebac4162e05353" }, "src/multiproof/zk/ZKVerifier.sol:ZkVerifier": { - "initCodeHash": "0xb50364a38af3ef7dbfab9a84210d08c5f61105638326e6a442828dfa5afc924d", - "sourceCodeHash": "0x0ffc935dbd768b4aad6c35e2d6826c556191cc5e8f20737045d1bc196b13e19f" + "initCodeHash": "0xcdc5bd5fa8ffad76358b8f34a53b8c0cb36540176c31f82d52546373d0bd5245", + "sourceCodeHash": "0xdb3774707dc5fa59c32636ab896eba284bb6466568bd04583d9767112857d405" }, "src/revenue-share/FeeDisburser.sol:FeeDisburser": { "initCodeHash": "0x1278027e3756e2989e80c0a7b513e221a5fe0d3dbd9ded108375a29b2c1f3d57", @@ -299,4 +299,4 @@ "initCodeHash": "0x2bfce526f82622288333d53ca3f43a0a94306ba1bab99241daa845f8f4b18bd4", "sourceCodeHash": "0xf49d7b0187912a6bb67926a3222ae51121e9239495213c975b3b4b217ee57a1b" } -} +} \ No newline at end of file diff --git a/src/multiproof/AggregateVerifier.sol b/src/multiproof/AggregateVerifier.sol index 30142622b..4c99f1d84 100644 --- a/src/multiproof/AggregateVerifier.sol +++ b/src/multiproof/AggregateVerifier.sol @@ -62,6 +62,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The maximum number of blocks that EIP-2935 can look back (~8192). uint256 public constant EIP2935_WINDOW = 8191; + + /// @notice The minimum number of proofs required to resolve the game. + uint256 public constant PROOF_THRESHOLD = 1; //////////////////////////////////////////////////////////////// // Immutables // //////////////////////////////////////////////////////////////// @@ -109,9 +112,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The game type ID. GameType internal immutable GAME_TYPE; - /// @notice The minimum number of proofs required to resolve the game. - uint256 public immutable PROOF_THRESHOLD; - //////////////////////////////////////////////////////////////// // State Vars // //////////////////////////////////////////////////////////////// @@ -261,7 +261,6 @@ 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 proofThreshold The minimum number of proofs required to resolve the game. constructor( GameType gameType_, IAnchorStateRegistry anchorStateRegistry_, @@ -273,17 +272,13 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { bytes32 configHash, uint256 l2ChainId, uint256 blockInterval, - uint256 intermediateBlockInterval, - uint256 proofThreshold + uint256 intermediateBlockInterval ) { // Block interval and intermediate block interval must be positive and divisible. if (blockInterval == 0 || intermediateBlockInterval == 0 || blockInterval % intermediateBlockInterval != 0) { revert InvalidBlockInterval(blockInterval, intermediateBlockInterval); } - // Proof threshold must be between 1 and 2. - if (proofThreshold != 1 && proofThreshold != 2) revert InvalidProofThreshold(); - // Set up initial game state. GAME_TYPE = gameType_; ANCHOR_STATE_REGISTRY = anchorStateRegistry_; @@ -298,7 +293,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { L2_CHAIN_ID = l2ChainId; BLOCK_INTERVAL = blockInterval; INTERMEDIATE_BLOCK_INTERVAL = intermediateBlockInterval; - PROOF_THRESHOLD = proofThreshold; INITIALIZE_CALLDATA_SIZE = 0x8E + 0x20 * intermediateOutputRootsCount(); } @@ -454,6 +448,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { status = GameStatus.CHALLENGER_WINS; } else { // Game must be completed with a valid proof and enough proofs. + if (_updateProofCount()) { + return status; + } if (!gameOver()) revert GameNotOver(); if (proofCount < PROOF_THRESHOLD) revert NotEnoughProofs(); @@ -592,9 +589,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // Nullify the verifier to prevent further proof verification. if (proofType == ProofType.ZK) { - // Delete the challenged intermediate root if one existed. - delete counteredByIntermediateRootIndexPlusOne; - IVerifier(ZK_VERIFIER).nullify(); } else if (proofType == ProofType.TEE) { IVerifier(TEE_VERIFIER).nullify(); @@ -787,6 +781,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @dev Should only occur if challenged or nullified. function _proofRefutedUpdate(ProofType proofType) internal { delete proofTypeToProver[proofType]; + // A ZK challenge is recorded in `counteredByIntermediateRootIndexPlusOne`; dropping the ZK proof clears it. + if (proofType == ProofType.ZK) { + delete counteredByIntermediateRootIndexPlusOne; + } // Should not be possible, but just in case. if (proofCount == 0) revert NotEnoughProofs(); @@ -812,6 +810,19 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { expectedResolution = Timestamp.wrap(uint64(block.timestamp) + delay); } + /// @notice Updates the proof count and returns true if a proof was nullified. + function _updateProofCount() internal returns (bool) { + if (proofTypeToProver[ProofType.TEE] != address(0) && TEE_VERIFIER.nullified()) { + _proofRefutedUpdate(ProofType.TEE); + return true; + } + if (proofTypeToProver[ProofType.ZK] != address(0) && ZK_VERIFIER.nullified()) { + _proofRefutedUpdate(ProofType.ZK); + return true; + } + return false; + } + function _getDelay() internal view returns (uint64) { if (proofCount >= 2) { return FAST_FINALIZATION_DELAY; diff --git a/src/multiproof/Verifier.sol b/src/multiproof/Verifier.sol index d9505f9ba..1857dde97 100644 --- a/src/multiproof/Verifier.sol +++ b/src/multiproof/Verifier.sol @@ -35,11 +35,13 @@ abstract contract Verifier is IVerifier { /// @notice Nullifies the verifier to prevent further proof verification. /// @dev Should only occur if a soundness issue is found. - /// @dev Should only be callable by a proper dispute game. + /// @dev Should only be callable by a registered, respected, not blacklisted, not retired dispute game. function nullify() external override { if ( - !ANCHOR_STATE_REGISTRY.isGameProper(IDisputeGame(msg.sender)) + !ANCHOR_STATE_REGISTRY.isGameRegistered(IDisputeGame(msg.sender)) || !ANCHOR_STATE_REGISTRY.isGameRespected(IDisputeGame(msg.sender)) + || ANCHOR_STATE_REGISTRY.isGameBlacklisted(IDisputeGame(msg.sender)) + || ANCHOR_STATE_REGISTRY.isGameRetired(IDisputeGame(msg.sender)) ) revert NotProperGame(); nullified = true; diff --git a/src/multiproof/tee/NitroEnclaveVerifier.sol b/src/multiproof/tee/NitroEnclaveVerifier.sol index b38a6ca9d..be4d3bc0f 100644 --- a/src/multiproof/tee/NitroEnclaveVerifier.sol +++ b/src/multiproof/tee/NitroEnclaveVerifier.sol @@ -591,8 +591,11 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver { * 5. Caches newly discovered certificates for future use * * The timestamp validation converts milliseconds to seconds and checks: - * - Attestation is not too old (timestamp + maxTimeDiff >= block.timestamp) - * - Attestation is not from the future (timestamp <= block.timestamp) + * - Attestation is not too old (timestamp + maxTimeDiff > block.timestamp) + * - Attestation is not from the future (timestamp < block.timestamp) + * Note that due to truncating timestamp from milliseconds, to seconds, + * some valid attestations may be rejected. However, this ensures all invalid + * timestamps are rejected. */ function _verifyJournal(VerifierJournal memory journal) internal returns (VerifierJournal memory) { if (journal.result != VerificationResult.Success) { diff --git a/src/multiproof/zk/ZKVerifier.sol b/src/multiproof/zk/ZKVerifier.sol index f254b8bd4..7508eab6e 100644 --- a/src/multiproof/zk/ZKVerifier.sol +++ b/src/multiproof/zk/ZKVerifier.sol @@ -15,9 +15,6 @@ contract ZkVerifier is Verifier { /// @notice The SP1 verifier gateway contract. ISP1Verifier public immutable SP1_VERIFIER; - /// @notice Thrown when SP1 proof verification reverts. - error SP1VerificationFailed(); - /// @param sp1Verifier The deployed SP1 verifier gateway address. /// @param anchorStateRegistry The anchor state registry for nullification checks. constructor(ISP1Verifier sp1Verifier, IAnchorStateRegistry anchorStateRegistry) Verifier(anchorStateRegistry) { diff --git a/test/multiproof/AggregateVerifier.t.sol b/test/multiproof/AggregateVerifier.t.sol index 2e02d4801..c8bf67fd1 100644 --- a/test/multiproof/AggregateVerifier.t.sol +++ b/test/multiproof/AggregateVerifier.t.sol @@ -375,8 +375,7 @@ contract AggregateVerifierTest is BaseTest { CONFIG_HASH, L2_CHAIN_ID, 0, - INTERMEDIATE_BLOCK_INTERVAL, - PROOF_THRESHOLD + INTERMEDIATE_BLOCK_INTERVAL ); // Case 2: INTERMEDIATE_BLOCK_INTERVAL is 0 @@ -392,8 +391,7 @@ contract AggregateVerifierTest is BaseTest { CONFIG_HASH, L2_CHAIN_ID, BLOCK_INTERVAL, - 0, - PROOF_THRESHOLD + 0 ); // Case 3: BLOCK_INTERVAL is not divisible by INTERMEDIATE_BLOCK_INTERVAL @@ -409,44 +407,7 @@ contract AggregateVerifierTest is BaseTest { CONFIG_HASH, L2_CHAIN_ID, 3, - 2, - PROOF_THRESHOLD - ); - } - - function testDeployWithInvalidProofThreshold() public { - // Case 1: PROOF_THRESHOLD is 0 - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.InvalidProofThreshold.selector)); - new AggregateVerifier( - AGGREGATE_VERIFIER_GAME_TYPE, - IAnchorStateRegistry(address(anchorStateRegistry)), - IDelayedWETH(payable(address(delayedWETH))), - IVerifier(address(teeVerifier)), - IVerifier(address(zkVerifier)), - TEE_IMAGE_HASH, - AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), - CONFIG_HASH, - L2_CHAIN_ID, - BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, - 0 - ); - - // Case 2: PROOF_THRESHOLD is > 2 - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.InvalidProofThreshold.selector)); - new AggregateVerifier( - AGGREGATE_VERIFIER_GAME_TYPE, - IAnchorStateRegistry(address(anchorStateRegistry)), - IDelayedWETH(payable(address(delayedWETH))), - IVerifier(address(teeVerifier)), - IVerifier(address(zkVerifier)), - TEE_IMAGE_HASH, - AggregateVerifier.ZkHashes(ZK_RANGE_HASH, ZK_AGGREGATE_HASH), - CONFIG_HASH, - L2_CHAIN_ID, - BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, - 3 + 2 ); } } diff --git a/test/multiproof/BaseTest.t.sol b/test/multiproof/BaseTest.t.sol index f7f8d36e2..55c4d311d 100644 --- a/test/multiproof/BaseTest.t.sol +++ b/test/multiproof/BaseTest.t.sol @@ -40,7 +40,6 @@ contract BaseTest is Test { uint256 public constant DELAYED_WETH_DELAY = 1 days; // Finality delay handled by the AggregateVerifier uint256 public constant FINALITY_DELAY = 0 days; - uint256 public constant PROOF_THRESHOLD = 1; uint256 public currentL2BlockNumber = 0; @@ -136,8 +135,7 @@ contract BaseTest is Test { CONFIG_HASH, L2_CHAIN_ID, BLOCK_INTERVAL, - INTERMEDIATE_BLOCK_INTERVAL, - PROOF_THRESHOLD + INTERMEDIATE_BLOCK_INTERVAL ); // Set the implementation for the aggregate verifier diff --git a/test/multiproof/Challenge.t.sol b/test/multiproof/Challenge.t.sol index d9292fdcd..b36cf03e4 100644 --- a/test/multiproof/Challenge.t.sol +++ b/test/multiproof/Challenge.t.sol @@ -208,4 +208,102 @@ contract ChallengeTest is BaseTest { vm.expectRevert(Verifier.Nullified.selector); game.challenge(zkProof3, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); } + + /// @notice A TEE+ZK challenge on game A is cleared when another game nullifies the shared ZK verifier; A then + /// resolves as defender after `SLOW_FINALIZATION_DELAY`. + function testChallengeRemovedWhenZkVerifierNullifiedByOtherGame() public { + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-challenge"))); + bytes memory teeProofA = _generateProof("tee-ch-a", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameA = _createAggregateVerifierGame( + TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + ); + + Claim rootChallenge = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-challenge"))); + bytes memory zkChallenge = _generateProof("zk-challenge", AggregateVerifier.ProofType.ZK); + vm.prank(ZK_PROVER); + gameA.challenge(zkChallenge, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootChallenge.raw()); + + assertEq(gameA.proofCount(), 2); + assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); + + currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-only-b"))); + bytes memory zkProofB = _generateProof("zk-init-b", AggregateVerifier.ProofType.ZK); + AggregateVerifier gameB = + _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); + + Claim rootNullifyB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-nullify-b"))); + bytes memory zkNullifyB = _generateProof("zk-nullify-b", AggregateVerifier.ProofType.ZK); + uint256 lastIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + gameB.nullify(zkNullifyB, lastIdx, rootNullifyB.raw()); + assertTrue(zkVerifier.nullified()); + + assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + assertEq(gameA.proofCount(), 1); + assertEq(gameA.counteredByIntermediateRootIndexPlusOne(), 0); + assertEq(address(gameA.zkProver()), address(0)); + + vm.warp(block.timestamp + 7 days); + assertEq(uint8(gameA.resolve()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(gameA.bondRecipient(), TEE_PROVER); + + uint256 balanceBefore = TEE_PROVER.balance; + gameA.claimCredit(); + vm.warp(block.timestamp + DELAYED_WETH_DELAY); + gameA.claimCredit(); + assertEq(TEE_PROVER.balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(gameA)), 0); + } + + /// @notice Game A is created with TEE and challenged with ZK. Another game nullifies the shared TEE verifier. + /// The first `resolve` persists the TEE refutation; after `SLOW_FINALIZATION_DELAY`, A finalizes as + /// challenger wins and the bond goes to the ZK challenger. + function testChallengeWinsWhenSharedTeeVerifierNullifiedByOtherGame() public { + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-challenge-tee-null"))); + bytes memory teeProofA = _generateProof("tee-proof-a", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameA = _createAggregateVerifierGame( + TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + ); + + Claim rootChallenge = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-challenge"))); + bytes memory zkChallenge = _generateProof("zk-challenge", AggregateVerifier.ProofType.ZK); + vm.prank(ZK_PROVER); + gameA.challenge(zkChallenge, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootChallenge.raw()); + + assertEq(gameA.proofCount(), 2); + assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); + + currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-only-b"))); + bytes memory teeProofB = _generateProof("tee-init-b", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameB = + _createAggregateVerifierGame(TEE_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), teeProofB); + + Claim rootNullifyB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee-nullify-b"))); + bytes memory teeNullifyB = _generateProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); + uint256 lastIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + gameB.nullify(teeNullifyB, lastIdx, rootNullifyB.raw()); + assertTrue(teeVerifier.nullified()); + + assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + assertEq(gameA.proofCount(), 1); + assertGt(gameA.counteredByIntermediateRootIndexPlusOne(), 0); + assertEq(address(gameA.teeProver()), address(0)); + assertEq(gameA.zkProver(), ZK_PROVER); + + vm.warp(block.timestamp + 7 days); + assertEq(uint8(gameA.resolve()), uint8(GameStatus.CHALLENGER_WINS)); + assertEq(gameA.bondRecipient(), ZK_PROVER); + + uint256 balanceBefore = ZK_PROVER.balance; + gameA.claimCredit(); + vm.warp(block.timestamp + DELAYED_WETH_DELAY); + gameA.claimCredit(); + assertEq(ZK_PROVER.balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(gameA)), 0); + } } diff --git a/test/multiproof/Nullify.t.sol b/test/multiproof/Nullify.t.sol index c1057f10d..5bc8c4aa9 100644 --- a/test/multiproof/Nullify.t.sol +++ b/test/multiproof/Nullify.t.sol @@ -169,4 +169,125 @@ contract NullifyTest is BaseTest { assertEq(game1.gameCreator().balance, balanceBefore + INIT_BOND); assertEq(delayedWETH.balanceOf(address(game1)), 0); } + + /// @notice `resolve` runs `_updateProofCount`; when the shared TEE verifier was nullified by another game, + /// refutation persists and `resolve` returns early `IN_PROGRESS` (no `Resolved` event) instead of + /// reverting. @dev All clones share the same `MockVerifier` TEE instance; `Verifier.nullify` requires a proper + /// factory game. + function testResolveEarlyReturnWhenSharedTeeVerifierNullifiedByAnotherGame() public { + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "game-a"))); + bytes memory teeProofA = _generateProof("tee-proof-a", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameA = _createAggregateVerifierGame( + TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + ); + + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "game-b"))); + bytes memory teeProofB = _generateProof("tee-proof-b", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameB = + _createAggregateVerifierGame(TEE_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), teeProofB); + + vm.warp(block.timestamp + 7 days); + assertTrue(gameA.gameOver()); + assertEq(gameA.proofCount(), 1); + + Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "nullify-b"))); + bytes memory teeProofNullify = _generateProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); + uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + gameB.nullify(teeProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); + + assertTrue(teeVerifier.nullified()); + assertEq(gameA.proofCount(), 1); + + assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + assertEq(gameA.proofCount(), 0); + assertEq(gameA.expectedResolution().raw(), type(uint64).max); + + vm.expectRevert(AggregateVerifier.GameNotOver.selector); + gameA.resolve(); + } + + /// @notice Same as `testResolveEarlyReturnWhenSharedTeeVerifierNullifiedByAnotherGame` but for the shared ZK + /// verifier. + function testResolveEarlyReturnWhenSharedZkVerifierNullifiedByAnotherGame() public { + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-game-a"))); + bytes memory zkProofA = _generateProof("zk-proof-a", AggregateVerifier.ProofType.ZK); + AggregateVerifier gameA = _createAggregateVerifierGame( + ZK_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), zkProofA + ); + + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-game-b"))); + bytes memory zkProofB = _generateProof("zk-proof-b", AggregateVerifier.ProofType.ZK); + AggregateVerifier gameB = + _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); + + vm.warp(block.timestamp + 7 days); + assertTrue(gameA.gameOver()); + assertEq(gameA.proofCount(), 1); + + Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk-nullify-b"))); + bytes memory zkProofNullify = _generateProof("zk-nullify-b", AggregateVerifier.ProofType.ZK); + uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + gameB.nullify(zkProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); + + assertTrue(zkVerifier.nullified()); + assertEq(gameA.proofCount(), 1); + + assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + assertEq(gameA.proofCount(), 0); + assertEq(gameA.expectedResolution().raw(), type(uint64).max); + + vm.expectRevert(AggregateVerifier.GameNotOver.selector); + gameA.resolve(); + } + + /// @notice With TEE + ZK, the fast window is 1 day. Another game nullifies the shared ZK verifier; the first + /// `resolve` persists the ZK refutation and returns `IN_PROGRESS`. After `SLOW_FINALIZATION_DELAY` + /// (7 days) from that moment, a second `resolve` finalizes with only the TEE proof. + function testTwoProofsResolveDelayedAfterExternalVerifierNullify() public { + currentL2BlockNumber += BLOCK_INTERVAL; + + Claim rootClaimA = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-a"))); + bytes memory teeProofA = _generateProof("tee-dual-a", AggregateVerifier.ProofType.TEE); + AggregateVerifier gameA = _createAggregateVerifierGame( + TEE_PROVER, rootClaimA, currentL2BlockNumber, address(anchorStateRegistry), teeProofA + ); + + bytes memory zkProofA = _generateProof("zk-dual-a", AggregateVerifier.ProofType.ZK); + vm.prank(ZK_PROVER); + gameA.verifyProposalProof(zkProofA); + + assertEq(gameA.proofCount(), 2); + assertEq(gameA.expectedResolution().raw(), block.timestamp + 1 days); + + vm.warp(block.timestamp + 1 days); + assertTrue(gameA.gameOver()); + + currentL2BlockNumber += BLOCK_INTERVAL; + Claim rootClaimB = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-b"))); + bytes memory zkProofB = _generateProof("zk-dual-b", AggregateVerifier.ProofType.ZK); + AggregateVerifier gameB = + _createAggregateVerifierGame(ZK_PROVER, rootClaimB, currentL2BlockNumber, address(gameA), zkProofB); + + Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "dual-nullify-b"))); + bytes memory zkProofNullify = _generateProof("zk-nullify-dual", AggregateVerifier.ProofType.ZK); + uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; + gameB.nullify(zkProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); + assertTrue(zkVerifier.nullified()); + + assertEq(uint8(gameA.resolve()), uint8(GameStatus.IN_PROGRESS)); + assertEq(gameA.proofCount(), 1); + assertEq(gameA.expectedResolution().raw(), block.timestamp + 7 days); + + vm.warp(block.timestamp + 7 days); + assertEq(uint8(gameA.resolve()), uint8(GameStatus.DEFENDER_WINS)); + assertEq(uint8(gameA.status()), uint8(GameStatus.DEFENDER_WINS)); + } } diff --git a/test/opcm/DeployImplementations.t.sol b/test/opcm/DeployImplementations.t.sol index 73315b82f..bfbcdbf50 100644 --- a/test/opcm/DeployImplementations.t.sol +++ b/test/opcm/DeployImplementations.t.sol @@ -243,7 +243,6 @@ contract DeployImplementations_Test is Test, FeatureFlags { 8453, // l2ChainID 100, // multiproofBlockInterval 10, // multiproofIntermediateBlockInterval - 1, // multiproofProofThreshold superchainConfigProxy, protocolVersionsProxy, superchainProxyAdmin, @@ -539,7 +538,6 @@ contract DeployImplementations_Test is Test, FeatureFlags { 8453, // l2ChainID 100, // multiproofBlockInterval 10, // multiproofIntermediateBlockInterval - 1, // multiproofProofThreshold superchainConfigProxy, protocolVersionsProxy, superchainProxyAdmin, diff --git a/test/opcm/DeployOPChain.t.sol b/test/opcm/DeployOPChain.t.sol index b8520079c..cb2875a55 100644 --- a/test/opcm/DeployOPChain.t.sol +++ b/test/opcm/DeployOPChain.t.sol @@ -101,7 +101,6 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { l2ChainID: 8453, multiproofBlockInterval: 100, multiproofIntermediateBlockInterval: 10, - multiproofProofThreshold: 1, superchainConfigProxy: dso.superchainConfigProxy, protocolVersionsProxy: dso.protocolVersionsProxy, superchainProxyAdmin: dso.superchainProxyAdmin,