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

/**
 * @title AereMLDSA44Verifier — on-chain NIST ML-DSA-44 (Dilithium2, FIPS 204) verifier
 * @notice A REAL, standalone module-lattice signature verifier that runs entirely on
 *         AERE chain 2800. Given an ML-DSA-44 public key, a message, and a signature,
 *         it performs the full FIPS 204 Verify_internal algorithm (Algorithm 8):
 *
 *           1. pkDecode: split pk into rho (32 bytes) and t1 (k=4 polynomials,
 *              coefficients packed 10 bits each, SimpleBitUnpack).
 *           2. sigDecode: split sigma into c~ (32 bytes), z (l=4 polynomials, 18-bit
 *              BitUnpack, z[i] = gamma1 - raw), and the hint h (omega+k=84 bytes,
 *              HintBitUnpack with strict-increasing / bound validation).
 *           3. ExpandA: sample the 4x4 matrix A_hat directly in the NTT domain from
 *              rho via SHAKE128 rejection sampling (CoeffFromThreeBytes).
 *           4. mu = H(H(pk,64) || M, 64) with H = SHAKE256; c = SampleInBall(c~).
 *           5. w'approx = NTT^{-1}(A_hat o NTT(z) - NTT(c) o NTT(t1 * 2^d)),
 *              a negacyclic ring product in Z_q[x]/(x^256+1), q = 8380417.
 *           6. w1 = UseHint(h, w'approx); c~' = H(mu || w1Encode(w1), 32).
 *           7. Accept iff ||z||_inf < gamma1 - beta (= 130994), the hint is valid,
 *              and c~' == c~.
 *
 *         This is the actual lattice/hash cryptography executed on-chain, not a
 *         proof-of-a-proof. The NTT here is over q = 8380417 (a different modulus and
 *         different roots than the Falcon verifiers' q = 12289), replicating the
 *         pq-crystals/dilithium reference NTT coefficient ordering so that the
 *         directly-sampled A_hat multiplies correctly against NTT(z).
 *
 *         VALIDATION: this verifier is validated bit-for-bit against the OFFICIAL NIST
 *         ACVP ML-DSA-sigVer-FIPS204 test vectors, test group 8 (ML-DSA-44, internal
 *         interface, externalMu = false): all 15 cases (3 valid + 12 crafted-invalid
 *         covering modified message / z / commitment / hint) reproduce NIST's expected
 *         testPassed values. See test/mldsa44Verifier.test.js and the deploy script.
 *
 * @dev Parameter set: ML-DSA-44 (Dilithium2) — q=8380417, n=256, (k,l)=(4,4), d=13,
 *      tau=39, gamma1=2^17, gamma2=(q-1)/88, eta=2, beta=78, omega=80, lambda=128.
 */
