Files
lattice/types/reduction_test.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

166 lines
4.4 KiB
Go

// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package types
import (
"errors"
"testing"
"unsafe"
)
func TestReductionMode_String(t *testing.T) {
cases := []struct {
m ReductionMode
want string
}{
{ReductionStrictEveryOp, "Strict"},
{ReductionLazy2, "Lazy2"},
{ReductionLazy4, "Lazy4"},
{ReductionLazy8, "Lazy8"},
{ReductionMode(0xFF), "Unknown"},
}
for _, c := range cases {
if got := c.m.String(); got != c.want {
t.Errorf("ReductionMode(%d) = %q, want %q", c.m, got, c.want)
}
}
}
func TestSafeBoundFor_StrictAlwaysOne(t *testing.T) {
moduli := []uint64{
0x10001, // 17 bits
0x7FFFFFFFFFFFFFC5, // 63 bits, prime
0xFFFFFFFFFFFFFFC5, // 64 bits, prime
}
for _, q := range moduli {
if got := SafeBoundFor(ReductionStrictEveryOp, q); got != 1 {
t.Errorf("SafeBoundFor(Strict, q=0x%x) = %d, want 1", q, got)
}
}
}
func TestSafeBoundFor_LazyByModulusBitlen(t *testing.T) {
cases := []struct {
mode ReductionMode
modulus uint64
want uint32
}{
// 30-bit modulus fits all lazy modes
{ReductionLazy2, (1 << 30) - 35, 2},
{ReductionLazy4, (1 << 30) - 35, 4},
{ReductionLazy8, (1 << 30) - 35, 8},
// 61-bit modulus is the upper bound for Lazy8
{ReductionLazy8, (1 << 61) - 1, 8},
// 62-bit modulus exceeds Lazy8 budget
{ReductionLazy8, (1 << 62) - 1, 0},
// 62-bit fits Lazy4
{ReductionLazy4, (1 << 62) - 1, 4},
// 63-bit exceeds Lazy4
{ReductionLazy4, (1 << 63) - 1, 0},
// 63-bit fits Lazy2
{ReductionLazy2, (1 << 63) - 1, 2},
// 64-bit exceeds Lazy2
{ReductionLazy2, ^uint64(0), 0},
// modulus 0 always returns 0
{ReductionLazy4, 0, 0},
}
for _, c := range cases {
if got := SafeBoundFor(c.mode, c.modulus); got != c.want {
t.Errorf("SafeBoundFor(%s, q=0x%x) = %d, want %d",
c.mode, c.modulus, got, c.want)
}
}
}
func TestNewReductionBudget_RejectsZeroModulus(t *testing.T) {
if _, err := NewReductionBudget(ReductionLazy4, 0); !errors.Is(err, ErrModulusZero) {
t.Errorf("expected ErrModulusZero, got %v", err)
}
}
func TestNewReductionBudget_RejectsTooLargeModulus(t *testing.T) {
// 64-bit modulus, request Lazy2 (caps at 63-bit) => reject.
_, err := NewReductionBudget(ReductionLazy2, ^uint64(0))
if !errors.Is(err, ErrModulusTooLargeForLazy) {
t.Errorf("expected ErrModulusTooLargeForLazy, got %v", err)
}
}
func TestReductionBudget_ChargeAndReset(t *testing.T) {
rb, err := NewReductionBudget(ReductionLazy4, (1<<30)-35)
if err != nil {
t.Fatalf("NewReductionBudget: %v", err)
}
if rb.NeedsReduce() {
t.Errorf("fresh budget should not require reduction")
}
if got := rb.RemainingOps(); got != 4 {
t.Errorf("RemainingOps = %d, want 4", got)
}
rb.Charge(2)
if got := rb.RemainingOps(); got != 2 {
t.Errorf("after charge 2, RemainingOps = %d, want 2", got)
}
rb.Charge(2)
if !rb.NeedsReduce() {
t.Errorf("after charge 4 with cap 4, should need reduce")
}
if got := rb.RemainingOps(); got != 0 {
t.Errorf("RemainingOps saturated should be 0, got %d", got)
}
rb.Reset()
if rb.NeedsReduce() {
t.Errorf("after reset, should not need reduce")
}
if got := rb.RemainingOps(); got != 4 {
t.Errorf("after reset, RemainingOps = %d, want 4", got)
}
}
func TestReductionBudget_RemainingOpsSaturates(t *testing.T) {
rb, err := NewReductionBudget(ReductionLazy2, 1<<30)
if err != nil {
t.Fatalf("NewReductionBudget: %v", err)
}
rb.Charge(1000) // way over cap
if got := rb.RemainingOps(); got != 0 {
t.Errorf("over-charged budget RemainingOps = %d, want 0", got)
}
if !rb.NeedsReduce() {
t.Errorf("over-charged budget should need reduce")
}
}
// TestReductionBudget_Layout pins layout for cgo compatibility.
func TestReductionBudget_Layout(t *testing.T) {
const wantSize = 24
if got := unsafe.Sizeof(ReductionBudget{}); got != wantSize {
t.Fatalf("sizeof(ReductionBudget) = %d, want %d", got, wantSize)
}
var rb ReductionBudget
base := uintptr(unsafe.Pointer(&rb))
cases := []struct {
name string
off uintptr
want uintptr
}{
{"Modulus", uintptr(unsafe.Pointer(&rb.Modulus)) - base, 0},
{"OpsSinceReduce", uintptr(unsafe.Pointer(&rb.OpsSinceReduce)) - base, 8},
{"MaxOpsBeforeOverflow", uintptr(unsafe.Pointer(&rb.MaxOpsBeforeOverflow)) - base, 12},
{"Mode", uintptr(unsafe.Pointer(&rb.Mode)) - base, 16},
}
for _, c := range cases {
if c.off != c.want {
t.Errorf("offset of %s = %d, want %d", c.name, c.off, c.want)
}
}
}