mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
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.
166 lines
4.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|