// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;

import {ISP1Verifier} from "../zkverify/ISP1Verifier.sol";

/**
 * @title AerePQAggregateVerifier, constant-cost on-chain verification of a t-of-n
 *        POST-QUANTUM committee authorization via ONE succinct SP1 proof.
 *
 * @notice One small SP1 proof attests that a t-of-n committee of NIST post-quantum
 *         signatures (ML-DSA / Falcon) all signed a given message digest, so the
 *         ON-CHAIN verification cost is INDEPENDENT of the committee size n. A relying
 *         party registers a committee once (a root commitment to the n authorized PQC
 *         public keys, a threshold t, a size n, and a scheme), and thereafter every
 *         authorization is a SINGLE gateway verifyProof call whose gas does not grow
 *         with n. This is the aggregation counterpart to AereThresholdPQCRegistry: that
 *         registry verifies t INDEPENDENT PQC signatures on-chain (cost linear in t,
 *         bounded by the per-transaction gas ceiling); this verifier moves the t
 *         signature checks OFF-CHAIN into an SP1 zkVM circuit and verifies one proof.
 *
 * @dev    THE HONEST BOUNDARY, read this before trusting or describing anything.
 *
 *         1. THE OFF-CHAIN SP1 AGGREGATION CIRCUIT IS NOT IMPLEMENTED HERE. What is built
 *            and tested in this repository is the ON-CHAIN side: this verifier, the
 *            public-input binding scheme, the committee registry, the fail-closed checks,
 *            and the ERC-7579 module (AerePQAggregateModule). The zkVM guest program that
 *            actually verifies n PQC signatures against the committee root and emits the
 *            public values is REAL cryptographic engineering that is NOT in this contract
 *            and is marked [MEASURE] in docs/AERE-PQ-AGGREGATE.md. Verifying lattice PQC
 *            (especially Falcon's FFT / floating-point sampling) inside a zkVM is hard;
 *            ML-DSA-65 (FIPS 204, integer arithmetic) is the tractable FIRST target.
 *            Do NOT describe the aggregation as working end-to-end: the on-chain verifier
 *            is proven against a MOCK SP1 gateway that checks the bound public values, the
 *            same test double used by AereComputeMarketV3 and the light clients.
 *
 *         2. SCOPE. This is ACCOUNT / AUTHORIZATION aggregation, not consensus. It changes
 *            nothing about Aere Network consensus, which remains classical ECDSA QBFT
 *            (Foundation validators). What is post-quantum here is the AUTHORITY that
 *            approves a message: it survives a quantum adversary that can forge ECDSA,
 *            because the underlying committee legs are NIST PQC signatures, proven in the
 *            zkVM. This contract holds NO funds and has NO admin or owner override.
 *
 *         3. THE PINNED CIRCUIT. AGGREGATE_PROGRAM_VKEY is the SP1 verification key of the
 *            ONE audited aggregation circuit this verifier accepts, fixed at construction.
 *            A registrant CANNOT substitute a different circuit: every committee is proven
 *            by the same pinned program through the same immutable gateway. Deploy one
 *            verifier per circuit (for example one for the ML-DSA-65 aggregation program).
 *
 *         4. FAIL-CLOSED. An authorization is accepted ONLY when ALL of the following hold,
 *            and is rejected otherwise:
 *              - the committee is registered (else UnknownCommittee),
 *              - the proof's committee root equals the registered root (else CommitteeRootMismatch),
 *              - the proof's threshold / size / scheme equal the registered committee
 *                (else ThresholdMismatch / SizeMismatch / SchemeMismatch),
 *              - the proof's message digest equals the digest the caller is authorizing
 *                (else MessageDigestMismatch),
 *              - the proof's proven valid-signature count is >= the committee threshold
 *                (else BelowThreshold),
 *              - the SP1 proof verifies against the pinned vkey through the gateway
 *                (verifyProof REVERTS on an invalid proof).
 *            The committee root is the sole authority binding: because the root commits to
 *            the exact n public keys, a proof for a different key set produces a different
 *            root and is rejected. Binding t, n and scheme in addition is defense in depth.
 *
 * @dev    PUBLIC-VALUES LAYOUT. The SP1 guest program MUST output its public values as the
 *         fixed 192-byte ABI encoding of six words:
 *           abi.encode(
 *             bytes32 committeeRoot,   // recomputed IN the circuit from the witnessed n keys
 *             uint256 threshold,       // t the circuit enforced (validCount was compared to it)
 *             uint256 committeeSize,   // n
 *             uint256 scheme,          // PQC scheme id (see SCHEME_* constants)
 *             bytes32 messageDigest,   // the 32-byte message every counted signature signed
 *             uint256 validCount       // number of DISTINCT valid committee signatures, >= t
 *           )
 *         The circuit is responsible for: recomputing committeeRoot from the witnessed keys,
 *         verifying each of the validCount signatures against a DISTINCT committed key over
 *         messageDigest, and never double-counting a key. This contract trusts the proof for
 *         those facts and only binds the public values to a registered committee and to the
 *         caller's claimed message. See docs/AERE-PQ-AGGREGATE.md.
 */
