mirror of
https://github.com/luxfi/math.git
synced 2026-09-24 00:25:16 +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.
185 lines
4.8 KiB
Go
185 lines
4.8 KiB
Go
// Copyright (c) 2026 Lux Industries Inc.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package codec
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func newReader(t *testing.T, data []byte, l Limits) *Reader {
|
|
t.Helper()
|
|
r, err := NewReader(bytes.NewReader(data), l)
|
|
if err != nil {
|
|
t.Fatalf("NewReader: %v", err)
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestLimits_Validate(t *testing.T) {
|
|
if err := DefaultLimitsLatticeWire.Validate(); err != nil {
|
|
t.Errorf("DefaultLimitsLatticeWire.Validate(): %v", err)
|
|
}
|
|
if err := (Limits{}).Validate(); err == nil {
|
|
t.Error("empty Limits.Validate() returned nil")
|
|
}
|
|
}
|
|
|
|
func TestNewReader_NilArgs(t *testing.T) {
|
|
if _, err := NewReader(nil, DefaultLimitsLatticeWire); err == nil {
|
|
t.Error("nil io.Reader: no error")
|
|
}
|
|
if _, err := NewReader(bytes.NewReader(nil), Limits{}); err == nil {
|
|
t.Error("zero Limits: no error")
|
|
}
|
|
}
|
|
|
|
func encodeUvarint(out *bytes.Buffer, v uint64) {
|
|
for v >= 0x80 {
|
|
out.WriteByte(byte(v) | 0x80)
|
|
v >>= 7
|
|
}
|
|
out.WriteByte(byte(v))
|
|
}
|
|
|
|
func TestReadUint64Slice_HappyPath(t *testing.T) {
|
|
want := []uint64{0xdeadbeef, 0xcafebabe, 0x1122334455667788}
|
|
var buf bytes.Buffer
|
|
encodeUvarint(&buf, uint64(len(want)))
|
|
for _, v := range want {
|
|
_ = binary.Write(&buf, binary.LittleEndian, v)
|
|
}
|
|
|
|
r := newReader(t, buf.Bytes(), DefaultLimitsLatticeWire)
|
|
got, err := r.ReadUint64Slice()
|
|
if err != nil {
|
|
t.Fatalf("ReadUint64Slice: %v", err)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("len: want %d got %d", len(want), len(got))
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Errorf("[%d]: want %#x got %#x", i, want[i], got[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestReadUint64Slice_RejectsHugeLength is the regression test for
|
|
// lattigo issue #4: 9-byte input asking for 70 trillion uint64s must
|
|
// be rejected before any allocation.
|
|
func TestReadUint64Slice_RejectsHugeLength(t *testing.T) {
|
|
// 9-byte attack input from the lattice issue #4 reproducer
|
|
// produces a varint whose decoded value is way beyond MaxUint64SliceLen.
|
|
// We synthesize: varint = 70_368_955_777_453 (~70T), then expect
|
|
// a LimitError on MaxUint64SliceLen.
|
|
huge := uint64(70_368_955_777_453)
|
|
var buf bytes.Buffer
|
|
encodeUvarint(&buf, huge)
|
|
|
|
r := newReader(t, buf.Bytes(), DefaultLimitsLatticeWire)
|
|
_, err := r.ReadUint64Slice()
|
|
if err == nil {
|
|
t.Fatal("ReadUint64Slice with 70T length: returned nil error")
|
|
}
|
|
if !errors.Is(err, ErrLimitExceeded) {
|
|
t.Errorf("want ErrLimitExceeded, got %v", err)
|
|
}
|
|
var le *LimitError
|
|
if !errors.As(err, &le) {
|
|
t.Fatalf("error not a *LimitError: %T %v", err, err)
|
|
}
|
|
if le.What != "MaxUint64SliceLen" {
|
|
t.Errorf("LimitError.What = %q, want MaxUint64SliceLen", le.What)
|
|
}
|
|
if le.Got != huge {
|
|
t.Errorf("LimitError.Got = %d, want %d", le.Got, huge)
|
|
}
|
|
}
|
|
|
|
func TestReadUint32Slice_RejectsHugeLength(t *testing.T) {
|
|
huge := uint64(1_000_000_000)
|
|
var buf bytes.Buffer
|
|
encodeUvarint(&buf, huge)
|
|
|
|
r := newReader(t, buf.Bytes(), DefaultLimitsLatticeWire)
|
|
_, err := r.ReadUint32Slice()
|
|
if !errors.Is(err, ErrLimitExceeded) {
|
|
t.Errorf("want ErrLimitExceeded, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadUint16Slice_RejectsHugeLength(t *testing.T) {
|
|
huge := uint64(1_000_000_000)
|
|
var buf bytes.Buffer
|
|
encodeUvarint(&buf, huge)
|
|
|
|
r := newReader(t, buf.Bytes(), DefaultLimitsLatticeWire)
|
|
_, err := r.ReadUint16Slice()
|
|
if !errors.Is(err, ErrLimitExceeded) {
|
|
t.Errorf("want ErrLimitExceeded, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReadUint16(t *testing.T) {
|
|
r := newReader(t, []byte{0xCD, 0xAB}, DefaultLimitsLatticeWire)
|
|
v, err := r.ReadUint16()
|
|
if err != nil || v != 0xABCD {
|
|
t.Errorf("ReadUint16: %v %#x", err, v)
|
|
}
|
|
}
|
|
|
|
func TestReadUint32(t *testing.T) {
|
|
r := newReader(t, []byte{0x78, 0x56, 0x34, 0x12}, DefaultLimitsLatticeWire)
|
|
v, err := r.ReadUint32()
|
|
if err != nil || v != 0x12345678 {
|
|
t.Errorf("ReadUint32: %v %#x", err, v)
|
|
}
|
|
}
|
|
|
|
func TestReadUint64(t *testing.T) {
|
|
r := newReader(t, []byte{
|
|
0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11,
|
|
}, DefaultLimitsLatticeWire)
|
|
v, err := r.ReadUint64()
|
|
if err != nil || v != 0x1122334455667788 {
|
|
t.Errorf("ReadUint64: %v %#x", err, v)
|
|
}
|
|
}
|
|
|
|
func TestDepth(t *testing.T) {
|
|
limits := DefaultLimitsLatticeWire
|
|
limits.MaxDepth = 2
|
|
|
|
r := newReader(t, []byte{}, limits)
|
|
if err := r.EnterDepth(); err != nil {
|
|
t.Fatalf("depth 1: %v", err)
|
|
}
|
|
if err := r.EnterDepth(); err != nil {
|
|
t.Fatalf("depth 2: %v", err)
|
|
}
|
|
if err := r.EnterDepth(); !errors.Is(err, ErrLimitExceeded) {
|
|
t.Fatalf("depth 3: want ErrLimitExceeded, got %v", err)
|
|
}
|
|
r.ExitDepth()
|
|
r.ExitDepth()
|
|
r.ExitDepth() // safe to over-exit
|
|
}
|
|
|
|
func TestFrameBytesCap(t *testing.T) {
|
|
limits := DefaultLimitsLatticeWire
|
|
limits.MaxFrameBytes = 4
|
|
|
|
r := newReader(t, []byte{1, 2, 3, 4, 5}, limits)
|
|
if _, err := r.ReadUint32(); err != nil {
|
|
t.Fatalf("first 4 bytes: %v", err)
|
|
}
|
|
// Next byte read must hit MaxFrameBytes.
|
|
if _, err := r.ReadUint16(); !errors.Is(err, ErrLimitExceeded) {
|
|
t.Errorf("over-cap: want ErrLimitExceeded, got %v", err)
|
|
}
|
|
}
|