Aere Network public source. Everything here can be checked against the live chain (chain id 2800, https://rpc.aere.network). Scope note, stated up front rather than buried: consensus on chain 2800 is classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at the signature, precompile, account and transport layers. Nothing here makes the consensus post-quantum, and no document in it should be read as claiming so.
198 lines
7.8 KiB
Solidity
198 lines
7.8 KiB
Solidity
// 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);
|
|
}
|
|
}
|