// SPDX-License-Identifier: CC0-1.0 pragma solidity ^0.8.28; import {IERC8262Verifier} from "./interfaces/IERC8262Verifier.sol"; import {IUltraVerifier} from "./interfaces/IUltraVerifier.sol"; import {IERC165} from "./interfaces/IERC165.sol"; import {ProofTypes} from "./libraries/ProofTypes.sol"; import {AccessControl} from "./libraries/AccessControl.sol"; import {Pausable} from "./libraries/Pausable.sol"; /// @title ERC8262Verifier -- Reference implementation of the ERC-8262 verifier /// @notice Routes proof verification to per-proof-type UltraHonk verifier contracts /// @dev Each proof type maps to a separate Noir circuit with its own generated verifier. /// Verifier contracts are generated by Barretenberg (`bb contract`) from compiled circuits. /// /// Privileged actions are split across roles (see AccessControl): /// - GUARDIAN: pause/unpause (global + per-proof-type), immediate version revocation /// - CONFIG: propose/execute verifier upgrades, schedule version revocations /// - owner: setVerifierInitial (deploy-time only), grant/revoke roles contract ERC8262Verifier is IERC8262Verifier, IERC165, AccessControl, Pausable { /// @notice Mapping from proof type ID to its UltraHonk verifier contract mapping(uint8 proofType => address verifier) internal _verifiers; /// @notice Append-only history of verifier addresses per proof type mapping(uint8 proofType => address[] history) internal _verifierHistory; /// @notice Pending verifier proposals per proof type mapping(uint8 proofType => VerifierProposal proposal) internal _pendingVerifiers; /// @notice Revoked verifier versions (cannot be used via verifyProofAtVersion) mapping(uint8 proofType => mapping(uint256 version => bool revoked)) internal _revokedVersions; /// @notice Per-proof-type pause state for surgical incident response mapping(uint8 proofType => bool isPaused) internal _proofTypePaused; /// @notice Pending revocation proposals: proofType => version => proposedAt timestamp. /// @dev 0 indicates no pending proposal. See proposeVersionRevocation for the rationale. mapping(uint8 proofType => mapping(uint256 version => uint256 proposedAt)) internal _pendingRevocations; struct VerifierProposal { address newVerifier; uint256 proposedAt; bytes32 expectedCodehash; } error VerifierNotSet(uint8 proofType); error VerifierAlreadySet(uint8 proofType); error InvalidVersion(uint8 proofType, uint256 version); error TimelockNotElapsed(uint8 proofType, uint256 readyAt); error NoPendingProposal(uint8 proofType); error ProposalAlreadyPending(uint8 proofType); error VersionRevoked(uint8 proofType, uint256 version); error CannotRevokeCurrentVersion(uint8 proofType); error AlreadyRevoked(uint8 proofType, uint256 version); error NotAContract(address addr); error ProofTypePaused(uint8 proofType); error ProofTypeNotPaused(uint8 proofType); error BatchLengthMismatch(); error EmptyBatch(); error BatchTooLarge(); error CodehashMismatch(address verifier, bytes32 expected, bytes32 actual); /// @notice Raised when a caller of `cancelVerifierProposal` / `cancelVersionRevocation` /// holds neither CONFIG_ROLE nor GUARDIAN_ROLE. Distinct from `NotRole` so /// decoders surface the "either role accepted" semantics. error NotCancelAuthorized(address account); /// @notice Maximum number of proofs in a single batch verification. /// @dev Calibrated against the per-proof gas baseline in `.gas-snapshot` /// (~2.4M for verifyProof). 10 × 2.4M = 24M, leaving ~6M headroom under /// the 30M mainnet block gas target. Audit F-3. uint256 public constant MAX_BATCH_SIZE = 10; /// @notice Delay before a proposed verifier can be executed uint256 public constant VERIFIER_TIMELOCK = 24 hours; /// @notice Delay before a proposed version revocation can be executed. /// @dev Shorter than VERIFIER_TIMELOCK because revocation is housekeeping that /// follows pauseProofType (the actual emergency response). The window protects /// against a compromised owner mass-revoking historical versions. uint256 public constant REVOCATION_TIMELOCK = 6 hours; event VerifierUpdated(uint8 indexed proofType, address indexed oldVerifier, address indexed newVerifier); event VerifierProposed( uint8 indexed proofType, address indexed newVerifier, bytes32 expectedCodehash, uint256 readyAt ); event VerifierProposalCancelled(uint8 indexed proofType, address indexed cancelledVerifier); event VerifierVersionRevoked(uint8 indexed proofType, uint256 indexed version, address indexed verifier); event VersionRevocationProposed(uint8 indexed proofType, uint256 indexed version, uint256 readyAt); event VersionRevocationCancelled(uint8 indexed proofType, uint256 indexed version); event ProofTypePausedEvent(uint8 indexed proofType, address indexed account); event ProofTypeUnpausedEvent(uint8 indexed proofType, address indexed account); constructor(address initialOwner) { if (initialOwner == address(0)) revert ZeroAddress(); owner = initialOwner; emit OwnershipTransferred(address(0), initialOwner); } // ------------------------------------------------------------------------- // IERC8262Verifier // ------------------------------------------------------------------------- /// @inheritdoc IERC8262Verifier function verifyProof(uint8 proofType, bytes calldata proof, bytes calldata publicInputs) external view whenNotPaused returns (bool valid) { return _verify(proofType, proof, publicInputs); } /// @inheritdoc IERC8262Verifier /// @dev This is a CRYPTOGRAPHIC primitive: it verifies UltraHonk proofs but performs /// no replay protection, jurisdiction enforcement, or context binding. Downstream /// protocols MUST use `ERC8262Oracle.submitCompliance` / /// `submitComplianceBatch` for any path that needs replay-keyed proofs, /// timestamp ratcheting, signed-signals policy, or attestation issuance. function verifyProofBatch(uint8[] calldata proofTypes, bytes[] calldata proofs, bytes[] calldata publicInputs) external view whenNotPaused returns (bool valid) { uint256 length = proofTypes.length; if (length == 0) revert EmptyBatch(); if (length > MAX_BATCH_SIZE) revert BatchTooLarge(); if (length != proofs.length || length != publicInputs.length) revert BatchLengthMismatch(); for (uint256 i; i < length;) { if (!_verify(proofTypes[i], proofs[i], publicInputs[i])) { return false; } unchecked { ++i; } } return true; } /// @inheritdoc IERC8262Verifier function getVerifier(uint8 proofType) external view returns (address verifier) { return _verifiers[proofType]; } /// @inheritdoc IERC8262Verifier function verifyProofAtVersion(uint8 proofType, uint256 version, bytes calldata proof, bytes calldata publicInputs) external view whenNotPaused returns (bool valid) { if (_proofTypePaused[proofType]) revert ProofTypePaused(proofType); if (_revokedVersions[proofType][version]) revert VersionRevoked(proofType, version); address verifier = _getVerifierAtVersion(proofType, version); ProofTypes.validatePublicInputs(proofType, publicInputs); bytes32[] memory inputs = ProofTypes.decodePublicInputs(publicInputs); return IUltraVerifier(verifier).verify(proof, inputs); } /// @inheritdoc IERC8262Verifier function getVerifierAtVersion(uint8 proofType, uint256 version) external view returns (address verifier) { return _getVerifierAtVersion(proofType, version); } /// @inheritdoc IERC8262Verifier function getVerifierVersion(uint8 proofType) external view returns (uint256 version) { return _verifierHistory[proofType].length; } // ------------------------------------------------------------------------- // Admin // ------------------------------------------------------------------------- /// @notice Set verifier for initial deployment only (no timelock) /// @dev Reverts if a verifier is already set for this proof type. /// Use proposeVerifier + executeVerifierUpdate for subsequent changes. /// @param proofType The proof type (0x01-0x08) /// @param verifier The UltraHonk verifier contract address function setVerifierInitial(uint8 proofType, address verifier) external onlyOwner { if (!ProofTypes.isValidProofType(proofType)) revert ProofTypes.InvalidProofType(proofType); if (verifier == address(0)) revert ZeroAddress(); if (verifier.code.length == 0) revert NotAContract(verifier); if (_verifiers[proofType] != address(0)) revert VerifierAlreadySet(proofType); _verifiers[proofType] = verifier; _verifierHistory[proofType].push(verifier); emit VerifierUpdated(proofType, address(0), verifier); } /// @notice Propose a new verifier for a proof type (starts timelock) /// @dev `expectedCodehash` is pinned at proposal time and re-checked at execute time. /// This blocks a compromised CONFIG_ROLE from swapping the bytecode at the proposed /// address during the 24h window (e.g. CREATE2 redeploy after SELFDESTRUCT in the /// same factory tx). Caller must obtain the codehash off-chain (`extcodehash`). /// @param proofType The proof type (0x01-0x08) /// @param newVerifier The proposed UltraHonk verifier contract address /// @param expectedCodehash The EXTCODEHASH the proposer commits to; must equal newVerifier's current codehash function proposeVerifier(uint8 proofType, address newVerifier, bytes32 expectedCodehash) external onlyRole(CONFIG_ROLE) { if (!ProofTypes.isValidProofType(proofType)) revert ProofTypes.InvalidProofType(proofType); if (newVerifier == address(0)) revert ZeroAddress(); if (newVerifier.code.length == 0) revert NotAContract(newVerifier); bytes32 actual = newVerifier.codehash; if (actual != expectedCodehash) revert CodehashMismatch(newVerifier, expectedCodehash, actual); if (_pendingVerifiers[proofType].proposedAt != 0) revert ProposalAlreadyPending(proofType); _pendingVerifiers[proofType] = VerifierProposal({ newVerifier: newVerifier, proposedAt: block.timestamp, expectedCodehash: expectedCodehash }); uint256 readyAt = block.timestamp + VERIFIER_TIMELOCK; emit VerifierProposed(proofType, newVerifier, expectedCodehash, readyAt); } /// @notice Execute a pending verifier update after the timelock has elapsed /// @dev Re-checks the codehash pinned at proposal time. A mid-window redeploy /// (CREATE2 same-address with different bytecode) reverts here even if the /// proposed address is unchanged. /// @param proofType The proof type (0x01-0x08) function executeVerifierUpdate(uint8 proofType) external onlyRole(CONFIG_ROLE) { VerifierProposal memory proposal = _pendingVerifiers[proofType]; if (proposal.proposedAt == 0) revert NoPendingProposal(proofType); uint256 readyAt = proposal.proposedAt + VERIFIER_TIMELOCK; if (block.timestamp < readyAt) revert TimelockNotElapsed(proofType, readyAt); bytes32 actual = proposal.newVerifier.codehash; if (actual != proposal.expectedCodehash) { revert CodehashMismatch(proposal.newVerifier, proposal.expectedCodehash, actual); } address old = _verifiers[proofType]; _verifiers[proofType] = proposal.newVerifier; _verifierHistory[proofType].push(proposal.newVerifier); delete _pendingVerifiers[proofType]; emit VerifierUpdated(proofType, old, proposal.newVerifier); } /// @notice Cancel a pending verifier proposal /// @dev GUARDIAN may cancel for emergency revert; CONFIG cancels through normal path. /// @param proofType The proof type (0x01-0x08) function cancelVerifierProposal(uint8 proofType) external { if (!_hasRole(CONFIG_ROLE, msg.sender) && !_hasRole(GUARDIAN_ROLE, msg.sender)) { revert NotCancelAuthorized(msg.sender); } VerifierProposal memory proposal = _pendingVerifiers[proofType]; if (proposal.proposedAt == 0) revert NoPendingProposal(proofType); delete _pendingVerifiers[proofType]; emit VerifierProposalCancelled(proofType, proposal.newVerifier); } /// @notice Get the pending verifier proposal for a proof type /// @param proofType The proof type /// @return newVerifier The proposed verifier address (address(0) if none) /// @return readyAt The timestamp when the proposal can be executed (0 if none) /// @return expectedCodehash The codehash pinned at proposal time (bytes32(0) if none) function getPendingVerifier(uint8 proofType) external view returns (address newVerifier, uint256 readyAt, bytes32 expectedCodehash) { VerifierProposal memory proposal = _pendingVerifiers[proofType]; if (proposal.proposedAt == 0) return (address(0), 0, bytes32(0)); return (proposal.newVerifier, proposal.proposedAt + VERIFIER_TIMELOCK, proposal.expectedCodehash); } /// @notice Revoke a historical verifier version IMMEDIATELY, with no delay. /// @dev Emergency-only path. The recommended path for routine revocation is /// proposeVersionRevocation + executeVersionRevocation (6h delay), which /// protects against owner-key compromise. Use this immediate path only /// when you have already paused the proof type via pauseProofType and /// need to lock in the revocation without waiting. Cannot revoke the /// current (latest) version; use proposeVerifier instead. /// @param proofType The proof type (0x01-0x08) /// @param version The version to revoke (1-indexed) function revokeVerifierVersion(uint8 proofType, uint256 version) external onlyRole(GUARDIAN_ROLE) { _doRevokeVersion(proofType, version); } /// @notice Schedule a version revocation. Takes effect after REVOCATION_TIMELOCK. /// @dev Routine path: protects against owner-compromise mass-revocation. Multiple /// versions of the same proof type may be in flight simultaneously, but each /// (proofType, version) pair allows only one pending proposal at a time. /// @param proofType The proof type (0x01-0x08) /// @param version The version to revoke (1-indexed) function proposeVersionRevocation(uint8 proofType, uint256 version) external onlyRole(CONFIG_ROLE) { address[] storage history = _verifierHistory[proofType]; if (version == 0 || version > history.length) revert InvalidVersion(proofType, version); if (version == history.length) revert CannotRevokeCurrentVersion(proofType); if (_revokedVersions[proofType][version]) revert AlreadyRevoked(proofType, version); if (_pendingRevocations[proofType][version] != 0) revert ProposalAlreadyPending(proofType); _pendingRevocations[proofType][version] = block.timestamp; uint256 readyAt = block.timestamp + REVOCATION_TIMELOCK; emit VersionRevocationProposed(proofType, version, readyAt); } /// @notice Execute a previously-scheduled revocation after the delay has elapsed. /// @dev Re-checks eligibility at execution time -- the version must still exist, /// still not be the current version, and still not be revoked. function executeVersionRevocation(uint8 proofType, uint256 version) external onlyRole(CONFIG_ROLE) { uint256 proposedAt = _pendingRevocations[proofType][version]; if (proposedAt == 0) revert NoPendingProposal(proofType); uint256 readyAt = proposedAt + REVOCATION_TIMELOCK; if (block.timestamp < readyAt) revert TimelockNotElapsed(proofType, readyAt); delete _pendingRevocations[proofType][version]; _doRevokeVersion(proofType, version); } /// @notice Cancel a pending revocation proposal. /// @param proofType The proof type /// @param version The version whose revocation should be cancelled function cancelVersionRevocation(uint8 proofType, uint256 version) external { if (!_hasRole(CONFIG_ROLE, msg.sender) && !_hasRole(GUARDIAN_ROLE, msg.sender)) { revert NotCancelAuthorized(msg.sender); } if (_pendingRevocations[proofType][version] == 0) revert NoPendingProposal(proofType); delete _pendingRevocations[proofType][version]; emit VersionRevocationCancelled(proofType, version); } /// @notice Get the pending revocation status for a (proofType, version). /// @return readyAt Timestamp when the proposal becomes executable. 0 if no pending proposal. function getPendingRevocation(uint8 proofType, uint256 version) external view returns (uint256 readyAt) { uint256 proposedAt = _pendingRevocations[proofType][version]; if (proposedAt == 0) return 0; return proposedAt + REVOCATION_TIMELOCK; } /// @dev Shared revocation logic used by the immediate and timelocked paths. function _doRevokeVersion(uint8 proofType, uint256 version) internal { address[] storage history = _verifierHistory[proofType]; if (version == 0 || version > history.length) revert InvalidVersion(proofType, version); if (version == history.length) revert CannotRevokeCurrentVersion(proofType); if (_revokedVersions[proofType][version]) revert AlreadyRevoked(proofType, version); _revokedVersions[proofType][version] = true; emit VerifierVersionRevoked(proofType, version, history[version - 1]); } /// @notice Check if a verifier version has been revoked /// @param proofType The proof type (0x01-0x08) /// @param version The version to check (1-indexed) /// @return revoked Whether the version has been revoked function isVersionRevoked(uint8 proofType, uint256 version) external view returns (bool revoked) { return _revokedVersions[proofType][version]; } /// @notice Pause the contract, blocking all proof verification function pause() external override onlyRole(GUARDIAN_ROLE) { if (paused) revert ContractPaused(); paused = true; emit Paused(msg.sender); } /// @notice Unpause the contract, resuming all proof verification function unpause() external override onlyRole(GUARDIAN_ROLE) { if (!paused) revert ContractNotPaused(); paused = false; emit Unpaused(msg.sender); } /// @notice Pause a single proof type (surgical response) /// @param proofType The proof type to pause (0x01-0x08) function pauseProofType(uint8 proofType) external onlyRole(GUARDIAN_ROLE) { if (!ProofTypes.isValidProofType(proofType)) revert ProofTypes.InvalidProofType(proofType); if (_proofTypePaused[proofType]) revert ProofTypePaused(proofType); _proofTypePaused[proofType] = true; emit ProofTypePausedEvent(proofType, msg.sender); } /// @notice Unpause a single proof type /// @param proofType The proof type to unpause (0x01-0x08) function unpauseProofType(uint8 proofType) external onlyRole(GUARDIAN_ROLE) { if (!ProofTypes.isValidProofType(proofType)) revert ProofTypes.InvalidProofType(proofType); if (!_proofTypePaused[proofType]) revert ProofTypeNotPaused(proofType); _proofTypePaused[proofType] = false; emit ProofTypeUnpausedEvent(proofType, msg.sender); } /// @notice Check if a specific proof type is paused /// @param proofType The proof type to check /// @return Whether the proof type is paused function isProofTypePaused(uint8 proofType) external view returns (bool) { return _proofTypePaused[proofType]; } // ------------------------------------------------------------------------- // Internal // ------------------------------------------------------------------------- /// @dev Verify a single proof by routing to the appropriate UltraHonk verifier function _verify(uint8 proofType, bytes calldata proof, bytes calldata publicInputs) internal view returns (bool valid) { if (!ProofTypes.isValidProofType(proofType)) revert ProofTypes.InvalidProofType(proofType); if (_proofTypePaused[proofType]) revert ProofTypePaused(proofType); address verifier = _verifiers[proofType]; if (verifier == address(0)) revert VerifierNotSet(proofType); ProofTypes.validatePublicInputs(proofType, publicInputs); bytes32[] memory inputs = ProofTypes.decodePublicInputs(publicInputs); return IUltraVerifier(verifier).verify(proof, inputs); } /// @dev Resolve a verifier address from the version history (1-indexed) function _getVerifierAtVersion(uint8 proofType, uint256 version) internal view returns (address verifier) { address[] storage history = _verifierHistory[proofType]; if (version == 0 || version > history.length) { revert InvalidVersion(proofType, version); } return history[version - 1]; } // ------------------------------------------------------------------------- // IERC165 // ------------------------------------------------------------------------- /// @inheritdoc IERC165 function supportsInterface(bytes4 interfaceId) external pure returns (bool) { return interfaceId == type(IERC8262Verifier).interfaceId || interfaceId == type(IERC165).interfaceId; } }