Files
math/codec/codec_test.go
zeekay c9bc60da0e LP-107 Phase 2: math substrate — params, backend, codec, modarith,
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.
2026-05-04 09:28:23 -07:00

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)
}
}