mirror of
https://github.com/luxfi/math.git
synced 2026-07-27 03:38:49 +00:00
ntt, poly, rns, sample
Eight pure-Go reference packages that own the canonical semantics of
every cryptographic-math primitive Lux protocols share. Backends
(AVX2 / NEON / cgo+C++ / CUDA / Metal / WGSL) plug in behind the
substrate; KATs gate byte-equality across them.
Packages added:
* params/ — ModulusID, NTTParamID, FHEParamID, PulsarParamID,
HashSuiteID, BackendID; KATHeader required-fields
schema. Stable string IDs; renaming is a breaking
change.
* backend/ — Policy enum (PureGo / NativeCPU / GPUPreferred /
GPURequired); Resolve(policy, registered) returns the
BackendID per LP-107's fallback chain. GPURequired
returns ErrUnavailable when no GPU backend is
registered.
* codec/ — Bounded readers (closes lattice issues #2 + #4 DoS
class permanently). Limits + LimitError + Reader.
ReadUint{16,32,64}Slice all reject the 70T-element
attack input before allocating; depth + frame-bytes
caps; iterative (no recursion). Regression test
TestReadUint64Slice_RejectsHugeLength encodes the
original lattice issue #4 input.
* modarith/ — Modulus type with QInv, R2, Barrett constants
(computed via math/big once per modulus). AddMod /
SubMod / MulMod (Div64 reference path). MontMulMod /
ToMontgomery / FromMontgomery cross-checked against
MulMod across 100 random Pulsar-Q pairs.
ReductionMode enum byte-equal to lattice/types.
* ntt/ — Service + Backend interface; pure-Go backend
delegates to lattice/v7/ring.SubRing.NTT/INTT (the
canonical Lattigo-derived Montgomery NTT body — no
re-implementation, just dispatch). Round-trip +
batch + determinism tests on Pulsar N=256.
* poly/ — Add / Sub / ScalarMul / PointwiseMul (NTT-domain) /
negacyclic Mul (NTT round-trip). Composes modarith +
ntt; no lower-level reach.
* rns/ — Basis(Moduli, Name) for FHE RNS chains. Single +
two-prime construction validated; rejects even moduli.
Phase 3 will add basis-extension and modulus-switching.
* sample/ — Uniform (rejection sampling, mask-and-retry); Ternary
(density + sign byte); CenteredBinomial (popcount
difference); DiscreteGaussianRejection (6-sigma cutoff).
All take an io.Reader so callers can KAT-replay.
Architecture invariants enforced in package docs:
- Go is the canonical semantic reference.
- Backend selection MUST NOT alter transcript bytes (consensus
paths default to PolicyPureGo / PolicyNativeCPU).
- No unbounded codec readers anywhere near wire formats.
- No re-implementation: where lattice/v7/ring already owns the
canonical body, we delegate, not fork.
- One ID space; renaming is a breaking change.
Test posture: 8/8 packages green via `GOWORK=off go test ./...`.
Phases 3-7 (queued):
3. lattice consumes math
4. pulsar consumes lattice + math
5. fhe consumes math
6. luxcpp/crypto/math native backend mirror
7. cross-runtime KAT release gate
See SUBSTRATE.md for the full posture and migration plan; full LP at
~/work/lux/lps/LP-107-lux-math-substrate.md.
117 lines
2.8 KiB
Go
117 lines
2.8 KiB
Go
// Copyright (c) 2026 Lux Industries Inc.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package sample
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/zeebo/blake3"
|
|
)
|
|
|
|
const PulsarQ = uint64(0x1000000004A01)
|
|
|
|
// deterministicReader returns an unbounded byte stream from a seed
|
|
// string. Same pattern as luxfi/threshold's deterministicRand test
|
|
// helper.
|
|
func deterministicReader(seed string) io.Reader {
|
|
h := blake3.New()
|
|
_, _ = h.Write([]byte("luxfi/math/sample/test/v1"))
|
|
_, _ = h.Write([]byte(seed))
|
|
return h.Digest()
|
|
}
|
|
|
|
func TestUniform_Determinism(t *testing.T) {
|
|
N := 256
|
|
a := make([]uint64, N)
|
|
b := make([]uint64, N)
|
|
if err := Uniform(a, PulsarQ, deterministicReader("uniform-1")); err != nil {
|
|
t.Fatalf("Uniform[a]: %v", err)
|
|
}
|
|
if err := Uniform(b, PulsarQ, deterministicReader("uniform-1")); err != nil {
|
|
t.Fatalf("Uniform[b]: %v", err)
|
|
}
|
|
if !bytes.Equal(uint64sToBytes(a), uint64sToBytes(b)) {
|
|
t.Error("Uniform with same seed: byte-mismatch")
|
|
}
|
|
for i, v := range a {
|
|
if v >= PulsarQ {
|
|
t.Fatalf("Uniform[%d] = %d >= q=%d", i, v, PulsarQ)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTernary_Distribution(t *testing.T) {
|
|
N := 4096
|
|
dst := make([]uint64, N)
|
|
if err := Ternary(dst, PulsarQ, 0.5, deterministicReader("ternary")); err != nil {
|
|
t.Fatalf("Ternary: %v", err)
|
|
}
|
|
// Coarse distribution check: with density=0.5, expect ~50%
|
|
// non-zero. Allow ±10% slack.
|
|
zero, plus, minus := 0, 0, 0
|
|
for _, v := range dst {
|
|
switch v {
|
|
case 0:
|
|
zero++
|
|
case 1:
|
|
plus++
|
|
case PulsarQ - 1:
|
|
minus++
|
|
default:
|
|
t.Fatalf("Ternary: unexpected value %d", v)
|
|
}
|
|
}
|
|
nonZero := plus + minus
|
|
if nonZero < N*4/10 || nonZero > N*6/10 {
|
|
t.Errorf("Ternary density: nonZero=%d/%d (expected ~50%%)", nonZero, N)
|
|
}
|
|
// Plus and minus should be roughly balanced.
|
|
if plus < nonZero/3 || minus < nonZero/3 {
|
|
t.Errorf("Ternary balance: plus=%d minus=%d", plus, minus)
|
|
}
|
|
}
|
|
|
|
func TestCenteredBinomial_RangeBounded(t *testing.T) {
|
|
N := 1024
|
|
dst := make([]uint64, N)
|
|
const eta = 2
|
|
if err := CenteredBinomial(dst, PulsarQ, eta, deterministicReader("cbd-2")); err != nil {
|
|
t.Fatalf("CenteredBinomial: %v", err)
|
|
}
|
|
// Output values should all be in {q-eta, ..., q-1, 0, 1, ..., eta}.
|
|
for i, v := range dst {
|
|
if v <= eta {
|
|
continue
|
|
}
|
|
if v >= PulsarQ-eta {
|
|
continue
|
|
}
|
|
t.Fatalf("CenteredBinomial[%d] = %d out of expected range", i, v)
|
|
}
|
|
}
|
|
|
|
func TestDiscreteGaussianRejection_Range(t *testing.T) {
|
|
// Coarse range test: 1000 samples should all lie within ~6 sigma.
|
|
const sigma = 3.2
|
|
r := deterministicReader("dgrr")
|
|
for i := 0; i < 1000; i++ {
|
|
_, err := DiscreteGaussianRejection(PulsarQ, sigma, r)
|
|
if err != nil {
|
|
t.Fatalf("[%d]: %v", i, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func uint64sToBytes(s []uint64) []byte {
|
|
out := make([]byte, len(s)*8)
|
|
for i, v := range s {
|
|
for j := 0; j < 8; j++ {
|
|
out[i*8+j] = byte(v >> (j * 8))
|
|
}
|
|
}
|
|
return out
|
|
}
|