// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/access/Ownable.sol"; /// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS /// NOTHING and REVERTS on an invalid proof. interface ISp1Verifier { function verifyProof( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view; } /** * @title AereZKMLVerifier — on-chain anchor for zero-knowledge ML inference * @notice Verifies SP1 zkVM proofs that a registered machine-learning model * classified a PRIVATE input, WITHOUT revealing the input. The reference * model is a trained, quantized MNIST handwritten-digit classifier * (784 -> 256 -> 10 MLP) whose forward pass runs inside the zkVM on a * private 28x28 image; only the predicted digit is made public. * * Each model is registered by the Foundation with: * - programVKey : SP1 verification key binding the exact guest ELF * (which embeds the quantized weights). * - modelHash : keccak256 of the quantized weight blob, recomputed * and committed INSIDE the zkVM, so a verifier knows * the classification came from THIS exact model. * - numClasses : output cardinality (10 for MNIST digits). * - accuracyBps : the model's measured test-set accuracy in basis * points (e.g. 9798 = 97.98% on the 10,000-image * MNIST test set) recorded for transparency. * * PUBLIC-VALUES CONVENTION (exactly 128 bytes): * abi.encode(uint256 chainId, address user, bytes32 modelHash, uint256 predictedClass) * * submitInference verifies the proof through the SP1 gateway, then binds * chainId == this chain, user == msg.sender, and modelHash == the * registered model, and records the user's latest verified prediction. * * IMMUTABILITY: SP1_VERIFIER is fixed at deploy; a model's programVKey, * modelHash and numClasses are immutable after registration. */ contract AereZKMLVerifier is Ownable { address public immutable SP1_VERIFIER; struct Model { bool registered; uint16 numClasses; uint32 accuracyBps; // measured test-set accuracy, basis points bytes32 programVKey; bytes32 modelHash; // keccak256 of the quantized weight blob string description; } struct Inference { bool exists; uint16 predictedClass; uint64 at; // block timestamp of the verified inference } /// @notice programVKey => Model mapping(bytes32 => Model) public models; /// @notice user => programVKey => latest verified inference mapping(address => mapping(bytes32 => Inference)) public inferences; /// @notice programVKey => count of verified inferences mapping(bytes32 => uint256) public inferenceCount; /* --------------------------------- events ------------------------------- */ event ModelRegistered( bytes32 indexed programVKey, bytes32 indexed modelHash, uint16 numClasses, uint32 accuracyBps, string description ); event InferenceVerified( address indexed user, bytes32 indexed programVKey, bytes32 indexed modelHash, uint256 predictedClass, uint256 at ); /* --------------------------------- errors ------------------------------- */ error UnknownModel(); error AlreadyRegistered(); error InvalidProof(); error WrongChain(uint256 inProof, uint256 onChain); error UserMismatch(address inProof, address msgSender); error ModelHashMismatch(bytes32 inProof, bytes32 registered); error ClassOutOfRange(uint256 predicted, uint16 numClasses); error BadPublicValuesLength(uint256 got); error ZeroAddress(); error ZeroModelHash(); /* ----------------------------- constructor ------------------------------ */ constructor(address sp1Verifier) { if (sp1Verifier == address(0)) revert ZeroAddress(); SP1_VERIFIER = sp1Verifier; } /* ------------------------------ governance ------------------------------ */ function registerModel( bytes32 programVKey, bytes32 modelHash, uint16 numClasses, uint32 accuracyBps, string calldata description ) external onlyOwner { if (models[programVKey].registered) revert AlreadyRegistered(); if (modelHash == bytes32(0)) revert ZeroModelHash(); models[programVKey] = Model({ registered: true, numClasses: numClasses, accuracyBps: accuracyBps, programVKey: programVKey, modelHash: modelHash, description: description }); emit ModelRegistered(programVKey, modelHash, numClasses, accuracyBps, description); } /* ------------------------------ proof submit ---------------------------- */ /// @notice Verify an SP1 proof that the model `programVKey` classified the /// caller's private input, and record the predicted class. The proof /// must commit (chainId, user, modelHash, predictedClass) with /// chainId == this chain, user == msg.sender, and modelHash equal to /// the registered model's hash. function submitInference( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external returns (uint256 predictedClass) { Model memory m = models[programVKey]; if (!m.registered) revert UnknownModel(); // 4 x 32 = 128 bytes for (uint256, address, bytes32, uint256). if (publicValues.length != 128) revert BadPublicValuesLength(publicValues.length); try ISp1Verifier(SP1_VERIFIER).verifyProof(programVKey, publicValues, proof) { // verified — did not revert } catch { revert InvalidProof(); } (uint256 chainId, address user, bytes32 modelHash, uint256 predicted) = abi.decode(publicValues, (uint256, address, bytes32, uint256)); if (chainId != block.chainid) revert WrongChain(chainId, block.chainid); if (user != msg.sender) revert UserMismatch(user, msg.sender); if (modelHash != m.modelHash) revert ModelHashMismatch(modelHash, m.modelHash); if (predicted >= m.numClasses) revert ClassOutOfRange(predicted, m.numClasses); inferences[user][programVKey] = Inference({ exists: true, predictedClass: uint16(predicted), at: uint64(block.timestamp) }); inferenceCount[programVKey] += 1; emit InferenceVerified(user, programVKey, modelHash, predicted, block.timestamp); return predicted; } /* ----------------------------------- views ------------------------------ */ /// @notice Stateless verification: reverts if the proof is invalid for the /// given programVKey, otherwise returns the decoded predicted class. /// Does not check chainId/user/modelHash and records nothing. function verify( bytes32 programVKey, bytes calldata publicValues, bytes calldata proof ) external view returns (uint256 predictedClass) { if (publicValues.length != 128) revert BadPublicValuesLength(publicValues.length); ISp1Verifier(SP1_VERIFIER).verifyProof(programVKey, publicValues, proof); (, , , predictedClass) = abi.decode(publicValues, (uint256, address, bytes32, uint256)); } function latestPrediction(address user, bytes32 programVKey) external view returns (bool exists, uint16 predictedClass, uint64 at) { Inference memory inf = inferences[user][programVKey]; return (inf.exists, inf.predictedClass, inf.at); } }