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:
hanzo-dev
2026-07-14 22:38:10 -07:00
parent 3466c5e327
commit 4bf8a15c35
9 changed files with 630 additions and 5 deletions
+1 -1
View File
@@ -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 -1
View File
@@ -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)"
+2 -2
View File
@@ -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();
+1 -1
View File
@@ -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
View File
@@ -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
}
+24
View File
@@ -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",
}
}
+148
View File
@@ -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
}
+222
View File
@@ -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)
}
}
+128
View File
@@ -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)
}
}