aere-contracts/contracts/pqc/AereMLDSA44Verifier.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

555 lines
25 KiB
Solidity

// 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);
}
}