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

/**
 * @title AereThresholdRegistry, an on-chain registry of t-of-n MPC/TSS signing
 *        committees for threshold ECDSA custody (AERE chain 2800)
 *
 * @notice The on-chain anchor a non-custodial custody committee (e.g. a Bank28 vault)
 *         uses to PUBLISH the composition of a t-of-n threshold-ECDSA signing group and
 *         to let any relying party VERIFY that a submitted signature was produced under
 *         that group's key. A committee is `{ members[], threshold t, groupPubKey }`.
 *         The group public key is a single secp256k1 point produced by an off-chain
 *         Distributed Key Generation (DKG); no single party ever holds the whole private
 *         key. On-chain we verify a signature under the group key with `ecrecover`, the
 *         same primitive that secures every EOA on this chain.
 *
 *         Lifecycle covered:
 *           - registerCommittee: publish members, t, and the DKG group key.
 *           - verifyThresholdSignature / isValidGroupSignature: relying-party checks.
 *           - reshareCommittee: change membership/threshold, SAME group key (a proactive
 *             resharing / refresh), authorized by a threshold signature of the CURRENT
 *             committee over the exact reshare intent.
 *           - rotateKey: move to a NEW group key (post-compromise recovery), authorized by
 *             a threshold signature of the current committee.
 *           - slashMember: record misbehaviour of a named member, authorized by a
 *             threshold signature of the committee (or by an optional guardian).
 *           - guardian pause/unpause for emergency freeze.
 *
 * @dev    HONEST SCOPE, read this before trusting anything.
 *
 *         1. WHAT ecrecover PROVES, AND WHAT IT DOES NOT. On-chain, a correctly produced
 *            t-of-n threshold-ECDSA signature is byte-for-byte INDISTINGUISHABLE from an
 *            ordinary single-key ECDSA signature: both are (r,s,v) that recover to the same
 *            address. Therefore this contract proves *authenticity under the group key*, it
 *            can NOT and does NOT prove that at least t distinct members actually
 *            participated. The "t-of-n" guarantee lives ENTIRELY in the off-chain DKG +
 *            signing protocol (no party ever reconstructs the full key). If someone
 *            reconstructs the group secret and signs alone, this contract would still
 *            accept it. That is a property of threshold ECDSA on the EVM in general, not a
 *            weakness unique to this contract. Do not read on-chain verification as a proof
 *            of live t-participation.
 *
 *         2. NOT POST-QUANTUM. The group key is classical secp256k1 ECDSA, verified by the
 *            chain's classical `ecrecover`. This contract has NOTHING to do with AERE's
 *            live PQC precompiles, and AERE consensus itself is classical ECDSA QBFT.
 *            Threshold PQC (threshold ML-DSA / Falcon) is an open research area and is NOT
 *            implemented here; see docs THRESHOLD_PQC.md for the honest maturity status.
 *
 *         3. CUSTODY. This registry holds NO funds. It is metadata + verification only. It
 *            never has custody of any key or asset and has no payable path and no
 *            selfdestruct. A vault contract would CONSUME this registry (call
 *            verifyThresholdSignature) to gate a withdrawal; that vault, not this, holds
 *            value.
 *
 * @dev    AUTHORIZATION MODEL. Governance actions (reshare / rotate / slash) are authorized
 *         by the committee ITSELF: the caller must supply a valid threshold ECDSA signature
 *         by the CURRENT group over a domain-separated intent hash binding
 *         {chainId, this contract, committeeId, epoch, actionNonce, action params}. This is
 *         replay-safe across chains, committees, epochs, and repeated actions. An optional
 *         `guardian` address (set at registration, may be zero) can additionally pause the
 *         committee and slash members, as a break-glass control; the guardian can never
 *         move the key or forge a signature.
 */
