2026-01-03 19:11:38 -08:00
|
|
|
// Copyright (c) 2025, Lux Industries Inc
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package fhe
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/luxfi/lattice/v7/core/rgsw/blindrot"
|
|
|
|
|
"github.com/luxfi/lattice/v7/core/rlwe"
|
|
|
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Evaluator evaluates boolean gates on encrypted data
|
|
|
|
|
// SECURITY: This evaluator does NOT require the secret key.
|
|
|
|
|
// It uses sample extraction and key switching for bootstrapping.
|
|
|
|
|
type Evaluator struct {
|
|
|
|
|
params Parameters
|
|
|
|
|
eval *blindrot.Evaluator
|
|
|
|
|
bsk *BootstrapKey
|
|
|
|
|
ringQLWE *ring.Ring
|
|
|
|
|
ringQBR *ring.Ring
|
|
|
|
|
|
|
|
|
|
// Key switching evaluator (BR -> LWE)
|
|
|
|
|
ksEval *rlwe.Evaluator
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewEvaluator creates a new evaluator with bootstrap key.
|
|
|
|
|
// SECURITY: No secret key is required - bootstrapping uses public key switching.
|
|
|
|
|
func NewEvaluator(params Parameters, bsk *BootstrapKey) *Evaluator {
|
|
|
|
|
// Create key switching evaluator using the key switch key in bootstrap key
|
|
|
|
|
var ksEval *rlwe.Evaluator
|
|
|
|
|
if bsk.KSK != nil {
|
|
|
|
|
ksEval = rlwe.NewEvaluator(params.paramsBR, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Evaluator{
|
|
|
|
|
params: params,
|
|
|
|
|
eval: blindrot.NewEvaluator(params.paramsBR, params.paramsLWE),
|
|
|
|
|
bsk: bsk,
|
|
|
|
|
ringQLWE: params.paramsLWE.RingQ(),
|
|
|
|
|
ringQBR: params.paramsBR.RingQ(),
|
|
|
|
|
ksEval: ksEval,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sampleExtractAndModSwitch extracts the result from an RLWE ciphertext
|
|
|
|
|
// after blind rotation and converts it to an LWE ciphertext.
|
|
|
|
|
//
|
|
|
|
|
// When LWE and BR use the same dimension and modulus (recommended configuration),
|
|
|
|
|
// this simply returns the ciphertext directly since no conversion is needed.
|
|
|
|
|
//
|
|
|
|
|
// SECURITY: This operation does NOT require the secret key. The ciphertext
|
|
|
|
|
// remains encrypted throughout.
|
|
|
|
|
func (eval *Evaluator) sampleExtractAndModSwitch(ctBR *rlwe.Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
// When same dimension and modulus, just return the ciphertext directly
|
|
|
|
|
// The key is the same, so no conversion needed
|
|
|
|
|
if eval.params.N() == eval.params.NBR() && eval.params.QLWE() == eval.params.QBR() {
|
|
|
|
|
// Copy to a new ciphertext in LWE parameters (same as BR here)
|
|
|
|
|
result := ctBR.CopyNew()
|
|
|
|
|
return &Ciphertext{result}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Different dimensions/moduli require modulus switching
|
|
|
|
|
levelBR := ctBR.Level()
|
|
|
|
|
ringQBR := eval.ringQBR.AtLevel(levelBR)
|
|
|
|
|
qBR := eval.params.QBR()
|
|
|
|
|
qLWE := eval.params.QLWE()
|
|
|
|
|
|
|
|
|
|
// Ensure we're working in coefficient domain
|
|
|
|
|
c0 := ctBR.Value[0].CopyNew()
|
|
|
|
|
c1 := ctBR.Value[1].CopyNew()
|
|
|
|
|
|
|
|
|
|
if ctBR.IsNTT {
|
|
|
|
|
ringQBR.INTT(*c0, *c0)
|
|
|
|
|
ringQBR.INTT(*c1, *c1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create output ciphertext in LWE parameters
|
|
|
|
|
nLWE := eval.params.N()
|
|
|
|
|
ctLWE := rlwe.NewCiphertext(eval.params.paramsLWE, 1, eval.params.paramsLWE.MaxLevel())
|
|
|
|
|
|
|
|
|
|
scaleFactor := float64(qLWE) / float64(qBR)
|
|
|
|
|
|
|
|
|
|
// Scale and copy first N_LWE coefficients
|
|
|
|
|
for i := 0; i < nLWE; i++ {
|
|
|
|
|
scaled0 := uint64(float64(c0.Coeffs[0][i])*scaleFactor + 0.5)
|
|
|
|
|
scaled1 := uint64(float64(c1.Coeffs[0][i])*scaleFactor + 0.5)
|
|
|
|
|
ctLWE.Value[0].Coeffs[0][i] = scaled0 % qLWE
|
|
|
|
|
ctLWE.Value[1].Coeffs[0][i] = scaled1 % qLWE
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convert to NTT
|
|
|
|
|
ringQLWE := eval.ringQLWE.AtLevel(eval.params.paramsLWE.MaxLevel())
|
|
|
|
|
ringQLWE.NTT(ctLWE.Value[0], ctLWE.Value[0])
|
|
|
|
|
ringQLWE.NTT(ctLWE.Value[1], ctLWE.Value[1])
|
|
|
|
|
ctLWE.IsNTT = true
|
|
|
|
|
|
|
|
|
|
return &Ciphertext{ctLWE}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// bootstrap performs programmable bootstrapping with the given test polynomial
|
|
|
|
|
// and returns a fresh LWE ciphertext with the result.
|
|
|
|
|
//
|
|
|
|
|
// SECURITY: This implementation does NOT decrypt - it uses sample extraction
|
|
|
|
|
// and key switching, which are public operations on ciphertexts.
|
|
|
|
|
func (eval *Evaluator) bootstrap(ct *Ciphertext, testPoly *ring.Poly) (*Ciphertext, error) {
|
|
|
|
|
// Create map for single slot evaluation
|
|
|
|
|
testPolyMap := map[int]*ring.Poly{0: testPoly}
|
|
|
|
|
|
|
|
|
|
// Step 1: Evaluate blind rotation
|
|
|
|
|
// This produces an RLWE ciphertext under SKBR with the test polynomial
|
|
|
|
|
// evaluated at the encrypted value
|
|
|
|
|
results, err := eval.eval.Evaluate(ct.Ciphertext, testPolyMap, eval.bsk.BRK)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, fmt.Errorf("bootstrap: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Extract result for slot 0
|
|
|
|
|
ctBR, ok := results[0]
|
|
|
|
|
if !ok {
|
|
|
|
|
return nil, fmt.Errorf("bootstrap: no result for slot 0")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Step 2: Sample extract and modulus switch
|
|
|
|
|
// This extracts the result and scales to the LWE modulus
|
|
|
|
|
return eval.sampleExtractAndModSwitch(ctBR)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addCiphertexts adds two ciphertexts element-wise
|
|
|
|
|
func (eval *Evaluator) addCiphertexts(ct1, ct2 *Ciphertext) *Ciphertext {
|
|
|
|
|
result := rlwe.NewCiphertext(eval.params.paramsLWE, 1, ct1.Level())
|
|
|
|
|
|
|
|
|
|
eval.ringQLWE.Add(ct1.Value[0], ct2.Value[0], result.Value[0])
|
|
|
|
|
eval.ringQLWE.Add(ct1.Value[1], ct2.Value[1], result.Value[1])
|
|
|
|
|
|
|
|
|
|
result.IsNTT = ct1.IsNTT
|
|
|
|
|
|
|
|
|
|
return &Ciphertext{result}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// doubleCiphertext multiplies a ciphertext by 2 (element-wise addition with itself)
|
|
|
|
|
// This is key to OpenFHE's optimized XOR: 2*(ct1+ct2) causes wrap-around for (T,T) case
|
|
|
|
|
func (eval *Evaluator) doubleCiphertext(ct *Ciphertext) *Ciphertext {
|
|
|
|
|
result := rlwe.NewCiphertext(eval.params.paramsLWE, 1, ct.Level())
|
|
|
|
|
|
|
|
|
|
eval.ringQLWE.Add(ct.Value[0], ct.Value[0], result.Value[0])
|
|
|
|
|
eval.ringQLWE.Add(ct.Value[1], ct.Value[1], result.Value[1])
|
|
|
|
|
|
|
|
|
|
result.IsNTT = ct.IsNTT
|
|
|
|
|
|
|
|
|
|
return &Ciphertext{result}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// negateCiphertext negates a ciphertext
|
|
|
|
|
func (eval *Evaluator) negateCiphertext(ct *Ciphertext) *Ciphertext {
|
|
|
|
|
result := rlwe.NewCiphertext(eval.params.paramsLWE, 1, ct.Level())
|
|
|
|
|
|
|
|
|
|
eval.ringQLWE.Neg(ct.Value[0], result.Value[0])
|
|
|
|
|
eval.ringQLWE.Neg(ct.Value[1], result.Value[1])
|
|
|
|
|
|
|
|
|
|
result.IsNTT = ct.IsNTT
|
|
|
|
|
|
|
|
|
|
return &Ciphertext{result}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// addConstant adds a scalar constant to the ciphertext's constant term (b)
|
|
|
|
|
// This is used for gate offsets like OpenFHE's gate constants
|
|
|
|
|
func (eval *Evaluator) addConstant(ct *Ciphertext, constant uint64) *Ciphertext {
|
|
|
|
|
result := ct.CopyNew()
|
|
|
|
|
|
|
|
|
|
// Add constant to the constant term (coefficient 0 of polynomial b)
|
|
|
|
|
// Need to handle NTT form
|
|
|
|
|
if result.IsNTT {
|
|
|
|
|
eval.ringQLWE.INTT(result.Value[1], result.Value[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
q := eval.params.QLWE()
|
|
|
|
|
result.Value[1].Coeffs[0][0] = (result.Value[1].Coeffs[0][0] + constant) % q
|
|
|
|
|
|
|
|
|
|
if ct.IsNTT {
|
|
|
|
|
eval.ringQLWE.NTT(result.Value[1], result.Value[1])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return &Ciphertext{result}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== Boolean Gates ==========
|
|
|
|
|
|
|
|
|
|
// NOT computes the logical NOT of the input
|
|
|
|
|
// NOT(a) = 1 - a (free operation - just negate)
|
|
|
|
|
func (eval *Evaluator) NOT(ct *Ciphertext) *Ciphertext {
|
|
|
|
|
return eval.negateCiphertext(ct)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AND computes the logical AND of two inputs
|
|
|
|
|
// AND(a, b) = 1 if a + b >= 1.5 (both are 1)
|
|
|
|
|
func (eval *Evaluator) AND(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyAND)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OR computes the logical OR of two inputs
|
|
|
|
|
// OR(a, b) = 1 if a + b >= 0.5 (at least one is 1)
|
|
|
|
|
func (eval *Evaluator) OR(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyOR)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XOR computes the logical XOR of two inputs
|
|
|
|
|
// Optimized algorithm matching OpenFHE: 2*(ct1 + ct2) with single bootstrap
|
|
|
|
|
// The doubling causes (T,T) → 2*0.25 = 0.5 to wrap around to -0.5,
|
|
|
|
|
// making the XOR test polynomial correctly return FALSE for both (T,T) and (F,F)
|
|
|
|
|
func (eval *Evaluator) XOR(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
doubled := eval.doubleCiphertext(sum) // Key: 2*(ct1+ct2)
|
|
|
|
|
return eval.bootstrap(doubled, eval.bsk.TestPolyXOR)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NAND computes the logical NAND of two inputs
|
|
|
|
|
func (eval *Evaluator) NAND(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyNAND)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOR computes the logical NOR of two inputs
|
|
|
|
|
func (eval *Evaluator) NOR(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyNOR)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// XNOR computes the logical XNOR of two inputs
|
|
|
|
|
// Optimized algorithm matching OpenFHE: 2*(ct1 + ct2) with single bootstrap
|
|
|
|
|
// Same as XOR but with inverted test polynomial
|
|
|
|
|
func (eval *Evaluator) XNOR(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
doubled := eval.doubleCiphertext(sum) // Key: 2*(ct1+ct2)
|
|
|
|
|
return eval.bootstrap(doubled, eval.bsk.TestPolyXNOR)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 13:58:30 -08:00
|
|
|
// CMPCOMBINE computes: isLess OR (isEqual AND bitLt) in one bootstrap
|
|
|
|
|
// This is an optimized gate for comparison propagation.
|
|
|
|
|
// Uses weighted encoding: 2*isLess + isEqual + bitLt, threshold at 0
|
|
|
|
|
func (eval *Evaluator) CMPCOMBINE(isLess, isEqual, bitLt *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
// Compute weighted sum: 2*isLess + isEqual + bitLt
|
|
|
|
|
doubledIsLess := eval.doubleCiphertext(isLess)
|
|
|
|
|
sum := eval.addCiphertexts(doubledIsLess, isEqual)
|
|
|
|
|
sum = eval.addCiphertexts(sum, bitLt)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyCMPCOMBINE)
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 19:11:38 -08:00
|
|
|
// ANDNY computes AND with negated first input: AND(NOT(a), b)
|
|
|
|
|
func (eval *Evaluator) ANDNY(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
return eval.AND(eval.NOT(ct1), ct2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ANDYN computes AND with negated second input: AND(a, NOT(b))
|
|
|
|
|
func (eval *Evaluator) ANDYN(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
return eval.AND(ct1, eval.NOT(ct2))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ORNY computes OR with negated first input: OR(NOT(a), b)
|
|
|
|
|
func (eval *Evaluator) ORNY(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
return eval.OR(eval.NOT(ct1), ct2)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ORYN computes OR with negated second input: OR(a, NOT(b))
|
|
|
|
|
func (eval *Evaluator) ORYN(ct1, ct2 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
return eval.OR(ct1, eval.NOT(ct2))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MUX computes the multiplexer: if sel then a else b
|
|
|
|
|
// MUX(sel, a, b) = (sel AND a) OR (NOT(sel) AND b)
|
|
|
|
|
func (eval *Evaluator) MUX(sel, ctTrue, ctFalse *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
selAndTrue, err := eval.AND(sel, ctTrue)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notSelAndFalse, err := eval.AND(eval.NOT(sel), ctFalse)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return eval.OR(selAndTrue, notSelAndFalse)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ========== Multi-Input Gates ==========
|
|
|
|
|
//
|
|
|
|
|
// Note: Single-bootstrap multi-input gates require careful offset tuning
|
|
|
|
|
// matching OpenFHE's gate constants. For correctness, we use 2-bootstrap
|
|
|
|
|
// composition here. Future optimization could add single-bootstrap versions.
|
|
|
|
|
|
|
|
|
|
// AND3 computes the logical AND of three inputs
|
|
|
|
|
// AND3(a, b, c) = AND(AND(a, b), c)
|
|
|
|
|
func (eval *Evaluator) AND3(ct1, ct2, ct3 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
ab, err := eval.AND(ct1, ct2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return eval.AND(ab, ct3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OR3 computes the logical OR of three inputs
|
|
|
|
|
// OR3(a, b, c) = OR(OR(a, b), c)
|
|
|
|
|
func (eval *Evaluator) OR3(ct1, ct2, ct3 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
ab, err := eval.OR(ct1, ct2)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return eval.OR(ab, ct3)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MAJORITY computes the majority vote of three inputs with single bootstrap
|
|
|
|
|
// MAJORITY(a, b, c) = 1 if at least two inputs are 1
|
|
|
|
|
// This uses a single bootstrap since the threshold at 0 correctly separates
|
|
|
|
|
// 0-1 true inputs (sum < 0) from 2-3 true inputs (sum > 0)
|
|
|
|
|
func (eval *Evaluator) MAJORITY(ct1, ct2, ct3 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
sum := eval.addCiphertexts(ct1, ct2)
|
|
|
|
|
sum = eval.addCiphertexts(sum, ct3)
|
|
|
|
|
return eval.bootstrap(sum, eval.bsk.TestPolyMAJORITY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NAND3 computes the logical NAND of three inputs
|
|
|
|
|
// NAND3(a, b, c) = NOT(AND3(a, b, c))
|
|
|
|
|
func (eval *Evaluator) NAND3(ct1, ct2, ct3 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
result, err := eval.AND3(ct1, ct2, ct3)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return eval.NOT(result), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NOR3 computes the logical NOR of three inputs
|
|
|
|
|
// NOR3(a, b, c) = NOT(OR3(a, b, c))
|
|
|
|
|
func (eval *Evaluator) NOR3(ct1, ct2, ct3 *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
result, err := eval.OR3(ct1, ct2, ct3)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return eval.NOT(result), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Copy creates a copy of a ciphertext
|
|
|
|
|
func (eval *Evaluator) Copy(ct *Ciphertext) *Ciphertext {
|
|
|
|
|
return &Ciphertext{ct.CopyNew()}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Refresh bootstraps a ciphertext to reduce noise
|
|
|
|
|
func (eval *Evaluator) Refresh(ct *Ciphertext) (*Ciphertext, error) {
|
|
|
|
|
return eval.bootstrap(ct, eval.bsk.TestPolyID)
|
|
|
|
|
}
|