diff --git a/deploy-config/hardhat.json b/deploy-config/hardhat.json index cc9b968f4..6f65b4940 100644 --- a/deploy-config/hardhat.json +++ b/deploy-config/hardhat.json @@ -74,6 +74,7 @@ "zkRangeHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "zkAggregationHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "nitroEnclaveVerifier": "0x0000000000000000000000000000000000000000", + "tdxVerifier": "0x0000000000000000000000000000000000000001", "multiproofGenesisOutputRoot": "0x0000000000000000000000000000000000000000000000000000000000000001", "multiproofGenesisBlockNumber": 0, "multiproofBlockInterval": 100, diff --git a/deploy-config/sepolia.json b/deploy-config/sepolia.json index 3cfbe7621..a09893dcc 100644 --- a/deploy-config/sepolia.json +++ b/deploy-config/sepolia.json @@ -1,5 +1,5 @@ { - "finalSystemOwner": "0x6e427c3212C0b63BE0C382F97715D49b011bFF33", + "finalSystemOwner": "0x8C1a617BdB47342F9C17Ac8750E0b070c372C721", "superchainConfigGuardian": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", "l1StartingBlockTag": "0x48f520cf4ddaf34c8336e6e490632ea3cf1e5e93b0b2bc6e917557e31845371b", "l1ChainID": 11155111, @@ -58,7 +58,7 @@ "useFaultProofs": true, "useRevenueShare": true, "chainFeesRecipient": "0xfd1D2e729aE8eEe2E146c033bf4400fE75284301", - "teeImageHash": "0xd04c8147410930fade36986c9357fd9a7591231e38e6e87e51183bd5909f509c", + "teeImageHash": "0xa227306080459e4bcf1324b229b344af4469846f9861aa8aff450f35046df9d6", "multiproofConfigHash": "0x12e9c45f19f9817c6d4385fad29e7a70c355502cf0883e76a9a7e478a85d1360", "multiproofGameType": 621, "teeProposer": "0xdb84125f2f4229c81c579f41bc129c71b174eb58", @@ -66,10 +66,11 @@ "zkRangeHash": "0x51708a6b4a3b800a14607e902c1aad47719905c12698a3e01ca8b3321761bc52", "zkAggregationHash": "0x005aa369dd9445e172018a9eaa4a0f9767b2c2079ece90ca120422b3c4c65f11", "nitroEnclaveVerifier": "0x77461a6434fFE3435206B19658F33274f3104e07", + "tdxVerifier": "0x5431a6Fd5365e9629BD2B0969e54c4f09d6781bA", "multiproofGenesisOutputRoot": "0xbc273d5876d1858ecd5aaf4ce4eaf16c73f0187ca4271b774ed5da7d2254ba79", "multiproofGenesisBlockNumber": 37223829, "multiproofBlockInterval": 600, "multiproofIntermediateBlockInterval": 30, - "risc0VerifierRouter": "0xb121b667dd2cf27f95f9f5107137696f56f188f6", + "risc0VerifierRouter": "0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187", "risc0SetBuilderImageId": "0x70909b25db0db00f1d4b4016aeb876f53568a3e5a8e6397cb562d79947a02cc9" } diff --git a/interfaces/multiproof/tee/ITDXVerifier.sol b/interfaces/multiproof/tee/ITDXVerifier.sol new file mode 100644 index 000000000..24fc2a6ed --- /dev/null +++ b/interfaces/multiproof/tee/ITDXVerifier.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { ZkCoProcessorType, ZkCoProcessorConfig } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; + +/// @notice Statuses that a TDX quote/collateral verifier may emit. +/// @dev Unknown is index 0 so uninitialized values fail closed. +enum TDXVerificationResult { + Unknown, + Success, + InvalidQuote, + QuoteSignatureInvalid, + RootCaNotTrusted, + PckCertChainInvalid, + TcbInfoInvalid, + QeIdentityInvalid, + TcbStatusNotAllowed, + CollateralExpired, + InvalidTimestamp, + ReportDataMismatch +} + +/// @notice Intel TDX TCB status reduced to the statuses this contract's policy needs. +/// @dev Unknown is index 0 so uninitialized values fail closed. +enum TDXTcbStatus { + Unknown, + UpToDate, + SwHardeningNeeded, + ConfigurationNeeded, + ConfigurationAndSwHardeningNeeded, + OutOfDate, + OutOfDateConfigurationNeeded, + Revoked +} + +/// @notice Public journal emitted by the off-chain/ZK TDX DCAP verifier. +/// @param result Overall verification result after quote and collateral validation in the guest. +/// @param tcbStatus Intel TDX TCB status for the platform. +/// @param timestamp Quote timestamp in milliseconds since Unix epoch. +/// @param collateralExpiration Earliest expiration timestamp in seconds across accepted collateral. +/// @param rootCaHash Hash of the Intel root CA used to validate the PCK/collateral signing chains. +/// @param pckCertHash Hash of the PCK leaf certificate that signed the quote attestation key chain. +/// @param tcbInfoHash Hash of the TCB info collateral consumed by the guest. +/// @param qeIdentityHash Hash of the QE identity collateral consumed by the guest. +/// @param publicKey Uncompressed secp256k1 public key: 0x04 || x || y. +/// @param signer Ethereum address derived from publicKey. +/// @param imageHash Multiproof-compatible image hash derived from MRTD and RTMR0-3. +/// @param mrTdHash keccak256 hash of the 48-byte MRTD measurement. +/// @param reportDataPrefix First 32 bytes of TDREPORT.REPORTDATA. +/// @param reportDataSuffix Last 32 bytes of TDREPORT.REPORTDATA, available for app-specific binding. +struct TDXVerifierJournal { + TDXVerificationResult result; + TDXTcbStatus tcbStatus; + uint64 timestamp; + uint64 collateralExpiration; + bytes32 rootCaHash; + bytes32 pckCertHash; + bytes32 tcbInfoHash; + bytes32 qeIdentityHash; + bytes publicKey; + address signer; + bytes32 imageHash; + bytes32 mrTdHash; + bytes32 reportDataPrefix; + bytes32 reportDataSuffix; +} + +/// @title ITDXVerifier +/// @notice Interface for Intel TDX quote verification used by TDX-aware TEE prover registries. +interface ITDXVerifier { + /// @notice Verifies a ZK proof of Intel TDX DCAP quote verification and returns attested signer metadata. + /// @param output ABI-encoded TDXVerifierJournal public values from the ZK verifier guest. + /// @param zkCoprocessor ZK proving system used to generate the proof. + /// @param proofBytes ZK proof bytes. + /// @return journal Verified TDX attestation metadata. + function verify( + bytes calldata output, + ZkCoProcessorType zkCoprocessor, + bytes calldata proofBytes + ) + external + returns (TDXVerifierJournal memory journal); + + /// @notice Retrieves the configuration for a specific coprocessor. + function getZkConfig(ZkCoProcessorType zkCoprocessor) external view returns (ZkCoProcessorConfig memory); + + /// @notice Returns whether a TCB status is accepted by verifier policy. + function allowedTcbStatuses(TDXTcbStatus status) external view returns (bool); + + /// @notice Updates the address authorized to submit verified proofs. + function setProofSubmitter(address newProofSubmitter) external; +} diff --git a/scripts/deploy/Deploy.s.sol b/scripts/deploy/Deploy.s.sol index d4943d2f5..250fa6ff3 100644 --- a/scripts/deploy/Deploy.s.sol +++ b/scripts/deploy/Deploy.s.sol @@ -238,6 +238,7 @@ contract Deploy is Deployer { multiproofConfigHash: cfg.multiproofConfigHash(), multiproofGameType: cfg.multiproofGameType(), nitroEnclaveVerifier: cfg.nitroEnclaveVerifier(), + tdxVerifier: cfg.tdxVerifier(), l2ChainID: cfg.l2ChainID(), multiproofBlockInterval: cfg.multiproofBlockInterval(), multiproofIntermediateBlockInterval: cfg.multiproofIntermediateBlockInterval(), diff --git a/scripts/deploy/DeployConfig.s.sol b/scripts/deploy/DeployConfig.s.sol index 890843946..5473e27cf 100644 --- a/scripts/deploy/DeployConfig.s.sol +++ b/scripts/deploy/DeployConfig.s.sol @@ -92,6 +92,7 @@ contract DeployConfig is Script { bytes32 public zkRangeHash; bytes32 public zkAggregationHash; address public nitroEnclaveVerifier; + address public tdxVerifier; bytes32 public multiproofGenesisOutputRoot; uint256 public multiproofGenesisBlockNumber; uint256 public multiproofBlockInterval; @@ -204,6 +205,7 @@ contract DeployConfig is Script { zkRangeHash = stdJson.readBytes32(_json, "$.zkRangeHash"); zkAggregationHash = stdJson.readBytes32(_json, "$.zkAggregationHash"); nitroEnclaveVerifier = _readOr(_json, "$.nitroEnclaveVerifier", address(0)); + tdxVerifier = _readOr(_json, "$.tdxVerifier", address(0)); multiproofGenesisOutputRoot = bytes32(_readOr(_json, "$.multiproofGenesisOutputRoot", uint256(1))); multiproofGenesisBlockNumber = _readOr(_json, "$.multiproofGenesisBlockNumber", 0); multiproofBlockInterval = _readOr(_json, "$.multiproofBlockInterval", 100); diff --git a/scripts/deploy/DeployImplementations.s.sol b/scripts/deploy/DeployImplementations.s.sol index 75c5dec36..4536e267b 100644 --- a/scripts/deploy/DeployImplementations.s.sol +++ b/scripts/deploy/DeployImplementations.s.sol @@ -44,6 +44,7 @@ import { MockVerifier } from "src/multiproof/mocks/MockVerifier.sol"; import { TEEVerifier } from "src/multiproof/tee/TEEVerifier.sol"; import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol"; import { GameType } from "src/dispute/lib/Types.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; contract DeployImplementations is Script { struct Input { @@ -64,6 +65,7 @@ contract DeployImplementations is Script { bytes32 multiproofConfigHash; uint256 multiproofGameType; address nitroEnclaveVerifier; + address tdxVerifier; uint256 l2ChainID; uint256 multiproofBlockInterval; uint256 multiproofIntermediateBlockInterval; @@ -613,7 +615,9 @@ contract DeployImplementations is Script { address teeVerifierImpl; { TEEProverRegistry scgImpl = new TEEProverRegistry( - INitroEnclaveVerifier(_input.nitroEnclaveVerifier), IDisputeGameFactory(address(1)) + INitroEnclaveVerifier(_input.nitroEnclaveVerifier), + ITDXVerifier(_input.tdxVerifier), + IDisputeGameFactory(address(1)) ); vm.label(address(scgImpl), "TEEProverRegistryImpl"); _output.teeProverRegistryImpl = scgImpl; @@ -678,6 +682,7 @@ contract DeployImplementations is Script { "DeployImplementations: disputeGameFinalityDelaySeconds not set" ); require(_input.mipsVersion != 0, "DeployImplementations: mipsVersion not set"); + require(_input.tdxVerifier != address(0), "DeployImplementations: tdxVerifier not set"); require( address(_input.superchainConfigProxy) != address(0), "DeployImplementations: superchainConfigProxy not set" ); diff --git a/scripts/multiproof/DeployDevNoNitro.s.sol b/scripts/multiproof/DeployDevNoNitro.s.sol index 13cfb48a0..d142b952c 100644 --- a/scripts/multiproof/DeployDevNoNitro.s.sol +++ b/scripts/multiproof/DeployDevNoNitro.s.sol @@ -26,10 +26,14 @@ pragma solidity 0.8.15; * SIGNER REGISTRATION (SIMPLIFIED) * ───────────────────────────────────────────────────────────────────────────────── * - * After deployment, register a signer with a single call: + * After deployment, register one dev Nitro signer and one dev TDX signer: * * cast send $TEE_PROVER_REGISTRY \ - * "addDevSigner(address,bytes32)" $SIGNER_ADDRESS $TEE_IMAGE_HASH \ + * "addDevSigner(address,bytes32)" $NITRO_SIGNER_ADDRESS $TEE_IMAGE_HASH \ + * --private-key $OWNER_KEY --rpc-url $RPC_URL + * + * cast send $TEE_PROVER_REGISTRY \ + * "addDevTDXSigner(address,bytes32)" $TDX_SIGNER_ADDRESS $TEE_IMAGE_HASH \ * --private-key $OWNER_KEY --rpc-url $RPC_URL * * No attestation, PCR0 registration, or certificate validation required. @@ -72,6 +76,7 @@ import { IVerifier } from "interfaces/multiproof/IVerifier.sol"; import { MockVerifier } from "src/multiproof/mocks/MockVerifier.sol"; import { DevTEEProverRegistry } from "src/multiproof/mocks/MockDevTEEProverRegistry.sol"; import { TEEProverRegistry } from "src/multiproof/tee/TEEProverRegistry.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { TEEVerifier } from "src/multiproof/tee/TEEVerifier.sol"; import { MinimalProxyAdmin } from "./mocks/MinimalProxyAdmin.sol"; @@ -89,6 +94,7 @@ contract DeployDevNoNitro is Script { DeployConfig public constant cfg = DeployConfig(address(uint160(uint256(keccak256(abi.encode("optimism.deployconfig")))))); + address public tdxVerifierAddr; address public teeProverRegistryProxy; address public teeVerifier; address public disputeGameFactory; @@ -99,6 +105,7 @@ contract DeployDevNoNitro is Script { function setUp() public { DeployUtils.etchLabelAndAllowCheatcodes({ _etchTo: address(cfg), _cname: "DeployConfig" }); cfg.read(Config.deployConfigPath()); + tdxVerifierAddr = cfg.tdxVerifier(); } function run() public { @@ -110,8 +117,10 @@ contract DeployDevNoNitro is Script { console.log("TEE Proposer:", cfg.teeProposer()); console.log("TEE Challenger:", cfg.teeChallenger()); console.log("Game Type:", cfg.multiproofGameType()); + console.log("TDXVerifier:", tdxVerifierAddr); console.log(""); console.log("NOTE: Using DevTEEProverRegistry - NO attestation required."); + require(tdxVerifierAddr != address(0), "tdxVerifier must be set in config"); vm.startBroadcast(); @@ -128,7 +137,11 @@ contract DeployDevNoNitro is Script { function _deployTEEContracts(GameType gameType) internal { address owner = cfg.finalSystemOwner(); address teeRegistryImpl = address( - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(disputeGameFactory)) + new DevTEEProverRegistry( + INitroEnclaveVerifier(address(0)), + ITDXVerifier(tdxVerifierAddr), + IDisputeGameFactory(disputeGameFactory) + ) ); address[] memory initialProposers = new address[](2); initialProposers[0] = cfg.teeProposer(); @@ -197,6 +210,7 @@ contract DeployDevNoNitro is Script { console.log("========================================"); console.log("\nTEE Contracts:"); console.log(" DevTEEProverRegistry:", teeProverRegistryProxy); + console.log(" TDXVerifier:", tdxVerifierAddr); console.log(" TEEVerifier:", teeVerifier); console.log("\nInfrastructure:"); console.log(" DisputeGameFactory:", disputeGameFactory); @@ -208,9 +222,13 @@ contract DeployDevNoNitro is Script { console.log(" TEE Image Hash:", vm.toString(cfg.teeImageHash())); console.log(" Config Hash:", vm.toString(cfg.multiproofConfigHash())); console.log("========================================"); - console.log("\n>>> NEXT STEP - Register dev signer (NO ATTESTATION NEEDED) <<<"); + console.log("\n>>> NEXT STEP - Register dev Nitro and TDX signers (NO ATTESTATION NEEDED) <<<"); + console.log("\ncast send", teeProverRegistryProxy); + console.log(' "addDevSigner(address,bytes32)" '); + console.log(" ", vm.toString(cfg.teeImageHash())); + console.log(" --private-key --rpc-url "); console.log("\ncast send", teeProverRegistryProxy); - console.log(' "addDevSigner(address,bytes32)" '); + console.log(' "addDevTDXSigner(address,bytes32)" '); console.log(" ", vm.toString(cfg.teeImageHash())); console.log(" --private-key --rpc-url "); console.log("\n========================================\n"); @@ -219,6 +237,7 @@ contract DeployDevNoNitro is Script { function _writeOutput() internal { string memory key = "deployment"; vm.serializeAddress(key, "TEEProverRegistry", teeProverRegistryProxy); + vm.serializeAddress(key, "TDXVerifier", tdxVerifierAddr); vm.serializeAddress(key, "TEEVerifier", teeVerifier); vm.serializeAddress(key, "DisputeGameFactory", disputeGameFactory); vm.serializeAddress(key, "AnchorStateRegistry", address(mockAnchorRegistry)); diff --git a/scripts/multiproof/DeployDevWithNitro.s.sol b/scripts/multiproof/DeployDevWithNitro.s.sol index ed34a3f5e..5a3cae27f 100644 --- a/scripts/multiproof/DeployDevWithNitro.s.sol +++ b/scripts/multiproof/DeployDevWithNitro.s.sol @@ -37,6 +37,7 @@ pragma solidity 0.8.15; */ import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { Proxy } from "src/universal/Proxy.sol"; import { Script } from "forge-std/Script.sol"; import { console2 as console } from "forge-std/console2.sol"; @@ -74,6 +75,7 @@ contract DeployDevWithNitro is Script { DeployConfig(address(uint160(uint256(keccak256(abi.encode("optimism.deployconfig")))))); address public nitroEnclaveVerifierAddr; + address public tdxVerifierAddr; address public teeProverRegistryProxy; address public teeVerifier; address public disputeGameFactory; @@ -91,10 +93,12 @@ contract DeployDevWithNitro is Script { // NitroEnclaveVerifier must be pre-deployed (via DeployRiscZeroStack.s.sol) nitroEnclaveVerifierAddr = cfg.nitroEnclaveVerifier(); + tdxVerifierAddr = cfg.tdxVerifier(); require( nitroEnclaveVerifierAddr != address(0), "nitroEnclaveVerifier must be set in config (deploy via DeployRiscZeroStack.s.sol first)" ); + require(tdxVerifierAddr != address(0), "tdxVerifier must be set in config"); console.log("=== Deploying Dev Infrastructure (WITH NITRO) ==="); console.log("Chain ID:", block.chainid); @@ -103,6 +107,7 @@ contract DeployDevWithNitro is Script { console.log("TEE Challenger:", cfg.teeChallenger()); console.log("Game Type:", cfg.multiproofGameType()); console.log("NitroEnclaveVerifier:", nitroEnclaveVerifierAddr); + console.log("TDXVerifier:", tdxVerifierAddr); console.log(""); console.log("NOTE: Using REAL TEEProverRegistry - ZK attestation proof REQUIRED."); @@ -122,7 +127,9 @@ contract DeployDevWithNitro is Script { address owner = cfg.finalSystemOwner(); address teeRegistryImpl = address( new TEEProverRegistry( - INitroEnclaveVerifier(nitroEnclaveVerifierAddr), IDisputeGameFactory(disputeGameFactory) + INitroEnclaveVerifier(nitroEnclaveVerifierAddr), + ITDXVerifier(tdxVerifierAddr), + IDisputeGameFactory(disputeGameFactory) ) ); address[] memory initialProposers = new address[](2); @@ -192,6 +199,7 @@ contract DeployDevWithNitro is Script { console.log("========================================"); console.log("\nTEE Contracts:"); console.log(" NitroEnclaveVerifier:", nitroEnclaveVerifierAddr); + console.log(" TDXVerifier:", tdxVerifierAddr); console.log(" TEEProverRegistry:", teeProverRegistryProxy); console.log(" TEEVerifier:", teeVerifier); console.log("\nInfrastructure:"); @@ -204,9 +212,12 @@ contract DeployDevWithNitro is Script { console.log(" TEE Image Hash:", vm.toString(cfg.teeImageHash())); console.log(" Config Hash:", vm.toString(cfg.multiproofConfigHash())); console.log("========================================"); - console.log("\n>>> NEXT STEP: Register signer with ZK attestation proof <<<"); + console.log("\n>>> NEXT STEP: Register one Nitro signer and one TDX signer <<<"); console.log("\n cast send", teeProverRegistryProxy); - console.log(' "registerSigner(bytes,bytes)" '); + console.log(' "registerSigner(bytes,bytes)" '); + console.log(" --private-key --rpc-url "); + console.log("\n cast send", teeProverRegistryProxy); + console.log(' "registerTDXSigner(bytes,bytes)" '); console.log(" --private-key --rpc-url "); console.log("\n========================================\n"); } @@ -216,6 +227,7 @@ contract DeployDevWithNitro is Script { vm.serializeAddress(key, "TEEProverRegistry", teeProverRegistryProxy); vm.serializeAddress(key, "TEEVerifier", teeVerifier); vm.serializeAddress(key, "NitroEnclaveVerifier", nitroEnclaveVerifierAddr); + vm.serializeAddress(key, "TDXVerifier", tdxVerifierAddr); vm.serializeAddress(key, "DisputeGameFactory", disputeGameFactory); vm.serializeAddress(key, "AnchorStateRegistry", address(mockAnchorRegistry)); vm.serializeAddress(key, "DelayedWETH", mockDelayedWETH); diff --git a/scripts/multiproof/DeployDevWithTDX.s.sol b/scripts/multiproof/DeployDevWithTDX.s.sol new file mode 100644 index 000000000..bc9776b49 --- /dev/null +++ b/scripts/multiproof/DeployDevWithTDX.s.sol @@ -0,0 +1,222 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +/** + * @title DeployDevWithTDX + * @notice Development deployment using the TDX signer-registration path. + * + * This deploys the same local multiproof testing infrastructure as the existing + * dev scripts, but configures TEEProverRegistry for TDX signer registration. Deploy + * TDXVerifier first with DeployTDXVerifier.s.sol and pass its address to run(). + * The default run(address) entrypoint configures DEFAULT_TDX_REGISTRATION_MANAGER + * as the registry manager so it can submit TDX signer registrations. + */ + +import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; +import { Proxy } from "src/universal/Proxy.sol"; +import { Script } from "forge-std/Script.sol"; +import { console2 as console } from "forge-std/console2.sol"; +import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; +import { IDelayedWETH } from "interfaces/dispute/IDelayedWETH.sol"; +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { DisputeGameFactory } from "src/dispute/DisputeGameFactory.sol"; +import { GameType, Hash } from "src/dispute/lib/Types.sol"; + +import { DeployConfig } from "scripts/deploy/DeployConfig.s.sol"; +import { Config } from "scripts/libraries/Config.sol"; +import { DeployUtils } from "scripts/libraries/DeployUtils.sol"; + +import { AggregateVerifier } from "src/multiproof/AggregateVerifier.sol"; +import { IVerifier } from "interfaces/multiproof/IVerifier.sol"; +import { MockVerifier } from "src/multiproof/mocks/MockVerifier.sol"; +import { TEEProverRegistry } from "src/multiproof/tee/TEEProverRegistry.sol"; +import { TEEVerifier } from "src/multiproof/tee/TEEVerifier.sol"; + +import { MinimalProxyAdmin } from "./mocks/MinimalProxyAdmin.sol"; +import { MockAnchorStateRegistry } from "./mocks/MockAnchorStateRegistry.sol"; +import { MockDelayedWETH } from "./mocks/MockDelayedWETH.sol"; + +contract DeployDevWithTDX is Script { + uint256 public constant INIT_BOND = 0.00001 ether; + address public constant DEFAULT_TDX_REGISTRATION_MANAGER = 0x93900CB7eCdB5994352b19DfD8a900Cd4fa437B7; + + DeployConfig public constant cfg = + DeployConfig(address(uint160(uint256(keccak256(abi.encode("optimism.deployconfig")))))); + + address public nitroEnclaveVerifierAddr; + address public tdxVerifierAddr; + address public tdxRegistrationManager; + address public teeProverRegistryProxy; + address public teeVerifier; + address public disputeGameFactory; + IAnchorStateRegistry public mockAnchorRegistry; + address public mockDelayedWETH; + address public aggregateVerifier; + + function setUp() public { + DeployUtils.etchLabelAndAllowCheatcodes({ _etchTo: address(cfg), _cname: "DeployConfig" }); + cfg.read(Config.deployConfigPath()); + } + + function run(address tdxVerifier) public { + run(tdxVerifier, DEFAULT_TDX_REGISTRATION_MANAGER); + } + + function run(address tdxVerifier, address registrationManager) public { + nitroEnclaveVerifierAddr = cfg.nitroEnclaveVerifier(); + require(tdxVerifier != address(0), "tdxVerifier must be non-zero"); + require(nitroEnclaveVerifierAddr != address(0), "nitroEnclaveVerifier must be set in config"); + require(registrationManager != address(0), "registrationManager must be non-zero"); + tdxVerifierAddr = tdxVerifier; + tdxRegistrationManager = registrationManager; + + GameType gameType = GameType.wrap(uint32(cfg.multiproofGameType())); + + console.log("=== Deploying Dev Infrastructure (WITH TDX) ==="); + console.log("Chain ID:", block.chainid); + console.log("Owner:", cfg.finalSystemOwner()); + console.log("TEE Proposer:", cfg.teeProposer()); + console.log("TEE Challenger:", cfg.teeChallenger()); + console.log("Game Type:", cfg.multiproofGameType()); + console.log("NitroEnclaveVerifier:", nitroEnclaveVerifierAddr); + console.log("TDXVerifier:", tdxVerifierAddr); + console.log("TDX Registration Manager:", tdxRegistrationManager); + console.log(""); + console.log("NOTE: TDXVerifier owner must be the broadcaster/finalSystemOwner."); + + vm.startBroadcast(); + + _deployInfrastructure(gameType); + _deployTDXContracts(gameType); + _deployAggregateVerifier(gameType); + + vm.stopBroadcast(); + + _printSummary(); + _writeOutput(); + } + + function _deployTDXContracts(GameType gameType) internal { + address owner = cfg.finalSystemOwner(); + address registryImpl = address( + new TEEProverRegistry( + INitroEnclaveVerifier(nitroEnclaveVerifierAddr), + ITDXVerifier(tdxVerifierAddr), + IDisputeGameFactory(disputeGameFactory) + ) + ); + + address[] memory initialProposers = new address[](2); + initialProposers[0] = cfg.teeProposer(); + initialProposers[1] = cfg.teeChallenger(); + + Proxy registryProxy = new Proxy(msg.sender); + registryProxy.upgradeToAndCall( + registryImpl, + abi.encodeCall(TEEProverRegistry.initialize, (owner, tdxRegistrationManager, initialProposers, gameType)) + ); + registryProxy.changeAdmin(address(0xdead)); + teeProverRegistryProxy = address(registryProxy); + + ITDXVerifier(tdxVerifierAddr).setProofSubmitter(teeProverRegistryProxy); + + teeVerifier = address(new TEEVerifier(TEEProverRegistry(teeProverRegistryProxy), mockAnchorRegistry)); + } + + function _deployInfrastructure(GameType gameType) internal { + address factoryImpl = address(new DisputeGameFactory()); + MinimalProxyAdmin proxyAdmin = new MinimalProxyAdmin(cfg.finalSystemOwner()); + + Proxy proxy = new Proxy(msg.sender); + proxy.upgradeTo(factoryImpl); + proxy.changeAdmin(address(proxyAdmin)); + DisputeGameFactory(address(proxy)).initialize(cfg.finalSystemOwner()); + disputeGameFactory = address(proxy); + + MockAnchorStateRegistry asr = new MockAnchorStateRegistry(); + mockAnchorRegistry = IAnchorStateRegistry(address(asr)); + asr.initialize( + disputeGameFactory, + Hash.wrap(cfg.multiproofGenesisOutputRoot()), + cfg.multiproofGenesisBlockNumber(), + gameType + ); + } + + function _deployAggregateVerifier(GameType gameType) internal { + address zkVerifier = address(new MockVerifier(mockAnchorRegistry)); + mockDelayedWETH = address(new MockDelayedWETH()); + + AggregateVerifier.ZkHashes memory zkHashes = + AggregateVerifier.ZkHashes({ rangeHash: cfg.zkRangeHash(), aggregateHash: cfg.zkAggregationHash() }); + + aggregateVerifier = address( + new AggregateVerifier( + gameType, + mockAnchorRegistry, + IDelayedWETH(payable(mockDelayedWETH)), + IVerifier(teeVerifier), + IVerifier(zkVerifier), + cfg.teeImageHash(), + zkHashes, + cfg.multiproofConfigHash(), + cfg.l2ChainID(), + cfg.multiproofBlockInterval(), + cfg.multiproofIntermediateBlockInterval() + ) + ); + + DisputeGameFactory factory = DisputeGameFactory(disputeGameFactory); + factory.setImplementation(gameType, IDisputeGame(aggregateVerifier), ""); + factory.setInitBond(gameType, INIT_BOND); + } + + function _printSummary() internal view { + console.log("\n========================================"); + console.log(" DEV DEPLOYMENT COMPLETE (TDX)"); + console.log("========================================"); + console.log("\nTDX Contracts:"); + console.log(" NitroEnclaveVerifier:", nitroEnclaveVerifierAddr); + console.log(" TDXVerifier:", tdxVerifierAddr); + console.log(" TEEProverRegistry:", teeProverRegistryProxy); + console.log(" TDX Registration Manager:", tdxRegistrationManager); + console.log(" TEEVerifier:", teeVerifier); + console.log("\nInfrastructure:"); + console.log(" DisputeGameFactory:", disputeGameFactory); + console.log(" AnchorStateRegistry (mock):", address(mockAnchorRegistry)); + console.log(" DelayedWETH (mock):", mockDelayedWETH); + console.log("\nGame:"); + console.log(" AggregateVerifier:", aggregateVerifier); + console.log(" Game Type:", cfg.multiproofGameType()); + console.log(" TEE Image Hash:", vm.toString(cfg.teeImageHash())); + console.log(" Config Hash:", vm.toString(cfg.multiproofConfigHash())); + console.log("========================================"); + console.log("\n>>> NEXT STEP: Register one Nitro signer and one TDX signer <<<"); + console.log("\n cast send", teeProverRegistryProxy); + console.log(' "registerSigner(bytes,bytes)" '); + console.log(" --private-key --rpc-url "); + console.log("\n cast send", teeProverRegistryProxy); + console.log(' "registerTDXSigner(bytes,bytes)" '); + console.log(" --private-key --rpc-url "); + console.log("\n========================================\n"); + } + + function _writeOutput() internal { + string memory key = "deployment"; + vm.serializeAddress(key, "NitroEnclaveVerifier", nitroEnclaveVerifierAddr); + vm.serializeAddress(key, "TDXVerifier", tdxVerifierAddr); + vm.serializeAddress(key, "TDXRegistrationManager", tdxRegistrationManager); + vm.serializeAddress(key, "TEEProverRegistry", teeProverRegistryProxy); + vm.serializeAddress(key, "TEEVerifier", teeVerifier); + vm.serializeAddress(key, "DisputeGameFactory", disputeGameFactory); + vm.serializeAddress(key, "AnchorStateRegistry", address(mockAnchorRegistry)); + vm.serializeAddress(key, "DelayedWETH", mockDelayedWETH); + string memory json = vm.serializeAddress(key, "AggregateVerifier", aggregateVerifier); + + string memory outPath = string.concat("deployments/", vm.toString(block.chainid), "-dev-with-tdx.json"); + vm.writeJson(json, outPath); + console.log("Deployment saved to:", outPath); + } +} diff --git a/scripts/multiproof/DeployTDXVerifier.s.sol b/scripts/multiproof/DeployTDXVerifier.s.sol new file mode 100644 index 000000000..e5f955771 --- /dev/null +++ b/scripts/multiproof/DeployTDXVerifier.s.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title DeployTDXVerifier + * @notice Deploys the Solidity TDX policy verifier used by TEEProverRegistry. + * + * This script is separated from DeployDevWithTDX.s.sol because TDXVerifier imports + * verifier interfaces that require Solidity ^0.8.20, while the multiproof stack is + * pinned to Solidity 0.8.15. + * + * Usage: + * + * forge script scripts/multiproof/DeployTDXVerifier.s.sol:DeployTDXVerifier \ + * --sig "run(address,address,bytes32,bytes32)" \ + * \ + * --rpc-url --broadcast --private-key + * + * After running DeployDevWithTDX.s.sol, the TDX verifier's proofSubmitter is + * updated to the deployed TEEProverRegistry. + */ + +import { Script } from "forge-std/Script.sol"; +import { console2 as console } from "forge-std/console2.sol"; + +import { ZkCoProcessorConfig, ZkCoProcessorType } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { TDXTcbStatus } from "interfaces/multiproof/tee/ITDXVerifier.sol"; +import { TDXVerifier } from "src/multiproof/tee/TDXVerifier.sol"; + +contract DeployTDXVerifier is Script { + /// @notice Maximum TDX quote age accepted by TDXVerifier. + uint64 internal constant TDX_MAX_TIME_DIFF = 3600; + + /// @param owner Owner for TDXVerifier. + /// @param risc0VerifierRouter Existing RISC Zero verifier router. + /// @param tdxVerifierId RISC Zero image ID for the TDX DCAP verifier guest. + /// @param intelRootCaHash Hash of the trusted Intel root CA consumed by the guest. + function run(address owner, address risc0VerifierRouter, bytes32 tdxVerifierId, bytes32 intelRootCaHash) public { + require(owner != address(0), "owner must be non-zero"); + require(risc0VerifierRouter != address(0), "risc0VerifierRouter must be non-zero"); + require(tdxVerifierId != bytes32(0), "tdxVerifierId must be non-zero"); + require(intelRootCaHash != bytes32(0), "intelRootCaHash must be non-zero"); + + TDXTcbStatus[] memory allowedStatuses = new TDXTcbStatus[](2); + allowedStatuses[0] = TDXTcbStatus.UpToDate; + allowedStatuses[1] = TDXTcbStatus.SwHardeningNeeded; + + ZkCoProcessorConfig memory zkConfig = ZkCoProcessorConfig({ + verifierId: tdxVerifierId, aggregatorId: bytes32(0), zkVerifier: risc0VerifierRouter + }); + + console.log("=== Deploying TDXVerifier ==="); + console.log("Owner:", owner); + console.log("RISC Zero Verifier Router:", risc0VerifierRouter); + console.log("TDX Verifier ID:", vm.toString(tdxVerifierId)); + console.log("Intel Root CA Hash:", vm.toString(intelRootCaHash)); + console.log("Max Time Diff:", TDX_MAX_TIME_DIFF); + console.log(""); + + vm.startBroadcast(); + + address tdxVerifier = address( + new TDXVerifier( + owner, TDX_MAX_TIME_DIFF, intelRootCaHash, owner, ZkCoProcessorType.RiscZero, zkConfig, allowedStatuses + ) + ); + + vm.stopBroadcast(); + + console.log("TDXVerifier:", tdxVerifier); + console.log(""); + console.log(">>> Use this address as the DeployDevWithTDX.s.sol argument <<<"); + + _writeOutput(tdxVerifier, risc0VerifierRouter, tdxVerifierId, intelRootCaHash); + } + + function _writeOutput( + address tdxVerifier, + address risc0VerifierRouter, + bytes32 tdxVerifierId, + bytes32 intelRootCaHash + ) + internal + { + string memory key = "deployment"; + vm.serializeAddress(key, "TDXVerifier", tdxVerifier); + vm.serializeAddress(key, "RiscZeroVerifierRouter", risc0VerifierRouter); + vm.serializeBytes32(key, "TDXVerifierId", tdxVerifierId); + vm.serializeBytes32(key, "IntelRootCaHash", intelRootCaHash); + string memory json = vm.serializeUint(key, "MaxTimeDiff", TDX_MAX_TIME_DIFF); + + string memory outPath = string.concat("deployments/", vm.toString(block.chainid), "-tdx-verifier.json"); + vm.writeJson(json, outPath); + console.log("Deployment saved to:", outPath); + } +} diff --git a/scripts/multiproof/README.md b/scripts/multiproof/README.md index 01155f444..826664341 100644 --- a/scripts/multiproof/README.md +++ b/scripts/multiproof/README.md @@ -40,13 +40,13 @@ Ensure `finalSystemOwner` is set to the address you will deploy from (i.e. the a Other relevant fields: -| Field | Description | -|---|---| -| `teeProposer` | Address to be registered as the TEE proposer | -| `teeImageHash` | PCR0 hash used when registering the dev signer (use `bytes32(0x01...01)` for dev) | -| `multiproofGameType` | Game type ID for the dispute game | -| `multiproofGenesisOutputRoot` | Initial anchor output root | -| `multiproofGenesisBlockNumber` | Initial anchor L2 block number | +| Field | Description | +| ------------------------------ | --------------------------------------------------------------------------------- | +| `teeProposer` | Address to be registered as the TEE proposer | +| `teeImageHash` | PCR0 hash used when registering the dev signer (use `bytes32(0x01...01)` for dev) | +| `multiproofGameType` | Game type ID for the dispute game | +| `multiproofGenesisOutputRoot` | Initial anchor output root | +| `multiproofGenesisBlockNumber` | Initial anchor L2 block number | ### Step 2: Deploy contracts @@ -96,9 +96,9 @@ This returns a raw byte array representing an uncompressed secp256k1 public key 0x0cbe4A965B41DA6B2D5AF4d53c0C16a37d6f9F7D ``` -### Step 5: Register the dev signer +### Step 5: Register the dev signers -Call `addDevSigner` on the deployed `DevTEEProverRegistry` with the **signer address** derived in Step 4. +Call `addDevSigner` for the Nitro signer and `addDevTDXSigner` for the TDX signer on the deployed `DevTEEProverRegistry`. > **Note:** PCR0 enforcement is handled by `AggregateVerifier` (which bakes `teeImageHash` into the > journal the enclave signs). The registry only tracks which signer addresses are valid. @@ -106,10 +106,19 @@ Call `addDevSigner` on the deployed `DevTEEProverRegistry` with the **signer add ```bash # Replace: # 0x587d... with the TEEProverRegistry address from your deployment output -# 0x080f... with the signer address derived in Step 4 +# 0x080f... with the Nitro signer address derived in Step 4 cast send 0x587d410B205449fB889EC4a5b351D375C656d084 \ - "addDevSigner(address)" \ + "addDevSigner(address,bytes32)" \ 0x080f42420846c613158D7b4334257C78bE5A9B90 \ + $TEE_IMAGE_HASH \ + --rpc-url https://c3-chainproxy-eth-sepolia-full-dev.cbhq.net \ + --ledger --mnemonic-derivation-path "m/44'/60'/1'/0/0" + +# Register a TDX dev signer for the same image hash. +cast send 0x587d410B205449fB889EC4a5b351D375C656d084 \ + "addDevTDXSigner(address,bytes32)" \ + $TDX_SIGNER_ADDRESS \ + $TEE_IMAGE_HASH \ --rpc-url https://c3-chainproxy-eth-sepolia-full-dev.cbhq.net \ --ledger --mnemonic-derivation-path "m/44'/60'/1'/0/0" ``` @@ -124,6 +133,134 @@ The deployer address (`finalSystemOwner`) is the owner of `DevTEEProverRegistry` --- +## Path 3: TDX (Production-Path PoC) + +The TDX path follows the same split as Nitro: expensive attestation verification happens off-chain in a ZK guest, +and Solidity verifies the proof plus the on-chain acceptance policy before registering the signer. + +| Contract | Purpose | +| ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `TDXVerifier` | Verifies a RISC Zero or SP1 proof whose public values are an ABI-encoded `TDXVerifierJournal`, then checks trusted Intel root, TCB status policy, collateral expiry, quote freshness, signer derivation, and `REPORTDATA` public-key binding. | +| `TEEProverRegistry` | Registers Nitro signers through `registerSigner(bytes,bytes)` and TDX signers through `registerTDXSigner(bytes,bytes)`, tracking which TEE type each signer came from for `TEEVerifier`. | + +The ZK verifier guest is expected to perform the full Intel DCAP verification path: + +```text +TD Quote signature +PCK certificate chain +TCB info signing chain and TCB status +QE identity signing chain +CRLs/revocation state +TDREPORT field extraction +``` + +The Solidity verifier then enforces local policy over the proven journal. The PoC maps TDX measurements into the existing multiproof `TEE_IMAGE_HASH` field as: + +```text +keccak256(MRTD || RTMR0 || RTMR1 || RTMR2 || RTMR3) +``` + +The attested public key must be supplied as an uncompressed 65-byte secp256k1 public key: + +```text +0x04 || x || y +``` + +The quote's TDREPORT `REPORTDATA` must put `keccak256(x || y)` in the first 32 bytes. The last 32 bytes are returned by the verifier as app-specific binding data and emitted by the registry. + +`TEEVerifier` is still the proposal-proof verifier, but a TEE proposal proof now requires two signatures over the same journal: one from a Nitro-registered signer and one from a TDX-registered signer. The proof bytes are `proposer || signatureA || signatureB`; either signature order is accepted as long as both registered TEE types are present and both signers match the expected `TEE_IMAGE_HASH`. + +> **PoC boundary:** this repo now contains the production-shaped Solidity path and policy checks. The remaining off-chain piece is the actual RISC Zero/SP1 TDX DCAP guest that emits `TDXVerifierJournal` after verifying Intel collateral. + +### Step 1: Deploy `TDXVerifier` + +`TDXVerifier` is deployed separately because it depends on verifier interfaces that require Solidity `^0.8.20`, while the rest of the multiproof deployment stack is pinned to Solidity `0.8.15`. + +For Sepolia TDX testing, `scripts/multiproof/justfile` defaults to: + +```bash +RISC0_VERIFIER_ROUTER=0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187 +TDX_VERIFIER_ID=0x1f1bc81fae82605af46a4c9a20f641922b7542befd9219644c7e6c59ccdeccab +INTEL_ROOT_CA_HASH=0xa1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d7009 +TDX_IMAGE_HASH=0xa227306080459e4bcf1324b229b344af4469846f9861aa8aff450f35046df9d6 +TDX_SIGNER=0x6A1f38f20044e8e69EAC755144F14f973e7b8d6E +TDX_REGISTRATION_MANAGER=0x93900CB7eCdB5994352b19DfD8a900Cd4fa437B7 +``` + +`deploy-config/sepolia.json` uses the same `RISC0_VERIFIER_ROUTER` and `TDX_IMAGE_HASH`. The `TEEProverRegistry` +constructor also requires a non-zero `tdxVerifier`; for config-driven deployments, set `tdxVerifier` in the deploy +config to the deployed `TDXVerifier` address. + +```bash +just --justfile scripts/multiproof/justfile deploy-tdx-verifier +``` + +To override any verifier input manually, pass all three verifier args: + +```bash +forge script scripts/multiproof/DeployTDXVerifier.s.sol:DeployTDXVerifier \ + --sig "run(address,address,bytes32,bytes32)" \ + $OWNER \ + $RISC0_VERIFIER_ROUTER \ + $TDX_VERIFIER_ID \ + $INTEL_ROOT_CA_HASH \ + --rpc-url $L1_RPC_URL \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +The script saves output to `deployments/-tdx-verifier.json`. + +### Step 2: Deploy the TDX multiproof test stack + +Set `DEPLOY_CONFIG_PATH` to the Sepolia deploy config and pass the `TDXVerifier` address from Step 1. The deploy config must also contain the `NitroEnclaveVerifier` address, because TEE proposal proofs now require both Nitro and TDX signatures. `finalSystemOwner` in the deploy config must be the account broadcasting this transaction because the script updates `TDXVerifier.proofSubmitter` to the deployed `TEEProverRegistry`. + +The TDX registry manager is set to `TDX_REGISTRATION_MANAGER`, allowing that address to call `registerTDXSigner(bytes,bytes)`. Register a Nitro signer through `registerSigner(bytes,bytes)` as well before submitting TEE proposal proofs. + +```bash +just --justfile scripts/multiproof/justfile deploy-tdx-stack $TDX_VERIFIER +``` + +To override the manager manually, use: + +```bash +forge script scripts/multiproof/DeployDevWithTDX.s.sol:DeployDevWithTDX \ + --sig "run(address,address)" \ + $TDX_VERIFIER \ + $TDX_REGISTRATION_MANAGER \ + --rpc-url $L1_RPC_URL \ + --broadcast \ + --private-key $PRIVATE_KEY +``` + +The script saves output to `deployments/-dev-with-tdx.json`. + +### Step 3: Register Nitro and TDX signers + +Register a Nitro signer with a ZK-proven Nitro attestation: + +```bash +cast send $TEE_PROVER_REGISTRY \ + "registerSigner(bytes,bytes)" \ + $NITRO_OUTPUT \ + $NITRO_PROOF_BYTES \ + --rpc-url $L1_RPC_URL \ + --private-key $PRIVATE_KEY +``` + +Once you have the ABI-encoded `TDXVerifierJournal` output and matching RISC Zero proof bytes from the TDX DCAP guest, register the signer through the TDX-aware registry: + +```bash +cast send $TEE_PROVER_REGISTRY \ + "registerTDXSigner(bytes,bytes)" \ + $TDX_OUTPUT \ + $PROOF_BYTES \ + --rpc-url $L1_RPC_URL \ + --private-key $PRIVATE_KEY +``` + +--- + ## Pre-Seeding Games (Post-Deployment) After deploying via either path, you can pre-seed the `DisputeGameFactory` with a chain of `AggregateVerifier` games. This is useful for testing forward traversal at proposer restart — the proposer can walk the linked list of games to find where to resume. @@ -184,9 +321,9 @@ forge script scripts/multiproof/SeedGames.s.sol \ Optional env vars: -| Variable | Default | Description | -|---|---|---| -| `GAME_COUNT` | 500 | Number of games to create | +| Variable | Default | Description | +| ------------ | ------------ | ----------------------------- | +| `GAME_COUNT` | 500 | Number of games to create | | `ROOTS_FILE` | `roots.json` | Path to the output roots JSON | ### Step 5: Verify on-chain diff --git a/scripts/multiproof/justfile b/scripts/multiproof/justfile new file mode 100644 index 000000000..86559bb92 --- /dev/null +++ b/scripts/multiproof/justfile @@ -0,0 +1,84 @@ +set dotenv-load + +repo_root := justfile_directory() + "/../.." +justfile_path := justfile_directory() + "/justfile" +default_deploy_config := "deploy-config/sepolia.json" +forge_account := "testnet-admin" +l1_rpc_url := "https://ethereum-full-sepolia-k8s-dev.cbhq.net" + +sepolia_risc0_verifier_router := "0x925d8331ddc0a1F0d96E68CF073DFE1d92b69187" +sepolia_tdx_verifier_id := "0x1f1bc81fae82605af46a4c9a20f641922b7542befd9219644c7e6c59ccdeccab" +sepolia_intel_root_ca_hash := "0xa1acc73eb45794fa1734f14d882e91925b6006f79d3bb2460df9d01b333d7009" +sepolia_tdx_image_hash := "0xa227306080459e4bcf1324b229b344af4469846f9861aa8aff450f35046df9d6" +sepolia_tdx_signer := "0x6A1f38f20044e8e69EAC755144F14f973e7b8d6E" +sepolia_tdx_registration_manager := "0x93900CB7eCdB5994352b19DfD8a900Cd4fa437B7" + +# List available multiproof deployment recipes. +default: + @just --justfile "{{justfile_path}}" --list + +# Deploy the TDXVerifier policy contract. +# +# Required env: +# L1_RPC_URL +# +# Args: +# risc0_router RISC Zero verifier router +# tdx_verifier_id RISC Zero image ID for the TDX DCAP verifier guest +# intel_root_ca_hash Trusted Intel root CA hash consumed by the guest +deploy-tdx-verifier risc0_router=sepolia_risc0_verifier_router tdx_verifier_id=sepolia_tdx_verifier_id intel_root_ca_hash=sepolia_intel_root_ca_hash: + #!/usr/bin/env bash + set -euo pipefail + cd "{{repo_root}}" + owner="$(cast wallet address --account "{{forge_account}}")" + echo "Using {{forge_account}} as owner/deployer: $owner" + forge script scripts/multiproof/DeployTDXVerifier.s.sol:DeployTDXVerifier \ + --sig "run(address,address,bytes32,bytes32)" \ + "$owner" \ + "{{risc0_router}}" \ + "{{tdx_verifier_id}}" \ + "{{intel_root_ca_hash}}" \ + --rpc-url "{{l1_rpc_url}}" \ + --broadcast \ + --account "{{forge_account}}" \ + --sender "$owner" + +# Print the Sepolia TDX constants used by these recipes. +tdx-sepolia-config: + @echo "RISC0_VERIFIER_ROUTER={{sepolia_risc0_verifier_router}}" + @echo "TDX_VERIFIER_ID={{sepolia_tdx_verifier_id}}" + @echo "INTEL_ROOT_CA_HASH={{sepolia_intel_root_ca_hash}}" + @echo "TDX_IMAGE_HASH={{sepolia_tdx_image_hash}}" + @echo "TDX_SIGNER={{sepolia_tdx_signer}}" + @echo "TDX_REGISTRATION_MANAGER={{sepolia_tdx_registration_manager}}" + +# Deploy the TDX multiproof dev/test stack using deploy-config/sepolia.json. +# +# Required env: +# L1_RPC_URL +# +# Args: +# tdx_verifier TDXVerifier address from deploy-tdx-verifier +# tdx_manager Manager that can call registerTDXSigner() +deploy-tdx-stack tdx_verifier tdx_manager=sepolia_tdx_registration_manager deploy_config=default_deploy_config: + #!/usr/bin/env bash + set -euo pipefail + cd "{{repo_root}}" + owner="$(cast wallet address --account "{{forge_account}}")" + echo "Using {{forge_account}} as deployer: $owner" + DEPLOY_CONFIG_PATH="{{deploy_config}}" \ + forge script scripts/multiproof/DeployDevWithTDX.s.sol:DeployDevWithTDX \ + --sig "run(address,address)" \ + "{{tdx_verifier}}" \ + "{{tdx_manager}}" \ + --rpc-url "{{l1_rpc_url}}" \ + --broadcast \ + --account "{{forge_account}}" \ + --sender "$owner" + +# Print the two-step Sepolia command template. +tdx-sepolia-help: + @echo 'export L1_RPC_URL=' + @echo 'just deploy-tdx-verifier' + @echo 'just deploy-tdx-stack [tdx-manager]' + @echo 'just tdx-sepolia-config' diff --git a/snapshots/abi/DevTEEProverRegistry.json b/snapshots/abi/DevTEEProverRegistry.json index 6d66717e0..0448f242a 100644 --- a/snapshots/abi/DevTEEProverRegistry.json +++ b/snapshots/abi/DevTEEProverRegistry.json @@ -6,6 +6,11 @@ "name": "nitroVerifier", "type": "address" }, + { + "internalType": "contract ITDXVerifier", + "name": "tdxVerifier", + "type": "address" + }, { "internalType": "contract IDisputeGameFactory", "name": "factory", @@ -54,6 +59,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TDX_VERIFIER", + "outputs": [ + { + "internalType": "contract ITDXVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -72,6 +90,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "imageHash", + "type": "bytes32" + } + ], + "name": "addDevTDXSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -253,6 +289,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "output", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "proofBytes", + "type": "bytes" + } + ], + "name": "registerTDXSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "renounceManagement", @@ -317,6 +371,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "signerTEEType", + "outputs": [ + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -465,6 +538,31 @@ "name": "SignerRegistered", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "imageHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reportDataSuffix", + "type": "bytes32" + } + ], + "name": "TDXSignerRegistered", + "type": "event" + }, { "inputs": [], "name": "AttestationTooOld", @@ -495,9 +593,40 @@ "name": "InvalidPublicKey", "type": "error" }, + { + "inputs": [], + "name": "InvalidTEEType", + "type": "error" + }, { "inputs": [], "name": "PCR0NotFound", "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "existingTEEType", + "type": "uint8" + }, + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "newTEEType", + "type": "uint8" + } + ], + "name": "SignerTEETypeMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "TDXVerifierNotSet", + "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/TDXVerifier.json b/snapshots/abi/TDXVerifier.json new file mode 100644 index 000000000..bda2cd07b --- /dev/null +++ b/snapshots/abi/TDXVerifier.json @@ -0,0 +1,808 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "uint64", + "name": "initialMaxTimeDiff", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "initialRootCaHash", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "initialProofSubmitter", + "type": "address" + }, + { + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "verifierId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "aggregatorId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "zkVerifier", + "type": "address" + } + ], + "internalType": "struct ZkCoProcessorConfig", + "name": "config", + "type": "tuple" + }, + { + "internalType": "enum TDXTcbStatus[]", + "name": "initialAllowedTcbStatuses", + "type": "uint8[]" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "enum TDXTcbStatus", + "name": "", + "type": "uint8" + } + ], + "name": "allowedTcbStatuses", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "cancelOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "completeOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + } + ], + "name": "getZkConfig", + "outputs": [ + { + "components": [ + { + "internalType": "bytes32", + "name": "verifierId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "aggregatorId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "zkVerifier", + "type": "address" + } + ], + "internalType": "struct ZkCoProcessorConfig", + "name": "", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "maxTimeDiff", + "outputs": [ + { + "internalType": "uint64", + "name": "", + "type": "uint64" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "result", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "ownershipHandoverExpiresAt", + "outputs": [ + { + "internalType": "uint256", + "name": "result", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "proofSubmitter", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "renounceOwnership", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "requestOwnershipHandover", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "rootCaHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "newMaxTimeDiff", + "type": "uint64" + } + ], + "name": "setMaxTimeDiff", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newProofSubmitter", + "type": "address" + } + ], + "name": "setProofSubmitter", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "newRootCaHash", + "type": "bytes32" + } + ], + "name": "setRootCaHash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum TDXTcbStatus", + "name": "status", + "type": "uint8" + }, + { + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "setTcbStatusAllowed", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "verifierId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "aggregatorId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "zkVerifier", + "type": "address" + } + ], + "internalType": "struct ZkCoProcessorConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "setZkConfiguration", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "output", + "type": "bytes" + }, + { + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + }, + { + "internalType": "bytes", + "name": "proofBytes", + "type": "bytes" + } + ], + "name": "verify", + "outputs": [ + { + "components": [ + { + "internalType": "enum TDXVerificationResult", + "name": "result", + "type": "uint8" + }, + { + "internalType": "enum TDXTcbStatus", + "name": "tcbStatus", + "type": "uint8" + }, + { + "internalType": "uint64", + "name": "timestamp", + "type": "uint64" + }, + { + "internalType": "uint64", + "name": "collateralExpiration", + "type": "uint64" + }, + { + "internalType": "bytes32", + "name": "rootCaHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "pckCertHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "tcbInfoHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "qeIdentityHash", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "publicKey", + "type": "bytes" + }, + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "imageHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "mrTdHash", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "reportDataPrefix", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "reportDataSuffix", + "type": "bytes32" + } + ], + "internalType": "struct TDXVerifierJournal", + "name": "journal", + "type": "tuple" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "version", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "enum ZkCoProcessorType", + "name": "", + "type": "uint8" + } + ], + "name": "zkConfig", + "outputs": [ + { + "internalType": "bytes32", + "name": "verifierId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "aggregatorId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "zkVerifier", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "imageHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "enum TDXTcbStatus", + "name": "tcbStatus", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "pckCertHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "tcbInfoHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "qeIdentityHash", + "type": "bytes32" + } + ], + "name": "AttestationSubmitted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint64", + "name": "maxTimeDiff", + "type": "uint64" + } + ], + "name": "MaxTimeDiffUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "OwnershipHandoverCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "pendingOwner", + "type": "address" + } + ], + "name": "OwnershipHandoverRequested", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "oldOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "proofSubmitter", + "type": "address" + } + ], + "name": "ProofSubmitterChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "rootCaHash", + "type": "bytes32" + } + ], + "name": "RootCaHashUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum TDXTcbStatus", + "name": "status", + "type": "uint8" + }, + { + "indexed": false, + "internalType": "bool", + "name": "allowed", + "type": "bool" + } + ], + "name": "TcbStatusPolicyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "verifierId", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "aggregatorId", + "type": "bytes32" + }, + { + "internalType": "address", + "name": "zkVerifier", + "type": "address" + } + ], + "indexed": false, + "internalType": "struct ZkCoProcessorConfig", + "name": "config", + "type": "tuple" + } + ], + "name": "ZKConfigurationUpdated", + "type": "event" + }, + { + "inputs": [], + "name": "AlreadyInitialized", + "type": "error" + }, + { + "inputs": [], + "name": "CallerNotProofSubmitter", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "collateralExpiration", + "type": "uint64" + } + ], + "name": "CollateralExpired", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPublicKey", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint64", + "name": "timestampSeconds", + "type": "uint64" + }, + { + "internalType": "uint256", + "name": "currentTimestamp", + "type": "uint256" + } + ], + "name": "InvalidTimestamp", + "type": "error" + }, + { + "inputs": [], + "name": "NewOwnerIsZeroAddress", + "type": "error" + }, + { + "inputs": [], + "name": "NoHandoverRequest", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "expected", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "actual", + "type": "bytes32" + } + ], + "name": "ReportDataMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "expected", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "actual", + "type": "bytes32" + } + ], + "name": "RootCaHashMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "expected", + "type": "address" + }, + { + "internalType": "address", + "name": "actual", + "type": "address" + } + ], + "name": "SignerMismatch", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum TDXVerificationResult", + "name": "result", + "type": "uint8" + } + ], + "name": "TDXVerificationFailed", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum TDXTcbStatus", + "name": "status", + "type": "uint8" + } + ], + "name": "TcbStatusNotAllowed", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "UnknownZkCoprocessor", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroMaxTimeDiff", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroProofSubmitter", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroRootCaHash", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "enum ZkCoProcessorType", + "name": "zkCoprocessor", + "type": "uint8" + } + ], + "name": "ZkVerifierNotConfigured", + "type": "error" + } +] \ No newline at end of file diff --git a/snapshots/abi/TEEProverRegistry.json b/snapshots/abi/TEEProverRegistry.json index 510962c5d..ceff44a08 100644 --- a/snapshots/abi/TEEProverRegistry.json +++ b/snapshots/abi/TEEProverRegistry.json @@ -6,6 +6,11 @@ "name": "nitroVerifier", "type": "address" }, + { + "internalType": "contract ITDXVerifier", + "name": "tdxVerifier", + "type": "address" + }, { "internalType": "contract IDisputeGameFactory", "name": "factory", @@ -54,6 +59,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "TDX_VERIFIER", + "outputs": [ + { + "internalType": "contract ITDXVerifier", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -235,6 +253,24 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "output", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "proofBytes", + "type": "bytes" + } + ], + "name": "registerTDXSigner", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [], "name": "renounceManagement", @@ -299,6 +335,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "signerTEEType", + "outputs": [ + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -447,6 +502,31 @@ "name": "SignerRegistered", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "imageHash", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reportDataSuffix", + "type": "bytes32" + } + ], + "name": "TDXSignerRegistered", + "type": "event" + }, { "inputs": [], "name": "AttestationTooOld", @@ -477,9 +557,40 @@ "name": "InvalidPublicKey", "type": "error" }, + { + "inputs": [], + "name": "InvalidTEEType", + "type": "error" + }, { "inputs": [], "name": "PCR0NotFound", "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "existingTEEType", + "type": "uint8" + }, + { + "internalType": "enum TEEProverRegistry.TEEType", + "name": "newTEEType", + "type": "uint8" + } + ], + "name": "SignerTEETypeMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "TDXVerifierNotSet", + "type": "error" } ] \ No newline at end of file diff --git a/snapshots/abi/TEEVerifier.json b/snapshots/abi/TEEVerifier.json index b491ef7a5..fff275ed7 100644 --- a/snapshots/abi/TEEVerifier.json +++ b/snapshots/abi/TEEVerifier.json @@ -116,6 +116,17 @@ "name": "VerifierNullified", "type": "event" }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + } + ], + "name": "DuplicateSigner", + "type": "error" + }, { "inputs": [ { @@ -164,6 +175,16 @@ "name": "InvalidSigner", "type": "error" }, + { + "inputs": [], + "name": "MissingNitroSignature", + "type": "error" + }, + { + "inputs": [], + "name": "MissingTDXSignature", + "type": "error" + }, { "inputs": [], "name": "NotProperGame", diff --git a/snapshots/semver-lock.json b/snapshots/semver-lock.json index 3bc67c56f..3887cc325 100644 --- a/snapshots/semver-lock.json +++ b/snapshots/semver-lock.json @@ -116,20 +116,24 @@ "sourceCodeHash": "0xc0ff6e93b6e2b9111c11e81b5df8948ab71d02b9d2c4dfda982fcb615519f1f7" }, "src/multiproof/AggregateVerifier.sol:AggregateVerifier": { - "initCodeHash": "0x10bb6a60f21de103d1da9ff310a38f571c53a8113d04a029087648debf3f0341", - "sourceCodeHash": "0xcd4c508223b9b461ec230bde03c1630fe68cd00b82ee84c39b93d9f3c0d71d37" + "initCodeHash": "0x9a77922f9f05c738e1a0884a1803a19302cd046e122af9ff38b8c3dc43e89409", + "sourceCodeHash": "0x9a48d272dd19ccd6df51999dcb6117308aea712f6d3731bdcc8b8d02b867c348" }, "src/multiproof/tee/NitroEnclaveVerifier.sol:NitroEnclaveVerifier": { "initCodeHash": "0x100364f9b0c63a61538386ba91e73dcffba22d3f6dfe8efdbbf5ff347b6fce47", "sourceCodeHash": "0xbedb66738a0ab18600b2ed21de51d552a210c4cc71b2c971440e325791dc3ce7" }, + "src/multiproof/tee/TDXVerifier.sol:TDXVerifier": { + "initCodeHash": "0xdfd5a00dc139565ec7102fd5ac05d91f923bfe1ef724e16a4cdfbcb60cd19c3c", + "sourceCodeHash": "0xb28e2632fc3591ecdb2d96cbc6b99a08854048a513f444d8282511293a0739e2" + }, "src/multiproof/tee/TEEProverRegistry.sol:TEEProverRegistry": { - "initCodeHash": "0xfd1942e1c2f59b0aa72b33d698a948a53b6e4cf1040106f173fb5d89f63f57b0", - "sourceCodeHash": "0xf1ec1f02f540da659a204b26acf986fdce7d7d63bba67a87923f52453fb92ccb" + "initCodeHash": "0x5304faf5062088ee90dbd08eedbdacea39bdf48ffca689228e63d8f71f952bfb", + "sourceCodeHash": "0x37ed4874679b18cdf447a4d1890ecfeff648b0d762d8302827cd5e823b0af3ce" }, "src/multiproof/tee/TEEVerifier.sol:TEEVerifier": { - "initCodeHash": "0x655576cc21cc5c603d55fb4dd2a2f0ef57b11deeaabd3e539b0a70a5f7e2c9af", - "sourceCodeHash": "0xd5e01b4f4e69313e56e51f2f46b7bbe699ef8dc24b8a6385b8ebac4162e05353" + "initCodeHash": "0x6af4a7e162b2589076869a920a1f5b8b458d7f0c555068fdd2b82d44a51f3b0f", + "sourceCodeHash": "0xb0453b1fee0d1f2d1f53e22cbac4c43dd9e33ab33ac373ec54f81d433aeabbd5" }, "src/multiproof/zk/ZKVerifier.sol:ZkVerifier": { "initCodeHash": "0xcdc5bd5fa8ffad76358b8f34a53b8c0cb36540176c31f82d52546373d0bd5245", diff --git a/snapshots/storageLayout/DevTEEProverRegistry.json b/snapshots/storageLayout/DevTEEProverRegistry.json index 93dcbfff3..30836afaa 100644 --- a/snapshots/storageLayout/DevTEEProverRegistry.json +++ b/snapshots/storageLayout/DevTEEProverRegistry.json @@ -75,5 +75,12 @@ "offset": 0, "slot": "105", "type": "struct EnumerableSetLib.AddressSet" + }, + { + "bytes": "32", + "label": "signerTEEType", + "offset": 0, + "slot": "106", + "type": "mapping(address => enum TEEProverRegistry.TEEType)" } ] \ No newline at end of file diff --git a/snapshots/storageLayout/TDXVerifier.json b/snapshots/storageLayout/TDXVerifier.json new file mode 100644 index 000000000..2f13d341b --- /dev/null +++ b/snapshots/storageLayout/TDXVerifier.json @@ -0,0 +1,37 @@ +[ + { + "bytes": "8", + "label": "maxTimeDiff", + "offset": 0, + "slot": "0", + "type": "uint64" + }, + { + "bytes": "20", + "label": "proofSubmitter", + "offset": 8, + "slot": "0", + "type": "address" + }, + { + "bytes": "32", + "label": "rootCaHash", + "offset": 0, + "slot": "1", + "type": "bytes32" + }, + { + "bytes": "32", + "label": "zkConfig", + "offset": 0, + "slot": "2", + "type": "mapping(enum ZkCoProcessorType => struct ZkCoProcessorConfig)" + }, + { + "bytes": "32", + "label": "allowedTcbStatuses", + "offset": 0, + "slot": "3", + "type": "mapping(enum TDXTcbStatus => bool)" + } +] \ No newline at end of file diff --git a/snapshots/storageLayout/TEEProverRegistry.json b/snapshots/storageLayout/TEEProverRegistry.json index 93dcbfff3..30836afaa 100644 --- a/snapshots/storageLayout/TEEProverRegistry.json +++ b/snapshots/storageLayout/TEEProverRegistry.json @@ -75,5 +75,12 @@ "offset": 0, "slot": "105", "type": "struct EnumerableSetLib.AddressSet" + }, + { + "bytes": "32", + "label": "signerTEEType", + "offset": 0, + "slot": "106", + "type": "mapping(address => enum TEEProverRegistry.TEEType)" } ] \ No newline at end of file diff --git a/src/multiproof/AggregateVerifier.sol b/src/multiproof/AggregateVerifier.sol index 4c99f1d84..0c63db975 100644 --- a/src/multiproof/AggregateVerifier.sol +++ b/src/multiproof/AggregateVerifier.sol @@ -65,6 +65,9 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { /// @notice The minimum number of proofs required to resolve the game. uint256 public constant PROOF_THRESHOLD = 1; + + /// @notice TEE proof payload size: nitro signature(65) + tdx signature(65). + uint256 internal constant TEE_PROOF_BYTES_LENGTH = 130; //////////////////////////////////////////////////////////////// // Immutables // //////////////////////////////////////////////////////////////// @@ -877,7 +880,7 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { } /// @notice Verifies a TEE proof for the current game. - /// @param proofBytes The proof: signature(65). + /// @param proofBytes The proof: nitro signature(65) + tdx signature(65). function _verifyTeeProof( bytes calldata proofBytes, address proposer, @@ -891,6 +894,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { internal view { + if (proofBytes.length != TEE_PROOF_BYTES_LENGTH) revert InvalidProof(); + bytes32 journal = keccak256( abi.encodePacked( proposer, @@ -1036,8 +1041,8 @@ contract AggregateVerifier is Clone, ReentrancyGuard, ISemver { } /// @notice Semantic version. - /// @custom:semver 0.1.0 + /// @custom:semver 0.2.0 function version() public pure virtual returns (string memory) { - return "0.1.0"; + return "0.2.0"; } } diff --git a/src/multiproof/mocks/MockDevTEEProverRegistry.sol b/src/multiproof/mocks/MockDevTEEProverRegistry.sol index c01b124f1..e21c04b7c 100644 --- a/src/multiproof/mocks/MockDevTEEProverRegistry.sol +++ b/src/multiproof/mocks/MockDevTEEProverRegistry.sol @@ -2,8 +2,8 @@ pragma solidity 0.8.15; import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; -import { EnumerableSetLib } from "@solady-v0.0.245/utils/EnumerableSetLib.sol"; import { TEEProverRegistry } from "src/multiproof/tee/TEEProverRegistry.sol"; @@ -12,25 +12,29 @@ import { TEEProverRegistry } from "src/multiproof/tee/TEEProverRegistry.sol"; /// @dev This contract adds addDevSigner() which bypasses AWS Nitro attestation verification. /// DO NOT deploy this contract to production networks. contract DevTEEProverRegistry is TEEProverRegistry { - using EnumerableSetLib for EnumerableSetLib.AddressSet; - constructor( INitroEnclaveVerifier nitroVerifier, + ITDXVerifier tdxVerifier, IDisputeGameFactory factory ) - TEEProverRegistry(nitroVerifier, factory) + TEEProverRegistry(nitroVerifier, tdxVerifier, factory) { } - /// @notice Registers a signer for testing (bypasses attestation verification). + /// @notice Registers a Nitro signer for testing (bypasses attestation verification). /// @dev Only callable by owner. For development/testing use only. /// The imageHash parameter is stored so isValidSigner() can validate against /// the current AggregateVerifier's TEE_IMAGE_HASH. /// @param signer The address of the signer to register. /// @param imageHash The TEE image hash to associate with this signer. function addDevSigner(address signer, bytes32 imageHash) external onlyOwner { - isRegisteredSigner[signer] = true; - signerImageHash[signer] = imageHash; - _registeredSigners.add(signer); - emit SignerRegistered(signer); + _registerSigner(signer, imageHash, TEEType.NITRO); + } + + /// @notice Registers a TDX signer for testing (bypasses attestation verification). + /// @dev Only callable by owner. For development/testing use only. + /// @param signer The address of the signer to register. + /// @param imageHash The TEE image hash to associate with this signer. + function addDevTDXSigner(address signer, bytes32 imageHash) external onlyOwner { + _registerSigner(signer, imageHash, TEEType.TDX); } } diff --git a/src/multiproof/tee/TDXVerifier.sol b/src/multiproof/tee/TDXVerifier.sol new file mode 100644 index 000000000..d8175830a --- /dev/null +++ b/src/multiproof/tee/TDXVerifier.sol @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Ownable } from "@solady/auth/Ownable.sol"; +import { ISP1Verifier } from "lib/sp1-contracts/contracts/src/ISP1Verifier.sol"; +import { IRiscZeroVerifier } from "lib/risc0-ethereum/contracts/src/IRiscZeroVerifier.sol"; + +import { ISemver } from "interfaces/universal/ISemver.sol"; +import { + ITDXVerifier, + TDXTcbStatus, + TDXVerificationResult, + TDXVerifierJournal, + ZkCoProcessorType, + ZkCoProcessorConfig +} from "interfaces/multiproof/tee/ITDXVerifier.sol"; + +/// @title TDXVerifier +/// @notice Production-shape Intel TDX DCAP verifier for multiproof signer registration. +/// @dev The heavy TDX work is expected to happen in a ZK guest: quote signature verification, PCK chain +/// validation, TCB info validation, QE identity validation, CRL checks, and extraction of TDREPORT fields. +/// This contract verifies the ZK proof and enforces on-chain policy over the verified journal. +contract TDXVerifier is Ownable, ITDXVerifier, ISemver { + /// @notice Conversion factor from milliseconds to seconds. + uint256 private constant MS_PER_SECOND = 1000; + + /// @notice Maximum accepted age of a TDX quote, in seconds. + uint64 public maxTimeDiff; + + /// @notice Address authorized to submit TDX proofs, expected to be the TDX-aware registry. + address public proofSubmitter; + + /// @notice Hash of the trusted Intel root CA used by the ZK verifier guest. + bytes32 public rootCaHash; + + /// @notice Configuration mapping for each supported ZK coprocessor type. + mapping(ZkCoProcessorType => ZkCoProcessorConfig) public zkConfig; + + /// @inheritdoc ITDXVerifier + mapping(TDXTcbStatus => bool) public allowedTcbStatuses; + + /// @notice Emitted when the trusted Intel root CA hash changes. + event RootCaHashUpdated(bytes32 indexed rootCaHash); + + /// @notice Emitted when the proof submitter changes. + event ProofSubmitterChanged(address indexed proofSubmitter); + + /// @notice Emitted when the quote timestamp tolerance changes. + event MaxTimeDiffUpdated(uint64 maxTimeDiff); + + /// @notice Emitted when a TCB status policy changes. + event TcbStatusPolicyUpdated(TDXTcbStatus indexed status, bool allowed); + + /// @notice Emitted when ZK configuration changes. + event ZKConfigurationUpdated(ZkCoProcessorType indexed zkCoprocessor, ZkCoProcessorConfig config); + + /// @notice Emitted after a TDX attestation journal is accepted. + event AttestationSubmitted( + address indexed signer, + bytes32 indexed imageHash, + TDXTcbStatus indexed tcbStatus, + bytes32 pckCertHash, + bytes32 tcbInfoHash, + bytes32 qeIdentityHash + ); + + /// @notice Thrown when a zero maxTimeDiff is provided. + error ZeroMaxTimeDiff(); + + /// @notice Thrown when a zero address is provided for the proof submitter. + error ZeroProofSubmitter(); + + /// @notice Thrown when a zero root CA hash is provided. + error ZeroRootCaHash(); + + /// @notice Thrown when the caller is not the configured proof submitter. + error CallerNotProofSubmitter(); + + /// @notice Thrown when the ZK coprocessor type is unknown. + error UnknownZkCoprocessor(); + + /// @notice Thrown when the configured ZK verifier address is zero. + error ZkVerifierNotConfigured(ZkCoProcessorType zkCoprocessor); + + /// @notice Thrown when the TDX verifier guest did not report success. + error TDXVerificationFailed(TDXVerificationResult result); + + /// @notice Thrown when the journal root does not match the trusted Intel root. + error RootCaHashMismatch(bytes32 expected, bytes32 actual); + + /// @notice Thrown when the journal's TCB status is not allowed. + error TcbStatusNotAllowed(TDXTcbStatus status); + + /// @notice Thrown when collateral consumed by the ZK guest is expired. + error CollateralExpired(uint64 collateralExpiration); + + /// @notice Thrown when the quote timestamp is outside policy. + error InvalidTimestamp(uint64 timestampSeconds, uint256 currentTimestamp); + + /// @notice Thrown when the public key is not an uncompressed secp256k1 public key. + error InvalidPublicKey(); + + /// @notice Thrown when the journal signer does not match the supplied public key. + error SignerMismatch(address expected, address actual); + + /// @notice Thrown when TDREPORT.REPORTDATA does not bind the supplied public key. + error ReportDataMismatch(bytes32 expected, bytes32 actual); + + constructor( + address owner, + uint64 initialMaxTimeDiff, + bytes32 initialRootCaHash, + address initialProofSubmitter, + ZkCoProcessorType zkCoprocessor, + ZkCoProcessorConfig memory config, + TDXTcbStatus[] memory initialAllowedTcbStatuses + ) { + _initializeOwner(owner); + _setMaxTimeDiff(initialMaxTimeDiff); + _setRootCaHash(initialRootCaHash); + _setProofSubmitter(initialProofSubmitter); + _setZkConfiguration(zkCoprocessor, config); + + for (uint256 i = 0; i < initialAllowedTcbStatuses.length; i++) { + _setTcbStatusAllowed(initialAllowedTcbStatuses[i], true); + } + } + + /// @inheritdoc ITDXVerifier + function getZkConfig(ZkCoProcessorType zkCoprocessor) external view returns (ZkCoProcessorConfig memory) { + return zkConfig[zkCoprocessor]; + } + + /// @notice Sets the trusted Intel root CA hash. + function setRootCaHash(bytes32 newRootCaHash) external onlyOwner { + _setRootCaHash(newRootCaHash); + } + + /// @notice Sets the proof submitter, expected to be the TDX-aware registry. + function setProofSubmitter(address newProofSubmitter) external onlyOwner { + _setProofSubmitter(newProofSubmitter); + } + + /// @notice Sets maximum allowed quote age, in seconds. + function setMaxTimeDiff(uint64 newMaxTimeDiff) external onlyOwner { + _setMaxTimeDiff(newMaxTimeDiff); + } + + /// @notice Sets whether a TDX TCB status is accepted. + function setTcbStatusAllowed(TDXTcbStatus status, bool allowed) external onlyOwner { + _setTcbStatusAllowed(status, allowed); + } + + /// @notice Configures a ZK verifier/program for a coprocessor. + function setZkConfiguration(ZkCoProcessorType zkCoprocessor, ZkCoProcessorConfig memory config) external onlyOwner { + _setZkConfiguration(zkCoprocessor, config); + } + + /// @inheritdoc ITDXVerifier + function verify( + bytes calldata output, + ZkCoProcessorType zkCoprocessor, + bytes calldata proofBytes + ) + external + returns (TDXVerifierJournal memory journal) + { + if (msg.sender != proofSubmitter) revert CallerNotProofSubmitter(); + + _verifyZk(zkCoprocessor, output, proofBytes); + journal = abi.decode(output, (TDXVerifierJournal)); + _verifyJournal(journal); + + emit AttestationSubmitted( + journal.signer, + journal.imageHash, + journal.tcbStatus, + journal.pckCertHash, + journal.tcbInfoHash, + journal.qeIdentityHash + ); + } + + /// @notice Semantic version. + /// @custom:semver 0.2.0 + function version() public pure virtual returns (string memory) { + return "0.2.0"; + } + + function _verifyJournal(TDXVerifierJournal memory journal) internal view { + if (journal.result != TDXVerificationResult.Success) revert TDXVerificationFailed(journal.result); + bytes32 expectedRootCaHash = rootCaHash; + if (journal.rootCaHash != expectedRootCaHash) { + revert RootCaHashMismatch(expectedRootCaHash, journal.rootCaHash); + } + if (!allowedTcbStatuses[journal.tcbStatus]) revert TcbStatusNotAllowed(journal.tcbStatus); + if (journal.collateralExpiration <= block.timestamp) revert CollateralExpired(journal.collateralExpiration); + + uint64 timestamp = journal.timestamp / uint64(MS_PER_SECOND); + if (timestamp + maxTimeDiff <= block.timestamp || timestamp >= block.timestamp) { + revert InvalidTimestamp(timestamp, block.timestamp); + } + + bytes32 publicKeyHash = _derivePublicKeyHash(journal.publicKey); + address signer = address(uint160(uint256(publicKeyHash))); + if (journal.signer != signer) revert SignerMismatch(signer, journal.signer); + if (journal.reportDataPrefix != publicKeyHash) { + revert ReportDataMismatch(publicKeyHash, journal.reportDataPrefix); + } + } + + function _verifyZk( + ZkCoProcessorType zkCoprocessor, + bytes calldata output, + bytes calldata proofBytes + ) + internal + view + { + ZkCoProcessorConfig memory config = zkConfig[zkCoprocessor]; + if (config.zkVerifier == address(0)) revert ZkVerifierNotConfigured(zkCoprocessor); + + if (zkCoprocessor == ZkCoProcessorType.RiscZero) { + IRiscZeroVerifier(config.zkVerifier).verify(proofBytes, config.verifierId, sha256(output)); + } else if (zkCoprocessor == ZkCoProcessorType.Succinct) { + ISP1Verifier(config.zkVerifier).verifyProof(config.verifierId, output, proofBytes); + } else { + revert UnknownZkCoprocessor(); + } + } + + function _derivePublicKeyHash(bytes memory publicKey) internal pure returns (bytes32 publicKeyHash) { + if (publicKey.length != 65 || publicKey[0] != 0x04) revert InvalidPublicKey(); + // Skip the 32-byte length word and the 0x04 uncompressed prefix; hash the 64-byte X||Y. + assembly { + publicKeyHash := keccak256(add(publicKey, 0x21), 64) + } + } + + function _setRootCaHash(bytes32 newRootCaHash) internal { + if (newRootCaHash == bytes32(0)) revert ZeroRootCaHash(); + rootCaHash = newRootCaHash; + emit RootCaHashUpdated(newRootCaHash); + } + + function _setProofSubmitter(address newProofSubmitter) internal { + if (newProofSubmitter == address(0)) revert ZeroProofSubmitter(); + proofSubmitter = newProofSubmitter; + emit ProofSubmitterChanged(newProofSubmitter); + } + + function _setMaxTimeDiff(uint64 newMaxTimeDiff) internal { + if (newMaxTimeDiff == 0) revert ZeroMaxTimeDiff(); + maxTimeDiff = newMaxTimeDiff; + emit MaxTimeDiffUpdated(newMaxTimeDiff); + } + + function _setTcbStatusAllowed(TDXTcbStatus status, bool allowed) internal { + allowedTcbStatuses[status] = allowed; + emit TcbStatusPolicyUpdated(status, allowed); + } + + function _setZkConfiguration(ZkCoProcessorType zkCoprocessor, ZkCoProcessorConfig memory config) internal { + zkConfig[zkCoprocessor] = config; + emit ZKConfigurationUpdated(zkCoprocessor, config); + } +} diff --git a/src/multiproof/tee/TEEProverRegistry.sol b/src/multiproof/tee/TEEProverRegistry.sol index ec6652b44..81b255c8d 100644 --- a/src/multiproof/tee/TEEProverRegistry.sol +++ b/src/multiproof/tee/TEEProverRegistry.sol @@ -9,6 +9,7 @@ import { Pcr, Bytes48 } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier, TDXVerifierJournal } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { OwnableManagedUpgradeable } from "lib/op-enclave/contracts/src/OwnableManagedUpgradeable.sol"; import { ISemver } from "interfaces/universal/ISemver.sol"; import { EnumerableSetLib } from "@solady-v0.0.245/utils/EnumerableSetLib.sol"; @@ -16,15 +17,22 @@ import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol" import { GameType } from "src/dispute/lib/Types.sol"; /// @title TEEProverRegistry -/// @notice Manages TEE signer registration via ZK-verified AWS Nitro attestation. -/// @dev Signers are registered by providing a ZK proof of a valid AWS Nitro attestation document, -/// verified through an external NitroEnclaveVerifier contract (Risc0). +/// @notice Manages TEE signer registration via ZK-verified Nitro or TDX attestation. +/// @dev Nitro signers are registered by providing a ZK proof of a valid AWS Nitro attestation document, +/// verified through an external NitroEnclaveVerifier contract (Risc0). TDX signers are registered +/// through the TDXVerifier configured on the implementation. /// Registration is PCR0-agnostic: any enclave with a valid attestation can register, /// enabling pre-registration before hardforks. PCR0 enforcement happens at proof-submission /// time in TEEVerifier, which checks signerImageHash against the AggregateVerifier's -/// TEE_IMAGE_HASH. +/// TEE_IMAGE_HASH and requires one Nitro signer plus one TDX signer. contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { using EnumerableSetLib for EnumerableSetLib.AddressSet; + enum TEEType { + NONE, + NITRO, + TDX + } + /// @notice Maximum age of an attestation document (60 minutes), in seconds. uint256 public constant MAX_AGE = 60 minutes; @@ -36,6 +44,9 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// @notice The external NitroEnclaveVerifier contract used for ZK attestation verification. INitroEnclaveVerifier public immutable NITRO_VERIFIER; + /// @notice The external TDXVerifier contract used for ZK TDX quote verification. + ITDXVerifier public immutable TDX_VERIFIER; + /// @notice The DisputeGameFactory used to look up the current AggregateVerifier and its TEE_IMAGE_HASH. IDisputeGameFactory public immutable DISPUTE_GAME_FACTORY; @@ -62,9 +73,15 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// Enables O(1) on-chain enumeration via `getRegisteredSigners()`. EnumerableSetLib.AddressSet internal _registeredSigners; + /// @notice Mapping of signer address to the attestation type used to register it. + mapping(address => TEEType) public signerTEEType; + /// @notice Emitted when a signer is registered. event SignerRegistered(address indexed signer); + /// @notice Emitted when a TDX signer is registered. + event TDXSignerRegistered(address indexed signer, bytes32 indexed imageHash, bytes32 reportDataSuffix); + /// @notice Emitted when a signer is deregistered. event SignerDeregistered(address indexed signer); @@ -95,9 +112,20 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// @notice Thrown when setting a game type whose AggregateVerifier has no TEE_IMAGE_HASH. error InvalidGameType(); - constructor(INitroEnclaveVerifier nitroVerifier, IDisputeGameFactory factory) { + /// @notice Thrown when the TDX verifier is not configured. + error TDXVerifierNotSet(); + + /// @notice Thrown when attempting to register a signer with no TEE type. + error InvalidTEEType(); + + /// @notice Thrown when a signer is already registered under another TEE type. + error SignerTEETypeMismatch(address signer, TEEType existingTEEType, TEEType newTEEType); + + constructor(INitroEnclaveVerifier nitroVerifier, ITDXVerifier tdxVerifier, IDisputeGameFactory factory) { if (address(factory) == address(0)) revert DisputeGameFactoryNotSet(); + if (address(tdxVerifier) == address(0)) revert TDXVerifierNotSet(); NITRO_VERIFIER = nitroVerifier; + TDX_VERIFIER = tdxVerifier; DISPUTE_GAME_FACTORY = factory; initialize({ initialOwner: address(0xdEaD), @@ -119,14 +147,8 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// @dev Validates that the new game type has an AggregateVerifier with a non-zero TEE_IMAGE_HASH. /// @param gameType_ The new game type ID. function setGameType(GameType gameType_) external onlyOwner { - // Validate the new game type points to a valid AggregateVerifier with a TEE_IMAGE_HASH - GameType oldGameType = gameType; + if (_getExpectedImageHash(gameType_) == bytes32(0)) revert InvalidGameType(); gameType = gameType_; - bytes32 imageHash = _getExpectedImageHash(); - if (imageHash == bytes32(0)) { - gameType = oldGameType; - revert InvalidGameType(); - } emit GameTypeUpdated(gameType_); } @@ -151,26 +173,30 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { // the attestation is generated and when it is submitted to this contract. if (journal.timestamp / MS_PER_SECOND + MAX_AGE <= block.timestamp) revert AttestationTooOld(); - // Extract the attestation's PCR0 and store it for TEEVerifier to check at - // proof-submission time. No comparison against the current TEE_IMAGE_HASH - // here — the registry accepts any valid attestation. bytes32 pcr0Hash = _extractPCR0Hash(journal.pcrs); - // The publicKey is encoded in ANSI X9.62 format: 0x04 || x || y (65 bytes). - // We skip the first byte (0x04 prefix) when hashing to derive the address. + // publicKey is ANSI X9.62 uncompressed: 0x04 || x || y (65 bytes). Skip the prefix and hash + // only the 64-byte x||y to derive the signer address. bytes memory pubKey = journal.publicKey; if (pubKey.length != 65) revert InvalidPublicKey(); bytes32 publicKeyHash; assembly { - // Length is hardcoded to 64 to skip the 0x04 prefix and hash only the x and y coordinates publicKeyHash := keccak256(add(pubKey, 0x21), 64) } address enclaveAddress = address(uint160(uint256(publicKeyHash))); - isRegisteredSigner[enclaveAddress] = true; - signerImageHash[enclaveAddress] = pcr0Hash; - _registeredSigners.add(enclaveAddress); - emit SignerRegistered(enclaveAddress); + _registerSigner(enclaveAddress, pcr0Hash, TEEType.NITRO); + } + + /// @notice Registers a signer using a ZK proof of Intel TDX DCAP quote verification. + /// @param output ABI-encoded TDXVerifierJournal public values from the ZK verifier guest. + /// @param proofBytes ZK proof bytes. + function registerTDXSigner(bytes calldata output, bytes calldata proofBytes) external onlyOwnerOrManager { + TDXVerifierJournal memory journal = TDX_VERIFIER.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + + _registerSigner(journal.signer, journal.imageHash, TEEType.TDX); + + emit TDXSignerRegistered(journal.signer, journal.imageHash, journal.reportDataSuffix); } /// @notice Deregisters a signer. @@ -178,6 +204,7 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { function deregisterSigner(address signer) external onlyOwnerOrManager { delete isRegisteredSigner[signer]; delete signerImageHash[signer]; + delete signerTEEType[signer]; _registeredSigners.remove(signer); emit SignerDeregistered(signer); } @@ -189,7 +216,8 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// @param signer The address to check. /// @return True if the signer is registered with the current image hash, false otherwise. function isValidSigner(address signer) external view returns (bool) { - return isRegisteredSigner[signer] && signerImageHash[signer] == _getExpectedImageHash(); + if (!isRegisteredSigner[signer]) return false; + return signerImageHash[signer] == _getExpectedImageHash(gameType); } /// @notice Returns all currently registered signer addresses. @@ -203,7 +231,7 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { /// @notice Returns the expected TEE image hash from the current AggregateVerifier. /// @return The TEE_IMAGE_HASH from the AggregateVerifier registered in the factory. function getExpectedImageHash() external view returns (bytes32) { - return _getExpectedImageHash(); + return _getExpectedImageHash(gameType); } /// @notice Initializes the contract with owner, manager, proposers, and game type. @@ -233,14 +261,29 @@ contract TEEProverRegistry is OwnableManagedUpgradeable, ISemver { } /// @notice Semantic version. - /// @custom:semver 0.5.0 + /// @custom:semver 0.7.0 function version() public pure virtual returns (string memory) { - return "0.5.0"; + return "0.7.0"; + } + + /// @dev Registers a signer and stores the image hash enforced by TEEVerifier at proof-submission time. + function _registerSigner(address signer, bytes32 imageHash, TEEType teeType) internal { + if (teeType == TEEType.NONE) revert InvalidTEEType(); + TEEType existingTEEType = signerTEEType[signer]; + if (existingTEEType != TEEType.NONE && existingTEEType != teeType) { + revert SignerTEETypeMismatch(signer, existingTEEType, teeType); + } + + isRegisteredSigner[signer] = true; + signerImageHash[signer] = imageHash; + signerTEEType[signer] = teeType; + _registeredSigners.add(signer); + emit SignerRegistered(signer); } - /// @dev Reads TEE_IMAGE_HASH from the AggregateVerifier registered in the factory. - function _getExpectedImageHash() internal view returns (bytes32) { - address impl = address(DISPUTE_GAME_FACTORY.gameImpls(gameType)); + /// @dev Reads TEE_IMAGE_HASH from the AggregateVerifier registered in the factory for `gameType_`. + function _getExpectedImageHash(GameType gameType_) internal view returns (bytes32) { + address impl = address(DISPUTE_GAME_FACTORY.gameImpls(gameType_)); // AggregateVerifier.TEE_IMAGE_HASH() selector (bool success, bytes memory data) = impl.staticcall(abi.encodeWithSignature("TEE_IMAGE_HASH()")); if (!success || data.length != 32) revert ImageHashReadFailed(); diff --git a/src/multiproof/tee/TEEVerifier.sol b/src/multiproof/tee/TEEVerifier.sol index 73878af80..6fa772ee6 100644 --- a/src/multiproof/tee/TEEVerifier.sol +++ b/src/multiproof/tee/TEEVerifier.sol @@ -10,19 +10,25 @@ import { TEEProverRegistry } from "./TEEProverRegistry.sol"; import { Verifier } from "../Verifier.sol"; /// @title TEEVerifier -/// @notice Stateless TEE proof verifier that validates signatures against registered signers. +/// @notice Stateless TEE proof verifier that validates Nitro and TDX signatures against registered signers. /// @dev This contract is designed to be used as the TEE_VERIFIER in the AggregateVerifier. -/// It verifies that proofs are signed by enclave addresses registered in TEEProverRegistry -/// via AWS Nitro attestation. PCR0 (enclave image hash) enforcement is handled by +/// It verifies that proofs are signed by a Nitro signer and a TDX signer registered in +/// TEEProverRegistry. PCR0 / TDX image hash enforcement is handled by /// AggregateVerifier, which bakes TEE_IMAGE_HASH into the journal that the enclave signs. /// 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 Verifier, ISemver { /// @notice The TEEProverRegistry contract that manages valid TEE signers. - /// @dev Signers are registered via AWS Nitro attestation in TEEProverRegistry. + /// @dev Signers are registered via Nitro or TDX attestation in TEEProverRegistry. TEEProverRegistry public immutable TEE_PROVER_REGISTRY; - /// @notice Thrown when the recovered signer is not a valid registered signer. + /// @notice Size of an ECDSA signature in bytes. + uint256 internal constant SIGNATURE_SIZE = 65; + + /// @notice Size of a TEE proof: proposer(20) + nitro signature(65) + tdx signature(65). + uint256 internal constant TEE_PROOF_SIZE = 20 + SIGNATURE_SIZE * 2; + + /// @notice Thrown when a recovered signer is not a valid registered signer. error InvalidSigner(address signer); /// @notice Thrown when the signer's registered image hash does not match the claimed imageId. @@ -37,6 +43,15 @@ contract TEEVerifier is Verifier, ISemver { /// @notice Thrown when the proposer is not a valid registered proposer. error InvalidProposer(address proposer); + /// @notice Thrown when both signatures recover to the same signer. + error DuplicateSigner(address signer); + + /// @notice Thrown when neither signature came from a Nitro-registered signer. + error MissingNitroSignature(); + + /// @notice Thrown when neither signature came from a TDX-registered signer. + error MissingTDXSignature(); + /// @notice Constructs the TEEVerifier contract. /// @param teeProverRegistry The TEEProverRegistry contract address. constructor( @@ -49,9 +64,10 @@ contract TEEVerifier is Verifier, ISemver { } /// @notice Verifies a TEE proof for a state transition. - /// @param proofBytes The proof: proposer(20) + signature(65) = 85 bytes. + /// @param proofBytes The proof: proposer(20) + two signatures(65 each) = 150 bytes. + /// One signature must recover to a Nitro signer and the other to a TDX signer. /// @param imageId The claimed TEE image hash (from the calling AggregateVerifier's TEE_IMAGE_HASH). - /// Validated against the signer's registered image hash to prevent cross-game-type attacks. + /// Validated against each signer's registered image hash to prevent cross-game-type attacks. /// @param journal The keccak256 hash of the proof's public inputs. /// @return valid Whether the proof is valid. function verify( @@ -65,42 +81,62 @@ contract TEEVerifier is Verifier, ISemver { notNullified returns (bool) { - if (proofBytes.length < 85) revert InvalidProofFormat(); + if (proofBytes.length != TEE_PROOF_SIZE) revert InvalidProofFormat(); address proposer = address(bytes20(proofBytes[0:20])); - bytes calldata signature = proofBytes[20:85]; - - // Recover the signer from the signature - // The signature should be over the journal hash directly (not eth-signed-message prefixed) - (address signer, ECDSA.RecoverError err) = ECDSA.tryRecover(journal, signature); - - if (err != ECDSA.RecoverError.NoError) { - revert InvalidSignature(); - } - if (!TEE_PROVER_REGISTRY.isValidProposer(proposer)) { revert InvalidProposer(proposer); } - // Check that the signer is registered - if (!TEE_PROVER_REGISTRY.isRegisteredSigner(signer)) { - revert InvalidSigner(signer); + bytes calldata firstSignature = proofBytes[20:20 + SIGNATURE_SIZE]; + bytes calldata secondSignature = proofBytes[20 + SIGNATURE_SIZE:TEE_PROOF_SIZE]; + + address firstSigner = _recoverSigner(journal, firstSignature); + address secondSigner = _recoverSigner(journal, secondSignature); + + TEEProverRegistry.TEEType firstTEEType = _validateSigner(firstSigner, imageId); + TEEProverRegistry.TEEType secondTEEType = _validateSigner(secondSigner, imageId); + + if (firstSigner == secondSigner) revert DuplicateSigner(firstSigner); + + if (firstTEEType != TEEProverRegistry.TEEType.NITRO && secondTEEType != TEEProverRegistry.TEEType.NITRO) { + revert MissingNitroSignature(); + } + if (firstTEEType != TEEProverRegistry.TEEType.TDX && secondTEEType != TEEProverRegistry.TEEType.TDX) { + revert MissingTDXSignature(); } - // Check that the signer's registered image hash matches the calling AggregateVerifier's imageId. - // This prevents a signer registered under one enclave image from being used in a game + return true; + } + + function _recoverSigner(bytes32 journal, bytes calldata signature) internal pure returns (address signer) { + // The signature should be over the journal hash directly (not eth-signed-message prefixed). + ECDSA.RecoverError err; + (signer, err) = ECDSA.tryRecover(journal, signature); + if (err != ECDSA.RecoverError.NoError) revert InvalidSignature(); + } + + function _validateSigner(address signer, bytes32 imageId) + internal + view + returns (TEEProverRegistry.TEEType teeType) + { + // A registered signer always has a non-NONE TEE type, so this single read also + // serves as the registration check (saves an SLOAD versus calling isRegisteredSigner). + teeType = TEE_PROVER_REGISTRY.signerTEEType(signer); + if (teeType == TEEProverRegistry.TEEType.NONE) revert InvalidSigner(signer); + + // Prevents a signer registered under one enclave image from being used in a game // that expects a different image (e.g., after an upgrade or across game types). bytes32 registeredImageHash = TEE_PROVER_REGISTRY.signerImageHash(signer); if (registeredImageHash != imageId) { revert ImageIdMismatch(registeredImageHash, imageId); } - - return true; } /// @notice Semantic version. - /// @custom:semver 0.2.0 + /// @custom:semver 0.3.0 function version() public pure virtual returns (string memory) { - return "0.2.0"; + return "0.3.0"; } } diff --git a/test/multiproof/AggregateVerifier.t.sol b/test/multiproof/AggregateVerifier.t.sol index c8bf67fd1..92f5b9028 100644 --- a/test/multiproof/AggregateVerifier.t.sol +++ b/test/multiproof/AggregateVerifier.t.sol @@ -268,7 +268,7 @@ contract AggregateVerifierTest is BaseTest { Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, new bytes(130)); vm.expectRevert( abi.encodeWithSelector(AggregateVerifier.L1OriginInFuture.selector, l1OriginNumber, block.number) @@ -290,7 +290,7 @@ contract AggregateVerifierTest is BaseTest { Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, new bytes(130)); vm.expectRevert(abi.encodeWithSelector(AggregateVerifier.L1OriginTooOld.selector, l1OriginNumber, block.number)); _createAggregateVerifierGame( @@ -326,7 +326,7 @@ contract AggregateVerifierTest is BaseTest { Claim rootClaim = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber))); bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, rootClaim.raw()); + abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), l1OriginHash, l1OriginNumber, new bytes(130)); _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes @@ -352,7 +352,7 @@ contract AggregateVerifierTest is BaseTest { ); bytes memory proofBytes = - abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), expectedHash, l1OriginNumber, rootClaim.raw()); + abi.encodePacked(uint8(AggregateVerifier.ProofType.TEE), expectedHash, l1OriginNumber, new bytes(130)); _createAggregateVerifierGame( TEE_PROVER, rootClaim, currentL2BlockNumber, address(anchorStateRegistry), proofBytes diff --git a/test/multiproof/BaseTest.t.sol b/test/multiproof/BaseTest.t.sol index 55c4d311d..7fd9e0b0d 100644 --- a/test/multiproof/BaseTest.t.sol +++ b/test/multiproof/BaseTest.t.sol @@ -218,10 +218,33 @@ contract BaseTest is Test { bytes32 l1OriginHash = blockhash(block.number - 1); // Use the previous block number as l1OriginNumber uint256 l1OriginNumber = block.number - 1; - // Add some padding/signature data (65 bytes minimum for a signature) - bytes memory signature = abi.encodePacked(salt, bytes32(0), bytes32(0), uint8(27)); + return abi.encodePacked(uint8(proofType), l1OriginHash, l1OriginNumber, _generateProofBody(salt, proofType)); + } + + function _generateProposalProof( + bytes memory salt, + AggregateVerifier.ProofType proofType + ) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(uint8(proofType), _generateProofBody(salt, proofType)); + } - return abi.encodePacked(uint8(proofType), l1OriginHash, l1OriginNumber, signature); + function _generateProofBody( + bytes memory salt, + AggregateVerifier.ProofType proofType + ) + internal + pure + returns (bytes memory) + { + if (proofType == AggregateVerifier.ProofType.TEE) { + bytes32 saltHash = keccak256(salt); + return abi.encodePacked(saltHash, bytes32(0), uint8(27), saltHash, bytes32(uint256(1)), uint8(28)); + } + return abi.encodePacked(salt, bytes32(0), bytes32(0), uint8(27)); } function _generateIntermediateRootsExceptLast(uint256 l2BlockNumber) internal pure returns (bytes memory) { diff --git a/test/multiproof/Challenge.t.sol b/test/multiproof/Challenge.t.sol index b36cf03e4..29399e507 100644 --- a/test/multiproof/Challenge.t.sol +++ b/test/multiproof/Challenge.t.sol @@ -80,7 +80,7 @@ contract ChallengeTest is BaseTest { ); Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + bytes memory teeProof2 = _generateProposalProof("tee-proof-2", AggregateVerifier.ProofType.TEE); vm.expectRevert(AggregateVerifier.InvalidProofType.selector); game1.challenge(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); @@ -170,7 +170,7 @@ contract ChallengeTest is BaseTest { ); Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + bytes memory teeProof2 = _generateProposalProof("tee-proof-2", AggregateVerifier.ProofType.TEE); game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); @@ -284,7 +284,7 @@ contract ChallengeTest is BaseTest { _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); + bytes memory teeNullifyB = _generateProposalProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); uint256 lastIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; gameB.nullify(teeNullifyB, lastIdx, rootNullifyB.raw()); assertTrue(teeVerifier.nullified()); diff --git a/test/multiproof/Nullify.t.sol b/test/multiproof/Nullify.t.sol index 5bc8c4aa9..3a28acc8c 100644 --- a/test/multiproof/Nullify.t.sol +++ b/test/multiproof/Nullify.t.sol @@ -20,7 +20,7 @@ contract NullifyTest is BaseTest { ); Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + bytes memory teeProof2 = _generateProposalProof("tee-proof-2", AggregateVerifier.ProofType.TEE); game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); @@ -87,7 +87,7 @@ contract NullifyTest is BaseTest { assertEq(game.expectedResolution().raw(), block.timestamp + 1 days); Claim rootClaim2 = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "tee2"))); - bytes memory teeProof2 = _generateProof("tee-proof-2", AggregateVerifier.ProofType.TEE); + bytes memory teeProof2 = _generateProposalProof("tee-proof-2", AggregateVerifier.ProofType.TEE); game.nullify(teeProof2, BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1, rootClaim2.raw()); assertEq(uint8(game.status()), uint8(GameStatus.IN_PROGRESS)); @@ -195,7 +195,7 @@ contract NullifyTest is BaseTest { assertEq(gameA.proofCount(), 1); Claim rootClaimNullify = Claim.wrap(keccak256(abi.encode(currentL2BlockNumber, "nullify-b"))); - bytes memory teeProofNullify = _generateProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); + bytes memory teeProofNullify = _generateProposalProof("tee-nullify-b", AggregateVerifier.ProofType.TEE); uint256 lastIntermediateIdx = BLOCK_INTERVAL / INTERMEDIATE_BLOCK_INTERVAL - 1; gameB.nullify(teeProofNullify, lastIntermediateIdx, rootClaimNullify.raw()); diff --git a/test/multiproof/TDXVerifier.t.sol b/test/multiproof/TDXVerifier.t.sol new file mode 100644 index 000000000..291b34614 --- /dev/null +++ b/test/multiproof/TDXVerifier.t.sol @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import { Test } from "forge-std/Test.sol"; + +import { TDXTcbStatus, TDXVerificationResult, TDXVerifierJournal } from "interfaces/multiproof/tee/ITDXVerifier.sol"; +import { ZkCoProcessorConfig, ZkCoProcessorType } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; + +import { TDXVerifier } from "src/multiproof/tee/TDXVerifier.sol"; + +contract TDXVerifierTest is Test { + TDXVerifier internal verifier; + + address internal owner; + address internal proofSubmitter; + address internal mockRiscZeroVerifier; + address internal mockSP1Verifier; + + bytes32 internal constant ROOT_CA_HASH = keccak256("intel-root-ca"); + bytes32 internal constant WRONG_ROOT_CA_HASH = keccak256("wrong-root-ca"); + bytes32 internal constant VERIFIER_ID = keccak256("tdx-verifier-id"); + bytes32 internal constant AGGREGATOR_ID = keccak256("tdx-aggregator-id"); + bytes32 internal constant IMAGE_HASH = keccak256("tdx-image"); + bytes32 internal constant MRTD_HASH = keccak256("mrtd"); + bytes32 internal constant REPORT_DATA_SUFFIX = keccak256("multiproof-tdx-poc"); + + uint64 internal constant MAX_TIME_DIFF = 3600; + uint256 internal constant NOW = 1_700_000_000; + + function setUp() public { + vm.warp(NOW); + + owner = address(this); + proofSubmitter = address(this); + mockRiscZeroVerifier = makeAddr("mock-risc-zero"); + mockSP1Verifier = makeAddr("mock-sp1"); + + TDXTcbStatus[] memory allowedStatuses = new TDXTcbStatus[](2); + allowedStatuses[0] = TDXTcbStatus.UpToDate; + allowedStatuses[1] = TDXTcbStatus.SwHardeningNeeded; + + verifier = new TDXVerifier( + owner, + MAX_TIME_DIFF, + ROOT_CA_HASH, + proofSubmitter, + ZkCoProcessorType.Succinct, + ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockSP1Verifier }), + allowedStatuses + ); + } + + function testVerifySucceedsWithSP1ProofAndAllowedJournal() public { + TDXVerifierJournal memory journal = _successJournal(); + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + TDXVerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + + assertEq(result.signer, journal.signer); + assertEq(result.imageHash, IMAGE_HASH); + assertEq(uint256(result.tcbStatus), uint256(TDXTcbStatus.UpToDate)); + } + + function testVerifySucceedsWithRiscZeroProof() public { + ZkCoProcessorConfig memory config = ZkCoProcessorConfig({ + verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockRiscZeroVerifier + }); + verifier.setZkConfiguration(ZkCoProcessorType.RiscZero, config); + + TDXVerifierJournal memory journal = _successJournal(); + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"5678"; + _mockRiscZeroVerify(VERIFIER_ID, output, proofBytes); + + TDXVerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.RiscZero, proofBytes); + + assertEq(result.signer, journal.signer); + assertEq(result.imageHash, IMAGE_HASH); + } + + function testVerifyRevertsIfNotProofSubmitter() public { + bytes memory output = abi.encode(_successJournal()); + + vm.prank(makeAddr("not-submitter")); + vm.expectRevert(TDXVerifier.CallerNotProofSubmitter.selector); + verifier.verify(output, ZkCoProcessorType.Succinct, ""); + } + + function testVerifyRevertsIfZkVerifierNotConfigured() public { + bytes memory output = abi.encode(_successJournal()); + + vm.expectRevert( + abi.encodeWithSelector(TDXVerifier.ZkVerifierNotConfigured.selector, ZkCoProcessorType.RiscZero) + ); + verifier.verify(output, ZkCoProcessorType.RiscZero, ""); + } + + function testVerifyRevertsForUnknownCoprocessor() public { + ZkCoProcessorConfig memory config = + ZkCoProcessorConfig({ verifierId: VERIFIER_ID, aggregatorId: AGGREGATOR_ID, zkVerifier: mockSP1Verifier }); + verifier.setZkConfiguration(ZkCoProcessorType.Unknown, config); + + bytes memory output = abi.encode(_successJournal()); + + vm.expectRevert(TDXVerifier.UnknownZkCoprocessor.selector); + verifier.verify(output, ZkCoProcessorType.Unknown, ""); + } + + function testVerifyRevertsWhenGuestReportsFailure() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.result = TDXVerificationResult.PckCertChainInvalid; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert(abi.encodeWithSelector(TDXVerifier.TDXVerificationFailed.selector, journal.result)); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenRootCaHashMismatches() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.rootCaHash = WRONG_ROOT_CA_HASH; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert( + abi.encodeWithSelector(TDXVerifier.RootCaHashMismatch.selector, ROOT_CA_HASH, WRONG_ROOT_CA_HASH) + ); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenTcbStatusIsNotAllowed() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.tcbStatus = TDXTcbStatus.ConfigurationNeeded; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert(abi.encodeWithSelector(TDXVerifier.TcbStatusNotAllowed.selector, journal.tcbStatus)); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testOwnerCanAllowAdditionalTcbStatus() public { + verifier.setTcbStatusAllowed(TDXTcbStatus.ConfigurationNeeded, true); + + TDXVerifierJournal memory journal = _successJournal(); + journal.tcbStatus = TDXTcbStatus.ConfigurationNeeded; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + TDXVerifierJournal memory result = verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + + assertEq(uint256(result.tcbStatus), uint256(TDXTcbStatus.ConfigurationNeeded)); + } + + function testVerifyRevertsWhenCollateralExpired() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.collateralExpiration = uint64(block.timestamp); + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert(abi.encodeWithSelector(TDXVerifier.CollateralExpired.selector, journal.collateralExpiration)); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenTimestampTooOld() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.timestamp = uint64(block.timestamp - MAX_TIME_DIFF) * 1000; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert( + abi.encodeWithSelector( + TDXVerifier.InvalidTimestamp.selector, uint64(block.timestamp - MAX_TIME_DIFF), block.timestamp + ) + ); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenTimestampIsFromFuture() public { + TDXVerifierJournal memory journal = _successJournal(); + journal.timestamp = uint64(block.timestamp) * 1000; + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert( + abi.encodeWithSelector(TDXVerifier.InvalidTimestamp.selector, uint64(block.timestamp), block.timestamp) + ); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenReportDataDoesNotBindPublicKey() public { + TDXVerifierJournal memory journal = _successJournal(); + bytes32 expected = journal.reportDataPrefix; + journal.reportDataPrefix = keccak256("wrong-report-data"); + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert( + abi.encodeWithSelector(TDXVerifier.ReportDataMismatch.selector, expected, journal.reportDataPrefix) + ); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function testVerifyRevertsWhenSignerDoesNotMatchPublicKey() public { + TDXVerifierJournal memory journal = _successJournal(); + address expected = journal.signer; + journal.signer = makeAddr("wrong-signer"); + bytes memory output = abi.encode(journal); + bytes memory proofBytes = hex"1234"; + _mockSP1Verify(VERIFIER_ID, output, proofBytes); + + vm.expectRevert(abi.encodeWithSelector(TDXVerifier.SignerMismatch.selector, expected, journal.signer)); + verifier.verify(output, ZkCoProcessorType.Succinct, proofBytes); + } + + function _successJournal() internal view returns (TDXVerifierJournal memory journal) { + bytes memory publicKey = _publicKey(); + bytes32 publicKeyHash; + assembly { + publicKeyHash := keccak256(add(publicKey, 0x21), 64) + } + + journal = TDXVerifierJournal({ + result: TDXVerificationResult.Success, + tcbStatus: TDXTcbStatus.UpToDate, + timestamp: uint64(block.timestamp - 1) * 1000, + collateralExpiration: uint64(block.timestamp + 1 days), + rootCaHash: ROOT_CA_HASH, + pckCertHash: keccak256("pck-cert"), + tcbInfoHash: keccak256("tcb-info"), + qeIdentityHash: keccak256("qe-identity"), + publicKey: publicKey, + signer: address(uint160(uint256(publicKeyHash))), + imageHash: IMAGE_HASH, + mrTdHash: MRTD_HASH, + reportDataPrefix: publicKeyHash, + reportDataSuffix: REPORT_DATA_SUFFIX + }); + } + + function _publicKey() internal pure returns (bytes memory publicKey) { + publicKey = new bytes(65); + publicKey[0] = 0x04; + for (uint256 i = 1; i < publicKey.length; i++) { + publicKey[i] = bytes1(uint8(i)); + } + } + + function _mockRiscZeroVerify(bytes32 programId, bytes memory output, bytes memory proofBytes) internal { + vm.mockCall( + mockRiscZeroVerifier, + abi.encodeWithSelector( + bytes4(keccak256("verify(bytes,bytes32,bytes32)")), proofBytes, programId, sha256(output) + ), + "" + ); + } + + function _mockSP1Verify(bytes32 programId, bytes memory output, bytes memory proofBytes) internal { + vm.mockCall( + mockSP1Verifier, + abi.encodeWithSelector( + bytes4(keccak256("verifyProof(bytes32,bytes,bytes)")), programId, output, proofBytes + ), + "" + ); + } +} diff --git a/test/multiproof/TEEProverRegistry.t.sol b/test/multiproof/TEEProverRegistry.t.sol index b4a64a178..ebab8ebac 100644 --- a/test/multiproof/TEEProverRegistry.t.sol +++ b/test/multiproof/TEEProverRegistry.t.sol @@ -7,6 +7,7 @@ import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/trans import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { GameType } from "src/dispute/lib/Types.sol"; @@ -75,8 +76,9 @@ contract TEEProverRegistryTest is Test { mockFactory.setImpl(TEST_GAME_TYPE, address(mockVerifier)); // Deploy implementation (using DevTEEProverRegistry for test flexibility) - DevTEEProverRegistry impl = - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(mockFactory))); + DevTEEProverRegistry impl = new DevTEEProverRegistry( + INitroEnclaveVerifier(address(0)), ITDXVerifier(address(1)), IDisputeGameFactory(address(mockFactory)) + ); // Deploy proxy admin proxyAdmin = new ProxyAdmin(address(this)); @@ -98,15 +100,16 @@ contract TEEProverRegistryTest is Test { function testInitialization() public view { assertEq(teeProverRegistry.owner(), owner); assertEq(teeProverRegistry.manager(), manager); - assertEq(teeProverRegistry.version(), "0.5.0"); + assertEq(teeProverRegistry.version(), "0.7.0"); } function testInitializationWithProposers() public { address proposer1 = makeAddr("proposer1"); address proposer2 = makeAddr("proposer2"); address proposer3 = makeAddr("proposer3"); - DevTEEProverRegistry impl2 = - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(1))); + DevTEEProverRegistry impl2 = new DevTEEProverRegistry( + INitroEnclaveVerifier(address(0)), ITDXVerifier(address(1)), IDisputeGameFactory(address(1)) + ); ProxyAdmin proxyAdmin2 = new ProxyAdmin(address(this)); address[] memory proposers = new address[](3); proposers[0] = proposer1; @@ -213,6 +216,35 @@ contract TEEProverRegistryTest is Test { teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); assertTrue(teeProverRegistry.isValidSigner(signer)); + assertEq(uint8(teeProverRegistry.signerTEEType(signer)), uint8(TEEProverRegistry.TEEType.NITRO)); + } + + function testAddDevTDXSigner() public { + address signer = makeAddr("dev-tdx-signer"); + + vm.prank(owner); + teeProverRegistry.addDevTDXSigner(signer, TEST_IMAGE_HASH); + + assertTrue(teeProverRegistry.isValidSigner(signer)); + assertEq(uint8(teeProverRegistry.signerTEEType(signer)), uint8(TEEProverRegistry.TEEType.TDX)); + } + + function testRegisteringSameSignerWithDifferentTEETypeFails() public { + address signer = makeAddr("dev-signer"); + + vm.prank(owner); + teeProverRegistry.addDevSigner(signer, TEST_IMAGE_HASH); + + vm.prank(owner); + vm.expectRevert( + abi.encodeWithSelector( + TEEProverRegistry.SignerTEETypeMismatch.selector, + signer, + TEEProverRegistry.TEEType.NITRO, + TEEProverRegistry.TEEType.TDX + ) + ); + teeProverRegistry.addDevTDXSigner(signer, TEST_IMAGE_HASH); } // ============ MAX_AGE Tests ============ diff --git a/test/multiproof/TEEProverRegistryTDX.t.sol b/test/multiproof/TEEProverRegistryTDX.t.sol new file mode 100644 index 000000000..0df45c5b6 --- /dev/null +++ b/test/multiproof/TEEProverRegistryTDX.t.sol @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.15; + +import { Test } from "forge-std/Test.sol"; + +import { IDisputeGame } from "interfaces/dispute/IDisputeGame.sol"; +import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; +import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { + ITDXVerifier, + TDXTcbStatus, + TDXVerificationResult, + TDXVerifierJournal +} from "interfaces/multiproof/tee/ITDXVerifier.sol"; +import { ZkCoProcessorConfig, ZkCoProcessorType } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { GameType } from "src/dispute/lib/Types.sol"; + +import { TEEProverRegistry } from "src/multiproof/tee/TEEProverRegistry.sol"; + +/// @notice Mock AggregateVerifier that returns a configurable TEE_IMAGE_HASH. +contract MockAggregateVerifierForTDXRegistry { + bytes32 public TEE_IMAGE_HASH; + + constructor(bytes32 imageHash) { + TEE_IMAGE_HASH = imageHash; + } +} + +/// @notice Mock DisputeGameFactory that returns a fixed game implementation. +contract MockDisputeGameFactoryForTDXRegistry { + mapping(uint32 => address) internal _impls; + + function setImpl(uint32 gameType, address impl) external { + _impls[gameType] = impl; + } + + function gameImpls(GameType gameType) external view returns (IDisputeGame) { + return IDisputeGame(_impls[GameType.unwrap(gameType)]); + } +} + +contract MockTDXVerifierForRegistry is ITDXVerifier { + TDXVerifierJournal internal _journal; + + function setJournal(TDXVerifierJournal memory journal) external { + _journal = journal; + } + + function verify( + bytes calldata, + ZkCoProcessorType, + bytes calldata + ) + external + view + returns (TDXVerifierJournal memory) + { + return _journal; + } + + function getZkConfig(ZkCoProcessorType) external pure returns (ZkCoProcessorConfig memory) { + return ZkCoProcessorConfig({ verifierId: bytes32(0), aggregatorId: bytes32(0), zkVerifier: address(0) }); + } + + function allowedTcbStatuses(TDXTcbStatus) external pure returns (bool) { + return true; + } + + function setProofSubmitter(address) external { } +} + +contract TEEProverRegistryTDXTest is Test { + bytes32 internal constant IMAGE_HASH = keccak256("tdx-image"); + bytes32 internal constant REPORT_DATA_SUFFIX = keccak256("multiproof-tdx-poc"); + + function testRegisterTDXSignerStoresImageHash() public { + TDXVerifierJournal memory journal = _successJournal(); + MockTDXVerifierForRegistry verifier = new MockTDXVerifierForRegistry(); + verifier.setJournal(journal); + + MockDisputeGameFactoryForTDXRegistry factory = new MockDisputeGameFactoryForTDXRegistry(); + factory.setImpl(0, address(new MockAggregateVerifierForTDXRegistry(IMAGE_HASH))); + + TEEProverRegistry registry = new TEEProverRegistry( + INitroEnclaveVerifier(address(0)), ITDXVerifier(address(verifier)), IDisputeGameFactory(address(factory)) + ); + + vm.prank(address(0xdEaD)); + registry.registerTDXSigner("", ""); + + assertTrue(registry.isRegisteredSigner(journal.signer)); + assertEq(registry.signerImageHash(journal.signer), IMAGE_HASH); + assertEq(uint8(registry.signerTEEType(journal.signer)), uint8(TEEProverRegistry.TEEType.TDX)); + assertTrue(registry.isValidSigner(journal.signer)); + } + + function testConstructorRevertsIfTDXVerifierNotSet() public { + MockDisputeGameFactoryForTDXRegistry factory = new MockDisputeGameFactoryForTDXRegistry(); + factory.setImpl(0, address(new MockAggregateVerifierForTDXRegistry(IMAGE_HASH))); + + vm.expectRevert(TEEProverRegistry.TDXVerifierNotSet.selector); + new TEEProverRegistry( + INitroEnclaveVerifier(address(0)), ITDXVerifier(address(0)), IDisputeGameFactory(address(factory)) + ); + } + + function _successJournal() internal pure returns (TDXVerifierJournal memory journal) { + address signer = address(0x1234); + + journal = TDXVerifierJournal({ + result: TDXVerificationResult.Success, + tcbStatus: TDXTcbStatus.UpToDate, + timestamp: 0, + collateralExpiration: 0, + rootCaHash: bytes32(0), + pckCertHash: bytes32(0), + tcbInfoHash: bytes32(0), + qeIdentityHash: bytes32(0), + publicKey: "", + signer: signer, + imageHash: IMAGE_HASH, + mrTdHash: bytes32(0), + reportDataPrefix: bytes32(0), + reportDataSuffix: REPORT_DATA_SUFFIX + }); + } +} diff --git a/test/multiproof/TEEVerifier.t.sol b/test/multiproof/TEEVerifier.t.sol index 89302645a..83b397f09 100644 --- a/test/multiproof/TEEVerifier.t.sol +++ b/test/multiproof/TEEVerifier.t.sol @@ -7,6 +7,7 @@ import { TransparentUpgradeableProxy } from "@openzeppelin/contracts/proxy/trans import { ProxyAdmin } from "src/universal/ProxyAdmin.sol"; import { INitroEnclaveVerifier } from "interfaces/multiproof/tee/INitroEnclaveVerifier.sol"; +import { ITDXVerifier } from "interfaces/multiproof/tee/ITDXVerifier.sol"; import { IAnchorStateRegistry } from "interfaces/dispute/IAnchorStateRegistry.sol"; import { IDisputeGameFactory } from "interfaces/dispute/IDisputeGameFactory.sol"; import { GameType } from "src/dispute/lib/Types.sol"; @@ -45,9 +46,12 @@ contract TEEVerifierTest is Test { ProxyAdmin public proxyAdmin; MockAnchorStateRegistry public anchorStateRegistry; - // Test signer - we'll derive address from private key - uint256 internal constant SIGNER_PRIVATE_KEY = 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; - address internal signerAddress; + uint256 internal constant NITRO_SIGNER_PRIVATE_KEY = + 0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + uint256 internal constant TDX_SIGNER_PRIVATE_KEY = + 0x2234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + address internal nitroSignerAddress; + address internal tdxSignerAddress; bytes32 internal constant IMAGE_ID = keccak256("test-image-id"); uint32 internal constant TEST_GAME_TYPE = 621; @@ -58,8 +62,8 @@ contract TEEVerifierTest is Test { function setUp() public { owner = address(this); - // Derive signer address from private key - signerAddress = vm.addr(SIGNER_PRIVATE_KEY); + nitroSignerAddress = vm.addr(NITRO_SIGNER_PRIVATE_KEY); + tdxSignerAddress = vm.addr(TDX_SIGNER_PRIVATE_KEY); // Deploy mock factory and verifier MockAggregateVerifierForVerifier mockVerifier = new MockAggregateVerifierForVerifier(IMAGE_ID); @@ -67,8 +71,9 @@ contract TEEVerifierTest is Test { mockFactory.setImpl(TEST_GAME_TYPE, address(mockVerifier)); // Deploy implementation (NitroEnclaveVerifier not needed for dev signer tests) - DevTEEProverRegistry impl = - new DevTEEProverRegistry(INitroEnclaveVerifier(address(0)), IDisputeGameFactory(address(mockFactory))); + DevTEEProverRegistry impl = new DevTEEProverRegistry( + INitroEnclaveVerifier(address(0)), ITDXVerifier(address(1)), IDisputeGameFactory(address(mockFactory)) + ); // Deploy proxy admin proxyAdmin = new ProxyAdmin(address(this)); @@ -84,8 +89,9 @@ contract TEEVerifierTest is Test { teeProverRegistry = DevTEEProverRegistry(address(proxy)); - // Register the signer with the correct image hash - teeProverRegistry.addDevSigner(signerAddress, IMAGE_ID); + // Register one Nitro signer and one TDX signer with the correct image hash. + teeProverRegistry.addDevSigner(nitroSignerAddress, IMAGE_ID); + teeProverRegistry.addDevTDXSigner(tdxSignerAddress, IMAGE_ID); // Set the proposer as valid teeProverRegistry.setProposer(PROPOSER, true); @@ -97,45 +103,29 @@ contract TEEVerifierTest is Test { ); } - function testVerifyValidSignature() public view { - // Create a journal hash + function testVerifyValidSignatures() public view { bytes32 journal = keccak256("test-journal"); - - // Sign the journal with the signer's private key - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - // Construct proof: proposer(20) + signature(65) = 85 bytes - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); - - // Verify should return true regardless of imageId (enforced via journal hash, not registry) + bytes memory proofBytes = _proofBytes(journal); bool result = verifier.verify(proofBytes, IMAGE_ID, journal); assertTrue(result); } function testVerifyFailsWithInvalidSignature() public { bytes32 journal = keccak256("test-journal"); - - // Create an invalid signature (all zeros except v) bytes memory invalidSignature = new bytes(65); invalidSignature[64] = bytes1(uint8(27)); // Set v to 27 - bytes memory proofBytes = abi.encodePacked(PROPOSER, invalidSignature); + bytes memory proofBytes = _buildProof(PROPOSER, invalidSignature, _signature(TDX_SIGNER_PRIVATE_KEY, journal)); vm.expectRevert(TEEVerifier.InvalidSignature.selector); verifier.verify(proofBytes, IMAGE_ID, journal); } function testVerifyFailsWithInvalidProposer() public { - // Create a journal hash bytes32 journal = keccak256("test-journal"); - - // Sign the journal with the signer's private key - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - // Construct proof: proposer(20) + signature(65) = 85 bytes - bytes memory proofBytes = abi.encodePacked(address(0), signature); + bytes memory proofBytes = _buildProof( + address(0), _signature(NITRO_SIGNER_PRIVATE_KEY, journal), _signature(TDX_SIGNER_PRIVATE_KEY, journal) + ); vm.expectRevert(abi.encodeWithSelector(TEEVerifier.InvalidProposer.selector, address(0))); verifier.verify(proofBytes, IMAGE_ID, journal); @@ -148,10 +138,8 @@ contract TEEVerifierTest is Test { bytes32 journal = keccak256("test-journal"); - (uint8 v, bytes32 r, bytes32 s) = vm.sign(unregisteredKey, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); + bytes memory proofBytes = + _buildProof(PROPOSER, _signature(unregisteredKey, journal), _signature(TDX_SIGNER_PRIVATE_KEY, journal)); vm.expectRevert(abi.encodeWithSelector(TEEVerifier.InvalidSigner.selector, unregisteredSigner)); verifier.verify(proofBytes, IMAGE_ID, journal); @@ -159,11 +147,7 @@ contract TEEVerifierTest is Test { function testVerifyFailsWithImageIdMismatch() public { bytes32 journal = keccak256("test-journal"); - - (uint8 v, bytes32 r, bytes32 s) = vm.sign(SIGNER_PRIVATE_KEY, journal); - bytes memory signature = abi.encodePacked(r, s, v); - - bytes memory proofBytes = abi.encodePacked(PROPOSER, signature); + bytes memory proofBytes = _proofBytes(journal); // Different imageId should fail — signer was registered with IMAGE_ID bytes32 wrongImageId = keccak256("different-image"); @@ -171,10 +155,47 @@ contract TEEVerifierTest is Test { verifier.verify(proofBytes, wrongImageId, journal); } + function testVerifyFailsWithDuplicateSigner() public { + bytes32 journal = keccak256("test-journal"); + bytes memory nitroSignature = _signature(NITRO_SIGNER_PRIVATE_KEY, journal); + bytes memory proofBytes = _buildProof(PROPOSER, nitroSignature, nitroSignature); + + vm.expectRevert(abi.encodeWithSelector(TEEVerifier.DuplicateSigner.selector, nitroSignerAddress)); + verifier.verify(proofBytes, IMAGE_ID, journal); + } + + function testVerifyFailsWithoutNitroSignature() public { + bytes32 journal = keccak256("test-journal"); + uint256 secondTDXKey = 0x3234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + + vm.prank(owner); + teeProverRegistry.addDevTDXSigner(vm.addr(secondTDXKey), IMAGE_ID); + + bytes memory proofBytes = + _buildProof(PROPOSER, _signature(TDX_SIGNER_PRIVATE_KEY, journal), _signature(secondTDXKey, journal)); + + vm.expectRevert(TEEVerifier.MissingNitroSignature.selector); + verifier.verify(proofBytes, IMAGE_ID, journal); + } + + function testVerifyFailsWithoutTDXSignature() public { + bytes32 journal = keccak256("test-journal"); + uint256 secondNitroKey = 0x4234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef; + + vm.prank(owner); + teeProverRegistry.addDevSigner(vm.addr(secondNitroKey), IMAGE_ID); + + bytes memory proofBytes = + _buildProof(PROPOSER, _signature(NITRO_SIGNER_PRIVATE_KEY, journal), _signature(secondNitroKey, journal)); + + vm.expectRevert(TEEVerifier.MissingTDXSignature.selector); + verifier.verify(proofBytes, IMAGE_ID, journal); + } + function testVerifyFailsWithInvalidProofFormat() public { bytes32 journal = keccak256("test-journal"); - // Proof too short (less than 85 bytes) + // Proof too short (less than proposer + two signatures). bytes memory shortProof = new bytes(50); vm.expectRevert(TEEVerifier.InvalidProofFormat.selector); @@ -184,4 +205,27 @@ contract TEEVerifierTest is Test { function testConstants() public view { assertEq(address(verifier.TEE_PROVER_REGISTRY()), address(teeProverRegistry)); } + + function _proofBytes(bytes32 journal) internal view returns (bytes memory) { + return _buildProof( + PROPOSER, _signature(NITRO_SIGNER_PRIVATE_KEY, journal), _signature(TDX_SIGNER_PRIVATE_KEY, journal) + ); + } + + function _signature(uint256 privateKey, bytes32 journal) internal pure returns (bytes memory) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(privateKey, journal); + return abi.encodePacked(r, s, v); + } + + function _buildProof( + address proposer, + bytes memory firstSignature, + bytes memory secondSignature + ) + internal + pure + returns (bytes memory) + { + return abi.encodePacked(proposer, firstSignature, secondSignature); + } } diff --git a/test/opcm/DeployImplementations.t.sol b/test/opcm/DeployImplementations.t.sol index 00fd553f5..c1fc8de93 100644 --- a/test/opcm/DeployImplementations.t.sol +++ b/test/opcm/DeployImplementations.t.sol @@ -176,6 +176,7 @@ contract DeployImplementations_Test is Test, FeatureFlags { bytes32(0), // multiproofConfigHash 621, // multiproofGameType address(0), // nitroEnclaveVerifier + address(1), // tdxVerifier 8453, // l2ChainID 100, // multiproofBlockInterval 10, // multiproofIntermediateBlockInterval @@ -312,6 +313,11 @@ contract DeployImplementations_Test is Test, FeatureFlags { vm.expectRevert("DeployImplementations: mipsVersion not set"); deployImplementations.run(input); + input = defaultInput(); + input.tdxVerifier = address(0); + vm.expectRevert("DeployImplementations: tdxVerifier not set"); + deployImplementations.run(input); + input = defaultInput(); input.superchainConfigProxy = ISuperchainConfig(address(0)); vm.expectRevert("DeployImplementations: superchainConfigProxy not set"); @@ -399,6 +405,7 @@ contract DeployImplementations_Test is Test, FeatureFlags { bytes32(0), // multiproofConfigHash 621, // multiproofGameType address(0), // nitroEnclaveVerifier + address(1), // tdxVerifier 8453, // l2ChainID 100, // multiproofBlockInterval 10, // multiproofIntermediateBlockInterval diff --git a/test/opcm/DeployOPChain.t.sol b/test/opcm/DeployOPChain.t.sol index 42f4353f9..9a6a75592 100644 --- a/test/opcm/DeployOPChain.t.sol +++ b/test/opcm/DeployOPChain.t.sol @@ -89,6 +89,7 @@ contract DeployOPChain_TestBase is Test, FeatureFlags { multiproofConfigHash: bytes32(0), multiproofGameType: 621, nitroEnclaveVerifier: address(0), + tdxVerifier: address(1), l2ChainID: 8453, multiproofBlockInterval: 100, multiproofIntermediateBlockInterval: 10,