diff --git a/interfaces/multiproof/IVerifier.sol b/interfaces/multiproof/IVerifier.sol index d6a6b874..e1ae29a5 100644 --- a/interfaces/multiproof/IVerifier.sol +++ b/interfaces/multiproof/IVerifier.sol @@ -2,5 +2,16 @@ pragma solidity 0.8.15; interface IVerifier { + + /// @notice Verifies a proof. + /// @param proofBytes The proof. + /// @param imageId The image ID. + /// @param journal The journal. + /// @return valid Whether the proof is valid. function verify(bytes calldata proofBytes, bytes32 imageId, bytes32 journal) external view returns (bool); + + /// @notice Nullifies the prover 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. + function nullify() external; } diff --git a/scripts/deploy/DeployImplementations.s.sol b/scripts/deploy/DeployImplementations.s.sol index a4676f0d..6ba8570a 100644 --- a/scripts/deploy/DeployImplementations.s.sol +++ b/scripts/deploy/DeployImplementations.s.sol @@ -717,14 +717,14 @@ contract DeployImplementations is Script { } function deployAggregateVerifierImpl(Input memory _input, Output memory _output) private { - address zkVerifier = address(new MockVerifier()); + address zkVerifier = address(new MockVerifier(_output.anchorStateRegistryImpl)); address teeVerifierImpl; { SystemConfigGlobal scgImpl = new SystemConfigGlobal(INitroEnclaveVerifier(_input.nitroEnclaveVerifier)); vm.label(address(scgImpl), "SystemConfigGlobalImpl"); _output.systemConfigGlobalImpl = scgImpl; - teeVerifierImpl = address(new TEEVerifier(scgImpl)); + teeVerifierImpl = address(new TEEVerifier(scgImpl, _output.anchorStateRegistryImpl)); } _output.aggregateVerifierImpl = IVerifier( diff --git a/scripts/multiproof/DeployDevNoNitro.s.sol b/scripts/multiproof/DeployDevNoNitro.s.sol index 04f5cc9e..2507f851 100644 --- a/scripts/multiproof/DeployDevNoNitro.s.sol +++ b/scripts/multiproof/DeployDevNoNitro.s.sol @@ -120,9 +120,9 @@ contract DeployDevNoNitro is Script { vm.startBroadcast(); - _deployTEEContracts(cfg.finalSystemOwner()); _registerProposer(cfg.teeProposer()); _deployInfrastructure(gameType); + _deployTEEContracts(cfg.finalSystemOwner()); _deployAggregateVerifier(gameType); vm.stopBroadcast(); @@ -140,7 +140,9 @@ contract DeployDevNoNitro is Script { ); console.log("DevSystemConfigGlobal:", systemConfigGlobalProxy); - teeVerifier = address(new TEEVerifier(SystemConfigGlobal(systemConfigGlobalProxy))); + teeVerifier = address( + new TEEVerifier(SystemConfigGlobal(systemConfigGlobalProxy), IAnchorStateRegistry(mockAnchorRegistry)) + ); console.log("TEEVerifier:", teeVerifier); } @@ -173,7 +175,7 @@ contract DeployDevNoNitro is Script { } function _deployAggregateVerifier(GameType gameType) internal { - address zkVerifier = address(new MockVerifier()); + address zkVerifier = address(new MockVerifier(IAnchorStateRegistry(mockAnchorRegistry))); console.log("MockVerifier (ZK):", zkVerifier); mockDelayedWETH = address(new MockDelayedWETH()); diff --git a/scripts/multiproof/DeployDevWithNitro.s.sol b/scripts/multiproof/DeployDevWithNitro.s.sol index 875674ae..ce54547f 100644 --- a/scripts/multiproof/DeployDevWithNitro.s.sol +++ b/scripts/multiproof/DeployDevWithNitro.s.sol @@ -154,9 +154,9 @@ contract DeployDevWithNitro is Script { vm.startBroadcast(); - _deployTEEContracts(cfg.finalSystemOwner(), cfg.nitroEnclaveVerifier()); _registerProposer(cfg.teeProposer()); _deployInfrastructure(gameType); + _deployTEEContracts(cfg.finalSystemOwner(), cfg.nitroEnclaveVerifier()); _deployAggregateVerifier(gameType); vm.stopBroadcast(); @@ -175,7 +175,9 @@ contract DeployDevWithNitro is Script { ); console.log("SystemConfigGlobal:", systemConfigGlobalProxy); - teeVerifier = address(new TEEVerifier(SystemConfigGlobal(systemConfigGlobalProxy))); + teeVerifier = address( + new TEEVerifier(SystemConfigGlobal(systemConfigGlobalProxy), IAnchorStateRegistry(mockAnchorRegistry)) + ); console.log("TEEVerifier:", teeVerifier); } @@ -208,7 +210,7 @@ contract DeployDevWithNitro is Script { } function _deployAggregateVerifier(GameType gameType) internal { - address zkVerifier = address(new MockVerifier()); + address zkVerifier = address(new MockVerifier(IAnchorStateRegistry(mockAnchorRegistry))); console.log("MockVerifier (ZK):", zkVerifier); mockDelayedWETH = address(new MockDelayedWETH()); diff --git a/snapshots/abi/DevSystemConfigGlobal.json b/snapshots/abi/DevSystemConfigGlobal.json new file mode 100644 index 00000000..a082b6f7 --- /dev/null +++ b/snapshots/abi/DevSystemConfigGlobal.json @@ -0,0 +1,458 @@ +[ + { + "inputs": [ + { + "internalType": "contract INitroEnclaveVerifier", + "name": "nitroVerifier", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "MAX_AGE", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "NITRO_VERIFIER", + "outputs": [ + { + "internalType": "contract INitroEnclaveVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "pcr0Hash", + "type": "bytes32" + } + ], + "name": "addDevSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "pcr0", + "type": "bytes" + } + ], + "name": "deregisterPCR0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "deregisterSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "initialOwner", + "type": "address" + }, + { + "internalType": "address", + "name": "initialManager", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "isValidProposer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "isValidSigner", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "manager", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "pcr0", + "type": "bytes" + } + ], + "name": "registerPCR0", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "output", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "proofBytes", + "type": "bytes" + } + ], + "name": "registerSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceManagement", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "internalType": "bool", + "name": "isValid", + "type": "bool" + } + ], + "name": "setProposer", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "signerPCR0", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newManager", + "type": "address" + } + ], + "name": "transferManagement", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "validPCR0s", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousManager", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newManager", + "type": "address" + } + ], + "name": "ManagementTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "pcr0Hash", + "type": "bytes32" + } + ], + "name": "PCR0Deregistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "pcr0Hash", + "type": "bytes32" + } + ], + "name": "PCR0Registered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "isValid", + "type": "bool" + } + ], + "name": "ProposerSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "SignerDeregistered", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "pcr0", + "type": "bytes32" + } + ], + "name": "SignerRegistered", + "type": "event" + }, + { + "inputs": [], + "name": "AttestationTooOld", + "type": "error" + }, + { + "inputs": [], + "name": "AttestationVerificationFailed", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPCR0", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKey", + "type": "error" + }, + { + "inputs": [], + "name": "PCR0NotFound", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/abi/MockSystemConfig.json b/snapshots/abi/MockSystemConfig.json new file mode 100644 index 00000000..b36dfe19 --- /dev/null +++ b/snapshots/abi/MockSystemConfig.json @@ -0,0 +1,33 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "guardian", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "paused", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/snapshots/abi/MockVerifier.json b/snapshots/abi/MockVerifier.json new file mode 100644 index 00000000..d36cb2b2 --- /dev/null +++ b/snapshots/abi/MockVerifier.json @@ -0,0 +1,31 @@ +[ + { + "inputs": [ + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "name": "verify", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 08bc12a0..c57d90e7 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -240,12 +240,12 @@ "sourceCodeHash": "0x955bd0c9b47e43219865e4e92abf28d916c96de20cbdf2f94c8ab14d02083759" }, "src/multiproof/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x6e30a9816642f1ea2887f16223868fcd04ac80c3a42a4583dfc611d8331f8674", - "sourceCodeHash": "0xb9786dc79b4b494d81905235f7bda044c5b1a98ad82416829f5f6948be1175cc" + "initCodeHash": "0x24c7c851cb37c11121fcbfc20b2474d9f15122c020ac5b9894e371cf2f669c5b", + "sourceCodeHash": "0x4fb3fe897e702eb2d47cfaf67fa5b9651112580ed47028a5f5b3b810866635af" }, "src/multiproof/AggregateVerifier.sol:AggregateVerifier:dispute": { - "initCodeHash": "0xc59c62a455533735aa9337d2db88b255713bd4dde20d3345265ec2fafe62af70", - "sourceCodeHash": "0xb9786dc79b4b494d81905235f7bda044c5b1a98ad82416829f5f6948be1175cc" + "initCodeHash": "0x317136b944b743a70c447d78e362edc3e17cda44526ee97d90e66584ba64b21c", + "sourceCodeHash": "0x4fb3fe897e702eb2d47cfaf67fa5b9651112580ed47028a5f5b3b810866635af" }, "src/multiproof/tee/SystemConfigGlobal.sol:SystemConfigGlobal": { "initCodeHash": "0x76da4f2a736d7a39a01720e5d900a85fcaa60ba0430fcacbb8ab367f55ba5411", @@ -256,12 +256,12 @@ "sourceCodeHash": "0xa6261402efe0105e2a4f9369818bafb4e65515e51850b44d47504151e1c39d01" }, "src/multiproof/tee/TEEVerifier.sol:TEEVerifier": { - "initCodeHash": "0x78317d9088a26523d938ce0d88ed0134abc60292c0cdb3f8a6a9530638fd9e9a", - "sourceCodeHash": "0x08e6d340b025c5a6c5997ef6fb69e6c14444f9e473b2c5337d6c10b6c89c0b4f" + "initCodeHash": "0x090330a5c64206b523fd5d9e199269268131035c62ff9eb518247a9024c4b703", + "sourceCodeHash": "0x87b0ad3f68294d64b584e54cc719cc3be0624cbab2940a0a341c502dcc69b4fc" }, "src/multiproof/tee/TEEVerifier.sol:TEEVerifier:dispute": { - "initCodeHash": "0x78317d9088a26523d938ce0d88ed0134abc60292c0cdb3f8a6a9530638fd9e9a", - "sourceCodeHash": "0x08e6d340b025c5a6c5997ef6fb69e6c14444f9e473b2c5337d6c10b6c89c0b4f" + "initCodeHash": "0x090330a5c64206b523fd5d9e199269268131035c62ff9eb518247a9024c4b703", + "sourceCodeHash": "0x87b0ad3f68294d64b584e54cc719cc3be0624cbab2940a0a341c502dcc69b4fc" }, "src/revenue-share/FeeDisburser.sol:FeeDisburser": { "initCodeHash": "0x1278027e3756e2989e80c0a7b513e221a5fe0d3dbd9ded108375a29b2c1f3d57", diff --git a/snapshots/storageLayout/DevSystemConfigGlobal.json b/snapshots/storageLayout/DevSystemConfigGlobal.json new file mode 100644 index 00000000..2bd347c5 --- /dev/null +++ b/snapshots/storageLayout/DevSystemConfigGlobal.json @@ -0,0 +1,65 @@ +[ + { + "bytes": "1", + "label": "_initialized", + "offset": 0, + "slot": "0", + "type": "uint8" + }, + { + "bytes": "1", + "label": "_initializing", + "offset": 1, + "slot": "0", + "type": "bool" + }, + { + "bytes": "1600", + "label": "__gap", + "offset": 0, + "slot": "1", + "type": "uint256[50]" + }, + { + "bytes": "20", + "label": "_owner", + "offset": 0, + "slot": "51", + "type": "address" + }, + { + "bytes": "20", + "label": "_manager", + "offset": 0, + "slot": "52", + "type": "address" + }, + { + "bytes": "1536", + "label": "__gap", + "offset": 0, + "slot": "53", + "type": "uint256[48]" + }, + { + "bytes": "32", + "label": "validPCR0s", + "offset": 0, + "slot": "101", + "type": "mapping(bytes32 => bool)" + }, + { + "bytes": "32", + "label": "signerPCR0", + "offset": 0, + "slot": "102", + "type": "mapping(address => bytes32)" + }, + { + "bytes": "32", + "label": "isValidProposer", + "offset": 0, + "slot": "103", + "type": "mapping(address => bool)" + } +] \ No newline at end of file diff --git a/snapshots/storageLayout/MockSystemConfig.json b/snapshots/storageLayout/MockSystemConfig.json new file mode 100644 index 00000000..8ef57702 --- /dev/null +++ b/snapshots/storageLayout/MockSystemConfig.json @@ -0,0 +1,9 @@ +[ + { + "bytes": "20", + "label": "guardian", + "offset": 0, + "slot": "0", + "type": "address" + } +] \ No newline at end of file diff --git a/snapshots/storageLayout/MockVerifier.json b/snapshots/storageLayout/MockVerifier.json new file mode 100644 index 00000000..0637a088 --- /dev/null +++ b/snapshots/storageLayout/MockVerifier.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/multiproof/AggregateVerifier.sol b/src/multiproof/AggregateVerifier.sol index 21d255d4..94af4655 100644 --- a/src/multiproof/AggregateVerifier.sol +++ b/src/multiproof/AggregateVerifier.sol @@ -56,10 +56,6 @@ 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 For when the game no longer accepts proofs and prevents resolution. - int8 internal constant NEGATIVE_PROOF_COUNT = type(int8).min; - //////////////////////////////////////////////////////////////// // Immutables // //////////////////////////////////////////////////////////////// @@ -141,18 +137,20 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The amount of the bond. uint256 public bondAmount; - /// @notice The address of the game that countered this game. - address public counteredByGameAddress; + /// @notice The index of the intermediate root that countered this game. + /// @dev The index is 1-based, so the countered intermediate root index is counteredByIntermediateRootIndexPlusOne - + /// 1. 0 is used to indicate that the game was not countered. + uint256 public counteredByIntermediateRootIndexPlusOne; /// @notice The address that provided a proof of the given type. + /// @dev The address is the zero address if no proof has been provided or the proof has been nullified. mapping(ProofType => address) internal proofTypeToProver; /// @notice The timestamp of the game's expected resolution. Timestamp public expectedResolution; /// @notice The number of proofs provided. - /// @dev Can be negative if a ZK proof is nullified. - int8 public proofCount; + uint8 public proofCount; //////////////////////////////////////////////////////////////// // Events // @@ -164,8 +162,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice Emitted when a proposal with a TEE proof is challenged with a ZK proof. /// @param challenger The address of the challenger. - /// @param game The game used to challenge this proposal. - event Challenged(address indexed challenger, IDisputeGame game); + /// @param intermediateRootIndex The index of the intermediate root that was countered. + event Challenged(address indexed challenger, uint256 intermediateRootIndex); /// @notice Emitted when the game is proved. /// @param prover The address of the prover. @@ -219,18 +217,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice When an invalid proof type is provided. error InvalidProofType(); - /// @notice When no proof was provided. - error NoProofProvided(); - - /// @notice When the countered by game is invalid. - error InvalidCounteredByGame(); - - /// @notice When the countered by game is not resolved. - error CounteredByGameNotResolved(); - - /// @notice When the bond recipient is empty. - error BondRecipientEmpty(); - /// @notice When the intermediate root index is invalid. error InvalidIntermediateRootIndex(); @@ -363,8 +349,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { l2SequenceNumber: parentGame.l2SequenceNumber(), root: Hash.wrap(parentGame.rootClaim().raw()) }); } else { - // When there is no parent game, the starting output root is the anchor state for the game type. - (startingOutputRoot.root, startingOutputRoot.l2SequenceNumber) = ANCHOR_STATE_REGISTRY.getAnchorRoot(); + // When there is no parent game, the starting output root is the starting root in the AnchorStateRegistry. + startingOutputRoot = ANCHOR_STATE_REGISTRY.getStartingAnchorRoot(); } // The block number must be BLOCK_INTERVAL blocks after the starting block number. @@ -405,9 +391,10 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { intermediateOutputRoots() ); - _updateProvingData(proofType, gameCreator()); + _proofVerifiedUpdate(proofType, gameCreator()); - emit Proved(gameCreator(), proofType); + // Set the bond recipient to the creator. It can change if challenged successfully. + bondRecipient = gameCreator(); // Deposit the bond. bondAmount = msg.value; @@ -438,9 +425,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { l2SequenceNumber(), intermediateOutputRoots() ); - _updateProvingData(proofType, msg.sender); - - emit Proved(msg.sender, proofType); + _proofVerifiedUpdate(proofType, msg.sender); } /// @notice Resolves the game after a proof has been provided and enough time has passed. @@ -452,21 +437,25 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // The parent game must have resolved. if (parentGameStatus == GameStatus.IN_PROGRESS) revert ParentGameNotResolved(); + bool isChallenged = counteredByIntermediateRootIndexPlusOne > 0; + // If the parent game's claim is invalid, blacklisted, or retired, then the current game's claim is invalid. if (parentGameStatus == GameStatus.CHALLENGER_WINS) { status = GameStatus.CHALLENGER_WINS; } else { // Game must be completed with a valid proof. if (!gameOver()) revert GameNotOver(); - status = GameStatus.DEFENDER_WINS; + // If the game is challenged, status is CHALLENGER_WINS. + // If the game is not challenged, status is DEFENDER_WINS. + status = isChallenged ? GameStatus.CHALLENGER_WINS : GameStatus.DEFENDER_WINS; } - // casting to 'int256' is safe because 1 <= PROOF_THRESHOLD <= 2 - // forge-lint: disable-next-line(unsafe-typecast) - if (proofCount < int256(PROOF_THRESHOLD)) revert NotEnoughProofs(); + if (proofCount < PROOF_THRESHOLD) revert NotEnoughProofs(); - // Bond is refunded as no challenge was made or parent is invalid. - bondRecipient = gameCreator(); + // Default bond recipient is the creator. We only change if successfully challenged. + if (isChallenged) { + bondRecipient = proofTypeToProver[ProofType.ZK]; + } // Mark the game as resolved. resolvedAt = Timestamp.wrap(uint64(block.timestamp)); emit Resolved(status); @@ -475,10 +464,16 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { } /// @notice Challenges the TEE proof with a ZK proof. - /// @param gameIndex The index of the game used to challenge. - /// @dev The game used to challenge must have a ZK proof for the same - /// block number but a different root claim as the current game. - function challenge(uint256 gameIndex) external { + /// @param proofBytes The proof bytes. + /// @param intermediateRootIndex The index of the intermediate root to challenge. + /// @param intermediateRootToProve The intermediate root that the proof claims to be correct. + function challenge( + bytes calldata proofBytes, + uint256 intermediateRootIndex, + bytes32 intermediateRootToProve + ) + external + { // Can only challenge a game that has not been challenged or resolved yet. if (status != GameStatus.IN_PROGRESS) revert ClaimAlreadyResolved(); @@ -489,41 +484,48 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { if (_getParentGameStatus() == GameStatus.CHALLENGER_WINS) revert InvalidParentGame(); // The TEE prover must not be empty. - // You should nullify the game if a ZK proof has already been provided. if (proofTypeToProver[ProofType.TEE] == address(0)) revert MissingProof(ProofType.TEE); + // You should nullify the game if a ZK proof has already been provided. + // This also prevents another challenge while the current challenge is in progress. if (proofTypeToProver[ProofType.ZK] != address(0)) revert AlreadyProven(ProofType.ZK); - // Prevents challenging after TEE nullification. - if (proofCount != 1) revert NotEnoughProofs(); - - (,, IDisputeGame game) = DISPUTE_GAME_FACTORY.gameAtIndex(gameIndex); + _checkIntermediateRoot(intermediateRootIndex, intermediateRootToProve); - // The game must be a valid game used to challenge. - if (!_isValidChallengingGame(game)) revert InvalidGame(); - - AggregateVerifier challengingGame = AggregateVerifier(address(game)); + // Can only challenge with a ZK proof. + ProofType proofType = ProofType(uint8(proofBytes[0])); + if (proofType != ProofType.ZK) revert InvalidProofType(); - // The ZK prover must not be empty. - if (challengingGame.zkProver() == address(0)) revert MissingProof(ProofType.ZK); + (bytes32 startingRoot, uint256 startingL2SequenceNumber, uint256 endingL2SequenceNumber) = + _getStartingIntermediateRootAndL2SequenceNumbers(intermediateRootIndex); - // Update the counteredBy address. - counteredByGameAddress = address(challengingGame); + _verifyProof( + proofBytes[1:], + proofType, + msg.sender, + l1Head().raw(), + startingRoot, + startingL2SequenceNumber, + intermediateRootToProve, + endingL2SequenceNumber, + abi.encodePacked(intermediateRootToProve) + ); - // Set the game as challenged. - status = GameStatus.CHALLENGER_WINS; + // This allows a ZK nullification to be performed. + proofTypeToProver[proofType] = msg.sender; - // Prevent resolution if any proof was somehow able to be provided later. - proofCount = NEGATIVE_PROOF_COUNT; + // This is only in case the ZK proof is nullified, which would lower the proof count. + // If the ZK is nullified, we allow the remaining TEE proof to resolve. + // The expected resolution time can no longer be increased as both proof types have been submitted. + proofCount += 1; - // Update the expected resolution. - _updateExpectedResolution(); + // We purposely increase the resolution to allow for a ZK nullification. + expectedResolution = Timestamp.wrap(uint64(block.timestamp + SLOW_FINALIZATION_DELAY)); - // Set the bond recipient. - // Bond cannot be claimed until the game used to challenge resolves as DEFENDER_WINS. - bondRecipient = challengingGame.zkProver(); + // Store which intermediate root was countered. + counteredByIntermediateRootIndexPlusOne = intermediateRootIndex + 1; // Emit the challenged event. - emit Challenged(challengingGame.zkProver(), game); + emit Challenged(msg.sender, intermediateRootIndex); } /// @notice Nullifies the game if a soundness issue is found. @@ -538,23 +540,27 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { ) external { - if (status != GameStatus.IN_PROGRESS) revert GameNotInProgress(); - - if (intermediateRootIndex >= intermediateOutputRootsCount()) revert InvalidIntermediateRootIndex(); - - bytes32 proposedIntermediateRoot = intermediateOutputRoot(intermediateRootIndex); - if (proposedIntermediateRoot == intermediateRootToProve) revert IntermediateRootSameAsProposed(); + // Can only nullify if the game is still in progress. + if (status != GameStatus.IN_PROGRESS) revert ClaimAlreadyResolved(); ProofType proofType = ProofType(uint8(proofBytes[0])); - if (proofTypeToProver[proofType] == address(0)) revert MissingProof(proofType); - bytes32 startingRoot = intermediateRootIndex == 0 - ? startingOutputRoot.root.raw() - : intermediateOutputRoot(intermediateRootIndex - 1); - uint256 startingL2SequenceNumber = - startingOutputRoot.l2SequenceNumber + intermediateRootIndex * INTERMEDIATE_BLOCK_INTERVAL; - uint256 endingL2SequenceNumber = startingL2SequenceNumber + INTERMEDIATE_BLOCK_INTERVAL; + // If this game has been challenged, can only nullify the challenged intermediate root and only with ZK. + if (counteredByIntermediateRootIndexPlusOne > 0) { + if (intermediateRootIndex != counteredByIntermediateRootIndexPlusOne - 1) { + revert InvalidIntermediateRootIndex(); + } + if (intermediateRootToProve != intermediateOutputRoot(intermediateRootIndex)) { + revert IntermediateRootMismatch(intermediateRootToProve, intermediateOutputRoot(intermediateRootIndex)); + } + if (proofType != ProofType.ZK) revert InvalidProofType(); + } else { + _checkIntermediateRoot(intermediateRootIndex, intermediateRootToProve); + } + + (bytes32 startingRoot, uint256 startingL2SequenceNumber, uint256 endingL2SequenceNumber) = + _getStartingIntermediateRootAndL2SequenceNumbers(intermediateRootIndex); _verifyProof( proofBytes[1:], @@ -568,33 +574,19 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { abi.encodePacked(intermediateRootToProve) ); + _proofRefutedUpdate(proofType); + + emit Nullified(msg.sender, intermediateRootIndex, intermediateRootToProve); + + // Nullify the verifier to prevent further proof verification. if (proofType == ProofType.ZK) { - // Since a ZK proof vetoes a TEE proof, we make the proof count negative for ZK nullifications. - // This ensures that the game cannot resolve if a TEE proof can somehow be provided later. - proofCount = NEGATIVE_PROOF_COUNT; + // Delete the challenged intermediate root if one existed. + delete counteredByIntermediateRootIndexPlusOne; - // Set the game as challenged so that child games can't resolve. - status = GameStatus.CHALLENGER_WINS; + IVerifier(ZK_VERIFIER).nullify(); } else if (proofType == ProofType.TEE) { - // The status is not updated here to still allow a ZK proof to be provided later. - proofCount -= 1; - - // Increase the expected resolution by the SLOW_FINALIZATION_DELAY. - // This gives us enough time to nullify a ZK proof if it was already provided. - // Otherwise the below _updateExpectedResolution() makes the expected resolution - // the maximum timestamp. - expectedResolution = Timestamp.wrap(uint64(block.timestamp + SLOW_FINALIZATION_DELAY)); + IVerifier(TEE_VERIFIER).nullify(); } - - // If there are no proofs, the expected resolution will be set to type(uint64).max. - // It's not possible to go from FAST_FINALIZATION_DELAY to SLOW_FINALIZATION_DELAY - // as we can only do a ZK nullification in this case, causing proofCount to be negative. - _updateExpectedResolution(); - - // Refund the bond as either a ZK proof was nullified or a ZK proof has to be provided later. - bondRecipient = gameCreator(); - - emit Nullified(msg.sender, intermediateRootIndex, intermediateRootToProve); } /// @notice Claim the credit belonging to the bond recipient. Reverts if the game isn't @@ -603,19 +595,13 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { // The bond must not have been claimed yet. if (bondClaimed) revert NoCreditToClaim(); - // The bond recipient must not be empty. - if (bondRecipient == address(0)) revert BondRecipientEmpty(); - - // If this game was challenged, the countered by game must be valid or else the bond is refunded. - if (counteredByGameAddress != address(0)) { - GameStatus counteredByGameStatus = IDisputeGame(counteredByGameAddress).status(); - if (counteredByGameStatus == GameStatus.IN_PROGRESS) { - revert CounteredByGameNotResolved(); - } - // If the countered by game is invalid or not resolved, the bond is refunded. - if (!_isValidChallengingGame(IDisputeGame(counteredByGameAddress))) { - bondRecipient = gameCreator(); - } + // The game must have resolved or 14 days have passed since creation. + // 14 days chosen as the proof system should have progressed enough so this can't update the + // anchor state registry anymore. + if (expectedResolution.raw() != type(uint64).max) { + if (resolvedAt.raw() == 0) revert GameNotResolved(); + } else { + if (block.timestamp < createdAt.raw() + 14 days) revert GameNotOver(); } if (!bondUnlocked) { @@ -625,10 +611,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { } bondClaimed = true; - // This can fail if this game was challenged and the countered by game is - // blacklisted/retired after it resolved to DEFENDER_WINS. - // The centralized functions in DELAYED_WETH will handle this as it's a already - // a very centralized action to blacklist/retire a valid challenging game. DELAYED_WETH.withdraw(bondRecipient, bondAmount); // Transfer the credit to the bond recipient. @@ -766,13 +748,22 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { return _getArgUint32(0x74); } - /// @notice Updates the expected resolution timestamp. - function _updateExpectedResolution() internal { + function _proofVerifiedUpdate(ProofType proofType, address prover) internal { + proofTypeToProver[proofType] = prover; + proofCount += 1; + + _decreaseExpectedResolution(); + + emit Proved(prover, proofType); + } + + /// @notice Decreases the expected resolution timestamp. + function _decreaseExpectedResolution() internal { uint64 delay; if (proofCount >= 2) { delay = FAST_FINALIZATION_DELAY; - } else if (proofCount >= 1) { + } else if (proofCount == 1) { delay = SLOW_FINALIZATION_DELAY; } else { // If there are no proofs, don't allow the game to resolve. @@ -780,21 +771,36 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { return; } + // Only allow decreases to the expected resolution. uint64 newResolution = uint64(block.timestamp) + delay; expectedResolution = Timestamp.wrap(uint64(FixedPointMathLib.min(newResolution, expectedResolution.raw()))); } - function _updateProvingData(ProofType proofType, address prover) internal { - proofTypeToProver[proofType] = prover; + /// @dev Should only occur if challenged or nullified. + function _proofRefutedUpdate(ProofType proofType) internal { + delete proofTypeToProver[proofType]; + proofCount -= 1; - // Bond can be reclaimed after a ZK proof is provided. - if (proofType == ProofType.ZK) { - bondRecipient = gameCreator(); - } + _increaseExpectedResolution(); + } - proofCount += 1; + function _increaseExpectedResolution() internal { + uint64 delay; + + if (proofCount >= 2) { + delay = FAST_FINALIZATION_DELAY; + } else if (proofCount == 1) { + delay = SLOW_FINALIZATION_DELAY; + } else { + // If there are no proofs, don't allow the game to resolve. + expectedResolution = Timestamp.wrap(type(uint64).max); + return; + } - _updateExpectedResolution(); + // We purposely increase the resolution even if it's longer than it should be + // as this can only occur if there is an issue with the proof system so + // we give enough time to resolve the issue and possibly blacklist this game. + expectedResolution = Timestamp.wrap(uint64(block.timestamp) + delay); } function _verifyProof( @@ -930,22 +936,6 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { && !ANCHOR_STATE_REGISTRY.isGameRetired(game) && (game.status() != GameStatus.CHALLENGER_WINS); } - /// @notice Checks if the game is a valid game used to challenge or nullify. - /// @param game The game to check. - function _isValidChallengingGame(IDisputeGame game) internal view returns (bool) { - return - // The game type must be the same. - game.gameType().raw() == GAME_TYPE.raw() && - // The parent game must be the same. - AggregateVerifier(address(game)).parentIndex() == parentIndex() && - // The block number must be the same. - game.l2SequenceNumber() == l2SequenceNumber() && - // The root claim must be different. - game.rootClaim().raw() != rootClaim().raw() && - // The game must be valid. - _isValidGame(game); - } - /// @notice Verifies that the claimed L1 origin hash matches the actual blockhash. /// @param l1OriginHash The L1 block hash claimed in the proof. /// @param l1OriginNumber The L1 block number claimed in the proof. @@ -983,6 +973,35 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { } } + /// @notice Checks if the intermediate root index is valid and that the intermediate root differs from the proposed + /// intermediate root. @param intermediateRootIndex The index of the intermediate root to check. + /// @param intermediateRootToProve The intermediate root that the proof claims to be correct. + function _checkIntermediateRoot(uint256 intermediateRootIndex, bytes32 intermediateRootToProve) internal view { + if (intermediateRootIndex >= intermediateOutputRootsCount()) revert InvalidIntermediateRootIndex(); + if (intermediateOutputRoot(intermediateRootIndex) == intermediateRootToProve) { + revert IntermediateRootSameAsProposed(); + } + } + + /// @notice Gets the starting intermediate root and the starting and ending L2 sequence numbers. + /// @param intermediateRootIndex The index of the intermediate root to get the starting intermediate root and L2 + /// sequence numbers for. @return startingRoot The starting intermediate root. + /// @return startingL2SequenceNumber The starting L2 sequence number. + /// @return endingL2SequenceNumber The ending L2 sequence number. + function _getStartingIntermediateRootAndL2SequenceNumbers(uint256 intermediateRootIndex) + internal + view + returns (bytes32, uint256, uint256) + { + bytes32 startingRoot = intermediateRootIndex == 0 + ? startingOutputRoot.root.raw() + : intermediateOutputRoot(intermediateRootIndex - 1); + uint256 startingL2SequenceNumber = + startingOutputRoot.l2SequenceNumber + intermediateRootIndex * INTERMEDIATE_BLOCK_INTERVAL; + uint256 endingL2SequenceNumber = startingL2SequenceNumber + INTERMEDIATE_BLOCK_INTERVAL; + return (startingRoot, startingL2SequenceNumber, endingL2SequenceNumber); + } + /// @notice Semantic version. /// @custom:semver 0.1.0 function version() public pure virtual returns (string memory) { diff --git a/src/multiproof/Verifier.sol b/src/multiproof/Verifier.sol new file mode 100644 index 00000000..a9e1e4c3 --- /dev/null +++ b/src/multiproof/Verifier.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.15; + +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IVerifier } from "interfaces/multiproof/IVerifier.sol"; + +abstract contract Verifier is IVerifier { + /// @notice The anchor state registry. + IAnchorStateRegistry public immutable ANCHOR_STATE_REGISTRY; + + /// @notice Whether this verifier has been nullified. + /// @dev This is used to prevent further proof verification after a soundness issue is found. + bool public nullified; + + /// @notice Thrown when the verifier has been nullified. + error Nullified(); + + /// @notice Thrown when the caller trying to nullify is not a proper dispute game. + error NotProperGame(); + + /// @notice Modifier to prevent execution if the verifier has been nullified. + modifier notNullified() { + if (nullified) revert Nullified(); + _; + } + + constructor(IAnchorStateRegistry anchorStateRegistry) { + ANCHOR_STATE_REGISTRY = anchorStateRegistry; + } + + /// @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. + function nullify() external override { + if ( + !ANCHOR_STATE_REGISTRY.isGameProper(IDisputeGame(msg.sender)) + || !ANCHOR_STATE_REGISTRY.isGameRespected(IDisputeGame(msg.sender)) + ) revert NotProperGame(); + nullified = true; + } +} diff --git a/src/multiproof/mocks/MockVerifier.sol b/src/multiproof/mocks/MockVerifier.sol index fff83d15..c9093337 100644 --- a/src/multiproof/mocks/MockVerifier.sol +++ b/src/multiproof/mocks/MockVerifier.sol @@ -1,10 +1,13 @@ // SPDX-License-Identifier: MIT pragma solidity 0.8.15; -import { IVerifier } from "interfaces/multiproof/IVerifier.sol"; +import { Verifier } from "../Verifier.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; -contract MockVerifier is IVerifier { - function verify(bytes calldata, bytes32, bytes32) external pure returns (bool) { +contract MockVerifier is Verifier { + constructor(IAnchorStateRegistry anchorStateRegistry) Verifier(anchorStateRegistry) { } + + function verify(bytes calldata, bytes32, bytes32) external view override notNullified returns (bool) { return true; } } diff --git a/src/multiproof/tee/TEEVerifier.sol b/src/multiproof/tee/TEEVerifier.sol index 5bbf4e90..ea35faa4 100644 --- a/src/multiproof/tee/TEEVerifier.sol +++ b/src/multiproof/tee/TEEVerifier.sol @@ -3,10 +3,11 @@ pragma solidity 0.8.15; import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; -import { IVerifier } from "interfaces/multiproof/IVerifier.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { SystemConfigGlobal } from "./SystemConfigGlobal.sol"; +import { Verifier } from "../Verifier.sol"; /// @title TEEVerifier /// @notice Stateless TEE proof verifier that validates signatures against registered signers. @@ -15,7 +16,7 @@ import { SystemConfigGlobal } from "./SystemConfigGlobal.sol"; /// via AWS Nitro attestation, and that the signer's PCR0 matches the claimed imageId. /// The contract is intentionally stateless - all state related to output proposals and /// L1 origin verification is managed by the calling contract (e.g., AggregateVerifier). -contract TEEVerifier is IVerifier, ISemver { +contract TEEVerifier is Verifier, ISemver { /// @notice The SystemConfigGlobal contract that manages valid TEE signers. /// @dev Signers are registered via AWS Nitro attestation in SystemConfigGlobal. SystemConfigGlobal public immutable SYSTEM_CONFIG_GLOBAL; @@ -37,7 +38,12 @@ contract TEEVerifier is IVerifier, ISemver { /// @notice Constructs the TEEVerifier contract. /// @param systemConfigGlobal The SystemConfigGlobal contract address. - constructor(SystemConfigGlobal systemConfigGlobal) { + constructor( + SystemConfigGlobal systemConfigGlobal, + IAnchorStateRegistry anchorStateRegistry + ) + Verifier(anchorStateRegistry) + { SYSTEM_CONFIG_GLOBAL = systemConfigGlobal; } @@ -46,7 +52,17 @@ contract TEEVerifier is IVerifier, ISemver { /// @param imageId The claimed TEE image hash (PCR0). Must match the signer's registered PCR0. /// @param journal The keccak256 hash of the proof's public inputs. /// @return valid Whether the proof is valid. - function verify(bytes calldata proofBytes, bytes32 imageId, bytes32 journal) external view override returns (bool) { + function verify( + bytes calldata proofBytes, + bytes32 imageId, + bytes32 journal + ) + external + view + override + notNullified + returns (bool) + { if (proofBytes.length < 85) revert InvalidProofFormat(); address proposer = address(bytes20(proofBytes[0:20])); diff --git a/test/multiproof/AggregateVerifier.t.sol b/test/multiproof/AggregateVerifier.t.sol index d7d00dff..a60f081e 100644 --- a/test/multiproof/AggregateVerifier.t.sol +++ b/test/multiproof/AggregateVerifier.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; -import { BadExtraData } from "src/dispute/lib/Errors.sol"; +import { BadExtraData, GameNotResolved } from "src/dispute/lib/Errors.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; @@ -34,7 +34,7 @@ contract AggregateVerifierTest is BaseTest { assertEq( game.extraData(), abi.encodePacked(currentL2BlockNumber, type(uint32).max, game.intermediateOutputRoots()) ); - assertEq(game.bondRecipient(), address(0)); + assertEq(game.bondRecipient(), TEE_PROVER); assertEq(anchorStateRegistry.isGameProper(IDisputeGame(address(game))), true); assertEq(delayedWETH.balanceOf(address(game)), INIT_BOND); assertEq(game.proofCount(), 1); @@ -87,8 +87,8 @@ contract AggregateVerifierTest is BaseTest { AggregateVerifier game = _createAggregateVerifierGame(TEE_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof); - // Cannot claim bond before resolving - vm.expectRevert(AggregateVerifier.BondRecipientEmpty.selector); + // Cannot claim bond before game is over + vm.expectRevert(GameNotResolved.selector); game.claimCredit(); // Resolve after 7 days @@ -120,6 +120,11 @@ contract AggregateVerifierTest is BaseTest { AggregateVerifier game = _createAggregateVerifierGame(ZK_PROVER, rootClaim, currentL2BlockNumber, type(uint32).max, proof); + // Resolve after 7 days + vm.warp(block.timestamp + 7 days); + game.resolve(); + assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); + // Unlock and reclaim bond after delay uint256 balanceBefore = game.gameCreator().balance; game.claimCredit(); @@ -128,11 +133,6 @@ contract AggregateVerifierTest is BaseTest { assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); assertEq(delayedWETH.balanceOf(address(game)), 0); - // Resolve after another 7 days - vm.warp(block.timestamp + 7 days); - game.resolve(); - assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); - // Update AnchorStateRegistry vm.warp(block.timestamp + 1); game.closeGame(); @@ -153,11 +153,7 @@ contract AggregateVerifierTest is BaseTest { _provideProof(game, ZK_PROVER, zkProof); assertEq(game.proofCount(), 2); - // Unlock bond - uint256 balanceBefore = game.gameCreator().balance; - game.claimCredit(); - - // Resolve after 1 day + // Resolve after 1 day (FAST_FINALIZATION_DELAY with 2 proofs) vm.warp(block.timestamp + 1 days); game.resolve(); assertEq(uint8(game.status()), uint8(GameStatus.DEFENDER_WINS)); @@ -170,6 +166,8 @@ contract AggregateVerifierTest is BaseTest { assertEq(l2SequenceNumber, currentL2BlockNumber); // Unlock and reclaim bond after delay + uint256 balanceBefore = game.gameCreator().balance; + game.claimCredit(); vm.warp(block.timestamp + DELAYED_WETH_DELAY); game.claimCredit(); assertEq(game.gameCreator().balance, balanceBefore + INIT_BOND); diff --git a/test/multiproof/BaseTest.t.sol b/test/multiproof/BaseTest.t.sol index 69cd7dd7..c4781146 100644 --- a/test/multiproof/BaseTest.t.sol +++ b/test/multiproof/BaseTest.t.sol @@ -101,8 +101,8 @@ contract BaseTest is Test { delayedWETH = DelayedWETH(payable(address(delayedWETHProxy))); // Deploy the verifiers - teeVerifier = new MockVerifier(); - zkVerifier = new MockVerifier(); + teeVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); + zkVerifier = new MockVerifier(IAnchorStateRegistry(address(anchorStateRegistry))); } function _initializeProxies() internal { diff --git a/test/multiproof/Challenge.t.sol b/test/multiproof/Challenge.t.sol index 3d0ef2f6..aefe5a4d 100644 --- a/test/multiproof/Challenge.t.sol +++ b/test/multiproof/Challenge.t.sol @@ -7,6 +7,7 @@ import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; import { Claim, GameStatus, Hash } from "src/dispute/lib/Types.sol"; import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol"; +import { Verifier } from "src/multiproof/Verifier.sol"; import { BaseTest } from "./BaseTest.t.sol"; @@ -14,44 +15,37 @@ contract ChallengeTest is BaseTest { function testChallengeTEEProofWithZKProof() public { currentL2BlockNumber += BLOCK_INTERVAL; - // Create first game with TEE proof + // Create game with TEE proof Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - AggregateVerifier game1 = + AggregateVerifier game = _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof); - // Create second game with different root claim and ZK proof + // Challenge game with ZK proof Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - AggregateVerifier game2 = - _createAggregateVerifierGame(ZK_PROVER, rootClaim2, currentL2BlockNumber, type(uint32).max, zkProof); + vm.prank(ZK_PROVER); + game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - // Get game index from factory - uint256 gameIndex = factory.gameCount() - 1; + assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); + // 2 proofs so that it can decrease to 1 if ZK is nullified and then the TEE proof can resolve + assertEq(game.proofCount(), 2); - // Challenge game1 with game2 - game1.challenge(gameIndex); + // Resolve after SLOW_FINALIZATION_DELAY + vm.warp(block.timestamp + 7 days); + game.resolve(); - assertEq(uint8(game1.status()), uint8(GameStatus.CHALLENGER_WINS)); - assertEq(game1.bondRecipient(), ZK_PROVER); - address counteredBy = game1.counteredByGameAddress(); - assertEq(counteredBy, address(game2)); - assertEq(game1.proofCount(), -128); - assertEq(game1.expectedResolution().raw(), type(uint64).max); + assertEq(uint8(game.status()), uint8(GameStatus.CHALLENGER_WINS)); + assertEq(game.bondRecipient(), ZK_PROVER); - // Retrieve bond after challenge - vm.warp(block.timestamp + 7 days); - game2.resolve(); - assertEq(uint8(game2.status()), uint8(GameStatus.DEFENDER_WINS)); - assertEq(ZK_PROVER.balance, 0); - assertEq(delayedWETH.balanceOf(address(game1)), INIT_BOND); - game1.claimCredit(); + uint256 balanceBefore = ZK_PROVER.balance; + game.claimCredit(); vm.warp(block.timestamp + DELAYED_WETH_DELAY); - game1.claimCredit(); - assertEq(ZK_PROVER.balance, INIT_BOND); - assertEq(delayedWETH.balanceOf(address(game1)), 0); + game.claimCredit(); + assertEq(ZK_PROVER.balance, balanceBefore + INIT_BOND); + assertEq(delayedWETH.balanceOf(address(game)), 0); } function testChallengeFailsIfNoTEEProof() public { @@ -64,45 +58,16 @@ contract ChallengeTest is BaseTest { AggregateVerifier game1 = _createAggregateVerifierGame(ZK_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, zkProof1); - // Create second game with different root claim and ZK proof - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2"))); + // Challenge game with ZK proof bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - _createAggregateVerifierGame(ZK_PROVER, rootClaim2, currentL2BlockNumber, type(uint32).max, zkProof2); - - uint256 gameIndex = factory.gameCount() - 1; - vm.expectRevert( abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.TEE) ); - game1.challenge(gameIndex); - } - - function testChallengeFailsIfDifferentParentIndex() public { - currentL2BlockNumber += BLOCK_INTERVAL; - - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); - bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); - - AggregateVerifier game1 = - _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof); - - // Create game2 with game1 as parent - uint256 game1Index = factory.gameCount() - 1; - uint256 nextBlockNumber = currentL2BlockNumber + BLOCK_INTERVAL; - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(nextBlockNumber, "zk"))); - bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - - // forge-lint: disable-next-line(unsafe-typecast) - _createAggregateVerifierGame(ZK_PROVER, rootClaim2, nextBlockNumber, uint32(game1Index), zkProof); - - uint256 gameIndex = factory.gameCount() - 1; - - vm.expectRevert(AggregateVerifier.InvalidGame.selector); - game1.challenge(gameIndex); + game1.challenge(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim1.raw()); } - function testChallengeFailsIfChallengingGameHasNoZKProof() public { + function testChallengeFailsIfNotZKProof() public { currentL2BlockNumber += BLOCK_INTERVAL; Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee1"))); @@ -114,12 +79,8 @@ contract ChallengeTest is BaseTest { Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - _createAggregateVerifierGame(TEE_PROVER, rootClaim2, currentL2BlockNumber, type(uint32).max, teeProof2); - - uint256 gameIndex = factory.gameCount() - 1; - - vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.ZK)); - game1.challenge(gameIndex); + vm.expectRevert(AggregateVerifier.InvalidProofType.selector); + game1.challenge(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); } function testChallengeFailsIfGameAlreadyResolved() public { @@ -139,11 +100,8 @@ contract ChallengeTest is BaseTest { Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1"))); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - _createAggregateVerifierGame(ZK_PROVER, rootClaim2, currentL2BlockNumber, type(uint32).max, zkProof); - - uint256 challengeIndex1 = factory.gameCount() - 1; vm.expectRevert(ClaimAlreadyResolved.selector); - game1.challenge(challengeIndex1); + game1.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); } function testChallengeFailsIfParentGameStatusIsChallenged() public { @@ -160,7 +118,7 @@ contract ChallengeTest is BaseTest { currentL2BlockNumber += BLOCK_INTERVAL; // create child game - Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); + Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); bytes memory childProof = _generateProof("child-proof", AggregateVerifier.ProofType.TEE); AggregateVerifier childGame = @@ -170,10 +128,12 @@ contract ChallengeTest is BaseTest { // blacklist parent game anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(parentGame))); - // challenge child game - uint256 childGameIndex = factory.gameCount() - 1; + // challenge child game with ZK proof + Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); + bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + vm.expectRevert(AggregateVerifier.InvalidParentGame.selector); - childGame.challenge(childGameIndex); + childGame.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); } function testChallengeFailsIfGameItselfIsBlacklisted() public { @@ -188,9 +148,11 @@ contract ChallengeTest is BaseTest { anchorStateRegistry.blacklistDisputeGame(IDisputeGame(address(game))); // challenge game - uint256 gameIndex = factory.gameCount() - 1; + Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); + bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + vm.expectRevert(AggregateVerifier.InvalidGame.selector); - game.challenge(gameIndex); + game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); } function testChallengeFailsAfterTEENullification() public { @@ -207,28 +169,37 @@ contract ChallengeTest is BaseTest { game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - // challenge game - uint256 gameIndex = factory.gameCount() - 1; - vm.expectRevert(AggregateVerifier.NotEnoughProofs.selector); - game.challenge(gameIndex); + // challenge game — TEE proof was nullified, so MissingProof(TEE) is expected + Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); + bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); + + vm.expectRevert( + abi.encodeWithSelector(AggregateVerifier.MissingProof.selector, AggregateVerifier.ProofType.TEE) + ); + game.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); } function testChallengeFailsAfterZKNullification() public { currentL2BlockNumber += BLOCK_INTERVAL; - Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk1"))); + Claim rootClaim1 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee"))); + bytes memory teeProof = _generateProof("tee-proof", AggregateVerifier.ProofType.TEE); bytes memory zkProof1 = _generateProof("zk-proof-1", AggregateVerifier.ProofType.ZK); + // create game with both proofs AggregateVerifier game = - _createAggregateVerifierGame(ZK_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, zkProof1); + _createAggregateVerifierGame(ZK_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof); + game.verifyProposalProof(zkProof1); + // nullify ZK proof Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk2"))); bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); - game.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - // challenge game - uint256 gameIndex = factory.gameCount() - 1; - vm.expectRevert(ClaimAlreadyResolved.selector); - game.challenge(gameIndex); + // challenge game — ZK is nullified so Nullified() is expected + Claim rootClaim3 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk3"))); + bytes memory zkProof3 = _generateProof("zk-proof-3", AggregateVerifier.ProofType.ZK); + + vm.expectRevert(Verifier.Nullified.selector); + game.challenge(zkProof3, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim3.raw()); } } diff --git a/test/multiproof/Nullify.t.sol b/test/multiproof/Nullify.t.sol index 9ee51c06..4674fc38 100644 --- a/test/multiproof/Nullify.t.sol +++ b/test/multiproof/Nullify.t.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.15; -import { GameNotInProgress } from "src/dispute/lib/Errors.sol"; +import { ClaimAlreadyResolved } from "src/dispute/lib/Errors.sol"; import { Claim, GameStatus } from "src/dispute/lib/Types.sol"; import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol"; @@ -28,6 +28,9 @@ contract NullifyTest is BaseTest { assertEq(game.proofCount(), 0); assertEq(game.expectedResolution().raw(), type(uint64).max); + // expectedResolution is uint64.max (no proofs left), so must wait 14 days from creation + vm.warp(block.timestamp + 14 days); + uint256 balanceBefore = game.gameCreator().balance; game.claimCredit(); vm.warp(block.timestamp + DELAYED_WETH_DELAY); @@ -50,11 +53,14 @@ contract NullifyTest is BaseTest { game1.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); - assertEq(uint8(game1.status()), uint8(GameStatus.CHALLENGER_WINS)); + assertEq(uint8(game1.status()), uint8(GameStatus.IN_PROGRESS)); assertEq(game1.bondRecipient(), ZK_PROVER); - assertEq(game1.proofCount(), -128); + assertEq(game1.proofCount(), 0); assertEq(game1.expectedResolution().raw(), type(uint64).max); + // expectedResolution is uint64.max (no proofs left), so must wait 14 days from creation + vm.warp(block.timestamp + 14 days); + uint256 balanceBefore = game1.gameCreator().balance; game1.claimCredit(); vm.warp(block.timestamp + DELAYED_WETH_DELAY); @@ -120,7 +126,7 @@ contract NullifyTest is BaseTest { Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); - vm.expectRevert(GameNotInProgress.selector); + vm.expectRevert(ClaimAlreadyResolved.selector); game1.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); } @@ -133,23 +139,25 @@ contract NullifyTest is BaseTest { AggregateVerifier game1 = _createAggregateVerifierGame(TEE_PROVER, rootClaim1, currentL2BlockNumber, type(uint32).max, teeProof1); - // Challenge game1 + // Challenge game1 with ZK proof Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "zk"))); bytes memory zkProof = _generateProof("zk-proof", AggregateVerifier.ProofType.ZK); - AggregateVerifier game2 = - _createAggregateVerifierGame(ZK_PROVER, rootClaim2, currentL2BlockNumber, type(uint32).max, zkProof); - - uint256 challengeIndex = factory.gameCount() - 1; - game1.challenge(challengeIndex); - assertEq(game1.bondRecipient(), ZK_PROVER); + vm.prank(ZK_PROVER); + game1.challenge(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); // Nullify can override challenge - game2.nullify(zkProof, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim1.raw()); + bytes memory zkProof2 = _generateProof("zk-proof-2", AggregateVerifier.ProofType.ZK); + game1.nullify(zkProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim1.raw()); + + assertEq(game1.bondRecipient(), TEE_PROVER); + + // After nullify, only TEE proof remains; expectedResolution = now + 7 days + vm.warp(block.timestamp + 7 days); + game1.resolve(); uint256 balanceBefore = game1.gameCreator().balance; game1.claimCredit(); - assertEq(game1.bondRecipient(), TEE_PROVER); vm.warp(block.timestamp + DELAYED_WETH_DELAY); game1.claimCredit(); assertEq(game1.gameCreator().balance, balanceBefore + INIT_BOND); diff --git a/test/multiproof/TEEVerifier.t.sol b/test/multiproof/TEEVerifier.t.sol index 3a1095d9..3b19a0c5 100644 --- a/test/multiproof/TEEVerifier.t.sol +++ b/test/multiproof/TEEVerifier.t.sol @@ -9,7 +9,9 @@ import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { INitroEnclaveVerifier } from "lib/aws-nitro-enclave-attestation/contracts/src/interfaces/INitroEnclaveVerifier.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { MockAnchorStateRegistry } from "scripts/multiproof/mocks/MockAnchorStateRegistry.sol"; import { DevSystemConfigGlobal } from "src/multiproof/mocks/MockDevSystemConfigGlobal.sol"; import { SystemConfigGlobal } from "src/multiproof/tee/SystemConfigGlobal.sol"; import { TEEVerifier } from "src/multiproof/tee/TEEVerifier.sol"; @@ -18,6 +20,7 @@ contract TEEVerifierTest is Test { TEEVerifier public verifier; DevSystemConfigGlobal public systemConfigGlobal; ProxyAdmin public proxyAdmin; + MockAnchorStateRegistry public anchorStateRegistry; // Test signer - we'll derive address from private key uint256 internal constant SIGNER_PRIVATE_KEY = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; @@ -55,7 +58,10 @@ contract TEEVerifierTest is Test { systemConfigGlobal.setProposer(PROPOSER, true); // Deploy TEEVerifier - verifier = new TEEVerifier(SystemConfigGlobal(address(systemConfigGlobal))); + anchorStateRegistry = new MockAnchorStateRegistry(); + verifier = new TEEVerifier( + SystemConfigGlobal(address(systemConfigGlobal)), IAnchorStateRegistry(address(anchorStateRegistry)) + ); } function testVerifyValidSignature() public view {