From d5d2f4083d4180dd5b5c868e29d7a307d640316d Mon Sep 17 00:00:00 2001 From: Hanzo AI Date: Mon, 27 Apr 2026 12:47:39 -0700 Subject: [PATCH] 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. --- types/artifact.go | 101 +++++++++++++++++ types/artifact_test.go | 131 +++++++++++++++++++++ types/ciphertext_header.go | 117 +++++++++++++++++++ types/ciphertext_header_test.go | 194 ++++++++++++++++++++++++++++++++ types/cross_lang_test.go | 106 +++++++++++++++++ types/scheme.go | 53 +++++++++ 6 files changed, 702 insertions(+) create mode 100644 types/artifact.go create mode 100644 types/artifact_test.go create mode 100644 types/ciphertext_header.go create mode 100644 types/ciphertext_header_test.go create mode 100644 types/cross_lang_test.go create mode 100644 types/scheme.go diff --git a/types/artifact.go b/types/artifact.go new file mode 100644 index 0000000..3a3719c --- /dev/null +++ b/types/artifact.go @@ -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{} +} diff --git a/types/artifact_test.go b/types/artifact_test.go new file mode 100644 index 0000000..2c0cebf --- /dev/null +++ b/types/artifact_test.go @@ -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) + } + } +} diff --git a/types/ciphertext_header.go b/types/ciphertext_header.go new file mode 100644 index 0000000..b9a27de --- /dev/null +++ b/types/ciphertext_header.go @@ -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 +} diff --git a/types/ciphertext_header_test.go b/types/ciphertext_header_test.go new file mode 100644 index 0000000..77a925e --- /dev/null +++ b/types/ciphertext_header_test.go @@ -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) + } + } +} diff --git a/types/cross_lang_test.go b/types/cross_lang_test.go new file mode 100644 index 0000000..4cdd3e8 --- /dev/null +++ b/types/cross_lang_test.go @@ -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) + } +} diff --git a/types/scheme.go b/types/scheme.go new file mode 100644 index 0000000..3538ed7 --- /dev/null +++ b/types/scheme.go @@ -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" + } +}