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

/**
 * @title AereDrandQuicknetBLS
 * @notice Real on-chain BLS12-381 verification of drand beacon signatures for
 *         the League of Entropy "quicknet" chain, built entirely on Aere's
 *         live EIP-2537 precompiles. This library replaces the honesty gap in
 *         the original AereRandomnessBeacon, whose `_verify` was a placeholder
 *         that returned `true` for every input (it verified nothing).
 *
 *   Target chain: drand quicknet (scheme id `bls-unchained-g1-rfc9380`).
 *     chain hash : 52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971
 *     period     : 3 seconds        genesis: 1692803367 (unix)
 *     public key : 96-byte compressed G2 point (see PUBKEY_G2 below, decompressed)
 *     signatures : 48-byte compressed G1 points
 *     message    : SHA-256( round_number as 8-byte big-endian )   [UNCHAINED]
 *     hash-to-curve DST: "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"
 *
 *   The min-signature-size BLS check (signature in G1, public key in G2) is:
 *
 *        e( H(m), pubkey )  ==  e( signature, G2_generator )
 *
 *   which we verify as a single EIP-2537 pairing product against 1:
 *
 *        e( H(m), pubkey ) * e( signature, -G2_generator )  ==  1
 *
 *   Every step is fail-closed. Any malformed input, any precompile that is
 *   absent (returns empty) or returns the wrong-length output, any failed
 *   pairing, all resolve to `false`. There is no path that returns `true`
 *   without a genuine pairing equality.
 *
 *   HONESTY / SCOPE NOTE: drand's BLS12-381 is CLASSICAL cryptography. It is
 *   breakable by a sufficiently large quantum computer (Shor). This library
 *   makes the beacon actually verify what it stores, which is a correctness and
 *   honesty fix. It does NOT make the randomness post-quantum. Aere's PQC work
 *   (Falcon/ML-DSA/SLH-DSA precompiles) is a separate track and is not involved
 *   in drand verification.
 *
 *   EIP-2537 precompiles used (all "Supported" on Aere mainnet, Prague parity):
 *     0x05 MODEXP (pre-Prague; modular exponentiation for hash-to-field reduction and sqrt)
 *     0x0b BLS12_G1ADD
 *     0x0f BLS12_PAIRING_CHECK
 *     0x10 BLS12_MAP_FP_TO_G1
 */
