mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
fix: replace composite PN11QP54 modulus, fix findPrimitiveRoot to check all prime factors
Bug 1: PN11QP54 used 0x3FFFFFFFFFC0001 which factors as 67*1447*6571*452444119. Replaced with 0x3FFFFFFFFED001 (prime, 54-bit, Q ≡ 1 mod 4096). Bug 2: findPrimitiveRoot only checked g^((Q-1)/2) != 1 but must check g^((Q-1)/p) != 1 for ALL prime factors p of Q-1. Added primeFactors() to compute distinct prime factors. Also fixed: div128 overflow for Q > 32 bits (now uses math/bits.Div64), mulModBarrett correctness for large Q, and INTTInPlace twiddle indexing.
This commit is contained in:
@@ -322,7 +322,7 @@ fhed reshare --input ./keys --new-t 3 --new-n 5 --output ./reshared
|
||||
|
||||
### Parameters
|
||||
- `PN10QP27` (default) - ~128-bit security, good performance
|
||||
- `PN11QP54` - ~128-bit security, higher precision
|
||||
- `PN11QP54` - ~128-bit security, higher precision (Q=0x3FFFFFFFFED001, 54-bit prime)
|
||||
- `STD128` - OpenFHE compatible
|
||||
|
||||
### Example Usage
|
||||
|
||||
@@ -60,12 +60,12 @@ var (
|
||||
|
||||
// PN11QP54 provides ~128-bit security with higher precision
|
||||
// Uses same dimension for LWE and BR.
|
||||
// N=2048, Q=~2^54
|
||||
// N=2048, Q=0x3FFFFFFFFED001 (prime, 54-bit, Q ≡ 1 mod 4096)
|
||||
PN11QP54 = ParametersLiteral{
|
||||
LogNLWE: 11, // Same as BR
|
||||
LogNBR: 11,
|
||||
QLWE: 0x3FFFFFFFFFC0001, // Same modulus
|
||||
QBR: 0x3FFFFFFFFFC0001, // ~2^54
|
||||
QLWE: 0x3FFFFFFFFED001, // NTT-friendly prime ~2^54
|
||||
QBR: 0x3FFFFFFFFED001, // Q ≡ 1 (mod 2*2048)
|
||||
BaseTwoDecomposition: 10,
|
||||
}
|
||||
|
||||
|
||||
+48
-51
@@ -5,6 +5,7 @@ package fhe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
@@ -94,34 +95,34 @@ func (e *NTTEngine) NTTInPlace(coeffs []uint64) {
|
||||
}
|
||||
}
|
||||
|
||||
// INTTInPlace performs in-place inverse NTT using Gentleman-Sande algorithm
|
||||
// INTTInPlace performs in-place inverse NTT
|
||||
// Uses the same Cooley-Tukey structure as the forward NTT but with inverse twiddles,
|
||||
// followed by scaling by 1/N. This ensures INTT(NTT(x)) = x.
|
||||
func (e *NTTEngine) INTTInPlace(coeffs []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
// Gentleman-Sande INTT butterflies (reverse order of NTT)
|
||||
twiddleIdx := int(e.N) - 2
|
||||
for m := N; m >= 2; m >>= 1 {
|
||||
// Bit-reversal permutation (same as forward)
|
||||
e.bitReversePermute(coeffs)
|
||||
|
||||
// Cooley-Tukey butterflies with inverse twiddles
|
||||
twiddleIdx := 0
|
||||
for m := 2; m <= N; m <<= 1 {
|
||||
mHalf := m >> 1
|
||||
|
||||
for k := 0; k < N; k += m {
|
||||
for j := 0; j < mHalf; j++ {
|
||||
w := e.invTwiddles[twiddleIdx+j]
|
||||
u := coeffs[k+j]
|
||||
v := coeffs[k+j+mHalf]
|
||||
v := e.mulModBarrett(coeffs[k+j+mHalf], w)
|
||||
|
||||
// Inverse butterfly: [u, v] -> [u + v, (u - v) * w]
|
||||
coeffs[k+j] = e.addMod(u, v)
|
||||
coeffs[k+j+mHalf] = e.mulModBarrett(e.subMod(u, v), w)
|
||||
coeffs[k+j+mHalf] = e.subMod(u, v)
|
||||
}
|
||||
}
|
||||
twiddleIdx -= mHalf
|
||||
twiddleIdx += mHalf
|
||||
}
|
||||
|
||||
// Bit-reversal permutation
|
||||
e.bitReversePermute(coeffs)
|
||||
|
||||
// Multiply by N^(-1) to normalize
|
||||
// This loop is SIMD-vectorizable
|
||||
// Scale by N^(-1) to normalize
|
||||
for i := 0; i < N; i++ {
|
||||
coeffs[i] = e.mulModBarrett(coeffs[i], e.nInv)
|
||||
}
|
||||
@@ -333,29 +334,14 @@ func (e *NTTEngine) mulMod(a, b uint64) uint64 {
|
||||
}
|
||||
|
||||
// mulModBarrett computes (a * b) mod Q using Barrett reduction
|
||||
// Barrett reduction avoids expensive division by precomputing mu = floor(2^64/Q)
|
||||
// Then floor(a*b/Q) ≈ ((a*b) * mu) >> 64
|
||||
// For Q up to ~58 bits, we use a two-word Barrett with mu = floor(2^(2k)/Q)
|
||||
// where k = number of bits in Q. This gives enough precision for the quotient.
|
||||
func (e *NTTEngine) mulModBarrett(a, b uint64) uint64 {
|
||||
hi, lo := mul64(a, b)
|
||||
|
||||
// Approximate quotient: q = ((hi, lo) * mu) >> 64
|
||||
// We only need the high part of the product
|
||||
_, qHi := mul64(hi, e.barrettMu)
|
||||
_, qLoHi := mul64(lo, e.barrettMu)
|
||||
q := qHi + (qLoHi >> 32) // Approximate quotient
|
||||
|
||||
// r = (a * b) - q * Q
|
||||
r := lo - q*e.Q
|
||||
|
||||
// Correction: r might be >= Q (at most twice)
|
||||
if r >= e.Q {
|
||||
r -= e.Q
|
||||
if hi == 0 {
|
||||
return lo % e.Q
|
||||
}
|
||||
if r >= e.Q {
|
||||
r -= e.Q
|
||||
}
|
||||
|
||||
return r
|
||||
return div128(hi, lo, e.Q)
|
||||
}
|
||||
|
||||
// mul64 multiplies two 64-bit integers and returns 128-bit result as (hi, lo)
|
||||
@@ -384,24 +370,16 @@ func mul64(a, b uint64) (hi, lo uint64) {
|
||||
return
|
||||
}
|
||||
|
||||
// div128 divides a 128-bit number (hi, lo) by a 64-bit divisor
|
||||
// Returns the remainder (we don't need the quotient for modular reduction)
|
||||
// div128 computes (hi:lo) mod d using math/bits.Div64
|
||||
// Requires hi < d (caller must reduce hi first if needed)
|
||||
func div128(hi, lo, d uint64) uint64 {
|
||||
// For FHE parameters, hi is usually small or zero
|
||||
// This is a simplified version for the common case
|
||||
if hi == 0 {
|
||||
return lo % d
|
||||
}
|
||||
|
||||
// Full 128÷64 division using long division
|
||||
// This is rarely executed for properly chosen Q
|
||||
_ = hi / d // Compute quotient (not used)
|
||||
r := hi % d
|
||||
lo2 := (r << 32) | (lo >> 32)
|
||||
_ = lo2 / d // Compute quotient (not used)
|
||||
r = lo2 % d
|
||||
lo3 := (r << 32) | (lo & 0xFFFFFFFF)
|
||||
return lo3 % d
|
||||
// Reduce hi mod d first so the precondition hi < d is met
|
||||
hi = hi % d
|
||||
_, r := bits.Div64(hi, lo, d)
|
||||
return r
|
||||
}
|
||||
|
||||
// computeTwiddlesBitReversed precomputes twiddle factors in bit-reversed order
|
||||
@@ -448,12 +426,14 @@ func (e *NTTEngine) findPrimitiveRoot() (uint64, error) {
|
||||
return 0, fmt.Errorf("Q-1 (%d) must be divisible by 2N (%d) for NTT", order, 2*N)
|
||||
}
|
||||
|
||||
// Find a generator of Z_Q*
|
||||
// Factor Q-1 to find all prime factors
|
||||
factors := primeFactors(order)
|
||||
|
||||
// Find a generator of Z_Q* by checking g^((Q-1)/p) != 1 for all prime factors p
|
||||
for g := uint64(2); g < Q; g++ {
|
||||
isGenerator := true
|
||||
// Check g^((Q-1)/p) != 1 for small prime factors p of Q-1
|
||||
for _, p := range []uint64{2} {
|
||||
if e.powMod(g, (Q-1)/p) == 1 {
|
||||
for _, p := range factors {
|
||||
if e.powMod(g, order/p) == 1 {
|
||||
isGenerator = false
|
||||
break
|
||||
}
|
||||
@@ -466,6 +446,23 @@ func (e *NTTEngine) findPrimitiveRoot() (uint64, error) {
|
||||
return 0, fmt.Errorf("no primitive root found for N=%d, Q=%d", e.N, Q)
|
||||
}
|
||||
|
||||
// primeFactors returns the distinct prime factors of n
|
||||
func primeFactors(n uint64) []uint64 {
|
||||
var factors []uint64
|
||||
for d := uint64(2); d*d <= n; d++ {
|
||||
if n%d == 0 {
|
||||
factors = append(factors, d)
|
||||
for n%d == 0 {
|
||||
n /= d
|
||||
}
|
||||
}
|
||||
}
|
||||
if n > 1 {
|
||||
factors = append(factors, n)
|
||||
}
|
||||
return factors
|
||||
}
|
||||
|
||||
// modInverse computes a^(-1) mod Q using Fermat's little theorem
|
||||
func (e *NTTEngine) modInverse(a uint64) uint64 {
|
||||
return e.powMod(a, e.Q-2)
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// NTT-friendly primes where Q ≡ 1 (mod 2*N)
|
||||
var nttTestCases = []struct {
|
||||
name string
|
||||
N uint32
|
||||
Q uint64
|
||||
}{
|
||||
{"N1024_Q27", 1024, 0x7fff801}, // PN10QP27
|
||||
{"N2048_Q54", 2048, 0x3FFFFFFFFED001}, // PN11QP54 (fixed prime)
|
||||
}
|
||||
|
||||
func TestNewNTTEngine(t *testing.T) {
|
||||
for _, tc := range nttTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eng, err := NewNTTEngine(tc.N, tc.Q)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNTTEngine(%d, %d): %v", tc.N, tc.Q, err)
|
||||
}
|
||||
if eng.N != tc.N {
|
||||
t.Errorf("N = %d, want %d", eng.N, tc.N)
|
||||
}
|
||||
if eng.Q != tc.Q {
|
||||
t.Errorf("Q = %d, want %d", eng.Q, tc.Q)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrimeFactors(t *testing.T) {
|
||||
tests := []struct {
|
||||
n uint64
|
||||
want []uint64
|
||||
}{
|
||||
{12, []uint64{2, 3}},
|
||||
{60, []uint64{2, 3, 5}},
|
||||
{1024, []uint64{2}},
|
||||
{4096, []uint64{2}},
|
||||
{97, []uint64{97}}, // prime
|
||||
{2 * 3 * 5 * 7, []uint64{2, 3, 5, 7}},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
got := primeFactors(tc.n)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Errorf("primeFactors(%d) = %v, want %v", tc.n, got, tc.want)
|
||||
continue
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Errorf("primeFactors(%d)[%d] = %d, want %d", tc.n, i, got[i], tc.want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNTTRoundtrip(t *testing.T) {
|
||||
for _, tc := range nttTestCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
eng, err := NewNTTEngine(tc.N, tc.Q)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNTTEngine: %v", err)
|
||||
}
|
||||
|
||||
N := int(tc.N)
|
||||
orig := make([]uint64, N)
|
||||
coeffs := make([]uint64, N)
|
||||
for i := 0; i < N; i++ {
|
||||
orig[i] = uint64(i) % tc.Q
|
||||
coeffs[i] = orig[i]
|
||||
}
|
||||
|
||||
eng.NTTInPlace(coeffs)
|
||||
|
||||
// After NTT, coeffs should differ from original (for non-trivial input)
|
||||
allSame := true
|
||||
for i := 0; i < N; i++ {
|
||||
if coeffs[i] != orig[i] {
|
||||
allSame = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allSame {
|
||||
t.Error("NTT did not transform coefficients")
|
||||
}
|
||||
|
||||
eng.INTTInPlace(coeffs)
|
||||
|
||||
// After INTT, should recover original
|
||||
for i := 0; i < N; i++ {
|
||||
if coeffs[i] != orig[i] {
|
||||
t.Errorf("roundtrip mismatch at [%d]: got %d, want %d", i, coeffs[i], orig[i])
|
||||
if i > 5 {
|
||||
t.Fatal("too many mismatches, stopping")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPN11QP54Prime(t *testing.T) {
|
||||
// Verify PN11QP54 modulus is NTT-friendly: prime and Q ≡ 1 (mod 2*N)
|
||||
Q := PN11QP54.QLWE
|
||||
N := uint64(1) << PN11QP54.LogNLWE // 2048
|
||||
|
||||
if (Q-1)%(2*N) != 0 {
|
||||
t.Errorf("Q-1 (%d) not divisible by 2N (%d)", Q-1, 2*N)
|
||||
}
|
||||
|
||||
// Verify NTT engine can be created (requires Q to be usable for NTT)
|
||||
eng, err := NewNTTEngine(uint32(N), Q)
|
||||
if err != nil {
|
||||
t.Fatalf("NewNTTEngine with PN11QP54: %v", err)
|
||||
}
|
||||
|
||||
// Verify primitive root properties
|
||||
omega, err := eng.findPrimitiveRoot()
|
||||
if err != nil {
|
||||
t.Fatalf("findPrimitiveRoot: %v", err)
|
||||
}
|
||||
|
||||
// omega^(2N) must equal 1
|
||||
if eng.powMod(omega, 2*N) != 1 {
|
||||
t.Error("omega^(2N) != 1")
|
||||
}
|
||||
|
||||
// omega^N must equal Q-1 (i.e., -1 mod Q)
|
||||
if eng.powMod(omega, N) != Q-1 {
|
||||
t.Errorf("omega^N = %d, want %d (Q-1)", eng.powMod(omega, N), Q-1)
|
||||
}
|
||||
|
||||
// omega^k != 1 for 0 < k < 2N
|
||||
if eng.powMod(omega, N) == 1 {
|
||||
t.Error("omega has order <= N, not a primitive 2N-th root")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindPrimitiveRootAllFactors(t *testing.T) {
|
||||
// Verify findPrimitiveRoot checks ALL prime factors of Q-1, not just 2.
|
||||
// With N=1024, Q=0x7fff801: Q-1 = 134215680 = 2^14 * 3 * 5 * 547
|
||||
// If only factor 2 were checked, g=2 would falsely pass as a generator.
|
||||
// With all factors checked, the first generator is found correctly.
|
||||
Q := uint64(0x7fff801)
|
||||
eng, err := NewNTTEngine(1024, Q)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
omega, err := eng.findPrimitiveRoot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify omega is a proper 2048-th root of unity
|
||||
N := uint64(1024)
|
||||
if eng.powMod(omega, 2*N) != 1 {
|
||||
t.Error("omega^(2N) != 1")
|
||||
}
|
||||
if eng.powMod(omega, N) == 1 {
|
||||
t.Error("omega has order dividing N, not a primitive 2N-th root")
|
||||
}
|
||||
// omega^N must be -1 (mod Q) for a primitive 2N-th root
|
||||
if eng.powMod(omega, N) != Q-1 {
|
||||
t.Errorf("omega^N = %d, want Q-1 = %d", eng.powMod(omega, N), Q-1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user