contract AereThresholdRegistry {
    // ==========================================================================
    //                                  Types
    // ==========================================================================

    enum Status {
        NONE, // 0: unused id
        ACTIVE, // 1: usable
        PAUSED, // 2: frozen by guardian, verification returns false
        RETIRED // 3: terminal, superseded or shut down
    }

    struct Committee {
        Status status;
        uint8 threshold; // t: signatures required off-chain to sign
        uint8 size; // n: number of members
        uint64 epoch; // increments on every reshare/rotate
        uint64 actionNonce; // increments on every governance action (replay guard)
        address groupAddress; // = address(keccak256(groupPubKey)[12:]) for ecrecover
        address guardian; // optional break-glass (0 = none)
        bytes32 membersHash; // keccak256(abi.encode(members)) integrity commitment
        bytes groupPubKey; // 64-byte uncompressed secp256k1 point (x||y), no 0x04 prefix
    }

    // secp256k1 group order; s in the LOWER half prevents signature malleability.
    uint256 internal constant SECP256K1_N =
        0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141;
    uint256 internal constant SECP256K1_N_HALF =
        0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0;

    // Domain tags for the governance intent hash (kept distinct so a reshare auth can
    // never be replayed as a rotate auth or a slash auth).
    bytes32 internal constant ACTION_RESHARE = keccak256("AereThresholdRegistry.reshare.v1");
    bytes32 internal constant ACTION_ROTATE = keccak256("AereThresholdRegistry.rotateKey.v1");
    bytes32 internal constant ACTION_SLASH = keccak256("AereThresholdRegistry.slashMember.v1");

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

    uint256 public committeeCount;
    mapping(uint256 => Committee) private _committees;
    // committeeId => member address => index+1 (0 = not a member)
    mapping(uint256 => mapping(address => uint256)) private _memberIndex1;
    // committeeId => member address => slashed?
    mapping(uint256 => mapping(address => bool)) public slashed;
    // committeeId => ordered member list
    mapping(uint256 => address[]) private _members;

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

    event CommitteeRegistered(
        uint256 indexed committeeId,
        address indexed groupAddress,
        uint8 threshold,
        uint8 size,
        address guardian
    );
    event CommitteeReshared(
        uint256 indexed committeeId, uint64 indexed epoch, uint8 threshold, uint8 size, bytes32 membersHash
    );
    event KeyRotated(
        uint256 indexed committeeId, uint64 indexed epoch, address indexed newGroupAddress, uint8 threshold, uint8 size
    );
    event MemberSlashed(uint256 indexed committeeId, address indexed member, bytes32 evidenceHash, address reporter);
    event CommitteePaused(uint256 indexed committeeId, address indexed by);
    event CommitteeUnpaused(uint256 indexed committeeId, address indexed by);
    event CommitteeRetired(uint256 indexed committeeId, address indexed by);

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

    /**
     * @notice Publish a new t-of-n committee.
     * @param members     Ordered, unique, non-zero member identity addresses (n = length).
     * @param threshold   t, with 1 <= t <= n. Off-chain the protocol needs >= t signers.
     * @param groupPubKey 64-byte uncompressed secp256k1 group key (x||y), no 0x04 prefix.
     * @param guardian    Optional break-glass address (0 for none).
     * @return committeeId The id of the new committee.
     *
     * @dev The registrant does NOT become privileged: all later governance is by threshold
     *      signature of the committee (plus the optional guardian). We deliberately do not
     *      keep an "admin" EOA, so the on-chain trust model matches the off-chain one.
     */
    function registerCommittee(
        address[] calldata members,
        uint8 threshold,
        bytes calldata groupPubKey,
        address guardian
    ) external returns (uint256 committeeId) {
        uint256 n = members.length;
        require(n >= 1 && n <= 255, "bad n");
        require(threshold >= 1 && threshold <= n, "bad t");
        address groupAddress = _pubKeyToAddress(groupPubKey);
        require(groupAddress != address(0), "bad groupPubKey");

        committeeId = ++committeeCount;

        for (uint256 i = 0; i < n; i++) {
            address m = members[i];
            require(m != address(0), "zero member");
            require(_memberIndex1[committeeId][m] == 0, "dup member");
            _memberIndex1[committeeId][m] = i + 1;
            _members[committeeId].push(m);
        }

        Committee storage c = _committees[committeeId];
        c.status = Status.ACTIVE;
        c.threshold = threshold;
        c.size = uint8(n);
        c.epoch = 1;
        c.actionNonce = 0;
        c.groupAddress = groupAddress;
        c.guardian = guardian;
        c.membersHash = keccak256(abi.encode(members));
        c.groupPubKey = groupPubKey;

        emit CommitteeRegistered(committeeId, groupAddress, threshold, uint8(n), guardian);
    }

    // ==========================================================================
    //                          Relying-party verification
    // ==========================================================================

    /**
     * @notice Verify a threshold-ECDSA signature under a committee's group key.
     * @param committeeId  Target committee.
     * @param messageHash  32-byte digest that was signed (caller's responsibility to bind
     *                     domain separation into this digest for its own use case).
     * @param signature    65-byte (r||s||v) ECDSA signature, v in {27,28}, s in lower half.
     * @return ok True iff the committee is ACTIVE and `signature` recovers to its group
     *            address. See the contract-level HONEST SCOPE note: this proves authenticity
     *            under the group key, NOT that >= t members actually participated.
     *
     * @dev Fail-closed: returns false (never reverts) for a paused/retired/unknown
     *      committee, a malformed signature, high-s, or a bad v.
     */
    function verifyThresholdSignature(uint256 committeeId, bytes32 messageHash, bytes calldata signature)
        public
        view
        returns (bool ok)
    {
        Committee storage c = _committees[committeeId];
        if (c.status != Status.ACTIVE) return false;
        address rec = _recover(messageHash, signature);
        return rec != address(0) && rec == c.groupAddress;
    }

    /**
     * @notice EIP-1271-style helper: returns the magic value if the committee's group key
     *         signed `hash`, else 0xffffffff. Lets a committee's group key be used anywhere
     *         an EIP-1271 signer is accepted (via a thin proxy that forwards here).
     */
    function isValidGroupSignature(uint256 committeeId, bytes32 hash, bytes calldata signature)
        external
        view
        returns (bytes4)
    {
        return verifyThresholdSignature(committeeId, hash, signature) ? bytes4(0x1626ba7e) : bytes4(0xffffffff);
    }

    // ==========================================================================
    //                       Governance (threshold-authorized)
    // ==========================================================================

    /**
     * @notice Change membership and/or threshold while KEEPING the same group key (a
     *         proactive resharing / share refresh). Authorized by a threshold signature of
     *         the CURRENT committee over the reshare intent.
     * @param newMembers   New ordered, unique, non-zero member set.
     * @param newThreshold New t (1..newMembers.length).
     * @param authSig      65-byte threshold ECDSA signature by the CURRENT group over the
     *                     intent hash (see {reshareIntent}).
     *
     * @dev groupPubKey is unchanged: an honest off-chain resharing preserves the group key.
     *      We increment epoch (invalidating any earlier resharing) and actionNonce.
     */
    function reshareCommittee(
        uint256 committeeId,
        address[] calldata newMembers,
        uint8 newThreshold,
        bytes calldata authSig
    ) external {
        Committee storage c = _committees[committeeId];
        require(c.status == Status.ACTIVE, "not active");
        uint256 n = newMembers.length;
        require(n >= 1 && n <= 255, "bad n");
        require(newThreshold >= 1 && newThreshold <= n, "bad t");

        bytes32 intent = reshareIntent(committeeId, newMembers, newThreshold);
        require(verifyThresholdSignature(committeeId, intent, authSig), "bad committee auth");

        _replaceMembers(committeeId, newMembers);

        c.threshold = newThreshold;
        c.size = uint8(n);
        c.epoch += 1;
        c.actionNonce += 1;
        c.membersHash = keccak256(abi.encode(newMembers));

        emit CommitteeReshared(committeeId, c.epoch, newThreshold, uint8(n), c.membersHash);
    }

    /**
     * @notice Rotate to a NEW group key (post-compromise recovery), optionally with new
     *         members / threshold. Authorized by a threshold signature of the CURRENT
     *         (pre-rotation) committee over the rotation intent.
     * @param newGroupPubKey 64-byte uncompressed secp256k1 key for the new group.
     * @param authSig        65-byte threshold ECDSA signature by the CURRENT group over the
     *                       rotation intent hash (see {rotateIntent}).
     */
    function rotateKey(
        uint256 committeeId,
        bytes calldata newGroupPubKey,
        address[] calldata newMembers,
        uint8 newThreshold,
        bytes calldata authSig
    ) external {
        Committee storage c = _committees[committeeId];
        require(c.status == Status.ACTIVE, "not active");
        uint256 n = newMembers.length;
        require(n >= 1 && n <= 255, "bad n");
        require(newThreshold >= 1 && newThreshold <= n, "bad t");
        address newGroupAddress = _pubKeyToAddress(newGroupPubKey);
        require(newGroupAddress != address(0), "bad newGroupPubKey");
        require(newGroupAddress != c.groupAddress, "key unchanged");

        bytes32 intent = rotateIntent(committeeId, newGroupPubKey, newMembers, newThreshold);
        require(verifyThresholdSignature(committeeId, intent, authSig), "bad committee auth");

        _replaceMembers(committeeId, newMembers);

        c.threshold = newThreshold;
        c.size = uint8(n);
        c.epoch += 1;
        c.actionNonce += 1;
        c.groupAddress = newGroupAddress;
        c.groupPubKey = newGroupPubKey;
        c.membersHash = keccak256(abi.encode(newMembers));

        emit KeyRotated(committeeId, c.epoch, newGroupAddress, newThreshold, uint8(n));
    }

    /**
     * @notice Record that a named member misbehaved. Authorized EITHER by a threshold
     *         signature of the committee over the slash intent, OR by the guardian.
     * @param member       The member being slashed (must be current member).
     * @param evidenceHash Off-chain evidence commitment (e.g. hash of an equivocation).
     * @param authSig      Threshold ECDSA signature over the slash intent; ignored (may be
     *                     empty) when the caller is the guardian.
     *
     * @dev This is a HOOK: it records the fact and emits an event for an external
     *      staking/insurance contract to act on. It does not itself move stake (this
     *      registry holds no funds). Slashing a member does not change t/n or the key; the
     *      committee should follow with a reshare that drops the member.
     */
    function slashMember(uint256 committeeId, address member, bytes32 evidenceHash, bytes calldata authSig)
        external
    {
        Committee storage c = _committees[committeeId];
        require(c.status == Status.ACTIVE || c.status == Status.PAUSED, "gone");
        require(_memberIndex1[committeeId][member] != 0, "not a member");
        require(!slashed[committeeId][member], "already slashed");

        bool byGuardian = (c.guardian != address(0) && msg.sender == c.guardian);
        if (!byGuardian) {
            bytes32 intent = slashIntent(committeeId, member, evidenceHash);
            require(verifyThresholdSignature(committeeId, intent, authSig), "bad committee auth");
        }

        slashed[committeeId][member] = true;
        c.actionNonce += 1;
        emit MemberSlashed(committeeId, member, evidenceHash, msg.sender);
    }

    // ==========================================================================
    //                            Guardian break-glass
    // ==========================================================================

    function pauseCommittee(uint256 committeeId) external {
        Committee storage c = _committees[committeeId];
        require(c.status == Status.ACTIVE, "not active");
        require(c.guardian != address(0) && msg.sender == c.guardian, "not guardian");
        c.status = Status.PAUSED;
        emit CommitteePaused(committeeId, msg.sender);
    }

    function unpauseCommittee(uint256 committeeId) external {
        Committee storage c = _committees[committeeId];
        require(c.status == Status.PAUSED, "not paused");
        require(c.guardian != address(0) && msg.sender == c.guardian, "not guardian");
        c.status = Status.ACTIVE;
        emit CommitteeUnpaused(committeeId, msg.sender);
    }

    // ==========================================================================
    //                       Intent hashes (domain-separated)
    // ==========================================================================

    function reshareIntent(uint256 committeeId, address[] calldata newMembers, uint8 newThreshold)
        public
        view
        returns (bytes32)
    {
        Committee storage c = _committees[committeeId];
        return keccak256(
            abi.encode(
                ACTION_RESHARE, block.chainid, address(this), committeeId, c.epoch, c.actionNonce, newMembers, newThreshold
            )
        );
    }

    function rotateIntent(
        uint256 committeeId,
        bytes calldata newGroupPubKey,
        address[] calldata newMembers,
        uint8 newThreshold
    ) public view returns (bytes32) {
        Committee storage c = _committees[committeeId];
        return keccak256(
            abi.encode(
                ACTION_ROTATE,
                block.chainid,
                address(this),
                committeeId,
                c.epoch,
                c.actionNonce,
                keccak256(newGroupPubKey),
                newMembers,
                newThreshold
            )
        );
    }

    function slashIntent(uint256 committeeId, address member, bytes32 evidenceHash) public view returns (bytes32) {
        Committee storage c = _committees[committeeId];
        return keccak256(
            abi.encode(ACTION_SLASH, block.chainid, address(this), committeeId, c.epoch, c.actionNonce, member, evidenceHash)
        );
    }

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

    function getCommittee(uint256 committeeId)
        external
        view
        returns (
            Status status,
            uint8 threshold,
            uint8 size,
            uint64 epoch,
            uint64 actionNonce,
            address groupAddress,
            address guardian,
            bytes32 membersHash,
            bytes memory groupPubKey
        )
    {
        Committee storage c = _committees[committeeId];
        return (c.status, c.threshold, c.size, c.epoch, c.actionNonce, c.groupAddress, c.guardian, c.membersHash, c.groupPubKey);
    }

    function getMembers(uint256 committeeId) external view returns (address[] memory) {
        return _members[committeeId];
    }

    function isMember(uint256 committeeId, address who) external view returns (bool) {
        return _memberIndex1[committeeId][who] != 0;
    }

    function groupAddressOf(uint256 committeeId) external view returns (address) {
        return _committees[committeeId].groupAddress;
    }

    // ==========================================================================
    //                                Internals
    // ==========================================================================

    function _replaceMembers(uint256 committeeId, address[] calldata newMembers) internal {
        // Clear old membership index.
        address[] storage old = _members[committeeId];
        for (uint256 i = 0; i < old.length; i++) {
            delete _memberIndex1[committeeId][old[i]];
            // Note: `slashed` flags persist by design (history of a slashed address).
        }
        delete _members[committeeId];

        // Install new membership.
        for (uint256 i = 0; i < newMembers.length; i++) {
            address m = newMembers[i];
            require(m != address(0), "zero member");
            require(_memberIndex1[committeeId][m] == 0, "dup member");
            _memberIndex1[committeeId][m] = i + 1;
            _members[committeeId].push(m);
        }
    }

    /// @dev secp256k1 uncompressed pubkey (64 bytes, x||y) -> ethereum address.
    function _pubKeyToAddress(bytes calldata pubKey) internal pure returns (address) {
        if (pubKey.length != 64) return address(0);
        return address(uint160(uint256(keccak256(pubKey))));
    }

    /// @dev Strict ECDSA recover: 65-byte sig, low-s, v in {27,28}. 0 on any failure.
    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 (uint256(s) > SECP256K1_N_HALF) return address(0);
        if (v != 27 && v != 28) return address(0);
        return ecrecover(hash, v, r, s);
    }
}
