aere-contracts/contracts/agentic/AereInferNet.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

254 lines
11 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AereInferNet — EU AI Act Article 12 inference-log commitments
* @notice EU AI Act Article 12 (in force from August 2 2026) requires
* providers and deployers of HIGH-RISK AI systems to maintain
* automatic event logs covering the system's operation. The
* logs must allow ex-post traceability and incident analysis
* by the relevant national competent authority.
*
* Today, "logs" lives in vendor S3 buckets. A subpoena later
* can show whatever is convenient. AereInferNet turns the
* commitment into a public-good primitive:
*
* 1. Each inference provider registers their model + the TEE
* attestation that bounds it (e.g. Intel SGX / AMD SEV-SNP
* measurement). The registration is immutable per (provider,
* modelId) — the model commitment cannot be retroactively
* "amended" without a new modelId.
*
* 2. Each epoch (~24h), the provider commits a Merkle root of
* (requestHash, responseHash, timestampMs, deployerId)
* leaves for every inference served in that epoch. The
* commitment is one transaction per provider per epoch —
* cheap enough that even a multi-million-request/day
* provider pays cents in gas.
*
* 3. Anyone (regulator, customer, journalist, the deployer
* themselves) can later prove inclusion of a specific
* inference by submitting a Merkle proof against the
* committed root. If the provider denies serving a request
* and the proof shows it WAS served, the public record
* speaks.
*
* 4. Slashing: providers MAY post bond via AereAgentBond,
* keyed on agentId = keccak256(modelId, providerAddress).
* Regulators / customers can dispute → trigger the slash
* path. AereInferNet does NOT enforce this layer — it's
* composable. Consumers that need stronger guarantees can
* require `bond.isBonded(provider, agentId, minBond)`
* before accepting the provider's commitments.
*
* DEPLOYER COMMITMENT:
* Article 12 obligates BOTH providers and deployers. A
* deployer who wires an inference into their own application
* records the same leaves on their side and can publish their
* own Merkle root — making provider/deployer collusion
* detectable (the two roots' leaves must reconcile).
*
* NO PII:
* Both `requestHash` and `responseHash` are content hashes
* computed off-chain — the chain stores only commitments,
* never raw prompts or responses. Aligned with GDPR Art 5(1)(c).
*
* IMMUTABILITY:
* - Model registry is append-only; per (provider, modelId)
* we set once and never overwrite. Bug-fix releases bump
* modelId.
* - Commitments append to per-provider epoch history. A
* commitment for an epoch already committed reverts.
*/
contract AereInferNet {
/* ================================ types ================================ */
/// @notice Severity tier the model is offered for per EU AI Act Annex III.
/// Used by indexers to filter; not enforced on-chain.
enum RiskTier {
Minimal, // 0
Limited, // 1
High, // 2 — Article 12 logging strictly applies
Unacceptable // 3 — should never register; left for completeness
}
struct ModelRegistration {
bytes32 modelDigest; // SHA256 of the model weights blob
bytes32 attestationDigest; // hash of the TEE attestation that pins the
// running binary to `modelDigest`
RiskTier risk;
string modelName; // human-readable, e.g. "claude-4.7-sonnet-20251022"
string attestationUri; // ipfs:// or https:// — full TEE quote + signing chain
uint64 registeredAt;
bool exists;
}
struct EpochCommitment {
bytes32 root; // Merkle root of (req, resp, ts, deployer) leaves
uint64 committedAt;
uint64 inferenceCount; // number of leaves under this root (informational)
string metadataUri; // optional pointer to provider-published leaves
}
/* ================================ storage ================================ */
/// @notice (provider, modelId) → ModelRegistration. modelId is provider-chosen.
mapping(address => mapping(bytes32 => ModelRegistration)) internal _models;
/// @notice (provider, modelId, epochId) → EpochCommitment.
/// AUDIT FIX (HIGH #25): previously the key was (provider, epochId) which
/// meant multi-model providers could only commit one root per epoch across
/// all models, and verifyInference had no way to bind a proof to a specific
/// model. Now keyed by all three.
mapping(address => mapping(bytes32 => mapping(uint64 => EpochCommitment))) internal _commits;
/// @notice (provider, modelId) → count of registered epochs (informational).
mapping(address => mapping(bytes32 => uint256)) public modelEpochCount;
/* ================================= events ================================= */
event ModelRegistered(
address indexed provider,
bytes32 indexed modelId,
bytes32 modelDigest,
bytes32 attestationDigest,
RiskTier risk,
string modelName,
string attestationUri
);
event EpochCommitted(
address indexed provider,
bytes32 indexed modelId,
uint64 indexed epochId,
bytes32 root,
uint64 inferenceCount,
string metadataUri
);
/* ================================= errors ================================= */
error ModelExists();
error ModelUnknown();
error EpochExists();
error EpochOutOfRange();
error ZeroDigest();
error EmptyName();
/* ============================== model registry ============================ */
/// @notice Provider registers a new model. msg.sender is the provider.
/// Reverts if the (provider, modelId) is already set; cannot be
/// amended — register a new modelId for the next version.
function registerModel(
bytes32 modelId,
bytes32 modelDigest,
bytes32 attestationDigest,
RiskTier risk,
string calldata modelName,
string calldata attestationUri
) external {
if (modelDigest == bytes32(0) || attestationDigest == bytes32(0)) revert ZeroDigest();
if (bytes(modelName).length == 0) revert EmptyName();
ModelRegistration storage m = _models[msg.sender][modelId];
if (m.exists) revert ModelExists();
m.modelDigest = modelDigest;
m.attestationDigest = attestationDigest;
m.risk = risk;
m.modelName = modelName;
m.attestationUri = attestationUri;
m.registeredAt = uint64(block.timestamp);
m.exists = true;
emit ModelRegistered(msg.sender, modelId, modelDigest, attestationDigest, risk, modelName, attestationUri);
}
/* ============================ epoch commitments ========================== */
/// @notice Provider commits the Merkle root for an epoch. The model
/// MUST already be registered (reverts otherwise — prevents
/// committing inferences for a phantom model).
/// epochId is provider-chosen but convention is unix-day:
/// epochId = uint64(block.timestamp / 86400)
function commitEpoch(
bytes32 modelId,
uint64 epochId,
bytes32 root,
uint64 inferenceCount,
string calldata metadataUri
) external {
if (root == bytes32(0)) revert ZeroDigest();
ModelRegistration storage m = _models[msg.sender][modelId];
if (!m.exists) revert ModelUnknown();
// AUDIT FIX (HIGH #26): epochId must be the current unix-day (with 1-day
// grace for the just-ended day). Prevents back-dating to rewrite history
// and forward-dating to commit to future inferences.
uint64 currentDay = uint64(block.timestamp / 86400);
if (epochId > currentDay) revert EpochOutOfRange();
if (currentDay > epochId && currentDay - epochId > 1) revert EpochOutOfRange();
EpochCommitment storage e = _commits[msg.sender][modelId][epochId];
if (e.committedAt != 0) revert EpochExists();
e.root = root;
e.committedAt = uint64(block.timestamp);
e.inferenceCount = inferenceCount;
e.metadataUri = metadataUri;
modelEpochCount[msg.sender][modelId] += 1;
emit EpochCommitted(msg.sender, modelId, epochId, root, inferenceCount, metadataUri);
}
/* ================================= views ================================= */
function modelOf(address provider, bytes32 modelId) external view returns (
bytes32 modelDigest,
bytes32 attestationDigest,
RiskTier risk,
string memory modelName,
string memory attestationUri,
uint64 registeredAt,
bool exists
) {
ModelRegistration storage m = _models[provider][modelId];
return (m.modelDigest, m.attestationDigest, m.risk, m.modelName, m.attestationUri, m.registeredAt, m.exists);
}
function epochOf(address provider, bytes32 modelId, uint64 epochId) external view returns (
bytes32 root, uint64 committedAt, uint64 inferenceCount, string memory metadataUri
) {
EpochCommitment storage e = _commits[provider][modelId][epochId];
return (e.root, e.committedAt, e.inferenceCount, e.metadataUri);
}
/// @notice Verify that a specific (requestHash, responseHash, timestampMs,
/// deployerId) tuple is committed under the provider's root for
/// (modelId, epochId). Returns false if the proof is invalid or the
/// (modelId, epoch) was never committed.
/// AUDIT FIX (HIGH #25): modelId is now part of the lookup key.
function verifyInference(
address provider,
bytes32 modelId,
uint64 epochId,
bytes32 requestHash,
bytes32 responseHash,
uint64 timestampMs,
bytes32 deployerId,
bytes32[] calldata proof
) external view returns (bool) {
EpochCommitment storage e = _commits[provider][modelId][epochId];
if (e.committedAt == 0) return false;
bytes32 leaf = keccak256(abi.encode(requestHash, responseHash, timestampMs, deployerId));
return _verifyProof(proof, e.root, leaf);
}
/* =============================== internal =============================== */
/// @dev OpenZeppelin-style Merkle proof verification (sorted-pairs hashing).
function _verifyProof(bytes32[] calldata proof, bytes32 root, bytes32 leaf) internal pure returns (bool) {
bytes32 h = leaf;
for (uint256 i = 0; i < proof.length; i++) {
bytes32 p = proof[i];
h = h <= p ? keccak256(abi.encodePacked(h, p)) : keccak256(abi.encodePacked(p, h));
}
return h == root;
}
}