diff --git a/packages/protocol/contracts/verifiers/AttestationVerifier.sol b/packages/protocol/contracts/verifiers/AttestationVerifier.sol new file mode 100644 index 0000000000..d725faa27d --- /dev/null +++ b/packages/protocol/contracts/verifiers/AttestationVerifier.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.24; + +import { IAttestation } from "./IAttestation.sol"; + +contract AttestationVerifier { + IAttestation public attestationVerifier; + + constructor(address _attestationVerifierAddr) { + attestationVerifier = IAttestation(_attestationVerifierAddr); + } + + error INVALID_REPORT(); + error INVALID_REPORT_DATA(); + error REPORT_DATA_MISMATCH(); + + function verifyAttestation(bytes calldata _report, bytes32 _userData) public { + if (address(attestationVerifier) == address(0)) return; + + (bool succ, bytes memory output) = attestationVerifier.verifyAndAttestOnChain(_report); + if (!succ) revert INVALID_REPORT(); + + if (output.length < 32) revert INVALID_REPORT_DATA(); + + bytes32 quoteBodyLast32; + assembly { + quoteBodyLast32 := mload(add(add(output, 0x20), sub(mload(output), 32))) + } + + if (quoteBodyLast32 != _userData) revert REPORT_DATA_MISMATCH(); + } +} diff --git a/packages/protocol/contracts/verifiers/IAttestation.sol b/packages/protocol/contracts/verifiers/IAttestation.sol new file mode 100644 index 0000000000..820d4e2768 --- /dev/null +++ b/packages/protocol/contracts/verifiers/IAttestation.sol @@ -0,0 +1,51 @@ +//SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +/** + * @title Interface standard that implement attestation contracts whose verification logic can be + * implemented + * both on-chain and with Risc0 ZK proofs + * @notice The interface simply provides two verification methods for a given attestation input. + * The user can either pay a possibly hefty gas cost to fully verify an attestation fully on-chain + * OR + * Provides ZK proofs from executing an off-chain program where the verification of such attestation + * is conducted. + * @dev should also implement Risc0 Guest Program to use this interface. + * See https://dev.risczero.com/api/blockchain-integration/bonsai-on-eth to learn more + */ +interface IAttestation { + /** + * @notice full on-chain verification for an attestation + * @dev must further specify the structure of inputs/outputs, to be serialized and passed to + * this method + * @param input - serialized raw input as defined by the project + * @return success - whether the quote has been successfully verified or not + * @return output - the output upon completion of verification. The output data may require + * post-processing by the consumer. + * For verification failures, the output is simply a UTF-8 encoded string, describing the reason + * for failure. + * @dev can directly type cast the failed output as a string + */ + function verifyAndAttestOnChain(bytes calldata input) + external + returns (bool success, bytes memory output); + + /** + * @param journal - The output of the Guest program, this includes: + * - VerifiedOutput struct + * - TcbInfo hash + * - QEID hash + * - RootCA hash + * - TCB Signing CA hash + * - Root CRL hash + * - Platform CRL hash + * - Processor CRL hash + * @param seal - The encoded cryptographic proof (i.e. SNARK). + */ + function verifyAndAttestWithZKProof( + bytes calldata journal, + bytes calldata seal + ) + external + returns (bool success, bytes memory output); +} diff --git a/packages/protocol/contracts/verifiers/ProverRegistry.sol b/packages/protocol/contracts/verifiers/ProverRegistry.sol new file mode 100644 index 0000000000..5bb0aeda7e --- /dev/null +++ b/packages/protocol/contracts/verifiers/ProverRegistry.sol @@ -0,0 +1,188 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.24; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "../automata-attestation/lib/QuoteV3Auth/V3Struct.sol"; +import "../common/EssentialContract.sol"; +import "../common/LibStrings.sol"; +import "../L1/ITaikoL1.sol"; +import "./libs/LibPublicInput.sol"; +import "./AttestationVerifier.sol"; +import "./IVerifier.sol"; + +contract ProverRegistry is EssentialContract, IVerifier { + struct ProverInstance { + address addr; + uint256 validUntil; + uint256 teeType; // 1: IntelTDX + } + + struct ReportData { + address addr; + uint256 teeType; + uint256 referenceBlockNumber; + bytes32 referenceBlockHash; + bytes32 binHash; + } + + error INVALID_BLOCK_NUMBER(); + error BLOCK_NUMBER_OUT_OF_DATE(); + error BLOCK_NUMBER_MISMATCH(); + error REPORT_USED(); + error PROVER_TYPE_MISMATCH(); + error PROVER_INVALID_INSTANCE_ID(uint256); + error PROVER_INVALID_ADDR(address); + error PROVER_ADDR_MISMATCH(address, address); + error PROVER_OUT_OF_DATE(uint256); + + event InstanceAdded( + uint256 indexed id, address indexed instance, address replaced, uint256 validUntil + ); + + AttestationVerifier public verifier; + uint256 public attestValiditySeconds; + uint256 public maxBlockNumberDiff; + uint256 public chainID; + uint256 public nextInstanceId = 0; + + mapping(bytes32 reportHash => bool used) public attestedReports; + mapping(uint256 proverInstanceID => ProverInstance instance) public attestedProvers; + + uint256[43] private __gap; + + constructor() { + _disableInitializers(); + } + + function initialize( + address _initialOwner, + address _verifierAddr, + address _rollupAddressManager, + uint256 _chainID, + uint256 _attestValiditySeconds, + uint256 _maxBlockNumberDiff + ) + public + initializer + { + verifier = AttestationVerifier(_verifierAddr); + chainID = _chainID; + attestValiditySeconds = _attestValiditySeconds; + maxBlockNumberDiff = _maxBlockNumberDiff; + __Essential_init(_initialOwner, _rollupAddressManager); + } + + function reinitialize( + uint8 i, + address _initialOwner, + address _verifierAddr, + uint256 _chainID, + uint256 _attestValiditySeconds, + uint256 _maxBlockNumberDiff + ) + public + reinitializer(i) + { + verifier = AttestationVerifier(_verifierAddr); + chainID = _chainID; + attestValiditySeconds = _attestValiditySeconds; + maxBlockNumberDiff = _maxBlockNumberDiff; + _transferOwnership(_initialOwner); + } + + /// @notice register prover instance with quote + function register(bytes calldata _report, ReportData calldata _data) external { + _checkBlockNumber(_data.referenceBlockNumber, _data.referenceBlockHash); + bytes32 dataHash = keccak256(abi.encode(_data)); + + verifier.verifyAttestation(_report, dataHash); + + bytes32 reportHash = keccak256(_report); + if (attestedReports[reportHash]) revert REPORT_USED(); + attestedReports[reportHash] = true; + + uint256 instanceID = nextInstanceId + 1; + nextInstanceId += 1; + + uint256 validUnitl = block.timestamp + attestValiditySeconds; + attestedProvers[instanceID] = ProverInstance(_data.addr, validUnitl, _data.teeType); + + emit InstanceAdded(instanceID, _data.addr, address(0), validUnitl); + } + + function checkProver( + uint256 _instanceID, + address _proverAddr + ) + public + view + returns (ProverInstance memory) + { + ProverInstance memory prover; + if (_instanceID == 0) revert PROVER_INVALID_INSTANCE_ID(_instanceID); + if (_proverAddr == address(0)) revert PROVER_INVALID_ADDR(_proverAddr); + prover = attestedProvers[_instanceID]; + if (prover.addr != _proverAddr) revert PROVER_ADDR_MISMATCH(prover.addr, _proverAddr); + if (prover.validUntil < block.timestamp) revert PROVER_OUT_OF_DATE(prover.validUntil); + return prover; + } + + // Due to the inherent unpredictability of blockHash, it mitigates the risk of mass-generation + // of attestation reports in a short time frame, preventing their delayed and gradual + // exploitation. + // This function will make sure the attestation report generated in recent ${maxBlockNumberDiff} + // blocks + function _checkBlockNumber(uint256 blockNumber, bytes32 blockHash) private view { + if (blockNumber >= block.number) revert INVALID_BLOCK_NUMBER(); + if (block.number - blockNumber >= maxBlockNumberDiff) { + revert BLOCK_NUMBER_OUT_OF_DATE(); + } + if (blockhash(blockNumber) != blockHash) revert BLOCK_NUMBER_MISMATCH(); + } + + function verifyProof( + Context calldata _ctx, + TaikoData.Transition calldata _tran, + TaikoData.TierProof calldata _proof + ) + external + { + // Do not run proof verification to contest an existing proof + if (_ctx.isContesting) return; + + // Size is: 89 bytes + // 4 bytes + 20 bytes + 65 bytes (signature) = 89 + // TODO: do we need this check? + // if (_proof.data.length != 89) revert SGX_INVALID_PROOF(); + + uint32 id = uint32(bytes4(_proof.data[:4])); + address newInstance = address(bytes20(_proof.data[4:24])); + + address oldInstance = ECDSA.recover( + LibPublicInput.hashPublicInputs( + _tran, address(this), newInstance, _ctx.prover, _ctx.metaHash, taikoChainId() + ), + _proof.data[24:] + ); + + ProverInstance memory prover = checkProver(id, oldInstance); + if (_proof.tier != prover.teeType) revert PROVER_TYPE_MISMATCH(); + if (oldInstance != newInstance) { + attestedProvers[id].addr = newInstance; + emit InstanceAdded(id, oldInstance, newInstance, prover.validUntil); + } + } + + /// @inheritdoc IVerifier + function verifyBatchProof( + ContextV2[] calldata, /*_ctxs*/ + TaikoData.TierProof calldata /*_proof*/ + ) + external + view + { } + + function taikoChainId() internal view virtual returns (uint64) { + return ITaikoL1(resolve(LibStrings.B_TAIKO, false)).getConfig().chainId; + } +}