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.
161 lines
4.8 KiB
Go
161 lines
4.8 KiB
Go
// Copyright (c) 2026 Lux Industries Inc.
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// Package backend defines how `luxfi/math` selects between CPU and GPU
|
|
// implementations of the same primitive.
|
|
//
|
|
// LP-107 §"Backend dispatch" — the canonical motivation. Backends are
|
|
// interchangeable performance realizations of one canonical contract;
|
|
// KATs prove byte-equality across them; the choice of backend MUST
|
|
// NEVER alter transcript bytes for consensus paths.
|
|
//
|
|
// Policy enum:
|
|
//
|
|
// BackendPureGo — pure-Go reference implementation. Always works.
|
|
// BackendNativeCPU — native CPU implementation (cgo + C++/SIMD).
|
|
// BackendGPUPreferred — try GPU; fall back to CPU on unavailability.
|
|
// BackendGPURequired — GPU MUST be available; error if not.
|
|
//
|
|
// Registry: each substrate package (math/ntt, math/poly, math/sample,
|
|
// math/codec, math/rns) exposes its own backend interface and a
|
|
// process-wide registry of registered backends. This package owns the
|
|
// shared Policy enum + lookup helper; per-primitive interfaces live in
|
|
// the consuming package.
|
|
package backend
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/luxfi/math/params"
|
|
)
|
|
|
|
// Policy selects a backend at dispatch time.
|
|
type Policy uint8
|
|
|
|
const (
|
|
// PolicyPureGo forces the pure-Go reference path. Used for
|
|
// debugging, incident response, and consensus-critical paths
|
|
// where determinism trumps speed.
|
|
PolicyPureGo Policy = 0
|
|
|
|
// PolicyNativeCPU prefers the native-cpu (cgo / SIMD) backend if
|
|
// available; falls back to pure-Go otherwise.
|
|
PolicyNativeCPU Policy = 1
|
|
|
|
// PolicyGPUPreferred prefers GPU if available, falls back to native-
|
|
// CPU, then pure-Go. Used when the workload amortizes GPU dispatch.
|
|
PolicyGPUPreferred Policy = 2
|
|
|
|
// PolicyGPURequired demands GPU; the call errors out if no GPU
|
|
// backend is registered.
|
|
PolicyGPURequired Policy = 3
|
|
)
|
|
|
|
// String makes Policy printable.
|
|
func (p Policy) String() string {
|
|
switch p {
|
|
case PolicyPureGo:
|
|
return "pure-go"
|
|
case PolicyNativeCPU:
|
|
return "native-cpu"
|
|
case PolicyGPUPreferred:
|
|
return "gpu-preferred"
|
|
case PolicyGPURequired:
|
|
return "gpu-required"
|
|
default:
|
|
return fmt.Sprintf("policy(%d)", uint8(p))
|
|
}
|
|
}
|
|
|
|
// Validate reports whether p is a known policy.
|
|
func (p Policy) Validate() error {
|
|
switch p {
|
|
case PolicyPureGo, PolicyNativeCPU, PolicyGPUPreferred, PolicyGPURequired:
|
|
return nil
|
|
}
|
|
return fmt.Errorf("backend: unknown Policy %d", uint8(p))
|
|
}
|
|
|
|
// ErrUnavailable signals that a required backend is not present in the
|
|
// process. Returned by ResolveOrError when PolicyGPURequired is set
|
|
// but no GPU backend is registered.
|
|
var ErrUnavailable = errors.New("backend: required backend unavailable")
|
|
|
|
// Resolve returns the appropriate BackendID for the given policy and
|
|
// the set of registered backends. The order of preference is:
|
|
//
|
|
// PolicyGPURequired: GPU only — error if no GPU registered
|
|
// PolicyGPUPreferred: GPU > native-cpu > pure-go
|
|
// PolicyNativeCPU: native-cpu > pure-go
|
|
// PolicyPureGo: pure-go only
|
|
//
|
|
// This function is the single decision point for dispatch ordering.
|
|
// Per-primitive packages (math/ntt, math/poly, etc.) consult it before
|
|
// invoking a backend.
|
|
func Resolve(policy Policy, registered map[params.BackendID]bool) (params.BackendID, error) {
|
|
if err := policy.Validate(); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
gpuFamily := []params.BackendID{
|
|
params.BackendCUDA, params.BackendMetal, params.BackendWGSL,
|
|
}
|
|
cpuNative := []params.BackendID{
|
|
params.BackendNative, params.BackendAVX2, params.BackendNEON,
|
|
}
|
|
|
|
first := func(family []params.BackendID) (params.BackendID, bool) {
|
|
for _, id := range family {
|
|
if registered[id] {
|
|
return id, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
switch policy {
|
|
case PolicyGPURequired:
|
|
if id, ok := first(gpuFamily); ok {
|
|
return id, nil
|
|
}
|
|
return "", fmt.Errorf("backend: %w (policy=GPURequired, registered=%v)",
|
|
ErrUnavailable, keysOf(registered))
|
|
case PolicyGPUPreferred:
|
|
if id, ok := first(gpuFamily); ok {
|
|
return id, nil
|
|
}
|
|
if id, ok := first(cpuNative); ok {
|
|
return id, nil
|
|
}
|
|
if registered[params.BackendPureGo] {
|
|
return params.BackendPureGo, nil
|
|
}
|
|
return "", fmt.Errorf("backend: no backend registered")
|
|
case PolicyNativeCPU:
|
|
if id, ok := first(cpuNative); ok {
|
|
return id, nil
|
|
}
|
|
if registered[params.BackendPureGo] {
|
|
return params.BackendPureGo, nil
|
|
}
|
|
return "", fmt.Errorf("backend: no backend registered")
|
|
case PolicyPureGo:
|
|
if registered[params.BackendPureGo] {
|
|
return params.BackendPureGo, nil
|
|
}
|
|
return "", fmt.Errorf("backend: PureGo not registered (impossible — pure-Go is the canonical reference)")
|
|
}
|
|
return "", fmt.Errorf("backend: unreachable")
|
|
}
|
|
|
|
func keysOf(m map[params.BackendID]bool) []params.BackendID {
|
|
out := make([]params.BackendID, 0, len(m))
|
|
for k, v := range m {
|
|
if v {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
return out
|
|
}
|