library AereDrandQuicknetBLS {
    // ── precompile addresses ──
    address internal constant MODEXP         = address(0x05);
    address internal constant BLS_G1ADD      = address(0x0b);
    address internal constant BLS_PAIRING    = address(0x0f);
    address internal constant BLS_MAP_FP_G1  = address(0x10);

    // ── field constants (BLS12-381 base field Fp), big-endian, 48 bytes ──
    // p = 0x1a0111ea...aaab
    bytes internal constant FIELD_MODULUS =
        hex"1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab";
    // (p + 1) / 4  — sqrt exponent, valid because p ≡ 3 (mod 4). 48 bytes.
    bytes internal constant SQRT_EXP =
        hex"0680447a8e5ff9a692c6e9ed90d2eb35d91dd2e13ce144afd9cc34a83dac3d8907aaffffac54ffffee7fbfffffffeaab";

    // RFC 9380 DST_prime = DST || I2OSP(len(DST), 1). len(DST) = 43 = 0x2b.
    // DST = "BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_"
    bytes internal constant DST_PRIME =
        hex"424c535f5349475f424c53313233383147315f584d443a5348412d3235365f535357555f524f5f4e554c5f2b";

    // drand quicknet aggregated public key, decompressed to an EIP-2537 G2
    // point (256 bytes: x.c0 || x.c1 || y.c0 || y.c1, each a 64-byte Fp element).
    // Decompressed off-chain from the canonical 96-byte compressed key
    //   83cf0f28...ece45a  (verifiable at api.drand.sh/<chainhash>/info).
    bytes internal constant PUBKEY_G2 =
        hex"000000000000000000000000000000000d1fec758c921cc22b0e17e63aaf4bcb5ed66304de9cf809bd274ca73bab4af5a6e9c76a4bc09e76eae8991ef5ece45a0000000000000000000000000000000003cf0f2896adee7eb8b5f01fcad3912212c437e0073e911fb90022d3e760183c8c4b450b6a0a6c3ac6a5776a2d106451000000000000000000000000000000000e5db2b6bfbb01c867749cadffca88b36c24f3012ba09fc4d3022c5c37dce0f977d3adb5d183c7477c442b1f045152730000000000000000000000000000000001a714f2edb74119a2f2b0d5a7c75ba902d163700a61bc224ededd8e63aef7be1aaf8e93d7a9718b047ccddb3eb5d68b";

    // Negated BLS12-381 G2 generator, EIP-2537 encoding (256 bytes). Constant
    // for all of BLS12-381; used so the pairing product checks against 1.
    bytes internal constant NEG_G2_GENERATOR =
        hex"00000000000000000000000000000000024aa2b2f08f0a91260805272dc51051c6e47ad4fa403b02b4510b647ae3d1770bac0326a805bbefd48056c8c121bdb80000000000000000000000000000000013e02b6052719f607dacd3a088274f65596bd0d09920b61ab5da61bbdc7f5049334cf11213945d57e5ac7d055d042b7e000000000000000000000000000000000d1b3cc2c7027888be51d9ef691d77bcb679afda66c73f17f9ee3837a55024f78c71363275a75d75d86bab79f74782aa0000000000000000000000000000000013fa4d4a0ad8b1ce186ed5061789213d993923066dddaf1040bc3ff59f825c78df74f2d75467e25e0f55f8a00fa030ed";

    // ───────────────────────── message ─────────────────────────

    /// drand quicknet is UNCHAINED: message = SHA-256(round as 8-byte big-endian).
    function messageForRound(uint64 round) internal pure returns (bytes32) {
        return sha256(abi.encodePacked(round));
    }

    /// Canonical drand randomness derivation: SHA-256 of the compressed signature.
    function canonicalRandomness(bytes memory compressedSig) internal pure returns (bytes32) {
        return sha256(compressedSig);
    }

    // ─────────────────── RFC 9380 hash-to-field ───────────────────

    /**
     * @dev expand_message_xmd with SHA-256, len_in_bytes = 128 (RFC 9380 §5.3.1),
     *      then reduce the two 64-byte blocks mod p into EIP-2537 Fp elements.
     *      Returns two 64-byte field elements (u0, u1). SHA-256 (0x02) and
     *      MODEXP (0x05) only, so this runs on any EVM (no EIP-2537 needed).
     */
    function hashToField(bytes32 message) internal view returns (bytes memory u0, bytes memory u1) {
        // msg_prime = Z_pad(64) || msg || l_i_b_str(0x0080) || 0x00 || DST_prime
        bytes memory zPad = new bytes(64);
        bytes32 b0 = sha256(abi.encodePacked(zPad, message, uint16(128), uint8(0), DST_PRIME));
        bytes32 b1 = sha256(abi.encodePacked(b0, uint8(1), DST_PRIME));
        bytes32 b2 = sha256(abi.encodePacked(b0 ^ b1, uint8(2), DST_PRIME));
        bytes32 b3 = sha256(abi.encodePacked(b0 ^ b2, uint8(3), DST_PRIME));
        bytes32 b4 = sha256(abi.encodePacked(b0 ^ b3, uint8(4), DST_PRIME));
        // uniform_bytes = b1 || b2 || b3 || b4 (128 bytes); two 64-byte blocks.
        u0 = _reduce64ModP(abi.encodePacked(b1, b2));
        u1 = _reduce64ModP(abi.encodePacked(b3, b4));
    }

    /**
     * @dev Full hash-to-curve to G1 (RFC 9380 SSWU_RO). Because EIP-2537's
     *      MAP_FP_TO_G1 already clears the cofactor, and cofactor clearing is a
     *      group homomorphism, map(u0) + map(u1) equals RFC 9380's
     *      clear_cofactor(map(u0) + map(u1)). Requires EIP-2537.
     * @return point 128-byte uncompressed G1 point, ok false if any precompile
     *         is absent or misbehaves (fail-closed).
     */
    function hashToG1(bytes32 message) internal view returns (bytes memory point, bool ok) {
        (bytes memory u0, bytes memory u1) = hashToField(message);
        (bytes memory p0, bool ok0) = _mapFpToG1(u0);
        (bytes memory p1, bool ok1) = _mapFpToG1(u1);
        if (!ok0 || !ok1) return (new bytes(0), false);
        (bool s, bytes memory out) = BLS_G1ADD.staticcall(abi.encodePacked(p0, p1));
        if (!s || out.length != 128) return (new bytes(0), false);
        return (out, true);
    }

    // ─────────────────── G1 signature decompression ───────────────────

    /**
     * @dev Decompress a 48-byte compressed G1 point (zcash/ETH2 serialization,
     *      which drand uses) into a 128-byte EIP-2537 uncompressed point.
     *      Uses only MODEXP, so it runs without EIP-2537. Fail-closed: returns
     *      ok=false for the point at infinity, non-compressed encodings,
     *      non-canonical x (>= p), or an x whose x^3+4 is not a square.
     */
    function decompressG1(bytes memory comp) internal view returns (bytes memory point, bool ok) {
        if (comp.length != 48) return (new bytes(0), false);
        uint8 flags = uint8(comp[0]);
        if (flags & 0x80 == 0) return (new bytes(0), false); // must be compressed
        if (flags & 0x40 != 0) return (new bytes(0), false); // infinity disallowed
        bool signBit = (flags & 0x20) != 0;

        bytes memory x = new bytes(48);
        x[0] = bytes1(flags & 0x1f); // clear the 3 metadata bits
        for (uint256 i = 1; i < 48; i++) {
            x[i] = comp[i];
        }
        if (_ge(x, FIELD_MODULUS)) return (new bytes(0), false); // x must be < p

        // y^2 = x^3 + 4 (mod p)
        bytes memory y2 = _addSmallModP(_modexp(x, hex"03", FIELD_MODULUS), 4);
        // candidate y = (y^2)^((p+1)/4) (mod p)
        bytes memory y = _modexp(y2, SQRT_EXP, FIELD_MODULUS);
        // reject if not a genuine square root (point not on curve)
        if (keccak256(_modexp(y, hex"02", FIELD_MODULUS)) != keccak256(y2)) {
            return (new bytes(0), false);
        }

        // pick the root whose "sign" matches the compression flag
        bytes memory negY = _sub(FIELD_MODULUS, y); // p - y
        bool yIsLarger = _ge(y, negY);
        bytes memory yFinal;
        if (signBit) {
            yFinal = yIsLarger ? y : negY;
        } else {
            yFinal = yIsLarger ? negY : y;
        }

        point = abi.encodePacked(bytes16(0), x, bytes16(0), yFinal); // 16+48+16+48 = 128
        ok = true;
    }

    // ───────────────────────── verify ─────────────────────────

    /**
     * @notice Verify a drand quicknet beacon round from its compressed signature.
     *         Fail-closed end to end. Requires EIP-2537 for the pairing; without
     *         it, hashToG1 / pairing produce empty output and this returns false.
     */
    function verifyRoundCompressed(uint64 round, bytes memory compressedSig)
        internal
        view
        returns (bool)
    {
        (bytes memory sigPoint, bool sigOk) = decompressG1(compressedSig);
        if (!sigOk) return false;
        return verifyRoundUncompressed(round, sigPoint);
    }

    /**
     * @notice Verify with an already-uncompressed 128-byte G1 signature point.
     *         Checks e(H(m), pubkey) * e(sig, -G2) == 1 via EIP-2537 pairing.
     */
    function verifyRoundUncompressed(uint64 round, bytes memory sigPoint)
        internal
        view
        returns (bool)
    {
        if (sigPoint.length != 128) return false;
        (bytes memory hm, bool ok) = hashToG1(messageForRound(round));
        if (!ok) return false;

        // pairing input: (H(m), pubkey) then (sig, -G2_generator) = 2 * 384 bytes
        bytes memory input = abi.encodePacked(hm, PUBKEY_G2, sigPoint, NEG_G2_GENERATOR);
        (bool s, bytes memory out) = BLS_PAIRING.staticcall(input);
        if (!s || out.length != 32) return false;
        return out[31] == 0x01 && _isOne(out);
    }

    // ───────────────────────── helpers ─────────────────────────

    function _mapFpToG1(bytes memory fp64) private view returns (bytes memory pt, bool ok) {
        (bool s, bytes memory out) = BLS_MAP_FP_G1.staticcall(fp64);
        if (s && out.length == 128) return (out, true);
        return (new bytes(0), false);
    }

    function _modexp(bytes memory base, bytes memory exp, bytes memory mod)
        private
        view
        returns (bytes memory)
    {
        bytes memory input = abi.encodePacked(
            uint256(base.length), uint256(exp.length), uint256(mod.length),
            base, exp, mod
        );
        (bool s, bytes memory out) = MODEXP.staticcall(input);
        require(s && out.length == mod.length, "AereDrand: MODEXP failed");
        return out;
    }

    /// Reduce a 64-byte big-endian value mod p, returning a 64-byte EIP-2537 Fp element.
    function _reduce64ModP(bytes memory chunk64) private view returns (bytes memory) {
        bytes memory reduced = _modexp(chunk64, hex"01", FIELD_MODULUS); // 48 bytes, < p
        return abi.encodePacked(bytes16(0), reduced); // left-pad to 64
    }

    /// a >= b for equal-length big-endian byte arrays.
    function _ge(bytes memory a, bytes memory b) private pure returns (bool) {
        for (uint256 i = 0; i < a.length; i++) {
            if (a[i] != b[i]) return uint8(a[i]) > uint8(b[i]);
        }
        return true; // equal
    }

    /// a - b for equal-length big-endian arrays, assuming a >= b.
    function _sub(bytes memory a, bytes memory b) private pure returns (bytes memory) {
        bytes memory r = new bytes(a.length);
        int256 borrow = 0;
        for (uint256 i = a.length; i > 0; i--) {
            int256 d = int256(uint256(uint8(a[i - 1]))) - int256(uint256(uint8(b[i - 1]))) - borrow;
            if (d < 0) {
                d += 256;
                borrow = 1;
            } else {
                borrow = 0;
            }
            r[i - 1] = bytes1(uint8(uint256(d)));
        }
        return r;
    }

    /// (t + c) mod p for a small c, where t is a 48-byte value < p.
    function _addSmallModP(bytes memory t, uint256 c) private pure returns (bytes memory) {
        bytes memory r = new bytes(t.length);
        for (uint256 i = 0; i < t.length; i++) {
            r[i] = t[i];
        }
        uint256 carry = c;
        for (uint256 i = t.length; i > 0 && carry > 0; i--) {
            uint256 v = uint256(uint8(r[i - 1])) + (carry & 0xff);
            r[i - 1] = bytes1(uint8(v & 0xff));
            carry = (carry >> 8) + (v >> 8);
        }
        if (_ge(r, FIELD_MODULUS)) {
            r = _sub(r, FIELD_MODULUS);
        }
        return r;
    }

    function _isOne(bytes memory out) private pure returns (bool) {
        for (uint256 i = 0; i < 31; i++) {
            if (out[i] != 0x00) return false;
        }
        return out[31] == 0x01;
    }
}

