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.
469 lines
21 KiB
Solidity
469 lines
21 KiB
Solidity
// SPDX-License-Identifier: MIT
|
|
pragma solidity 0.8.23;
|
|
|
|
/**
|
|
* @title AereAgentActionReceipt — PQC-anchored, publicly verifiable AI provenance
|
|
* (AERE chain 2800)
|
|
*
|
|
* @notice An on-chain provenance record for an autonomous/AI agent's output. An
|
|
* agent commits the HASH of an output (or action) it produced, signed by a
|
|
* short-lived secp256k1 SESSION key that was itself issued under a
|
|
* quantum-durable Falcon ROOT via AereAgentDID (proof-of-possession verified
|
|
* on-chain by AERE's live native PQC precompile). The result is a permanent,
|
|
* append-only, publicly re-verifiable statement: "agent A produced an output
|
|
* whose hash is H, authorized by session S under Falcon root A."
|
|
*
|
|
* WHY THIS EXISTS. A wallet claiming an AI output is a claim, not a property.
|
|
* Off-chain "the model said X" is unverifiable and forgeable. A receipt here
|
|
* binds the output hash to a PQC-rooted identity with a signature ANYONE can
|
|
* re-check in a single read, and cross-wires it to the agent's live economic
|
|
* accountability: its AereAgentBond stake and its AereAIReputation score /
|
|
* slashing history. A verifier learns, from one contract, both "is this
|
|
* provenance genuine?" and "how much is this agent economically on the hook,
|
|
* and has it been slashed?".
|
|
*
|
|
* QUANTUM DURABILITY. The receipt's authorship chain terminates at a Falcon
|
|
* root key. The hot session signature is classical secp256k1 (cheap ecrecover),
|
|
* but a session only exists because a Falcon PoP authorized it in AereAgentDID,
|
|
* and a session (hence any new receipt under it) stops validating the instant
|
|
* its Falcon root is rotated or revoked. The commitment hash itself is content
|
|
* you choose (e.g. a SHA3/Keccak digest of the output), durable by construction.
|
|
*
|
|
* HONEST SCOPE. Application-layer provenance + authorization. This does NOT
|
|
* change AERE consensus (blocks are still Besu QBFT, classical ECDSA). Falcon
|
|
* verification lives in AereAgentDID / AerePQCKeyRegistry via the live native
|
|
* precompile, not here. This contract holds no funds and never mutates the DID,
|
|
* bond, or reputation contracts — it only reads them. Emitting is permissionless
|
|
* (any relayer may submit): the SESSION SIGNATURE is the sole authority, so the
|
|
* relayer paying gas is irrelevant to authorship.
|
|
*
|
|
* CROSS-WIRE CONVENTION. AereAgentDID keys an agent by its Falcon rootKeyId
|
|
* (a uint256). AereAgentBond / AereAIReputation key by (operator, bytes32
|
|
* agentId). This contract reads the economic layer at the canonical key
|
|
* (operator = the DID controller of the root key, agentId = bytes32(rootKeyId)).
|
|
* An operator that wants its DID-rooted agent's stake / reputation surfaced
|
|
* alongside its receipts posts its bond under agentId == bytes32(rootKeyId)
|
|
* (see {bondKeyOf}). If it does not, the cross-reads simply return zero — the
|
|
* provenance signature is unaffected and remains fully verifiable.
|
|
*/
|
|
|
|
/// @dev Read-only surface of AereAgentDID used by this contract.
|
|
interface IAereAgentDID {
|
|
function getSession(uint256 sessionId)
|
|
external
|
|
view
|
|
returns (
|
|
uint256 rootKeyId,
|
|
address sessionAddr,
|
|
bytes32 scopeHash,
|
|
uint256 spendCap,
|
|
uint256 spent,
|
|
uint64 issuedBlock,
|
|
uint64 expiry,
|
|
uint64 actionNonce,
|
|
bool revoked
|
|
);
|
|
|
|
function isSessionValid(uint256 sessionId) external view returns (bool);
|
|
|
|
function getAgent(uint256 rootKeyId)
|
|
external
|
|
view
|
|
returns (address controller, uint64 createdBlock, uint64 sessionNonce, uint256 sessionCountForAgent);
|
|
|
|
function agentExists(uint256 rootKeyId) external view returns (bool);
|
|
}
|
|
|
|
/// @dev Read-only surface of AereAgentBond used for economic cross-reads.
|
|
interface IAereAgentBondView {
|
|
function bondOf(address operator, bytes32 agentId)
|
|
external
|
|
view
|
|
returns (uint256 amount, uint64 requestedAt, uint256 lifetimeSlashed, bool exists);
|
|
|
|
function operatorLifetimeSlashed(address operator) external view returns (uint256);
|
|
}
|
|
|
|
/// @dev Read-only surface of AereAIReputation used for reputation cross-reads.
|
|
interface IAereAIReputationView {
|
|
function scoreOf(address operator, bytes32 agentId) external view returns (uint256);
|
|
|
|
function statsOf(address operator, bytes32 agentId)
|
|
external
|
|
view
|
|
returns (uint256 positiveCount, uint256 disputeCount, uint256 lifetimeSlashed);
|
|
}
|
|
|
|
contract AereAgentActionReceipt {
|
|
// ----- domain separator (distinct from AereAgentDID's ACTION_DOMAIN so a DID
|
|
// action signature can never be replayed as a provenance receipt) -----
|
|
bytes32 public constant RECEIPT_DOMAIN = keccak256("AereAgentActionReceipt.v1.receipt");
|
|
|
|
// ----- immutable wiring -----
|
|
IAereAgentDID public immutable did; // Falcon-rooted DID: session verification source of truth
|
|
address public immutable agentBond; // AereAgentBond (slashable stake) or 0
|
|
address public immutable aiReputation; // AereAIReputation (composable score) or 0
|
|
|
|
// ----- provenance records (global incrementing id; append-only) -----
|
|
struct Receipt {
|
|
uint256 agentId; // the Falcon rootKeyId (AereAgentDID agent) that authored this
|
|
uint256 sessionId; // the DID session that signed it
|
|
address signer; // the session secp256k1 address recovered at emit time
|
|
bytes32 outputHash; // the committed hash of the agent's output/action
|
|
bytes32 scope; // the session scope tag this output was produced under
|
|
uint64 nonce; // per-session receipt nonce used in the signed digest
|
|
uint64 emittedBlock;
|
|
uint64 emittedAt; // unix timestamp
|
|
bytes sig; // the 65-byte session signature (stored so anyone can re-verify)
|
|
}
|
|
|
|
Receipt[] private _receipts; // receiptId is the index (0-based)
|
|
mapping(uint256 => uint256[]) private _agentReceipts; // rootKeyId => receiptIds
|
|
mapping(uint256 => uint256[]) private _sessionReceipts; // sessionId => receiptIds
|
|
mapping(uint256 => uint64) private _receiptNonce; // sessionId => next receipt nonce
|
|
|
|
// ----- events -----
|
|
event ReceiptEmitted(
|
|
uint256 indexed receiptId,
|
|
uint256 indexed agentId,
|
|
uint256 indexed sessionId,
|
|
address signer,
|
|
bytes32 outputHash,
|
|
bytes32 scope,
|
|
uint64 nonce
|
|
);
|
|
|
|
// ----- errors -----
|
|
error ZeroDID();
|
|
error EmptyOutputHash();
|
|
error RevokedOrInvalidSession(uint256 sessionId);
|
|
error ScopeMismatch(uint256 sessionId);
|
|
error BadSessionSignature(uint256 sessionId);
|
|
error UnknownReceipt(uint256 receiptId);
|
|
|
|
constructor(address did_, address agentBond_, address aiReputation_) {
|
|
if (did_ == address(0)) revert ZeroDID();
|
|
did = IAereAgentDID(did_);
|
|
agentBond = agentBond_;
|
|
aiReputation = aiReputation_;
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Emit
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Record a provenance receipt for an agent output. Permissionless relay:
|
|
* the SESSION SIGNATURE is the authority, not msg.sender.
|
|
*
|
|
* Reverts (recording nothing) if the output hash is zero, the session is
|
|
* not currently valid (revoked / expired / its Falcon root no longer ACTIVE
|
|
* / unknown session), the scope does not match the session's scope, or the
|
|
* signature is not by the session key over the exact commitment (which binds
|
|
* the output hash — a tampered output hash recovers a different address and
|
|
* is rejected).
|
|
*
|
|
* @param sessionId the AereAgentDID session that signs this output.
|
|
* @param scope the scope tag for this output (must equal the session's scopeHash).
|
|
* @param outputHash a 32-byte commitment to the agent's output/action (non-zero).
|
|
* @param sig 65-byte secp256k1 signature (r||s||v) by the session address over
|
|
* receiptDigest(agentId, sessionId, outputHash, scope, nonce).
|
|
* @return receiptId the id of the newly recorded receipt.
|
|
*/
|
|
function emitReceipt(uint256 sessionId, bytes32 scope, bytes32 outputHash, bytes calldata sig)
|
|
external
|
|
returns (uint256 receiptId)
|
|
{
|
|
if (outputHash == bytes32(0)) revert EmptyOutputHash();
|
|
|
|
// A live session gates everything: revoked / expired / root-not-active / unknown
|
|
// all collapse to "invalid" here (isSessionValid returns false, never reverts).
|
|
if (!did.isSessionValid(sessionId)) revert RevokedOrInvalidSession(sessionId);
|
|
|
|
(uint256 rootKeyId, address sessionAddr, bytes32 scopeHash,,,,,,) = did.getSession(sessionId);
|
|
if (scope != scopeHash) revert ScopeMismatch(sessionId);
|
|
|
|
uint64 nonce = _receiptNonce[sessionId];
|
|
bytes32 digest = _receiptDigest(rootKeyId, sessionId, outputHash, scope, nonce);
|
|
address recovered = _recover(digest, sig);
|
|
// A tampered outputHash (or wrong signer) recovers a different / null address.
|
|
if (recovered == address(0) || recovered != sessionAddr) revert BadSessionSignature(sessionId);
|
|
|
|
// Effects.
|
|
_receiptNonce[sessionId] = nonce + 1;
|
|
receiptId = _receipts.length;
|
|
_receipts.push(
|
|
Receipt({
|
|
agentId: rootKeyId,
|
|
sessionId: sessionId,
|
|
signer: sessionAddr,
|
|
outputHash: outputHash,
|
|
scope: scope,
|
|
nonce: nonce,
|
|
emittedBlock: uint64(block.number),
|
|
emittedAt: uint64(block.timestamp),
|
|
sig: sig
|
|
})
|
|
);
|
|
_agentReceipts[rootKeyId].push(receiptId);
|
|
_sessionReceipts[sessionId].push(receiptId);
|
|
|
|
emit ReceiptEmitted(receiptId, rootKeyId, sessionId, sessionAddr, outputHash, scope, nonce);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Verification
|
|
// ==========================================================================
|
|
|
|
/**
|
|
* @notice Independently re-verify a stored receipt in one read. Re-derives the
|
|
* signed digest from the stored fields, recovers the signer from the stored
|
|
* signature, and reports whether it matches the recorded session key, plus
|
|
* whether that session is STILL live now (a receipt stays genuine forever;
|
|
* `sessionLiveNow` tells a consumer whether the agent is currently active or
|
|
* has since been revoked / expired / rotated away).
|
|
*
|
|
* @return sigValid the stored signature genuinely authorizes the stored output.
|
|
* @return sessionLiveNow the authoring session is valid at the current block.
|
|
* @return agentId the Falcon rootKeyId that authored the receipt.
|
|
* @return signer the session address the signature recovers to.
|
|
* @return outputHash the committed output hash.
|
|
*/
|
|
function verifyReceipt(uint256 receiptId)
|
|
external
|
|
view
|
|
returns (bool sigValid, bool sessionLiveNow, uint256 agentId, address signer, bytes32 outputHash)
|
|
{
|
|
if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
|
|
Receipt storage r = _receipts[receiptId];
|
|
bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
|
|
address recovered = _recover(digest, r.sig);
|
|
sigValid = (recovered != address(0) && recovered == r.signer);
|
|
sessionLiveNow = did.isSessionValid(r.sessionId);
|
|
return (sigValid, sessionLiveNow, r.agentId, r.signer, r.outputHash);
|
|
}
|
|
|
|
/**
|
|
* @notice Verify that a receipt attests EXACTLY `outputHash` and that its stored
|
|
* signature is genuine. This is the tamper check at read time: pass the hash
|
|
* of the output you hold; a byte-different output yields a different hash and
|
|
* returns false. Returns false (never reverts) for an out-of-range receiptId.
|
|
*/
|
|
function verifyReceiptOutput(uint256 receiptId, bytes32 outputHash) external view returns (bool) {
|
|
if (receiptId >= _receipts.length) return false;
|
|
Receipt storage r = _receipts[receiptId];
|
|
if (r.outputHash != outputHash) return false;
|
|
bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
|
|
address recovered = _recover(digest, r.sig);
|
|
return (recovered != address(0) && recovered == r.signer);
|
|
}
|
|
|
|
/**
|
|
* @notice The full provenance picture in one call: the receipt's identity + validity
|
|
* AND the agent's live economic accountability, read from AereAgentBond and
|
|
* AereAIReputation at the canonical cross-wire key (operator = DID controller,
|
|
* agentId = bytes32(rootKeyId)). All economic reads are defensive: if a
|
|
* cross-link is unset or reverts, its fields come back zero and the
|
|
* provenance fields (sigValid / signer / outputHash) are still returned.
|
|
*
|
|
* @return agentId the Falcon rootKeyId author.
|
|
* @return operator the DID controller (bond/reputation key operator).
|
|
* @return signer the authoring session key.
|
|
* @return outputHash the committed output hash.
|
|
* @return scope the scope the output was produced under.
|
|
* @return emittedAt unix timestamp the receipt was recorded.
|
|
* @return sigValid the stored signature genuinely authorizes the output.
|
|
* @return sessionLiveNow the authoring session is valid now.
|
|
* @return reputationScore AereAIReputation.scoreOf(operator, bytes32(agentId)).
|
|
* @return bondAmount current AereAgentBond stake at the canonical key.
|
|
* @return bondLifetimeSlashed cumulative slash on that (operator, agentId) bond.
|
|
* @return operatorTotalSlashed operator-level cumulative slash across all its agents.
|
|
*/
|
|
function provenanceOf(uint256 receiptId)
|
|
external
|
|
view
|
|
returns (
|
|
uint256 agentId,
|
|
address operator,
|
|
address signer,
|
|
bytes32 outputHash,
|
|
bytes32 scope,
|
|
uint64 emittedAt,
|
|
bool sigValid,
|
|
bool sessionLiveNow,
|
|
uint256 reputationScore,
|
|
uint256 bondAmount,
|
|
uint256 bondLifetimeSlashed,
|
|
uint256 operatorTotalSlashed
|
|
)
|
|
{
|
|
if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
|
|
Receipt storage r = _receipts[receiptId];
|
|
|
|
agentId = r.agentId;
|
|
signer = r.signer;
|
|
outputHash = r.outputHash;
|
|
scope = r.scope;
|
|
emittedAt = r.emittedAt;
|
|
|
|
bytes32 digest = _receiptDigest(r.agentId, r.sessionId, r.outputHash, r.scope, r.nonce);
|
|
address recovered = _recover(digest, r.sig);
|
|
sigValid = (recovered != address(0) && recovered == r.signer);
|
|
sessionLiveNow = did.isSessionValid(r.sessionId);
|
|
|
|
operator = _operatorOf(r.agentId);
|
|
bytes32 bondKey = bytes32(r.agentId);
|
|
|
|
(reputationScore) = _scoreOf(operator, bondKey);
|
|
(bondAmount, bondLifetimeSlashed) = _bondOf(operator, bondKey);
|
|
operatorTotalSlashed = _operatorLifetimeSlashed(operator);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Digest / cross-wire views
|
|
// ==========================================================================
|
|
|
|
/// @notice The digest a session key must sign to record a receipt at `nonce`.
|
|
function receiptDigest(uint256 agentId, uint256 sessionId, bytes32 outputHash, bytes32 scope, uint64 nonce)
|
|
external
|
|
view
|
|
returns (bytes32)
|
|
{
|
|
return _receiptDigest(agentId, sessionId, outputHash, scope, nonce);
|
|
}
|
|
|
|
function _receiptDigest(uint256 agentId, uint256 sessionId, bytes32 outputHash, bytes32 scope, uint64 nonce)
|
|
internal
|
|
view
|
|
returns (bytes32)
|
|
{
|
|
return keccak256(
|
|
abi.encode(RECEIPT_DOMAIN, block.chainid, address(this), agentId, sessionId, outputHash, scope, nonce)
|
|
);
|
|
}
|
|
|
|
/// @notice The next receipt nonce for a session (the value the next emit will sign).
|
|
function receiptNonceOf(uint256 sessionId) external view returns (uint64) {
|
|
return _receiptNonce[sessionId];
|
|
}
|
|
|
|
/// @notice The canonical AereAgentBond / AereAIReputation agentId for a DID rootKeyId.
|
|
/// Operators post their bond under this key to have stake surfaced by
|
|
/// {provenanceOf}. Pure — no state, just the encoding convention.
|
|
function bondKeyOf(uint256 rootKeyId) external pure returns (bytes32) {
|
|
return bytes32(rootKeyId);
|
|
}
|
|
|
|
/// @notice The operator (bond/reputation key) this contract reads for an agent:
|
|
/// the DID controller of the root key. Returns address(0) if unknown.
|
|
function operatorOf(uint256 rootKeyId) external view returns (address) {
|
|
return _operatorOf(rootKeyId);
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Views
|
|
// ==========================================================================
|
|
|
|
/// @notice Total receipts ever recorded (receiptIds run 0..receiptCount-1).
|
|
function receiptCount() external view returns (uint256) {
|
|
return _receipts.length;
|
|
}
|
|
|
|
/// @notice Read a receipt. Reverts on an out-of-range receiptId.
|
|
function getReceipt(uint256 receiptId)
|
|
external
|
|
view
|
|
returns (
|
|
uint256 agentId,
|
|
uint256 sessionId,
|
|
address signer,
|
|
bytes32 outputHash,
|
|
bytes32 scope,
|
|
uint64 nonce,
|
|
uint64 emittedBlock,
|
|
uint64 emittedAt,
|
|
bytes memory sig
|
|
)
|
|
{
|
|
if (receiptId >= _receipts.length) revert UnknownReceipt(receiptId);
|
|
Receipt storage r = _receipts[receiptId];
|
|
return (r.agentId, r.sessionId, r.signer, r.outputHash, r.scope, r.nonce, r.emittedBlock, r.emittedAt, r.sig);
|
|
}
|
|
|
|
/// @notice All receiptIds authored by an agent (append-only, chronological).
|
|
function receiptsOf(uint256 rootKeyId) external view returns (uint256[] memory) {
|
|
return _agentReceipts[rootKeyId];
|
|
}
|
|
|
|
/// @notice All receiptIds recorded under a session (append-only, chronological).
|
|
function receiptsOfSession(uint256 sessionId) external view returns (uint256[] memory) {
|
|
return _sessionReceipts[sessionId];
|
|
}
|
|
|
|
// ==========================================================================
|
|
// Defensive economic cross-reads
|
|
// ==========================================================================
|
|
|
|
function _operatorOf(uint256 rootKeyId) internal view returns (address) {
|
|
try did.getAgent(rootKeyId) returns (address controller, uint64, uint64, uint256) {
|
|
return controller;
|
|
} catch {
|
|
return address(0);
|
|
}
|
|
}
|
|
|
|
function _scoreOf(address operator, bytes32 bondKey) internal view returns (uint256) {
|
|
if (aiReputation == address(0) || operator == address(0)) return 0;
|
|
try IAereAIReputationView(aiReputation).scoreOf(operator, bondKey) returns (uint256 s) {
|
|
return s;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function _bondOf(address operator, bytes32 bondKey) internal view returns (uint256 amount, uint256 slashed) {
|
|
if (agentBond == address(0) || operator == address(0)) return (0, 0);
|
|
try IAereAgentBondView(agentBond).bondOf(operator, bondKey) returns (
|
|
uint256 a, uint64, uint256 sl, bool
|
|
) {
|
|
return (a, sl);
|
|
} catch {
|
|
return (0, 0);
|
|
}
|
|
}
|
|
|
|
function _operatorLifetimeSlashed(address operator) internal view returns (uint256) {
|
|
if (agentBond == address(0) || operator == address(0)) return 0;
|
|
try IAereAgentBondView(agentBond).operatorLifetimeSlashed(operator) returns (uint256 s) {
|
|
return s;
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// ==========================================================================
|
|
// ECDSA recovery
|
|
// ==========================================================================
|
|
|
|
/// @dev Minimal ecrecover with malleability guard (low-s, v in {27,28}). Returns
|
|
/// address(0) on any malformed input so callers treat it as an invalid signature.
|
|
/// Mirrors AereAgentDID._recover so a session key signs both surfaces identically.
|
|
function _recover(bytes32 digest, bytes memory sig) internal pure returns (address) {
|
|
if (sig.length != 65) return address(0);
|
|
bytes32 r;
|
|
bytes32 s;
|
|
uint8 v;
|
|
assembly {
|
|
r := mload(add(sig, 0x20))
|
|
s := mload(add(sig, 0x40))
|
|
v := byte(0, mload(add(sig, 0x60)))
|
|
}
|
|
// Reject high-s (EIP-2) to prevent signature malleability.
|
|
if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) {
|
|
return address(0);
|
|
}
|
|
if (v != 27 && v != 28) return address(0);
|
|
return ecrecover(digest, v, r, s);
|
|
}
|
|
}
|