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

import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";

/**
 * @title AereConfidentialCompute, on-chain verifier for the AERE threshold-MPC
 *        confidential-compute layer (chain 2800).
 *
 * @notice HONEST SCOPE. This contract does NOT run any MPC. The confidential
 *         computation happens OFF chain, in a committee of reference nodes that
 *         jointly evaluate an agreed arithmetic circuit over the parties' PRIVATE
 *         inputs using real Shamir secret sharing and a real BGW multiplication
 *         gate (see foundation-ops/mpc-committee). No single node, and no
 *         colluding minority below the reconstruction threshold, ever learns any
 *         party's input; only the agreed public outputs are revealed. This
 *         contract's job is narrow and verifiable: it records a session's public
 *         outputs only if a quorum of the REGISTERED committee members has
 *         ECDSA-signed the canonical result. The chain does NOT re-execute the
 *         MPC and does NOT verify the confidentiality of the inputs; the
 *         confidentiality is provided by the secret sharing off chain. The chain
 *         verifies committee agreement on the output.
 *
 *         THREAT MODEL of the off-chain MPC: SEMI-HONEST (honest but curious),
 *         secure against a colluding minority of nodes. It is NOT malicious
 *         secure: there is no SPDX-MAC / cheater detection, so a deviating node
 *         is not cryptographically caught by the protocol. The on-chain quorum of
 *         signatures is the trust anchor: a result is accepted only if at least
 *         `signatureThreshold` distinct registered members signed it.
 *
 *         CENTRALIZATION. The committee is Foundation-run initially, so it is as
 *         centralized as AERE consensus is today, and it decentralizes as the
 *         committee grows. The owner can register or remove committee signing
 *         keys and set the quorum threshold. The owner CANNOT forge a session
 *         result: there is no function that records outputs without a real quorum
 *         of committee signatures, and the owner cannot produce those signatures
 *         for keys it does not hold. Once recorded, a session result is immutable.
 *
 *         SUPPORTED CIRCUITS. This is a confidential-compute PRIMITIVE (private
 *         arithmetic tally over a fixed agreed circuit, for example a confidential
 *         quadratic-voting / funding tally that reveals only sum(v) and sum(v^2)).
 *         It is NOT a general-purpose confidential VM or fhEVM. `circuitId` binds
 *         the signed result to the exact circuit that was computed.
 *
 *         TEE PATH NOT BUILT. A hardware-attested (SEV-SNP / TDX / SGX) execution
 *         path was intentionally not built: the AERE server is a
 *         cloud guest with no usable trusted-execution attestation. The security
 *         here is pure-software secret-sharing MPC, which needs no trusted
 *         hardware.
 */
