mirror of
https://github.com/luxfi/precompile.git
synced 2026-07-27 03:33:45 +00:00
fix(bindings/rust): lux-precompile-sys — un-collide the FFI crate from the sdk-rs wrapper
88b7c9f renamed luxprecompile-sys -> lux-precompile for lux-consensus naming
consistency, but that (a) broke every consumer importing luxprecompile_sys and
(b) collided with sdk-rs's idiomatic wrapper crate, which is ALSO lux-precompile
— cargo refuses two same-named packages in one graph. Keep the family consistent
AND the Rust FFI convention: the -sys crate is lux-precompile-sys, the wrapper
stays lux-precompile. Consumed as a local path dep from sdk-rs (no crates.io
publish required).
This commit is contained in:
Generated
+1
-1
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "lux-precompile"
|
||||
name = "lux-precompile-sys"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "lux-precompile"
|
||||
name = "lux-precompile-sys"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Lux Network — FFI bindings to libluxprecompile (all Lux EVM precompiles via Go shared library)"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# luxprecompile-sys
|
||||
# lux-precompile-sys
|
||||
|
||||
FFI bindings to `libluxprecompile` — every Lux EVM precompile (PQ verify, Quasar,
|
||||
Blake3, FROST, dex, AI, ZK, …) reachable via one Go-built shared library, one
|
||||
@@ -34,7 +34,7 @@ The `build.rs` searches for the library in this order:
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use luxprecompile_sys::{run, list, required_gas};
|
||||
use lux_precompile_sys::{run, list, required_gas};
|
||||
|
||||
// List all registered precompiles
|
||||
let precompiles = list().unwrap();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,ignore
|
||||
//! use luxprecompile_sys::{run, list, required_gas};
|
||||
//! use lux_precompile_sys::{run, list, required_gas};
|
||||
//!
|
||||
//! // List all registered precompiles
|
||||
//! let precompiles = list().unwrap();
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/crypto"
|
||||
)
|
||||
|
||||
// GPU acceleration of the FHE precompile, stated precisely.
|
||||
//
|
||||
// luxfi/fhe is an FHEW/TFHE scheme: each ciphertext is a bundle of LWE bits
|
||||
// and every arithmetic/comparison op is a Boolean circuit whose gates are
|
||||
// programmable bootstraps. The dominant cost of a bootstrap is the negacyclic
|
||||
// number-theoretic transform (NTT) over the blind-rotation ring, run on the
|
||||
// CPU by github.com/luxfi/lattice/v7. The GPU FHE kernels accelerate exactly
|
||||
// that NTT (and, in the fused-PBS bridge, the whole blind rotation).
|
||||
//
|
||||
// THE CONSENSUS INVARIANT. The FHE precompile is on a deterministic execution
|
||||
// path: every validator must derive the byte-identical result ciphertext, or
|
||||
// the chain forks. Therefore GPU acceleration here is *only ever* a
|
||||
// transparent, byte-identical optimization of the underlying NTT. It must
|
||||
// never change which ciphertext is produced. The gate below runs the GPU NTT
|
||||
// and the canonical pure-Go NTT on a deterministic corpus and refuses to arm
|
||||
// unless every coefficient is byte-identical. Any divergence, missing GPU
|
||||
// library, or link/runtime error leaves the precompile on the pure-Go path
|
||||
// (fail-closed to CPU). A one-coefficient disagreement between an accelerated
|
||||
// validator and a pure-Go validator is a fork; the gate exists to make that
|
||||
// impossible by construction.
|
||||
//
|
||||
// This is the orthogonal decomposition (church of Hickey): WHAT the precompile
|
||||
// computes (a TFHE op over LWE-bit ciphertexts) is one concern, owned by the
|
||||
// CPU path in fhe_ops.go; WHETHER the NTT underneath runs on a byte-identical
|
||||
// GPU kernel is a separate concern, owned here. The two are never braided: the
|
||||
// op handlers do not know or care whether the NTT was accelerated.
|
||||
|
||||
// GPUNTTStatus reports whether byte-identical GPU acceleration of the TFHE
|
||||
// bootstrap NTT is armed for the FHE precompile's ring on this build and host.
|
||||
type GPUNTTStatus struct {
|
||||
Built bool // compiled with `-tags gpu` and cgo
|
||||
Armed bool // byte-equality gate passed and GPU NTT dispatch enabled
|
||||
Backend string // "CPU (pure Go)", "Metal", "CUDA", ...
|
||||
N int // bootstrap ring degree the gate probed
|
||||
Q uint64 // bootstrap ring modulus the gate probed
|
||||
Reason string // human-readable status / why not armed
|
||||
}
|
||||
|
||||
var (
|
||||
gpuNTTOnce sync.Once
|
||||
gpuNTTStatus GPUNTTStatus
|
||||
)
|
||||
|
||||
// GPUNTT returns the GPU-NTT acceleration status for the FHE precompile,
|
||||
// running the byte-equality gate exactly once. Safe for concurrent use. In a
|
||||
// default build it always reports the pure-Go CPU path.
|
||||
func GPUNTT() GPUNTTStatus {
|
||||
gpuNTTOnce.Do(func() { gpuNTTStatus = armGPUNTT() })
|
||||
return gpuNTTStatus
|
||||
}
|
||||
|
||||
// byteEqualCorpus is the determinism gate's sole decision predicate: it reports
|
||||
// whether every coefficient of every GPU output vector equals the corresponding
|
||||
// CPU output vector. Only a true result may arm GPU dispatch. It is factored
|
||||
// out here, free of any GPU dependency, so the fail-closed behaviour is
|
||||
// unit-testable on every platform — including CI nodes that have no GPU.
|
||||
//
|
||||
// On divergence it returns ok=false with the (vector, coeff) coordinates of the
|
||||
// first mismatch so a rejection is diagnosable in logs.
|
||||
func byteEqualCorpus(cpu, gpu [][]uint64) (ok bool, vec, coeff int) {
|
||||
if len(cpu) != len(gpu) {
|
||||
return false, -1, -1
|
||||
}
|
||||
for v := range cpu {
|
||||
if len(cpu[v]) != len(gpu[v]) {
|
||||
return false, v, -1
|
||||
}
|
||||
for i := range cpu[v] {
|
||||
if cpu[v][i] != gpu[v][i] {
|
||||
return false, v, i
|
||||
}
|
||||
}
|
||||
}
|
||||
return true, -1, -1
|
||||
}
|
||||
|
||||
// deterministicVec returns a length-N ring element whose coefficients are a
|
||||
// domain-separated keccak stream reduced mod Q. Deterministic in (seed, N, Q)
|
||||
// so the byte-equality corpus is identical on every validator and every run —
|
||||
// a probe vector must not depend on wall-clock or PRNG state.
|
||||
func deterministicVec(seed uint64, N int, Q uint64) []uint64 {
|
||||
out := make([]uint64, N)
|
||||
var ctr [16]byte
|
||||
binary.BigEndian.PutUint64(ctr[:8], seed)
|
||||
for i := 0; i < N; i++ {
|
||||
binary.BigEndian.PutUint64(ctr[8:], uint64(i))
|
||||
h := crypto.Keccak256(ctr[:])
|
||||
out[i] = binary.BigEndian.Uint64(h[:8]) % Q
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build !cgo || !gpu
|
||||
|
||||
package fhe
|
||||
|
||||
// builtWithGPU reports whether this binary was compiled with the GPU NTT
|
||||
// substrate available (`-tags gpu` + cgo).
|
||||
const builtWithGPU = false
|
||||
|
||||
// armGPUNTT is the default build's no-op. Without `-tags gpu` (and cgo), the
|
||||
// FHE precompile runs the pure-Go TFHE path end-to-end; there is no GPU
|
||||
// dispatcher to arm. GPU NTT acceleration is opt-in at build time via
|
||||
// `-tags gpu` with cgo enabled and the lux-lattice GPU library linked
|
||||
// (pkg-config: lux-lattice). See gpu_ntt_on.go for the armed path.
|
||||
func armGPUNTT() GPUNTTStatus {
|
||||
return GPUNTTStatus{
|
||||
Built: false,
|
||||
Armed: false,
|
||||
Backend: "CPU (pure Go)",
|
||||
Reason: "built without -tags gpu (cgo): pure-Go TFHE NTT, no GPU dispatcher",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo && gpu
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
luxfhe "github.com/luxfi/fhe"
|
||||
"github.com/luxfi/lattice/v7/gpu"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
)
|
||||
|
||||
// builtWithGPU reports whether this binary was compiled with the GPU NTT
|
||||
// substrate available (`-tags gpu` + cgo).
|
||||
const builtWithGPU = true
|
||||
|
||||
// gpuProbeVectors is the size of the deterministic byte-equality corpus the
|
||||
// gate runs (forward and inverse) before arming GPU dispatch. Each vector is a
|
||||
// full ring element. Sized to exercise many distinct coefficient values
|
||||
// through the GPU modular-reduction path for the FHE bootstrap modulus.
|
||||
const gpuProbeVectors = 64
|
||||
|
||||
// armGPUNTT derives the FHE bootstrap ring for the precompile's parameter
|
||||
// literal (fhe.PN10QP27), binds the lattice GPU Montgomery NTT context to that
|
||||
// exact SubRing, and runs the byte-equality gate: forward and inverse GPU NTT
|
||||
// versus the canonical pure-Go SubRing NTT across a deterministic corpus. GPU
|
||||
// dispatch is enabled ONLY if every coefficient is byte-identical. Any
|
||||
// divergence or error unregisters the GPU context and leaves the ring on the
|
||||
// pure-Go path (fail-closed to CPU).
|
||||
//
|
||||
// Reach (stated honestly): the precompile and luxfi/fhe share the bootstrap
|
||||
// ring's (N, Q) but not the *ring.SubRing pointer — fhe encapsulates paramsBR,
|
||||
// exposing only N()/NBR()/QLWE()/QBR(). The lattice GPU dispatcher keys by
|
||||
// pointer identity (gpu.LookupSubRing), so this function proves and arms the
|
||||
// byte-identical GPU NTT substrate for the precompile's exact ring parameters.
|
||||
// Routing fhe's *own* bootstrap SubRing through that substrate is a one-line
|
||||
// gpu.RegisterSubRing hook inside luxfi/fhe (NewShortIntEvaluator), under the
|
||||
// same build tag; the byte-equality contract proven here is the prerequisite
|
||||
// that makes that hook consensus-safe. See the GPU bridge note in the package
|
||||
// doc / report.
|
||||
func armGPUNTT() GPUNTTStatus {
|
||||
// Derive the exact bootstrap ring parameters from the precompile's FHE
|
||||
// parameter literal so this gate tracks any future parameter change
|
||||
// instead of hard-coding (N, Q).
|
||||
params, err := luxfhe.NewParametersFromLiteral(luxfhe.PN10QP27)
|
||||
if err != nil {
|
||||
return GPUNTTStatus{Built: true, Backend: "CPU (pure Go)",
|
||||
Reason: fmt.Sprintf("param load failed: %v; pure-Go NTT", err)}
|
||||
}
|
||||
N := params.NBR()
|
||||
Q := params.QBR()
|
||||
st := GPUNTTStatus{Built: true, Backend: "CPU (pure Go)", N: N, Q: Q}
|
||||
|
||||
if !gpu.Available() {
|
||||
st.Reason = "GPU library unavailable at runtime; pure-Go NTT"
|
||||
return st
|
||||
}
|
||||
|
||||
// Canonical pure-Go ring (the consensus oracle) for (N, Q).
|
||||
r, err := ring.NewRing(N, []uint64{Q})
|
||||
if err != nil {
|
||||
st.Reason = fmt.Sprintf("ring.NewRing(N=%d, Q=%#x): %v; pure-Go NTT", N, Q, err)
|
||||
return st
|
||||
}
|
||||
sr := r.SubRings[0]
|
||||
|
||||
// Bind a Montgomery GPU context to this exact SubRing. The context
|
||||
// captures the SubRing's roots / MRedConstant / NInv, so its output is
|
||||
// byte-equal to ring.SubRing.NTT by construction — the gate below
|
||||
// verifies that construction holds for this build and host.
|
||||
if _, err := gpu.RegisterSubRing(sr); err != nil {
|
||||
st.Reason = fmt.Sprintf("gpu.RegisterSubRing: %v; pure-Go NTT", err)
|
||||
return st
|
||||
}
|
||||
|
||||
if ok, vec, coeff := probeRingByteEqual(sr, N, Q); !ok {
|
||||
gpu.UnregisterSubRing(sr) // FAIL CLOSED: divergent GPU never arms.
|
||||
st.Reason = fmt.Sprintf(
|
||||
"GPU NTT diverged from CPU at vec=%d coeff=%d; fail-closed to CPU", vec, coeff)
|
||||
return st
|
||||
}
|
||||
|
||||
// Gate passed. Enable single-poly GPU dispatch at >= N for registered
|
||||
// rings. The lattice dispatcher still falls through to the pure-Go NTT for
|
||||
// any unregistered ring or on any C error, so arming only ever *adds* a
|
||||
// byte-identical fast path; it never removes the CPU fallback.
|
||||
gpu.SetNTTThreshold(uint32(N))
|
||||
st.Armed = true
|
||||
st.Backend = gpu.GetBackend()
|
||||
st.Reason = fmt.Sprintf(
|
||||
"GPU NTT armed for FHE bootstrap ring N=%d Q=%#x on %s (byte-identical gate passed)",
|
||||
N, Q, st.Backend)
|
||||
return st
|
||||
}
|
||||
|
||||
// probeRingByteEqual runs the byte-equality corpus through the GPU-bound
|
||||
// SubRing context and the pure-Go SubRing, in both directions, and reports
|
||||
// whether every coefficient is byte-identical. Returns the (vector, coeff)
|
||||
// coordinates of the first divergence otherwise.
|
||||
func probeRingByteEqual(sr *ring.SubRing, N int, Q uint64) (bool, int, int) {
|
||||
ctx := gpu.LookupSubRing(sr)
|
||||
if ctx == nil {
|
||||
return false, -1, -1
|
||||
}
|
||||
|
||||
// Forward direction.
|
||||
cpuFwd := make([][]uint64, gpuProbeVectors)
|
||||
gpuFwd := make([][]uint64, gpuProbeVectors)
|
||||
for v := 0; v < gpuProbeVectors; v++ {
|
||||
in := deterministicVec(uint64(v), N, Q)
|
||||
c := make([]uint64, N)
|
||||
g := make([]uint64, N)
|
||||
copy(c, in)
|
||||
copy(g, in)
|
||||
sr.NTT(c, c) // pure-Go consensus oracle
|
||||
if err := ctx.Forward(g, 1); err != nil {
|
||||
return false, v, -1
|
||||
}
|
||||
cpuFwd[v] = c
|
||||
gpuFwd[v] = g
|
||||
}
|
||||
if ok, v, i := byteEqualCorpus(cpuFwd, gpuFwd); !ok {
|
||||
return false, v, i
|
||||
}
|
||||
|
||||
// Inverse direction, applied to the (matching) forward outputs.
|
||||
cpuBwd := make([][]uint64, gpuProbeVectors)
|
||||
gpuBwd := make([][]uint64, gpuProbeVectors)
|
||||
for v := 0; v < gpuProbeVectors; v++ {
|
||||
c := make([]uint64, N)
|
||||
g := make([]uint64, N)
|
||||
copy(c, cpuFwd[v])
|
||||
copy(g, cpuFwd[v])
|
||||
sr.INTT(c, c)
|
||||
if err := ctx.Backward(g, 1); err != nil {
|
||||
return false, v, -1
|
||||
}
|
||||
cpuBwd[v] = c
|
||||
gpuBwd[v] = g
|
||||
}
|
||||
if ok, v, i := byteEqualCorpus(cpuBwd, gpuBwd); !ok {
|
||||
return false, v, i
|
||||
}
|
||||
return true, -1, -1
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
//go:build cgo && gpu
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
luxfhe "github.com/luxfi/fhe"
|
||||
"github.com/luxfi/lattice/v7/gpu"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
)
|
||||
|
||||
// These tests are the real-hardware determinism corpus. They run only in a
|
||||
// `-tags gpu` cgo build linked against the lux-lattice GPU library, on a host
|
||||
// with a usable GPU (Metal on Apple Silicon, CUDA on NVIDIA). They prove the
|
||||
// consensus invariant the gate depends on: the GPU negacyclic NTT is
|
||||
// byte-identical to the canonical pure-Go NTT for the FHE precompile's exact
|
||||
// bootstrap ring parameters.
|
||||
//
|
||||
// Run on Metal (M1):
|
||||
// SDKROOT=$(xcrun --show-sdk-path) \
|
||||
// PKG_CONFIG_PATH=<dir with lux-lattice.pc -> libluxlattice> \
|
||||
// DYLD_LIBRARY_PATH=/opt/homebrew/lib \
|
||||
// CGO_ENABLED=1 GOWORK=off go test -tags gpu ./fhe/ -run GPU -v
|
||||
|
||||
// fheBootstrapRing returns the precompile's actual TFHE blind-rotation ring
|
||||
// (PN10QP27: N=1024, Q=0x7fff801) as a fully-initialized pure-Go SubRing.
|
||||
func fheBootstrapRing(t *testing.T) (*ring.SubRing, int, uint64) {
|
||||
t.Helper()
|
||||
params, err := luxfhe.NewParametersFromLiteral(luxfhe.PN10QP27)
|
||||
if err != nil {
|
||||
t.Fatalf("load PN10QP27: %v", err)
|
||||
}
|
||||
N, Q := params.NBR(), params.QBR()
|
||||
r, err := ring.NewRing(N, []uint64{Q})
|
||||
if err != nil {
|
||||
t.Fatalf("ring.NewRing(N=%d, Q=%#x): %v", N, Q, err)
|
||||
}
|
||||
return r.SubRings[0], N, Q
|
||||
}
|
||||
|
||||
// TestGPU_FHEBootstrapNTT_ByteIdenticalToCPU is the keystone proof: for the FHE
|
||||
// precompile's exact bootstrap modulus, the GPU forward and inverse NTT equal
|
||||
// the pure-Go SubRing NTT coefficient-for-coefficient across a large corpus.
|
||||
// The existing lattice gpu suite only covers a 61-bit prime; this proves it for
|
||||
// 0x7fff801, the 27-bit modulus the precompile actually uses.
|
||||
func TestGPU_FHEBootstrapNTT_ByteIdenticalToCPU(t *testing.T) {
|
||||
if !gpu.Available() {
|
||||
t.Skip("GPU library not available at runtime")
|
||||
}
|
||||
sr, N, Q := fheBootstrapRing(t)
|
||||
t.Logf("FHE bootstrap ring: N=%d Q=%#x backend=%s", N, Q, gpu.GetBackend())
|
||||
|
||||
ctx, err := gpu.NewMontgomeryNTTContext(sr)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMontgomeryNTTContext: %v", err)
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
const vectors = 1024
|
||||
for v := 0; v < vectors; v++ {
|
||||
in := deterministicVec(uint64(v), N, Q)
|
||||
|
||||
// Forward: GPU vs pure-Go, byte-for-byte.
|
||||
cpuFwd := make([]uint64, N)
|
||||
gpuFwd := make([]uint64, N)
|
||||
copy(cpuFwd, in)
|
||||
copy(gpuFwd, in)
|
||||
sr.NTT(cpuFwd, cpuFwd)
|
||||
if err := ctx.Forward(gpuFwd, 1); err != nil {
|
||||
t.Fatalf("vec=%d Forward: %v", v, err)
|
||||
}
|
||||
for i := 0; i < N; i++ {
|
||||
if cpuFwd[i] != gpuFwd[i] {
|
||||
t.Fatalf("FORWARD divergence vec=%d coeff=%d: cpu=%#016x gpu=%#016x",
|
||||
v, i, cpuFwd[i], gpuFwd[i])
|
||||
}
|
||||
}
|
||||
|
||||
// Inverse on the matching forward output: GPU vs pure-Go.
|
||||
cpuBwd := make([]uint64, N)
|
||||
gpuBwd := make([]uint64, N)
|
||||
copy(cpuBwd, cpuFwd)
|
||||
copy(gpuBwd, cpuFwd)
|
||||
sr.INTT(cpuBwd, cpuBwd)
|
||||
if err := ctx.Backward(gpuBwd, 1); err != nil {
|
||||
t.Fatalf("vec=%d Backward: %v", v, err)
|
||||
}
|
||||
for i := 0; i < N; i++ {
|
||||
if cpuBwd[i] != gpuBwd[i] {
|
||||
t.Fatalf("INVERSE divergence vec=%d coeff=%d: cpu=%#016x gpu=%#016x",
|
||||
v, i, cpuBwd[i], gpuBwd[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("byte-identical GPU<->CPU NTT proven over %d vectors (forward+inverse) for FHE ring N=%d Q=%#x",
|
||||
vectors, N, Q)
|
||||
}
|
||||
|
||||
// TestGPU_RingDispatch_ByteIdentical proves the END-TO-END dispatcher path the
|
||||
// TFHE bootstrap actually hits: ring.Ring.NTT routes a *registered* SubRing
|
||||
// through the GPU and an *unregistered* SubRing through pure-Go. With dispatch
|
||||
// armed at threshold N, both must yield identical bytes — i.e. if luxfi/fhe
|
||||
// registers its bootstrap ring, every ringQBR.NTT inside blindrot.Evaluate is
|
||||
// byte-identical to the pure-Go path, hence so is the whole bootstrap, hence so
|
||||
// is every FHE op the precompile computes.
|
||||
func TestGPU_RingDispatch_ByteIdentical(t *testing.T) {
|
||||
if !gpu.Available() {
|
||||
t.Skip("GPU library not available at runtime")
|
||||
}
|
||||
_, N, Q := fheBootstrapRing(t)
|
||||
|
||||
// Two independent rings with the SAME parameters. Register only one.
|
||||
gpuRing, err := ring.NewRing(N, []uint64{Q})
|
||||
if err != nil {
|
||||
t.Fatalf("NewRing gpu: %v", err)
|
||||
}
|
||||
cpuRing, err := ring.NewRing(N, []uint64{Q})
|
||||
if err != nil {
|
||||
t.Fatalf("NewRing cpu: %v", err)
|
||||
}
|
||||
srGPU := gpuRing.SubRings[0]
|
||||
srCPU := cpuRing.SubRings[0]
|
||||
|
||||
if _, err := gpu.RegisterSubRing(srGPU); err != nil {
|
||||
t.Fatalf("RegisterSubRing: %v", err)
|
||||
}
|
||||
defer gpu.UnregisterSubRing(srGPU)
|
||||
gpu.SetNTTThreshold(uint32(N)) // arm single-poly dispatch at >= N
|
||||
defer gpu.SetNTTThreshold(0) // restore default (off)
|
||||
|
||||
const vectors = 256
|
||||
for v := 0; v < vectors; v++ {
|
||||
in := deterministicVec(uint64(v)+0xABCD, N, Q)
|
||||
viaGPU := make([]uint64, N)
|
||||
viaCPU := make([]uint64, N)
|
||||
copy(viaGPU, in)
|
||||
copy(viaCPU, in)
|
||||
|
||||
// Both calls are the exact ring.SubRing.NTT entry blindrot uses; only
|
||||
// srGPU is registered, so only it dispatches to the GPU.
|
||||
srGPU.NTT(viaGPU, viaGPU)
|
||||
srCPU.NTT(viaCPU, viaCPU)
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
if viaGPU[i] != viaCPU[i] {
|
||||
t.Fatalf("dispatch divergence vec=%d coeff=%d: gpu=%#016x cpu=%#016x",
|
||||
v, i, viaGPU[i], viaCPU[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Logf("ring.NTT dispatcher byte-identical (GPU-registered vs pure-Go) over %d vectors", vectors)
|
||||
}
|
||||
|
||||
// TestGPU_ArmGPUNTT_ArmsAndIsByteSafe proves the production gate (armGPUNTT via
|
||||
// GPUNTT) arms on real hardware AND reports a real GPU backend — meaning the
|
||||
// byte-equality corpus inside the gate passed for the FHE ring on this host.
|
||||
func TestGPU_ArmGPUNTT_ArmsAndIsByteSafe(t *testing.T) {
|
||||
if !gpu.Available() {
|
||||
t.Skip("GPU library not available at runtime")
|
||||
}
|
||||
st := GPUNTT()
|
||||
if !st.Built {
|
||||
t.Fatalf("Built=false in a -tags gpu binary")
|
||||
}
|
||||
if !st.Armed {
|
||||
t.Fatalf("gate did not arm on real GPU: %s", st.Reason)
|
||||
}
|
||||
if st.Backend == "CPU (pure Go)" {
|
||||
t.Fatalf("armed but backend still CPU: %s", st.Reason)
|
||||
}
|
||||
if st.N != 1024 || st.Q != 0x7fff801 {
|
||||
t.Fatalf("unexpected FHE ring params N=%d Q=%#x (want 1024/0x7fff801)", st.N, st.Q)
|
||||
}
|
||||
t.Logf("armed: %s", st.Reason)
|
||||
}
|
||||
|
||||
// TestGPU_Gate_FailClosedOnInjectedDivergence proves the gate's rejection
|
||||
// branch at the corpus level on real hardware: take the genuine GPU output,
|
||||
// flip one coefficient, and confirm byteEqualCorpus rejects it — the same
|
||||
// predicate armGPUNTT uses to decide whether to UnregisterSubRing and fall back
|
||||
// to CPU. (Metal cannot be made to diverge on demand, so divergence is injected
|
||||
// into a copy of its real output; the decision predicate is identical.)
|
||||
func TestGPU_Gate_FailClosedOnInjectedDivergence(t *testing.T) {
|
||||
if !gpu.Available() {
|
||||
t.Skip("GPU library not available at runtime")
|
||||
}
|
||||
sr, N, Q := fheBootstrapRing(t)
|
||||
ctx, err := gpu.NewMontgomeryNTTContext(sr)
|
||||
if err != nil {
|
||||
t.Fatalf("NewMontgomeryNTTContext: %v", err)
|
||||
}
|
||||
defer ctx.Close()
|
||||
|
||||
in := deterministicVec(7, N, Q)
|
||||
cpu := make([]uint64, N)
|
||||
gpuOut := make([]uint64, N)
|
||||
copy(cpu, in)
|
||||
copy(gpuOut, in)
|
||||
sr.NTT(cpu, cpu)
|
||||
if err := ctx.Forward(gpuOut, 1); err != nil {
|
||||
t.Fatalf("Forward: %v", err)
|
||||
}
|
||||
|
||||
// Genuine output is byte-identical: gate accepts.
|
||||
if ok, _, _ := byteEqualCorpus([][]uint64{cpu}, [][]uint64{gpuOut}); !ok {
|
||||
t.Fatalf("genuine GPU output rejected — real divergence on this host")
|
||||
}
|
||||
|
||||
// Inject a single-coefficient divergence: gate must reject (fail-closed).
|
||||
bad := make([]uint64, N)
|
||||
copy(bad, gpuOut)
|
||||
bad[N/2] ^= 1
|
||||
if ok, v, i := byteEqualCorpus([][]uint64{cpu}, [][]uint64{bad}); ok {
|
||||
t.Fatalf("injected divergence ACCEPTED — gate would fail open (fork risk)")
|
||||
} else {
|
||||
t.Logf("fail-closed confirmed: rejected at vec=%d coeff=%d", v, i)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (C) 2019-2026, Lux Industries, Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package fhe
|
||||
|
||||
import "testing"
|
||||
|
||||
// These tests pin the determinism gate's decision logic and the default-build
|
||||
// posture. They carry NO build tag and require NO GPU, so they run on every CI
|
||||
// node and prove the fail-closed property independently of any accelerator.
|
||||
|
||||
// TestByteEqualCorpus_AcceptsIdentical proves the gate accepts a byte-identical
|
||||
// corpus (the only condition under which GPU dispatch may arm).
|
||||
func TestByteEqualCorpus_AcceptsIdentical(t *testing.T) {
|
||||
cpu := [][]uint64{{1, 2, 3, 4}, {5, 6, 7, 8}}
|
||||
gpu := [][]uint64{{1, 2, 3, 4}, {5, 6, 7, 8}}
|
||||
ok, v, i := byteEqualCorpus(cpu, gpu)
|
||||
if !ok {
|
||||
t.Fatalf("identical corpus rejected at vec=%d coeff=%d", v, i)
|
||||
}
|
||||
}
|
||||
|
||||
// TestByteEqualCorpus_FailClosedOnDivergence proves the gate REJECTS any
|
||||
// single-coefficient divergence and pinpoints it. This is the consensus
|
||||
// safety property: a GPU NTT that differs from the CPU NTT in even one
|
||||
// coefficient must never arm, so a divergent ciphertext can never commit.
|
||||
func TestByteEqualCorpus_FailClosedOnDivergence(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cpu, gpu [][]uint64
|
||||
wantVec int
|
||||
wantCoeff int
|
||||
}{
|
||||
{
|
||||
name: "single coeff flip",
|
||||
cpu: [][]uint64{{1, 2, 3, 4}, {5, 6, 7, 8}},
|
||||
gpu: [][]uint64{{1, 2, 3, 4}, {5, 6, 99, 8}}, // 7 -> 99
|
||||
wantVec: 1,
|
||||
wantCoeff: 2,
|
||||
},
|
||||
{
|
||||
name: "off-by-one in last coeff",
|
||||
cpu: [][]uint64{{0, 0, 0, 0}},
|
||||
gpu: [][]uint64{{0, 0, 0, 1}},
|
||||
wantVec: 0,
|
||||
wantCoeff: 3,
|
||||
},
|
||||
{
|
||||
name: "length mismatch (vector count)",
|
||||
cpu: [][]uint64{{1}, {2}},
|
||||
gpu: [][]uint64{{1}},
|
||||
wantVec: -1,
|
||||
wantCoeff: -1,
|
||||
},
|
||||
{
|
||||
name: "length mismatch (vector size)",
|
||||
cpu: [][]uint64{{1, 2, 3}},
|
||||
gpu: [][]uint64{{1, 2}},
|
||||
wantVec: 0,
|
||||
wantCoeff: -1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ok, v, i := byteEqualCorpus(tc.cpu, tc.gpu)
|
||||
if ok {
|
||||
t.Fatalf("divergent corpus was ACCEPTED — gate failed open (fork risk)")
|
||||
}
|
||||
if v != tc.wantVec || i != tc.wantCoeff {
|
||||
t.Fatalf("divergence located at vec=%d coeff=%d, want vec=%d coeff=%d",
|
||||
v, i, tc.wantVec, tc.wantCoeff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeterministicVec_StableAndInRange proves the probe corpus is a pure
|
||||
// function of (seed, N, Q) — identical across runs/validators — and that every
|
||||
// coefficient is reduced into [0, Q). A probe vector that varied per run could
|
||||
// let a divergence slip past the gate on one node but not another.
|
||||
func TestDeterministicVec_StableAndInRange(t *testing.T) {
|
||||
const N = 1024
|
||||
const Q = uint64(0x7fff801) // FHE bootstrap modulus (PN10QP27)
|
||||
a := deterministicVec(42, N, Q)
|
||||
b := deterministicVec(42, N, Q)
|
||||
if len(a) != N {
|
||||
t.Fatalf("len=%d want %d", len(a), N)
|
||||
}
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
t.Fatalf("non-deterministic at i=%d: %d != %d", i, a[i], b[i])
|
||||
}
|
||||
if a[i] >= Q {
|
||||
t.Fatalf("coeff %d = %d not reduced mod Q=%d", i, a[i], Q)
|
||||
}
|
||||
}
|
||||
// Different seed must give a different stream (sanity: not all-equal).
|
||||
c := deterministicVec(43, N, Q)
|
||||
same := true
|
||||
for i := range a {
|
||||
if a[i] != c[i] {
|
||||
same = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if same {
|
||||
t.Fatalf("seed 42 and 43 produced identical vectors")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGPUNTT_DefaultOff_IsPureGo proves the default build (no -tags gpu) never
|
||||
// arms GPU dispatch and reports the pure-Go CPU path. In a `-tags gpu` build
|
||||
// this test is replaced by the real-HW arming assertions in gpu_ntt_on_test.go.
|
||||
func TestGPUNTT_DefaultOff_IsPureGo(t *testing.T) {
|
||||
if builtWithGPU {
|
||||
t.Skip("built with -tags gpu; armed-path assertions live in gpu_ntt_on_test.go")
|
||||
}
|
||||
st := GPUNTT()
|
||||
if st.Built {
|
||||
t.Fatalf("default build reports Built=true")
|
||||
}
|
||||
if st.Armed {
|
||||
t.Fatalf("default build reports Armed=true — GPU must be opt-in")
|
||||
}
|
||||
if st.Backend != "CPU (pure Go)" {
|
||||
t.Fatalf("default backend = %q, want CPU (pure Go)", st.Backend)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user