aere-contracts/contracts/pqc/AereFalcon1024Verifier.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

507 lines
24 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
/**
* @title AereFalcon1024Verifier, spec-complete NIST Falcon-1024 signature verifier, on-chain
* @notice A REAL, standalone Falcon-1024 (FIPS 206 / NIST PQC round-3) signature
* verifier that runs entirely on chain 2800. It is the degree-1024
* re-parameterization of the proven AereFalcon512Verifier: same modulus
* q = 12289 and same negacyclic-NTT verification pipeline, but over the
* ring Z_q[x]/(x^1024+1), with the higher Falcon-1024 acceptance bound,
* the 1793-byte public-key decode, and fresh 2048th-root NTT constants.
*
* Given a public key, a message, and a signature it performs the full
* Falcon verification:
*
* 1. HashToPoint: SHAKE256(nonce || message) is streamed and
* rejection-sampled into a challenge polynomial c in Z_q[x]/(x^1024+1)
* (q = 12289), exactly per the reference hash_to_point_vartime.
* 2. Public-key decode: the 1793-byte NIST public key (0x0A header +
* 1024 coefficients packed at 14 bits each) is decoded to h.
* 3. Signature decode: the compressed Gaussian encoding of s2 is decoded
* (Falcon comp_decode: sign bit, 7 low bits, unary high bits).
* 4. Recompute -s1 = s2*h - c in the ring (negacyclic convolution mod q),
* then center coefficients into (-q/2, q/2].
* 5. Norm check: accept iff ||(s1, s2)||^2 <= floor(beta^2) = 70265242,
* the Falcon-1024 acceptance bound.
*
* This is the actual lattice/hash cryptography, executed on-chain, not a
* proof-of-a-proof. It is validated bit-for-bit against the OFFICIAL NIST
* Falcon-1024 Known-Answer-Test vectors (falcon1024-KAT.rsp, SHA-256
* 036a0bf5...b20d, from the round-3 submission): real (pk, message,
* signature) triples verify true; tampered signatures verify false.
*
* The chain's block gas limit is effectively unlimited, so the full
* verification runs as an ordinary transaction; there is no trusted
* prover and no off-chain step. Anyone can call verify() and get the same
* accept/reject the reference C implementation produces.
*/
contract AereFalcon1024Verifier {
// ----- Falcon-1024 parameters -----
uint256 public constant Q = 12289; // modulus
uint256 public constant N = 1024; // ring degree
uint256 public constant L2BOUND = 70265242; // floor(beta^2), Falcon-1024 acceptance bound
uint256 internal constant HQ = 6144; // floor(q/2), centering threshold
// Negacyclic NTT constants for Z_q[x]/(x^1024+1), q = 12289:
// PSI = primitive 2048th (2n-th) root of unity; W = PSI^2 (1024th root)
// Derived from generator g = 11: PSI = g^(12288/2048) = g^6 mod q.
// Self-checked: PSI^1024 == q-1 (-1), PSI^2048 == 1, W^512 == q-1, W^1024 == 1.
uint256 internal constant PSI = 1945;
uint256 internal constant PSI_INV = 4050;
uint256 internal constant W = 10302; // PSI^2 mod q
uint256 internal constant W_INV = 8974;
uint256 internal constant N_INV = 12277; // 1024^-1 mod q
uint8 public constant PK_HEADER = 0x0A; // 0x00 + logn, logn = 10
uint8 public constant SIG_HEADER = 0x2A; // 0x20 + logn, logn = 10 (NIST esig header)
uint256 public constant NONCE_LEN = 40;
// ----- recorded on-chain verification results (for state-changing calls) -----
uint256 public verifyCount;
bool public lastResult;
event Verified(address indexed caller, bool result, uint256 index);
// ==========================================================================
// Keccak-f[1600] / SHAKE256
// ==========================================================================
/// @dev In-place Keccak-f[1600] permutation on a 25-lane state (one lane per
/// 32-byte memory word, low 64 bits used). Written in assembly for gas:
/// the naive array version cost ~1.26M gas per permutation. Validated
/// against the FIPS 202 SHAKE256 KAT and the Falcon HashToPoint oracle.
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) // scratch (25 lanes), left above the free pointer
let rc := add(b, 0x320) // 24 round constants after the scratch lanes
// round constants
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) } {
// ---- theta: column parities C[x] -> b[0..4] ----
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)))))
}
// D[x] = C[(x+4)%5] ^ rol(C[(x+1)%5],1) -> b[5..9]
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)))
}
// apply D to state: lane (x + 5y) ^= D[x]
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))
}
// ---- rho + pi: b[piDest[i]] = rol(st[i], rho[i]) ----
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))
// ---- chi: st[y+x] = b[y+x] ^ ((~b[y+(x+1)%5]) & b[y+(x+2)%5]) ----
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))))
}
// ---- iota ----
mstore(add(s, 0), xor(mload(add(s, 0)), mload(add(rc, mul(rnd, 0x20)))))
}
}
}
/// @dev Absorb `input` into a fresh SHAKE256 sponge (rate 136 bytes, domain
/// 0x1F, pad10*1) and return the state after the final permutation.
function _shakeAbsorb(bytes memory input) internal pure returns (uint64[25] memory st) {
uint256 len = input.length;
uint256 off = 0;
// full 136-byte blocks
while (len - off >= 136) {
for (uint256 j = 0; j < 17; 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 += 136;
}
// final (partial) block with padding
uint256 rem = len - off;
bytes memory blk = new bytes(136);
for (uint256 i = 0; i < rem; i++) {
blk[i] = input[off + i];
}
blk[rem] = bytes1(uint8(blk[rem]) ^ 0x1F); // domain separation
blk[135] = bytes1(uint8(blk[135]) ^ 0x80); // final bit of pad10*1
for (uint256 j = 0; j < 17; 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 136-byte rate block (little-endian lanes 0..16).
function _rateBlock(uint64[25] memory st) internal pure returns (bytes memory out) {
out = new bytes(136);
for (uint256 j = 0; j < 17; j++) {
uint64 lane = st[j];
for (uint256 b = 0; b < 8; b++) {
out[j * 8 + b] = bytes1(uint8(lane >> (8 * b)));
}
}
}
/// @notice Squeeze `outLen` bytes of SHAKE256(input). Exposed for KAT testing.
function shake256(bytes memory input, uint256 outLen) public pure returns (bytes memory out) {
uint64[25] memory st = _shakeAbsorb(input);
out = new bytes(outLen);
uint256 pos = 0;
while (pos < outLen) {
bytes memory blk = _rateBlock(st);
uint256 take = outLen - pos;
if (take > 136) take = 136;
for (uint256 i = 0; i < take; i++) {
out[pos + i] = blk[i];
}
pos += take;
if (pos < outLen) _keccakf(st);
}
}
// ==========================================================================
// HashToPoint
// ==========================================================================
/// @notice HashToPoint(nonce || message) -> challenge polynomial c (1024 coeffs).
/// @dev Streams SHAKE256 two bytes at a time; keeps a big-endian 16-bit sample
/// when it is < 5*q = 61445, reducing it mod q. Matches the reference
/// hash_to_point_vartime exactly.
function hashToPoint(bytes memory nonce, bytes memory message) public pure returns (uint256[1024] memory cc) {
bytes memory input = bytes.concat(nonce, message);
uint64[25] memory st = _shakeAbsorb(input);
bytes memory blk = _rateBlock(st);
uint256 bi = 0; // byte index into current rate block (0..135)
uint256 filled = 0;
while (filled < 1024) {
// pull two bytes, refilling / permuting when the rate block is exhausted
if (bi == 136) {
_keccakf(st);
blk = _rateBlock(st);
bi = 0;
}
uint8 hi = uint8(blk[bi]);
bi++;
if (bi == 136) {
_keccakf(st);
blk = _rateBlock(st);
bi = 0;
}
uint8 lo = uint8(blk[bi]);
bi++;
uint256 w = (uint256(hi) << 8) | uint256(lo);
if (w < 61445) {
cc[filled] = w % Q;
filled++;
}
}
}
// ==========================================================================
// Public key / signature decode
// ==========================================================================
/// @notice Decode a 1793-byte Falcon-1024 public key into h (1024 coeffs).
/// @return h the 1024 coefficients, ok false if the encoding is invalid.
function decodePublicKey(bytes memory pk) public pure returns (uint256[1024] memory h, bool ok) {
if (pk.length != 1793 || uint8(pk[0]) != PK_HEADER) {
return (h, false);
}
uint256 acc = 0;
uint256 accLen = 0;
uint256 u = 0;
// 1792 encoded bytes = 1024 * 14 bits, exact
for (uint256 i = 1; i < 1793 && u < 1024; i++) {
acc = ((acc << 8) | uint256(uint8(pk[i]))) & 0xFFFFFFFF;
accLen += 8;
if (accLen >= 14) {
accLen -= 14;
uint256 w = (acc >> accLen) & 0x3FFF;
if (w >= Q) return (h, false);
h[u] = w;
u++;
}
}
if (u != 1024) return (h, false);
// trailing bits must be zero
if ((acc & ((uint256(1) << accLen) - 1)) != 0) return (h, false);
return (h, true);
}
/// @notice Decode the compressed Falcon signature bytes into s2 (1024 signed coeffs).
/// @dev Mirrors the reference comp_decode: for each coefficient read a byte
/// giving sign + 7 low bits, then read bits until a 1 for the high part.
/// Rejects "-0", overflow (>2047), running off the buffer, or nonzero
/// trailing bits. Requires the whole buffer to be consumed.
/// @return s2 signed coefficients (centered), ok false if invalid.
function decodeSignature(bytes memory sig) public pure returns (int256[1024] memory s2, bool ok) {
uint256 acc = 0;
uint256 accLen = 0;
uint256 v = 0;
uint256 maxLen = sig.length;
for (uint256 u = 0; u < 1024; u++) {
if (v >= maxLen) return (s2, false);
acc = ((acc << 8) | uint256(uint8(sig[v]))) & 0xFFFFFFFF;
v++;
uint256 bb = acc >> accLen;
uint256 s = bb & 128;
uint256 m = bb & 127;
// read high bits until a set bit
while (true) {
if (accLen == 0) {
if (v >= maxLen) return (s2, false);
acc = ((acc << 8) | uint256(uint8(sig[v]))) & 0xFFFFFFFF;
v++;
accLen = 8;
}
accLen--;
if (((acc >> accLen) & 1) != 0) break;
m += 128;
if (m > 2047) return (s2, false);
}
if (s != 0 && m == 0) return (s2, false); // "-0" forbidden
s2[u] = s != 0 ? -int256(m) : int256(m);
}
// unused bits in the last consumed byte must be zero
if ((acc & ((uint256(1) << accLen) - 1)) != 0) return (s2, false);
// the whole signature buffer must be consumed exactly
if (v != maxLen) return (s2, false);
return (s2, true);
}
// ==========================================================================
// Ring arithmetic + norm
// ==========================================================================
/// @dev b^e mod q.
function _modpow(uint256 b, uint256 e) internal pure returns (uint256 r) {
r = 1;
b %= Q;
while (e > 0) {
if (e & 1 == 1) r = mulmod(r, b, Q);
b = mulmod(b, b, Q);
e >>= 1;
}
}
/// @dev In-place bit-reversal permutation of a length-1024 array.
function _bitrev(uint256[1024] memory a) internal pure {
uint256 j = 0;
for (uint256 i = 1; i < 1024; i++) {
uint256 bit = 512; // n >> 1
while (j & bit != 0) {
j ^= bit;
bit >>= 1;
}
j ^= bit;
if (i < j) {
uint256 tmp = a[i];
a[i] = a[j];
a[j] = tmp;
}
}
}
/// @dev Iterative in-place NTT (natural order in and out) with the given root.
function _ntt(uint256[1024] memory a, uint256 root) internal pure {
_bitrev(a);
for (uint256 len = 2; len <= 1024; len <<= 1) {
uint256 wlen = _modpow(root, 1024 / len);
uint256 half = len >> 1;
for (uint256 i = 0; i < 1024; i += len) {
uint256 wn = 1;
for (uint256 k = i; k < i + half; k++) {
uint256 vv = mulmod(a[k + half], wn, Q);
uint256 u = a[k];
a[k] = addmod(u, vv, Q);
a[k + half] = addmod(u, Q - vv, Q);
wn = mulmod(wn, wlen, Q);
}
}
}
}
/// @dev Negacyclic convolution t = a*b in Z_q[x]/(x^1024+1) via NTT.
/// Pre-twist by PSI^i, length-1024 NTT (root W), pointwise multiply,
/// inverse NTT, post-twist by PSI^-i and multiply by n^-1.
function _ringMul(uint256[1024] memory A, uint256[1024] memory B) internal pure returns (uint256[1024] memory r) {
uint256[1024] memory a = A;
uint256[1024] memory b = B;
uint256 p = 1;
for (uint256 i = 0; i < 1024; i++) {
a[i] = mulmod(a[i] % Q, p, Q);
b[i] = mulmod(b[i] % Q, p, Q);
p = mulmod(p, PSI, Q);
}
_ntt(a, W);
_ntt(b, W);
for (uint256 i = 0; i < 1024; i++) {
a[i] = mulmod(a[i], b[i], Q);
}
_ntt(a, W_INV);
// post-twist: r[i] = a[i] * n^-1 * PSI^-i
uint256 pinv = N_INV; // n^-1 * PSI^-0
for (uint256 i = 0; i < 1024; i++) {
r[i] = mulmod(a[i], pinv, Q);
pinv = mulmod(pinv, PSI_INV, Q);
}
}
// ==========================================================================
// Verification
// ==========================================================================
/// @notice Verify a Falcon-1024 signature given decomposed inputs.
/// @param pk 1793-byte NIST public key (0x0A header + packed h).
/// @param message the signed message bytes.
/// @param nonce 40-byte salt r.
/// @param compSig compressed encoding of s2 (comp_encode output).
/// @return ok true iff the signature is a valid Falcon-1024 signature.
function verify(
bytes memory pk,
bytes memory message,
bytes memory nonce,
bytes memory compSig
) public pure returns (bool ok) {
if (nonce.length != NONCE_LEN) return false;
(uint256[1024] memory h, bool okPk) = decodePublicKey(pk);
if (!okPk) return false;
(int256[1024] memory s2, bool okSig) = decodeSignature(compSig);
if (!okSig) return false;
uint256[1024] memory cc = hashToPoint(nonce, message);
// s2 reduced into [0, q)
uint256[1024] memory s2m;
for (uint256 i = 0; i < 1024; i++) {
int256 x = s2[i] % int256(Q);
if (x < 0) x += int256(Q);
s2m[i] = uint256(x);
}
// t = s2 * h (mod q, ring)
uint256[1024] memory t = _ringMul(s2m, h);
// s1 = c - t, centered into (-q/2, q/2]; accumulate squared norm
uint256 norm = 0;
for (uint256 i = 0; i < 1024; i++) {
uint256 val = addmod(cc[i], Q - t[i], Q); // (c - t) mod q, in [0,q)
int256 s1 = val > HQ ? int256(val) - int256(Q) : int256(val);
norm += uint256(s1 * s1);
int256 z = s2[i];
norm += uint256(z * z);
if (norm > L2BOUND) return false; // early out (also guards accumulation)
}
return norm <= L2BOUND;
}
/// @notice Verify a Falcon-1024 signature from a raw NIST "signed message" (sm)
/// blob plus the public key. This consumes the exact bytes found in
/// the official KAT: sm = sigLen(2, big-endian) || nonce(40) ||
/// message || esig, where esig = 0x2A || compSig.
function verifySignedMessage(bytes memory pk, bytes memory sm) public pure returns (bool ok) {
uint256 smlen = sm.length;
if (smlen < 2 + NONCE_LEN) return false;
uint256 sigLen = (uint256(uint8(sm[0])) << 8) | uint256(uint8(sm[1]));
if (sigLen > smlen - 2 - NONCE_LEN) return false;
uint256 msgLen = smlen - 2 - NONCE_LEN - sigLen;
if (sigLen < 1) return false;
// esig begins at 2 + NONCE_LEN + msgLen; its first byte is the header
uint256 esigStart = 2 + NONCE_LEN + msgLen;
if (uint8(sm[esigStart]) != SIG_HEADER) return false;
bytes memory nonce = new bytes(NONCE_LEN);
for (uint256 i = 0; i < NONCE_LEN; i++) nonce[i] = sm[2 + i];
bytes memory message = new bytes(msgLen);
for (uint256 i = 0; i < msgLen; i++) message[i] = sm[2 + NONCE_LEN + i];
bytes memory compSig = new bytes(sigLen - 1);
for (uint256 i = 0; i < sigLen - 1; i++) compSig[i] = sm[esigStart + 1 + i];
return verify(pk, message, nonce, compSig);
}
/// @notice State-changing wrapper: verify and record the result on-chain.
function verifyAndRecord(bytes memory pk, bytes memory sm) external returns (bool result) {
result = verifySignedMessage(pk, sm);
lastResult = result;
uint256 idx = verifyCount;
verifyCount = idx + 1;
emit Verified(msg.sender, result, idx);
}
}