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

import "@openzeppelin/contracts/access/Ownable.sol";

/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
///      NOTHING and REVERTS on an invalid proof.
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereProofAggregator: on-chain anchor for RECURSIVE SP1 proof aggregation
 * @notice Verifies a single SP1 Groth16 "proof of proofs": the aggregation guest
 *         recursively verified N inner SP1 proofs (each from a real, distinct AERE
 *         production program on chain 2800) inside the zkVM via
 *         `verify_sp1_proof`, and committed one composite digest binding all N
 *         (vkey, publicValues) pairs. This contract verifies that Groth16 proof
 *         through the SP1 gateway, recomputes the composite from the supplied
 *         inner data to bind it to the proof, and records the aggregation.
 *
 *         COMPOSITE (recomputed here byte-for-byte with the guest):
 *           composite = sha256(
 *               uint32_be(n)
 *               || (innerVKeyDigest_0 || innerPvHash_0)
 *               || ...
 *               || (innerVKeyDigest_{n-1} || innerPvHash_{n-1})
 *           )
 *         where
 *           innerVKeyDigest_i : the KoalaBear (SP1Field) verifying-key digest the
 *                               recursion verifier uses (SP1VerifyingKey::hash_u32,
 *                               big-endian). This is what `verify_sp1_proof`
 *                               actually checked, so binding it here is maximally
 *                               sound.
 *           innerPvHash_i     : sha256 of inner proof i's public values.
 *
 *         Because the on-chain (BN254) vkey and the KoalaBear digest are two
 *         different encodings of the same key, the Foundation registers the
 *         mapping (registerInnerProgram) so each folded program can be labelled
 *         and cross-checked against its published on-chain vkey.
 *
 *         The aggregation program itself is registered by the Foundation
 *         (registerAggProgram) and is the only vkey routed to the gateway.
 *
 *         IMMUTABILITY: SP1_VERIFIER is fixed at deploy. Recorded aggregations
 *         cannot be altered or removed.
 */
