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

/**
 * @title AereDACommitteeV2, M-of-N data availability attestation committee (validium-grade)
 * @notice Bug-fix redeploy of AereDACommittee. Same validium-grade M-of-N model:
 *         N member keys are registered; committee members sign availability
 *         attestations off-chain over (rollupId, epoch, dataCommitment); the
 *         contract records an (rollupId, epoch) as data-available only when at
 *         least M distinct registered members present valid ECDSA signatures over
 *         that tuple.
 *
 *         ─────────────────────────── WHY V2 ───────────────────────────
 *         V1 (0xC2a0e6f38084A08CcA768C01af3bFB52C5e99A43) had a FAIL-OPEN hole:
 *           - removeMember() clamped `threshold = memberCount` on the way down, so
 *             an admin removing ALL members drove threshold to 0.
 *           - attest() then accepted an EMPTY signature set: valid(0) < threshold(0)
 *             is false, so `attest(rollupId, epoch, dc, [])` recorded availability
 *             with ZERO committee signatures — breaking the documented "no admin
 *             backdoor to forge availability" invariant.
 *
 *         V2 fixes:
 *           (1) INVARIANT memberCount >= threshold >= 1 is preserved everywhere.
 *               threshold can never be driven to 0.
 *           (2) removeMember() REVERTS (WouldBreakThreshold) if removing the member
 *               would leave memberCount < threshold. The admin must first LOWER the
 *               threshold explicitly via setThreshold (which still forbids 0), and
 *               can never remove the last member of a threshold-1 committee.
 *           (3) attest() REQUIRES sigs.length > 0 (EmptySignatureSet) in addition to
 *               valid >= threshold. Belt-and-suspenders against any future path to
 *               a zero threshold.
 *
 *         Everything else is byte-for-byte the same behavior as V1: M distinct,
 *         strictly-ascending-recovered-address member signatures; AlreadyRecorded
 *         idempotency; the same EIP-191 attestation digest.
 *
 * @dev    HONEST SCOPE. This is VALIDIUM-GRADE data availability: a TRUSTED
 *         committee attests that data is available. It is NOT trustless data
 *         availability sampling (DAS) and provides NO cryptographic guarantee
 *         that the data actually exists off-chain. Availability is exactly as
 *         trustworthy as the committee is honest and live. It is "as centralized
 *         as the committee." There is NO admin backdoor to forge availability: the
 *         ONLY path that can set an epoch available is `attest`, which strictly
 *         requires >= M distinct registered-member signatures with M >= 1.
 */
