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

import "./lib/BLS12381.sol";
import "./lib/SSZ.sol";

/**
 * @title AereEthLightClient — trust-minimized INBOUND light client: Ethereum -> AERE.
 *
 * @notice An on-chain Ethereum beacon-chain (Altair) light client running on AERE
 *         Network (chain 2800). It tracks Ethereum's sync committee and finalized
 *         beacon headers by verifying sync-committee BLS signatures against AERE's
 *         LIVE EIP-2537 BLS12-381 precompiles. No bridge multisig, no relayer trust:
 *         an AERE contract can learn "Ethereum finalized this header" from math alone.
 *
 *         Per update it checks, entirely on chain:
 *           1. the supplied 512 committee pubkeys hash (SSZ) to the committed
 *              sync-committee root — binding them to the trusted committee;
 *           2. participation >= 2/3 of 512, and aggregates the participants (G1ADD);
 *           3. the aggregate BLS signature over the SSZ signing root of the attested
 *              header verifies (hash-to-curve + pairing on the precompiles);
 *           4. the finalized header is in the attested state (SSZ Merkle branch);
 *           5. finality advances monotonically; committee rotation via next-committee
 *              branch.
 *
 * @dev    SECURITY MODEL — read before quoting.
 *         * Security reduces to the honesty of >= 2/3 of Ethereum's 512-member sync
 *           committee, exactly like every other Altair light client. It is NOT full
 *           Ethereum consensus and NOT a fraud-proof/zk bridge.
 *         * The inbound signature scheme is BLS12-381 — a CLASSICAL, pairing-friendly
 *           curve. It is quantum-VULNERABLE. This is Ethereum's crypto, inherited; it
 *           says nothing about AERE's own PQC work. Do not claim this path is
 *           post-quantum.
 *         * Bootstrap is weakly subjective: the constructor pins a trusted committee
 *           root + finalized header (a checkpoint). From there it is self-securing.
 *         * The DST is a constructor parameter. For Ethereum MAINNET set the sync
 *           protocol DST "BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_". The genesis
 *           validators root and fork version must match the target Ethereum network.
 */
