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.
172 lines
6.9 KiB
Solidity
172 lines
6.9 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
|
|
|
|
/**
|
|
* @title AereAIProof — anchors signed (model, input, output) triples for
|
|
* AI inference / generation provenance
|
|
* @notice AI providers register a `modelId` with a signing key. For each
|
|
* inference / generation request they expose, the provider commits
|
|
* an EIP-712 signed statement:
|
|
* {modelId, inputHash, outputHash, timestamp, requesterHash}
|
|
* where requesterHash is keccak256(abi.encode(requester, nonce)).
|
|
*
|
|
* Use cases:
|
|
* - Agentic transactions (AERE402 caller proves the LLM output
|
|
* it acted on came from a specific signed model)
|
|
* - Tamper-evident audit logs for regulated AI deployments
|
|
* - Cross-tenant accountability without revealing raw I/O
|
|
*
|
|
* IMMUTABILITY:
|
|
* - Provider's signer key is rotatable (via setSigner) but the
|
|
* modelId itself is permanent once registered
|
|
* - Anchored attestations are append-only — never editable
|
|
*
|
|
* PRIVACY:
|
|
* - inputHash + outputHash + requesterHash are all hashes
|
|
* - The on-chain record never sees raw prompts or completions
|
|
*
|
|
* @dev EIP-712 domain pinned at deploy. Signatures are verified by
|
|
* ECDSA.recover against the provider's currently-registered signer.
|
|
*/
|
|
contract AereAIProof {
|
|
using ECDSA for bytes32;
|
|
|
|
/* --------------------------------- types --------------------------------- */
|
|
|
|
struct Model {
|
|
bool registered;
|
|
address signer;
|
|
address operator; // can rotate signer
|
|
string description; // human-readable identifier
|
|
}
|
|
|
|
struct Attestation {
|
|
bytes32 modelId;
|
|
bytes32 inputHash;
|
|
bytes32 outputHash;
|
|
bytes32 requesterHash;
|
|
uint256 timestamp;
|
|
uint256 nonce; // per-modelId monotonic
|
|
}
|
|
|
|
/* ------------------------------- immutable ------------------------------- */
|
|
|
|
bytes32 public immutable DOMAIN_SEPARATOR;
|
|
bytes32 public constant ATTESTATION_TYPEHASH = keccak256(
|
|
"Attestation(bytes32 modelId,bytes32 inputHash,bytes32 outputHash,bytes32 requesterHash,uint256 timestamp,uint256 nonce)"
|
|
);
|
|
|
|
/* --------------------------------- state -------------------------------- */
|
|
|
|
mapping(bytes32 => Model) public models;
|
|
mapping(bytes32 => mapping(uint256 => bool)) public usedNonce; // modelId → nonce → consumed
|
|
mapping(bytes32 => uint256) public attestationCount; // modelId → total anchored
|
|
/// @notice M3 fix: nonces below this watermark are inadmissible — set
|
|
/// to the highest used nonce + 1 on every signer rotation so
|
|
/// the new signer cannot re-use nonces issued under the old
|
|
/// signer's context, polluting provenance history.
|
|
mapping(bytes32 => uint256) public minValidNonce;
|
|
mapping(bytes32 => uint256) public highestNonceSeen;
|
|
|
|
/* --------------------------------- events ------------------------------- */
|
|
|
|
event ModelRegistered(bytes32 indexed modelId, address indexed operator, address indexed signer, string description);
|
|
event SignerRotated(bytes32 indexed modelId, address indexed newSigner);
|
|
event Anchored(
|
|
bytes32 indexed modelId,
|
|
bytes32 indexed inputHash,
|
|
bytes32 indexed outputHash,
|
|
bytes32 requesterHash,
|
|
uint256 timestamp,
|
|
uint256 nonce
|
|
);
|
|
|
|
/* --------------------------------- errors ------------------------------- */
|
|
|
|
error ModelExists();
|
|
error UnknownModel();
|
|
error NotOperator();
|
|
error BadSignature();
|
|
error NonceConsumed();
|
|
error NonceBelowWatermark(uint256 got, uint256 minValid);
|
|
error AttestationInFuture();
|
|
error ZeroAddress();
|
|
|
|
/* ----------------------------- constructor ------------------------------ */
|
|
|
|
constructor() {
|
|
DOMAIN_SEPARATOR = keccak256(abi.encode(
|
|
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"),
|
|
keccak256("AereAIProof"),
|
|
keccak256("1"),
|
|
block.chainid,
|
|
address(this)
|
|
));
|
|
}
|
|
|
|
/* ------------------------------ registration ---------------------------- */
|
|
|
|
function registerModel(bytes32 modelId, address signer, string calldata description) external {
|
|
if (modelId == bytes32(0)) revert ZeroAddress();
|
|
if (signer == address(0)) revert ZeroAddress();
|
|
if (models[modelId].registered) revert ModelExists();
|
|
models[modelId] = Model({
|
|
registered: true,
|
|
signer: signer,
|
|
operator: msg.sender,
|
|
description: description
|
|
});
|
|
emit ModelRegistered(modelId, msg.sender, signer, description);
|
|
}
|
|
|
|
function setSigner(bytes32 modelId, address newSigner) external {
|
|
Model storage m = models[modelId];
|
|
if (!m.registered) revert UnknownModel();
|
|
if (msg.sender != m.operator) revert NotOperator();
|
|
if (newSigner == address(0)) revert ZeroAddress();
|
|
m.signer = newSigner;
|
|
// M3 fix: invalidate any nonces below this point so new signer
|
|
// cannot reuse the old signer's nonce space.
|
|
minValidNonce[modelId] = highestNonceSeen[modelId] + 1;
|
|
emit SignerRotated(modelId, newSigner);
|
|
}
|
|
|
|
/* ------------------------------- anchor --------------------------------- */
|
|
|
|
/// @notice Submit a provider-signed attestation. Anyone can submit
|
|
/// (the provider, the requester, an indexer). The signature
|
|
/// must come from the model's CURRENT signer.
|
|
function anchor(
|
|
bytes32 modelId,
|
|
bytes32 inputHash,
|
|
bytes32 outputHash,
|
|
bytes32 requesterHash,
|
|
uint256 timestamp,
|
|
uint256 nonce,
|
|
bytes calldata signature
|
|
) external {
|
|
Model memory m = models[modelId];
|
|
if (!m.registered) revert UnknownModel();
|
|
if (timestamp > block.timestamp) revert AttestationInFuture();
|
|
if (usedNonce[modelId][nonce]) revert NonceConsumed();
|
|
// M3 fix: post-rotation, old nonce space is gone.
|
|
if (nonce < minValidNonce[modelId]) revert NonceBelowWatermark(nonce, minValidNonce[modelId]);
|
|
|
|
bytes32 structHash = keccak256(abi.encode(
|
|
ATTESTATION_TYPEHASH,
|
|
modelId, inputHash, outputHash, requesterHash, timestamp, nonce
|
|
));
|
|
bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
|
|
address recovered = digest.recover(signature);
|
|
if (recovered == address(0) || recovered != m.signer) revert BadSignature();
|
|
|
|
usedNonce[modelId][nonce] = true;
|
|
if (nonce > highestNonceSeen[modelId]) highestNonceSeen[modelId] = nonce;
|
|
attestationCount[modelId] += 1;
|
|
|
|
emit Anchored(modelId, inputHash, outputHash, requesterHash, timestamp, nonce);
|
|
}
|
|
}
|