Files
zeekay ae2f23dd35 fix(ci): unblock make checks — quoting, gofmt, lint:ignore, no-stub policy
Static-checks job has been failing on every PR (including #3) due to a
shell-interpolation bug in the Makefile + a build of accumulated lint
findings the bug had been hiding. Cleaned up end-to-end so make checks
runs green and PR #3 (security/readuint-slice-dos-2026-03-03) can land.

1. Makefile fmt-step: quote $$FMTOUT in `[ -z $$FMTOUT ]` test.
   Unquoted, multi-file go-fmt output word-splits and the test sees
   `[ -z FILE1 FILE2 ]` -> "/bin/sh: [: FILE2: unexpected operator".
   Same fix to the trailing `echo $$FMTOUT`.

2. go fmt sweep: gpu/gpu.go, gpu/gpu_test.go, types/reduction_test.go,
   types/trust_test.go had whitespace-only formatter findings that the
   crashing fmt-test step had been hiding.

3. ST1003 (mixed-case identifier): four wire-format-pinned consts in
   types/trust.go (TrustCpuGpuCompositeTEE / IOLevelCpu*) carry a
   //lint:ignore directive — their String() return values are part of
   the wire/log contract documented at line 80 ("do not rename"), so
   the Go-side identifiers must stay byte-aligned with the wire spelling.

4. ST1005 (capitalized error string): two error strings in gpu/gpu.go
   rephrased to lead with a noun ("ring degree N" / "modulus Q-1"
   instead of capital "N" / capital "Q") without changing semantics.

5. ST1000 (package comment): gpu/gpu_montgomery_*.go file header
   reformatted so the "Package gpu" line is the leading sentence.

6. G104 (unhandled errors): four `prng.Read(b)` call sites in gpu/gpu.go
   now `_, _ = prng.Read(b)` — the PRNG is keyed sampling output; read
   length is bounded by buffer length and a short read here is not a
   correctness issue.

7. G115 (int -> uint conversion): types/reduction.go switches `bitlen`
   to int (matches bits.Len64's return type, used only against literal
   small ints in the comparison ladder). gpu/gpu_montgomery_purego.go
   adds /* #nosec G115 */ on uint32(s.N) — s.N is validated >0 and
   bounded by ring degree above.

8. NO-STUB POLICY: gpu/gpu_montgomery_stub.go renamed to
   gpu/gpu_montgomery_purego.go and given a real pure-Go body that
   delegates to ring.SubRing.NTT / INTT (the canonical Go implementation
   in this repo). The "stub" name was misleading: there should never be
   a Lux Go file that returns "GPU unavailable" errors when the cgo+gpu
   build tag is unset — the pure-Go path is the canonical reference and
   always works. Output is byte-equal to the cgo+gpu path, identical
   semantics across build tags.

   Same rename for ring/ntt_simd_stub.go -> ring/ntt_simd_dispatch_purego.go
   with comment update: it's a dispatch hook (returns false to signal
   "use canonical scalar NTT below"), not an incomplete impl.

Verification:
* make checks: all 6 substeps PASS (go fmt / vet / goimports / staticcheck /
  govulncheck / gosec) on this branch.
