mirror of
https://github.com/luxfi/fhe-coprocessor.git
synced 2026-10-30 16:56:19 +00:00
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
131 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|