aere-contracts/contracts/compliance/AereAIProof.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
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.
2026-07-20 01:02:37 +03:00

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);
}
}