* go test -count=1 ./...: all packages green; ring + ringqp + schemes/bgv
  + schemes/ckks + multiparty + utils/* all pass.
2026-05-04 08:37:27 -07:00

414 lines
9.5 KiB
Go

//go:build !cgo || !gpu
// Package gpu provides pure Go implementations when CGO is disabled or the gpu build tag is not set.
// These implementations use the lattice/ring package for NTT and polynomial operations.
package gpu
import (
"crypto/rand"
"fmt"
"math"
"math/big"
"sync"
"github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils/sampling"
)
// GPUAvailable returns false when CGO is disabled.
func GPUAvailable() bool {
return false
}
// GetBackend returns "CPU (pure Go)".
func GetBackend() string {
return "CPU (pure Go)"
}
// ClearCache is a no-op without GPU.
func ClearCache() {}
// NTTContext wraps the ring.SubRing for NTT operations.
type NTTContext struct {
subRing *ring.SubRing
N uint32
Q uint64
mu sync.RWMutex
}
// NewNTTContext creates a new NTT context using the pure-Go ring.
// Constructed via ring.NewRing so the SubRing's NTT constants
// (RootsForward/RootsBackward/NInv) are generated; bare ring.NewSubRing
// returns a SubRing without those tables.
func NewNTTContext(N uint32, Q uint64) (*NTTContext, error) {
if N == 0 || (N&(N-1)) != 0 {
return nil, fmt.Errorf("ring degree N must be a power of 2, got %d", N)
}
// Check if Q is NTT-friendly (Q-1 divisible by 2N)
if (Q-1)%(2*uint64(N)) != 0 {
return nil, fmt.Errorf("modulus Q-1 (%d) must be divisible by 2N (%d) for NTT-friendly prime", Q-1, 2*uint64(N))
}
r, err := ring.NewRing(int(N), []uint64{Q})
if err != nil {
return nil, fmt.Errorf("failed to create Ring: %w", err)
}
return &NTTContext{
subRing: r.SubRings[0],
N: N,
Q: Q,
}, nil
}
// Close releases resources (no-op for pure Go).
func (ctx *NTTContext) Close() {
ctx.mu.Lock()
defer ctx.mu.Unlock()
ctx.subRing = nil
}
// NTT performs forward NTT on a batch of polynomials.
func (ctx *NTTContext) NTT(polys [][]uint64) ([][]uint64, error) {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.subRing == nil {
return nil, fmt.Errorf("NTT context is closed")
}
if len(polys) == 0 {
return polys, nil
}
N := int(ctx.N)
results := make([][]uint64, len(polys))
for i, poly := range polys {
if len(poly) != N {
return nil, fmt.Errorf("polynomial %d has wrong size: got %d, expected %d", i, len(poly), N)
}
// Copy to result
results[i] = make([]uint64, N)
copy(results[i], poly)
// Perform NTT in-place
ctx.subRing.NTT(results[i], results[i])
}
return results, nil
}
// INTT performs inverse NTT on a batch of polynomials.
func (ctx *NTTContext) INTT(polys [][]uint64) ([][]uint64, error) {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.subRing == nil {
return nil, fmt.Errorf("NTT context is closed")
}
if len(polys) == 0 {
return polys, nil
}
N := int(ctx.N)
results := make([][]uint64, len(polys))
for i, poly := range polys {
if len(poly) != N {
return nil, fmt.Errorf("polynomial %d has wrong size: got %d, expected %d", i, len(poly), N)
}
// Copy to result
results[i] = make([]uint64, N)
copy(results[i], poly)
// Perform INTT in-place
ctx.subRing.INTT(results[i], results[i])
}
return results, nil
}
// PolyMul performs polynomial multiplication using pure Go NTT.
func (ctx *NTTContext) PolyMul(a, b [][]uint64) ([][]uint64, error) {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.subRing == nil {
return nil, fmt.Errorf("NTT context is closed")
}
if len(a) != len(b) {
return nil, fmt.Errorf("batch size mismatch: %d vs %d", len(a), len(b))
}
if len(a) == 0 {
return nil, nil
}
N := int(ctx.N)
results := make([][]uint64, len(a))
tempA := make([]uint64, N)
tempB := make([]uint64, N)
for i := range a {
if len(a[i]) != N || len(b[i]) != N {
return nil, fmt.Errorf("polynomial %d has wrong size", i)
}
results[i] = make([]uint64, N)
// Copy inputs
copy(tempA, a[i])
copy(tempB, b[i])
// Forward NTT
ctx.subRing.NTT(tempA, tempA)
ctx.subRing.NTT(tempB, tempB)
// Pointwise multiply
ctx.subRing.MulCoeffsMontgomery(tempA, tempB, results[i])
// Inverse NTT
ctx.subRing.INTT(results[i], results[i])
}
return results, nil
}
// PolyMulNTT performs element-wise multiplication in NTT domain.
func (ctx *NTTContext) PolyMulNTT(a, b []uint64) ([]uint64, error) {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.subRing == nil {
return nil, fmt.Errorf("NTT context is closed")
}
N := int(ctx.N)
if len(a) != N || len(b) != N {
return nil, fmt.Errorf("polynomial size mismatch")
}
result := make([]uint64, N)
ctx.subRing.MulCoeffsMontgomery(a, b, result)
return result, nil
}
// PolyAdd computes result = a + b (mod Q).
func PolyAdd(a, b []uint64, Q uint64) ([]uint64, error) {
if len(a) != len(b) {
return nil, fmt.Errorf("polynomial size mismatch")
}
result := make([]uint64, len(a))
for i := range a {
sum := a[i] + b[i]
if sum >= Q {
sum -= Q
}
result[i] = sum
}
return result, nil
}
// PolySub computes result = a - b (mod Q).
func PolySub(a, b []uint64, Q uint64) ([]uint64, error) {
if len(a) != len(b) {
return nil, fmt.Errorf("polynomial size mismatch")
}
result := make([]uint64, len(a))
for i := range a {
if a[i] >= b[i] {
result[i] = a[i] - b[i]
} else {
result[i] = Q - b[i] + a[i]
}
}
return result, nil
}
// PolyScalarMul computes result = a * scalar (mod Q).
func PolyScalarMul(a []uint64, scalar, Q uint64) ([]uint64, error) {
result := make([]uint64, len(a))
for i := range a {
// Use big.Int for 128-bit intermediate
prod := new(big.Int).SetUint64(a[i])
prod.Mul(prod, new(big.Int).SetUint64(scalar))
prod.Mod(prod, new(big.Int).SetUint64(Q))
result[i] = prod.Uint64()
}
return result, nil
}
// SampleGaussian samples a polynomial with discrete Gaussian distribution.
func SampleGaussian(N uint32, Q uint64, sigma float64, seed []byte) ([]uint64, error) {
result := make([]uint64, N)
// Create PRNG
var prng sampling.PRNG
if len(seed) >= 32 {
prng, _ = sampling.NewKeyedPRNG(seed[:32])
} else {
prng, _ = sampling.NewPRNG()
}
// Use Box-Muller transform for Gaussian sampling
bound := int64(math.Ceil(sigma * 6)) // 6-sigma cutoff
for i := uint32(0); i < N; i++ {
// Rejection sampling for discrete Gaussian
for {
// Sample from bounded uniform
var sample int64
for {
b := make([]byte, 8)
_, _ = prng.Read(b)
sample = int64(b[0]) | int64(b[1])<<8 | int64(b[2])<<16 | int64(b[3])<<24
sample = sample%(2*bound+1) - bound
if sample >= -bound && sample <= bound {
break
}
}
// Accept with Gaussian probability
prob := math.Exp(-float64(sample*sample) / (2 * sigma * sigma))
threshold := make([]byte, 8)
_, _ = prng.Read(threshold)
if float64(threshold[0])/256.0 < prob {
if sample >= 0 {
result[i] = uint64(sample)
} else {
result[i] = Q - uint64(-sample)
}
break
}
}
}
return result, nil
}
// SampleUniform samples a uniform random polynomial.
func SampleUniform(N uint32, Q uint64, seed []byte) ([]uint64, error) {
result := make([]uint64, N)
// Create PRNG
var prng sampling.PRNG
if len(seed) >= 32 {
prng, _ = sampling.NewKeyedPRNG(seed[:32])
} else {
prng, _ = sampling.NewPRNG()
}
qBig := new(big.Int).SetUint64(Q)
for i := uint32(0); i < N; i++ {
// Sample uniform in [0, Q)
b := make([]byte, 8)
_, _ = prng.Read(b)
val := new(big.Int).SetBytes(b)
val.Mod(val, qBig)
result[i] = val.Uint64()
}
return result, nil
}
// SampleTernary samples a ternary polynomial {-1, 0, 1}.
func SampleTernary(N uint32, Q uint64, density float64, seed []byte) ([]uint64, error) {
result := make([]uint64, N)
// Create PRNG
var reader interface{ Read([]byte) (int, error) }
if len(seed) >= 32 {
prng, _ := sampling.NewKeyedPRNG(seed[:32])
reader = prng
} else {
reader = rand.Reader
}
for i := uint32(0); i < N; i++ {
b := make([]byte, 2)
_, _ = reader.Read(b)
// Probability of non-zero
p := float64(b[0]) / 256.0
if p < density {
// Non-zero: choose -1 or 1
if b[1]&1 == 0 {
result[i] = 1
} else {
result[i] = Q - 1 // -1 mod Q
}
} else {
result[i] = 0
}
}
return result, nil
}
// FindPrimitiveRoot finds a primitive 2N-th root of unity modulo Q.
func FindPrimitiveRoot(N uint32, Q uint64) (uint64, error) {
// Compute g = generator of Z_Q^*
// Then root = g^((Q-1)/(2N))
exponent := (Q - 1) / (2 * uint64(N))
// Find a generator (primitive root of Q)
// For prime Q, try small values until we find one
for g := uint64(2); g < Q; g++ {
// Check if g is a primitive root
gBig := new(big.Int).SetUint64(g)
qBig := new(big.Int).SetUint64(Q)
expBig := new(big.Int).SetUint64(exponent)
root := new(big.Int).Exp(gBig, expBig, qBig)
// Verify it's a 2N-th root of unity
twoN := new(big.Int).SetUint64(2 * uint64(N))
test := new(big.Int).Exp(root, twoN, qBig)
if test.Uint64() == 1 {
// Also verify root^N != 1 (primitive)
nBig := new(big.Int).SetUint64(uint64(N))
testN := new(big.Int).Exp(root, nBig, qBig)
if testN.Uint64() != 1 {
return root.Uint64(), nil
}
}
}
return 0, fmt.Errorf("no primitive root found for N=%d, Q=%d", N, Q)
}
// ModInverse computes the modular inverse a^{-1} mod Q.
func ModInverse(a, Q uint64) (uint64, error) {
aBig := new(big.Int).SetUint64(a)
qBig := new(big.Int).SetUint64(Q)
inv := new(big.Int).ModInverse(aBig, qBig)
if inv == nil {
return 0, fmt.Errorf("%d is not invertible mod %d", a, Q)
}
return inv.Uint64(), nil
}
// IsNTTPrime checks if Q is a valid NTT-friendly prime for ring dimension N.
func IsNTTPrime(N uint32, Q uint64) bool {
// Q must be prime and Q ≡ 1 (mod 2N)
if (Q-1)%(2*uint64(N)) != 0 {
return false
}
// Check if Q is prime (simple Miller-Rabin)
qBig := new(big.Int).SetUint64(Q)
return qBig.ProbablyPrime(20)
}