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

/// @dev Canonical SP1 verifier / SP1VerifierGateway ABI: verifyProof RETURNS
///      NOTHING and REVERTS on an invalid proof. On chain 2800 the deployed
///      SP1VerifierGateway is 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628 and the
///      SP1 Groth16 route is selected by the proof's leading 4-byte selector,
///      exactly as AereRollupValidity / AereEVMValidity use it.
interface ISp1Verifier {
    function verifyProof(
        bytes32 programVKey,
        bytes calldata publicValues,
        bytes calldata proof
    ) external view;
}

/**
 * @title AereZkEthLightClientV2 — succinct (zk) Ethereum beacon light client on AERE.
 *         CORRECTED fork of AereZkEthLightClient 0x2a2b6D93…29E9d (see THE FIX below).
 *
 * @notice Advances a trust-minimized view of Ethereum finality on AERE (chain 2800)
 *         by verifying an SP1 zkVM **Groth16 proof** that ONE Ethereum beacon
 *         `LightClientUpdate` is valid against the currently-trusted sync committee.
 *         The proof (guest `eth-lightclient-guest`, UNCHANGED — same ELF, same vkey
 *         0x00f03e1a…d191) checks, INSIDE the zkVM:
 *           1. the supplied 512 committee pubkeys hash (SSZ) to the trusted root;
 *           2. participation >= 2/3 of 512, and aggregates the participants (G1);
 *           3. the aggregate BLS12-381 signature over the SSZ signing root of the
 *              attested header verifies (hash-to-G2 + pairing);
 *           4. the finalized header is committed in the attested state (Electra
 *              finality Merkle branch, gindex 169);
 *           5. the delivered next sync committee is committed in the attested state
 *              (next-committee branch, gindex 87);
 *           6. the attested slot is not behind the finalized slot.
 *
 * @dev    ------------------------------------------------------------------
 *         THE FIX (vs V1 0x2a2b6D93…29E9d) — UNBOUND TRUST ANCHOR IN verify().
 *
 *         V1's stateful `processProof` was SAFE: it bound the proof's committed
 *         `gvr` to the immutable `genesisValidatorsRoot` and its committed
 *         `prevCommitteeRoot` to the stored `trustedCommitteeRoot` before advancing.
 *
 *         V1's PUBLIC `verify()` view did NOT. It ran the Groth16 verify and
 *         abi.decoded the public values with NO anchor binding at all. A proof is
 *         only ever a statement of the form "SOME committee, whose root is
 *         `prevCommitteeRoot`, signed this update" — the guest is honest, but it
 *         cannot know WHICH committee is Ethereum's real one. So a prover could
 *         generate a perfectly VALID proof over a sync committee of their OWN 512
 *         keys, self-sign the update, and V1's `verify()` would return the decoded
 *         tuple without reverting. Any consumer treating `verify()` as a finality
 *         oracle ("it didn't revert => this Ethereum header is final") could be
 *         handed an arbitrary attacker-chosen `finalizedHeaderRoot`.
 *
 *         V2 makes `verify()` apply the SAME anchor binds as `processProof`:
 *           * `gvr == genesisValidatorsRoot` (IMMUTABLE anchor), and
 *           * `prevCommitteeRoot == trustedCommitteeRoot` (this client's lineage), and
 *           * `nextCommitteeRoot != 0`.
 *         An unbound proof over a prover-chosen committee now REVERTS with
 *         `CommitteeMismatch`. This mirrors the sibling AereZkQbftLightClient
 *         0xCaDA54FA…6488, whose `verify()` was always bound, and the same
 *         anchor-binding fix shipped as AereOutboundVerifierV2 0x08b68bd5…a96F.
 *
 *         WHAT V2 DELIBERATELY DOES NOT BIND IN verify(): the monotonic slot
 *         advance (`newFinalizedSlot > finalizedSlot`). That is a property of THIS
 *         client's head, not of the proof's trust anchor, and a stateless view has
 *         no head to advance. Omitting it is intentional and matches
 *         AereZkQbftLightClient.verify(). CONSUMER NOTE: `verify()` returning means
 *         "this update was signed by the committee this client currently trusts" —
 *         it does NOT mean the update is newer than the tracked head. A consumer
 *         that needs recency must read `finalizedSlot()` / use `processProof`.
 *         ------------------------------------------------------------------
 *
 * @dev    WHY THIS CONTRACT EXISTS AT ALL. The sibling `AereEthLightClient` verifies
 *         the SAME update DIRECTLY on chain against AERE's EIP-2537 BLS12-381
 *         precompiles. That path is correct but its 512-pubkey committee-root
 *         recomputation + BLS aggregation costs ~13.2M+ gas and the full update OOGs
 *         at AERE's EIP-7825 2^24 (16,777,216) per-tx gas cap. Moving the heavy
 *         verification into a succinct proof makes the on-chain step a single Groth16
 *         verify — well under the cap.
 *
 * @dev    SECURITY MODEL — read before quoting.
 *         * What is proven ON CHAIN here is the Groth16 proof + the public-value
 *           binding (committee lineage, slot advance, gvr). What is proven OFF CHAIN
 *           (inside the zkVM) is the sync-committee signature + SSZ branches.
 *         * Security reduces to the honesty of >= 2/3 of Ethereum's 512-member sync
 *           committee, exactly like every Altair light client. NOT full Ethereum
 *           consensus, NOT a fraud/zk execution bridge.
 *         * TRUST-MINIMIZED, NOT TRUSTLESS and NOT QUANTUM-SAFE: the inbound signature
 *           scheme is BLS12-381 (classical, Ethereum's inherited crypto) and the proof
 *           wrap is an SP1 Groth16 SNARK over BN254 (classical). Both are
 *           quantum-vulnerable. This says nothing about AERE's own PQC work, and this
 *           contract does NOT touch AERE consensus (still classical ECDSA QBFT).
 *         * Bootstrap is weakly subjective: the constructor pins a trusted committee
 *           root + finalized slot/header (a checkpoint). From there it self-advances,
 *           one committee per accepted proof.
 *         * COMMITTEE LINEAGE IS AN EXPIRING CREDENTIAL. `trustedCommitteeRoot` only
 *           moves when someone submits a proof. If nobody advances it for > ~1 sync
 *           period, this client's trusted committee falls behind Ethereum's live one
 *           and `verify()` will (correctly) reject proofs for the current committee.
 *           A consumer MUST check `finalizedSlot()` for recency; a bound `verify()`
 *           is not a freshness guarantee.
 *         * PROGRAM BINDING: PROGRAM_VKEY is the SP1 vkey of the exact guest ELF that
 *           runs the update verification. Binding the vkey binds the exact program.
 *
 *         PUBLIC-VALUES CONVENTION (exactly 192 bytes = 6 x 32) — IDENTICAL to V1,
 *         because the guest is UNCHANGED:
 *           abi.encode(
 *             bytes32 gvr,                 // genesis validators root used for domain
 *             bytes32 prevCommitteeRoot,   // trusted signing-committee SSZ root
 *             uint64  finalizedSlot,       // new finalized slot
 *             bytes32 finalizedHeaderRoot, // new finalized beacon header root
 *             bytes32 nextCommitteeRoot,   // delivered next sync-committee root
 *             uint64  participation        // participants (guest enforces >= 342)
 *           )
 *
 *         IMMUTABILITY / TRUST: SP1_VERIFIER, PROGRAM_VKEY and genesisValidatorsRoot
 *         are fixed at deploy. There is NO owner and NO admin: advancing is
 *         permissionless. Soundness rests entirely on the SP1 Groth16 proof plus the
 *         on-chain lineage/monotonicity checks.
 */