contract AereDACommitteeV2 {
    /* --------------------------------- roles -------------------------------- */

    /// @notice Committee registrar. Can add/remove members and set the threshold,
    ///         or freeze membership permanently. Cannot forge availability.
    address public admin;

    /// @notice Once true, membership and threshold are immutable forever.
    bool public frozen;

    /* -------------------------------- committee ----------------------------- */

    uint256 public threshold;   // M  (invariant: memberCount >= threshold >= 1)
    uint256 public memberCount; // N
    mapping(address => bool) public isMember;
    address[] public members;

    /* ------------------------------- attestations --------------------------- */

    struct Attestation {
        bool available;
        bytes32 dataCommitment;
        uint64 attestedAt;
        uint256 signatures;
    }

    // rollupId => epoch => Attestation
    mapping(bytes32 => mapping(uint256 => Attestation)) private _att;

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

    event AdminTransferred(address indexed prev, address indexed next);
    event MemberAdded(address indexed member);
    event MemberRemoved(address indexed member);
    event ThresholdSet(uint256 threshold);
    event Frozen();
    event DataAvailable(
        bytes32 indexed rollupId,
        uint256 indexed epoch,
        bytes32 dataCommitment,
        uint256 signatures
    );

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

    error NotAdmin();
    error IsFrozen();
    error ZeroAddress();
    error AlreadyMember();
    error NotMember();
    error BadThreshold();
    error WouldBreakThreshold(uint256 newMemberCount, uint256 threshold); // V2
    error EmptySignatureSet();                                            // V2
    error AlreadyRecorded();
    error ThresholdNotMet(uint256 got, uint256 need);
    error UnsortedOrDuplicate();
    error NotACommitteeMember(address recovered);

    modifier onlyAdmin() {
        if (msg.sender != admin) revert NotAdmin();
        _;
    }

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

    constructor(address admin_, address[] memory members_, uint256 threshold_) {
        if (admin_ == address(0)) revert ZeroAddress();
        admin = admin_;
        emit AdminTransferred(address(0), admin_);

        for (uint256 i = 0; i < members_.length; i++) {
            address m = members_[i];
            if (m == address(0)) revert ZeroAddress();
            if (isMember[m]) revert AlreadyMember();
            isMember[m] = true;
            members.push(m);
            emit MemberAdded(m);
        }
        memberCount = members_.length;

        // INVARIANT: memberCount >= threshold >= 1
        if (threshold_ == 0 || threshold_ > memberCount) revert BadThreshold();
        threshold = threshold_;
        emit ThresholdSet(threshold_);
    }

    /* ------------------------ committee governance -------------------------- */
    /*   NOTE: none of these can mark data available. They only manage the      */
    /*   member set / threshold, and are disabled forever once frozen.          */

    function addMember(address m) external onlyAdmin {
        if (frozen) revert IsFrozen();
        if (m == address(0)) revert ZeroAddress();
        if (isMember[m]) revert AlreadyMember();
        isMember[m] = true;
        members.push(m);
        memberCount++;
        emit MemberAdded(m);
    }

    /// @notice Remove a committee member. V2 FIX: reverts if the removal would
    ///         leave memberCount < threshold (which in V1 silently clamped the
    ///         threshold, all the way to 0 when the last member was removed). The
    ///         admin must LOWER the threshold explicitly (setThreshold, min 1)
    ///         before shrinking the committee below the current threshold. The
    ///         last member of a threshold-1 committee can never be removed, so
    ///         threshold can never reach 0.
    function removeMember(address m) external onlyAdmin {
        if (frozen) revert IsFrozen();
        if (!isMember[m]) revert NotMember();

        uint256 newCount = memberCount - 1;
        if (newCount < threshold) revert WouldBreakThreshold(newCount, threshold);

        isMember[m] = false;
        uint256 n = members.length;
        for (uint256 i = 0; i < n; i++) {
            if (members[i] == m) {
                members[i] = members[n - 1];
                members.pop();
                break;
            }
        }
        memberCount = newCount;
        emit MemberRemoved(m);
    }

    function setThreshold(uint256 t) external onlyAdmin {
        if (frozen) revert IsFrozen();
        // INVARIANT: memberCount >= threshold >= 1
        if (t == 0 || t > memberCount) revert BadThreshold();
        threshold = t;
        emit ThresholdSet(t);
    }

    function transferAdmin(address next) external onlyAdmin {
        if (next == address(0)) revert ZeroAddress();
        emit AdminTransferred(admin, next);
        admin = next;
    }

    /// @notice Permanently renounce all membership/threshold control. After this,
    ///         the committee is immutable and only `attest` can act.
    function freeze() external onlyAdmin {
        frozen = true;
        emit Frozen();
    }

    /* ------------------------------ attestation ----------------------------- */

    /// @notice The EIP-191 personal-sign digest committee members sign off-chain.
    function attestationDigest(
        bytes32 rollupId,
        uint256 epoch,
        bytes32 dataCommitment
    ) public view returns (bytes32) {
        bytes32 h = keccak256(
            abi.encode("AereDACommittee:attest", block.chainid, address(this), rollupId, epoch, dataCommitment)
        );
        return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", h));
    }

    /// @notice Record (rollupId, epoch) as data-available. The ONLY path to
    ///         availability. Requires a NON-EMPTY set of >= M distinct
    ///         registered-member signatures over (rollupId, epoch, dataCommitment).
    ///         Signatures MUST be ordered by strictly ascending recovered signer
    ///         address, which guarantees distinctness without an in-memory set.
    function attest(
        bytes32 rollupId,
        uint256 epoch,
        bytes32 dataCommitment,
        bytes[] calldata sigs
    ) external returns (bool) {
        // V2 FIX: never accept an empty signature set, independent of threshold.
        if (sigs.length == 0) revert EmptySignatureSet();

        Attestation storage a = _att[rollupId][epoch];
        if (a.available) revert AlreadyRecorded();

        bytes32 digest = attestationDigest(rollupId, epoch, dataCommitment);

        uint256 valid;
        address last = address(0);
        for (uint256 i = 0; i < sigs.length; i++) {
            address rec = _recover(digest, sigs[i]);
            if (rec == address(0) || !isMember[rec]) revert NotACommitteeMember(rec);
            if (rec <= last) revert UnsortedOrDuplicate(); // strictly ascending => distinct
            last = rec;
            valid++;
        }
        if (valid < threshold) revert ThresholdNotMet(valid, threshold);

        a.available = true;
        a.dataCommitment = dataCommitment;
        a.attestedAt = uint64(block.timestamp);
        a.signatures = valid;

        emit DataAvailable(rollupId, epoch, dataCommitment, valid);
        return true;
    }

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

    function isAvailable(bytes32 rollupId, uint256 epoch) external view returns (bool) {
        return _att[rollupId][epoch].available;
    }

    function getAttestation(bytes32 rollupId, uint256 epoch)
        external
        view
        returns (bool available, bytes32 dataCommitment, uint64 attestedAt, uint256 signatures)
    {
        Attestation storage a = _att[rollupId][epoch];
        return (a.available, a.dataCommitment, a.attestedAt, a.signatures);
    }

    function getMembers() external view returns (address[] memory) {
        return members;
    }

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

    function _recover(bytes32 hash, bytes calldata sig) internal pure returns (address) {
        if (sig.length != 65) return address(0);
        bytes32 r;
        bytes32 s;
        uint8 v;
        assembly {
            r := calldataload(sig.offset)
            s := calldataload(add(sig.offset, 32))
            v := byte(0, calldataload(add(sig.offset, 64)))
        }
        if (v < 27) v += 27;
        if (v != 27 && v != 28) return address(0);
        return ecrecover(hash, v, r, s);
    }
}