contract AereProofAggregator is Ownable {
    address public immutable SP1_VERIFIER;

    /// @notice Registered aggregation-program vkeys (BN254 bytes32) allowed to be
    ///         routed to the gateway, with a human description.
    mapping(bytes32 => bool) public isAggProgram;
    mapping(bytes32 => string) public aggProgramDesc;

    struct InnerProgram {
        bool known;
        bytes32 onChainVKey; // BN254 bytes32 published for this program
        string label;        // e.g. "zkscreen", "over18", "zkml-mnist"
    }

    /// @notice KoalaBear inner vkey digest => registered program metadata.
    mapping(bytes32 => InnerProgram) public innerPrograms;

    struct Aggregation {
        bool exists;
        bytes32 aggVKey;
        uint32 count;
        uint32 recognized;
        uint64 at;
        address submitter;
    }

    /// @notice composite digest => recorded aggregation.
    mapping(bytes32 => Aggregation) public aggregations;
    uint256 public aggregationCount;

    /* --------------------------------- events ------------------------------- */

    event AggProgramRegistered(bytes32 indexed aggVKey, string description);
    event InnerProgramRegistered(bytes32 indexed innerVKeyDigest, bytes32 onChainVKey, string label);
    event AggregationVerified(
        bytes32 indexed composite,
        bytes32 indexed aggVKey,
        uint32 count,
        uint32 recognized,
        bytes32[] innerVKeyDigests,
        address indexed submitter,
        uint256 at
    );

    /* --------------------------------- errors ------------------------------- */

    error ZeroAddress();
    error UnknownAggProgram(bytes32 aggVKey);
    error LengthMismatch(uint256 vkeys, uint256 pvHashes);
    error EmptyAggregation();
    error BadPublicValuesLength(uint256 got);
    error CompositeMismatch(bytes32 recomputed, bytes32 committed);
    error InvalidProof();

    /* ----------------------------- constructor ------------------------------ */

    constructor(address sp1Verifier) {
        if (sp1Verifier == address(0)) revert ZeroAddress();
        SP1_VERIFIER = sp1Verifier;
    }

    /* ------------------------------ governance ------------------------------ */

    function registerAggProgram(bytes32 aggVKey, string calldata description) external onlyOwner {
        isAggProgram[aggVKey] = true;
        aggProgramDesc[aggVKey] = description;
        emit AggProgramRegistered(aggVKey, description);
    }

    function registerInnerProgram(
        bytes32 innerVKeyDigest,
        bytes32 onChainVKey,
        string calldata label
    ) external onlyOwner {
        innerPrograms[innerVKeyDigest] = InnerProgram({
            known: true,
            onChainVKey: onChainVKey,
            label: label
        });
        emit InnerProgramRegistered(innerVKeyDigest, onChainVKey, label);
    }

    /* --------------------------- record aggregation ------------------------- */

    /// @notice Verify a recursive aggregation Groth16 proof and record it.
    /// @param aggVKey            registered aggregation-program vkey (BN254 bytes32)
    /// @param innerVKeyDigests   KoalaBear vkey digests of the folded inner proofs
    /// @param innerPvHashes      sha256 of each inner proof's public values
    /// @param publicValues       the aggregation proof's committed 32-byte composite
    /// @param proof              the SP1 Groth16 proof bytes (selector 0x4388a21c)
    /// @return composite         the verified composite digest
    function recordAggregation(
        bytes32 aggVKey,
        bytes32[] calldata innerVKeyDigests,
        bytes32[] calldata innerPvHashes,
        bytes calldata publicValues,
        bytes calldata proof
    ) external returns (bytes32 composite) {
        if (!isAggProgram[aggVKey]) revert UnknownAggProgram(aggVKey);
        uint256 n = innerVKeyDigests.length;
        if (n != innerPvHashes.length) revert LengthMismatch(n, innerPvHashes.length);
        if (n == 0) revert EmptyAggregation();
        if (publicValues.length != 32) revert BadPublicValuesLength(publicValues.length);

        // recompute the composite exactly as the aggregation guest committed it.
        composite = _composite(innerVKeyDigests, innerPvHashes);

        bytes32 committed;
        assembly {
            committed := calldataload(publicValues.offset)
        }
        if (composite != committed) revert CompositeMismatch(composite, committed);

        // verify the Groth16 proof of proofs through the SP1 gateway.
        try ISp1Verifier(SP1_VERIFIER).verifyProof(aggVKey, publicValues, proof) {
            // verified: did not revert
        } catch {
            revert InvalidProof();
        }

        // count how many folded inner programs are registered/recognized.
        uint32 recognized;
        for (uint256 i; i < n; ++i) {
            if (innerPrograms[innerVKeyDigests[i]].known) recognized++;
        }

        aggregations[composite] = Aggregation({
            exists: true,
            aggVKey: aggVKey,
            count: uint32(n),
            recognized: recognized,
            at: uint64(block.timestamp),
            submitter: msg.sender
        });
        aggregationCount += 1;

        emit AggregationVerified(
            composite,
            aggVKey,
            uint32(n),
            recognized,
            innerVKeyDigests,
            msg.sender,
            block.timestamp
        );
    }

    /* ----------------------------------- views ------------------------------ */

    /// @notice Stateless verification: recomputes the composite, checks it equals
    ///         the committed public values, and verifies the Groth16 proof.
    ///         Reverts if anything is invalid; records nothing.
    function verifyAggregation(
        bytes32 aggVKey,
        bytes32[] calldata innerVKeyDigests,
        bytes32[] calldata innerPvHashes,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view returns (bytes32 composite) {
        if (!isAggProgram[aggVKey]) revert UnknownAggProgram(aggVKey);
        uint256 n = innerVKeyDigests.length;
        if (n != innerPvHashes.length) revert LengthMismatch(n, innerPvHashes.length);
        if (n == 0) revert EmptyAggregation();
        if (publicValues.length != 32) revert BadPublicValuesLength(publicValues.length);

        composite = _composite(innerVKeyDigests, innerPvHashes);
        bytes32 committed;
        assembly {
            committed := calldataload(publicValues.offset)
        }
        if (composite != committed) revert CompositeMismatch(composite, committed);

        ISp1Verifier(SP1_VERIFIER).verifyProof(aggVKey, publicValues, proof);
    }

    function getAggregation(bytes32 composite)
        external
        view
        returns (bool exists, bytes32 aggVKey, uint32 count, uint32 recognized, uint64 at, address submitter)
    {
        Aggregation memory a = aggregations[composite];
        return (a.exists, a.aggVKey, a.count, a.recognized, a.at, a.submitter);
    }

    /* ---------------------------------- internal ---------------------------- */

    function _composite(bytes32[] calldata innerVKeyDigests, bytes32[] calldata innerPvHashes)
        internal
        pure
        returns (bytes32)
    {
        uint256 n = innerVKeyDigests.length;
        bytes memory packed = abi.encodePacked(uint32(n));
        for (uint256 i; i < n; ++i) {
            packed = bytes.concat(packed, innerVKeyDigests[i], innerPvHashes[i]);
        }
        return sha256(packed);
    }
}
