Files
lattice/gpu/gpu_montgomery_purego.go
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

191 lines
6.4 KiB
Go

//go:build !cgo || !gpu
// Copyright (c) 2024-2025 Lux Industries Inc.
// SPDX-License-Identifier: Apache-2.0
// Package gpu — pure-Go Montgomery NTT dispatch path.
//
// Default build (no cgo, or no `-tags gpu`) routes Forward/Backward
// through the canonical pure-Go Montgomery NTT in
// github.com/luxfi/lattice/v7/ring (SubRing.NTT / INTT). There is one
// and only one Go implementation of the Montgomery NTT in the Lux
// stack — this file does not re-implement it, only delegates.
//
// Output is byte-equal to the cgo+gpu path (which itself is byte-equal
// to the pure-Go reference by contract). Callers see identical
// semantics regardless of build tags; cgo + gpu is purely an opt-in
// optimization, never a correctness boundary.
package gpu
import (
"fmt"
"sync"
"sync/atomic"
"github.com/luxfi/lattice/v7/ring"
)
// MontgomeryNTTContext binds a luxfi/lattice/v7/ring.SubRing to the
// pure-Go Montgomery NTT path. Forward and Backward delegate directly
// to ring.SubRing.NTT and ring.SubRing.INTT — the canonical Go
// implementation. There is no re-implementation here; this struct is
// a thin dispatch wrapper that mirrors gpu_montgomery_cgo.go's API so
// callers compile identically across build tags.
type MontgomeryNTTContext struct {
subring *ring.SubRing
N uint32
Q uint64
mu sync.RWMutex
closed atomic.Bool
}
// NewMontgomeryNTTContext binds to a SubRing. The SubRing's existing
// NTT tables are the source of truth — we hold a pointer, never a copy.
func NewMontgomeryNTTContext(s *ring.SubRing) (*MontgomeryNTTContext, error) {
if s == nil {
return nil, fmt.Errorf("nil SubRing")
}
if s.NTTTable == nil {
return nil, fmt.Errorf("SubRing.NTTTable is nil -- call ring.NewRing first")
}
if s.N <= 0 {
return nil, fmt.Errorf("invalid SubRing.N=%d", s.N)
}
if len(s.RootsForward) < s.N || len(s.RootsBackward) < s.N {
return nil, fmt.Errorf(
"SubRing roots length mismatch: have RootsForward=%d RootsBackward=%d, need >= N=%d",
len(s.RootsForward), len(s.RootsBackward), s.N)
}
// s.N is positive (validated above) and bounded by ring degree
// (16384 max in production); the int -> uint32 conversion is safe.
/* #nosec G115 -- s.N is validated above to be > 0, bounded by max ring degree */
N := uint32(s.N)
return &MontgomeryNTTContext{
subring: s,
N: N,
Q: s.Modulus,
}, nil
}
// Close releases the context. The pure-Go path holds no external
// resources; Close is idempotent and exists only to mirror the cgo API.
func (ctx *MontgomeryNTTContext) Close() {
ctx.closed.Store(true)
}
// Forward applies the NTT in-place via ring.SubRing.NTT. data must
// have length batch*N. For batch > 1, processes each polynomial
// sequentially.
func (ctx *MontgomeryNTTContext) Forward(data []uint64, batch uint32) error {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.closed.Load() {
return fmt.Errorf("context closed")
}
N := int(ctx.N)
if int(batch)*N > len(data) {
return fmt.Errorf("buffer too small: need %d, got %d",
int(batch)*N, len(data))
}
for i := uint32(0); i < batch; i++ {
off := int(i) * N
ctx.subring.NTT(data[off:off+N], data[off:off+N])
}
return nil
}
// Backward applies the inverse NTT in-place via ring.SubRing.INTT.
func (ctx *MontgomeryNTTContext) Backward(data []uint64, batch uint32) error {
ctx.mu.RLock()
defer ctx.mu.RUnlock()
if ctx.closed.Load() {
return fmt.Errorf("context closed")
}
N := int(ctx.N)
if int(batch)*N > len(data) {
return fmt.Errorf("buffer too small: need %d, got %d",
int(batch)*N, len(data))
}
for i := uint32(0); i < batch; i++ {
off := int(i) * N
ctx.subring.INTT(data[off:off+N], data[off:off+N])
}
return nil
}
// =============================================================================
// SubRing dispatch — pure-Go path
// =============================================================================
//
// nttThreshold mirrors the cgo build's signature so callers can probe
// and override consistently across build tags. In the pure-Go build
// the value is honored only by Forward/Backward through an explicit
// MontgomeryNTTContext; the ring core's SubRing.NTT runs on its own
// pure-Go path without consulting this dispatcher.
var nttThreshold atomic.Uint32
// SetNTTThreshold stores the threshold.
func SetNTTThreshold(n uint32) { nttThreshold.Store(n) }
// NTTThreshold returns the current threshold.
func NTTThreshold() uint32 { return nttThreshold.Load() }
// Available reports whether a hardware-accelerated NTT path is
// reachable. False in the pure-Go build: SubRing.NTT is the canonical
// path and the ring core invokes it directly. Returning false here is
// the contract by which NTTSubRingForward / NTTSubRingBackward signal
// "use the default path" to the ring dispatcher.
func Available() bool { return false }
// SubRing -> Montgomery context registry. Mirrors the cgo build's
// surface so callers compile identically across builds.
var (
subRingRegistryMu sync.RWMutex
subRingRegistry = make(map[*ring.SubRing]*MontgomeryNTTContext)
)
// RegisterSubRing binds a Montgomery context to a SubRing. Subsequent
// Forward/Backward calls delegate to the canonical pure-Go NTT in
// ring.SubRing.NTT / INTT.
func RegisterSubRing(s *ring.SubRing) (*MontgomeryNTTContext, error) {
subRingRegistryMu.Lock()
defer subRingRegistryMu.Unlock()
if existing, ok := subRingRegistry[s]; ok && existing != nil {
return existing, nil
}
ctx, err := NewMontgomeryNTTContext(s)
if err != nil {
return nil, err
}
subRingRegistry[s] = ctx
return ctx, nil
}
// UnregisterSubRing removes the binding.
func UnregisterSubRing(s *ring.SubRing) {
subRingRegistryMu.Lock()
defer subRingRegistryMu.Unlock()
if ctx, ok := subRingRegistry[s]; ok {
ctx.Close()
delete(subRingRegistry, s)
}
}
// LookupSubRing returns the bound context, or nil if none.
func LookupSubRing(s *ring.SubRing) *MontgomeryNTTContext {
subRingRegistryMu.RLock()
defer subRingRegistryMu.RUnlock()
return subRingRegistry[s]
}
// NTTSubRingForward is the dispatcher hook consulted by ring.SubRing.
// In the pure-Go build it always returns false: SubRing.NTT is the
// canonical pure-Go path and the ring core runs it without our
// involvement. Returning false signals "use the default path."
func NTTSubRingForward(_ *ring.SubRing, _, _ []uint64) bool { return false }
// NTTSubRingBackward is the inverse dispatcher hook. Same contract as
// NTTSubRingForward.
func NTTSubRingBackward(_ *ring.SubRing, _, _ []uint64) bool { return false }