contract AereConfidentialCompute {
    using ECDSA for bytes32;

    // ----- ownership -----
    address public owner;

    // ----- committee registry -----
    uint256 public committeeCount;
    mapping(address => bool) public isCommittee;
    address[] private committeeList;

    // Minimum number of distinct registered committee signatures required to
    // finalize a session. Set by the owner; must satisfy 1 <= threshold <= count.
    uint256 public signatureThreshold;

    // ----- EIP-712 -----
    bytes32 public immutable DOMAIN_SEPARATOR;
    bytes32 public constant EIP712DOMAIN_TYPEHASH =
        keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");
    bytes32 public constant SESSIONRESULT_TYPEHASH =
        keccak256("SessionResult(uint256 sessionId,bytes32 circuitId,uint256[] outputs)");

    // ----- session results -----
    struct Session {
        bool exists;
        bytes32 circuitId;
        bytes32 outputsHash; // keccak256(abi.encodePacked(outputs))
        uint32 numOutputs;
        uint32 numSigners;
        uint256 finalizedAtBlock;
    }
    mapping(uint256 => Session) public sessions;
    mapping(uint256 => uint256[]) private sessionOutputs;

    // ----- optional input commitments (audit trail, inputs stay private) -----
    // sessionId => partyId => commitment (e.g. keccak256(input || salt)). A party
    // may post its commitment BEFORE the result is finalized, so the flow is
    // tamper-evident without revealing the input. Commitments are write-once.
    mapping(uint256 => mapping(bytes32 => bytes32)) public inputCommitment;
    mapping(uint256 => uint256) public inputCommitmentCount;

    // ----- events -----
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
    event CommitteeMemberRegistered(address indexed member, uint256 committeeCount);
    event CommitteeMemberRemoved(address indexed member, uint256 committeeCount);
    event SignatureThresholdSet(uint256 threshold, uint256 committeeCount);
    event InputCommitted(uint256 indexed sessionId, bytes32 indexed partyId, address indexed committer, bytes32 commitment);
    event SessionFinalized(
        uint256 indexed sessionId,
        bytes32 indexed circuitId,
        bytes32 outputsHash,
        uint256 numOutputs,
        uint256 numSigners,
        uint256 threshold
    );

    modifier onlyOwner() {
        require(msg.sender == owner, "not owner");
        _;
    }

    constructor() {
        owner = msg.sender;
        emit OwnershipTransferred(address(0), msg.sender);
        DOMAIN_SEPARATOR = keccak256(
            abi.encode(
                EIP712DOMAIN_TYPEHASH,
                keccak256(bytes("AereConfidentialCompute")),
                keccak256(bytes("1")),
                block.chainid,
                address(this)
            )
        );
    }

    // ==========================================================================
    //                       Foundation: committee registry
    // ==========================================================================

    function registerCommitteeMember(address member) public onlyOwner {
        require(member != address(0), "member=0");
        require(!isCommittee[member], "already registered");
        isCommittee[member] = true;
        committeeList.push(member);
        committeeCount += 1;
        emit CommitteeMemberRegistered(member, committeeCount);
    }

    function registerCommittee(address[] calldata members) external onlyOwner {
        for (uint256 i = 0; i < members.length; i++) {
            registerCommitteeMember(members[i]);
        }
    }

    function removeCommitteeMember(address member) external onlyOwner {
        require(isCommittee[member], "not registered");
        isCommittee[member] = false;
        committeeCount -= 1;
        // compact the enumeration array
        uint256 len = committeeList.length;
        for (uint256 i = 0; i < len; i++) {
            if (committeeList[i] == member) {
                committeeList[i] = committeeList[len - 1];
                committeeList.pop();
                break;
            }
        }
        if (signatureThreshold > committeeCount) signatureThreshold = committeeCount;
        emit CommitteeMemberRemoved(member, committeeCount);
    }

    /// @notice Set the quorum: how many distinct registered committee signatures a
    ///         session result must carry. Must be at least 1 and at most the
    ///         committee size. For a t-of-n MPC committee a sensible quorum is the
    ///         reconstruction threshold or higher.
    function setSignatureThreshold(uint256 threshold) external onlyOwner {
        require(threshold >= 1, "threshold < 1");
        require(threshold <= committeeCount, "threshold > committee");
        signatureThreshold = threshold;
        emit SignatureThresholdSet(threshold, committeeCount);
    }

    function transferOwnership(address newOwner) external onlyOwner {
        require(newOwner != address(0), "newOwner=0");
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }

    // ==========================================================================
    //                    Parties: optional input commitments
    // ==========================================================================

    /// @notice Post a write-once commitment to a private input before the session
    ///         is finalized. The input itself is never revealed on chain; the
    ///         commitment gives a tamper-evident audit trail. Permissionless: any
    ///         party may commit for its own partyId.
    function commitInput(uint256 sessionId, bytes32 partyId, bytes32 commitment) external {
        require(!sessions[sessionId].exists, "session finalized");
        require(commitment != bytes32(0), "commitment=0");
        require(inputCommitment[sessionId][partyId] == bytes32(0), "already committed");
        inputCommitment[sessionId][partyId] = commitment;
        inputCommitmentCount[sessionId] += 1;
        emit InputCommitted(sessionId, partyId, msg.sender, commitment);
    }

    // ==========================================================================
    //                 Anyone (relayer): submit a signed result
    // ==========================================================================

    /// @notice Finalize a confidential-compute session. Records `outputs` for
    ///         `sessionId` iff at least `signatureThreshold` DISTINCT registered
    ///         committee members signed the canonical EIP-712 SessionResult over
    ///         (sessionId, circuitId, outputs). msg.sender is only a gas-paying
    ///         relayer and gains no authority: it cannot forge committee
    ///         signatures, and there is no owner backdoor that records a result
    ///         without them.
    /// @param signatures 65-byte ECDSA signatures, ORDERED BY SIGNER ADDRESS
    ///        STRICTLY ASCENDING (this both de-duplicates signers cheaply and
    ///        makes the quorum count unambiguous).
    function submitSession(
        uint256 sessionId,
        bytes32 circuitId,
        uint256[] calldata outputs,
        bytes[] calldata signatures
    ) external {
        require(committeeCount > 0, "no committee");
        require(signatureThreshold > 0, "threshold unset");
        require(!sessions[sessionId].exists, "already finalized");
        require(signatures.length >= signatureThreshold, "insufficient signatures");
        require(outputs.length <= type(uint32).max, "too many outputs");

        bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
        bytes32 structHash = keccak256(
            abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
        );
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));

        // Count distinct registered signers. Requiring strictly ascending signer
        // addresses rejects duplicates (a repeated key cannot inflate the quorum)
        // and any non-committee (forged) signature fails the isCommittee check.
        address last = address(0);
        uint256 valid = 0;
        for (uint256 i = 0; i < signatures.length; i++) {
            address signer = digest.recover(signatures[i]);
            require(signer > last, "sigs unsorted or duplicate");
            require(isCommittee[signer], "signer not committee");
            last = signer;
            valid += 1;
        }
        require(valid >= signatureThreshold, "quorum not met");

        sessions[sessionId] = Session({
            exists: true,
            circuitId: circuitId,
            outputsHash: outputsHash,
            numOutputs: uint32(outputs.length),
            numSigners: uint32(valid),
            finalizedAtBlock: block.number
        });
        sessionOutputs[sessionId] = outputs;

        emit SessionFinalized(sessionId, circuitId, outputsHash, outputs.length, valid, signatureThreshold);
    }

    // ==========================================================================
    //                                  Views
    // ==========================================================================

    /// @notice The EIP-712 digest for a result (handy for off-chain signers/tests).
    function digestFor(uint256 sessionId, bytes32 circuitId, uint256[] calldata outputs)
        external
        view
        returns (bytes32)
    {
        bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
        bytes32 structHash = keccak256(
            abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
        );
        return keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
    }

    function isFinalized(uint256 sessionId) external view returns (bool) {
        return sessions[sessionId].exists;
    }

    function getOutputs(uint256 sessionId) external view returns (uint256[] memory) {
        require(sessions[sessionId].exists, "not finalized");
        return sessionOutputs[sessionId];
    }

    function getCommittee() external view returns (address[] memory) {
        return committeeList;
    }

    /// @notice Recover the signer of a signature over a session digest. View
    ///         helper used by the adversarial harness to show forged (non
    ///         committee) signatures are rejected.
    function recoverSigner(
        uint256 sessionId,
        bytes32 circuitId,
        uint256[] calldata outputs,
        bytes calldata signature
    ) external view returns (address) {
        bytes32 outputsHash = keccak256(abi.encodePacked(outputs));
        bytes32 structHash = keccak256(
            abi.encode(SESSIONRESULT_TYPEHASH, sessionId, circuitId, outputsHash)
        );
        bytes32 digest = keccak256(abi.encodePacked("\x19\x01", DOMAIN_SEPARATOR, structHash));
        return digest.recover(signature);
    }
}
