Files
fhe-coprocessor/pkg/fhe/adapter_test.go
zooqueen bfb13c2488 pkg/fhe: rewrite adapter onto current luxfi/fhe API; fix build
The adapter targeted a removed tfhe-rs-style API (fhe.ServerKey,
DeserializeServerKey, DeserializeCiphertext, ct.Serialize,
DeserializeClientKey) and pinned a stale fhe v1.7.5, so the package did
not compile. The canonical API is radix-integer based:
IntegerEvaluator over *RadixCiphertext, built from IntegerParams +
BootstrapKey, with Marshal/UnmarshalBinary serialization.

- Rewrite the adapter: serverKey bytes -> *fhe.BootstrapKey (the public
  evaluation key); Ciphertext.Data -> marshaled *fhe.RadixCiphertext;
  ops -> IntegerEvaluator methods; Encrypt/Decrypt helpers ->
  IntegerEncryptor/Decryptor over a marshaled *fhe.SecretKey.
- Fix PN10QP27 / 2-bit-block params (repo default) in one place.
- Shl/Shr/Rotl/Rotr take a plaintext shift in the real API (rotates do
  not exist); reject them honestly instead of fabricating a shift from a
  ciphertext rhs.
- Bump fhe v1.7.5 -> v1.8.9; go mod tidy. v1.8.9 (proposed) adds the
  RadixCiphertext codec the adapter needs; a temporary replace points at
  the local checkout until that tag is published (see go.mod TODO).

Adds the repo's first tests: Encrypt/Decrypt round-trip, Execute
end-to-end plumbing, and unsupported-shift rejection.

GOWORK=off CGO_ENABLED=1 go build ./...  -> exit 0
go test ./pkg/fhe/...                     -> PASS
2026-06-21 22:34:50 -07:00

131 lines
3.6 KiB
Go

package fhe
import (
"context"
"testing"
canon "github.com/luxfi/fhe"
)
// keyMaterial generates a fresh secret key (client key) and the marshaled
// bootstrap key (server/eval key) for the fixed coprocessor parameters.
func keyMaterial(t *testing.T) (clientKey, serverKey []byte) {
t.Helper()
params, err := canon.NewParametersFromLiteral(canon.PN10QP27)
if err != nil {
t.Fatalf("params: %v", err)
}
kg := canon.NewKeyGenerator(params)
sk := kg.GenSecretKey()
bsk := kg.GenBootstrapKey(sk)
clientKey, err = sk.MarshalBinary()
if err != nil {
t.Fatalf("marshal secret key: %v", err)
}
serverKey, err = bsk.MarshalBinary()
if err != nil {
t.Fatalf("marshal bootstrap key: %v", err)
}
return clientKey, serverKey
}
// TestEncryptDecryptRoundTrip proves the adapter's Encrypt/Decrypt helpers wrap
// the canonical radix-integer codec losslessly across several widths.
func TestEncryptDecryptRoundTrip(t *testing.T) {
clientKey, _ := keyMaterial(t)
cases := []struct {
name string
value uint64
bitLen uint8
}{
{"u8", 0x5A, 8},
{"u16", 0x1234, 16},
{"u32", 0x00ABCDEF, 32},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ct, err := Encrypt(clientKey, tc.value, tc.bitLen)
if err != nil {
t.Fatalf("encrypt: %v", err)
}
if len(ct.Data) == 0 {
t.Fatal("encrypt produced empty ciphertext")
}
if ct.BitLen != tc.bitLen {
t.Fatalf("bitlen: got %d want %d", ct.BitLen, tc.bitLen)
}
got, err := Decrypt(clientKey, ct)
if err != nil {
t.Fatalf("decrypt: %v", err)
}
mask := uint64(1)<<uint(tc.bitLen) - 1
if got&mask != tc.value&mask {
t.Fatalf("round trip: got %#x want %#x", got&mask, tc.value&mask)
}
})
}
}
// TestExecutePlumbing proves a binary operation flows end to end: load operands
// as adapter ciphertexts, run Execute, and decode the result into a valid,
// decryptable radix ciphertext. It asserts the wiring and the result's
// structural validity. It does NOT assert the homomorphic numeric result: the
// canonical IntegerEvaluator does not return correct values at the PN10QP27
// parameter set (verified separately), so a value assertion here would be
// dishonest. Op-level numeric correctness is gated on the canonical evaluator
// and a sufficient parameter set, tracked upstream in luxfi/fhe.
func TestExecutePlumbing(t *testing.T) {
clientKey, serverKey := keyMaterial(t)
a, err := Encrypt(clientKey, 5, 8)
if err != nil {
t.Fatalf("encrypt a: %v", err)
}
b, err := Encrypt(clientKey, 3, 8)
if err != nil {
t.Fatalf("encrypt b: %v", err)
}
adapter, err := NewAdapter(serverKey)
if err != nil {
t.Fatalf("new adapter: %v", err)
}
res, err := adapter.Execute(context.Background(), OpAdd, a, b)
if err != nil {
t.Fatalf("execute: %v", err)
}
if len(res.Data) == 0 {
t.Fatal("execute produced empty result")
}
// The result must decode back to a valid radix ciphertext.
if _, err := Decrypt(clientKey, res); err != nil {
t.Fatalf("result is not a decryptable ciphertext: %v", err)
}
}
// TestExecuteUnsupportedShift proves shifts/rotates are rejected honestly rather
// than fabricated from a ciphertext right-hand side.
func TestExecuteUnsupportedShift(t *testing.T) {
clientKey, serverKey := keyMaterial(t)
a, err := Encrypt(clientKey, 5, 8)
if err != nil {
t.Fatalf("encrypt: %v", err)
}
adapter, err := NewAdapter(serverKey)
if err != nil {
t.Fatalf("new adapter: %v", err)
}
for _, op := range []OperationType{OpShl, OpShr, OpRotl, OpRotr} {
if _, err := adapter.Execute(context.Background(), op, a, a); err == nil {
t.Fatalf("op %v: expected ErrUnsupportedOp, got nil", op)
}
}
}