contract AerePQAggregateVerifier {
    // ==========================================================================
    //                          Scheme labels (informational)
    // ==========================================================================
    //
    // These name the PQC scheme a committee aggregates. They are bound into the
    // public values as defense in depth; the real circuit binding is the pinned
    // vkey. ML-DSA-65 is the tractable first aggregation target (see the doc).

    uint8 public constant SCHEME_FALCON512 = 1;
    uint8 public constant SCHEME_FALCON1024 = 2;
    uint8 public constant SCHEME_MLDSA44 = 3;
    uint8 public constant SCHEME_SLHDSA128S = 4;
    uint8 public constant SCHEME_MLDSA65 = 5; // FIPS 204 ML-DSA-65, the tractable first target

    /// @notice Exact byte length of the SP1 public values (six 32-byte words).
    uint256 internal constant PUBLIC_VALUES_LEN = 192;

    // ==========================================================================
    //                              Immutable wiring
    // ==========================================================================

    /// @notice The deployed SP1 verifier gateway that checks aggregation proofs on-chain.
    ///         On Aere Network mainnet (chain 2800) this is the canonical Succinct gateway
    ///         at 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628. verifyProof REVERTS on an
    ///         invalid proof, which is the fail-closed backbone of this verifier.
    ISP1Verifier public immutable SP1_GATEWAY;

    /// @notice The SP1 verification key of the ONE audited aggregation circuit this verifier
    ///         accepts. Fixed at construction; a registrant cannot substitute another circuit.
    bytes32 public immutable AGGREGATE_PROGRAM_VKEY;

    // ==========================================================================
    //                                  Types
    // ==========================================================================

    struct Committee {
        bool exists;
        uint8 scheme; // one of SCHEME_* (the whole committee shares one scheme)
        uint32 threshold; // t: distinct valid signatures the proof must attest (>= t)
        uint32 size; // n: number of authorized committee members
        address registrant; // who registered (informational; carries no authority)
        bytes32 root; // commitment to the n authorized PQC public keys
    }

    /// @notice A recorded authorization (provenance), keyed by (committeeId, messageDigest).
    struct Attestation {
        bool exists;
        uint32 validCount; // distinct valid signatures the proof attested
        uint64 blockNumber; // block the attestation was recorded
    }

    // ==========================================================================
    //                                 Storage
    // ==========================================================================

    uint256 public committeeCount;
    mapping(uint256 => Committee) private _committees;
    // keccak256(committeeId, messageDigest) => recorded attestation
    mapping(bytes32 => Attestation) private _attestations;

    // ==========================================================================
    //                                 Events
    // ==========================================================================

    event CommitteeRegistered(
        uint256 indexed committeeId,
        address indexed registrant,
        uint8 indexed scheme,
        uint32 threshold,
        uint32 size,
        bytes32 root
    );

    event AggregateVerified(
        uint256 indexed committeeId,
        bytes32 indexed messageDigest,
        uint32 threshold,
        uint32 validCount,
        uint64 blockNumber
    );

    // ==========================================================================
    //                                 Errors
    // ==========================================================================

    error ZeroGateway();
    error VerifierHasNoCode();
    error ZeroVKey();
    error BadCommitteeParams();
    error ZeroRoot();
    error BadScheme(uint8 scheme);
    error UnknownCommittee(uint256 committeeId);
    error BadPublicValues(); // wrong length, not the fixed 192-byte layout
    error CommitteeRootMismatch(bytes32 provenRoot, bytes32 registeredRoot);
    error ThresholdMismatch(uint256 provenThreshold, uint32 registeredThreshold);
    error SizeMismatch(uint256 provenSize, uint32 registeredSize);
    error SchemeMismatch(uint256 provenScheme, uint8 registeredScheme);
    error MessageDigestMismatch(bytes32 provenDigest, bytes32 claimedDigest);
    error BelowThreshold(uint256 provenValidCount, uint32 requiredThreshold);

    // ==========================================================================
    //                               Construction
    // ==========================================================================

    /// @param gateway               the deployed SP1 verifier gateway (chain 2800: 0x9ca4…0628).
    /// @param aggregateProgramVKey  the SP1 vkey of the one aggregation circuit this verifier accepts.
    constructor(address gateway, bytes32 aggregateProgramVKey) {
        if (gateway == address(0)) revert ZeroGateway();
        // A code-less gateway would make verifyProof (which returns no data) succeed silently,
        // accepting every "proof" = post-quantum authorization bypass. Guard it exactly as the
        // sibling AereFinalityCertificateVerifier does. The deployed value must be the live SP1 gateway.
        if (gateway.code.length == 0) revert VerifierHasNoCode();
        if (aggregateProgramVKey == bytes32(0)) revert ZeroVKey();
        SP1_GATEWAY = ISP1Verifier(gateway);
        AGGREGATE_PROGRAM_VKEY = aggregateProgramVKey;
    }

    // ==========================================================================
    //                              Registration
    // ==========================================================================

    /**
     * @notice Register a t-of-n post-quantum committee for constant-cost aggregate verification.
     * @param root      a binding commitment to the exact n authorized PQC public keys (for
     *                  example a Merkle root over the canonically ordered keys, or the keccak256
     *                  of the canonical key list). The SP1 circuit recomputes THIS value from the
     *                  witnessed keys and emits it, so a proof for any other key set yields a
     *                  different root and is rejected. Must be non-zero.
     * @param threshold t, with 1 <= t <= n. The proof must attest at least t distinct valid signatures.
     * @param size      n, the committee size (1 <= n <= 2^32 - 1). Bound into the public values.
     * @param scheme    the committee's PQC scheme (one of SCHEME_*; must be non-zero).
     * @return committeeId the id of the new committee.
     *
     * @dev Permissionless: the registrant gains no privilege. Authority is ONLY the committed key
     *      set behind `root`; there is no admin, no owner override, and no fund custody. The keys
     *      themselves are NOT stored on-chain (storing them would reintroduce the O(n) cost this
     *      design removes); the root is the sole on-chain commitment to them.
     */
    function registerCommittee(bytes32 root, uint32 threshold, uint32 size, uint8 scheme)
        external
        returns (uint256 committeeId)
    {
        if (root == bytes32(0)) revert ZeroRoot();
        if (size == 0 || threshold == 0 || threshold > size) revert BadCommitteeParams();
        if (scheme == 0) revert BadScheme(scheme);

        committeeId = ++committeeCount;
        _committees[committeeId] = Committee({
            exists: true,
            scheme: scheme,
            threshold: threshold,
            size: size,
            registrant: msg.sender,
            root: root
        });

        emit CommitteeRegistered(committeeId, msg.sender, scheme, threshold, size, root);
    }

    // ==========================================================================
    //                          Aggregate verification
    // ==========================================================================

    /**
     * @notice STRICT verification: reverts (fail-closed) unless the aggregate proof attests that a
     *         t-of-n committee signed `messageDigest`. Verification is a SINGLE gateway verifyProof
     *         call; the gas does NOT grow with the committee size n (no per-member work happens
     *         on-chain). Callable as a view by any relying contract.
     *
     * @param committeeId   the registered committee whose root and threshold the proof must match.
     * @param messageDigest the 32-byte message the caller is authorizing; must equal the digest the
     *                      proof's public values bind (the message every counted signature signed).
     * @param publicValues  the SP1 public values, the fixed 192-byte layout documented above.
     * @param proofBytes    the SP1 proof; its first 4 bytes select the gateway route.
     * @return true on success (reverts otherwise).
     */
    function verifyAggregate(
        uint256 committeeId,
        bytes32 messageDigest,
        bytes calldata publicValues,
        bytes calldata proofBytes
    ) public view returns (bool) {
        Committee storage c = _committees[committeeId];
        if (!c.exists) revert UnknownCommittee(committeeId);

        (
            bytes32 provenRoot,
            uint256 provenThreshold,
            uint256 provenSize,
            uint256 provenScheme,
            bytes32 provenDigest,
            uint256 provenValidCount
        ) = _decodePublicValues(publicValues);

        // Bind the proof to THIS committee and to the caller's claimed message.
        if (provenRoot != c.root) revert CommitteeRootMismatch(provenRoot, c.root);
        if (provenThreshold != c.threshold) revert ThresholdMismatch(provenThreshold, c.threshold);
        if (provenSize != c.size) revert SizeMismatch(provenSize, c.size);
        if (provenScheme != uint256(c.scheme)) revert SchemeMismatch(provenScheme, c.scheme);
        if (provenDigest != messageDigest) revert MessageDigestMismatch(provenDigest, messageDigest);
        if (provenValidCount < c.threshold) revert BelowThreshold(provenValidCount, c.threshold);

        // Single on-chain proof check. verifyProof reverts on an invalid proof, so a bad proof
        // reverts the whole call and authorizes nothing. This is the only cryptographic gate and
        // its cost is fixed by the SP1 verifier, independent of the committee size n.
        SP1_GATEWAY.verifyProof(AGGREGATE_PROGRAM_VKEY, publicValues, proofBytes);
        return true;
    }

    /**
     * @notice NON-REVERTING verification: returns true iff `verifyAggregate` would succeed, and
     *         false on ANY failure (unknown committee, malformed public values, a binding mismatch,
     *         below threshold, or an invalid proof). This is the variant ERC-4337 / ERC-7579
     *         validators use, since a validator SHOULD return a failure code rather than revert.
     */
    function tryVerifyAggregate(
        uint256 committeeId,
        bytes32 messageDigest,
        bytes calldata publicValues,
        bytes calldata proofBytes
    ) external view returns (bool) {
        try this.verifyAggregate(committeeId, messageDigest, publicValues, proofBytes) returns (bool ok) {
            return ok;
        } catch {
            return false;
        }
    }

    /**
     * @notice STATE-CHANGING attestation: verify the aggregate (strict, fail-closed) and record an
     *         on-chain provenance entry keyed by (committeeId, messageDigest). Reverts unless the
     *         proof verifies, so a recorded attestation is always backed by a valid proof.
     *
     * @dev Replay note: this contract does NOT itself add nonce-based replay protection. The
     *      `messageDigest` is expected to already be domain and context bound by the caller (for
     *      ERC-4337 the digest is the userOpHash, which carries the account nonce; for a custom
     *      use the caller binds chainId + contract + nonce into the digest, exactly as
     *      AereComputeMarketV3.settlementDigest does). Re-recording the same digest simply overwrites
     *      the same provenance slot; it grants no new authority.
     */
    function recordAggregate(
        uint256 committeeId,
        bytes32 messageDigest,
        bytes calldata publicValues,
        bytes calldata proofBytes
    ) external returns (uint32 validCount) {
        // Strict verify first (reverts fail-closed on any problem).
        verifyAggregate(committeeId, messageDigest, publicValues, proofBytes);

        // Pull the proven count and threshold for the provenance record.
        (, , , , , uint256 provenValidCount) = _decodePublicValues(publicValues);
        validCount = uint32(provenValidCount);
        uint32 threshold = _committees[committeeId].threshold;

        _attestations[_attestationId(committeeId, messageDigest)] =
            Attestation({exists: true, validCount: validCount, blockNumber: uint64(block.number)});

        emit AggregateVerified(committeeId, messageDigest, threshold, validCount, uint64(block.number));
    }

    // ==========================================================================
    //                          Views / decode helpers
    // ==========================================================================

    /// @dev Decode the fixed 192-byte public-values layout. Reverts BadPublicValues on any other
    ///      length; the fixed layout means a correct-length blob always decodes without reverting.
    function _decodePublicValues(bytes calldata publicValues)
        internal
        pure
        returns (
            bytes32 committeeRoot,
            uint256 threshold,
            uint256 committeeSize,
            uint256 scheme,
            bytes32 messageDigest,
            uint256 validCount
        )
    {
        if (publicValues.length != PUBLIC_VALUES_LEN) revert BadPublicValues();
        (committeeRoot, threshold, committeeSize, scheme, messageDigest, validCount) =
            abi.decode(publicValues, (bytes32, uint256, uint256, uint256, bytes32, uint256));
    }

    /// @notice Helper for off-chain tooling and the circuit harness: the exact 192-byte public
    ///         values a proof for `committeeId` authorizing `messageDigest` with `validCount`
    ///         distinct signatures must emit. Reverts on an unknown committee.
    function expectedPublicValues(uint256 committeeId, bytes32 messageDigest, uint256 validCount)
        external
        view
        returns (bytes memory)
    {
        Committee storage c = _committees[committeeId];
        if (!c.exists) revert UnknownCommittee(committeeId);
        return abi.encode(
            c.root, uint256(c.threshold), uint256(c.size), uint256(c.scheme), messageDigest, validCount
        );
    }

    /// @notice Deterministic id of the attestation for (committeeId, messageDigest).
    function _attestationId(uint256 committeeId, bytes32 messageDigest) internal pure returns (bytes32) {
        return keccak256(abi.encode(committeeId, messageDigest));
    }

    function attestationId(uint256 committeeId, bytes32 messageDigest) external pure returns (bytes32) {
        return _attestationId(committeeId, messageDigest);
    }

    /// @notice True iff `committeeId` has been registered.
    function committeeExists(uint256 committeeId) external view returns (bool) {
        return _committees[committeeId].exists;
    }

    function getCommittee(uint256 committeeId)
        external
        view
        returns (bool exists, uint8 scheme, uint32 threshold, uint32 size, address registrant, bytes32 root)
    {
        Committee storage c = _committees[committeeId];
        return (c.exists, c.scheme, c.threshold, c.size, c.registrant, c.root);
    }

    /// @notice Read a recorded attestation (provenance) for (committeeId, messageDigest).
    function getAttestation(uint256 committeeId, bytes32 messageDigest)
        external
        view
        returns (bool exists, uint32 validCount, uint64 blockNumber)
    {
        Attestation storage a = _attestations[_attestationId(committeeId, messageDigest)];
        return (a.exists, a.validCount, a.blockNumber);
    }

    /// @notice True iff a valid aggregate has been recorded for (committeeId, messageDigest).
    function isAttested(uint256 committeeId, bytes32 messageDigest) external view returns (bool) {
        return _attestations[_attestationId(committeeId, messageDigest)].exists;
    }
}
