mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
feat(types): FHECiphertextHeader + FHEPrecompileArtifact
LP-137-FHE-TYPING. New package github.com/luxfi/fhe/types wrapping every FHE buffer with the metadata required for safe dispatch: - FHEScheme (uint32 enum): TFHE, FHEW, CKKS, BFV, BGV. - FHECiphertextHeader (144-byte struct): ParamsHash, KeyID, CircuitID, Scheme, Level, N, ModulusCount, Domain, Reserved. Digest() returns SHA-256 of canonical 144-byte little-endian encoding; deterministic across runs. MatchesContext() rejects N + Domain mismatch. - FHEPrecompileArtifact (232-byte struct): seven 32-byte digest fields (ParamsHash, KeyRoot, InputCiphertextRoot, OutputCiphertextRoot, CircuitRoot, ThresholdTranscriptRoot, AttestationRoot) + OpCount + FailedCount. Digest() is the contribution to fchain_fhe_root. IsThreshold / IsAttested flag-helpers on the *Root fields. Layout byte-stable with the C++ mirror at luxcpp/fhe/include/lux/fhe/types. Cross-language byte image tests verify both struct types. 14 Go tests pass.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
// FHEPrecompileArtifact is the fixed-size record produced by an FHE
|
||||
// precompile invocation. It is the unit of evidence consumed by the
|
||||
// QuasarGPU integration layer (LP-132) and the FChainTFHE cert lane
|
||||
// (LP-013, LP-020 §3.0).
|
||||
//
|
||||
// All *Root fields are 32-byte SHA-256 digests over the canonical encoding
|
||||
// of the corresponding payload. Zero (all-zero bytes) signals "not
|
||||
// applicable for this artifact" and must be accepted by the verifier as
|
||||
// long as the corresponding feature was not requested.
|
||||
//
|
||||
// Layout is byte-stable across Go and C++. Total size: 232 bytes.
|
||||
type FHEPrecompileArtifact struct {
|
||||
// ParamsHash binds the artifact to a specific FHE parameter set.
|
||||
// 32 bytes, offset 0.
|
||||
ParamsHash [32]byte
|
||||
|
||||
// KeyRoot is the digest of the public/evaluation-key material used.
|
||||
// 32 bytes, offset 32.
|
||||
KeyRoot [32]byte
|
||||
|
||||
// InputCiphertextRoot is a Merkle / hash-list digest of all input
|
||||
// ciphertext headers consumed by this precompile call.
|
||||
// 32 bytes, offset 64.
|
||||
InputCiphertextRoot [32]byte
|
||||
|
||||
// OutputCiphertextRoot is the digest of the output ciphertext headers
|
||||
// produced by this precompile call.
|
||||
// 32 bytes, offset 96.
|
||||
OutputCiphertextRoot [32]byte
|
||||
|
||||
// CircuitRoot is the digest of the circuit / policy program executed.
|
||||
// 32 bytes, offset 128.
|
||||
CircuitRoot [32]byte
|
||||
|
||||
// ThresholdTranscriptRoot is the digest of the threshold-decryption
|
||||
// transcript when this artifact is part of a threshold round.
|
||||
// All-zero if not threshold.
|
||||
// 32 bytes, offset 160.
|
||||
ThresholdTranscriptRoot [32]byte
|
||||
|
||||
// AttestationRoot is the digest of the GPU attestation chain proving
|
||||
// the precompile ran on attested hardware (LP-127).
|
||||
// All-zero if confidential attestation was not requested.
|
||||
// 32 bytes, offset 192.
|
||||
AttestationRoot [32]byte
|
||||
|
||||
// OpCount is the number of FHE operations (gates, mults, rotations)
|
||||
// charged to this artifact.
|
||||
// 4 bytes, offset 224.
|
||||
OpCount uint32
|
||||
|
||||
// FailedCount is the number of operations that failed validation
|
||||
// (e.g. domain mismatch rejected at the boundary). Non-zero values
|
||||
// mean the artifact is observable evidence of a misconfigured caller.
|
||||
// 4 bytes, offset 228.
|
||||
FailedCount uint32
|
||||
}
|
||||
|
||||
// canonicalEncoding returns the deterministic byte representation used for
|
||||
// hashing.
|
||||
func (a *FHEPrecompileArtifact) canonicalEncoding() []byte {
|
||||
buf := make([]byte, 232)
|
||||
copy(buf[0:32], a.ParamsHash[:])
|
||||
copy(buf[32:64], a.KeyRoot[:])
|
||||
copy(buf[64:96], a.InputCiphertextRoot[:])
|
||||
copy(buf[96:128], a.OutputCiphertextRoot[:])
|
||||
copy(buf[128:160], a.CircuitRoot[:])
|
||||
copy(buf[160:192], a.ThresholdTranscriptRoot[:])
|
||||
copy(buf[192:224], a.AttestationRoot[:])
|
||||
binary.LittleEndian.PutUint32(buf[224:228], a.OpCount)
|
||||
binary.LittleEndian.PutUint32(buf[228:232], a.FailedCount)
|
||||
return buf
|
||||
}
|
||||
|
||||
// Digest returns SHA-256 of the canonical encoding. This value is the
|
||||
// `fchain_fhe_root` contribution from this precompile call.
|
||||
func (a *FHEPrecompileArtifact) Digest() [32]byte {
|
||||
return sha256.Sum256(a.canonicalEncoding())
|
||||
}
|
||||
|
||||
// IsThreshold reports whether this artifact participated in a threshold
|
||||
// decryption round.
|
||||
func (a *FHEPrecompileArtifact) IsThreshold() bool {
|
||||
return a.ThresholdTranscriptRoot != [32]byte{}
|
||||
}
|
||||
|
||||
// IsAttested reports whether this artifact was produced under hardware
|
||||
// attestation.
|
||||
func (a *FHEPrecompileArtifact) IsAttested() bool {
|
||||
return a.AttestationRoot != [32]byte{}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func makeArtifact() FHEPrecompileArtifact {
|
||||
var a FHEPrecompileArtifact
|
||||
for i := range a.ParamsHash {
|
||||
a.ParamsHash[i] = byte(i)
|
||||
}
|
||||
for i := range a.KeyRoot {
|
||||
a.KeyRoot[i] = byte(i + 1)
|
||||
}
|
||||
for i := range a.InputCiphertextRoot {
|
||||
a.InputCiphertextRoot[i] = byte(i + 2)
|
||||
}
|
||||
for i := range a.OutputCiphertextRoot {
|
||||
a.OutputCiphertextRoot[i] = byte(i + 3)
|
||||
}
|
||||
for i := range a.CircuitRoot {
|
||||
a.CircuitRoot[i] = byte(i + 4)
|
||||
}
|
||||
for i := range a.ThresholdTranscriptRoot {
|
||||
a.ThresholdTranscriptRoot[i] = byte(i + 5)
|
||||
}
|
||||
for i := range a.AttestationRoot {
|
||||
a.AttestationRoot[i] = byte(i + 6)
|
||||
}
|
||||
a.OpCount = 1024
|
||||
a.FailedCount = 0
|
||||
return a
|
||||
}
|
||||
|
||||
func TestFHEPrecompileArtifact_DigestDeterministic(t *testing.T) {
|
||||
a1 := makeArtifact()
|
||||
a2 := makeArtifact()
|
||||
d1 := a1.Digest()
|
||||
d2 := a2.Digest()
|
||||
if d1 != d2 {
|
||||
t.Errorf("two equal artifacts produced different digests:\n d1=%x\n d2=%x", d1, d2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFHEPrecompileArtifact_DigestSensitiveToFields(t *testing.T) {
|
||||
base := makeArtifact()
|
||||
baseDigest := base.Digest()
|
||||
mutate := func(name string, mut func(*FHEPrecompileArtifact)) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
a := makeArtifact()
|
||||
mut(&a)
|
||||
if got := a.Digest(); got == baseDigest {
|
||||
t.Errorf("mutating %s did not change digest", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
mutate("ParamsHash", func(a *FHEPrecompileArtifact) { a.ParamsHash[0] ^= 0xFF })
|
||||
mutate("KeyRoot", func(a *FHEPrecompileArtifact) { a.KeyRoot[0] ^= 0xFF })
|
||||
mutate("InputCiphertextRoot", func(a *FHEPrecompileArtifact) { a.InputCiphertextRoot[0] ^= 0xFF })
|
||||
mutate("OutputCiphertextRoot", func(a *FHEPrecompileArtifact) { a.OutputCiphertextRoot[0] ^= 0xFF })
|
||||
mutate("CircuitRoot", func(a *FHEPrecompileArtifact) { a.CircuitRoot[0] ^= 0xFF })
|
||||
mutate("ThresholdTranscriptRoot", func(a *FHEPrecompileArtifact) { a.ThresholdTranscriptRoot[0] ^= 0xFF })
|
||||
mutate("AttestationRoot", func(a *FHEPrecompileArtifact) { a.AttestationRoot[0] ^= 0xFF })
|
||||
mutate("OpCount", func(a *FHEPrecompileArtifact) { a.OpCount++ })
|
||||
mutate("FailedCount", func(a *FHEPrecompileArtifact) { a.FailedCount++ })
|
||||
}
|
||||
|
||||
func TestFHEPrecompileArtifact_IsThresholdAndIsAttested(t *testing.T) {
|
||||
var a FHEPrecompileArtifact
|
||||
if a.IsThreshold() {
|
||||
t.Errorf("zero artifact should not be threshold")
|
||||
}
|
||||
if a.IsAttested() {
|
||||
t.Errorf("zero artifact should not be attested")
|
||||
}
|
||||
a.ThresholdTranscriptRoot[0] = 1
|
||||
if !a.IsThreshold() {
|
||||
t.Errorf("non-zero ThresholdTranscriptRoot should be threshold")
|
||||
}
|
||||
a.AttestationRoot[31] = 1
|
||||
if !a.IsAttested() {
|
||||
t.Errorf("non-zero AttestationRoot should be attested")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFHEPrecompileArtifact_CanonicalEncoding(t *testing.T) {
|
||||
a := makeArtifact()
|
||||
enc := a.canonicalEncoding()
|
||||
if len(enc) != 232 {
|
||||
t.Errorf("canonical encoding len = %d, want 232", len(enc))
|
||||
}
|
||||
// determinism
|
||||
enc2 := a.canonicalEncoding()
|
||||
if !bytes.Equal(enc, enc2) {
|
||||
t.Errorf("canonical encoding not deterministic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFHEPrecompileArtifact_Layout(t *testing.T) {
|
||||
const wantSize = 232
|
||||
if got := unsafe.Sizeof(FHEPrecompileArtifact{}); got != wantSize {
|
||||
t.Fatalf("sizeof(FHEPrecompileArtifact) = %d, want %d", got, wantSize)
|
||||
}
|
||||
var a FHEPrecompileArtifact
|
||||
base := uintptr(unsafe.Pointer(&a))
|
||||
cases := []struct {
|
||||
name string
|
||||
off uintptr
|
||||
want uintptr
|
||||
}{
|
||||
{"ParamsHash", uintptr(unsafe.Pointer(&a.ParamsHash)) - base, 0},
|
||||
{"KeyRoot", uintptr(unsafe.Pointer(&a.KeyRoot)) - base, 32},
|
||||
{"InputCiphertextRoot", uintptr(unsafe.Pointer(&a.InputCiphertextRoot)) - base, 64},
|
||||
{"OutputCiphertextRoot", uintptr(unsafe.Pointer(&a.OutputCiphertextRoot)) - base, 96},
|
||||
{"CircuitRoot", uintptr(unsafe.Pointer(&a.CircuitRoot)) - base, 128},
|
||||
{"ThresholdTranscriptRoot", uintptr(unsafe.Pointer(&a.ThresholdTranscriptRoot)) - base, 160},
|
||||
{"AttestationRoot", uintptr(unsafe.Pointer(&a.AttestationRoot)) - base, 192},
|
||||
{"OpCount", uintptr(unsafe.Pointer(&a.OpCount)) - base, 224},
|
||||
{"FailedCount", uintptr(unsafe.Pointer(&a.FailedCount)) - base, 228},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.off != c.want {
|
||||
t.Errorf("offset of %s = %d, want %d", c.name, c.off, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
|
||||
lattice "github.com/luxfi/lattice/v7/types"
|
||||
)
|
||||
|
||||
// FHECiphertextHeader wraps every FHE buffer with the metadata required for
|
||||
// safe dispatch.
|
||||
//
|
||||
// Without this header, two ciphertexts produced under different parameter
|
||||
// sets, evaluation keys, or domains can be combined silently and produce
|
||||
// either undecryptable ciphertexts or an outright privacy break (key
|
||||
// reuse across circuits).
|
||||
//
|
||||
// Layout is byte-stable across Go and C++. Field order MUST NOT change
|
||||
// without updating the C++ mirror and matching layout tests.
|
||||
//
|
||||
// Total size: 144 bytes.
|
||||
type FHECiphertextHeader struct {
|
||||
// ParamsHash is the SHA-256 of the canonical encoding of the full
|
||||
// parameter set (q, p, N, log_p, sigma, dnum, etc.).
|
||||
// 32 bytes, offset 0.
|
||||
ParamsHash [32]byte
|
||||
|
||||
// KeyID is the SHA-256 of the public/evaluation-key material that
|
||||
// produced or will operate on this ciphertext.
|
||||
// 32 bytes, offset 32.
|
||||
KeyID [32]byte
|
||||
|
||||
// CircuitID is the SHA-256 of the circuit / policy program under
|
||||
// which this ciphertext was produced. Used to bind ciphertexts to
|
||||
// their authorised computation set.
|
||||
// 32 bytes, offset 64.
|
||||
CircuitID [32]byte
|
||||
|
||||
// Scheme identifies the FHE scheme.
|
||||
// 4 bytes, offset 96.
|
||||
Scheme FHEScheme
|
||||
|
||||
// Level is the modulus-switching level (multiplicative depth budget).
|
||||
// 4 bytes, offset 100.
|
||||
Level uint32
|
||||
|
||||
// N is the polynomial ring degree.
|
||||
// 4 bytes, offset 104.
|
||||
N uint32
|
||||
|
||||
// ModulusCount is the number of RNS moduli currently active.
|
||||
// 4 bytes, offset 108.
|
||||
ModulusCount uint32
|
||||
|
||||
// Domain is the polynomial domain the ciphertext data lives in.
|
||||
// 1 byte, offset 112.
|
||||
Domain lattice.PolyDomain
|
||||
|
||||
// _pad pads to the next 8-byte boundary so the trailing 32-byte
|
||||
// fields stay aligned at offsets that are multiples of 8.
|
||||
// 7 bytes, offset 113.
|
||||
_pad [7]uint8
|
||||
|
||||
// Reserved provides 24 bytes for forward-compatible extension fields
|
||||
// (e.g. attestation hashes, transcript pointers). Initialised to zero
|
||||
// and ignored by Digest() unless promoted to a named field.
|
||||
// 24 bytes, offset 120.
|
||||
Reserved [24]byte
|
||||
}
|
||||
|
||||
// canonicalEncoding returns the deterministic byte representation used for
|
||||
// hashing. Field order matches struct layout; integers use little-endian.
|
||||
func (h *FHECiphertextHeader) canonicalEncoding() []byte {
|
||||
buf := make([]byte, 144)
|
||||
copy(buf[0:32], h.ParamsHash[:])
|
||||
copy(buf[32:64], h.KeyID[:])
|
||||
copy(buf[64:96], h.CircuitID[:])
|
||||
binary.LittleEndian.PutUint32(buf[96:100], uint32(h.Scheme))
|
||||
binary.LittleEndian.PutUint32(buf[100:104], h.Level)
|
||||
binary.LittleEndian.PutUint32(buf[104:108], h.N)
|
||||
binary.LittleEndian.PutUint32(buf[108:112], h.ModulusCount)
|
||||
buf[112] = uint8(h.Domain)
|
||||
// bytes 113..119 are pad, fixed zero
|
||||
copy(buf[120:144], h.Reserved[:])
|
||||
return buf
|
||||
}
|
||||
|
||||
// Digest returns the SHA-256 of the canonical encoding of the header.
|
||||
//
|
||||
// Determinism: same field values always yield the same digest, across
|
||||
// processes and runs. This is the value bound into precompile artifacts and
|
||||
// threshold transcripts.
|
||||
func (h *FHECiphertextHeader) Digest() [32]byte {
|
||||
return sha256.Sum256(h.canonicalEncoding())
|
||||
}
|
||||
|
||||
// MatchesContext reports whether this ciphertext header is compatible with
|
||||
// the given NTTContext, i.e. the polynomial degree and domain agree.
|
||||
//
|
||||
// Use this at the kernel boundary BEFORE dispatching a forward/inverse NTT
|
||||
// or a pointwise multiply: if the ciphertext is in PolyDomainNTTMontgomery
|
||||
// but the kernel's OutputDomain is PolyDomainNTTStandard, the dispatch is
|
||||
// silently corrupting and must be rejected.
|
||||
func (h *FHECiphertextHeader) MatchesContext(ctx *lattice.NTTContext) bool {
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
if h.N != ctx.N {
|
||||
return false
|
||||
}
|
||||
// The header's Domain describes the *current* domain of the buffer.
|
||||
// A kernel with InputDomain == h.Domain accepts this ciphertext.
|
||||
return h.Domain == ctx.InputDomain
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
lattice "github.com/luxfi/lattice/v7/types"
|
||||
)
|
||||
|
||||
func TestFHEScheme_StableValues(t *testing.T) {
|
||||
cases := []struct {
|
||||
s FHEScheme
|
||||
want uint32
|
||||
}{
|
||||
{FHESchemeTFHE, 0},
|
||||
{FHESchemeFHEW, 1},
|
||||
{FHESchemeCKKS, 2},
|
||||
{FHESchemeBFV, 3},
|
||||
{FHESchemeBGV, 4},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if uint32(c.s) != c.want {
|
||||
t.Errorf("FHEScheme(%s) = %d, want %d", c.s, c.s, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFHEScheme_StableStrings(t *testing.T) {
|
||||
cases := []struct {
|
||||
s FHEScheme
|
||||
want string
|
||||
}{
|
||||
{FHESchemeTFHE, "TFHE"},
|
||||
{FHESchemeFHEW, "FHEW"},
|
||||
{FHESchemeCKKS, "CKKS"},
|
||||
{FHESchemeBFV, "BFV"},
|
||||
{FHESchemeBGV, "BGV"},
|
||||
{FHEScheme(0xFF), "Unknown"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := c.s.String(); got != c.want {
|
||||
t.Errorf("FHEScheme(%d).String() = %q, want %q", c.s, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeHeader returns a deterministic test header.
|
||||
func makeHeader() FHECiphertextHeader {
|
||||
var h FHECiphertextHeader
|
||||
for i := range h.ParamsHash {
|
||||
h.ParamsHash[i] = byte(i)
|
||||
}
|
||||
for i := range h.KeyID {
|
||||
h.KeyID[i] = byte(i + 0x40)
|
||||
}
|
||||
for i := range h.CircuitID {
|
||||
h.CircuitID[i] = byte(i + 0x80)
|
||||
}
|
||||
h.Scheme = FHESchemeCKKS
|
||||
h.Level = 7
|
||||
h.N = 1 << 14
|
||||
h.ModulusCount = 8
|
||||
h.Domain = lattice.PolyDomainNTTMontgomery
|
||||
return h
|
||||
}
|
||||
|
||||
// TestFHECiphertextHeader_DigestDeterministic asserts that the same field
|
||||
// values always produce the same digest. Determinism is the basis for
|
||||
// using the digest as a binding tag in precompile artifacts.
|
||||
func TestFHECiphertextHeader_DigestDeterministic(t *testing.T) {
|
||||
h1 := makeHeader()
|
||||
h2 := makeHeader()
|
||||
d1 := h1.Digest()
|
||||
d2 := h2.Digest()
|
||||
if d1 != d2 {
|
||||
t.Errorf("two equal headers produced different digests:\n d1=%x\n d2=%x", d1, d2)
|
||||
}
|
||||
// Stability: re-digesting the same struct must give the same result.
|
||||
for i := 0; i < 4; i++ {
|
||||
if got := h1.Digest(); got != d1 {
|
||||
t.Errorf("re-digest %d differed: %x vs %x", i, got, d1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFHECiphertextHeader_DigestSensitiveToFields asserts that changing
|
||||
// any single field changes the digest.
|
||||
func TestFHECiphertextHeader_DigestSensitiveToFields(t *testing.T) {
|
||||
base := makeHeader()
|
||||
baseDigest := base.Digest()
|
||||
|
||||
mutate := func(name string, mut func(*FHECiphertextHeader)) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
h := makeHeader()
|
||||
mut(&h)
|
||||
if got := h.Digest(); got == baseDigest {
|
||||
t.Errorf("mutating %s did not change digest", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
mutate("ParamsHash", func(h *FHECiphertextHeader) { h.ParamsHash[0] ^= 0xFF })
|
||||
mutate("KeyID", func(h *FHECiphertextHeader) { h.KeyID[31] ^= 0xFF })
|
||||
mutate("CircuitID", func(h *FHECiphertextHeader) { h.CircuitID[15] ^= 0xFF })
|
||||
mutate("Scheme", func(h *FHECiphertextHeader) { h.Scheme = FHESchemeBGV })
|
||||
mutate("Level", func(h *FHECiphertextHeader) { h.Level++ })
|
||||
mutate("N", func(h *FHECiphertextHeader) { h.N <<= 1 })
|
||||
mutate("ModulusCount", func(h *FHECiphertextHeader) { h.ModulusCount++ })
|
||||
mutate("Domain", func(h *FHECiphertextHeader) { h.Domain = lattice.PolyDomainStandard })
|
||||
mutate("Reserved", func(h *FHECiphertextHeader) { h.Reserved[0] = 1 })
|
||||
}
|
||||
|
||||
func TestFHECiphertextHeader_MatchesContext(t *testing.T) {
|
||||
h := makeHeader() // N=16384, Domain=NTTMontgomery
|
||||
good := &lattice.NTTContext{
|
||||
Modulus: 0xFFFFFFFFFFFFFFC5, N: 1 << 14,
|
||||
InputDomain: lattice.PolyDomainNTTMontgomery, RootDomain: lattice.PolyDomainNTTMontgomery,
|
||||
OutputDomain: lattice.PolyDomainMontgomery,
|
||||
}
|
||||
if !h.MatchesContext(good) {
|
||||
t.Errorf("matching N+Domain should match")
|
||||
}
|
||||
|
||||
wrongN := &lattice.NTTContext{
|
||||
Modulus: 0xFFFFFFFFFFFFFFC5, N: 1 << 12,
|
||||
InputDomain: lattice.PolyDomainNTTMontgomery, RootDomain: lattice.PolyDomainNTTMontgomery,
|
||||
OutputDomain: lattice.PolyDomainMontgomery,
|
||||
}
|
||||
if h.MatchesContext(wrongN) {
|
||||
t.Errorf("different N should not match")
|
||||
}
|
||||
|
||||
wrongDomain := &lattice.NTTContext{
|
||||
Modulus: 0xFFFFFFFFFFFFFFC5, N: 1 << 14,
|
||||
InputDomain: lattice.PolyDomainNTTStandard, RootDomain: lattice.PolyDomainNTTStandard,
|
||||
OutputDomain: lattice.PolyDomainStandard,
|
||||
}
|
||||
if h.MatchesContext(wrongDomain) {
|
||||
t.Errorf("different Domain should not match")
|
||||
}
|
||||
|
||||
if h.MatchesContext(nil) {
|
||||
t.Errorf("nil ctx should not match")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFHECiphertextHeader_CanonicalEncodingDeterministic verifies the byte
|
||||
// representation used by Digest() is identical for identical inputs and
|
||||
// has the expected length.
|
||||
func TestFHECiphertextHeader_CanonicalEncodingDeterministic(t *testing.T) {
|
||||
h1 := makeHeader()
|
||||
h2 := makeHeader()
|
||||
enc1 := h1.canonicalEncoding()
|
||||
enc2 := h2.canonicalEncoding()
|
||||
if !bytes.Equal(enc1, enc2) {
|
||||
t.Fatalf("canonical encoding not deterministic\n e1=%x\n e2=%x", enc1, enc2)
|
||||
}
|
||||
if len(enc1) != 144 {
|
||||
t.Errorf("canonical encoding len = %d, want 144", len(enc1))
|
||||
}
|
||||
}
|
||||
|
||||
// TestFHECiphertextHeader_Layout pins the byte layout for cgo compatibility.
|
||||
func TestFHECiphertextHeader_Layout(t *testing.T) {
|
||||
const wantSize = 144
|
||||
if got := unsafe.Sizeof(FHECiphertextHeader{}); got != wantSize {
|
||||
t.Fatalf("sizeof(FHECiphertextHeader) = %d, want %d", got, wantSize)
|
||||
}
|
||||
var h FHECiphertextHeader
|
||||
base := uintptr(unsafe.Pointer(&h))
|
||||
cases := []struct {
|
||||
name string
|
||||
off uintptr
|
||||
want uintptr
|
||||
}{
|
||||
{"ParamsHash", uintptr(unsafe.Pointer(&h.ParamsHash)) - base, 0},
|
||||
{"KeyID", uintptr(unsafe.Pointer(&h.KeyID)) - base, 32},
|
||||
{"CircuitID", uintptr(unsafe.Pointer(&h.CircuitID)) - base, 64},
|
||||
{"Scheme", uintptr(unsafe.Pointer(&h.Scheme)) - base, 96},
|
||||
{"Level", uintptr(unsafe.Pointer(&h.Level)) - base, 100},
|
||||
{"N", uintptr(unsafe.Pointer(&h.N)) - base, 104},
|
||||
{"ModulusCount", uintptr(unsafe.Pointer(&h.ModulusCount)) - base, 108},
|
||||
{"Domain", uintptr(unsafe.Pointer(&h.Domain)) - base, 112},
|
||||
{"Reserved", uintptr(unsafe.Pointer(&h.Reserved)) - base, 120},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if c.off != c.want {
|
||||
t.Errorf("offset of %s = %d, want %d", c.name, c.off, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
lattice "github.com/luxfi/lattice/v7/types"
|
||||
)
|
||||
|
||||
// TestFHECiphertextHeader_CrossLangByteImage asserts byte-identical layout
|
||||
// against the C++ mirror at luxcpp/fhe/test/types/types_test.cpp
|
||||
// (test_ciphertext_header_memcmp).
|
||||
func TestFHECiphertextHeader_CrossLangByteImage(t *testing.T) {
|
||||
var h FHECiphertextHeader
|
||||
for i := range h.ParamsHash {
|
||||
h.ParamsHash[i] = byte(i)
|
||||
}
|
||||
for i := range h.KeyID {
|
||||
h.KeyID[i] = byte(i + 0x40)
|
||||
}
|
||||
for i := range h.CircuitID {
|
||||
h.CircuitID[i] = byte(i + 0x80)
|
||||
}
|
||||
h.Scheme = FHESchemeCKKS
|
||||
h.Level = 7
|
||||
h.N = 16384
|
||||
h.ModulusCount = 8
|
||||
h.Domain = lattice.PolyDomainNTTMontgomery
|
||||
|
||||
want := make([]byte, 144)
|
||||
for i := 0; i < 32; i++ {
|
||||
want[i] = byte(i)
|
||||
want[32+i] = byte(i + 0x40)
|
||||
want[64+i] = byte(i + 0x80)
|
||||
}
|
||||
// scheme = CKKS (2)
|
||||
want[96] = 0x02
|
||||
// level = 7
|
||||
want[100] = 0x07
|
||||
// N = 16384 = 0x4000 little-endian
|
||||
want[104] = 0x00
|
||||
want[105] = 0x40
|
||||
// modulus_count = 8
|
||||
want[108] = 0x08
|
||||
// domain = NTTMontgomery (3)
|
||||
want[112] = 0x03
|
||||
// 113..119 pad zero, 120..143 reserved zero (already)
|
||||
|
||||
got := unsafe.Slice((*byte)(unsafe.Pointer(&h)), unsafe.Sizeof(h))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("FHECiphertextHeader byte image mismatch with C++ side\n got: %x\n want: %x", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFHEPrecompileArtifact_CrossLangByteImage asserts byte-identical layout
|
||||
// against the C++ mirror at luxcpp/fhe/test/types/types_test.cpp
|
||||
// (test_artifact_memcmp).
|
||||
func TestFHEPrecompileArtifact_CrossLangByteImage(t *testing.T) {
|
||||
var a FHEPrecompileArtifact
|
||||
for i := range a.ParamsHash {
|
||||
a.ParamsHash[i] = byte(i)
|
||||
}
|
||||
for i := range a.KeyRoot {
|
||||
a.KeyRoot[i] = byte(i + 1)
|
||||
}
|
||||
for i := range a.InputCiphertextRoot {
|
||||
a.InputCiphertextRoot[i] = byte(i + 2)
|
||||
}
|
||||
for i := range a.OutputCiphertextRoot {
|
||||
a.OutputCiphertextRoot[i] = byte(i + 3)
|
||||
}
|
||||
for i := range a.CircuitRoot {
|
||||
a.CircuitRoot[i] = byte(i + 4)
|
||||
}
|
||||
for i := range a.ThresholdTranscriptRoot {
|
||||
a.ThresholdTranscriptRoot[i] = byte(i + 5)
|
||||
}
|
||||
for i := range a.AttestationRoot {
|
||||
a.AttestationRoot[i] = byte(i + 6)
|
||||
}
|
||||
a.OpCount = 1024
|
||||
a.FailedCount = 0
|
||||
|
||||
want := make([]byte, 232)
|
||||
for i := 0; i < 32; i++ {
|
||||
want[i] = byte(i)
|
||||
want[32+i] = byte(i + 1)
|
||||
want[64+i] = byte(i + 2)
|
||||
want[96+i] = byte(i + 3)
|
||||
want[128+i] = byte(i + 4)
|
||||
want[160+i] = byte(i + 5)
|
||||
want[192+i] = byte(i + 6)
|
||||
}
|
||||
// op_count = 1024 = 0x00000400 LE
|
||||
want[224] = 0x00
|
||||
want[225] = 0x04
|
||||
|
||||
got := unsafe.Slice((*byte)(unsafe.Pointer(&a)), unsafe.Sizeof(a))
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatalf("FHEPrecompileArtifact byte image mismatch with C++ side\n got: %x\n want: %x", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Package types defines the FHE-GPU ciphertext header and precompile artifact
|
||||
// types. These wrap every FHE buffer with the minimal metadata required to
|
||||
// reject domain mismatches, parameter mismatches, and key/circuit drift at
|
||||
// the kernel boundary.
|
||||
//
|
||||
// Reference: LP-137-FHE-TYPING.md.
|
||||
//
|
||||
// Copyright (c) 2026, Lux Industries Inc.
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package types
|
||||
|
||||
// FHEScheme identifies which FHE scheme produced or consumes a ciphertext.
|
||||
//
|
||||
// Wire stable: enum values are fixed and MUST NOT be reordered. Add new
|
||||
// schemes by appending.
|
||||
type FHEScheme uint32
|
||||
|
||||
const (
|
||||
// FHESchemeTFHE is the TFHE/FHEW boolean-circuit family with blind
|
||||
// rotation bootstrap, as implemented by luxfi/fhe over luxfi/lattice.
|
||||
FHESchemeTFHE FHEScheme = 0
|
||||
|
||||
// FHESchemeFHEW is the FHEW variant (kept distinct from TFHE for
|
||||
// parameter-set hashing even though the kernel path can be shared).
|
||||
FHESchemeFHEW FHEScheme = 1
|
||||
|
||||
// FHESchemeCKKS is approximate-arithmetic CKKS (real/complex).
|
||||
FHESchemeCKKS FHEScheme = 2
|
||||
|
||||
// FHESchemeBFV is exact-arithmetic BFV (integer).
|
||||
FHESchemeBFV FHEScheme = 3
|
||||
|
||||
// FHESchemeBGV is exact-arithmetic BGV (integer).
|
||||
FHESchemeBGV FHEScheme = 4
|
||||
)
|
||||
|
||||
// String returns a stable, human-readable name for the scheme.
|
||||
func (s FHEScheme) String() string {
|
||||
switch s {
|
||||
case FHESchemeTFHE:
|
||||
return "TFHE"
|
||||
case FHESchemeFHEW:
|
||||
return "FHEW"
|
||||
case FHESchemeCKKS:
|
||||
return "CKKS"
|
||||
case FHESchemeBFV:
|
||||
return "BFV"
|
||||
case FHESchemeBGV:
|
||||
return "BGV"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user