// SPDX-License-Identifier: MIT pragma solidity 0.8.23; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol"; /// @dev The one AERE402Facilitator surface paidQuery consumes. The facilitator is the /// existing agentic payment rail (see contracts/agentic/AERE402Facilitator.sol); this /// contract references it and never redeploys it. `settle` debits the paying agent's /// prepaid balance (via the AereAgent registry the facilitator is bound to), takes the /// 25-bps protocol fee to AereSink, and transfers the remainder to `payee`. paidQuery /// passes address(this) as `payee` so the facilitator's own `msg.sender == payee` guard /// is satisfied, then forwards the received tokens to the store's payment recipient. interface IAERE402Facilitator { function settle( uint256 agentId, address token, address payee, uint256 amount, bytes32 resourceId, uint256 nonce, uint256 deadline, bytes calldata signature ) external; function consumed(uint256 agentId, uint256 nonce) external view returns (bool); } /** * @title AereVectorStore, verifiable semantic memory for AI agents on AERE (chain 2800) * * @notice On-chain provenance, ownership and attested-retrieval for an AI agent's vector * store (its long-term semantic memory). An owner (an account or an agent identity) * commits a VERSIONED commitment root over the vectors + metadata it stores off-chain; * a retrieval provider later attests "for this query, against store version V, these * are the results (resultRoot)". A consumer can then verify, on-chain, that a returned * vector was actually a member of a committed store version, that the retrieval named a * committed version, and who owns the memory. Query access is metered through the * existing AERE402 agentic payment rail. * * WHAT THE CHAIN GUARANTEES (buildable NOW, tested): * 1. AUTHENTICITY of the committed root. A commit to a post-quantum store must carry * a Falcon-512 signature that verifies, in full, on-chain, through AERE's LIVE * native precompile at 0x0AE1 over this contract's per-version challenge. An * invalid signature is rejected fail-closed; nothing is recorded. * 2. APPEND-ONLY HISTORY. Each commit appends a new version; no function can rewrite * or delete a committed root. There is no admin, no owner override, no upgrade * hook. The current root and every past version stay queryable forever. * 3. RETRIEVAL-AGAINST-A-COMMITTED-VERSION. attestRetrieval reverts unless the named * version exists (1..current). A recorded attestation therefore always references a * real, committed store state, and carries an inclusion-proof shape so a consumer * can check a returned vector was in the committed root. * 4. OWNERSHIP. Every store is bound to an owner at creation; a classical store's * commits require msg.sender == owner, a post-quantum store's commits require the * Falcon key (authorship IS the PQC signature, so a relayer only pays gas). * 5. PAID, ATTESTED RETRIEVAL. On a priced store, a retrieval attestation must be * backed by an AERE402 payment (paidQuery), so retrieval is metered on the rail. * * WHAT THE CHAIN DOES NOT GUARANTEE (off-chain, [MEASURE]): * - It does NOT compute embeddings, does NOT run the vector similarity search, and * does NOT hold the vectors. The embedding model + the nearest-neighbour index + * the raw vector data all live off-chain. The chain proves PROVENANCE and OWNERSHIP * of a committed root and that a retrieval named a committed version; it does NOT * and CANNOT prove that the embeddings are semantically good, that the index is * well-built, or that the provider ran an honest similarity search. Semantic * quality is an off-chain property, measured off-chain. * * HONEST SCOPE. Application-layer PQC authentication + settlement. This does NOT change * AERE consensus: blocks are still produced and signed by Besu QBFT validators with * classical ECDSA (secp256k1). PQC-authenticated memory commits do not make consensus * post-quantum. What is post-quantum here is the AUTHENTICITY of a store commit, via the * live Falcon-512 precompile. No new token is introduced; paid queries settle on the * existing AERE402 rail in whatever ERC-20 the store owner selects. * * @dev Falcon-512 precompile I/O (0x0AE1), identical to AerePQCAttestation: * input = pk(897) || sm * sm = sigLen(2, big-endian) || nonce(40) || message(32) || esig * esig = 0x29 || compressedSig, sigLen == esig.length * falconSig = nonce(40) || esig (the envelope handed to commit) * Output: a 32-byte word, 0x..01 valid else 0x..00. An empty/zero return (a stale * caller hitting an address with no precompile) is treated as INVALID, so a pre-fork * node can never record a commit. */ contract AereVectorStore is ReentrancyGuard { using MerkleProof for bytes32[]; /* ------------------------------ Falcon-512 ------------------------------ */ uint8 internal constant SCHEME_FALCON512 = 1; address public constant PRECOMPILE_FALCON512 = address(0x0AE1); uint256 internal constant FALCON512_PK_LEN = 897; uint8 internal constant FALCON512_PK_HEADER = 0x09; // 0x00 | logn=9 uint256 internal constant FALCON_NONCE_LEN = 40; /// @notice Domain tag binding a commit challenge to THIS contract + chain + store + version. bytes32 public constant COMMIT_DOMAIN = keccak256("AereVectorStore.v1.commit"); /// @notice Domain tag binding a paid-query resourceId to THIS contract + chain + store + query. bytes32 public constant QUERY_DOMAIN = keccak256("AereVectorStore.v1.query"); /* ------------------------------- payment rail ---------------------------- */ /// @notice The existing AERE402 facilitator. Immutable; referenced, never redeployed. IAERE402Facilitator public immutable FACILITATOR; /* --------------------------------- types -------------------------------- */ struct Store { bool exists; address owner; // account / agent identity that owns the memory (fixed at creation) address settlementToken; // ERC-20 that priced queries settle in (0 => free store) address paymentRecipient; // where paidQuery net proceeds go (owner or retrieval provider) uint256 queryPrice; // minimum AERE402 payment per query (0 => free, no payment gate) uint64 version; // current version count (0 => no commits yet) bytes falconPubKey; // empty => classical store; 897-byte Falcon-512 key => PQC store } struct Version { bytes32 vectorRoot; // Merkle/commitment root over the stored vectors + metadata uint64 vectorCount; // number of vectors the root commits to uint64 committedAt; // block.timestamp of the commit uint64 blockNumber; // block the commit landed in string uri; // pointer to the off-chain index snapshot (ipfs:// / https://) } struct Retrieval { bool exists; uint64 version; // the committed store version this retrieval was against uint64 attestedAt; uint64 blockNumber; address provider; // the attester (retrieval provider) bytes32 queryHash; // commitment to the query the results answer bytes32 resultRoot; // Merkle root over the returned result set } struct QueryReceipt { bool exists; bool consumed; // flipped true when an attestRetrieval binds to it (one-shot) uint64 paidAtVersion; // store version at payment time (informational) bytes32 storeId; // the store the payment was for (binds a receipt to one store) bytes32 queryHash; // the query the payment was for uint256 amount; // gross amount paid through the rail address payer; // msg.sender of paidQuery (provider relayer or consumer) address provider; // the ONLY address allowed to consume this receipt in attestRetrieval } /* -------------------------------- storage ------------------------------- */ mapping(bytes32 => Store) internal _stores; mapping(bytes32 => Version[]) internal _versions; // storeId => append-only version history mapping(bytes32 => Retrieval) internal _retrievals; // retrievalId => Retrieval mapping(bytes32 => uint64) public retrievalCount; // storeId => number of attestations mapping(bytes32 => QueryReceipt) internal _receipts; // receiptId => QueryReceipt /// @notice Total commits recorded across all stores (monotonic). uint256 public commitCount; /* --------------------------------- events ------------------------------- */ event StoreCreated( bytes32 indexed storeId, address indexed owner, bool postQuantum, address settlementToken, uint256 queryPrice, address paymentRecipient ); event Committed( bytes32 indexed storeId, uint64 indexed version, bytes32 vectorRoot, uint64 vectorCount, bytes32 challenge, string uri ); event RetrievalAttested( bytes32 indexed storeId, bytes32 indexed retrievalId, uint64 indexed version, address provider, bytes32 queryHash, bytes32 resultRoot ); event QueryPaid( bytes32 indexed storeId, bytes32 indexed receiptId, address indexed payer, uint256 agentId, uint256 amount, bytes32 queryHash ); /* --------------------------------- errors ------------------------------- */ error ZeroAddress(); error StoreExists(); error UnknownStore(); error InvalidPubKey(); error InvalidConfig(); error NotOwner(); error EmptyRoot(); error InvalidSignatureLength(); error PQCVerificationFailed(); error UnknownVersion(); error StoreIsFree(); error PaymentRequired(); error ReceiptExists(); error ReceiptConsumed(); error ReceiptMismatch(); error NotReceiptProvider(); error Underpaid(uint256 paid, uint256 price); error UnknownRetrieval(); error TransferFailed(); /* ------------------------------- constructor ---------------------------- */ /// @param facilitator the existing AERE402Facilitator (the agentic payment rail). constructor(address facilitator) { if (facilitator == address(0)) revert ZeroAddress(); FACILITATOR = IAERE402Facilitator(facilitator); } /* ================================ stores ================================ */ /** * @notice Create a vector store bound to msg.sender as owner. Config is fixed at creation: * there is no setter for the Falcon key, the price, the token or the recipient, so * a store's authentication mode and payment terms cannot be silently changed later. * @param storeId caller-chosen identifier (must be unused). * @param falconPubKey empty for a classical (owner-signed) store, or a 897-byte * Falcon-512 public key (header 0x09) for a post-quantum store whose * commits are authenticated by the live 0x0AE1 precompile. * @param settlementToken ERC-20 that priced queries settle in (must be set iff queryPrice>0). * @param queryPrice minimum AERE402 payment per query, in settlementToken units * (0 => free store, retrieval attestations need no payment). * @param paymentRecipient where paidQuery net proceeds are forwarded (must be set iff priced). */ function createStore( bytes32 storeId, bytes calldata falconPubKey, address settlementToken, uint256 queryPrice, address paymentRecipient ) external { if (_stores[storeId].exists) revert StoreExists(); bool postQuantum = falconPubKey.length != 0; if (postQuantum) { // Fail-closed on a malformed PQC key: a store can never be created with a Falcon // key the precompile could not parse. if (falconPubKey.length != FALCON512_PK_LEN || uint8(falconPubKey[0]) != FALCON512_PK_HEADER) { revert InvalidPubKey(); } } // Pricing config must be internally consistent: a priced store needs a token to be // paid in and a recipient to be paid to; a free store must set neither. if (queryPrice != 0) { if (settlementToken == address(0) || paymentRecipient == address(0)) revert InvalidConfig(); } else { if (settlementToken != address(0) || paymentRecipient != address(0)) revert InvalidConfig(); } Store storage s = _stores[storeId]; s.exists = true; s.owner = msg.sender; s.settlementToken = settlementToken; s.paymentRecipient = paymentRecipient; s.queryPrice = queryPrice; s.falconPubKey = falconPubKey; emit StoreCreated(storeId, msg.sender, postQuantum, settlementToken, queryPrice, paymentRecipient); } /** * @notice Append a new versioned commitment root for a store. Version numbers start at 1 and * increase by exactly one per commit; the challenge binds the version, so a signature * is single-use and history is append-only. * * AUTHENTICATION: * - post-quantum store: `falconSig` MUST verify, on-chain via 0x0AE1, over * commitChallenge(...). An invalid or malformed signature reverts * PQCVerificationFailed / InvalidSignatureLength and records nothing (fail-closed). * msg.sender is unrestricted: authorship is the Falcon signature, so any relayer * may submit and only the key holder can produce a valid commit. * - classical store: msg.sender MUST be the owner; `falconSig` is ignored. * * @return version the new version number (== the store's current version after this call). */ function commit( bytes32 storeId, bytes32 vectorRoot, uint64 vectorCount, string calldata uri, bytes calldata falconSig ) external returns (uint64 version) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); if (vectorRoot == bytes32(0)) revert EmptyRoot(); version = s.version + 1; bytes32 challenge = commitChallenge(storeId, version, vectorRoot, vectorCount, uri); if (s.falconPubKey.length != 0) { // Fail-closed post-quantum authentication. if (!_verifyFalcon512(s.falconPubKey, challenge, falconSig)) revert PQCVerificationFailed(); } else { // Classical owner authentication. if (msg.sender != s.owner) revert NotOwner(); } // Effects: append the new version. No path can overwrite an earlier entry. _versions[storeId].push( Version({ vectorRoot: vectorRoot, vectorCount: vectorCount, committedAt: uint64(block.timestamp), blockNumber: uint64(block.number), uri: uri }) ); s.version = version; commitCount += 1; emit Committed(storeId, version, vectorRoot, vectorCount, challenge, uri); } /* ============================== retrieval =============================== */ /** * @notice Record a retrieval attestation: a provider (msg.sender) attests that, for `queryHash` * against committed store `version`, the returned result set is committed by `resultRoot`. * The named version MUST exist (1..current) or the call reverts fail-closed, so an * attestation can never reference a store state that was never committed. * * On a PRICED store, the attestation MUST be backed by a matching, unconsumed AERE402 * payment receipt (see paidQuery); the receipt is consumed one-shot AND only its bound * provider (recorded at paidQuery) may consume it, so a third party cannot front-run the * honest provider to burn the receipt with a junk resultRoot. On a FREE store, `receiptId` * is ignored. * * @param version the committed store version the retrieval ran against. * @param queryHash commitment to the query (e.g. keccak256 of the normalized query embedding). * @param resultRoot Merkle root over the returned result set (leaves = per-result commitments). * @param receiptId the paidQuery receipt backing this retrieval (priced stores only). * @return retrievalId deterministic id of the recorded attestation. */ function attestRetrieval( bytes32 storeId, uint64 version, bytes32 queryHash, bytes32 resultRoot, bytes32 receiptId ) external returns (bytes32 retrievalId) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); // Fail-closed: the store version must have been committed. if (version == 0 || version > s.version) revert UnknownVersion(); if (resultRoot == bytes32(0)) revert EmptyRoot(); // Payment gate for priced stores. if (s.queryPrice != 0) { QueryReceipt storage r = _receipts[receiptId]; if (!r.exists) revert PaymentRequired(); if (r.consumed) revert ReceiptConsumed(); // Only the provider the payer bound at paidQuery may consume this receipt. Without this a // third party could front-run the honest provider and burn the receipt with a junk // resultRoot (griefing / denial-of-service on a paid-for retrieval). if (r.provider != msg.sender) revert NotReceiptProvider(); if (r.storeId != storeId || r.queryHash != queryHash) revert ReceiptMismatch(); if (r.amount < s.queryPrice) revert Underpaid(r.amount, s.queryPrice); r.consumed = true; } uint64 idx = retrievalCount[storeId]; retrievalId = keccak256(abi.encode(address(this), storeId, idx)); _retrievals[retrievalId] = Retrieval({ exists: true, version: version, attestedAt: uint64(block.timestamp), blockNumber: uint64(block.number), provider: msg.sender, queryHash: queryHash, resultRoot: resultRoot }); retrievalCount[storeId] = idx + 1; emit RetrievalAttested(storeId, retrievalId, version, msg.sender, queryHash, resultRoot); } /* ============================== paid query ============================== */ /** * @notice Pay for a query against a priced store, on the existing AERE402 rail, and mint a * receipt that a retrieval provider can later cite in attestRetrieval. The paying * AGENT authorizes the payment with its EIP-712 signature to the facilitator (exactly * as any AERE402 payment); this contract is the `payee`, so the facilitator debits the * agent, takes the protocol fee to AereSink, and transfers the remainder here, which is * then forwarded to the store's payment recipient in the same transaction. This contract * custodies no balance across calls. * * @param queryHash commitment to the query being paid for (bound into the receipt + resourceId). * @param provider the retrieval provider this payment is for; the ONLY address that may later * consume the minted receipt in attestRetrieval (binds the receipt so a third * party cannot grief the honest provider). Must be non-zero. * @param agentId the paying AERE402 agent id (in the AereAgent registry the facilitator uses). * @param amount gross payment; must be >= the store's queryPrice. * @param nonce the agent's AERE402 nonce for this payment (facilitator replay guard). * @param deadline unix seconds after which the payment auth is stale. * @param authSig the agent's EIP-712 PaymentAuth signature over (agentId, token, this, amount, * resourceId, nonce, deadline) for the facilitator. * @return receiptId deterministic id of the minted receipt. */ function paidQuery( bytes32 storeId, bytes32 queryHash, address provider, uint256 agentId, uint256 amount, uint256 nonce, uint256 deadline, bytes calldata authSig ) external nonReentrant returns (bytes32 receiptId) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); if (s.queryPrice == 0) revert StoreIsFree(); if (provider == address(0)) revert ZeroAddress(); if (amount < s.queryPrice) revert Underpaid(amount, s.queryPrice); receiptId = keccak256(abi.encode(address(this), storeId, queryHash, agentId, nonce)); if (_receipts[receiptId].exists) revert ReceiptExists(); address token = s.settlementToken; address recipient = s.paymentRecipient; // Bind the payment to THIS store + query so the facilitator's resourceId is meaningful. bytes32 resourceId = queryResourceId(storeId, queryHash); // Settle on the AERE402 rail. address(this) is the payee, satisfying the facilitator's // msg.sender == payee guard. The facilitator sends (amount - fee) here; measure the delta // so a changed fee never strands tokens, then forward the whole receipt to the recipient. uint256 balBefore = IERC20(token).balanceOf(address(this)); FACILITATOR.settle(agentId, token, address(this), amount, resourceId, nonce, deadline, authSig); uint256 received = IERC20(token).balanceOf(address(this)) - balBefore; if (received > 0) { if (!IERC20(token).transfer(recipient, received)) revert TransferFailed(); } _receipts[receiptId] = QueryReceipt({ exists: true, consumed: false, paidAtVersion: s.version, storeId: storeId, queryHash: queryHash, amount: amount, payer: msg.sender, provider: provider }); emit QueryPaid(storeId, receiptId, msg.sender, agentId, amount, queryHash); } /* ============================= inclusion ============================== */ /** * @notice Verify that `leaf` is a member of a committed store version's vectorRoot. This is the * consumer's check that a returned vector was ACTUALLY in the committed store at version * V. The leaf is the caller-supplied commitment to the vector + its metadata; the proof * is a standard OpenZeppelin (sorted-pair keccak256) Merkle proof against vectorRoot. * @dev Proves membership in a committed root only. It says nothing about semantic relevance; * relevance is an off-chain property. */ function verifyVectorInclusion(bytes32 storeId, uint64 version, bytes32 leaf, bytes32[] calldata proof) external view returns (bool) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); if (version == 0 || version > s.version) revert UnknownVersion(); bytes32 root = _versions[storeId][version - 1].vectorRoot; return proof.verify(root, leaf); } /** * @notice Verify that `leaf` is a member of a recorded retrieval's resultRoot, i.e. that the * returned result the consumer holds was in the set the provider attested to. */ function verifyResultInclusion(bytes32 retrievalId, bytes32 leaf, bytes32[] calldata proof) external view returns (bool) { Retrieval storage r = _retrievals[retrievalId]; if (!r.exists) revert UnknownRetrieval(); return proof.verify(r.resultRoot, leaf); } /* ================================ views ================================ */ /// @notice The exact 32-byte challenge a Falcon-512 commit must sign for (storeId, version). /// Off-chain signers query this to know what to sign. function commitChallenge(bytes32 storeId, uint64 version, bytes32 vectorRoot, uint64 vectorCount, string calldata uri) public view returns (bytes32) { return keccak256( abi.encode(COMMIT_DOMAIN, block.chainid, address(this), storeId, version, vectorRoot, vectorCount, keccak256(bytes(uri))) ); } /// @notice The resourceId a paidQuery binds into its AERE402 settlement for (storeId, queryHash). function queryResourceId(bytes32 storeId, bytes32 queryHash) public view returns (bytes32) { return keccak256(abi.encode(QUERY_DOMAIN, block.chainid, address(this), storeId, queryHash)); } /// @notice True iff the store exists. function storeExists(bytes32 storeId) external view returns (bool) { return _stores[storeId].exists; } /// @notice Read a store's configuration and current version count. function getStore(bytes32 storeId) external view returns ( bool exists, address owner, bool postQuantum, address settlementToken, uint256 queryPrice, address paymentRecipient, uint64 version ) { Store storage s = _stores[storeId]; return (s.exists, s.owner, s.falconPubKey.length != 0, s.settlementToken, s.queryPrice, s.paymentRecipient, s.version); } /// @notice The registered Falcon-512 public key for a store (empty for a classical store). function falconKeyOf(bytes32 storeId) external view returns (bytes memory) { return _stores[storeId].falconPubKey; } /// @notice The number of committed versions for a store (== current version; 0 if none). function versionCount(bytes32 storeId) external view returns (uint64) { return _stores[storeId].version; } /// @notice The current (latest) committed root for a store. Reverts if nothing committed yet. function currentRoot(bytes32 storeId) external view returns (uint64 version, bytes32 vectorRoot, uint64 vectorCount, string memory uri) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); if (s.version == 0) revert UnknownVersion(); Version storage v = _versions[storeId][s.version - 1]; return (s.version, v.vectorRoot, v.vectorCount, v.uri); } /// @notice Read any committed version (1..current) of a store. function getVersion(bytes32 storeId, uint64 version) external view returns (bytes32 vectorRoot, uint64 vectorCount, uint64 committedAt, uint64 blockNumber, string memory uri) { Store storage s = _stores[storeId]; if (!s.exists) revert UnknownStore(); if (version == 0 || version > s.version) revert UnknownVersion(); Version storage v = _versions[storeId][version - 1]; return (v.vectorRoot, v.vectorCount, v.committedAt, v.blockNumber, v.uri); } /// @notice Read a recorded retrieval attestation. function getRetrieval(bytes32 retrievalId) external view returns ( bool exists, uint64 version, address provider, bytes32 queryHash, bytes32 resultRoot, uint64 attestedAt, uint64 blockNumber ) { Retrieval storage r = _retrievals[retrievalId]; return (r.exists, r.version, r.provider, r.queryHash, r.resultRoot, r.attestedAt, r.blockNumber); } /// @notice Read a paid-query receipt. function getReceipt(bytes32 receiptId) external view returns ( bool exists, bool consumed, bytes32 storeId, bytes32 queryHash, uint256 amount, address payer, uint64 paidAtVersion, address provider ) { QueryReceipt storage r = _receipts[receiptId]; return (r.exists, r.consumed, r.storeId, r.queryHash, r.amount, r.payer, r.paidAtVersion, r.provider); } /* ============================ Falcon-512 verify ========================== */ /** * @dev Build the exact Falcon-512 precompile input for `message`, staticcall 0x0AE1, and return * true iff it answers 0x..01. `falconSig` = nonce(40) || esig. Any empty/short/zero return * (a pre-fork node) is invalid. Same construction as AerePQCAttestation, restricted to * Falcon-512. */ function _verifyFalcon512(bytes memory pubKey, bytes32 message, bytes memory falconSig) internal view returns (bool) { if (falconSig.length <= FALCON_NONCE_LEN) revert InvalidSignatureLength(); uint256 sigLen = falconSig.length - FALCON_NONCE_LEN; // esig length if (sigLen > type(uint16).max) revert InvalidSignatureLength(); bytes memory nonce = _slice(falconSig, 0, FALCON_NONCE_LEN); bytes memory esig = _slice(falconSig, FALCON_NONCE_LEN, sigLen); // sm = sigLen(2, big-endian) || nonce(40) || message(32) || esig bytes memory sm = abi.encodePacked(uint16(sigLen), nonce, message, esig); bytes memory input = abi.encodePacked(pubKey, sm); (bool ok, bytes memory ret) = PRECOMPILE_FALCON512.staticcall(input); return ok && ret.length >= 32 && ret[31] == 0x01; } function _slice(bytes memory data, uint256 start, uint256 len) internal pure returns (bytes memory out) { out = new bytes(len); for (uint256 i = 0; i < len; i++) { out[i] = data[start + i]; } } }