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
295 lines
7.8 KiB
Go
295 lines
7.8 KiB
Go
// Package fhe provides a coprocessor-facing adapter over the canonical
|
|
// github.com/luxfi/fhe homomorphic-encryption library.
|
|
//
|
|
// The canonical library exposes a radix-integer API: encryption produces a
|
|
// *fhe.RadixCiphertext, and homomorphic operations are methods on a
|
|
// *fhe.IntegerEvaluator constructed from public parameters plus a
|
|
// *fhe.BootstrapKey (the public evaluation key — the FHE analogue of a
|
|
// "server key"). This adapter wraps that API behind the byte-oriented contract
|
|
// the worker relies on: ciphertexts travel as length-prefixed bytes and
|
|
// operations are selected by OperationType.
|
|
//
|
|
// Parameter set: PN10QP27 with 2-bit radix blocks. This is the repository-wide
|
|
// default (every cmd/* binary and the precompile use it) and must match the
|
|
// parameters used to generate the BootstrapKey passed to NewAdapter; ciphertext
|
|
// bytes produced under different parameters will not evaluate correctly.
|
|
package fhe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
fhe "github.com/luxfi/fhe"
|
|
)
|
|
|
|
// Common errors.
|
|
var (
|
|
ErrInvalidCiphertext = errors.New("invalid ciphertext")
|
|
ErrOperationFailed = errors.New("FHE operation failed")
|
|
ErrKeyNotFound = errors.New("key not found")
|
|
ErrUnsupportedOp = errors.New("unsupported operation")
|
|
)
|
|
|
|
// radixBlockBits is the radix block width used across the coprocessor. It must
|
|
// match the parameters used to generate the bootstrap and secret keys.
|
|
const radixBlockBits = 2
|
|
|
|
// OperationType defines supported FHE operations.
|
|
type OperationType uint8
|
|
|
|
const (
|
|
OpAdd OperationType = iota
|
|
OpSub
|
|
OpMul
|
|
OpNot
|
|
OpAnd
|
|
OpOr
|
|
OpXor
|
|
OpEq
|
|
OpNe
|
|
OpGt
|
|
OpGe
|
|
OpLt
|
|
OpLe
|
|
OpShl
|
|
OpShr
|
|
OpRotl
|
|
OpRotr
|
|
OpMin
|
|
OpMax
|
|
OpNeg
|
|
)
|
|
|
|
// Adapter wraps canonical FHE integer operations with context support. It holds
|
|
// the integer parameters and a bootstrap-key-backed evaluator; it carries no
|
|
// secret material.
|
|
type Adapter struct {
|
|
params *fhe.IntegerParams
|
|
eval *fhe.IntegerEvaluator
|
|
}
|
|
|
|
// NewAdapter creates a new adapter from the serialized public evaluation key
|
|
// (a marshaled *fhe.BootstrapKey).
|
|
func NewAdapter(serverKeyBytes []byte) (*Adapter, error) {
|
|
params, err := integerParams()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bsk := new(fhe.BootstrapKey)
|
|
if err := bsk.UnmarshalBinary(serverKeyBytes); err != nil {
|
|
return nil, fmt.Errorf("deserialize bootstrap key: %w", err)
|
|
}
|
|
|
|
return &Adapter{
|
|
params: params,
|
|
eval: fhe.NewIntegerEvaluator(params, bsk),
|
|
}, nil
|
|
}
|
|
|
|
// Ciphertext represents an encrypted integer value. Data is a marshaled
|
|
// *fhe.RadixCiphertext; BitLen records the integer width (used to choose the
|
|
// FHE type when (re)constructing the radix ciphertext).
|
|
type Ciphertext struct {
|
|
Data []byte
|
|
BitLen uint8
|
|
}
|
|
|
|
// Execute performs an FHE operation on one or two ciphertexts.
|
|
func (a *Adapter) Execute(ctx context.Context, op OperationType, lhs, rhs *Ciphertext) (*Ciphertext, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if lhs == nil {
|
|
return nil, ErrInvalidCiphertext
|
|
}
|
|
|
|
lhsCt, err := decodeRadix(lhs.Data)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("decode lhs: %w", err)
|
|
}
|
|
|
|
var result *fhe.RadixCiphertext
|
|
|
|
switch op {
|
|
case OpNot, OpNeg:
|
|
result, err = a.executeUnary(op, lhsCt)
|
|
case OpShl, OpShr, OpRotl, OpRotr:
|
|
// The canonical API models shifts/rotates as a ciphertext plus a
|
|
// plaintext shift amount (Shl/Shr(a, shift int)); rotates are not
|
|
// implemented at all. None can be honestly expressed against an
|
|
// opaque ciphertext right-hand side, so reject rather than fabricate
|
|
// a shift derived from a second ciphertext.
|
|
return nil, fmt.Errorf("%w: %v requires a plaintext shift amount", ErrUnsupportedOp, op)
|
|
default:
|
|
if rhs == nil {
|
|
return nil, ErrInvalidCiphertext
|
|
}
|
|
rhsCt, decErr := decodeRadix(rhs.Data)
|
|
if decErr != nil {
|
|
return nil, fmt.Errorf("decode rhs: %w", decErr)
|
|
}
|
|
result, err = a.executeBinary(op, lhsCt, rhsCt)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrOperationFailed, err)
|
|
}
|
|
|
|
data, err := result.MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal result: %w", err)
|
|
}
|
|
|
|
return &Ciphertext{Data: data, BitLen: lhs.BitLen}, nil
|
|
}
|
|
|
|
func (a *Adapter) executeUnary(op OperationType, ct *fhe.RadixCiphertext) (*fhe.RadixCiphertext, error) {
|
|
switch op {
|
|
case OpNot:
|
|
return a.eval.Not(ct)
|
|
case OpNeg:
|
|
return a.eval.Neg(ct)
|
|
default:
|
|
return nil, fmt.Errorf("%w: unary %d", ErrUnsupportedOp, op)
|
|
}
|
|
}
|
|
|
|
func (a *Adapter) executeBinary(op OperationType, lhs, rhs *fhe.RadixCiphertext) (*fhe.RadixCiphertext, error) {
|
|
switch op {
|
|
case OpAdd:
|
|
return a.eval.Add(lhs, rhs)
|
|
case OpSub:
|
|
return a.eval.Sub(lhs, rhs)
|
|
case OpMul:
|
|
return a.eval.Mul(lhs, rhs)
|
|
case OpAnd:
|
|
return a.eval.And(lhs, rhs)
|
|
case OpOr:
|
|
return a.eval.Or(lhs, rhs)
|
|
case OpXor:
|
|
return a.eval.Xor(lhs, rhs)
|
|
case OpEq:
|
|
return a.eval.Eq(lhs, rhs)
|
|
case OpNe:
|
|
return a.eval.Ne(lhs, rhs)
|
|
case OpGt:
|
|
return a.eval.Gt(lhs, rhs)
|
|
case OpGe:
|
|
return a.eval.Ge(lhs, rhs)
|
|
case OpLt:
|
|
return a.eval.Lt(lhs, rhs)
|
|
case OpLe:
|
|
return a.eval.Le(lhs, rhs)
|
|
case OpMin:
|
|
return a.eval.Min(lhs, rhs)
|
|
case OpMax:
|
|
return a.eval.Max(lhs, rhs)
|
|
default:
|
|
return nil, fmt.Errorf("%w: binary %d", ErrUnsupportedOp, op)
|
|
}
|
|
}
|
|
|
|
// Decrypt decrypts a ciphertext using the client key (for testing only). The
|
|
// clientKeyBytes argument is a marshaled *fhe.SecretKey.
|
|
func Decrypt(clientKeyBytes []byte, ct *Ciphertext) (uint64, error) {
|
|
params, sk, err := decryptorParts(clientKeyBytes)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
rc, err := decodeRadix(ct.Data)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("decode ciphertext: %w", err)
|
|
}
|
|
|
|
return fhe.NewIntegerDecryptor(params, sk).DecryptUint64(rc), nil
|
|
}
|
|
|
|
// Encrypt encrypts a plaintext value (for testing only). The clientKeyBytes
|
|
// argument is a marshaled *fhe.SecretKey.
|
|
func Encrypt(clientKeyBytes []byte, value uint64, bitLen uint8) (*Ciphertext, error) {
|
|
params, sk, err := decryptorParts(clientKeyBytes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
rc, err := fhe.NewIntegerEncryptor(params, sk).EncryptUint64(value, fheTypeFor(bitLen))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encrypt: %w", err)
|
|
}
|
|
|
|
data, err := rc.MarshalBinary()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal: %w", err)
|
|
}
|
|
|
|
return &Ciphertext{Data: data, BitLen: bitLen}, nil
|
|
}
|
|
|
|
// integerParams builds the fixed PN10QP27 / 2-bit-block integer parameters.
|
|
func integerParams() (*fhe.IntegerParams, error) {
|
|
base, err := fhe.NewParametersFromLiteral(fhe.PN10QP27)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fhe parameters: %w", err)
|
|
}
|
|
params, err := fhe.NewIntegerParams(base, radixBlockBits)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("integer parameters: %w", err)
|
|
}
|
|
return params, nil
|
|
}
|
|
|
|
// decryptorParts builds the integer parameters and secret key used by the
|
|
// test-only Encrypt/Decrypt helpers.
|
|
func decryptorParts(clientKeyBytes []byte) (*fhe.IntegerParams, *fhe.SecretKey, error) {
|
|
params, err := integerParams()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
sk := new(fhe.SecretKey)
|
|
if err := sk.UnmarshalBinary(clientKeyBytes); err != nil {
|
|
return nil, nil, fmt.Errorf("deserialize secret key: %w", err)
|
|
}
|
|
return params, sk, nil
|
|
}
|
|
|
|
// decodeRadix unmarshals a radix ciphertext from its byte form.
|
|
func decodeRadix(data []byte) (*fhe.RadixCiphertext, error) {
|
|
if len(data) == 0 {
|
|
return nil, ErrInvalidCiphertext
|
|
}
|
|
rc := new(fhe.RadixCiphertext)
|
|
if err := rc.UnmarshalBinary(data); err != nil {
|
|
return nil, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
// fheTypeFor maps an integer bit width to the canonical FheUintType, rounding
|
|
// up to the next supported width.
|
|
func fheTypeFor(bitLen uint8) fhe.FheUintType {
|
|
switch {
|
|
case bitLen <= 1:
|
|
return fhe.FheBool
|
|
case bitLen <= 4:
|
|
return fhe.FheUint4
|
|
case bitLen <= 8:
|
|
return fhe.FheUint8
|
|
case bitLen <= 16:
|
|
return fhe.FheUint16
|
|
case bitLen <= 32:
|
|
return fhe.FheUint32
|
|
case bitLen <= 64:
|
|
return fhe.FheUint64
|
|
case bitLen <= 128:
|
|
return fhe.FheUint128
|
|
default:
|
|
return fhe.FheUint256
|
|
}
|
|
}
|