contract AereMLDSA44Verifier {
    // ----- ML-DSA-44 parameters -----
    uint256 internal constant Q = 8380417;
    uint256 internal constant N = 256;
    uint256 internal constant K = 4;
    uint256 internal constant L = 4;
    uint256 internal constant D = 13;
    uint256 internal constant TAU = 39;
    uint256 internal constant GAMMA1 = 131072; // 2^17
    uint256 internal constant GAMMA2 = 95232; // (q-1)/88
    uint256 internal constant BETA = 78; // tau*eta
    uint256 internal constant OMEGA = 80;
    uint256 internal constant M_HINT = 44; // (q-1)/(2*gamma2)
    uint256 internal constant Z_NORM_BOUND = 130994; // gamma1 - beta

    uint256 internal constant ROOT = 1753; // primitive 512th root of unity mod q
    uint256 internal constant INV256 = 8347681; // 256^-1 mod q
    uint256 internal constant TWO_D = 8192; // 2^d = 2^13

    // Encoded byte sizes
    uint256 internal constant PK_BYTES = 1312; // 32 + k*320
    uint256 internal constant SIG_BYTES = 2420; // 32 + l*576 + (omega+k)
    uint256 internal constant CTILDE_BYTES = 32; // lambda/4

    // SHAKE rates (bytes)
    uint256 internal constant RATE256 = 136; // SHAKE256
    uint256 internal constant RATE128 = 168; // SHAKE128

    // ----- recorded on-chain verification results (state-changing path) -----
    uint256 public verifyCount;
    bool public lastResult;
    event Verified(address indexed caller, bool result, uint256 index);

    // ==========================================================================
    //                       Keccak-f[1600] (FIPS 202)
    // ==========================================================================
    // Same assembly permutation used by AereFalcon512Verifier (validated against the
    // FIPS 202 SHAKE KAT). Rate-independent; SHAKE128/256 differ only in absorb rate.
    function _keccakf(uint64[25] memory st) internal pure {
        assembly {
            function rol(x, n) -> r {
                r := and(0xffffffffffffffff, or(shl(n, x), shr(sub(64, n), x)))
            }
            let s := st
            let b := mload(0x40)
            let rc := add(b, 0x320)
            mstore(add(rc, 0x000), 0x0000000000000001) mstore(add(rc, 0x020), 0x0000000000008082)
            mstore(add(rc, 0x040), 0x800000000000808a) mstore(add(rc, 0x060), 0x8000000080008000)
            mstore(add(rc, 0x080), 0x000000000000808b) mstore(add(rc, 0x0a0), 0x0000000080000001)
            mstore(add(rc, 0x0c0), 0x8000000080008081) mstore(add(rc, 0x0e0), 0x8000000000008009)
            mstore(add(rc, 0x100), 0x000000000000008a) mstore(add(rc, 0x120), 0x0000000000000088)
            mstore(add(rc, 0x140), 0x0000000080008009) mstore(add(rc, 0x160), 0x000000008000000a)
            mstore(add(rc, 0x180), 0x000000008000808b) mstore(add(rc, 0x1a0), 0x800000000000008b)
            mstore(add(rc, 0x1c0), 0x8000000000008089) mstore(add(rc, 0x1e0), 0x8000000000008003)
            mstore(add(rc, 0x200), 0x8000000000008002) mstore(add(rc, 0x220), 0x8000000000000080)
            mstore(add(rc, 0x240), 0x000000000000800a) mstore(add(rc, 0x260), 0x800000008000000a)
            mstore(add(rc, 0x280), 0x8000000080008081) mstore(add(rc, 0x2a0), 0x8000000000008080)
            mstore(add(rc, 0x2c0), 0x0000000080000001) mstore(add(rc, 0x2e0), 0x8000000080008008)
            for { let rnd := 0 } lt(rnd, 24) { rnd := add(rnd, 1) } {
                for { let x := 0 } lt(x, 5) { x := add(x, 1) } {
                    let o := mul(x, 0x20)
                    mstore(add(b, o),
                        xor(xor(xor(xor(
                            mload(add(s, o)),
                            mload(add(s, add(o, 0xa0)))),
                            mload(add(s, add(o, 0x140)))),
                            mload(add(s, add(o, 0x1e0)))),
                            mload(add(s, add(o, 0x280)))))
                }
                for { let x := 0 } lt(x, 5) { x := add(x, 1) } {
                    let cm := mload(add(b, mul(mod(add(x, 4), 5), 0x20)))
                    let cp := mload(add(b, mul(mod(add(x, 1), 5), 0x20)))
                    mstore(add(b, add(0xa0, mul(x, 0x20))), xor(cm, rol(cp, 1)))
                }
                for { let x := 0 } lt(x, 5) { x := add(x, 1) } {
                    let d := mload(add(b, add(0xa0, mul(x, 0x20))))
                    let o := mul(x, 0x20)
                    mstore(add(s, o), xor(mload(add(s, o)), d))
                    mstore(add(s, add(o, 0xa0)), xor(mload(add(s, add(o, 0xa0))), d))
                    mstore(add(s, add(o, 0x140)), xor(mload(add(s, add(o, 0x140))), d))
                    mstore(add(s, add(o, 0x1e0)), xor(mload(add(s, add(o, 0x1e0))), d))
                    mstore(add(s, add(o, 0x280)), xor(mload(add(s, add(o, 0x280))), d))
                }
                mstore(add(b, 0x000), rol(mload(add(s, 0x000)), 0))
                mstore(add(b, 0x140), rol(mload(add(s, 0x020)), 1))
                mstore(add(b, 0x280), rol(mload(add(s, 0x040)), 62))
                mstore(add(b, 0x0a0), rol(mload(add(s, 0x060)), 28))
                mstore(add(b, 0x1e0), rol(mload(add(s, 0x080)), 27))
                mstore(add(b, 0x200), rol(mload(add(s, 0x0a0)), 36))
                mstore(add(b, 0x020), rol(mload(add(s, 0x0c0)), 44))
                mstore(add(b, 0x160), rol(mload(add(s, 0x0e0)), 6))
                mstore(add(b, 0x2a0), rol(mload(add(s, 0x100)), 55))
                mstore(add(b, 0x0c0), rol(mload(add(s, 0x120)), 20))
                mstore(add(b, 0x0e0), rol(mload(add(s, 0x140)), 3))
                mstore(add(b, 0x220), rol(mload(add(s, 0x160)), 10))
                mstore(add(b, 0x040), rol(mload(add(s, 0x180)), 43))
                mstore(add(b, 0x180), rol(mload(add(s, 0x1a0)), 25))
                mstore(add(b, 0x2c0), rol(mload(add(s, 0x1c0)), 39))
                mstore(add(b, 0x2e0), rol(mload(add(s, 0x1e0)), 41))
                mstore(add(b, 0x100), rol(mload(add(s, 0x200)), 45))
                mstore(add(b, 0x240), rol(mload(add(s, 0x220)), 15))
                mstore(add(b, 0x060), rol(mload(add(s, 0x240)), 21))
                mstore(add(b, 0x1a0), rol(mload(add(s, 0x260)), 8))
                mstore(add(b, 0x1c0), rol(mload(add(s, 0x280)), 18))
                mstore(add(b, 0x300), rol(mload(add(s, 0x2a0)), 2))
                mstore(add(b, 0x120), rol(mload(add(s, 0x2c0)), 61))
                mstore(add(b, 0x260), rol(mload(add(s, 0x2e0)), 56))
                mstore(add(b, 0x080), rol(mload(add(s, 0x300)), 14))
                for { let yo := 0 } lt(yo, 0x320) { yo := add(yo, 0xa0) } {
                    let b0 := mload(add(b, yo))
                    let b1 := mload(add(b, add(yo, 0x20)))
                    let b2 := mload(add(b, add(yo, 0x40)))
                    let b3 := mload(add(b, add(yo, 0x60)))
                    let b4 := mload(add(b, add(yo, 0x80)))
                    mstore(add(s, yo), and(0xffffffffffffffff, xor(b0, and(not(b1), b2))))
                    mstore(add(s, add(yo, 0x20)), and(0xffffffffffffffff, xor(b1, and(not(b2), b3))))
                    mstore(add(s, add(yo, 0x40)), and(0xffffffffffffffff, xor(b2, and(not(b3), b4))))
                    mstore(add(s, add(yo, 0x60)), and(0xffffffffffffffff, xor(b3, and(not(b4), b0))))
                    mstore(add(s, add(yo, 0x80)), and(0xffffffffffffffff, xor(b4, and(not(b0), b1))))
                }
                mstore(add(s, 0), xor(mload(add(s, 0)), mload(add(rc, mul(rnd, 0x20)))))
            }
        }
    }

    /// @dev Absorb `input` into a fresh SHAKE sponge at the given rate (domain 0x1F,
    ///      pad10*1) and return the state after the final permutation. rate is a
    ///      multiple of 8 (136 for SHAKE256, 168 for SHAKE128).
    function _absorb(bytes memory input, uint256 rate) internal pure returns (uint64[25] memory st) {
        uint256 words = rate / 8;
        uint256 len = input.length;
        uint256 off = 0;
        while (len - off >= rate) {
            for (uint256 j = 0; j < words; j++) {
                uint64 lane = 0;
                uint256 base = off + j * 8;
                for (uint256 b = 0; b < 8; b++) {
                    lane |= uint64(uint8(input[base + b])) << (8 * b);
                }
                st[j] ^= lane;
            }
            _keccakf(st);
            off += rate;
        }
        uint256 rem = len - off;
        bytes memory blk = new bytes(rate);
        for (uint256 i = 0; i < rem; i++) {
            blk[i] = input[off + i];
        }
        blk[rem] = bytes1(uint8(blk[rem]) ^ 0x1F);
        blk[rate - 1] = bytes1(uint8(blk[rate - 1]) ^ 0x80);
        for (uint256 j = 0; j < words; j++) {
            uint64 lane = 0;
            uint256 base = j * 8;
            for (uint256 b = 0; b < 8; b++) {
                lane |= uint64(uint8(blk[base + b])) << (8 * b);
            }
            st[j] ^= lane;
        }
        _keccakf(st);
    }

    /// @dev Extract the current `rate`-byte rate block (little-endian lanes).
    function _squeezeBlock(uint64[25] memory st, uint256 rate) internal pure returns (bytes memory out) {
        uint256 words = rate / 8;
        out = new bytes(rate);
        for (uint256 j = 0; j < words; j++) {
            uint64 lane = st[j];
            for (uint256 b = 0; b < 8; b++) {
                out[j * 8 + b] = bytes1(uint8(lane >> (8 * b)));
            }
        }
    }

    /// @dev SHAKE(input) squeezed to `outLen` bytes at the given rate.
    function _shake(bytes memory input, uint256 rate, uint256 outLen) internal pure returns (bytes memory out) {
        uint64[25] memory st = _absorb(input, rate);
        out = new bytes(outLen);
        uint256 pos = 0;
        while (pos < outLen) {
            bytes memory blk = _squeezeBlock(st, rate);
            uint256 take = outLen - pos;
            if (take > rate) take = rate;
            for (uint256 i = 0; i < take; i++) {
                out[pos + i] = blk[i];
            }
            pos += take;
            if (pos < outLen) _keccakf(st);
        }
    }

    // ==========================================================================
    //                          NTT over Z_q[x]/(x^256+1)
    // ==========================================================================

    function _modpow(uint256 base, uint256 e) internal pure returns (uint256 r) {
        r = 1;
        base %= Q;
        while (e > 0) {
            if (e & 1 == 1) r = mulmod(r, base, Q);
            base = mulmod(base, base, Q);
            e >>= 1;
        }
    }

    /// @dev 8-bit reversal.
    function _brv8(uint256 i) internal pure returns (uint256 r) {
        for (uint256 b = 0; b < 8; b++) {
            r = (r << 1) | ((i >> b) & 1);
        }
    }

    /// @dev Plain zetas table: zetas[i] = ROOT^brv8(i) mod q. Replicates the
    ///      pq-crystals/dilithium ntt.c coefficient ordering (their table carries the
    ///      Montgomery factor 2^32 which montgomery_reduce strips, leaving exactly this).
    function _zetas() internal pure returns (uint256[256] memory z) {
        for (uint256 i = 0; i < 256; i++) {
            z[i] = _modpow(ROOT, _brv8(i));
        }
    }

    /// @dev In-place forward NTT (Cooley-Tukey), reference ordering, plain arithmetic.
    function _ntt(uint256[256] memory a, uint256[256] memory zt) internal pure {
        uint256 k = 0;
        for (uint256 len = 128; len >= 1; len >>= 1) {
            for (uint256 start = 0; start < 256; start += (len << 1)) {
                k++;
                uint256 zeta = zt[k];
                for (uint256 j = start; j < start + len; j++) {
                    uint256 t = mulmod(zeta, a[j + len], Q);
                    a[j + len] = addmod(a[j], Q - t, Q);
                    a[j] = addmod(a[j], t, Q);
                }
            }
        }
    }

    /// @dev In-place inverse NTT (Gentleman-Sande), reference ordering, plain
    ///      arithmetic; final scaling by 256^-1 (pure inverse, no residual factor).
    function _invntt(uint256[256] memory a, uint256[256] memory zt) internal pure {
        uint256 k = 256;
        for (uint256 len = 1; len < 256; len <<= 1) {
            for (uint256 start = 0; start < 256; start += (len << 1)) {
                k--;
                uint256 zeta = Q - zt[k]; // -zetas[k]
                for (uint256 j = start; j < start + len; j++) {
                    uint256 t = a[j];
                    uint256 u = a[j + len];
                    a[j] = addmod(t, u, Q);
                    a[j + len] = mulmod(zeta, addmod(t, Q - u, Q), Q);
                }
            }
        }
        for (uint256 i = 0; i < 256; i++) {
            a[i] = mulmod(a[i], INV256, Q);
        }
    }

    // ==========================================================================
    //                          Decode helpers
    // ==========================================================================

    /// @dev SimpleBitUnpack: 256 coefficients, `bitlen` bits each, LSB-first stream,
    ///      starting at `byteOff` in `data`. Returns raw unpacked values in [0,2^bitlen).
    function _unpack(bytes memory data, uint256 byteOff, uint256 bitlen)
        internal
        pure
        returns (uint256[256] memory out)
    {
        uint256 mask = (uint256(1) << bitlen) - 1;
        uint256 acc = 0;
        uint256 accBits = 0;
        uint256 p = byteOff;
        for (uint256 i = 0; i < 256; i++) {
            while (accBits < bitlen) {
                acc |= uint256(uint8(data[p])) << accBits;
                p++;
                accBits += 8;
            }
            out[i] = acc & mask;
            acc >>= bitlen;
            accBits -= bitlen;
        }
    }

    /// @dev HintBitUnpack (FIPS 204 Algorithm 21). y is the last omega+k bytes of the
    ///      signature. Returns (h, ok). ok=false on any malformed encoding.
    function _hintUnpack(bytes memory sig, uint256 off)
        internal
        pure
        returns (uint8[256][K] memory h, bool ok)
    {
        uint256 index = 0;
        for (uint256 i = 0; i < K; i++) {
            uint256 end = uint8(sig[off + OMEGA + i]);
            if (end < index || end > OMEGA) return (h, false);
            uint256 first = index;
            while (index < end) {
                if (index > first) {
                    if (uint8(sig[off + index - 1]) >= uint8(sig[off + index])) return (h, false);
                }
                h[i][uint8(sig[off + index])] = 1;
                index++;
            }
        }
        for (uint256 i = index; i < OMEGA; i++) {
            if (uint8(sig[off + i]) != 0) return (h, false);
        }
        return (h, true);
    }

    // ==========================================================================
    //                    ExpandA / SampleInBall / UseHint
    // ==========================================================================

    /// @dev RejNTTPoly (FIPS 204 Algorithm 14): SHAKE128 rejection sampling into an
    ///      NTT-domain polynomial. seed = rho || s || r (34 bytes).
    function _rejNTTPoly(bytes memory seed) internal pure returns (uint256[256] memory a) {
        uint64[25] memory st = _absorb(seed, RATE128);
        bytes memory blk = _squeezeBlock(st, RATE128);
        uint256 bi = 0;
        uint256 j = 0;
        while (j < 256) {
            if (bi == RATE128) {
                _keccakf(st);
                blk = _squeezeBlock(st, RATE128);
                bi = 0;
            }
            // RATE128 (168) is divisible by 3, so 3-byte groups never straddle a block.
            uint256 z = (uint256(uint8(blk[bi + 2])) & 0x7F) << 16
                | uint256(uint8(blk[bi + 1])) << 8
                | uint256(uint8(blk[bi]));
            bi += 3;
            if (z < Q) {
                a[j] = z;
                j++;
            }
        }
    }

    /// @dev SampleInBall (FIPS 204 Algorithm 29): challenge polynomial c with tau
    ///      nonzero +/-1 coefficients, from SHAKE256(c~). Returns c as coeffs mod q.
    function _sampleInBall(bytes memory ctilde) internal pure returns (uint256[256] memory c) {
        uint64[25] memory st = _absorb(ctilde, RATE256);
        bytes memory blk = _squeezeBlock(st, RATE256);
        uint256 signs = 0;
        for (uint256 i = 0; i < 8; i++) {
            signs |= uint256(uint8(blk[i])) << (8 * i);
        }
        uint256 bi = 8;
        for (uint256 i = 256 - TAU; i < 256; i++) {
            uint256 jb;
            while (true) {
                if (bi == RATE256) {
                    _keccakf(st);
                    blk = _squeezeBlock(st, RATE256);
                    bi = 0;
                }
                jb = uint8(blk[bi]);
                bi++;
                if (jb <= i) break;
            }
            c[i] = c[jb];
            c[jb] = (signs & 1) == 1 ? Q - 1 : 1; // -1 mod q or +1
            signs >>= 1;
        }
    }

    /// @dev Decompose (FIPS 204 Algorithm 36) then UseHint (Algorithm 32) for a single
    ///      coefficient r in [0,q). Returns w1 in [0, M_HINT).
    function _useHint(uint8 hint, uint256 r) internal pure returns (uint256) {
        uint256 twoG = 2 * GAMMA2;
        uint256 rp = r % Q;
        uint256 r0m = rp % twoG;
        int256 r0 = r0m > GAMMA2 ? int256(r0m) - int256(twoG) : int256(r0m);
        int256 r1;
        if (int256(rp) - r0 == int256(Q) - 1) {
            r1 = 0;
            r0 = r0 - 1;
        } else {
            r1 = (int256(rp) - r0) / int256(twoG);
        }
        if (hint == 0) {
            return uint256(r1);
        }
        if (r0 > 0) {
            return uint256((r1 + 1 + int256(M_HINT)) % int256(M_HINT));
        } else {
            return uint256((r1 - 1 + int256(M_HINT)) % int256(M_HINT));
        }
    }

    // ==========================================================================
    //                          w1Encode
    // ==========================================================================

    /// @dev w1Encode: SimpleBitPack each of the k polynomials with 6 bits/coeff,
    ///      LSB-first. Returns 768 bytes (k*192).
    function _w1Encode(uint256[256][K] memory w1) internal pure returns (bytes memory out) {
        out = new bytes(K * 192);
        uint256 outPos = 0;
        uint256 acc = 0;
        uint256 accBits = 0;
        for (uint256 i = 0; i < K; i++) {
            for (uint256 t = 0; t < 256; t++) {
                acc |= (w1[i][t] & 0x3F) << accBits;
                accBits += 6;
                while (accBits >= 8) {
                    out[outPos] = bytes1(uint8(acc & 0xFF));
                    outPos++;
                    acc >>= 8;
                    accBits -= 8;
                }
            }
        }
    }

    // ==========================================================================
    //                              Verify
    // ==========================================================================

    /// @notice ML-DSA-44 Verify_internal (FIPS 204 Algorithm 8), internal interface,
    ///         externalMu = false: mu is computed from tr = H(pk) and the message M.
    /// @param pk  1312-byte ML-DSA-44 public key (rho || t1).
    /// @param message  the signed message M (= M' for the internal interface).
    /// @param sig 2420-byte ML-DSA-44 signature (c~ || z || h).
    /// @return ok true iff the signature is valid.
    function verify(bytes memory pk, bytes memory message, bytes memory sig)
        public
        pure
        returns (bool ok)
    {
        if (pk.length != PK_BYTES || sig.length != SIG_BYTES) return false;

        uint256[256] memory zt = _zetas();

        // --- sigDecode: hint ---
        (uint8[256][K] memory h, bool okH) = _hintUnpack(sig, 32 + L * 576);
        if (!okH) return false;

        // --- sigDecode: z (and ||z||_inf check) ---
        uint256[256][L] memory zhat;
        for (uint256 jj = 0; jj < L; jj++) {
            uint256[256] memory raw = _unpack(sig, 32 + jj * 576, 18);
            for (uint256 t = 0; t < 256; t++) {
                // z = gamma1 - raw ; centered abs and mod-q reduction
                int256 zc = int256(GAMMA1) - int256(raw[t]);
                uint256 absz = zc < 0 ? uint256(-zc) : uint256(zc);
                if (absz >= Z_NORM_BOUND) return false;
                zhat[jj][t] = zc < 0 ? uint256(int256(Q) + zc) : uint256(zc);
            }
            _ntt(zhat[jj], zt);
        }

        // --- mu = H(H(pk,64) || M, 64) ---
        bytes memory tr = _shake(pk, RATE256, 64);
        bytes memory mu = _shake(bytes.concat(tr, message), RATE256, 64);

        // --- c = SampleInBall(c~) then NTT ---
        bytes memory ctilde = new bytes(CTILDE_BYTES);
        for (uint256 i = 0; i < CTILDE_BYTES; i++) ctilde[i] = sig[i];
        uint256[256] memory chat = _sampleInBall(ctilde);
        _ntt(chat, zt);

        // --- w = NTT^-1(A_hat o zhat - chat o NTT(t1<<d)); w1 = UseHint(h, w) ---
        bytes memory rho = new bytes(32);
        for (uint256 i = 0; i < 32; i++) rho[i] = pk[i];

        uint256[256][K] memory w1;
        for (uint256 i = 0; i < K; i++) {
            uint256[256] memory acc;
            for (uint256 jj = 0; jj < L; jj++) {
                uint256[256] memory Aij = _rejNTTPoly(bytes.concat(rho, bytes1(uint8(jj)), bytes1(uint8(i))));
                for (uint256 t = 0; t < 256; t++) {
                    acc[t] = addmod(acc[t], mulmod(Aij[t], zhat[jj][t], Q), Q);
                }
            }
            // t1[i] << d, then NTT
            uint256[256] memory t1i = _unpack(pk, 32 + i * 320, 10);
            for (uint256 t = 0; t < 256; t++) {
                t1i[t] = mulmod(t1i[t], TWO_D, Q);
            }
            _ntt(t1i, zt);
            uint256[256] memory wpoly;
            for (uint256 t = 0; t < 256; t++) {
                uint256 ct1 = mulmod(chat[t], t1i[t], Q);
                wpoly[t] = addmod(acc[t], Q - ct1, Q);
            }
            _invntt(wpoly, zt);
            for (uint256 t = 0; t < 256; t++) {
                w1[i][t] = _useHint(h[i][t], wpoly[t]);
            }
        }

        // --- c~' = H(mu || w1Encode(w1), 32) ; compare ---
        bytes memory ctilde2 = _shake(bytes.concat(mu, _w1Encode(w1)), RATE256, CTILDE_BYTES);
        for (uint256 i = 0; i < CTILDE_BYTES; i++) {
            if (ctilde2[i] != sig[i]) return false;
        }
        return true;
    }

    /// @notice State-changing wrapper: verify and record the result on-chain.
    /// @dev A full ML-DSA-44 verify is heavy; if the estimate exceeds the EIP-7825
    ///      per-tx gas cap this cannot be mined as an L1 tx (use verify() via eth_call).
    function verifyAndRecord(bytes memory pk, bytes memory message, bytes memory sig)
        external
        returns (bool result)
    {
        result = verify(pk, message, sig);
        lastResult = result;
        uint256 idx = verifyCount;
        verifyCount = idx + 1;
        emit Verified(msg.sender, result, idx);
    }
}