contract AereEthLightClient {
    using BLS12381 for bytes;

    // ---- constants ----------------------------------------------------------
    uint256 public constant SYNC_COMMITTEE_SIZE = 512;
    uint256 public constant MIN_PARTICIPANTS = 342; // ceil(2 * 512 / 3)
    bytes4 public constant DOMAIN_SYNC_COMMITTEE = 0x07000000;
    // ELECTRA generalized indices (Ethereum mainnet is on the Electra/Fulu BeaconState
    // layout: >32 top-level fields pushed the container tree one level deeper vs Altair,
    // so every gindex through the state root gained one depth). Confirmed against the
    // consensus-specs Electra light-client constants AND empirically against a real live
    // mainnet finality_update / update (finality branch len 7 verifies at gindex 169;
    // next-committee branch len 6 verifies at gindex 87; current-committee gindex 86).
    //   finalized_checkpoint.root: FINALIZED_ROOT_GINDEX_ELECTRA = 169 -> depth 7, index 41.
    uint256 internal constant FINALITY_DEPTH = 7;
    uint256 internal constant FINALITY_INDEX = 41;
    //   next_sync_committee: NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA = 87 -> depth 6, index 23.
    uint256 internal constant NEXT_COMMITTEE_DEPTH = 6;
    uint256 internal constant NEXT_COMMITTEE_INDEX = 23;

    // (p-1)/2 as a 48-byte big-endian value, for the compressed-pubkey sign bit.
    bytes internal constant HALF_P =
        hex"0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555";

    // ---- immutable config ---------------------------------------------------
    bytes32 public immutable genesisValidatorsRoot;
    bytes public dst; // RFC-9380 domain separation tag used by the signing scheme

    // ---- tracked state ------------------------------------------------------
    bytes32 public currentSyncCommitteeRoot;
    bytes32 public nextSyncCommitteeRoot;
    uint64 public finalizedSlot;
    bytes32 public finalizedHeaderRoot;
    bytes32 public finalizedStateRoot;
    bytes32 public finalizedBodyRoot;

    /// @notice Every beacon body root this client has ever finalized. Retained so a
    ///         message whose block is no longer the finalized HEAD can still be proven
    ///         (finality is monotonic, so a once-finalized root stays valid forever).
    mapping(bytes32 => bool) public finalizedBodyRootSeen;

    struct BeaconHeader {
        uint64 slot;
        uint64 proposerIndex;
        bytes32 parentRoot;
        bytes32 stateRoot;
        bytes32 bodyRoot;
    }

    struct LightClientUpdate {
        BeaconHeader attestedHeader;
        BeaconHeader finalizedHeader;
        bytes32[] finalityBranch; // len FINALITY_DEPTH
        bytes participationBits; // 64 bytes (SSZ Bitvector[512])
        bytes signature; // 256-byte G2
        bytes pubkeys; // SYNC_COMMITTEE_SIZE * 128 (uncompressed G1), signing committee
        bytes aggregatePubkey; // 128 bytes
        bytes4 signatureForkVersion;
        bool signedByNextCommittee; // true when the NEXT committee produced this signature
        // optional committee rotation payload (proven against attestedHeader.stateRoot)
        bool hasNextCommittee;
        bytes nextPubkeys;
        bytes nextAggregatePubkey;
        bytes32[] nextSyncCommitteeBranch; // len NEXT_COMMITTEE_DEPTH
    }

    event Bootstrapped(bytes32 committeeRoot, uint64 finalizedSlot, bytes32 finalizedHeaderRoot);
    event FinalityUpdated(uint64 finalizedSlot, bytes32 finalizedHeaderRoot, bytes32 finalizedBodyRoot);
    event NextCommitteeStored(bytes32 nextCommitteeRoot);
    event CommitteeRotated(bytes32 newCurrentCommitteeRoot);

    constructor(
        bytes memory dst_,
        bytes32 genesisValidatorsRoot_,
        bytes32 bootstrapCommitteeRoot,
        BeaconHeader memory bootstrapFinalized
    ) {
        require(bootstrapCommitteeRoot != bytes32(0), "LC:zero committee");
        dst = dst_;
        genesisValidatorsRoot = genesisValidatorsRoot_;
        currentSyncCommitteeRoot = bootstrapCommitteeRoot;
        finalizedSlot = bootstrapFinalized.slot;
        finalizedHeaderRoot = SSZ.beaconBlockHeaderRoot(
            bootstrapFinalized.slot,
            bootstrapFinalized.proposerIndex,
            bootstrapFinalized.parentRoot,
            bootstrapFinalized.stateRoot,
            bootstrapFinalized.bodyRoot
        );
        finalizedStateRoot = bootstrapFinalized.stateRoot;
        finalizedBodyRoot = bootstrapFinalized.bodyRoot;
        finalizedBodyRootSeen[bootstrapFinalized.bodyRoot] = true;
        emit Bootstrapped(bootstrapCommitteeRoot, finalizedSlot, finalizedHeaderRoot);
    }

    // -------------------------------------------------------------------------
    //                              update processing
    // -------------------------------------------------------------------------

    function processUpdate(LightClientUpdate calldata u) external {
        // 1. bind the supplied committee to the committed root.
        bytes32 committeeRoot = _syncCommitteeRoot(u.pubkeys, u.aggregatePubkey);
        bytes32 expected = u.signedByNextCommittee ? nextSyncCommitteeRoot : currentSyncCommitteeRoot;
        require(expected != bytes32(0), "LC:no committee");
        require(committeeRoot == expected, "LC:committee mismatch");

        // 2. participation threshold + participant aggregation.
        bytes memory aggPk = _aggregateParticipants(u.pubkeys, u.participationBits);

        // 3. verify the aggregate BLS signature over the signing root.
        bytes32 attestedRoot = _headerRoot(u.attestedHeader);
        bytes32 domain = _computeDomain(u.signatureForkVersion);
        bytes32 signingRoot = SSZ.hashPair(attestedRoot, domain);
        require(
            BLS12381.verify(aggPk, abi.encodePacked(signingRoot), u.signature, dst),
            "LC:bad signature"
        );

        // 4. finalized header must be committed in the attested state.
        bytes32 finalRoot = _headerRoot(u.finalizedHeader);
        require(
            SSZ.isValidMerkleBranch(
                finalRoot, u.finalityBranch, FINALITY_DEPTH, FINALITY_INDEX, u.attestedHeader.stateRoot
            ),
            "LC:bad finality branch"
        );

        // 5. monotonic finality advance.
        require(u.finalizedHeader.slot > finalizedSlot, "LC:stale finality");
        require(u.attestedHeader.slot >= u.finalizedHeader.slot, "LC:attested<final");

        // If this signature came from the next committee, rotate current <- next.
        if (u.signedByNextCommittee) {
            currentSyncCommitteeRoot = nextSyncCommitteeRoot;
            nextSyncCommitteeRoot = bytes32(0);
            emit CommitteeRotated(currentSyncCommitteeRoot);
        }

        finalizedSlot = u.finalizedHeader.slot;
        finalizedHeaderRoot = finalRoot;
        finalizedStateRoot = u.finalizedHeader.stateRoot;
        finalizedBodyRoot = u.finalizedHeader.bodyRoot;
        finalizedBodyRootSeen[u.finalizedHeader.bodyRoot] = true;
        emit FinalityUpdated(finalizedSlot, finalRoot, finalizedBodyRoot);

        // 6. optional: store the next sync committee, proven in the attested state.
        if (u.hasNextCommittee) {
            bytes32 nextRoot = _syncCommitteeRoot(u.nextPubkeys, u.nextAggregatePubkey);
            require(
                SSZ.isValidMerkleBranch(
                    nextRoot,
                    u.nextSyncCommitteeBranch,
                    NEXT_COMMITTEE_DEPTH,
                    NEXT_COMMITTEE_INDEX,
                    u.attestedHeader.stateRoot
                ),
                "LC:bad next-committee branch"
            );
            nextSyncCommitteeRoot = nextRoot;
            emit NextCommitteeStored(nextRoot);
        }
    }

    // -------------------------------------------------------------------------
    //                       execution-payload attestation (for the inbox)
    // -------------------------------------------------------------------------

    /// @notice Verify that `receiptsRoot` is committed under an Ethereum beacon body
    ///         that THIS client has finalized (`bodyRoot` must have been seen). The
    ///         (depth, subtreeIndex) locate the execution payload's receipts_root under
    ///         body_root (fork-dependent; the caller/inbox supplies the correct
    ///         generalized index for the target Ethereum fork). Because finality is
    ///         monotonic, any once-finalized body root stays valid, so a message stays
    ///         deliverable after finality advances past its block.
    function verifyReceiptsRootAgainstFinalized(
        bytes32 receiptsRoot,
        bytes32 bodyRoot,
        bytes32[] calldata branch,
        uint256 depth,
        uint256 subtreeIndex
    ) external view returns (bool) {
        if (!finalizedBodyRootSeen[bodyRoot]) return false;
        return SSZ.isValidMerkleBranch(receiptsRoot, branch, depth, subtreeIndex, bodyRoot);
    }

    // -------------------------------------------------------------------------
    //                                internals
    // -------------------------------------------------------------------------

    function _headerRoot(BeaconHeader calldata h) internal view returns (bytes32) {
        return SSZ.beaconBlockHeaderRoot(h.slot, h.proposerIndex, h.parentRoot, h.stateRoot, h.bodyRoot);
    }

    /// @dev compute_domain(DOMAIN_SYNC_COMMITTEE, forkVersion, genesisValidatorsRoot).
    function _computeDomain(bytes4 forkVersion) internal view returns (bytes32) {
        bytes32 forkDataRoot = SSZ.hashPair(bytes32(forkVersion), genesisValidatorsRoot);
        // domain = domainType(4) || forkDataRoot[0:28]
        return (bytes32(DOMAIN_SYNC_COMMITTEE)) | (forkDataRoot >> 32);
    }

    /// @dev Aggregate the participating pubkeys (per the SSZ Bitvector[512]) via G1ADD,
    ///      requiring at least MIN_PARTICIPANTS distinct set bits.
    function _aggregateParticipants(bytes calldata pubkeys, bytes calldata bits)
        internal
        view
        returns (bytes memory acc)
    {
        require(pubkeys.length == SYNC_COMMITTEE_SIZE * 128, "LC:pubkeys len");
        require(bits.length == SYNC_COMMITTEE_SIZE / 8, "LC:bits len");
        acc = new bytes(128); // point at infinity
        uint256 count = 0;
        for (uint256 i = 0; i < SYNC_COMMITTEE_SIZE; i++) {
            if ((uint8(bits[i >> 3]) >> (i & 7)) & 1 == 1) {
                acc = BLS12381.g1Add(acc, _pubkeyAt(pubkeys, i));
                count++;
            }
        }
        require(count >= MIN_PARTICIPANTS, "LC:insufficient participation");
    }

    function _pubkeyAt(bytes calldata pubkeys, uint256 i) internal pure returns (bytes memory pk) {
        pk = new bytes(128);
        uint256 off = i * 128;
        for (uint256 j = 0; j < 128; j++) pk[j] = pubkeys[off + j];
    }

    /// @dev hash_tree_root(SyncCommittee{ Vector[Bytes48,512] pubkeys, Bytes48 aggregate }).
    ///      Pubkeys are supplied UNCOMPRESSED (128B G1) and compressed on chain to the
    ///      48-byte form the SSZ root commits to.
    function _syncCommitteeRoot(bytes calldata pubkeys, bytes calldata aggregate)
        internal
        view
        returns (bytes32)
    {
        require(pubkeys.length == SYNC_COMMITTEE_SIZE * 128, "LC:pubkeys len");
        require(aggregate.length == 128, "LC:agg len");
        bytes32[] memory leaves = new bytes32[](SYNC_COMMITTEE_SIZE);
        for (uint256 i = 0; i < SYNC_COMMITTEE_SIZE; i++) {
            leaves[i] = _pubkeyLeaf(_compressAt(pubkeys, i * 128));
        }
        bytes32 pubkeysRoot = SSZ.merkleize(leaves);
        bytes32 aggLeaf = _pubkeyLeaf(_compressCalldata(aggregate));
        return SSZ.hashPair(pubkeysRoot, aggLeaf);
    }

    /// @dev SSZ htr of a Bytes48: two chunks, second right-zero-padded.
    function _pubkeyLeaf(bytes memory compressed48) internal view returns (bytes32) {
        bytes32 c0;
        bytes32 c1;
        assembly {
            c0 := mload(add(compressed48, 32)) // bytes 0..31
        }
        // bytes 32..47 into the high 16 bytes of c1, low 16 zero.
        uint256 hi;
        for (uint256 k = 0; k < 16; k++) {
            hi |= uint256(uint8(compressed48[32 + k])) << (8 * (31 - k));
        }
        c1 = bytes32(hi);
        return SSZ.hashPair(c0, c1);
    }

    /// @dev Compress the uncompressed G1 pubkey at `pubkeys[off:off+128]` to 48 bytes.
    function _compressAt(bytes calldata pubkeys, uint256 off) internal pure returns (bytes memory out) {
        out = new bytes(48);
        // x value is bytes [off+16 : off+64]; y value is [off+80 : off+128]
        for (uint256 i = 0; i < 48; i++) out[i] = pubkeys[off + 16 + i];
        out[0] = bytes1(uint8(out[0]) | 0x80); // compression flag
        if (_yIsLarger(pubkeys, off + 80)) out[0] = bytes1(uint8(out[0]) | 0x20);
    }

    function _compressCalldata(bytes calldata point) internal pure returns (bytes memory out) {
        out = new bytes(48);
        for (uint256 i = 0; i < 48; i++) out[i] = point[16 + i];
        out[0] = bytes1(uint8(out[0]) | 0x80);
        if (_yIsLarger(point, 80)) out[0] = bytes1(uint8(out[0]) | 0x20);
    }

    /// @dev True iff the 48-byte big-endian y value at `data[yOff:yOff+48]` exceeds (p-1)/2.
    function _yIsLarger(bytes calldata data, uint256 yOff) internal pure returns (bool) {
        for (uint256 i = 0; i < 48; i++) {
            uint8 a = uint8(data[yOff + i]);
            uint8 b = uint8(HALF_P[i]);
            if (a != b) return a > b;
        }
        return false; // exactly equal -> not larger
    }
}
