// 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];
        }
    }
}
