mirror of
https://github.com/luxfi/crypto.git
synced 2026-07-27 01:54:50 +00:00
263 lines
6.7 KiB
Go
263 lines
6.7 KiB
Go
// Copyright (C) 2020-2026, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package poly_mul implements polynomial multiplication over the
|
|
// canonical FFT prime Q = 998244353 = 119 * 2^23 + 1.
|
|
//
|
|
// Two paths are exposed:
|
|
//
|
|
// - Schoolbook O(n^2) -- always correct, fastest below n=64
|
|
// - NTT-based O(n log n) for the negacyclic convolution that lattice
|
|
// cryptography (ML-KEM, ML-DSA, Corona) uses
|
|
//
|
|
// Q = 998244353 is the canonical FFT-friendly prime used by AtCoder, Cyclone-FFT,
|
|
// the SPOJ POLYMUL reference, and most competitive-programming NTT references.
|
|
// Its 2-adicity is 23, so the NTT supports lengths up to N = 2^23.
|
|
//
|
|
// Negacyclic convolution: c[k] = sum_{i+j=k} a[i]*b[j]
|
|
// - sum_{i+j=k+n} a[i]*b[j]
|
|
//
|
|
// over Z_Q[X]/(X^n + 1) with q the prime above. This is the exact shape
|
|
// used by lattice schemes; cyclic convolution is recovered by padding.
|
|
//
|
|
// All inputs/outputs are reduced mod Q. Inputs above Q are reduced on
|
|
// entry; outputs are always strictly less than Q.
|
|
package poly_mul
|
|
|
|
import (
|
|
"errors"
|
|
"math/bits"
|
|
)
|
|
|
|
// Q is the canonical FFT prime used here: 998244353 = 119 * 2^23 + 1.
|
|
// 2-adicity = 23 -> NTT length up to 2^23.
|
|
const Q uint64 = 998244353
|
|
|
|
// PrimitiveRoot is a 2^MaxLogN-th primitive root of unity in F_Q.
|
|
// Computed as 3^((Q-1)/2^MaxLogN) mod Q where 3 is a primitive root of F_Q.
|
|
const PrimitiveRoot uint64 = 629671588
|
|
|
|
// MaxLogN is the largest log2(n) supported by the NTT path.
|
|
// Q's 2-adicity is 23 so technically up to 23 is supported; we cap at 16
|
|
// (n=65536) which is far above ML-KEM (256), ML-DSA (256), Corona's typical
|
|
// dimensions.
|
|
const MaxLogN = 16
|
|
|
|
// ErrLength is returned when slice lengths violate the API contract.
|
|
var ErrLength = errors.New("poly_mul: invalid length")
|
|
|
|
// addMod returns (a + b) mod Q. Inputs may be in [0, Q-1].
|
|
// Since Q < 2^30, addition cannot overflow uint64.
|
|
func addMod(a, b uint64) uint64 {
|
|
s := a + b
|
|
if s >= Q {
|
|
s -= Q
|
|
}
|
|
return s
|
|
}
|
|
|
|
// subMod returns (a - b) mod Q. Inputs may be in [0, Q-1].
|
|
func subMod(a, b uint64) uint64 {
|
|
if a >= b {
|
|
return a - b
|
|
}
|
|
return a + Q - b
|
|
}
|
|
|
|
// mulMod returns (a * b) mod Q. Q < 2^30 so a*b < 2^60 fits in uint64.
|
|
// One full-width 64-bit multiply and one division.
|
|
func mulMod(a, b uint64) uint64 {
|
|
return (a * b) % Q
|
|
}
|
|
|
|
// powMod returns base^exp mod Q via square-and-multiply.
|
|
func powMod(base, exp uint64) uint64 {
|
|
result := uint64(1)
|
|
b := base % Q
|
|
e := exp
|
|
for e > 0 {
|
|
if e&1 == 1 {
|
|
result = mulMod(result, b)
|
|
}
|
|
b = mulMod(b, b)
|
|
e >>= 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
// invMod returns the modular inverse of a in F_Q via Fermat. Assumes a != 0.
|
|
func invMod(a uint64) uint64 {
|
|
return powMod(a, Q-2)
|
|
}
|
|
|
|
// rootOfUnity returns a primitive n-th root of unity for n = 2^logN, logN in [0, MaxLogN].
|
|
func rootOfUnity(logN int) uint64 {
|
|
if logN < 0 || logN > MaxLogN {
|
|
panic("poly_mul: log_n out of range")
|
|
}
|
|
step := uint64(1) << (MaxLogN - logN)
|
|
return powMod(PrimitiveRoot, step)
|
|
}
|
|
|
|
// bitReverseCopy permutes data in place using the bit-reversal permutation.
|
|
func bitReverseCopy(data []uint64) {
|
|
n := len(data)
|
|
if n <= 1 {
|
|
return
|
|
}
|
|
logN := bits.TrailingZeros(uint(n))
|
|
for i := 0; i < n; i++ {
|
|
j := int(bits.Reverse(uint(i)) >> (bits.UintSize - logN))
|
|
if i < j {
|
|
data[i], data[j] = data[j], data[i]
|
|
}
|
|
}
|
|
}
|
|
|
|
// ntt performs an iterative Cooley-Tukey NTT in place. forward=false runs
|
|
// the inverse direction (no n^-1 scaling here -- caller multiplies).
|
|
func ntt(data []uint64, forward bool) error {
|
|
n := len(data)
|
|
if n == 0 || n&(n-1) != 0 || n > (1<<MaxLogN) {
|
|
return ErrLength
|
|
}
|
|
logN := bits.TrailingZeros(uint(n))
|
|
bitReverseCopy(data)
|
|
for s := 1; s <= logN; s++ {
|
|
m := 1 << s
|
|
var wm uint64
|
|
if forward {
|
|
wm = rootOfUnity(s)
|
|
} else {
|
|
wm = invMod(rootOfUnity(s))
|
|
}
|
|
for k := 0; k < n; k += m {
|
|
w := uint64(1)
|
|
for j := 0; j < m/2; j++ {
|
|
t := mulMod(w, data[k+j+m/2])
|
|
u := data[k+j]
|
|
data[k+j] = addMod(u, t)
|
|
data[k+j+m/2] = subMod(u, t)
|
|
w = mulMod(w, wm)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// NTTForward performs a forward NTT in place. data must have power-of-2
|
|
// length up to 2^MaxLogN.
|
|
func NTTForward(data []uint64) error {
|
|
return ntt(data, true)
|
|
}
|
|
|
|
// NTTInverse performs an inverse NTT in place, including the 1/n scaling.
|
|
func NTTInverse(data []uint64) error {
|
|
if err := ntt(data, false); err != nil {
|
|
return err
|
|
}
|
|
nInv := invMod(uint64(len(data)))
|
|
for i := range data {
|
|
data[i] = mulMod(data[i], nInv)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// reduce reduces a slice element-wise mod Q (returns a fresh copy).
|
|
func reduce(a []uint64) []uint64 {
|
|
out := make([]uint64, len(a))
|
|
for i, v := range a {
|
|
out[i] = v % Q
|
|
}
|
|
return out
|
|
}
|
|
|
|
// MulSchoolbook computes a*b in Z_Q[X]/(X^n + 1) via O(n^2) schoolbook.
|
|
// Reference path: every other path is tested against this.
|
|
func MulSchoolbook(a, b []uint64) ([]uint64, error) {
|
|
n := len(a)
|
|
if n == 0 || len(b) != n {
|
|
return nil, ErrLength
|
|
}
|
|
ar := reduce(a)
|
|
br := reduce(b)
|
|
c := make([]uint64, n)
|
|
for i := 0; i < n; i++ {
|
|
for j := 0; j < n; j++ {
|
|
prod := mulMod(ar[i], br[j])
|
|
k := i + j
|
|
if k < n {
|
|
c[k] = addMod(c[k], prod)
|
|
} else {
|
|
// Negacyclic wrap: X^n = -1
|
|
c[k-n] = subMod(c[k-n], prod)
|
|
}
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// MulNTT computes a*b in Z_Q[X]/(X^n + 1) via NTT-based negacyclic
|
|
// convolution. n must be a power of 2 in [2, 2^MaxLogN].
|
|
//
|
|
// For negacyclic convolution we need a 2n-th primitive root psi.
|
|
// Pre-multiply a[i] by psi^i and b[i] by psi^i, run length-n NTT, do
|
|
// pointwise multiply, inverse NTT, and post-multiply by psi^-i.
|
|
func MulNTT(a, b []uint64) ([]uint64, error) {
|
|
n := len(a)
|
|
if n == 0 || n&(n-1) != 0 || n > (1<<MaxLogN) || len(b) != n {
|
|
return nil, ErrLength
|
|
}
|
|
logN := bits.TrailingZeros(uint(n))
|
|
if logN+1 > MaxLogN {
|
|
return nil, ErrLength
|
|
}
|
|
psi := rootOfUnity(logN + 1)
|
|
psiInv := invMod(psi)
|
|
|
|
aw := reduce(a)
|
|
bw := reduce(b)
|
|
psiPow := uint64(1)
|
|
for i := 0; i < n; i++ {
|
|
aw[i] = mulMod(aw[i], psiPow)
|
|
bw[i] = mulMod(bw[i], psiPow)
|
|
psiPow = mulMod(psiPow, psi)
|
|
}
|
|
|
|
if err := NTTForward(aw); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := NTTForward(bw); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c := make([]uint64, n)
|
|
for i := 0; i < n; i++ {
|
|
c[i] = mulMod(aw[i], bw[i])
|
|
}
|
|
|
|
if err := NTTInverse(c); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
psiInvPow := uint64(1)
|
|
for i := 0; i < n; i++ {
|
|
c[i] = mulMod(c[i], psiInvPow)
|
|
psiInvPow = mulMod(psiInvPow, psiInv)
|
|
}
|
|
|
|
return c, nil
|
|
}
|
|
|
|
// Mul picks the fastest correct path for the input size and returns
|
|
// a*b in Z_Q[X]/(X^n + 1). Below n=64 schoolbook wins; above, NTT.
|
|
func Mul(a, b []uint64) ([]uint64, error) {
|
|
if len(a) == 0 || len(b) != len(a) {
|
|
return nil, ErrLength
|
|
}
|
|
if len(a) < 64 || len(a)&(len(a)-1) != 0 {
|
|
return MulSchoolbook(a, b)
|
|
}
|
|
return MulNTT(a, b)
|
|
}
|