contract AereZkEthLightClientV2 {
    /// @notice SP1 verifier gateway (routes to the Groth16 verifier by selector).
    address public immutable SP1_VERIFIER;

    /// @notice SP1 vkey binding the exact eth-lightclient guest ELF.
    bytes32 public immutable PROGRAM_VKEY;

    /// @notice Ethereum genesis validators root (mainnet), bound in every proof's
    ///         domain. A proof for a different network's gvr is rejected.
    bytes32 public immutable genesisValidatorsRoot;

    /// @notice The sync-committee root a valid next proof MUST have signed with.
    bytes32 public trustedCommitteeRoot;

    /// @notice The highest finalized beacon slot advanced so far.
    uint64 public finalizedSlot;

    /// @notice The finalized beacon block header root at `finalizedSlot`.
    bytes32 public finalizedHeaderRoot;

    /// @notice Number of proofs accepted (finality advances).
    uint64 public updateCount;

    event Bootstrapped(bytes32 committeeRoot, uint64 finalizedSlot, bytes32 finalizedHeaderRoot, bytes32 gvr);
    event FinalityAdvanced(
        uint64 indexed finalizedSlot,
        bytes32 finalizedHeaderRoot,
        bytes32 rotatedFromCommittee,
        bytes32 rotatedToCommittee,
        uint64 participation,
        address prover
    );

    error InvalidProof();
    error BadPublicValuesLength(uint256 got);
    error WrongGenesisValidatorsRoot(bytes32 inProof, bytes32 expected);
    error CommitteeMismatch(bytes32 inProof, bytes32 trusted);
    error StaleFinality(uint64 inProof, uint64 current);
    error ZeroNextCommittee();
    error ZeroAddress();
    error ZeroValue();
    error VerifierHasNoCode();

    constructor(
        address sp1Verifier,
        bytes32 programVKey,
        bytes32 gvr,
        bytes32 bootstrapCommitteeRoot,
        uint64 bootstrapFinalizedSlot,
        bytes32 bootstrapFinalizedHeaderRoot
    ) {
        if (sp1Verifier == address(0)) revert ZeroAddress();
        // A code-less verifier would make verifyProof (which returns no data) succeed
        // silently, accepting every "proof". Mirrors the AereZkQbftLightClient /
        // AereOutboundVerifierV2 guard: removes a deploy-time footgun.
        if (sp1Verifier.code.length == 0) revert VerifierHasNoCode();
        if (programVKey == bytes32(0) || gvr == bytes32(0) || bootstrapCommitteeRoot == bytes32(0)) {
            revert ZeroValue();
        }
        SP1_VERIFIER = sp1Verifier;
        PROGRAM_VKEY = programVKey;
        genesisValidatorsRoot = gvr;
        trustedCommitteeRoot = bootstrapCommitteeRoot;
        finalizedSlot = bootstrapFinalizedSlot;
        finalizedHeaderRoot = bootstrapFinalizedHeaderRoot;
        emit Bootstrapped(bootstrapCommitteeRoot, bootstrapFinalizedSlot, bootstrapFinalizedHeaderRoot, gvr);
    }

    /// @dev The trust-anchor bind, applied IDENTICALLY by `processProof` and `verify`.
    ///      This is the whole point of V2: there is exactly ONE place that decides
    ///      whether a proof is about the committee/network this client trusts, and
    ///      BOTH entry points go through it.
    function _bindToAnchor(
        bytes32 gvr,
        bytes32 prevCommitteeRoot,
        bytes32 nextCommitteeRoot
    ) internal view {
        if (gvr != genesisValidatorsRoot) revert WrongGenesisValidatorsRoot(gvr, genesisValidatorsRoot);
        if (prevCommitteeRoot != trustedCommitteeRoot) {
            revert CommitteeMismatch(prevCommitteeRoot, trustedCommitteeRoot);
        }
        if (nextCommitteeRoot == bytes32(0)) revert ZeroNextCommittee();
    }

    /// @dev Verify the Groth16 proof through the gateway and decode the 192-byte
    ///      public values. Reverts on a bad length or an invalid proof.
    function _verifyAndDecode(bytes calldata publicValues, bytes calldata proof)
        internal
        view
        returns (
            bytes32 gvr,
            bytes32 prevCommitteeRoot,
            uint64 newFinalizedSlot,
            bytes32 newFinalizedHeaderRoot,
            bytes32 nextCommitteeRoot,
            uint64 participation
        )
    {
        if (publicValues.length != 192) revert BadPublicValuesLength(publicValues.length);

        try ISp1Verifier(SP1_VERIFIER).verifyProof(PROGRAM_VKEY, publicValues, proof) {
            // verified — did not revert
        } catch {
            revert InvalidProof();
        }

        (gvr, prevCommitteeRoot, newFinalizedSlot, newFinalizedHeaderRoot, nextCommitteeRoot, participation) =
            abi.decode(publicValues, (bytes32, bytes32, uint64, bytes32, bytes32, uint64));
    }

    /// @notice Verify a zk proof of one Ethereum LightClientUpdate and, on success,
    ///         advance finality + rotate the trusted committee. Permissionless.
    /// @param publicValues abi.encode(gvr, prevCommitteeRoot, finalizedSlot,
    ///                     finalizedHeaderRoot, nextCommitteeRoot, participation), 192 bytes.
    /// @param proof        SP1 Groth16 proof bytes.
    function processProof(bytes calldata publicValues, bytes calldata proof) external {
        (
            bytes32 gvr,
            bytes32 prevCommitteeRoot,
            uint64 newFinalizedSlot,
            bytes32 newFinalizedHeaderRoot,
            bytes32 nextCommitteeRoot,
            uint64 participation
        ) = _verifyAndDecode(publicValues, proof);

        // 1) bind the proof to THIS client's trust anchor (gvr + committee lineage).
        _bindToAnchor(gvr, prevCommitteeRoot, nextCommitteeRoot);

        // 2) stateful-only: the head must strictly advance.
        if (newFinalizedSlot <= finalizedSlot) revert StaleFinality(newFinalizedSlot, finalizedSlot);

        // 3) advance finality + rotate the trusted committee to the delivered next.
        bytes32 from = trustedCommitteeRoot;
        finalizedSlot = newFinalizedSlot;
        finalizedHeaderRoot = newFinalizedHeaderRoot;
        trustedCommitteeRoot = nextCommitteeRoot;
        unchecked {
            updateCount += 1;
        }

        emit FinalityAdvanced(
            newFinalizedSlot,
            newFinalizedHeaderRoot,
            from,
            nextCommitteeRoot,
            participation,
            msg.sender
        );
    }

    /// @notice Stateless verification, BOUND TO THIS CLIENT'S TRUST ANCHOR (the V2 fix).
    ///         Reverts if the proof is invalid for PROGRAM_VKEY, if the committed gvr is
    ///         not this client's immutable `genesisValidatorsRoot`, or if the committed
    ///         `prevCommitteeRoot` is not the committee this client currently trusts.
    ///         Otherwise returns the decoded fields. Records nothing.
    /// @dev    NO monotonicity constraint (a stateless view has no head to advance) —
    ///         see the contract-level note. Returning means "signed by the committee this
    ///         client trusts", NOT "newer than the tracked head".
    function verify(bytes calldata publicValues, bytes calldata proof)
        external
        view
        returns (
            bytes32 gvr,
            bytes32 prevCommitteeRoot,
            uint64 newFinalizedSlot,
            bytes32 newFinalizedHeaderRoot,
            bytes32 nextCommitteeRoot,
            uint64 participation
        )
    {
        (gvr, prevCommitteeRoot, newFinalizedSlot, newFinalizedHeaderRoot, nextCommitteeRoot, participation) =
            _verifyAndDecode(publicValues, proof);

        // THE V2 FIX: the same anchor bind processProof applies. V1 omitted this,
        // so verify() would return true for a proof over a prover-chosen committee.
        _bindToAnchor(gvr, prevCommitteeRoot, nextCommitteeRoot);
    }
}