/**
 * @title AereRandomnessBeaconV2
 * @notice Fail-closed, self-verifying drand quicknet randomness beacon for Aere.
 *
 *   This is the honest replacement for AereRandomnessBeacon's stub verifier.
 *   `submitRound` ALWAYS performs a real on-chain BLS12-381 pairing check
 *   against the drand quicknet public key before storing anything. There is no
 *   "phase 1 trust the submitter" mode and no admin switch that turns
 *   verification off: a round that does not verify cannot be stored, and the
 *   stored randomness is the canonical drand value SHA-256(signature).
 *
 *   This contract is a BUILT-AND-TESTED replacement. It is NOT a live swap of
 *   the deployed beacon; activation on Aere mainnet is founder-gated.
 *
 *   Quantum note: drand BLS is classical (Shor-breakable). Verifying it does
 *   not make the randomness post-quantum. This is a correctness/honesty fix.
 */
contract AereRandomnessBeaconV2 {
    using AereDrandQuicknetBLS for *;

    bytes32 public constant DRAND_CHAIN_HASH =
        0x52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971;
    uint64 public constant DRAND_PERIOD = 3;
    uint64 public constant DRAND_GENESIS = 1692803367;

    mapping(uint64 => bytes32) public randomnessOfRound;
    mapping(uint64 => bytes) public signatureOfRound;
    mapping(uint64 => address) public submitterOfRound;

    event RoundSubmitted(uint64 indexed round, bytes32 randomness, address indexed submitter);

    /**
     * @notice Submit and verify a drand quicknet round. Anyone may call; the
     *         submitter pays gas. Reverts (fail-closed) if the BLS signature
     *         does not verify against the quicknet public key.
     * @param round     drand round number.
     * @param signature 48-byte compressed G1 drand signature (as published at api.drand.sh).
     * @return randomness canonical drand randomness = SHA-256(signature).
     */
    function submitRound(uint64 round, bytes calldata signature)
        external
        returns (bytes32 randomness)
    {
        require(round > 0, "AereDrand: bad round");
        require(signature.length == 48, "AereDrand: sig must be 48 bytes (G1 compressed)");
        require(randomnessOfRound[round] == bytes32(0), "AereDrand: already submitted");

        uint64 roundTime = DRAND_GENESIS + (round * DRAND_PERIOD);
        require(roundTime <= block.timestamp + 30, "AereDrand: round not yet emitted by drand");

        // MANDATORY, fail-closed on-chain BLS12-381 verification.
        require(
            AereDrandQuicknetBLS.verifyRoundCompressed(round, signature),
            "AereDrand: BLS verification failed"
        );

        randomness = AereDrandQuicknetBLS.canonicalRandomness(signature);
        randomnessOfRound[round] = randomness;
        signatureOfRound[round] = signature;
        submitterOfRound[round] = msg.sender;

        emit RoundSubmitted(round, randomness, msg.sender);
    }

    function getRandomness(uint64 round) external view returns (bytes32) {
        return randomnessOfRound[round];
    }

    function isAvailable(uint64 round) external view returns (bool) {
        return randomnessOfRound[round] != bytes32(0);
    }

    function roundAtTime(uint64 unixTimestamp) external pure returns (uint64) {
        require(unixTimestamp >= DRAND_GENESIS, "AereDrand: before drand genesis");
        return uint64((unixTimestamp - DRAND_GENESIS + DRAND_PERIOD - 1) / DRAND_PERIOD);
    }

    // ───────── transparency / test surface (pure crypto, safe to expose) ─────────

    /// The BLS message that is signed for `round` (SHA-256 of the 8-byte round).
    function messageForRound(uint64 round) external pure returns (bytes32) {
        return AereDrandQuicknetBLS.messageForRound(round);
    }

    /// RFC 9380 hash-to-field of a round's message: two 64-byte EIP-2537 Fp elements.
    function hashToFieldForRound(uint64 round) external view returns (bytes memory u0, bytes memory u1) {
        return AereDrandQuicknetBLS.hashToField(AereDrandQuicknetBLS.messageForRound(round));
    }

    /// Full RFC 9380 hash-to-curve of a round's message to G1 (needs EIP-2537).
    function hashToG1ForRound(uint64 round) external view returns (bytes memory point, bool ok) {
        return AereDrandQuicknetBLS.hashToG1(AereDrandQuicknetBLS.messageForRound(round));
    }

    /// Decompress a 48-byte compressed G1 signature to a 128-byte point.
    function decompressSignature(bytes calldata compressedSig)
        external
        view
        returns (bytes memory point, bool ok)
    {
        return AereDrandQuicknetBLS.decompressG1(compressedSig);
    }

    /// Canonical drand randomness for a compressed signature (SHA-256).
    function canonicalRandomness(bytes calldata compressedSig) external pure returns (bytes32) {
        return AereDrandQuicknetBLS.canonicalRandomness(compressedSig);
    }

    /// Read-only verify (does not store). Fail-closed; requires EIP-2537.
    function verifyRound(uint64 round, bytes calldata compressedSig) external view returns (bool) {
        return AereDrandQuicknetBLS.verifyRoundCompressed(round, compressedSig);
    }
}
