mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
fix: restore FHE Go implementation files
Restore core FHE Go files that were removed during cleanup. These are required for precompile/fhe to build.
This commit is contained in:
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
+1003
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"github.com/luxfi/lattice/v7/core/rlwe"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
)
|
||||
|
||||
// Decryptor decrypts FHE ciphertexts to boolean values
|
||||
type Decryptor struct {
|
||||
params Parameters
|
||||
decryptor *rlwe.Decryptor
|
||||
ringQ *ring.Ring
|
||||
}
|
||||
|
||||
// NewDecryptor creates a new decryptor from secret key
|
||||
func NewDecryptor(params Parameters, sk *SecretKey) *Decryptor {
|
||||
return &Decryptor{
|
||||
params: params,
|
||||
decryptor: rlwe.NewDecryptor(params.paramsLWE, sk.SKLWE),
|
||||
ringQ: params.paramsLWE.RingQ(),
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt decrypts a ciphertext to a boolean
|
||||
func (dec *Decryptor) Decrypt(ct *Ciphertext) bool {
|
||||
pt := rlwe.NewPlaintext(dec.params.paramsLWE, ct.Level())
|
||||
dec.decryptor.Decrypt(ct.Ciphertext, pt)
|
||||
|
||||
if pt.IsNTT {
|
||||
dec.ringQ.INTT(pt.Value, pt.Value)
|
||||
}
|
||||
|
||||
// Get the constant term
|
||||
c := pt.Value.Coeffs[0][0]
|
||||
q := dec.params.QLWE()
|
||||
qHalf := q >> 1
|
||||
|
||||
// Decode:
|
||||
// - true was encoded as Q/8, so c ∈ [0, Q/2) means true
|
||||
// - false was encoded as 7Q/8, so c ∈ [Q/2, Q) means false
|
||||
return c < qHalf
|
||||
}
|
||||
|
||||
// DecryptBit returns the decrypted bit as int (0 or 1)
|
||||
func (dec *Decryptor) DecryptBit(ct *Ciphertext) int {
|
||||
if dec.Decrypt(ct) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// DecryptByte decrypts 8 ciphertexts to a byte
|
||||
func (dec *Decryptor) DecryptByte(cts [8]*Ciphertext) byte {
|
||||
var b byte
|
||||
for i := 0; i < 8; i++ {
|
||||
if dec.Decrypt(cts[i]) {
|
||||
b |= 1 << i
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// DecryptUint32 decrypts 32 ciphertexts to uint32
|
||||
func (dec *Decryptor) DecryptUint32(cts [32]*Ciphertext) uint32 {
|
||||
var v uint32
|
||||
for i := 0; i < 32; i++ {
|
||||
if dec.Decrypt(cts[i]) {
|
||||
v |= 1 << i
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// DecryptUint64 decrypts 64 ciphertexts to uint64
|
||||
func (dec *Decryptor) DecryptUint64(cts [64]*Ciphertext) uint64 {
|
||||
var v uint64
|
||||
for i := 0; i < 64; i++ {
|
||||
if dec.Decrypt(cts[i]) {
|
||||
v |= 1 << i
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// DecryptUint256 decrypts 256 ciphertexts to 4 uint64s
|
||||
func (dec *Decryptor) DecryptUint256(cts [256]*Ciphertext) [4]uint64 {
|
||||
var v [4]uint64
|
||||
for w := 0; w < 4; w++ {
|
||||
for i := 0; i < 64; i++ {
|
||||
if dec.Decrypt(cts[w*64+i]) {
|
||||
v[w] |= 1 << i
|
||||
}
|
||||
}
|
||||
}
|
||||
return v
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"github.com/luxfi/lattice/v7/core/rlwe"
|
||||
)
|
||||
|
||||
// Encryptor encrypts boolean values into FHE ciphertexts
|
||||
type Encryptor struct {
|
||||
params Parameters
|
||||
encryptor *rlwe.Encryptor
|
||||
scale float64
|
||||
}
|
||||
|
||||
// NewEncryptor creates a new encryptor from secret key
|
||||
func NewEncryptor(params Parameters, sk *SecretKey) *Encryptor {
|
||||
return &Encryptor{
|
||||
params: params,
|
||||
encryptor: rlwe.NewEncryptor(params.paramsLWE, sk.SKLWE),
|
||||
scale: float64(params.QLWE()) / 4.0, // Encode bit as Q/4 or -Q/4
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts a boolean value
|
||||
// Note: Panics on error (should not happen with valid parameters)
|
||||
func (enc *Encryptor) Encrypt(value bool) *Ciphertext {
|
||||
pt := rlwe.NewPlaintext(enc.params.paramsLWE, enc.params.paramsLWE.MaxLevel())
|
||||
|
||||
q := enc.params.QLWE()
|
||||
// Encode with Q/8 scale so sums of two bits stay in distinguishable range:
|
||||
// - true -> +Q/8 (normalized +0.5 relative to Q/4 scale)
|
||||
// - false -> -Q/8 (normalized -0.5)
|
||||
// After sum:
|
||||
// - (0,0): -Q/4 normalized to -1
|
||||
// - (0,1): 0 normalized to 0
|
||||
// - (1,1): +Q/4 normalized to +1
|
||||
if value {
|
||||
pt.Value.Coeffs[0][0] = q / 8
|
||||
} else {
|
||||
pt.Value.Coeffs[0][0] = q - (q / 8) // = -Q/8 mod Q
|
||||
}
|
||||
|
||||
enc.params.paramsLWE.RingQ().NTT(pt.Value, pt.Value)
|
||||
|
||||
ct := rlwe.NewCiphertext(enc.params.paramsLWE, 1, enc.params.paramsLWE.MaxLevel())
|
||||
if err := enc.encryptor.Encrypt(pt, ct); err != nil {
|
||||
panic(err) // Should not happen with valid parameters
|
||||
}
|
||||
|
||||
return &Ciphertext{ct}
|
||||
}
|
||||
|
||||
// EncryptBit is an alias for Encrypt
|
||||
func (enc *Encryptor) EncryptBit(bit int) *Ciphertext {
|
||||
return enc.Encrypt(bit != 0)
|
||||
}
|
||||
|
||||
// EncryptByte encrypts a byte as 8 ciphertexts (LSB first)
|
||||
func (enc *Encryptor) EncryptByte(b byte) [8]*Ciphertext {
|
||||
var cts [8]*Ciphertext
|
||||
for i := 0; i < 8; i++ {
|
||||
cts[i] = enc.Encrypt((b>>i)&1 == 1)
|
||||
}
|
||||
return cts
|
||||
}
|
||||
|
||||
// EncryptUint32 encrypts a uint32 as 32 ciphertexts (LSB first)
|
||||
func (enc *Encryptor) EncryptUint32(v uint32) [32]*Ciphertext {
|
||||
var cts [32]*Ciphertext
|
||||
for i := 0; i < 32; i++ {
|
||||
cts[i] = enc.Encrypt((v>>i)&1 == 1)
|
||||
}
|
||||
return cts
|
||||
}
|
||||
|
||||
// EncryptUint64 encrypts a uint64 as 64 ciphertexts (LSB first)
|
||||
func (enc *Encryptor) EncryptUint64(v uint64) [64]*Ciphertext {
|
||||
var cts [64]*Ciphertext
|
||||
for i := 0; i < 64; i++ {
|
||||
cts[i] = enc.Encrypt((v>>i)&1 == 1)
|
||||
}
|
||||
return cts
|
||||
}
|
||||
|
||||
// EncryptUint256 encrypts a 256-bit value as 256 ciphertexts (LSB first)
|
||||
func (enc *Encryptor) EncryptUint256(v [4]uint64) [256]*Ciphertext {
|
||||
var cts [256]*Ciphertext
|
||||
for w := 0; w < 4; w++ {
|
||||
for i := 0; i < 64; i++ {
|
||||
cts[w*64+i] = enc.Encrypt((v[w]>>i)&1 == 1)
|
||||
}
|
||||
}
|
||||
return cts
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// Package fhe implements the FHE (Threshold Fully Homomorphic Encryption) scheme
|
||||
// for boolean circuit evaluation on encrypted data.
|
||||
//
|
||||
// FHE enables computation on encrypted bits with bootstrapping after each gate,
|
||||
// making it ideal for arbitrary boolean circuits including EVM execution.
|
||||
//
|
||||
// This implementation is built on luxfi/lattice primitives:
|
||||
// - LWE encryption for bits
|
||||
// - RGSW for bootstrap keys
|
||||
// - Blind rotations for programmable bootstrapping
|
||||
//
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"github.com/luxfi/lattice/v7/core/rgsw/blindrot"
|
||||
"github.com/luxfi/lattice/v7/core/rlwe"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
"github.com/luxfi/lattice/v7/utils"
|
||||
)
|
||||
|
||||
// Parameters defines the FHE parameter set
|
||||
type Parameters struct {
|
||||
// paramsLWE defines parameters for LWE samples (encrypted bits)
|
||||
paramsLWE rlwe.Parameters
|
||||
// paramsBR defines parameters for blind rotation (bootstrapping)
|
||||
paramsBR rlwe.Parameters
|
||||
// evkParams defines evaluation key decomposition
|
||||
evkParams rlwe.EvaluationKeyParameters
|
||||
}
|
||||
|
||||
// ParametersLiteral is a user-friendly parameter specification
|
||||
type ParametersLiteral struct {
|
||||
// LogNLWE is log2 of the LWE dimension (typically 9-10)
|
||||
LogNLWE int
|
||||
// LogNBR is log2 of the blind rotation dimension (typically 10-11)
|
||||
LogNBR int
|
||||
// QLWE is the LWE modulus
|
||||
QLWE uint64
|
||||
// QBR is the blind rotation modulus
|
||||
QBR uint64
|
||||
// BaseTwoDecomposition for key switching (typically 7-10)
|
||||
BaseTwoDecomposition int
|
||||
}
|
||||
|
||||
// Standard parameter sets
|
||||
var (
|
||||
// PN10QP27 provides ~128-bit security with good performance
|
||||
// Uses same dimension for LWE and BR to avoid key switching complexity.
|
||||
// This simplifies bootstrapping while maintaining security.
|
||||
// N=1024, Q=134215681
|
||||
PN10QP27 = ParametersLiteral{
|
||||
LogNLWE: 10, // Same as BR for simplified key switching
|
||||
LogNBR: 10,
|
||||
QLWE: 0x7fff801, // Same modulus for direct compatibility
|
||||
QBR: 0x7fff801, // ~134M
|
||||
BaseTwoDecomposition: 7,
|
||||
}
|
||||
|
||||
// PN11QP54 provides ~128-bit security with higher precision
|
||||
// Uses same dimension for LWE and BR.
|
||||
// N=2048, Q=~2^54
|
||||
PN11QP54 = ParametersLiteral{
|
||||
LogNLWE: 11, // Same as BR
|
||||
LogNBR: 11,
|
||||
QLWE: 0x3FFFFFFFFFC0001, // Same modulus
|
||||
QBR: 0x3FFFFFFFFFC0001, // ~2^54
|
||||
BaseTwoDecomposition: 10,
|
||||
}
|
||||
|
||||
// PN9QP28_STD128 matches OpenFHE's STD128_LMKCDEY as closely as possible
|
||||
// OpenFHE uses LWEDim=447, we use 512 (nearest power of 2)
|
||||
// This enables apples-to-apples comparison with C++ OpenFHE
|
||||
// Security: 128-bit classical
|
||||
// Note: Uses NTT-friendly prime Q ≡ 1 (mod 2048)
|
||||
PN9QP28_STD128 = ParametersLiteral{
|
||||
LogNLWE: 9, // N=512 (OpenFHE uses 447)
|
||||
LogNBR: 10, // N=1024 (matches OpenFHE)
|
||||
QLWE: 0x10001801, // Prime ~2^28 ≡ 1 (mod 2048)
|
||||
QBR: 0x10001801, // Prime ~2^28 ≡ 1 (mod 2048)
|
||||
BaseTwoDecomposition: 5, // Base 32 = 2^5 (matches OpenFHE)
|
||||
}
|
||||
|
||||
// PN9QP27_STD128Q matches OpenFHE's STD128Q_LMKCDEY (post-quantum)
|
||||
// OpenFHE uses LWEDim=483, we use 512 (nearest power of 2)
|
||||
// Security: 128-bit post-quantum
|
||||
// Note: Uses NTT-friendly prime Q ≡ 1 (mod 2048)
|
||||
PN9QP27_STD128Q = ParametersLiteral{
|
||||
LogNLWE: 9, // N=512 (OpenFHE uses 483)
|
||||
LogNBR: 10, // N=1024 (matches OpenFHE)
|
||||
QLWE: 0x8007001, // Prime ~2^27 ≡ 1 (mod 2048)
|
||||
QBR: 0x8007001, // Prime ~2^27 ≡ 1 (mod 2048)
|
||||
BaseTwoDecomposition: 5, // Base 32 = 2^5 (matches OpenFHE)
|
||||
}
|
||||
)
|
||||
|
||||
// NewParametersFromLiteral creates Parameters from a literal specification
|
||||
func NewParametersFromLiteral(lit ParametersLiteral) (params Parameters, err error) {
|
||||
params.paramsLWE, err = rlwe.NewParametersFromLiteral(rlwe.ParametersLiteral{
|
||||
LogN: lit.LogNLWE,
|
||||
Q: []uint64{lit.QLWE},
|
||||
NTTFlag: true,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
params.paramsBR, err = rlwe.NewParametersFromLiteral(rlwe.ParametersLiteral{
|
||||
LogN: lit.LogNBR,
|
||||
Q: []uint64{lit.QBR},
|
||||
NTTFlag: true,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
params.evkParams = rlwe.EvaluationKeyParameters{
|
||||
BaseTwoDecomposition: utils.Pointy(lit.BaseTwoDecomposition),
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// N returns the LWE dimension
|
||||
func (p Parameters) N() int {
|
||||
return p.paramsLWE.N()
|
||||
}
|
||||
|
||||
// NBR returns the blind rotation dimension
|
||||
func (p Parameters) NBR() int {
|
||||
return p.paramsBR.N()
|
||||
}
|
||||
|
||||
// QLWE returns the LWE modulus
|
||||
func (p Parameters) QLWE() uint64 {
|
||||
return p.paramsLWE.Q()[0]
|
||||
}
|
||||
|
||||
// QBR returns the blind rotation modulus
|
||||
func (p Parameters) QBR() uint64 {
|
||||
return p.paramsBR.Q()[0]
|
||||
}
|
||||
|
||||
// SecretKey contains the LWE and RLWE secret keys
|
||||
type SecretKey struct {
|
||||
// LWE secret key for encrypting bits
|
||||
SKLWE *rlwe.SecretKey
|
||||
// RLWE secret key for blind rotation results
|
||||
SKBR *rlwe.SecretKey
|
||||
}
|
||||
|
||||
// PublicKey contains the LWE public key for encryption
|
||||
// This allows users to encrypt data without having the secret key
|
||||
type PublicKey struct {
|
||||
// PKLWE is the LWE public key for encrypting bits
|
||||
PKLWE *rlwe.PublicKey
|
||||
}
|
||||
|
||||
// BootstrapKey contains the keys needed for bootstrapping
|
||||
type BootstrapKey struct {
|
||||
// BRK is the blind rotation key (RGSW encryptions of LWE secret key bits)
|
||||
BRK blindrot.BlindRotationEvaluationKeySet
|
||||
// KSK is the key switching key from SKBR to SKLWE
|
||||
// This enables sample extraction without decryption
|
||||
KSK *rlwe.EvaluationKey
|
||||
// TestPolyAND is the test polynomial for AND gate
|
||||
TestPolyAND *ring.Poly
|
||||
// TestPolyOR is the test polynomial for OR gate
|
||||
TestPolyOR *ring.Poly
|
||||
// TestPolyXOR is the test polynomial for XOR gate
|
||||
TestPolyXOR *ring.Poly
|
||||
// TestPolyNAND is the test polynomial for NAND gate
|
||||
TestPolyNAND *ring.Poly
|
||||
// TestPolyNOR is the test polynomial for NOR gate
|
||||
TestPolyNOR *ring.Poly
|
||||
// TestPolyXNOR is the test polynomial for XNOR gate
|
||||
TestPolyXNOR *ring.Poly
|
||||
// TestPolyID is the test polynomial for identity (refresh/NOT)
|
||||
TestPolyID *ring.Poly
|
||||
// TestPolyMAJORITY is the test polynomial for majority vote (2 of 3)
|
||||
TestPolyMAJORITY *ring.Poly
|
||||
// Parameters
|
||||
params Parameters
|
||||
}
|
||||
|
||||
// Ciphertext represents an encrypted bit
|
||||
type Ciphertext struct {
|
||||
*rlwe.Ciphertext
|
||||
}
|
||||
|
||||
// KeyGenerator generates FHE keys
|
||||
type KeyGenerator struct {
|
||||
params Parameters
|
||||
kgenLWE *rlwe.KeyGenerator
|
||||
kgenBR *rlwe.KeyGenerator
|
||||
ringQBR *ring.Ring
|
||||
scaleBR float64
|
||||
}
|
||||
|
||||
// NewKeyGenerator creates a new key generator
|
||||
func NewKeyGenerator(params Parameters) *KeyGenerator {
|
||||
return &KeyGenerator{
|
||||
params: params,
|
||||
kgenLWE: rlwe.NewKeyGenerator(params.paramsLWE),
|
||||
kgenBR: rlwe.NewKeyGenerator(params.paramsBR),
|
||||
ringQBR: params.paramsBR.RingQ(),
|
||||
scaleBR: float64(params.QBR()) / 8.0, // Scale for [-1, 1] -> [-Q/8, Q/8]
|
||||
}
|
||||
}
|
||||
|
||||
// GenSecretKey generates a new secret key pair
|
||||
func (kg *KeyGenerator) GenSecretKey() *SecretKey {
|
||||
// When LWE and BR have the same dimension, use the same key for both
|
||||
// This simplifies bootstrapping by eliminating key switching
|
||||
if kg.params.N() == kg.params.NBR() {
|
||||
sk := kg.kgenBR.GenSecretKeyNew()
|
||||
return &SecretKey{
|
||||
SKLWE: sk,
|
||||
SKBR: sk,
|
||||
}
|
||||
}
|
||||
// Different dimensions require separate keys
|
||||
return &SecretKey{
|
||||
SKLWE: kg.kgenLWE.GenSecretKeyNew(),
|
||||
SKBR: kg.kgenBR.GenSecretKeyNew(),
|
||||
}
|
||||
}
|
||||
|
||||
// GenPublicKey generates a public key from a secret key
|
||||
// The public key can be shared with users to allow them to encrypt data
|
||||
// without having access to the secret key
|
||||
func (kg *KeyGenerator) GenPublicKey(sk *SecretKey) *PublicKey {
|
||||
return &PublicKey{
|
||||
PKLWE: kg.kgenLWE.GenPublicKeyNew(sk.SKLWE),
|
||||
}
|
||||
}
|
||||
|
||||
// GenKeyPair generates both a secret key and corresponding public key
|
||||
func (kg *KeyGenerator) GenKeyPair() (*SecretKey, *PublicKey) {
|
||||
sk := kg.GenSecretKey()
|
||||
pk := kg.GenPublicKey(sk)
|
||||
return sk, pk
|
||||
}
|
||||
|
||||
// GenBootstrapKey generates the bootstrap key from secret keys
|
||||
func (kg *KeyGenerator) GenBootstrapKey(sk *SecretKey) *BootstrapKey {
|
||||
// Generate blind rotation key
|
||||
brk := blindrot.GenEvaluationKeyNew(kg.params.paramsBR, sk.SKBR, kg.params.paramsLWE, sk.SKLWE, kg.params.evkParams)
|
||||
|
||||
// Generate key switching key from SKBR to SKLWE
|
||||
// This key switches from the extraction key (SKBR coefficients treated as LWE key)
|
||||
// to the LWE secret key (SKLWE).
|
||||
//
|
||||
// The extraction key for sample extraction from RLWE(N_BR) is the polynomial
|
||||
// secret key SKBR, where decryption becomes:
|
||||
// m[0] = c0[0] + <extraction_vector, c1_coeffs>
|
||||
// where extraction_vector is derived from SKBR coefficients.
|
||||
//
|
||||
// For simplicity and to work with the lattice library, we generate a key switching
|
||||
// key that operates in the BR dimension and switches to an SKLWE-compatible key.
|
||||
ksk := kg.kgenBR.GenEvaluationKeyNew(sk.SKBR, kg.createExtendedSKLWE(sk.SKLWE), kg.params.evkParams)
|
||||
|
||||
// Scale for test polynomials
|
||||
scale := rlwe.NewScale(kg.scaleBR)
|
||||
|
||||
// Test polynomials for FHE gates
|
||||
// With Q/8 encoding, after adding two bits the normalized positions are:
|
||||
// - true+true: highest x (> 0.25)
|
||||
// - true+false: middle x (∈ [-0.25, 0.25])
|
||||
// - false+false: lowest x (< -0.25)
|
||||
|
||||
// AND: output 1 only when both inputs are 1 (x >= 0.25)
|
||||
// Use >= to handle exact boundary case when sum of two TRUE = 0.25
|
||||
testPolyAND := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x >= 0.25 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// OR: output 1 when at least one input is 1 (x > -0.25)
|
||||
testPolyOR := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x > -0.25 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// XOR: output 1 when exactly one input is 1
|
||||
// With 2*(ct1+ct2) pre-processing (matching OpenFHE):
|
||||
// - (F,F): 2*(-0.25) = -0.5
|
||||
// - (T,F) or (F,T): 2*(0) = 0
|
||||
// - (T,T): 2*(0.25) = 0.5 → wraps to -0.5
|
||||
// So XOR = TRUE only when x ≈ 0
|
||||
// Use 0.30 boundaries for noise margin in carry chains (FALSE at ±0.5 has 0.20 margin)
|
||||
testPolyXOR := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x > -0.30 && x < 0.30 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// NAND: output 0 only when both inputs are 1
|
||||
// Use >= to handle exact boundary case when sum of two TRUE = 0.25
|
||||
testPolyNAND := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x >= 0.25 {
|
||||
return -1.0
|
||||
}
|
||||
return 1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// NOR: output 1 only when both inputs are 0 (x < -0.25)
|
||||
testPolyNOR := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x > -0.25 {
|
||||
return -1.0
|
||||
}
|
||||
return 1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// XNOR: output 1 when both inputs same (NOT of XOR)
|
||||
// With 2*(ct1+ct2) pre-processing:
|
||||
// - (F,F): -0.5 → TRUE
|
||||
// - (T,F) or (F,T): 0 → FALSE
|
||||
// - (T,T): -0.5 (wrapped) → TRUE
|
||||
// Use 0.30 boundaries to match XOR noise margin
|
||||
testPolyXNOR := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x > -0.30 && x < 0.30 {
|
||||
return -1.0
|
||||
}
|
||||
return 1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// Identity (for refresh): preserve input bit (TRUE for high values)
|
||||
testPolyID := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x >= 0 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// ========== Multi-Input Gates ==========
|
||||
// MAJORITY: output 1 when at least 2 of 3 inputs are 1
|
||||
// For 3 inputs with Q/8 encoding, sum ranges from -3Q/8 to +3Q/8:
|
||||
// - 0 true: -3/8 = -0.375 → FALSE
|
||||
// - 1 true: -1/8 = -0.125 → FALSE
|
||||
// - 2 true: +1/8 = +0.125 → TRUE
|
||||
// - 3 true: +3/8 = +0.375 → TRUE
|
||||
// Threshold at 0 correctly separates these cases
|
||||
testPolyMAJORITY := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
if x > 0 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, kg.ringQBR, -1, 1)
|
||||
|
||||
// Note: AND3 and OR3 use composition (2 bootstraps) rather than
|
||||
// single-bootstrap with gate constants. Single-bootstrap versions
|
||||
// would require OpenFHE-style gate constant offsets.
|
||||
|
||||
return &BootstrapKey{
|
||||
BRK: brk,
|
||||
KSK: ksk,
|
||||
TestPolyAND: &testPolyAND,
|
||||
TestPolyOR: &testPolyOR,
|
||||
TestPolyXOR: &testPolyXOR,
|
||||
TestPolyNAND: &testPolyNAND,
|
||||
TestPolyNOR: &testPolyNOR,
|
||||
TestPolyXNOR: &testPolyXNOR,
|
||||
TestPolyID: &testPolyID,
|
||||
TestPolyMAJORITY: &testPolyMAJORITY,
|
||||
params: kg.params,
|
||||
}
|
||||
}
|
||||
|
||||
// createExtendedSKLWE creates a secret key in the BR dimension that is compatible
|
||||
// with SKLWE for key switching purposes. The extended key has SKLWE coefficients
|
||||
// in the first N_LWE positions and zeros elsewhere.
|
||||
func (kg *KeyGenerator) createExtendedSKLWE(sklwe *rlwe.SecretKey) *rlwe.SecretKey {
|
||||
// Create a new secret key in BR parameters
|
||||
extendedSK := rlwe.NewSecretKey(kg.params.paramsBR)
|
||||
|
||||
// Get the polynomial rings
|
||||
ringQLWE := kg.params.paramsLWE.RingQ()
|
||||
ringQBR := kg.params.paramsBR.RingQ()
|
||||
|
||||
// Convert SKLWE from NTT to coefficient form to copy coefficients
|
||||
sklweCoeffs := ringQLWE.NewPoly()
|
||||
sklweCoeffs.CopyLvl(ringQLWE.Level(), sklwe.Value.Q)
|
||||
ringQLWE.INTT(sklweCoeffs, sklweCoeffs)
|
||||
|
||||
// The extended secret key in BR dimension
|
||||
// For sample extraction compatibility, we embed the LWE key as:
|
||||
// s_ext[i] = s_lwe[i] for i < N_LWE, s_ext[i] = 0 for i >= N_LWE
|
||||
extCoeffs := ringQBR.NewPoly()
|
||||
nLWE := ringQLWE.N()
|
||||
for i := 0; i < nLWE; i++ {
|
||||
extCoeffs.Coeffs[0][i] = sklweCoeffs.Coeffs[0][i]
|
||||
}
|
||||
|
||||
// Convert to NTT form (required by lattice library)
|
||||
ringQBR.NTT(extCoeffs, extendedSK.Value.Q)
|
||||
ringQBR.MForm(extendedSK.Value.Q, extendedSK.Value.Q)
|
||||
|
||||
return extendedSK
|
||||
}
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
//go:build !cgo
|
||||
|
||||
// Package gpu provides GPU-accelerated FHE operations.
|
||||
// When CGO is disabled, this package delegates to the pure Go fhe package.
|
||||
package gpu
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/fhe"
|
||||
)
|
||||
|
||||
// SecurityLevel represents the security strength
|
||||
type SecurityLevel int
|
||||
|
||||
const (
|
||||
SecuritySTD128 SecurityLevel = iota
|
||||
SecuritySTD192
|
||||
SecuritySTD256
|
||||
)
|
||||
|
||||
// Method represents the FHE method/variant
|
||||
type Method int
|
||||
|
||||
const (
|
||||
MethodAP Method = iota // Alperin-Sheriff-Peikert
|
||||
MethodGINX // GINX bootstrapping
|
||||
MethodLMKCDEY // LMKCDEY bootstrapping
|
||||
)
|
||||
|
||||
// Context wraps the pure Go FHE context
|
||||
type Context struct {
|
||||
params fhe.Parameters
|
||||
kgen *fhe.KeyGenerator
|
||||
evaluator *fhe.Evaluator
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// SecretKey wraps the pure Go secret key
|
||||
type SecretKey struct {
|
||||
key *fhe.SecretKey
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
// PublicKey wraps the pure Go bootstrap key
|
||||
type PublicKey struct {
|
||||
bsk *fhe.BootstrapKey
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
// Ciphertext wraps the pure Go ciphertext
|
||||
type Ciphertext struct {
|
||||
ct *fhe.Ciphertext
|
||||
ctx *Context
|
||||
}
|
||||
|
||||
// Integer wraps encrypted integer bits
|
||||
type Integer struct {
|
||||
bits []*fhe.Ciphertext
|
||||
ctx *Context
|
||||
bitLen int
|
||||
}
|
||||
|
||||
// getParamsLiteral maps security level to parameter literal
|
||||
func getParamsLiteral(level SecurityLevel) fhe.ParametersLiteral {
|
||||
switch level {
|
||||
case SecuritySTD192, SecuritySTD256:
|
||||
return fhe.PN11QP54
|
||||
default:
|
||||
return fhe.PN10QP27
|
||||
}
|
||||
}
|
||||
|
||||
// NewContext creates a new FHE context using pure Go implementation
|
||||
func NewContext(level SecurityLevel, method Method) (*Context, error) {
|
||||
literal := getParamsLiteral(level)
|
||||
params, err := fhe.NewParametersFromLiteral(literal)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := &Context{
|
||||
params: params,
|
||||
kgen: fhe.NewKeyGenerator(params),
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
// Free releases the context resources
|
||||
func (c *Context) Free() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.evaluator = nil
|
||||
c.kgen = nil
|
||||
}
|
||||
|
||||
// GenerateSecretKey generates a new secret key
|
||||
func (c *Context) GenerateSecretKey() (*SecretKey, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.kgen == nil {
|
||||
return nil, errors.New("context not initialized")
|
||||
}
|
||||
|
||||
sk := c.kgen.GenSecretKey()
|
||||
return &SecretKey{
|
||||
key: sk,
|
||||
ctx: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Free releases the secret key
|
||||
func (sk *SecretKey) Free() {
|
||||
sk.key = nil
|
||||
}
|
||||
|
||||
// GenerateBootstrapKey generates the bootstrapping key
|
||||
func (c *Context) GenerateBootstrapKey(sk *SecretKey) error {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.kgen == nil {
|
||||
return errors.New("context not initialized")
|
||||
}
|
||||
if sk.key == nil {
|
||||
return errors.New("secret key is nil")
|
||||
}
|
||||
|
||||
bsk := c.kgen.GenBootstrapKey(sk.key)
|
||||
c.evaluator = fhe.NewEvaluator(c.params, bsk)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GeneratePublicKey generates a public key from secret key
|
||||
func (c *Context) GeneratePublicKey(sk *SecretKey) (*PublicKey, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.kgen == nil {
|
||||
return nil, errors.New("context not initialized")
|
||||
}
|
||||
if sk.key == nil {
|
||||
return nil, errors.New("secret key is nil")
|
||||
}
|
||||
|
||||
bsk := c.kgen.GenBootstrapKey(sk.key)
|
||||
return &PublicKey{
|
||||
bsk: bsk,
|
||||
ctx: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Free releases the public key
|
||||
func (pk *PublicKey) Free() {
|
||||
pk.bsk = nil
|
||||
}
|
||||
|
||||
// EncryptBit encrypts a single bit using secret key
|
||||
func (c *Context) EncryptBit(sk *SecretKey, value bool) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if sk.key == nil {
|
||||
return nil, errors.New("secret key is nil")
|
||||
}
|
||||
|
||||
enc := fhe.NewEncryptor(c.params, sk.key)
|
||||
var bit int
|
||||
if value {
|
||||
bit = 1
|
||||
}
|
||||
ct := enc.EncryptBit(bit)
|
||||
|
||||
return &Ciphertext{
|
||||
ct: ct,
|
||||
ctx: c,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DecryptBit decrypts a ciphertext to a boolean
|
||||
func (c *Context) DecryptBit(sk *SecretKey, ct *Ciphertext) (bool, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if sk.key == nil {
|
||||
return false, errors.New("secret key is nil")
|
||||
}
|
||||
|
||||
dec := fhe.NewDecryptor(c.params, sk.key)
|
||||
bit := dec.DecryptBit(ct.ct)
|
||||
return bit == 1, nil
|
||||
}
|
||||
|
||||
// AND performs homomorphic AND on two ciphertexts
|
||||
func (c *Context) AND(a, b *Ciphertext) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.evaluator == nil {
|
||||
return nil, ErrNoBootstrapKey
|
||||
}
|
||||
|
||||
result, err := c.evaluator.AND(a.ct, b.ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Ciphertext{ct: result, ctx: c}, nil
|
||||
}
|
||||
|
||||
// OR performs homomorphic OR on two ciphertexts
|
||||
func (c *Context) OR(a, b *Ciphertext) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.evaluator == nil {
|
||||
return nil, ErrNoBootstrapKey
|
||||
}
|
||||
|
||||
result, err := c.evaluator.OR(a.ct, b.ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Ciphertext{ct: result, ctx: c}, nil
|
||||
}
|
||||
|
||||
// XOR performs homomorphic XOR on two ciphertexts
|
||||
func (c *Context) XOR(a, b *Ciphertext) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.evaluator == nil {
|
||||
return nil, ErrNoBootstrapKey
|
||||
}
|
||||
|
||||
result, err := c.evaluator.XOR(a.ct, b.ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Ciphertext{ct: result, ctx: c}, nil
|
||||
}
|
||||
|
||||
// NOT performs homomorphic NOT on a ciphertext
|
||||
func (c *Context) NOT(a *Ciphertext) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.evaluator == nil {
|
||||
return nil, ErrNoBootstrapKey
|
||||
}
|
||||
|
||||
result := c.evaluator.NOT(a.ct)
|
||||
return &Ciphertext{ct: result, ctx: c}, nil
|
||||
}
|
||||
|
||||
// NAND performs homomorphic NAND on two ciphertexts
|
||||
func (c *Context) NAND(a, b *Ciphertext) (*Ciphertext, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
if c.evaluator == nil {
|
||||
return nil, ErrNoBootstrapKey
|
||||
}
|
||||
|
||||
result, err := c.evaluator.NAND(a.ct, b.ct)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Ciphertext{ct: result, ctx: c}, nil
|
||||
}
|
||||
|
||||
// GPUAvailable returns false when CGO is disabled
|
||||
func GPUAvailable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// GetBackend returns "CPU (pure Go)" when CGO is disabled
|
||||
func GetBackend() string {
|
||||
return "CPU (pure Go)"
|
||||
}
|
||||
|
||||
// ErrNoBootstrapKey is returned when operations are attempted without a bootstrap key
|
||||
var ErrNoBootstrapKey = errors.New("bootstrap key not generated - call GenerateBootstrapKey first")
|
||||
+422
@@ -0,0 +1,422 @@
|
||||
//go:build luxgpu
|
||||
|
||||
// Package gpu - TFHE-specific array operations
|
||||
// These operations are needed for GPU-accelerated TFHE bootstrapping
|
||||
|
||||
package gpu
|
||||
|
||||
/*
|
||||
#include <lux/gpu/gpu.h>
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// intsToCInts converts a Go []int slice to []C.int for CGO calls
|
||||
// Go int is 64-bit on 64-bit systems, C int is 32-bit
|
||||
func intsToCInts(ints []int) []C.int {
|
||||
cInts := make([]C.int, len(ints))
|
||||
for i, v := range ints {
|
||||
cInts[i] = C.int(v)
|
||||
}
|
||||
return cInts
|
||||
}
|
||||
|
||||
// SliceArg represents slicing arguments for Slice operation
|
||||
type SliceArg struct {
|
||||
Start int
|
||||
Stop int
|
||||
}
|
||||
|
||||
// Subtract performs element-wise subtraction: a - b
|
||||
func Subtract(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_subtract(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Divide performs element-wise division: a / b
|
||||
func Divide(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_divide(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Remainder computes element-wise remainder: a % b
|
||||
func Remainder(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_remainder(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Floor computes element-wise floor
|
||||
func Floor(a *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_floor(a.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Round rounds elements to nearest integer
|
||||
func Round(a *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_round(a.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// RightShift performs element-wise right shift: a >> b
|
||||
func RightShift(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_right_shift(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// AsType casts array to specified dtype
|
||||
func AsType(a *Array, dtype Dtype) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_astype(a.handle, C.int(dtype))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Full creates an array filled with a constant value
|
||||
func Full(shape []int, value interface{}, dtype Dtype) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
var fval float64
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
fval = v
|
||||
case float32:
|
||||
fval = float64(v)
|
||||
case int:
|
||||
fval = float64(v)
|
||||
case int32:
|
||||
fval = float64(v)
|
||||
case int64:
|
||||
fval = float64(v)
|
||||
case uint64:
|
||||
fval = float64(v)
|
||||
}
|
||||
|
||||
cShape := intsToCInts(shape)
|
||||
handle := C.mlx_full(&cShape[0], C.int(len(shape)), C.double(fval), C.int(dtype))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: shape,
|
||||
dtype: dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Reshape reshapes array to new shape
|
||||
func Reshape(a *Array, shape []int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
cShape := intsToCInts(shape)
|
||||
handle := C.mlx_reshape(a.handle, &cShape[0], C.int(len(shape)))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Squeeze removes dimension of size 1 at specified axis
|
||||
func Squeeze(a *Array, axis int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_squeeze(a.handle, C.int(axis))
|
||||
|
||||
// Calculate new shape
|
||||
newShape := make([]int, 0, len(a.shape)-1)
|
||||
for i, s := range a.shape {
|
||||
if i != axis && (axis >= 0 || i != len(a.shape)+axis) {
|
||||
newShape = append(newShape, s)
|
||||
}
|
||||
}
|
||||
if len(newShape) == 0 {
|
||||
newShape = []int{1}
|
||||
}
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: newShape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Broadcast broadcasts array to specified shape
|
||||
func Broadcast(a *Array, shape []int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
cShape := intsToCInts(shape)
|
||||
handle := C.mlx_broadcast(a.handle, &cShape[0], C.int(len(shape)))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Slice extracts a slice from the array
|
||||
func Slice(a *Array, args []SliceArg) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
ndim := len(args)
|
||||
starts := make([]int, ndim)
|
||||
stops := make([]int, ndim)
|
||||
|
||||
for i, arg := range args {
|
||||
starts[i] = arg.Start
|
||||
stops[i] = arg.Stop
|
||||
}
|
||||
|
||||
cStarts := intsToCInts(starts)
|
||||
cStops := intsToCInts(stops)
|
||||
handle := C.mlx_slice_nd(a.handle, &cStarts[0], &cStops[0], C.int(ndim))
|
||||
|
||||
// Calculate new shape
|
||||
newShape := make([]int, ndim)
|
||||
for i := 0; i < ndim; i++ {
|
||||
newShape[i] = stops[i] - starts[i]
|
||||
}
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: newShape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Take gathers elements along an axis
|
||||
func Take(a *Array, indices *Array, axis int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_take(a.handle, indices.handle, C.int(axis))
|
||||
|
||||
// Shape depends on indices shape replacing the axis dimension
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: indices.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// TakeAlongAxis gathers values along an axis using indices of the same shape
|
||||
func TakeAlongAxis(a, indices *Array, axis int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_take_along_axis(a.handle, indices.handle, C.int(axis))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: indices.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Concatenate concatenates arrays along an axis
|
||||
func Concatenate(arrays []Array, axis int) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handles := make([]unsafe.Pointer, len(arrays))
|
||||
for i := range arrays {
|
||||
handles[i] = arrays[i].handle
|
||||
}
|
||||
|
||||
handle := C.mlx_concatenate(&handles[0], C.int(len(arrays)), C.int(axis))
|
||||
|
||||
// Calculate output shape
|
||||
newShape := make([]int, len(arrays[0].shape))
|
||||
copy(newShape, arrays[0].shape)
|
||||
for i := 1; i < len(arrays); i++ {
|
||||
newShape[axis] += arrays[i].shape[axis]
|
||||
}
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: newShape,
|
||||
dtype: arrays[0].dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// GreaterEqual compares element-wise: a >= b
|
||||
func GreaterEqual(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_greater_equal(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: Bool,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Less compares element-wise: a < b
|
||||
func Less(a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_less(a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: Bool,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// Where selects elements based on condition: cond ? a : b
|
||||
func Where(cond, a, b *Array) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
handle := C.mlx_where(cond.handle, a.handle, b.handle)
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: a.shape,
|
||||
dtype: a.dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
|
||||
// ToSlice downloads array data to a Go slice
|
||||
func ToSlice[T int64 | float64 | float32 | int32](a *Array) []T {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
// Calculate total size
|
||||
total := 1
|
||||
for _, s := range a.shape {
|
||||
total *= s
|
||||
}
|
||||
|
||||
// For int64
|
||||
result := make([]T, total)
|
||||
|
||||
switch any(result).(type) {
|
||||
case []int64:
|
||||
cOut := (*C.longlong)(unsafe.Pointer(&result[0]))
|
||||
C.mlx_to_slice_int64(a.handle, cOut, C.int(total))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ArangeInt creates an array with sequential integer values [start, stop) with step
|
||||
func ArangeInt(start, stop, step int, dtype Dtype) *Array {
|
||||
DefaultContext.mu.Lock()
|
||||
defer DefaultContext.mu.Unlock()
|
||||
|
||||
size := (stop - start + step - 1) / step
|
||||
if size <= 0 {
|
||||
size = 0
|
||||
}
|
||||
|
||||
handle := C.mlx_arange(C.double(start), C.double(stop), C.double(step))
|
||||
|
||||
arr := &Array{
|
||||
handle: handle,
|
||||
shape: []int{size},
|
||||
dtype: dtype,
|
||||
}
|
||||
DefaultContext.arrays[handle] = arr
|
||||
return arr
|
||||
}
|
||||
+1094
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,564 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2024-2025, Lux Industries Inc
|
||||
//
|
||||
// Optimized Encrypted Comparison for Solidity/EVM
|
||||
//
|
||||
// Key innovation: Kogge-Stone parallel prefix comparison with GPU acceleration
|
||||
// - Traditional: Serial bit-by-bit comparison O(n) depth
|
||||
// - Optimized: Parallel prefix O(log n) depth with early termination hints
|
||||
//
|
||||
// Patent: PAT-FHE-C8 - Encrypted Comparison for Solidity
|
||||
// For enterprise licensing: fhe@lux.network
|
||||
|
||||
#ifndef LUXFHE_ENCRYPTED_COMPARE_H
|
||||
#define LUXFHE_ENCRYPTED_COMPARE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// =============================================================================
|
||||
// Forward Declarations
|
||||
// =============================================================================
|
||||
|
||||
typedef void* LuxFHEEngine;
|
||||
typedef void* LuxFHECiphertext;
|
||||
typedef void* LuxFHEInteger;
|
||||
typedef void* LuxFHECompareContext;
|
||||
|
||||
// =============================================================================
|
||||
// Comparison Result Types
|
||||
// =============================================================================
|
||||
|
||||
// Comparison operations for Solidity FHE precompile
|
||||
typedef enum {
|
||||
LUXFHE_CMP_LT = 0, // a < b
|
||||
LUXFHE_CMP_LE = 1, // a <= b
|
||||
LUXFHE_CMP_GT = 2, // a > b
|
||||
LUXFHE_CMP_GE = 3, // a >= b
|
||||
LUXFHE_CMP_EQ = 4, // a == b
|
||||
LUXFHE_CMP_NE = 5, // a != b
|
||||
LUXFHE_CMP_MIN = 6, // min(a, b)
|
||||
LUXFHE_CMP_MAX = 7 // max(a, b)
|
||||
} LuxFHECompareOp;
|
||||
|
||||
// Integer bit widths matching Solidity FHE types
|
||||
typedef enum {
|
||||
LUXFHE_UINT4 = 4,
|
||||
LUXFHE_UINT8 = 8,
|
||||
LUXFHE_UINT16 = 16,
|
||||
LUXFHE_UINT32 = 32,
|
||||
LUXFHE_UINT64 = 64,
|
||||
LUXFHE_UINT128 = 128,
|
||||
LUXFHE_UINT160 = 160, // Ethereum address
|
||||
LUXFHE_UINT256 = 256
|
||||
} LuxFHEUintWidth;
|
||||
|
||||
// =============================================================================
|
||||
// Kogge-Stone Parallel Comparison Tree
|
||||
// =============================================================================
|
||||
|
||||
// Configuration for parallel prefix network
|
||||
typedef struct {
|
||||
uint32_t num_bits; // Total bits to compare
|
||||
uint32_t block_size; // Bits per radix block (2 or 4)
|
||||
uint32_t num_stages; // log2(num_bits) stages
|
||||
bool use_gpu; // Enable GPU acceleration
|
||||
bool early_termination; // Enable early termination hints
|
||||
uint32_t batch_size; // Operations to batch for GPU
|
||||
} LuxFHEKoggeStoneConfig;
|
||||
|
||||
// Stage data for Kogge-Stone tree
|
||||
typedef struct {
|
||||
uint32_t stage; // Current stage index
|
||||
uint32_t stride; // Distance between compared elements
|
||||
uint32_t num_ops; // Operations in this stage
|
||||
} LuxFHEKoggeStoneStage;
|
||||
|
||||
// =============================================================================
|
||||
// Parallel Compare Context
|
||||
// =============================================================================
|
||||
|
||||
// Create comparison context with Kogge-Stone configuration
|
||||
LuxFHECompareContext luxfhe_compare_context_create(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHEKoggeStoneConfig config
|
||||
);
|
||||
|
||||
// Free comparison context
|
||||
void luxfhe_compare_context_free(LuxFHECompareContext ctx);
|
||||
|
||||
// Get Kogge-Stone stage info
|
||||
LuxFHEKoggeStoneStage luxfhe_compare_get_stage_info(
|
||||
LuxFHECompareContext ctx,
|
||||
uint32_t stage
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Core Comparison Operations (Solidity FHE Interface)
|
||||
// =============================================================================
|
||||
|
||||
// FHE.lt(euintN a, euintN b) -> ebool
|
||||
// Parallel prefix less-than comparison
|
||||
// Algorithm:
|
||||
// 1. Compute bit differences: d[i] = a[i] XOR b[i]
|
||||
// 2. Kogge-Stone parallel prefix to find MSB difference
|
||||
// 3. Result = b[msb_diff_position]
|
||||
LuxFHECiphertext luxfhe_lt(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.le(euintN a, euintN b) -> ebool
|
||||
LuxFHECiphertext luxfhe_le(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.gt(euintN a, euintN b) -> ebool
|
||||
LuxFHECiphertext luxfhe_gt(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.ge(euintN a, euintN b) -> ebool
|
||||
LuxFHECiphertext luxfhe_ge(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.eq(euintN a, euintN b) -> ebool
|
||||
// Parallel XOR-reduction for equality
|
||||
LuxFHECiphertext luxfhe_eq(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.ne(euintN a, euintN b) -> ebool
|
||||
LuxFHECiphertext luxfhe_ne(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.min(euintN a, euintN b) -> euintN
|
||||
// Returns encrypted minimum using oblivious selection
|
||||
LuxFHEInteger luxfhe_min(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// FHE.max(euintN a, euintN b) -> euintN
|
||||
LuxFHEInteger luxfhe_max(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// Batch Comparison (GPU-Optimized)
|
||||
// =============================================================================
|
||||
|
||||
// Batch comparison for multiple pairs
|
||||
// Enables GPU kernel saturation for high throughput
|
||||
typedef struct {
|
||||
LuxFHEInteger* a_values; // Array of first operands
|
||||
LuxFHEInteger* b_values; // Array of second operands
|
||||
uint32_t count; // Number of comparisons
|
||||
LuxFHECompareOp op; // Comparison operation
|
||||
} LuxFHECompareBatch;
|
||||
|
||||
// Execute batch comparison
|
||||
// Returns array of results (ebool for lt/le/gt/ge/eq/ne, euint for min/max)
|
||||
void* luxfhe_compare_batch(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHECompareBatch* batch
|
||||
);
|
||||
|
||||
// Free batch results
|
||||
void luxfhe_compare_batch_free(void* results, uint32_t count, LuxFHECompareOp op);
|
||||
|
||||
// =============================================================================
|
||||
// Scalar Comparison (Plaintext Operand)
|
||||
// =============================================================================
|
||||
|
||||
// FHE.lt(euintN a, uintN b) -> ebool (plaintext right operand)
|
||||
// Optimization: precompute b's bit representation
|
||||
LuxFHECiphertext luxfhe_lt_scalar(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
const uint8_t* b_bytes,
|
||||
uint32_t b_len
|
||||
);
|
||||
|
||||
// FHE.eq(euintN a, uintN b) -> ebool (plaintext right operand)
|
||||
LuxFHECiphertext luxfhe_eq_scalar(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
const uint8_t* b_bytes,
|
||||
uint32_t b_len
|
||||
);
|
||||
|
||||
// =============================================================================
|
||||
// GPU Kernel Interface (Metal/CUDA)
|
||||
// =============================================================================
|
||||
|
||||
#ifdef LUXFHE_GPU_ENABLED
|
||||
|
||||
// Kogge-Stone parallel prefix kernel configuration
|
||||
typedef struct {
|
||||
uint32_t workgroup_size; // Threads per workgroup
|
||||
uint32_t num_workgroups; // Total workgroups
|
||||
uint32_t shared_mem_bytes; // Shared memory per workgroup
|
||||
} LuxFHEGPUKernelConfig;
|
||||
|
||||
// Get optimal kernel config for hardware
|
||||
LuxFHEGPUKernelConfig luxfhe_compare_get_kernel_config(
|
||||
LuxFHEEngine engine,
|
||||
uint32_t num_bits,
|
||||
uint32_t batch_size
|
||||
);
|
||||
|
||||
// Launch Kogge-Stone parallel prefix kernel
|
||||
// Computes (G, P) pairs: Generate and Propagate signals for comparison
|
||||
// G[i] = a[i] > b[i], P[i] = a[i] == b[i]
|
||||
int luxfhe_gpu_kogge_stone_prefix(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEGPUKernelConfig* config,
|
||||
LuxFHEInteger* a_batch,
|
||||
LuxFHEInteger* b_batch,
|
||||
uint32_t batch_size,
|
||||
LuxFHECiphertext* results
|
||||
);
|
||||
|
||||
// Early termination hint computation
|
||||
// Returns encrypted hint indicating MSB difference position
|
||||
LuxFHECiphertext luxfhe_gpu_early_term_hint(
|
||||
LuxFHEEngine engine,
|
||||
LuxFHECompareContext ctx,
|
||||
LuxFHEInteger a,
|
||||
LuxFHEInteger b
|
||||
);
|
||||
|
||||
#endif // LUXFHE_GPU_ENABLED
|
||||
|
||||
// =============================================================================
|
||||
// Statistics and Profiling
|
||||
// =============================================================================
|
||||
|
||||
typedef struct {
|
||||
uint64_t total_comparisons; // Total comparison operations
|
||||
uint64_t gpu_kernel_launches; // GPU kernel invocations
|
||||
uint64_t early_terminations; // Early termination opportunities
|
||||
double avg_depth_reduction; // Average depth reduction vs serial
|
||||
double avg_latency_us; // Average latency in microseconds
|
||||
double throughput_ops_sec; // Operations per second
|
||||
} LuxFHECompareStats;
|
||||
|
||||
// Get comparison statistics
|
||||
LuxFHECompareStats luxfhe_compare_get_stats(LuxFHECompareContext ctx);
|
||||
|
||||
// Reset statistics
|
||||
void luxfhe_compare_reset_stats(LuxFHECompareContext ctx);
|
||||
|
||||
// =============================================================================
|
||||
// Solidity Interface Specification
|
||||
// =============================================================================
|
||||
|
||||
/*
|
||||
* Solidity FHE Precompile Interface for Comparison Operations
|
||||
*
|
||||
* Address: 0x0100 (FHE Precompile Base)
|
||||
*
|
||||
* Function Selectors:
|
||||
* lt(bytes32 a, bytes32 b) -> 0x1234... // Returns encrypted bool
|
||||
* le(bytes32 a, bytes32 b) -> 0x2345...
|
||||
* gt(bytes32 a, bytes32 b) -> 0x3456...
|
||||
* ge(bytes32 a, bytes32 b) -> 0x4567...
|
||||
* eq(bytes32 a, bytes32 b) -> 0x5678...
|
||||
* ne(bytes32 a, bytes32 b) -> 0x6789...
|
||||
* min(bytes32 a, bytes32 b) -> 0x789a... // Returns encrypted uint
|
||||
* max(bytes32 a, bytes32 b) -> 0x89ab...
|
||||
*
|
||||
* Input Format:
|
||||
* - bytes32 handle: Ciphertext handle in global ciphertext store
|
||||
* - Type info encoded in handle's upper bits
|
||||
*
|
||||
* Gas Costs (approximate):
|
||||
* - lt/gt/le/ge: 50000 + 500 * log2(bit_width) gas
|
||||
* - eq/ne: 30000 + 300 * log2(bit_width) gas
|
||||
* - min/max: 80000 + 800 * log2(bit_width) gas
|
||||
*
|
||||
* Example Solidity Usage:
|
||||
*
|
||||
* import "fhe/FHE.sol";
|
||||
*
|
||||
* contract Auction {
|
||||
* euint256 highestBid;
|
||||
* eaddress highestBidder;
|
||||
*
|
||||
* function bid(einput encryptedBid, bytes calldata proof) external {
|
||||
* euint256 bidAmount = FHE.asEuint256(encryptedBid, proof);
|
||||
*
|
||||
* // Encrypted comparison: is new bid higher?
|
||||
* ebool isHigher = FHE.gt(bidAmount, highestBid);
|
||||
*
|
||||
* // Oblivious selection: update if higher
|
||||
* highestBid = FHE.select(isHigher, bidAmount, highestBid);
|
||||
* highestBidder = FHE.select(isHigher,
|
||||
* FHE.asEaddress(msg.sender),
|
||||
* highestBidder);
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// C++ API (when compiled as C++)
|
||||
// =============================================================================
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
|
||||
namespace luxfhe {
|
||||
namespace compare {
|
||||
|
||||
// Encrypted bit type (forward declaration)
|
||||
class EncryptedBit;
|
||||
class EncryptedInteger;
|
||||
|
||||
// =============================================================================
|
||||
// Kogge-Stone Parallel Prefix Tree
|
||||
// =============================================================================
|
||||
|
||||
// Generate-Propagate pair for comparison
|
||||
// G = a > b (generate: this position determines result)
|
||||
// P = a == b (propagate: defer to higher position)
|
||||
struct GPPair {
|
||||
std::shared_ptr<EncryptedBit> G; // Generate signal
|
||||
std::shared_ptr<EncryptedBit> P; // Propagate signal
|
||||
};
|
||||
|
||||
// Kogge-Stone operator: combines two GP pairs
|
||||
// (G1, P1) o (G0, P0) = (G1 OR (P1 AND G0), P1 AND P0)
|
||||
class KoggeStoneOp {
|
||||
public:
|
||||
virtual ~KoggeStoneOp() = default;
|
||||
|
||||
// Combine two GP pairs homomorphically
|
||||
virtual GPPair combine(const GPPair& high, const GPPair& low) = 0;
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// ParallelCompare Class
|
||||
// =============================================================================
|
||||
|
||||
class ParallelCompare {
|
||||
public:
|
||||
// Configuration
|
||||
struct Config {
|
||||
uint32_t num_bits = 256;
|
||||
uint32_t block_size = 4;
|
||||
bool use_gpu = true;
|
||||
bool early_termination = true;
|
||||
uint32_t batch_size = 1024;
|
||||
};
|
||||
|
||||
// Constructor
|
||||
explicit ParallelCompare(Config config);
|
||||
~ParallelCompare();
|
||||
|
||||
// Disable copy, enable move
|
||||
ParallelCompare(const ParallelCompare&) = delete;
|
||||
ParallelCompare& operator=(const ParallelCompare&) = delete;
|
||||
ParallelCompare(ParallelCompare&&) noexcept;
|
||||
ParallelCompare& operator=(ParallelCompare&&) noexcept;
|
||||
|
||||
// =========================================================================
|
||||
// Core Comparison (Kogge-Stone Algorithm)
|
||||
// =========================================================================
|
||||
|
||||
// Less-than comparison using parallel prefix
|
||||
// Depth: O(log n) vs O(n) for serial
|
||||
std::shared_ptr<EncryptedBit> lessThan(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// Equality using parallel XOR-reduction
|
||||
std::shared_ptr<EncryptedBit> equal(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Derived Operations
|
||||
// =========================================================================
|
||||
|
||||
std::shared_ptr<EncryptedBit> lessEqual(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
std::shared_ptr<EncryptedBit> greaterThan(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
std::shared_ptr<EncryptedBit> greaterEqual(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
std::shared_ptr<EncryptedBit> notEqual(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Selection Operations
|
||||
// =========================================================================
|
||||
|
||||
// Oblivious minimum: returns a if a < b, else b
|
||||
std::shared_ptr<EncryptedInteger> min(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// Oblivious maximum
|
||||
std::shared_ptr<EncryptedInteger> max(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// Oblivious selection: returns a if cond is true, else b
|
||||
std::shared_ptr<EncryptedInteger> select(
|
||||
const EncryptedBit& cond,
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Batch Operations (GPU-Optimized)
|
||||
// =========================================================================
|
||||
|
||||
std::vector<std::shared_ptr<EncryptedBit>> batchLessThan(
|
||||
const std::vector<EncryptedInteger>& a_vec,
|
||||
const std::vector<EncryptedInteger>& b_vec
|
||||
);
|
||||
|
||||
std::vector<std::shared_ptr<EncryptedBit>> batchEqual(
|
||||
const std::vector<EncryptedInteger>& a_vec,
|
||||
const std::vector<EncryptedInteger>& b_vec
|
||||
);
|
||||
|
||||
// =========================================================================
|
||||
// Scalar Comparisons (Optimized)
|
||||
// =========================================================================
|
||||
|
||||
std::shared_ptr<EncryptedBit> lessThanScalar(
|
||||
const EncryptedInteger& a,
|
||||
const std::vector<uint8_t>& b_plaintext
|
||||
);
|
||||
|
||||
std::shared_ptr<EncryptedBit> equalScalar(
|
||||
const EncryptedInteger& a,
|
||||
const std::vector<uint8_t>& b_plaintext
|
||||
);
|
||||
|
||||
private:
|
||||
class Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
|
||||
// Kogge-Stone tree building
|
||||
std::vector<GPPair> buildGPPairs(
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b
|
||||
);
|
||||
|
||||
// Parallel prefix computation
|
||||
GPPair parallelPrefix(const std::vector<GPPair>& gp_pairs);
|
||||
|
||||
// GPU kernel dispatch
|
||||
void dispatchGPUKernel(
|
||||
const std::vector<GPPair>& gp_pairs,
|
||||
uint32_t stage,
|
||||
uint32_t stride
|
||||
);
|
||||
};
|
||||
|
||||
// =============================================================================
|
||||
// GPU Kernel Launcher
|
||||
// =============================================================================
|
||||
|
||||
class GPUCompareKernel {
|
||||
public:
|
||||
struct LaunchParams {
|
||||
uint32_t workgroup_size = 256;
|
||||
uint32_t num_workgroups = 0; // 0 = auto
|
||||
size_t shared_mem_bytes = 0;
|
||||
};
|
||||
|
||||
virtual ~GPUCompareKernel() = default;
|
||||
|
||||
// Launch Kogge-Stone stage kernel
|
||||
// Each stage computes GP pairs with distance 2^stage
|
||||
virtual void launchKoggeStoneStage(
|
||||
const LaunchParams& params,
|
||||
std::vector<GPPair>& gp_pairs,
|
||||
uint32_t stage
|
||||
) = 0;
|
||||
|
||||
// Launch XOR-reduction kernel for equality
|
||||
virtual void launchXorReduction(
|
||||
const LaunchParams& params,
|
||||
std::vector<std::shared_ptr<EncryptedBit>>& xor_bits
|
||||
) = 0;
|
||||
|
||||
// Launch oblivious selection kernel
|
||||
virtual void launchSelect(
|
||||
const LaunchParams& params,
|
||||
const EncryptedBit& cond,
|
||||
const EncryptedInteger& a,
|
||||
const EncryptedInteger& b,
|
||||
EncryptedInteger& result
|
||||
) = 0;
|
||||
};
|
||||
|
||||
// Factory for platform-specific kernel
|
||||
std::unique_ptr<GPUCompareKernel> createGPUCompareKernel();
|
||||
|
||||
} // namespace compare
|
||||
} // namespace luxfhe
|
||||
|
||||
#endif // __cplusplus
|
||||
|
||||
#endif // LUXFHE_ENCRYPTED_COMPARE_H
|
||||
+1151
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,242 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
//
|
||||
// C bridge header for OpenFHE BinFHE (TFHE) operations
|
||||
// This header defines the C interface that Go calls via CGO
|
||||
|
||||
#ifndef TFHE_BRIDGE_H
|
||||
#define TFHE_BRIDGE_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
// Opaque handle types
|
||||
typedef void* TfheContext;
|
||||
typedef void* TfheSecretKey;
|
||||
typedef void* TfhePublicKey;
|
||||
typedef void* TfheCiphertext;
|
||||
typedef void* TfheBootstrapKey;
|
||||
|
||||
// Security levels matching OpenFHE BINFHE_PARAMSET
|
||||
typedef enum {
|
||||
TFHE_TOY = 0, // ~16-bit security (testing only)
|
||||
TFHE_STD128 = 1, // 128-bit security (CGGI/GINX)
|
||||
TFHE_STD128_AP = 2, // 128-bit security (AP variant)
|
||||
TFHE_STD128_LMKCDEY = 3, // 128-bit security (LMKCDEY - fastest)
|
||||
TFHE_STD192 = 4, // 192-bit security
|
||||
TFHE_STD256 = 5 // 256-bit security
|
||||
} TfheSecurityLevel;
|
||||
|
||||
// Bootstrapping method
|
||||
typedef enum {
|
||||
TFHE_METHOD_GINX = 0, // GINX (default)
|
||||
TFHE_METHOD_AP = 1, // AP variant
|
||||
TFHE_METHOD_LMKCDEY = 2 // LMKCDEY (fastest)
|
||||
} TfheMethod;
|
||||
|
||||
// =============================================================================
|
||||
// Context Management
|
||||
// =============================================================================
|
||||
|
||||
// Create a new TFHE context with specified security level and method
|
||||
TfheContext tfhe_context_new(TfheSecurityLevel level, TfheMethod method);
|
||||
|
||||
// Free a TFHE context
|
||||
void tfhe_context_free(TfheContext ctx);
|
||||
|
||||
// Get version of the bridge library
|
||||
uint32_t tfhe_version(void);
|
||||
|
||||
// =============================================================================
|
||||
// Key Generation
|
||||
// =============================================================================
|
||||
|
||||
// Generate a secret key
|
||||
TfheSecretKey tfhe_keygen(TfheContext ctx);
|
||||
|
||||
// Free a secret key
|
||||
void tfhe_secretkey_free(TfheSecretKey sk);
|
||||
|
||||
// Generate bootstrap key (required for gate evaluation)
|
||||
int tfhe_bootstrap_keygen(TfheContext ctx, TfheSecretKey sk);
|
||||
|
||||
// Generate key switching key
|
||||
int tfhe_keyswitch_keygen(TfheContext ctx, TfheSecretKey sk);
|
||||
|
||||
// Check if bootstrap key is generated
|
||||
bool tfhe_has_bootstrap_key(TfheContext ctx);
|
||||
|
||||
// =============================================================================
|
||||
// Public Key Generation
|
||||
// =============================================================================
|
||||
|
||||
// Generate a public key from secret key
|
||||
TfhePublicKey tfhe_public_keygen(TfheContext ctx, TfheSecretKey sk);
|
||||
|
||||
// Free a public key
|
||||
void tfhe_public_key_free(TfhePublicKey pk);
|
||||
|
||||
// Serialize public key to bytes
|
||||
int tfhe_publickey_serialize(TfhePublicKey pk, uint8_t** out, size_t* out_len);
|
||||
|
||||
// Deserialize public key from bytes
|
||||
TfhePublicKey tfhe_publickey_deserialize(TfheContext ctx, const uint8_t* data, size_t len);
|
||||
|
||||
// =============================================================================
|
||||
// Encryption / Decryption (Boolean)
|
||||
// =============================================================================
|
||||
|
||||
// Encrypt a boolean value (0 or 1)
|
||||
TfheCiphertext tfhe_encrypt(TfheContext ctx, TfheSecretKey sk, int value);
|
||||
|
||||
// Encrypt a bit (alias for tfhe_encrypt)
|
||||
TfheCiphertext tfhe_encrypt_bit(TfheContext ctx, TfheSecretKey sk, int value);
|
||||
|
||||
// Encrypt a bit with public key
|
||||
TfheCiphertext tfhe_encrypt_bit_public(TfheContext ctx, TfhePublicKey pk, int value);
|
||||
|
||||
// Decrypt a ciphertext to boolean
|
||||
int tfhe_decrypt(TfheContext ctx, TfheSecretKey sk, TfheCiphertext ct);
|
||||
|
||||
// Decrypt a bit (alias for tfhe_decrypt)
|
||||
int tfhe_decrypt_bit(TfheContext ctx, TfheSecretKey sk, TfheCiphertext ct);
|
||||
|
||||
// Free a ciphertext
|
||||
void tfhe_ciphertext_free(TfheCiphertext ct);
|
||||
|
||||
// Free a secret key (alias for backward compatibility)
|
||||
void tfhe_secret_key_free(TfheSecretKey sk);
|
||||
|
||||
// Clone a ciphertext
|
||||
TfheCiphertext tfhe_ciphertext_clone(TfheCiphertext ct);
|
||||
|
||||
// =============================================================================
|
||||
// Boolean Gates (with bootstrapping)
|
||||
// =============================================================================
|
||||
|
||||
TfheCiphertext tfhe_and(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_or(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_xor(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_nand(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_nor(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_xnor(TfheContext ctx, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
TfheCiphertext tfhe_not(TfheContext ctx, TfheCiphertext ct);
|
||||
TfheCiphertext tfhe_mux(TfheContext ctx, TfheCiphertext sel, TfheCiphertext ct1, TfheCiphertext ct2);
|
||||
|
||||
// =============================================================================
|
||||
// Integer Operations (radix representation)
|
||||
// =============================================================================
|
||||
|
||||
// Integer ciphertext handle
|
||||
typedef void* TfheInteger;
|
||||
|
||||
// Integer types
|
||||
typedef enum {
|
||||
TFHE_UINT4 = 4,
|
||||
TFHE_UINT8 = 8,
|
||||
TFHE_UINT16 = 16,
|
||||
TFHE_UINT32 = 32,
|
||||
TFHE_UINT64 = 64,
|
||||
TFHE_UINT128 = 128,
|
||||
TFHE_UINT160 = 160,
|
||||
TFHE_UINT256 = 256
|
||||
} TfheIntType;
|
||||
|
||||
// Encrypt an integer
|
||||
TfheInteger tfhe_encrypt_integer(TfheContext ctx, TfheSecretKey sk, int64_t value, int bitLen);
|
||||
|
||||
// Encrypt an integer with public key
|
||||
TfheInteger tfhe_encrypt_integer_public(TfheContext ctx, TfhePublicKey pk, int64_t value, int bitLen);
|
||||
|
||||
// Decrypt an integer
|
||||
int64_t tfhe_decrypt_integer(TfheContext ctx, TfheSecretKey sk, TfheInteger ct);
|
||||
|
||||
// Free an integer ciphertext
|
||||
void tfhe_integer_free(TfheInteger ct);
|
||||
|
||||
// Clone an integer ciphertext
|
||||
TfheInteger tfhe_integer_clone(TfheInteger ct);
|
||||
|
||||
// Get the type of an integer ciphertext
|
||||
TfheIntType tfhe_integer_type(TfheInteger ct);
|
||||
|
||||
// =============================================================================
|
||||
// Integer Arithmetic
|
||||
// =============================================================================
|
||||
|
||||
TfheInteger tfhe_add(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_sub(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_neg(TfheContext ctx, TfheInteger a);
|
||||
TfheInteger tfhe_add_scalar(TfheContext ctx, TfheInteger a, int64_t scalar);
|
||||
TfheInteger tfhe_sub_scalar(TfheContext ctx, TfheInteger a, int64_t scalar);
|
||||
TfheInteger tfhe_mul_scalar(TfheContext ctx, TfheInteger a, int64_t scalar);
|
||||
|
||||
// =============================================================================
|
||||
// Integer Comparisons
|
||||
// =============================================================================
|
||||
|
||||
TfheCiphertext tfhe_eq(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheCiphertext tfhe_ne(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheCiphertext tfhe_lt(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheCiphertext tfhe_le(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheCiphertext tfhe_gt(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheCiphertext tfhe_ge(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_min(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_max(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
|
||||
// =============================================================================
|
||||
// Integer Bitwise Operations
|
||||
// =============================================================================
|
||||
|
||||
TfheInteger tfhe_bitwise_and(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_bitwise_or(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_bitwise_xor(TfheContext ctx, TfheInteger a, TfheInteger b);
|
||||
TfheInteger tfhe_bitwise_not(TfheContext ctx, TfheInteger a);
|
||||
TfheInteger tfhe_shl(TfheContext ctx, TfheInteger a, int bits);
|
||||
TfheInteger tfhe_shr(TfheContext ctx, TfheInteger a, int bits);
|
||||
|
||||
// =============================================================================
|
||||
// Control Flow
|
||||
// =============================================================================
|
||||
|
||||
TfheInteger tfhe_select(TfheContext ctx, TfheCiphertext cond, TfheInteger if_true, TfheInteger if_false);
|
||||
TfheInteger tfhe_cast_to(TfheContext ctx, TfheInteger a, int target_bitlen);
|
||||
|
||||
// =============================================================================
|
||||
// Serialization
|
||||
// =============================================================================
|
||||
|
||||
// Serialize ciphertext to bytes (returns malloc'd buffer, caller must free)
|
||||
uint8_t* tfhe_serialize_ciphertext(TfheContext ctx, TfheCiphertext ct, size_t* out_len);
|
||||
|
||||
// Deserialize ciphertext from bytes
|
||||
TfheCiphertext tfhe_deserialize_ciphertext(TfheContext ctx, const uint8_t* data, size_t len);
|
||||
|
||||
// Serialize secret key to bytes (returns malloc'd buffer, caller must free)
|
||||
uint8_t* tfhe_serialize_secret_key(TfheContext ctx, TfheSecretKey sk, size_t* out_len);
|
||||
|
||||
// Deserialize secret key from bytes
|
||||
TfheSecretKey tfhe_deserialize_secret_key(TfheContext ctx, const uint8_t* data, size_t len);
|
||||
|
||||
// Serialize public key to bytes (returns malloc'd buffer, caller must free)
|
||||
uint8_t* tfhe_serialize_public_key(TfheContext ctx, TfhePublicKey pk, size_t* out_len);
|
||||
|
||||
// Deserialize public key from bytes
|
||||
TfhePublicKey tfhe_deserialize_public_key(TfheContext ctx, const uint8_t* data, size_t len);
|
||||
|
||||
// Serialize integer ciphertext to bytes (returns malloc'd buffer, caller must free)
|
||||
uint8_t* tfhe_serialize_integer(TfheContext ctx, TfheInteger ct, size_t* out_len);
|
||||
|
||||
// Deserialize integer ciphertext from bytes
|
||||
TfheInteger tfhe_deserialize_integer(TfheContext ctx, const uint8_t* data, size_t len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif // TFHE_BRIDGE_H
|
||||
+637
@@ -0,0 +1,637 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// ========== Comparison Operations ==========
|
||||
|
||||
// Eq returns 1 if a == b, 0 otherwise
|
||||
func (eval *IntegerEvaluator) Eq(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
// Compare each block, AND all results
|
||||
numBlocks := len(a.blocks)
|
||||
var result *Ciphertext
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
// Check if blocks are equal using XOR and NOT
|
||||
// a[i] == b[i] iff (a[i] XOR b[i]) == 0
|
||||
xored, err := eval.xorBlocks(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d xor: %w", i, err)
|
||||
}
|
||||
|
||||
// Check if xor result is zero
|
||||
isZero, err := eval.isZeroBlock(xored)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d isZero: %w", i, err)
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = isZero
|
||||
} else {
|
||||
// AND with previous result
|
||||
result, err = eval.boolEval.AND(result, isZero)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert boolean result to RadixCiphertext
|
||||
return eval.boolToRadix(result, FheBool)
|
||||
}
|
||||
|
||||
// Ne returns 1 if a != b, 0 otherwise
|
||||
func (eval *IntegerEvaluator) Ne(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
eq, err := eval.Eq(a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Not(eq)
|
||||
}
|
||||
|
||||
// Lt returns 1 if a < b, 0 otherwise (unsigned comparison)
|
||||
func (eval *IntegerEvaluator) Lt(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
// Compare from MSB to LSB
|
||||
// a < b iff there exists i such that a[i] < b[i] and for all j > i, a[j] == b[j]
|
||||
numBlocks := len(a.blocks)
|
||||
|
||||
var isLess *Ciphertext // Accumulated "definitely less" flag
|
||||
var isEqual *Ciphertext // Accumulated "still equal" flag
|
||||
|
||||
// Start from MSB
|
||||
for i := numBlocks - 1; i >= 0; i-- {
|
||||
blockLt, err := eval.blockLt(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d lt: %w", i, err)
|
||||
}
|
||||
|
||||
blockEq, err := eval.blockEq(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d eq: %w", i, err)
|
||||
}
|
||||
|
||||
if isLess == nil {
|
||||
isLess = blockLt
|
||||
isEqual = blockEq
|
||||
} else {
|
||||
// isLess = isLess OR (isEqual AND blockLt)
|
||||
eqAndLt, err := eval.boolEval.AND(isEqual, blockLt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
isLess, err = eval.boolEval.OR(isLess, eqAndLt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// isEqual = isEqual AND blockEq
|
||||
isEqual, err = eval.boolEval.AND(isEqual, blockEq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eval.boolToRadix(isLess, FheBool)
|
||||
}
|
||||
|
||||
// Le returns 1 if a <= b, 0 otherwise
|
||||
func (eval *IntegerEvaluator) Le(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
gt, err := eval.Gt(a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Not(gt)
|
||||
}
|
||||
|
||||
// Gt returns 1 if a > b, 0 otherwise
|
||||
func (eval *IntegerEvaluator) Gt(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
// a > b iff b < a
|
||||
return eval.Lt(b, a)
|
||||
}
|
||||
|
||||
// Ge returns 1 if a >= b, 0 otherwise
|
||||
func (eval *IntegerEvaluator) Ge(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
lt, err := eval.Lt(a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Not(lt)
|
||||
}
|
||||
|
||||
// Min returns the minimum of a and b
|
||||
func (eval *IntegerEvaluator) Min(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
// min(a, b) = a < b ? a : b
|
||||
isLt, err := eval.Lt(a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Select(isLt, a, b)
|
||||
}
|
||||
|
||||
// Max returns the maximum of a and b
|
||||
func (eval *IntegerEvaluator) Max(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
// max(a, b) = a > b ? a : b
|
||||
isGt, err := eval.Gt(a, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Select(isGt, a, b)
|
||||
}
|
||||
|
||||
// ========== Bitwise Operations ==========
|
||||
|
||||
// And performs bitwise AND on two radix integers
|
||||
func (eval *IntegerEvaluator) And(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
anded, err := eval.andBlocks(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d: %w", i, err)
|
||||
}
|
||||
resultBlocks[i] = anded
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Or performs bitwise OR on two radix integers
|
||||
func (eval *IntegerEvaluator) Or(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
ored, err := eval.orBlocks(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d: %w", i, err)
|
||||
}
|
||||
resultBlocks[i] = ored
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Xor performs bitwise XOR on two radix integers
|
||||
func (eval *IntegerEvaluator) Xor(a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
xored, err := eval.xorBlocks(a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d: %w", i, err)
|
||||
}
|
||||
resultBlocks[i] = xored
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Not performs bitwise NOT on a radix integer
|
||||
func (eval *IntegerEvaluator) Not(a *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
notted, err := eval.notBlock(a.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d: %w", i, err)
|
||||
}
|
||||
resultBlocks[i] = notted
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ========== Shift Operations ==========
|
||||
|
||||
// Shl performs left shift by a scalar amount
|
||||
func (eval *IntegerEvaluator) Shl(a *RadixCiphertext, shift int) (*RadixCiphertext, error) {
|
||||
if shift < 0 {
|
||||
return nil, fmt.Errorf("negative shift amount: %d", shift)
|
||||
}
|
||||
if shift == 0 {
|
||||
return eval.copy(a), nil
|
||||
}
|
||||
|
||||
totalBits := a.NumBits()
|
||||
if shift >= totalBits {
|
||||
// Shift by more than width returns 0
|
||||
return eval.zeroRadix(a.fheType)
|
||||
}
|
||||
|
||||
// Calculate block-level and intra-block shifts
|
||||
blockShift := shift / a.blockBits
|
||||
bitShift := shift % a.blockBits
|
||||
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
// Initialize lower blocks to zero
|
||||
for i := 0; i < blockShift && i < numBlocks; i++ {
|
||||
zero, err := eval.shortEval.ScalarAdd(a.blocks[0], 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Actually encrypt 0
|
||||
resultBlocks[i] = zero
|
||||
}
|
||||
|
||||
// Shift remaining blocks
|
||||
for i := blockShift; i < numBlocks; i++ {
|
||||
srcIdx := i - blockShift
|
||||
if bitShift == 0 {
|
||||
resultBlocks[i] = &ShortInt{
|
||||
ct: a.blocks[srcIdx].ct.CopyNew(),
|
||||
msgBits: a.blocks[srcIdx].msgBits,
|
||||
msgSpace: a.blocks[srcIdx].msgSpace,
|
||||
}
|
||||
} else {
|
||||
// Need intra-block shift with carry from lower block
|
||||
shifted, err := eval.shortEval.ScalarMul(a.blocks[srcIdx], 1<<bitShift)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resultBlocks[i] = shifted
|
||||
}
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Shr performs right shift by a scalar amount
|
||||
func (eval *IntegerEvaluator) Shr(a *RadixCiphertext, shift int) (*RadixCiphertext, error) {
|
||||
if shift < 0 {
|
||||
return nil, fmt.Errorf("negative shift amount: %d", shift)
|
||||
}
|
||||
if shift == 0 {
|
||||
return eval.copy(a), nil
|
||||
}
|
||||
|
||||
totalBits := a.NumBits()
|
||||
if shift >= totalBits {
|
||||
return eval.zeroRadix(a.fheType)
|
||||
}
|
||||
|
||||
blockShift := shift / a.blockBits
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
// Shift blocks down
|
||||
for i := 0; i < numBlocks-blockShift; i++ {
|
||||
srcIdx := i + blockShift
|
||||
resultBlocks[i] = &ShortInt{
|
||||
ct: a.blocks[srcIdx].ct.CopyNew(),
|
||||
msgBits: a.blocks[srcIdx].msgBits,
|
||||
msgSpace: a.blocks[srcIdx].msgSpace,
|
||||
}
|
||||
}
|
||||
|
||||
// Zero upper blocks
|
||||
for i := numBlocks - blockShift; i < numBlocks; i++ {
|
||||
zero, _ := eval.shortEval.ScalarMul(a.blocks[0], 0)
|
||||
resultBlocks[i] = zero
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ========== Conditional Selection ==========
|
||||
|
||||
// Select returns a if condition is true, b otherwise
|
||||
// condition should be an encrypted boolean (RadixCiphertext with FheBool type)
|
||||
func (eval *IntegerEvaluator) Select(cond, a, b *RadixCiphertext) (*RadixCiphertext, error) {
|
||||
if a.fheType != b.fheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.fheType, b.fheType)
|
||||
}
|
||||
|
||||
// Get condition as boolean ciphertext
|
||||
if len(cond.blocks) == 0 {
|
||||
return nil, fmt.Errorf("empty condition")
|
||||
}
|
||||
condBool := &Ciphertext{cond.blocks[0].ct}
|
||||
|
||||
numBlocks := len(a.blocks)
|
||||
resultBlocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
selected, err := eval.selectBlock(condBool, a.blocks[i], b.blocks[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("block %d: %w", i, err)
|
||||
}
|
||||
resultBlocks[i] = selected
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: resultBlocks,
|
||||
blockBits: a.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: a.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
// xorBlocks XORs two shortint blocks
|
||||
func (eval *IntegerEvaluator) xorBlocks(a, b *ShortInt) (*ShortInt, error) {
|
||||
// Use LUT for XOR on each possible pair
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace*msgSpace))
|
||||
|
||||
// Create XOR LUT (depends on both a and b encoded in single input)
|
||||
// This is a simplified approach - proper implementation would use tensor product
|
||||
sum := eval.shortEval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
xorLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
// Decode a and b from sum
|
||||
combined := int((x + 1) * float64(msgSpace*msgSpace) / 2)
|
||||
aVal := combined / msgSpace
|
||||
bVal := combined % msgSpace
|
||||
result := aVal ^ bVal
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(sum, &xorLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// andBlocks ANDs two shortint blocks
|
||||
func (eval *IntegerEvaluator) andBlocks(a, b *ShortInt) (*ShortInt, error) {
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace*msgSpace))
|
||||
|
||||
sum := eval.shortEval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
andLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
combined := int((x + 1) * float64(msgSpace*msgSpace) / 2)
|
||||
aVal := combined / msgSpace
|
||||
bVal := combined % msgSpace
|
||||
result := aVal & bVal
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(sum, &andLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// orBlocks ORs two shortint blocks
|
||||
func (eval *IntegerEvaluator) orBlocks(a, b *ShortInt) (*ShortInt, error) {
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace*msgSpace))
|
||||
|
||||
sum := eval.shortEval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
orLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
combined := int((x + 1) * float64(msgSpace*msgSpace) / 2)
|
||||
aVal := combined / msgSpace
|
||||
bVal := combined % msgSpace
|
||||
result := aVal | bVal
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(sum, &orLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// notBlock performs bitwise NOT on a shortint block
|
||||
func (eval *IntegerEvaluator) notBlock(a *ShortInt) (*ShortInt, error) {
|
||||
msgSpace := a.msgSpace
|
||||
mask := msgSpace - 1
|
||||
|
||||
// NOT via LUT
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace))
|
||||
|
||||
notLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
val := int((x + 1) * float64(msgSpace) / 2)
|
||||
if val >= msgSpace {
|
||||
val = msgSpace - 1
|
||||
}
|
||||
result := (^val) & mask
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(a.ct, ¬LUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isZeroBlock returns 1 if block is 0, else 0
|
||||
func (eval *IntegerEvaluator) isZeroBlock(a *ShortInt) (*Ciphertext, error) {
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / 8.0)
|
||||
|
||||
isZeroLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
val := int((x + 1) * float64(msgSpace) / 2)
|
||||
if val == 0 {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(a.ct, &isZeroLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Ciphertext{resultCt}, nil
|
||||
}
|
||||
|
||||
// blockLt returns 1 if a < b, else 0 (for single blocks)
|
||||
func (eval *IntegerEvaluator) blockLt(a, b *ShortInt) (*Ciphertext, error) {
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace*msgSpace))
|
||||
|
||||
sum := eval.shortEval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
ltLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
combined := int((x + 1) * float64(msgSpace*msgSpace) / 2)
|
||||
aVal := combined / msgSpace
|
||||
bVal := combined % msgSpace
|
||||
if aVal < bVal {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(sum, <LUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Ciphertext{resultCt}, nil
|
||||
}
|
||||
|
||||
// blockEq returns 1 if a == b, else 0 (for single blocks)
|
||||
func (eval *IntegerEvaluator) blockEq(a, b *ShortInt) (*Ciphertext, error) {
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.fheParams.QBR()) / float64(2*msgSpace*msgSpace))
|
||||
|
||||
sum := eval.shortEval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
eqLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
combined := int((x + 1) * float64(msgSpace*msgSpace) / 2)
|
||||
aVal := combined / msgSpace
|
||||
bVal := combined % msgSpace
|
||||
if aVal == bVal {
|
||||
return 1.0
|
||||
}
|
||||
return -1.0
|
||||
}, scale, eval.shortEval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.shortEval.bootstrap(sum, &eqLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Ciphertext{resultCt}, nil
|
||||
}
|
||||
|
||||
// selectBlock selects between two blocks based on condition
|
||||
func (eval *IntegerEvaluator) selectBlock(cond *Ciphertext, a, b *ShortInt) (*ShortInt, error) {
|
||||
// Use MUX: cond ? a : b
|
||||
// For shortints, we need a custom LUT approach
|
||||
// Simplified: use boolean MUX bit by bit (slow but correct)
|
||||
|
||||
// For now, delegate to the boolean MUX
|
||||
// This is a placeholder - proper implementation needs tensor product
|
||||
resultCt, err := eval.boolEval.MUX(cond, &Ciphertext{a.ct}, &Ciphertext{b.ct})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt.Ciphertext,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// boolToRadix converts a boolean ciphertext to RadixCiphertext
|
||||
func (eval *IntegerEvaluator) boolToRadix(ct *Ciphertext, t FheUintType) (*RadixCiphertext, error) {
|
||||
return &RadixCiphertext{
|
||||
blocks: []*ShortInt{{
|
||||
ct: ct.Ciphertext,
|
||||
msgBits: eval.params.blockBits,
|
||||
msgSpace: 1 << eval.params.blockBits,
|
||||
}},
|
||||
blockBits: eval.params.blockBits,
|
||||
numBlocks: 1,
|
||||
fheType: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// zeroRadix returns an encrypted zero
|
||||
func (eval *IntegerEvaluator) zeroRadix(t FheUintType) (*RadixCiphertext, error) {
|
||||
numBlocks := (t.NumBits() + eval.params.blockBits - 1) / eval.params.blockBits
|
||||
blocks := make([]*ShortInt, numBlocks)
|
||||
|
||||
for i := 0; i < numBlocks; i++ {
|
||||
zero, err := eval.shortEval.ScalarMul(
|
||||
&ShortInt{
|
||||
ct: rlwe.NewCiphertext(eval.params.fheParams.paramsLWE, 1, eval.params.fheParams.paramsLWE.MaxLevel()),
|
||||
msgBits: eval.params.blockBits,
|
||||
msgSpace: 1 << eval.params.blockBits,
|
||||
}, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
blocks[i] = zero
|
||||
}
|
||||
|
||||
return &RadixCiphertext{
|
||||
blocks: blocks,
|
||||
blockBits: eval.params.blockBits,
|
||||
numBlocks: numBlocks,
|
||||
fheType: t,
|
||||
}, nil
|
||||
}
|
||||
+1102
File diff suppressed because it is too large
Load Diff
+704
@@ -0,0 +1,704 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package fhe provides lazy carry propagation for encrypted integers.
|
||||
//
|
||||
// Key Innovation: Defer carry propagation to reduce expensive programmable bootstrapping (PBS).
|
||||
//
|
||||
// - Traditional: Propagate carry after every add (expensive PBS per limb)
|
||||
// - Lazy: Accumulate carries, propagate only when needed
|
||||
// - Each limb stores value in extended range [0, 2^k * max_ops)
|
||||
// - Propagate when: (1) overflow imminent, (2) comparison needed, (3) explicit request
|
||||
//
|
||||
// For euint256 with 8 x 32-bit limbs:
|
||||
// - Traditional add: 7 PBS for carry chain
|
||||
// - Lazy add (up to 16 ops): 0 PBS
|
||||
// - Lazy propagate: 7 PBS (amortized over 16 ops)
|
||||
//
|
||||
// This provides 10-16x reduction in PBS operations for arithmetic-heavy workloads.
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// LazyCarryConfig holds configuration for lazy carry propagation.
|
||||
type LazyCarryConfig struct {
|
||||
// MaxOpsBeforePropagate is the maximum number of additions before forcing propagation.
|
||||
// Higher values amortize PBS cost better but increase per-limb noise.
|
||||
// Recommended: 8-16 for 128-bit security, 4-8 for 256-bit security.
|
||||
MaxOpsBeforePropagate int
|
||||
|
||||
// OverflowMargin is the safety margin before limb overflow.
|
||||
// When remaining headroom < OverflowMargin, propagation is triggered.
|
||||
OverflowMargin uint64
|
||||
|
||||
// PropagateOnCompare forces propagation before any comparison operation.
|
||||
// Must be true for correct comparison results.
|
||||
PropagateOnCompare bool
|
||||
}
|
||||
|
||||
// DefaultLazyCarryConfig returns sensible defaults for EVM workloads.
|
||||
func DefaultLazyCarryConfig() LazyCarryConfig {
|
||||
return LazyCarryConfig{
|
||||
MaxOpsBeforePropagate: 16,
|
||||
OverflowMargin: 1 << 16, // 64K safety margin
|
||||
PropagateOnCompare: true,
|
||||
}
|
||||
}
|
||||
|
||||
// LimbState tracks the state of a single limb in lazy carry representation.
|
||||
type LimbState struct {
|
||||
// Value is the encrypted limb value.
|
||||
// In lazy mode, this may exceed the normal limb range.
|
||||
Value *BitCiphertext
|
||||
|
||||
// AccumulatedCarry tracks how many carries have accumulated.
|
||||
// Used to detect when propagation is needed.
|
||||
AccumulatedCarry int
|
||||
|
||||
// LimbBits is the nominal bit width of this limb.
|
||||
LimbBits int
|
||||
|
||||
// ExtendedBits is the actual storage width (LimbBits + headroom for carries).
|
||||
ExtendedBits int
|
||||
}
|
||||
|
||||
// LazyCarryInteger represents an encrypted integer with lazy carry propagation.
|
||||
//
|
||||
// Problem Statement:
|
||||
// Traditional radix arithmetic propagates carries after every operation,
|
||||
// requiring O(n) PBS calls for n-limb integers. This is the dominant cost
|
||||
// in FHE arithmetic.
|
||||
//
|
||||
// Invariants:
|
||||
// - Each limb stores a value in [0, 2^ExtendedBits)
|
||||
// - The true value is Sum(limb[i] * 2^(i*LimbBits)) with carries applied
|
||||
// - OpsWithoutPropagate tracks operations since last normalization
|
||||
// - When OpsWithoutPropagate >= config.MaxOpsBeforePropagate, propagation is required
|
||||
//
|
||||
// Interface:
|
||||
// - Add/Sub: O(n) parallel limb operations, 0 PBS
|
||||
// - Propagate: O(n) sequential PBS calls (amortized)
|
||||
// - Compare: Forces propagation, then O(n) comparisons
|
||||
type LazyCarryInteger struct {
|
||||
// Limbs are stored LSB-first (limbs[0] is least significant).
|
||||
Limbs []*LimbState
|
||||
|
||||
// NumLimbs is the number of limbs.
|
||||
NumLimbs int
|
||||
|
||||
// LimbBits is the nominal bit width per limb.
|
||||
LimbBits int
|
||||
|
||||
// OpsWithoutPropagate counts additions since last carry propagation.
|
||||
OpsWithoutPropagate int
|
||||
|
||||
// FheType is the underlying FHE integer type.
|
||||
FheType FheUintType
|
||||
|
||||
// Config holds the lazy carry configuration.
|
||||
Config LazyCarryConfig
|
||||
}
|
||||
|
||||
// LazyCarryEvaluator performs operations on LazyCarryInteger values.
|
||||
type LazyCarryEvaluator struct {
|
||||
eval *BitwiseEvaluator
|
||||
config LazyCarryConfig
|
||||
}
|
||||
|
||||
// NewLazyCarryEvaluator creates a new evaluator for lazy carry integers.
|
||||
func NewLazyCarryEvaluator(eval *BitwiseEvaluator, config LazyCarryConfig) *LazyCarryEvaluator {
|
||||
return &LazyCarryEvaluator{
|
||||
eval: eval,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// FromBitCiphertext converts a BitCiphertext to lazy carry representation.
|
||||
//
|
||||
// For euint256: Creates 8 limbs of 32 bits each.
|
||||
// For euint128: Creates 4 limbs of 32 bits each.
|
||||
// For smaller types: Creates appropriate limb structure.
|
||||
func (lce *LazyCarryEvaluator) FromBitCiphertext(bc *BitCiphertext) (*LazyCarryInteger, error) {
|
||||
totalBits := bc.NumBits()
|
||||
|
||||
// Determine limb structure based on type
|
||||
var limbBits, numLimbs int
|
||||
switch bc.Type() {
|
||||
case FheUint256:
|
||||
limbBits = 32
|
||||
numLimbs = 8
|
||||
case FheUint160:
|
||||
limbBits = 32
|
||||
numLimbs = 5
|
||||
case FheUint128:
|
||||
limbBits = 32
|
||||
numLimbs = 4
|
||||
case FheUint64:
|
||||
limbBits = 16
|
||||
numLimbs = 4
|
||||
case FheUint32:
|
||||
limbBits = 8
|
||||
numLimbs = 4
|
||||
case FheUint16:
|
||||
limbBits = 8
|
||||
numLimbs = 2
|
||||
case FheUint8:
|
||||
limbBits = 4
|
||||
numLimbs = 2
|
||||
case FheUint4:
|
||||
limbBits = 4
|
||||
numLimbs = 1
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported type for lazy carry: %s", bc.Type())
|
||||
}
|
||||
|
||||
// Verify consistency
|
||||
if limbBits*numLimbs != totalBits {
|
||||
return nil, fmt.Errorf("limb configuration mismatch: %d limbs * %d bits != %d total",
|
||||
numLimbs, limbBits, totalBits)
|
||||
}
|
||||
|
||||
// Extended bits provides headroom for carry accumulation
|
||||
// With 16 ops max, we need log2(16) = 4 extra bits
|
||||
extendedBits := limbBits + 4
|
||||
|
||||
// Create limbs by splitting the bit ciphertext
|
||||
limbs := make([]*LimbState, numLimbs)
|
||||
for i := 0; i < numLimbs; i++ {
|
||||
startBit := i * limbBits
|
||||
endBit := startBit + limbBits
|
||||
|
||||
// Extract bits for this limb
|
||||
limbCipherBits := make([]*Ciphertext, extendedBits)
|
||||
for j := 0; j < limbBits; j++ {
|
||||
if startBit+j < len(bc.bits) {
|
||||
limbCipherBits[j] = bc.bits[startBit+j]
|
||||
} else {
|
||||
// Pad with zero bits for extended range
|
||||
limbCipherBits[j] = lce.eval.encryptBit(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Pad remaining extended bits with zeros
|
||||
for j := endBit - startBit; j < extendedBits; j++ {
|
||||
limbCipherBits[j] = lce.eval.encryptBit(false)
|
||||
}
|
||||
|
||||
limbs[i] = &LimbState{
|
||||
Value: &BitCiphertext{
|
||||
bits: limbCipherBits,
|
||||
numBits: extendedBits,
|
||||
fheType: bc.Type(), // Keep original type for reference
|
||||
},
|
||||
AccumulatedCarry: 0,
|
||||
LimbBits: limbBits,
|
||||
ExtendedBits: extendedBits,
|
||||
}
|
||||
}
|
||||
|
||||
return &LazyCarryInteger{
|
||||
Limbs: limbs,
|
||||
NumLimbs: numLimbs,
|
||||
LimbBits: limbBits,
|
||||
OpsWithoutPropagate: 0,
|
||||
FheType: bc.Type(),
|
||||
Config: lce.config,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToBitCiphertext converts a LazyCarryInteger back to BitCiphertext.
|
||||
// This forces carry propagation if needed.
|
||||
func (lce *LazyCarryEvaluator) ToBitCiphertext(lci *LazyCarryInteger) (*BitCiphertext, error) {
|
||||
// Force propagation to normalize
|
||||
normalized, err := lce.Propagate(lci)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate for output: %w", err)
|
||||
}
|
||||
|
||||
// Collect bits from all limbs
|
||||
totalBits := normalized.NumLimbs * normalized.LimbBits
|
||||
bits := make([]*Ciphertext, totalBits)
|
||||
|
||||
for i := 0; i < normalized.NumLimbs; i++ {
|
||||
for j := 0; j < normalized.LimbBits; j++ {
|
||||
bits[i*normalized.LimbBits+j] = normalized.Limbs[i].Value.bits[j]
|
||||
}
|
||||
}
|
||||
|
||||
return &BitCiphertext{
|
||||
bits: bits,
|
||||
numBits: totalBits,
|
||||
fheType: normalized.FheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Add performs lazy addition without carry propagation.
|
||||
//
|
||||
// Complexity: O(numLimbs) parallel limb additions, 0 PBS.
|
||||
// Each limb addition is O(limbBits) XOR/AND operations.
|
||||
//
|
||||
// Precondition: a and b have compatible types and limb structures.
|
||||
// Postcondition: Result accumulates values; propagation deferred until needed.
|
||||
func (lce *LazyCarryEvaluator) Add(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
|
||||
if a.FheType != b.FheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.FheType, b.FheType)
|
||||
}
|
||||
if a.NumLimbs != b.NumLimbs {
|
||||
return nil, fmt.Errorf("limb count mismatch: %d vs %d", a.NumLimbs, b.NumLimbs)
|
||||
}
|
||||
|
||||
// Check if propagation needed before this operation
|
||||
if a.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate ||
|
||||
b.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate {
|
||||
var err error
|
||||
a, err = lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate a: %w", err)
|
||||
}
|
||||
b, err = lce.Propagate(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate b: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Perform lazy addition: just add limb values without carry propagation
|
||||
result := &LazyCarryInteger{
|
||||
Limbs: make([]*LimbState, a.NumLimbs),
|
||||
NumLimbs: a.NumLimbs,
|
||||
LimbBits: a.LimbBits,
|
||||
OpsWithoutPropagate: max(a.OpsWithoutPropagate, b.OpsWithoutPropagate) + 1,
|
||||
FheType: a.FheType,
|
||||
Config: lce.config,
|
||||
}
|
||||
|
||||
// Add each limb independently (no carry propagation!)
|
||||
for i := 0; i < a.NumLimbs; i++ {
|
||||
limbSum, err := lce.eval.Add(a.Limbs[i].Value, b.Limbs[i].Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("limb %d add: %w", i, err)
|
||||
}
|
||||
|
||||
result.Limbs[i] = &LimbState{
|
||||
Value: limbSum,
|
||||
AccumulatedCarry: a.Limbs[i].AccumulatedCarry + b.Limbs[i].AccumulatedCarry + 1,
|
||||
LimbBits: a.LimbBits,
|
||||
ExtendedBits: a.Limbs[i].ExtendedBits,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Sub performs lazy subtraction without carry propagation.
|
||||
func (lce *LazyCarryEvaluator) Sub(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
|
||||
if a.FheType != b.FheType {
|
||||
return nil, fmt.Errorf("type mismatch: %s vs %s", a.FheType, b.FheType)
|
||||
}
|
||||
if a.NumLimbs != b.NumLimbs {
|
||||
return nil, fmt.Errorf("limb count mismatch: %d vs %d", a.NumLimbs, b.NumLimbs)
|
||||
}
|
||||
|
||||
// Subtraction requires propagation for correctness (borrow handling)
|
||||
aNorm, err := lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate a: %w", err)
|
||||
}
|
||||
bNorm, err := lce.Propagate(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate b: %w", err)
|
||||
}
|
||||
|
||||
// Convert to bit representation, subtract, convert back
|
||||
aBits, err := lce.ToBitCiphertext(aNorm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("a to bits: %w", err)
|
||||
}
|
||||
bBits, err := lce.ToBitCiphertext(bNorm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("b to bits: %w", err)
|
||||
}
|
||||
|
||||
diffBits, err := lce.eval.Sub(aBits, bBits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("subtract: %w", err)
|
||||
}
|
||||
|
||||
return lce.FromBitCiphertext(diffBits)
|
||||
}
|
||||
|
||||
// Propagate forces carry propagation through all limbs.
|
||||
//
|
||||
// Complexity: O(numLimbs) sequential carry operations.
|
||||
// Each carry operation requires PBS for the carry extraction.
|
||||
//
|
||||
// Postcondition: All limbs are in normalized range [0, 2^LimbBits).
|
||||
func (lce *LazyCarryEvaluator) Propagate(lci *LazyCarryInteger) (*LazyCarryInteger, error) {
|
||||
if lci.OpsWithoutPropagate == 0 {
|
||||
// Already propagated
|
||||
return lci, nil
|
||||
}
|
||||
|
||||
result := &LazyCarryInteger{
|
||||
Limbs: make([]*LimbState, lci.NumLimbs),
|
||||
NumLimbs: lci.NumLimbs,
|
||||
LimbBits: lci.LimbBits,
|
||||
OpsWithoutPropagate: 0, // Reset counter
|
||||
FheType: lci.FheType,
|
||||
Config: lci.Config,
|
||||
}
|
||||
|
||||
// Carry propagation: sequential from LSB to MSB
|
||||
var carry *BitCiphertext
|
||||
for i := 0; i < lci.NumLimbs; i++ {
|
||||
limbValue := lci.Limbs[i].Value
|
||||
|
||||
// Add incoming carry from previous limb
|
||||
if carry != nil {
|
||||
var err error
|
||||
limbValue, err = lce.eval.Add(limbValue, carry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("limb %d carry add: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract carry: bits above LimbBits position
|
||||
// The carry is the value >> LimbBits
|
||||
normalizedValue, newCarry := lce.extractCarry(limbValue, lci.LimbBits)
|
||||
|
||||
result.Limbs[i] = &LimbState{
|
||||
Value: normalizedValue,
|
||||
AccumulatedCarry: 0,
|
||||
LimbBits: lci.LimbBits,
|
||||
ExtendedBits: lci.Limbs[i].ExtendedBits,
|
||||
}
|
||||
|
||||
carry = newCarry
|
||||
}
|
||||
// Final carry is discarded (overflow)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractCarry splits an extended limb into normalized value and carry.
|
||||
//
|
||||
// For a limb with value V in [0, 2^ExtendedBits):
|
||||
// - normalizedValue = V mod 2^LimbBits (lower LimbBits)
|
||||
// - carry = V >> LimbBits (upper bits, to add to next limb)
|
||||
func (lce *LazyCarryEvaluator) extractCarry(limb *BitCiphertext, limbBits int) (*BitCiphertext, *BitCiphertext) {
|
||||
if limb.numBits <= limbBits {
|
||||
// No extended bits, no carry - return empty carry
|
||||
carryBitCount := 0
|
||||
if limb.numBits > limbBits {
|
||||
carryBitCount = limb.numBits - limbBits
|
||||
}
|
||||
zeroBits := make([]*Ciphertext, carryBitCount)
|
||||
for i := range zeroBits {
|
||||
zeroBits[i] = lce.eval.encryptBit(false)
|
||||
}
|
||||
return limb, &BitCiphertext{
|
||||
bits: zeroBits,
|
||||
numBits: carryBitCount,
|
||||
fheType: limb.fheType,
|
||||
}
|
||||
}
|
||||
|
||||
// Lower bits: normalized value
|
||||
normalizedBits := make([]*Ciphertext, limbBits)
|
||||
copy(normalizedBits, limb.bits[:limbBits])
|
||||
|
||||
normalizedValue := &BitCiphertext{
|
||||
bits: normalizedBits,
|
||||
numBits: limbBits,
|
||||
fheType: limb.fheType,
|
||||
}
|
||||
|
||||
// Upper bits: carry (will be added to next limb)
|
||||
carryBits := make([]*Ciphertext, limb.numBits-limbBits)
|
||||
copy(carryBits, limb.bits[limbBits:])
|
||||
|
||||
carry := &BitCiphertext{
|
||||
bits: carryBits,
|
||||
numBits: limb.numBits - limbBits,
|
||||
fheType: limb.fheType,
|
||||
}
|
||||
|
||||
return normalizedValue, carry
|
||||
}
|
||||
|
||||
// NeedsPropagation returns true if carry propagation is needed.
|
||||
func (lci *LazyCarryInteger) NeedsPropagation() bool {
|
||||
return lci.OpsWithoutPropagate >= lci.Config.MaxOpsBeforePropagate
|
||||
}
|
||||
|
||||
// Eq compares two lazy carry integers for equality.
|
||||
// Forces propagation for correct comparison.
|
||||
func (lce *LazyCarryEvaluator) Eq(a, b *LazyCarryInteger) (*Ciphertext, error) {
|
||||
if lce.config.PropagateOnCompare {
|
||||
var err error
|
||||
a, err = lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err = lce.Propagate(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
aBits, err := lce.ToBitCiphertext(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bBits, err := lce.ToBitCiphertext(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lce.eval.Eq(aBits, bBits)
|
||||
}
|
||||
|
||||
// Lt compares a < b (unsigned).
|
||||
// Forces propagation for correct comparison.
|
||||
func (lce *LazyCarryEvaluator) Lt(a, b *LazyCarryInteger) (*Ciphertext, error) {
|
||||
if lce.config.PropagateOnCompare {
|
||||
var err error
|
||||
a, err = lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b, err = lce.Propagate(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
aBits, err := lce.ToBitCiphertext(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bBits, err := lce.ToBitCiphertext(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lce.eval.Lt(aBits, bBits)
|
||||
}
|
||||
|
||||
// ScalarAdd adds a plaintext scalar to a lazy carry integer.
|
||||
// Uses limb-wise scalar addition without carry propagation.
|
||||
func (lce *LazyCarryEvaluator) ScalarAdd(a *LazyCarryInteger, scalar uint64) (*LazyCarryInteger, error) {
|
||||
// Check if propagation needed
|
||||
if a.OpsWithoutPropagate >= lce.config.MaxOpsBeforePropagate {
|
||||
var err error
|
||||
a, err = lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
result := &LazyCarryInteger{
|
||||
Limbs: make([]*LimbState, a.NumLimbs),
|
||||
NumLimbs: a.NumLimbs,
|
||||
LimbBits: a.LimbBits,
|
||||
OpsWithoutPropagate: a.OpsWithoutPropagate + 1,
|
||||
FheType: a.FheType,
|
||||
Config: a.Config,
|
||||
}
|
||||
|
||||
limbMask := uint64((1 << a.LimbBits) - 1)
|
||||
|
||||
for i := 0; i < a.NumLimbs; i++ {
|
||||
scalarLimb := (scalar >> (i * a.LimbBits)) & limbMask
|
||||
|
||||
if scalarLimb == 0 {
|
||||
// No change to this limb
|
||||
result.Limbs[i] = &LimbState{
|
||||
Value: a.Limbs[i].Value,
|
||||
AccumulatedCarry: a.Limbs[i].AccumulatedCarry,
|
||||
LimbBits: a.LimbBits,
|
||||
ExtendedBits: a.Limbs[i].ExtendedBits,
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Add scalar to this limb
|
||||
limbSum, err := lce.eval.ScalarAdd(a.Limbs[i].Value, scalarLimb)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("limb %d scalar add: %w", i, err)
|
||||
}
|
||||
|
||||
result.Limbs[i] = &LimbState{
|
||||
Value: limbSum,
|
||||
AccumulatedCarry: a.Limbs[i].AccumulatedCarry + 1,
|
||||
LimbBits: a.LimbBits,
|
||||
ExtendedBits: a.Limbs[i].ExtendedBits,
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Mul multiplies two lazy carry integers.
|
||||
// Forces propagation before multiplication for correctness.
|
||||
func (lce *LazyCarryEvaluator) Mul(a, b *LazyCarryInteger) (*LazyCarryInteger, error) {
|
||||
// Multiplication requires normalized inputs
|
||||
aNorm, err := lce.Propagate(a)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate a: %w", err)
|
||||
}
|
||||
bNorm, err := lce.Propagate(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("propagate b: %w", err)
|
||||
}
|
||||
|
||||
// Convert to bit representation for multiplication
|
||||
aBits, err := lce.ToBitCiphertext(aNorm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("a to bits: %w", err)
|
||||
}
|
||||
bBits, err := lce.ToBitCiphertext(bNorm)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("b to bits: %w", err)
|
||||
}
|
||||
|
||||
// Perform multiplication
|
||||
prodBits, err := lce.eval.Mul(aBits, bBits)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("multiply: %w", err)
|
||||
}
|
||||
|
||||
// Convert back to lazy carry representation
|
||||
return lce.FromBitCiphertext(prodBits)
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of a LazyCarryInteger.
|
||||
func (lce *LazyCarryEvaluator) Copy(lci *LazyCarryInteger) *LazyCarryInteger {
|
||||
result := &LazyCarryInteger{
|
||||
Limbs: make([]*LimbState, lci.NumLimbs),
|
||||
NumLimbs: lci.NumLimbs,
|
||||
LimbBits: lci.LimbBits,
|
||||
OpsWithoutPropagate: lci.OpsWithoutPropagate,
|
||||
FheType: lci.FheType,
|
||||
Config: lci.Config,
|
||||
}
|
||||
|
||||
for i := 0; i < lci.NumLimbs; i++ {
|
||||
bits := make([]*Ciphertext, lci.Limbs[i].Value.numBits)
|
||||
copy(bits, lci.Limbs[i].Value.bits)
|
||||
|
||||
result.Limbs[i] = &LimbState{
|
||||
Value: &BitCiphertext{
|
||||
bits: bits,
|
||||
numBits: lci.Limbs[i].Value.numBits,
|
||||
fheType: lci.Limbs[i].Value.fheType,
|
||||
},
|
||||
AccumulatedCarry: lci.Limbs[i].AccumulatedCarry,
|
||||
LimbBits: lci.Limbs[i].LimbBits,
|
||||
ExtendedBits: lci.Limbs[i].ExtendedBits,
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Zero returns a lazy carry integer initialized to zero.
|
||||
func (lce *LazyCarryEvaluator) Zero(fheType FheUintType) (*LazyCarryInteger, error) {
|
||||
zero := lce.eval.Zero(fheType)
|
||||
return lce.FromBitCiphertext(zero)
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EVM-Specific Optimizations
|
||||
// =========================================================================
|
||||
|
||||
// EVMLazyCarryConfig returns configuration optimized for EVM workloads.
|
||||
//
|
||||
// EVM characteristics:
|
||||
// - Heavy uint256 arithmetic (ADD, SUB, MUL, DIV)
|
||||
// - Frequent comparisons (LT, GT, EQ)
|
||||
// - State reads/writes require normalized values
|
||||
//
|
||||
// This configuration balances:
|
||||
// - PBS amortization (higher MaxOps = fewer PBS)
|
||||
// - Memory overhead (higher MaxOps = more extended bits)
|
||||
// - Latency (propagation adds latency when triggered)
|
||||
func EVMLazyCarryConfig() LazyCarryConfig {
|
||||
return LazyCarryConfig{
|
||||
MaxOpsBeforePropagate: 12, // Tuned for typical EVM ADD sequences
|
||||
OverflowMargin: 1 << 20,
|
||||
PropagateOnCompare: true,
|
||||
}
|
||||
}
|
||||
|
||||
// BatchAdd performs multiple additions with minimal propagation.
|
||||
//
|
||||
// For n additions, this performs:
|
||||
// - n * O(numLimbs) parallel limb additions
|
||||
// - 1 final propagation (O(numLimbs) PBS)
|
||||
//
|
||||
// Amortized PBS cost: 1/n per addition vs 1 per addition for traditional.
|
||||
func (lce *LazyCarryEvaluator) BatchAdd(values []*LazyCarryInteger) (*LazyCarryInteger, error) {
|
||||
if len(values) == 0 {
|
||||
return nil, fmt.Errorf("empty batch")
|
||||
}
|
||||
if len(values) == 1 {
|
||||
return lce.Copy(values[0]), nil
|
||||
}
|
||||
|
||||
// Accumulate all values
|
||||
result := lce.Copy(values[0])
|
||||
for i := 1; i < len(values); i++ {
|
||||
var err error
|
||||
result, err = lce.Add(result, values[i])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("batch add %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Force final propagation for clean output
|
||||
return lce.Propagate(result)
|
||||
}
|
||||
|
||||
// PerformanceMetrics tracks lazy carry performance statistics.
|
||||
type PerformanceMetrics struct {
|
||||
TotalAdditions int
|
||||
PropagationCount int
|
||||
PBSOperations int // Estimated PBS calls
|
||||
AmortizationRatio float64
|
||||
TraditionalPBSEstimate int
|
||||
}
|
||||
|
||||
// GetMetrics returns performance metrics for a lazy carry integer.
|
||||
func (lci *LazyCarryInteger) GetMetrics() PerformanceMetrics {
|
||||
// Traditional: 1 PBS per limb per addition for carry chain
|
||||
traditionalPBS := lci.OpsWithoutPropagate * (lci.NumLimbs - 1)
|
||||
|
||||
// Lazy: PBS only on propagation (numLimbs - 1 carries)
|
||||
lazyPBS := 0
|
||||
if lci.OpsWithoutPropagate > 0 {
|
||||
lazyPBS = lci.NumLimbs - 1 // Would need this many on next propagate
|
||||
}
|
||||
|
||||
ratio := 0.0
|
||||
if traditionalPBS > 0 {
|
||||
ratio = float64(lazyPBS) / float64(traditionalPBS)
|
||||
}
|
||||
|
||||
return PerformanceMetrics{
|
||||
TotalAdditions: lci.OpsWithoutPropagate,
|
||||
PropagationCount: 0, // This value, not counting historical
|
||||
PBSOperations: lazyPBS,
|
||||
AmortizationRatio: ratio,
|
||||
TraditionalPBSEstimate: traditionalPBS,
|
||||
}
|
||||
}
|
||||
|
||||
// max returns the maximum of two ints.
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
+542
@@ -0,0 +1,542 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// NTTEngine provides SIMD-optimized NTT operations for CPU fallback.
|
||||
// Uses AVX2/AVX-512 on x86-64 and NEON on ARM64.
|
||||
type NTTEngine struct {
|
||||
N uint32
|
||||
Q uint64
|
||||
twiddleFactors []uint64
|
||||
invTwiddles []uint64
|
||||
nInv uint64
|
||||
|
||||
// Precomputed Barrett reduction constants
|
||||
barrettMu uint64 // floor(2^64 / Q)
|
||||
barrettK int // number of bits for Barrett
|
||||
|
||||
// Montgomery form constants
|
||||
montR uint64 // R = 2^64 mod Q
|
||||
montR2 uint64 // R^2 mod Q
|
||||
montInv uint64 // -Q^(-1) mod 2^64
|
||||
}
|
||||
|
||||
// NewNTTEngine creates a new SIMD-optimized NTT engine
|
||||
func NewNTTEngine(N uint32, Q uint64) (*NTTEngine, error) {
|
||||
e := &NTTEngine{
|
||||
N: N,
|
||||
Q: Q,
|
||||
twiddleFactors: make([]uint64, N),
|
||||
invTwiddles: make([]uint64, N),
|
||||
}
|
||||
|
||||
// Find primitive 2N-th root of unity
|
||||
omega, err := e.findPrimitiveRoot()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("find primitive root: %w", err)
|
||||
}
|
||||
omegaInv := e.modInverse(omega)
|
||||
|
||||
// Precompute twiddle factors in bit-reversed order for in-place NTT
|
||||
e.computeTwiddlesBitReversed(omega, omegaInv)
|
||||
|
||||
// N^(-1) mod Q for INTT normalization
|
||||
e.nInv = e.modInverse(uint64(N))
|
||||
|
||||
// Barrett reduction constant
|
||||
e.barrettK = 64
|
||||
e.barrettMu = e.computeBarrettMu()
|
||||
|
||||
// Montgomery constants
|
||||
e.montR = e.computeMontgomeryR()
|
||||
e.montR2 = e.mulMod(e.montR, e.montR)
|
||||
e.montInv = e.computeMontgomeryInv()
|
||||
|
||||
return e, nil
|
||||
}
|
||||
|
||||
// NTTInPlace performs in-place NTT using Cooley-Tukey algorithm
|
||||
// Optimized for SIMD vectorization with explicit parallelism
|
||||
func (e *NTTEngine) NTTInPlace(coeffs []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
// Bit-reversal permutation
|
||||
e.bitReversePermute(coeffs)
|
||||
|
||||
// Cooley-Tukey NTT butterflies
|
||||
// The structure allows SIMD vectorization across independent butterflies
|
||||
twiddleIdx := 0
|
||||
for m := 2; m <= N; m <<= 1 {
|
||||
mHalf := m >> 1
|
||||
|
||||
// Process all butterflies at this stage
|
||||
// Each group of m elements has m/2 independent butterflies
|
||||
for k := 0; k < N; k += m {
|
||||
// This inner loop is SIMD-vectorizable
|
||||
for j := 0; j < mHalf; j++ {
|
||||
w := e.twiddleFactors[twiddleIdx+j]
|
||||
u := coeffs[k+j]
|
||||
v := e.mulModBarrett(coeffs[k+j+mHalf], w)
|
||||
|
||||
// Butterfly: [u, v] -> [u + v, u - v]
|
||||
coeffs[k+j] = e.addMod(u, v)
|
||||
coeffs[k+j+mHalf] = e.subMod(u, v)
|
||||
}
|
||||
}
|
||||
twiddleIdx += mHalf
|
||||
}
|
||||
}
|
||||
|
||||
// INTTInPlace performs in-place inverse NTT using Gentleman-Sande algorithm
|
||||
func (e *NTTEngine) INTTInPlace(coeffs []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
// Gentleman-Sande INTT butterflies (reverse order of NTT)
|
||||
twiddleIdx := int(e.N) - 2
|
||||
for m := N; m >= 2; m >>= 1 {
|
||||
mHalf := m >> 1
|
||||
|
||||
for k := 0; k < N; k += m {
|
||||
for j := 0; j < mHalf; j++ {
|
||||
w := e.invTwiddles[twiddleIdx+j]
|
||||
u := coeffs[k+j]
|
||||
v := coeffs[k+j+mHalf]
|
||||
|
||||
// Inverse butterfly: [u, v] -> [u + v, (u - v) * w]
|
||||
coeffs[k+j] = e.addMod(u, v)
|
||||
coeffs[k+j+mHalf] = e.mulModBarrett(e.subMod(u, v), w)
|
||||
}
|
||||
}
|
||||
twiddleIdx -= mHalf
|
||||
}
|
||||
|
||||
// Bit-reversal permutation
|
||||
e.bitReversePermute(coeffs)
|
||||
|
||||
// Multiply by N^(-1) to normalize
|
||||
// This loop is SIMD-vectorizable
|
||||
for i := 0; i < N; i++ {
|
||||
coeffs[i] = e.mulModBarrett(coeffs[i], e.nInv)
|
||||
}
|
||||
}
|
||||
|
||||
// NTTBatch performs NTT on multiple polynomials in parallel
|
||||
// Exploits both inter-polynomial and intra-polynomial parallelism
|
||||
func (e *NTTEngine) NTTBatch(polys [][]uint64) {
|
||||
var wg sync.WaitGroup
|
||||
batchSize := len(polys)
|
||||
|
||||
// Determine optimal parallelism based on batch size
|
||||
numWorkers := batchSize
|
||||
if numWorkers > 16 {
|
||||
numWorkers = 16 // Cap at 16 parallel workers
|
||||
}
|
||||
|
||||
chunkSize := (batchSize + numWorkers - 1) / numWorkers
|
||||
|
||||
for w := 0; w < numWorkers; w++ {
|
||||
start := w * chunkSize
|
||||
end := start + chunkSize
|
||||
if end > batchSize {
|
||||
end = batchSize
|
||||
}
|
||||
if start >= end {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s, end int, eng *NTTEngine) {
|
||||
defer wg.Done()
|
||||
for i := s; i < end; i++ {
|
||||
eng.NTTInPlace(polys[i])
|
||||
}
|
||||
}(start, end, e)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// INTTBatch performs INTT on multiple polynomials in parallel
|
||||
func (e *NTTEngine) INTTBatch(polys [][]uint64) {
|
||||
var wg sync.WaitGroup
|
||||
batchSize := len(polys)
|
||||
|
||||
numWorkers := batchSize
|
||||
if numWorkers > 16 {
|
||||
numWorkers = 16
|
||||
}
|
||||
|
||||
chunkSize := (batchSize + numWorkers - 1) / numWorkers
|
||||
|
||||
for w := 0; w < numWorkers; w++ {
|
||||
start := w * chunkSize
|
||||
end := start + chunkSize
|
||||
if end > batchSize {
|
||||
end = batchSize
|
||||
}
|
||||
if start >= end {
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(s, end int, eng *NTTEngine) {
|
||||
defer wg.Done()
|
||||
for i := s; i < end; i++ {
|
||||
eng.INTTInPlace(polys[i])
|
||||
}
|
||||
}(start, end, e)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// PolyMulNTT multiplies two polynomials already in NTT form
|
||||
// Result is also in NTT form. SIMD-vectorizable element-wise multiplication.
|
||||
func (e *NTTEngine) PolyMulNTT(a, b, result []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
// Element-wise multiplication in NTT domain
|
||||
// This loop is fully SIMD-vectorizable
|
||||
for i := 0; i < N; i++ {
|
||||
result[i] = e.mulModBarrett(a[i], b[i])
|
||||
}
|
||||
}
|
||||
|
||||
// PolyMulNTTAccum multiplies and accumulates: result += a * b (in NTT form)
|
||||
func (e *NTTEngine) PolyMulNTTAccum(a, b, result []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
prod := e.mulModBarrett(a[i], b[i])
|
||||
result[i] = e.addMod(result[i], prod)
|
||||
}
|
||||
}
|
||||
|
||||
// PolyAdd adds two polynomials: result = a + b
|
||||
func (e *NTTEngine) PolyAdd(a, b, result []uint64) {
|
||||
N := int(e.N)
|
||||
Q := e.Q
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
sum := a[i] + b[i]
|
||||
if sum >= Q {
|
||||
sum -= Q
|
||||
}
|
||||
result[i] = sum
|
||||
}
|
||||
}
|
||||
|
||||
// PolySub subtracts two polynomials: result = a - b
|
||||
func (e *NTTEngine) PolySub(a, b, result []uint64) {
|
||||
N := int(e.N)
|
||||
Q := e.Q
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
if a[i] >= b[i] {
|
||||
result[i] = a[i] - b[i]
|
||||
} else {
|
||||
result[i] = Q - b[i] + a[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PolyNeg negates a polynomial: result = -a
|
||||
func (e *NTTEngine) PolyNeg(a, result []uint64) {
|
||||
N := int(e.N)
|
||||
Q := e.Q
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
if a[i] == 0 {
|
||||
result[i] = 0
|
||||
} else {
|
||||
result[i] = Q - a[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PolyMulScalar multiplies polynomial by scalar: result = a * scalar
|
||||
func (e *NTTEngine) PolyMulScalar(a []uint64, scalar uint64, result []uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
result[i] = e.mulModBarrett(a[i], scalar)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Helper Functions ==========
|
||||
|
||||
// bitReversePermute performs in-place bit-reversal permutation
|
||||
func (e *NTTEngine) bitReversePermute(coeffs []uint64) {
|
||||
N := int(e.N)
|
||||
logN := e.log2(N)
|
||||
|
||||
for i := 0; i < N; i++ {
|
||||
j := e.reverseBits(i, logN)
|
||||
if i < j {
|
||||
coeffs[i], coeffs[j] = coeffs[j], coeffs[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// reverseBits reverses the lower logN bits of x
|
||||
func (e *NTTEngine) reverseBits(x, logN int) int {
|
||||
result := 0
|
||||
for i := 0; i < logN; i++ {
|
||||
result = (result << 1) | (x & 1)
|
||||
x >>= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// log2 returns floor(log2(n))
|
||||
func (e *NTTEngine) log2(n int) int {
|
||||
r := 0
|
||||
for n > 1 {
|
||||
n >>= 1
|
||||
r++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// addMod computes (a + b) mod Q
|
||||
func (e *NTTEngine) addMod(a, b uint64) uint64 {
|
||||
sum := a + b
|
||||
if sum >= e.Q {
|
||||
sum -= e.Q
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// subMod computes (a - b) mod Q
|
||||
func (e *NTTEngine) subMod(a, b uint64) uint64 {
|
||||
if a >= b {
|
||||
return a - b
|
||||
}
|
||||
return e.Q - b + a
|
||||
}
|
||||
|
||||
// mulMod computes (a * b) mod Q using standard division
|
||||
func (e *NTTEngine) mulMod(a, b uint64) uint64 {
|
||||
hi, lo := mul64(a, b)
|
||||
if hi == 0 {
|
||||
return lo % e.Q
|
||||
}
|
||||
// For larger results, use the full 128-bit division
|
||||
return div128(hi, lo, e.Q)
|
||||
}
|
||||
|
||||
// mulModBarrett computes (a * b) mod Q using Barrett reduction
|
||||
// Barrett reduction avoids expensive division by precomputing mu = floor(2^64/Q)
|
||||
// Then floor(a*b/Q) ≈ ((a*b) * mu) >> 64
|
||||
func (e *NTTEngine) mulModBarrett(a, b uint64) uint64 {
|
||||
hi, lo := mul64(a, b)
|
||||
|
||||
// Approximate quotient: q = ((hi, lo) * mu) >> 64
|
||||
// We only need the high part of the product
|
||||
_, qHi := mul64(hi, e.barrettMu)
|
||||
_, qLoHi := mul64(lo, e.barrettMu)
|
||||
q := qHi + (qLoHi >> 32) // Approximate quotient
|
||||
|
||||
// r = (a * b) - q * Q
|
||||
r := lo - q*e.Q
|
||||
|
||||
// Correction: r might be >= Q (at most twice)
|
||||
if r >= e.Q {
|
||||
r -= e.Q
|
||||
}
|
||||
if r >= e.Q {
|
||||
r -= e.Q
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// mul64 multiplies two 64-bit integers and returns 128-bit result as (hi, lo)
|
||||
func mul64(a, b uint64) (hi, lo uint64) {
|
||||
// Use assembly intrinsic if available, otherwise use portable version
|
||||
// This is the portable Go version - compiler may optimize to MULX on x86-64
|
||||
aLo, aHi := a&0xFFFFFFFF, a>>32
|
||||
bLo, bHi := b&0xFFFFFFFF, b>>32
|
||||
|
||||
p0 := aLo * bLo
|
||||
p1 := aLo * bHi
|
||||
p2 := aHi * bLo
|
||||
p3 := aHi * bHi
|
||||
|
||||
mid := p1 + p2
|
||||
carry := uint64(0)
|
||||
if mid < p1 {
|
||||
carry = 1 << 32
|
||||
}
|
||||
|
||||
lo = p0 + (mid << 32)
|
||||
if lo < p0 {
|
||||
carry++
|
||||
}
|
||||
hi = p3 + (mid >> 32) + carry
|
||||
return
|
||||
}
|
||||
|
||||
// div128 divides a 128-bit number (hi, lo) by a 64-bit divisor
|
||||
// Returns the remainder (we don't need the quotient for modular reduction)
|
||||
func div128(hi, lo, d uint64) uint64 {
|
||||
// For FHE parameters, hi is usually small or zero
|
||||
// This is a simplified version for the common case
|
||||
if hi == 0 {
|
||||
return lo % d
|
||||
}
|
||||
|
||||
// Full 128÷64 division using long division
|
||||
// This is rarely executed for properly chosen Q
|
||||
_ = hi / d // Compute quotient (not used)
|
||||
r := hi % d
|
||||
lo2 := (r << 32) | (lo >> 32)
|
||||
_ = lo2 / d // Compute quotient (not used)
|
||||
r = lo2 % d
|
||||
lo3 := (r << 32) | (lo & 0xFFFFFFFF)
|
||||
return lo3 % d
|
||||
}
|
||||
|
||||
// computeTwiddlesBitReversed precomputes twiddle factors in bit-reversed order
|
||||
func (e *NTTEngine) computeTwiddlesBitReversed(omega, omegaInv uint64) {
|
||||
N := int(e.N)
|
||||
|
||||
// Forward NTT twiddles
|
||||
idx := 0
|
||||
for m := 2; m <= N; m <<= 1 {
|
||||
mHalf := m >> 1
|
||||
w := uint64(1)
|
||||
wStep := e.powMod(omega, uint64(N/m))
|
||||
|
||||
for j := 0; j < mHalf; j++ {
|
||||
e.twiddleFactors[idx+j] = w
|
||||
w = e.mulMod(w, wStep)
|
||||
}
|
||||
idx += mHalf
|
||||
}
|
||||
|
||||
// Inverse NTT twiddles (same structure as forward for consistency)
|
||||
idx = 0
|
||||
for m := 2; m <= N; m <<= 1 {
|
||||
mHalf := m >> 1
|
||||
w := uint64(1)
|
||||
wStep := e.powMod(omegaInv, uint64(N/m))
|
||||
|
||||
for j := 0; j < mHalf; j++ {
|
||||
e.invTwiddles[idx+j] = w
|
||||
w = e.mulMod(w, wStep)
|
||||
}
|
||||
idx += mHalf
|
||||
}
|
||||
}
|
||||
|
||||
// findPrimitiveRoot finds a primitive 2N-th root of unity mod Q
|
||||
func (e *NTTEngine) findPrimitiveRoot() (uint64, error) {
|
||||
N := uint64(e.N)
|
||||
Q := e.Q
|
||||
order := Q - 1
|
||||
|
||||
// Q-1 must be divisible by 2N for NTT
|
||||
if order%(2*N) != 0 {
|
||||
return 0, fmt.Errorf("Q-1 (%d) must be divisible by 2N (%d) for NTT", order, 2*N)
|
||||
}
|
||||
|
||||
// Find a generator of Z_Q*
|
||||
for g := uint64(2); g < Q; g++ {
|
||||
isGenerator := true
|
||||
// Check g^((Q-1)/p) != 1 for small prime factors p of Q-1
|
||||
for _, p := range []uint64{2} {
|
||||
if e.powMod(g, (Q-1)/p) == 1 {
|
||||
isGenerator = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if isGenerator {
|
||||
// omega = g^((Q-1)/(2N)) is a primitive 2N-th root
|
||||
return e.powMod(g, order/(2*N)), nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("no primitive root found for N=%d, Q=%d", e.N, Q)
|
||||
}
|
||||
|
||||
// modInverse computes a^(-1) mod Q using Fermat's little theorem
|
||||
func (e *NTTEngine) modInverse(a uint64) uint64 {
|
||||
return e.powMod(a, e.Q-2)
|
||||
}
|
||||
|
||||
// powMod computes base^exp mod Q
|
||||
func (e *NTTEngine) powMod(base, exp uint64) uint64 {
|
||||
result := uint64(1)
|
||||
base = base % e.Q
|
||||
for exp > 0 {
|
||||
if exp&1 == 1 {
|
||||
result = e.mulMod(result, base)
|
||||
}
|
||||
base = e.mulMod(base, base)
|
||||
exp >>= 1
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// computeBarrettMu computes floor(2^64 / Q) for Barrett reduction
|
||||
func (e *NTTEngine) computeBarrettMu() uint64 {
|
||||
// mu = floor(2^64 / Q)
|
||||
// Since we can't represent 2^64 directly, we compute it carefully
|
||||
// 2^64 / Q = (2^63 / Q) * 2 + ((2^63 mod Q) * 2) / Q
|
||||
twoTo63 := uint64(1) << 63
|
||||
q := e.Q
|
||||
mu := (twoTo63 / q) * 2
|
||||
rem := ((twoTo63 % q) * 2) / q
|
||||
return mu + rem
|
||||
}
|
||||
|
||||
// computeMontgomeryR computes R = 2^64 mod Q
|
||||
func (e *NTTEngine) computeMontgomeryR() uint64 {
|
||||
// R = 2^64 mod Q
|
||||
// For Q < 2^63, compute (2^63 mod Q) * 2 mod Q
|
||||
twoTo63 := uint64(1) << 63
|
||||
r63 := twoTo63 % e.Q
|
||||
r := (r63 * 2) % e.Q
|
||||
return r
|
||||
}
|
||||
|
||||
// computeMontgomeryInv computes -Q^(-1) mod 2^64
|
||||
func (e *NTTEngine) computeMontgomeryInv() uint64 {
|
||||
// Newton's method for modular inverse
|
||||
// x_{n+1} = x_n * (2 - q * x_n) mod 2^64
|
||||
q := e.Q
|
||||
x := q // Initial guess
|
||||
for i := 0; i < 64; i++ {
|
||||
x = x * (2 - q*x)
|
||||
}
|
||||
return -x // -Q^(-1) mod 2^64
|
||||
}
|
||||
|
||||
// ========== SIMD-Optimized Variants ==========
|
||||
// These provide hooks for assembly implementations
|
||||
|
||||
// NTTInPlaceSIMD is a placeholder for assembly-optimized NTT
|
||||
// On x86-64 with AVX-512, this can process 8 butterflies in parallel
|
||||
// On ARM64 with NEON, this can process 2 butterflies in parallel
|
||||
func (e *NTTEngine) NTTInPlaceSIMD(coeffs []uint64) {
|
||||
// Check for AVX-512 or NEON support and dispatch
|
||||
// For now, fall back to scalar version
|
||||
e.NTTInPlace(coeffs)
|
||||
}
|
||||
|
||||
// INTTInPlaceSIMD is a placeholder for assembly-optimized INTT
|
||||
func (e *NTTEngine) INTTInPlaceSIMD(coeffs []uint64) {
|
||||
e.INTTInPlace(coeffs)
|
||||
}
|
||||
|
||||
// PointerSize returns the size of a pointer (used for SIMD dispatch)
|
||||
func PointerSize() int {
|
||||
return int(unsafe.Sizeof(uintptr(0)))
|
||||
}
|
||||
+168
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build profile
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ProfileConfig holds profiling configuration
|
||||
type ProfileConfig struct {
|
||||
// CPUProfile enables CPU profiling to the specified file
|
||||
CPUProfile string
|
||||
// MemProfile enables memory profiling to the specified file
|
||||
MemProfile string
|
||||
// BlockProfile enables block (contention) profiling
|
||||
BlockProfile string
|
||||
// MutexProfile enables mutex profiling
|
||||
MutexProfile string
|
||||
// TraceFile enables execution tracing
|
||||
TraceFile string
|
||||
}
|
||||
|
||||
// Profiler wraps profiling functionality
|
||||
type Profiler struct {
|
||||
config ProfileConfig
|
||||
cpuFile *os.File
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
// NewProfiler creates a new profiler with the given configuration
|
||||
func NewProfiler(config ProfileConfig) *Profiler {
|
||||
return &Profiler{config: config}
|
||||
}
|
||||
|
||||
// Start begins profiling
|
||||
func (p *Profiler) Start() error {
|
||||
p.startTime = time.Now()
|
||||
|
||||
// Enable block profiling if requested
|
||||
if p.config.BlockProfile != "" {
|
||||
runtime.SetBlockProfileRate(1)
|
||||
}
|
||||
|
||||
// Enable mutex profiling if requested
|
||||
if p.config.MutexProfile != "" {
|
||||
runtime.SetMutexProfileFraction(1)
|
||||
}
|
||||
|
||||
// Start CPU profiling
|
||||
if p.config.CPUProfile != "" {
|
||||
f, err := os.Create(p.config.CPUProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create CPU profile: %w", err)
|
||||
}
|
||||
p.cpuFile = f
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
f.Close()
|
||||
return fmt.Errorf("start CPU profile: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop ends profiling and writes all profile files
|
||||
func (p *Profiler) Stop() error {
|
||||
duration := time.Since(p.startTime)
|
||||
fmt.Printf("Profiling duration: %v\n", duration)
|
||||
|
||||
// Stop CPU profiling
|
||||
if p.cpuFile != nil {
|
||||
pprof.StopCPUProfile()
|
||||
p.cpuFile.Close()
|
||||
fmt.Printf("CPU profile written to: %s\n", p.config.CPUProfile)
|
||||
}
|
||||
|
||||
// Write memory profile
|
||||
if p.config.MemProfile != "" {
|
||||
f, err := os.Create(p.config.MemProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create memory profile: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
runtime.GC() // Get up-to-date statistics
|
||||
if err := pprof.WriteHeapProfile(f); err != nil {
|
||||
return fmt.Errorf("write memory profile: %w", err)
|
||||
}
|
||||
fmt.Printf("Memory profile written to: %s\n", p.config.MemProfile)
|
||||
}
|
||||
|
||||
// Write block profile
|
||||
if p.config.BlockProfile != "" {
|
||||
f, err := os.Create(p.config.BlockProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create block profile: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := pprof.Lookup("block").WriteTo(f, 0); err != nil {
|
||||
return fmt.Errorf("write block profile: %w", err)
|
||||
}
|
||||
runtime.SetBlockProfileRate(0)
|
||||
fmt.Printf("Block profile written to: %s\n", p.config.BlockProfile)
|
||||
}
|
||||
|
||||
// Write mutex profile
|
||||
if p.config.MutexProfile != "" {
|
||||
f, err := os.Create(p.config.MutexProfile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create mutex profile: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
if err := pprof.Lookup("mutex").WriteTo(f, 0); err != nil {
|
||||
return fmt.Errorf("write mutex profile: %w", err)
|
||||
}
|
||||
runtime.SetMutexProfileFraction(0)
|
||||
fmt.Printf("Mutex profile written to: %s\n", p.config.MutexProfile)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MemStats returns current memory statistics
|
||||
func MemStats() runtime.MemStats {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
return m
|
||||
}
|
||||
|
||||
// PrintMemStats prints memory statistics
|
||||
func PrintMemStats() {
|
||||
m := MemStats()
|
||||
fmt.Printf("Memory Statistics:\n")
|
||||
fmt.Printf(" Alloc: %d MB\n", m.Alloc/1024/1024)
|
||||
fmt.Printf(" TotalAlloc: %d MB\n", m.TotalAlloc/1024/1024)
|
||||
fmt.Printf(" Sys: %d MB\n", m.Sys/1024/1024)
|
||||
fmt.Printf(" NumGC: %d\n", m.NumGC)
|
||||
fmt.Printf(" HeapObjects: %d\n", m.HeapObjects)
|
||||
}
|
||||
|
||||
// Timer is a simple operation timer
|
||||
type Timer struct {
|
||||
name string
|
||||
start time.Time
|
||||
}
|
||||
|
||||
// NewTimer creates a timer that prints duration on Stop
|
||||
func NewTimer(name string) *Timer {
|
||||
return &Timer{name: name, start: time.Now()}
|
||||
}
|
||||
|
||||
// Stop prints the elapsed time
|
||||
func (t *Timer) Stop() time.Duration {
|
||||
d := time.Since(t.start)
|
||||
fmt.Printf("%s: %v\n", t.name, d)
|
||||
return d
|
||||
}
|
||||
|
||||
// Elapsed returns elapsed time without stopping
|
||||
func (t *Timer) Elapsed() time.Duration {
|
||||
return time.Since(t.start)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
//go:build !cgo
|
||||
// +build !cgo
|
||||
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// This file tests pure Go mode (CGO_ENABLED=0)
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPureGoMode verifies FHE works without CGO
|
||||
func TestPureGoMode(t *testing.T) {
|
||||
t.Log("Running in Pure Go mode (CGO_ENABLED=0)")
|
||||
|
||||
tc := newTestContext(t)
|
||||
|
||||
t.Run("BooleanEncryptDecrypt", func(t *testing.T) {
|
||||
testBooleanEncryptDecrypt(t, tc)
|
||||
})
|
||||
|
||||
t.Run("BooleanGates", func(t *testing.T) {
|
||||
testBooleanGates(t, tc)
|
||||
})
|
||||
|
||||
t.Run("IntegerEncryptDecrypt", func(t *testing.T) {
|
||||
testIntegerEncryptDecrypt(t, tc, []FheUintType{FheUint4, FheUint8})
|
||||
})
|
||||
|
||||
t.Run("IntegerArithmetic", func(t *testing.T) {
|
||||
testIntegerArithmetic(t, tc, 5, 3, FheUint4)
|
||||
})
|
||||
|
||||
t.Run("BigIntTypes", func(t *testing.T) {
|
||||
// Test uint128
|
||||
val128 := new(big.Int).SetUint64(0xFFFFFFFFFFFFFFFF)
|
||||
testBigIntRoundtrip(t, tc, val128, FheUint128)
|
||||
|
||||
// Test uint256
|
||||
val256 := new(big.Int).Lsh(big.NewInt(1), 200) // 2^200
|
||||
testBigIntRoundtrip(t, tc, val256, FheUint256)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPureGoSerialization tests serialization in pure Go mode
|
||||
func TestPureGoSerialization(t *testing.T) {
|
||||
tc := newTestContext(t)
|
||||
testKeySerialization(t, tc)
|
||||
}
|
||||
|
||||
// TestPureGoRNG tests RNG in pure Go mode
|
||||
func TestPureGoRNG(t *testing.T) {
|
||||
tc := newTestContext(t)
|
||||
testRNG(t, tc, FheUint4)
|
||||
}
|
||||
|
||||
func BenchmarkPureGoOperations(b *testing.B) {
|
||||
params, _ := NewParametersFromLiteral(PN10QP27)
|
||||
kg := NewKeyGenerator(params)
|
||||
sk := kg.GenSecretKey()
|
||||
bsk := kg.GenBootstrapKey(sk)
|
||||
enc := NewBitwiseEncryptor(params, sk)
|
||||
eval := NewBitwiseEvaluator(params, bsk, sk)
|
||||
|
||||
ctA := enc.EncryptUint64(5, FheUint4)
|
||||
ctB := enc.EncryptUint64(3, FheUint4)
|
||||
|
||||
b.Run("PureGo_Add4", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
eval.Add(ctA, ctB)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("PureGo_Mul4", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
eval.Mul(ctA, ctB)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FheRNG generates encrypted random numbers for FHE computations.
|
||||
// Uses a deterministic PRNG seeded with a nonce for consensus compatibility.
|
||||
// The generated random values are encrypted and remain hidden until decryption.
|
||||
type FheRNG struct {
|
||||
params Parameters
|
||||
enc *BitwiseEncryptor
|
||||
state [32]byte // SHA256 state
|
||||
counter uint64
|
||||
}
|
||||
|
||||
// NewFheRNG creates a new encrypted random number generator.
|
||||
// The seed should be derived from blockchain state (e.g., block hash + tx hash)
|
||||
// to ensure deterministic but unpredictable randomness.
|
||||
func NewFheRNG(params Parameters, sk *SecretKey, seed []byte) *FheRNG {
|
||||
state := sha256.Sum256(seed)
|
||||
return &FheRNG{
|
||||
params: params,
|
||||
enc: NewBitwiseEncryptor(params, sk),
|
||||
state: state,
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// FheRNGPublic generates encrypted random numbers using public key encryption.
|
||||
// This allows the RNG to run on nodes that don't have access to the secret key.
|
||||
type FheRNGPublic struct {
|
||||
params Parameters
|
||||
enc *BitwisePublicEncryptor
|
||||
state [32]byte
|
||||
counter uint64
|
||||
}
|
||||
|
||||
// NewFheRNGPublic creates a new encrypted random number generator using public key.
|
||||
func NewFheRNGPublic(params Parameters, pk *PublicKey, seed []byte) *FheRNGPublic {
|
||||
state := sha256.Sum256(seed)
|
||||
return &FheRNGPublic{
|
||||
params: params,
|
||||
enc: NewBitwisePublicEncryptor(params, pk),
|
||||
state: state,
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// advance advances the PRNG state and returns the next 32 bytes of randomness
|
||||
func (rng *FheRNG) advance() [32]byte {
|
||||
// Combine state with counter
|
||||
var data [40]byte
|
||||
copy(data[:32], rng.state[:])
|
||||
binary.LittleEndian.PutUint64(data[32:], rng.counter)
|
||||
rng.counter++
|
||||
|
||||
// Update state
|
||||
rng.state = sha256.Sum256(data[:])
|
||||
return rng.state
|
||||
}
|
||||
|
||||
// advance advances the public RNG state
|
||||
func (rng *FheRNGPublic) advance() [32]byte {
|
||||
var data [40]byte
|
||||
copy(data[:32], rng.state[:])
|
||||
binary.LittleEndian.PutUint64(data[32:], rng.counter)
|
||||
rng.counter++
|
||||
rng.state = sha256.Sum256(data[:])
|
||||
return rng.state
|
||||
}
|
||||
|
||||
// RandomBit generates a single encrypted random bit
|
||||
// Note: Uses secret key encryption which cannot fail with valid parameters
|
||||
func (rng *FheRNG) RandomBit() *Ciphertext {
|
||||
random := rng.advance()
|
||||
bit := (random[0] & 1) == 1
|
||||
return rng.enc.enc.Encrypt(bit)
|
||||
}
|
||||
|
||||
// RandomBit generates a single encrypted random bit using public key
|
||||
func (rng *FheRNGPublic) RandomBit() (*Ciphertext, error) {
|
||||
random := rng.advance()
|
||||
bit := (random[0] & 1) == 1
|
||||
ct, err := rng.enc.Encrypt(bit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("random bit encrypt: %w", err)
|
||||
}
|
||||
return ct, nil
|
||||
}
|
||||
|
||||
// RandomUint generates an encrypted random integer of the specified type
|
||||
// Note: Uses secret key encryption which cannot fail with valid parameters
|
||||
func (rng *FheRNG) RandomUint(t FheUintType) *BitCiphertext {
|
||||
numBits := t.NumBits()
|
||||
bits := make([]*Ciphertext, numBits)
|
||||
|
||||
// Get enough random bytes
|
||||
bytesNeeded := (numBits + 7) / 8
|
||||
var randomBytes []byte
|
||||
|
||||
for len(randomBytes) < bytesNeeded {
|
||||
random := rng.advance()
|
||||
randomBytes = append(randomBytes, random[:]...)
|
||||
}
|
||||
|
||||
// Encrypt each bit
|
||||
for i := 0; i < numBits; i++ {
|
||||
byteIdx := i / 8
|
||||
bitIdx := i % 8
|
||||
bit := (randomBytes[byteIdx] >> bitIdx) & 1
|
||||
bits[i] = rng.enc.enc.Encrypt(bit == 1)
|
||||
}
|
||||
|
||||
return &BitCiphertext{
|
||||
bits: bits,
|
||||
numBits: numBits,
|
||||
fheType: t,
|
||||
}
|
||||
}
|
||||
|
||||
// RandomUint generates an encrypted random integer using public key
|
||||
func (rng *FheRNGPublic) RandomUint(t FheUintType) (*BitCiphertext, error) {
|
||||
numBits := t.NumBits()
|
||||
bits := make([]*Ciphertext, numBits)
|
||||
|
||||
bytesNeeded := (numBits + 7) / 8
|
||||
var randomBytes []byte
|
||||
|
||||
for len(randomBytes) < bytesNeeded {
|
||||
random := rng.advance()
|
||||
randomBytes = append(randomBytes, random[:]...)
|
||||
}
|
||||
|
||||
for i := 0; i < numBits; i++ {
|
||||
byteIdx := i / 8
|
||||
bitIdx := i % 8
|
||||
bit := (randomBytes[byteIdx] >> bitIdx) & 1
|
||||
ct, err := rng.enc.Encrypt(bit == 1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("random uint encrypt bit %d: %w", i, err)
|
||||
}
|
||||
bits[i] = ct
|
||||
}
|
||||
|
||||
return &BitCiphertext{
|
||||
bits: bits,
|
||||
numBits: numBits,
|
||||
fheType: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RandomBounded generates an encrypted random integer in range [0, bound)
|
||||
// Uses rejection sampling to ensure uniform distribution
|
||||
// Note: This reveals the number of attempts but not the final value
|
||||
func (rng *FheRNG) RandomBounded(t FheUintType, bound uint64) *BitCiphertext {
|
||||
// For now, just generate a random value and let the caller handle modular reduction
|
||||
// True rejection sampling would require homomorphic comparison which is expensive
|
||||
// The caller can use eval.Mod() or similar operations if needed
|
||||
return rng.RandomUint(t)
|
||||
}
|
||||
|
||||
// Counter returns the current RNG counter (for verification/debugging)
|
||||
func (rng *FheRNG) Counter() uint64 {
|
||||
return rng.counter
|
||||
}
|
||||
|
||||
// Counter returns the current RNG counter
|
||||
func (rng *FheRNGPublic) Counter() uint64 {
|
||||
return rng.counter
|
||||
}
|
||||
|
||||
// Reseed reseeds the RNG with a new seed
|
||||
func (rng *FheRNG) Reseed(seed []byte) {
|
||||
rng.state = sha256.Sum256(seed)
|
||||
rng.counter = 0
|
||||
}
|
||||
|
||||
// Reseed reseeds the public RNG with a new seed
|
||||
func (rng *FheRNGPublic) Reseed(seed []byte) {
|
||||
rng.state = sha256.Sum256(seed)
|
||||
rng.counter = 0
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
// Package fhe - Security Levels
|
||||
//
|
||||
// This file defines standard security parameter sets that are compatible
|
||||
// across both Go (luxfi/fhe) and C++ (OpenFHE) implementations.
|
||||
//
|
||||
// # Security Levels
|
||||
//
|
||||
// All parameter sets target specific security levels against classical and/or
|
||||
// quantum attacks, with varying performance characteristics.
|
||||
//
|
||||
// # Classical vs Quantum Security
|
||||
//
|
||||
// - STD128: 128-bit classical security
|
||||
// - STD128Q: 128-bit quantum security (post-quantum resistant)
|
||||
// - STD192: 192-bit classical security
|
||||
// - STD192Q: 192-bit quantum security
|
||||
// - STD256: 256-bit classical security
|
||||
// - STD256Q: 256-bit quantum security
|
||||
//
|
||||
// # Bootstrapping Methods
|
||||
//
|
||||
// - AP: Ducas-Micciancio variant (original TFHE)
|
||||
// - GINX: Chillotti-Gama-Georgieva-Izabachene variant
|
||||
// - LMKCDEY: Lee-Micciancio-Kim-Choi-Deryabin-Eom-Yoo variant (fastest)
|
||||
//
|
||||
// The LMKCDEY method uses Gaussian secrets which enables smaller parameters
|
||||
// and faster bootstrapping compared to AP/GINX with uniform secrets.
|
||||
//
|
||||
// # Parameter Set Naming Convention
|
||||
//
|
||||
// Format: STD{bits}[Q][_{inputs}][_LMKCDEY]
|
||||
//
|
||||
// - bits: Security level (128, 192, 256)
|
||||
// - Q: Quantum-resistant (post-quantum)
|
||||
// - inputs: Number of gate inputs (default 2, can be 3 or 4)
|
||||
// - LMKCDEY: Optimized for LMKCDEY bootstrapping method
|
||||
//
|
||||
// # OpenFHE Compatibility
|
||||
//
|
||||
// These parameter sets directly correspond to OpenFHE's BINFHE_PARAMSET enum:
|
||||
//
|
||||
// Go C++ (OpenFHE) Security Failure Prob
|
||||
// ------------------------------------------------------------------
|
||||
// STD128_LMKCDEY STD128_LMKCDEY 128-bit 2^(-55)
|
||||
// STD128Q_LMKCDEY STD128Q_LMKCDEY 128-bit PQ 2^(-50)
|
||||
// STD192_LMKCDEY STD192_LMKCDEY 192-bit 2^(-60)
|
||||
// STD192Q_LMKCDEY STD192Q_LMKCDEY 192-bit PQ 2^(-70)
|
||||
// STD256_LMKCDEY STD256_LMKCDEY 256-bit 2^(-50)
|
||||
// STD256Q_LMKCDEY STD256Q_LMKCDEY 256-bit PQ 2^(-60)
|
||||
//
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
package fhe
|
||||
|
||||
// SecurityLevel represents the target security level
|
||||
type SecurityLevel int
|
||||
|
||||
const (
|
||||
// Security128 provides 128-bit classical security
|
||||
Security128 SecurityLevel = 128
|
||||
// Security128Q provides 128-bit post-quantum security
|
||||
Security128Q SecurityLevel = 1128
|
||||
// Security192 provides 192-bit classical security
|
||||
Security192 SecurityLevel = 192
|
||||
// Security192Q provides 192-bit post-quantum security
|
||||
Security192Q SecurityLevel = 1192
|
||||
// Security256 provides 256-bit classical security
|
||||
Security256 SecurityLevel = 256
|
||||
// Security256Q provides 256-bit post-quantum security
|
||||
Security256Q SecurityLevel = 1256
|
||||
)
|
||||
|
||||
// BootstrapMethod represents the bootstrapping algorithm
|
||||
type BootstrapMethod int
|
||||
|
||||
const (
|
||||
// MethodAP is the Ducas-Micciancio (original TFHE) method
|
||||
MethodAP BootstrapMethod = iota
|
||||
// MethodGINX is the Chillotti-Gama-Georgieva-Izabachene method
|
||||
MethodGINX
|
||||
// MethodLMKCDEY is the Lee-Micciancio-Kim-Choi-Deryabin-Eom-Yoo method (fastest)
|
||||
MethodLMKCDEY
|
||||
)
|
||||
|
||||
// SecretDistribution represents the type of secret key distribution
|
||||
type SecretDistribution int
|
||||
|
||||
const (
|
||||
// UniformTernary uses uniform ternary secrets (-1, 0, 1)
|
||||
UniformTernary SecretDistribution = iota
|
||||
// Gaussian uses Gaussian-distributed secrets
|
||||
Gaussian
|
||||
)
|
||||
|
||||
// SecurityParams defines a complete security parameter specification
|
||||
// matching OpenFHE's internal parameter structure
|
||||
type SecurityParams struct {
|
||||
// Name is the parameter set identifier
|
||||
Name string
|
||||
// Security is the target security level
|
||||
Security SecurityLevel
|
||||
// Method is the bootstrapping method
|
||||
Method BootstrapMethod
|
||||
// LogQ is the log2 of the ciphertext modulus
|
||||
LogQ int
|
||||
// RingDim is the polynomial ring dimension (N)
|
||||
RingDim int
|
||||
// LWEDim is the LWE dimension (n)
|
||||
LWEDim int
|
||||
// BootstrapBase is the decomposition base for bootstrapping
|
||||
BootstrapBase int
|
||||
// KeySwitchBase is the decomposition base for key switching
|
||||
KeySwitchBase int
|
||||
// SecretDist is the secret key distribution
|
||||
SecretDist SecretDistribution
|
||||
// FailureProb is the approximate log2 of failure probability
|
||||
FailureProb int
|
||||
}
|
||||
|
||||
// Standard security parameter sets matching OpenFHE
|
||||
// These are the recommended parameter sets for production use
|
||||
var (
|
||||
// STD128_LMKCDEY provides 128-bit classical security with LMKCDEY bootstrapping
|
||||
// This is the recommended default for most applications.
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD128_LMKCDEY
|
||||
STD128_LMKCDEY = SecurityParams{
|
||||
Name: "STD128_LMKCDEY",
|
||||
Security: Security128,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 28,
|
||||
RingDim: 1024,
|
||||
LWEDim: 447,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 1024,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -55,
|
||||
}
|
||||
|
||||
// STD128Q_LMKCDEY provides 128-bit post-quantum security
|
||||
// Use this for applications requiring quantum resistance.
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD128Q_LMKCDEY
|
||||
STD128Q_LMKCDEY = SecurityParams{
|
||||
Name: "STD128Q_LMKCDEY",
|
||||
Security: Security128Q,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 27,
|
||||
RingDim: 1024,
|
||||
LWEDim: 483,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 512,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -50,
|
||||
}
|
||||
|
||||
// STD192_LMKCDEY provides 192-bit classical security
|
||||
// Higher security with larger parameters.
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD192_LMKCDEY
|
||||
STD192_LMKCDEY = SecurityParams{
|
||||
Name: "STD192_LMKCDEY",
|
||||
Security: Security192,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 39,
|
||||
RingDim: 2048,
|
||||
LWEDim: 716,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 1048576,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -60,
|
||||
}
|
||||
|
||||
// STD192Q_LMKCDEY provides 192-bit post-quantum security
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD192Q_LMKCDEY
|
||||
STD192Q_LMKCDEY = SecurityParams{
|
||||
Name: "STD192Q_LMKCDEY",
|
||||
Security: Security192Q,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 36,
|
||||
RingDim: 2048,
|
||||
LWEDim: 776,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 262144,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -70,
|
||||
}
|
||||
|
||||
// STD256_LMKCDEY provides 256-bit classical security
|
||||
// Maximum security level.
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD256_LMKCDEY
|
||||
STD256_LMKCDEY = SecurityParams{
|
||||
Name: "STD256_LMKCDEY",
|
||||
Security: Security256,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 30,
|
||||
RingDim: 2048,
|
||||
LWEDim: 939,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 1024,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -50,
|
||||
}
|
||||
|
||||
// STD256Q_LMKCDEY provides 256-bit post-quantum security
|
||||
// Maximum security with quantum resistance.
|
||||
// OpenFHE equivalent: BINFHE_PARAMSET::STD256Q_LMKCDEY
|
||||
STD256Q_LMKCDEY = SecurityParams{
|
||||
Name: "STD256Q_LMKCDEY",
|
||||
Security: Security256Q,
|
||||
Method: MethodLMKCDEY,
|
||||
LogQ: 28,
|
||||
RingDim: 2048,
|
||||
LWEDim: 1019,
|
||||
BootstrapBase: 32,
|
||||
KeySwitchBase: 1024,
|
||||
SecretDist: Gaussian,
|
||||
FailureProb: -60,
|
||||
}
|
||||
)
|
||||
|
||||
// AllSecurityParams returns all available security parameter sets
|
||||
func AllSecurityParams() []SecurityParams {
|
||||
return []SecurityParams{
|
||||
STD128_LMKCDEY,
|
||||
STD128Q_LMKCDEY,
|
||||
STD192_LMKCDEY,
|
||||
STD192Q_LMKCDEY,
|
||||
STD256_LMKCDEY,
|
||||
STD256Q_LMKCDEY,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSecurityParams returns the SecurityParams for a given name
|
||||
func GetSecurityParams(name string) (SecurityParams, bool) {
|
||||
for _, p := range AllSecurityParams() {
|
||||
if p.Name == name {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
return SecurityParams{}, false
|
||||
}
|
||||
|
||||
// ToParametersLiteral converts SecurityParams to ParametersLiteral
|
||||
// for use with the existing FHE implementation
|
||||
func (sp SecurityParams) ToParametersLiteral() ParametersLiteral {
|
||||
// Calculate Q from LogQ
|
||||
q := uint64(1) << sp.LogQ
|
||||
|
||||
// For compatibility with existing code, we use ring dimension for both
|
||||
// LWE and BR when they match OpenFHE's LMKCDEY parameters
|
||||
logN := 0
|
||||
for n := sp.RingDim; n > 1; n >>= 1 {
|
||||
logN++
|
||||
}
|
||||
|
||||
return ParametersLiteral{
|
||||
LogNLWE: logN,
|
||||
LogNBR: logN,
|
||||
QLWE: q,
|
||||
QBR: q,
|
||||
BaseTwoDecomposition: 5, // log2(32) = 5 for LMKCDEY
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
// Copyright (c) 2025, Lux Industries Inc
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package fhe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/luxfi/lattice/v7/core/rlwe"
|
||||
"github.com/luxfi/lattice/v7/ring"
|
||||
)
|
||||
|
||||
// ========== Secret Key Serialization ==========
|
||||
|
||||
// MarshalBinary serializes the secret key to binary format
|
||||
func (sk *SecretKey) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Serialize SKLWE
|
||||
if err := serializeSecretKey(&buf, sk.SKLWE); err != nil {
|
||||
return nil, fmt.Errorf("serialize SKLWE: %w", err)
|
||||
}
|
||||
|
||||
// Serialize SKBR
|
||||
if err := serializeSecretKey(&buf, sk.SKBR); err != nil {
|
||||
return nil, fmt.Errorf("serialize SKBR: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary deserializes the secret key from binary format
|
||||
func (sk *SecretKey) UnmarshalBinary(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
// Deserialize SKLWE
|
||||
sklwe, err := deserializeSecretKey(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize SKLWE: %w", err)
|
||||
}
|
||||
sk.SKLWE = sklwe
|
||||
|
||||
// Deserialize SKBR
|
||||
skbr, err := deserializeSecretKey(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize SKBR: %w", err)
|
||||
}
|
||||
sk.SKBR = skbr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializeSecretKey(w io.Writer, sk *rlwe.SecretKey) error {
|
||||
enc := gob.NewEncoder(w)
|
||||
return enc.Encode(sk)
|
||||
}
|
||||
|
||||
func deserializeSecretKey(r io.Reader) (*rlwe.SecretKey, error) {
|
||||
dec := gob.NewDecoder(r)
|
||||
var sk rlwe.SecretKey
|
||||
if err := dec.Decode(&sk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &sk, nil
|
||||
}
|
||||
|
||||
// ========== Public Key Serialization ==========
|
||||
|
||||
// MarshalBinary serializes the public key to binary format
|
||||
func (pk *PublicKey) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Serialize PKLWE using gob
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(pk.PKLWE); err != nil {
|
||||
return nil, fmt.Errorf("serialize PKLWE: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary deserializes the public key from binary format
|
||||
func (pk *PublicKey) UnmarshalBinary(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
dec := gob.NewDecoder(buf)
|
||||
var pklwe rlwe.PublicKey
|
||||
if err := dec.Decode(&pklwe); err != nil {
|
||||
return fmt.Errorf("deserialize PKLWE: %w", err)
|
||||
}
|
||||
pk.PKLWE = &pklwe
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== Bootstrap Key Serialization ==========
|
||||
|
||||
// BootstrapKeyData holds serializable bootstrap key data
|
||||
type BootstrapKeyData struct {
|
||||
BRKData []byte
|
||||
TestPolyAND []byte
|
||||
TestPolyOR []byte
|
||||
TestPolyNAND []byte
|
||||
TestPolyNOR []byte
|
||||
}
|
||||
|
||||
// MarshalBinary serializes the bootstrap key to binary format
|
||||
func (bsk *BootstrapKey) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Serialize BRK using gob
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(bsk.BRK); err != nil {
|
||||
return nil, fmt.Errorf("serialize BRK: %w", err)
|
||||
}
|
||||
|
||||
// Serialize test polynomials
|
||||
if err := serializePoly(&buf, bsk.TestPolyAND); err != nil {
|
||||
return nil, fmt.Errorf("serialize TestPolyAND: %w", err)
|
||||
}
|
||||
if err := serializePoly(&buf, bsk.TestPolyOR); err != nil {
|
||||
return nil, fmt.Errorf("serialize TestPolyOR: %w", err)
|
||||
}
|
||||
if err := serializePoly(&buf, bsk.TestPolyNAND); err != nil {
|
||||
return nil, fmt.Errorf("serialize TestPolyNAND: %w", err)
|
||||
}
|
||||
if err := serializePoly(&buf, bsk.TestPolyNOR); err != nil {
|
||||
return nil, fmt.Errorf("serialize TestPolyNOR: %w", err)
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary deserializes the bootstrap key from binary format
|
||||
func (bsk *BootstrapKey) UnmarshalBinary(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
// Deserialize BRK
|
||||
dec := gob.NewDecoder(buf)
|
||||
if err := dec.Decode(&bsk.BRK); err != nil {
|
||||
return fmt.Errorf("deserialize BRK: %w", err)
|
||||
}
|
||||
|
||||
// Deserialize test polynomials
|
||||
var err error
|
||||
bsk.TestPolyAND, err = deserializePoly(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize TestPolyAND: %w", err)
|
||||
}
|
||||
bsk.TestPolyOR, err = deserializePoly(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize TestPolyOR: %w", err)
|
||||
}
|
||||
bsk.TestPolyNAND, err = deserializePoly(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize TestPolyNAND: %w", err)
|
||||
}
|
||||
bsk.TestPolyNOR, err = deserializePoly(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deserialize TestPolyNOR: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func serializePoly(w io.Writer, poly *ring.Poly) error {
|
||||
// Write number of levels
|
||||
numLevels := len(poly.Coeffs)
|
||||
if err := binary.Write(w, binary.LittleEndian, uint32(numLevels)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, coeffs := range poly.Coeffs {
|
||||
// Write number of coefficients
|
||||
if err := binary.Write(w, binary.LittleEndian, uint32(len(coeffs))); err != nil {
|
||||
return err
|
||||
}
|
||||
// Write coefficients
|
||||
for _, c := range coeffs {
|
||||
if err := binary.Write(w, binary.LittleEndian, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deserializePoly(r io.Reader) (*ring.Poly, error) {
|
||||
var numLevels uint32
|
||||
if err := binary.Read(r, binary.LittleEndian, &numLevels); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
coeffs := make([][]uint64, numLevels)
|
||||
for i := range coeffs {
|
||||
var numCoeffs uint32
|
||||
if err := binary.Read(r, binary.LittleEndian, &numCoeffs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
coeffs[i] = make([]uint64, numCoeffs)
|
||||
for j := range coeffs[i] {
|
||||
if err := binary.Read(r, binary.LittleEndian, &coeffs[i][j]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &ring.Poly{Coeffs: coeffs}, nil
|
||||
}
|
||||
|
||||
// ========== Ciphertext Serialization ==========
|
||||
|
||||
// MarshalBinary serializes a ciphertext to binary format
|
||||
func (ct *Ciphertext) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := gob.NewEncoder(&buf)
|
||||
if err := enc.Encode(ct.Ciphertext); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary deserializes a ciphertext from binary format
|
||||
func (ct *Ciphertext) UnmarshalBinary(data []byte) error {
|
||||
dec := gob.NewDecoder(bytes.NewReader(data))
|
||||
ct.Ciphertext = new(rlwe.Ciphertext)
|
||||
return dec.Decode(ct.Ciphertext)
|
||||
}
|
||||
|
||||
// ========== BitCiphertext Serialization ==========
|
||||
|
||||
// MarshalBinary serializes a BitCiphertext to binary format
|
||||
func (bc *BitCiphertext) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
// Write metadata
|
||||
if err := binary.Write(&buf, binary.LittleEndian, uint32(bc.numBits)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Write(&buf, binary.LittleEndian, uint8(bc.fheType)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write each bit ciphertext
|
||||
for i, bit := range bc.bits {
|
||||
bitData, err := bit.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bit %d: %w", i, err)
|
||||
}
|
||||
// Write length prefix
|
||||
if err := binary.Write(&buf, binary.LittleEndian, uint32(len(bitData))); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := buf.Write(bitData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// UnmarshalBinary deserializes a BitCiphertext from binary format
|
||||
func (bc *BitCiphertext) UnmarshalBinary(data []byte) error {
|
||||
buf := bytes.NewReader(data)
|
||||
|
||||
// Read metadata
|
||||
var numBits uint32
|
||||
if err := binary.Read(buf, binary.LittleEndian, &numBits); err != nil {
|
||||
return err
|
||||
}
|
||||
bc.numBits = int(numBits)
|
||||
|
||||
var fheType uint8
|
||||
if err := binary.Read(buf, binary.LittleEndian, &fheType); err != nil {
|
||||
return err
|
||||
}
|
||||
bc.fheType = FheUintType(fheType)
|
||||
|
||||
// Read each bit ciphertext
|
||||
bc.bits = make([]*Ciphertext, bc.numBits)
|
||||
for i := 0; i < bc.numBits; i++ {
|
||||
var bitLen uint32
|
||||
if err := binary.Read(buf, binary.LittleEndian, &bitLen); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bitData := make([]byte, bitLen)
|
||||
if _, err := io.ReadFull(buf, bitData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bc.bits[i] = new(Ciphertext)
|
||||
if err := bc.bits[i].UnmarshalBinary(bitData); err != nil {
|
||||
return fmt.Errorf("bit %d: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ========== Compact Serialization for Network Transfer ==========
|
||||
|
||||
// CompactCiphertext is a space-efficient representation for network transfer
|
||||
type CompactCiphertext struct {
|
||||
Data []byte
|
||||
NumBits int
|
||||
Type FheUintType
|
||||
}
|
||||
|
||||
// ToCompact converts a BitCiphertext to a compact format
|
||||
func (bc *BitCiphertext) ToCompact() (*CompactCiphertext, error) {
|
||||
data, err := bc.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &CompactCiphertext{
|
||||
Data: data,
|
||||
NumBits: bc.numBits,
|
||||
Type: bc.fheType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// FromCompact creates a BitCiphertext from compact format
|
||||
func FromCompact(cc *CompactCiphertext) (*BitCiphertext, error) {
|
||||
bc := new(BitCiphertext)
|
||||
if err := bc.UnmarshalBinary(cc.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bc, nil
|
||||
}
|
||||
+561
@@ -0,0 +1,561 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
// ShortInt represents a small encrypted integer (1-4 bits) in a single ciphertext.
|
||||
// Uses LUT-based programmable bootstrapping for operations.
|
||||
// This is the building block for larger radix integers.
|
||||
type ShortInt struct {
|
||||
ct *rlwe.Ciphertext
|
||||
msgBits int // Number of message bits (1-4)
|
||||
msgSpace int // Message space = 2^msgBits
|
||||
}
|
||||
|
||||
// ShortIntParams holds parameters for shortint operations
|
||||
type ShortIntParams struct {
|
||||
params Parameters
|
||||
msgBits int
|
||||
msgSpace int
|
||||
scale float64 // Q / (2 * msgSpace) for encoding
|
||||
}
|
||||
|
||||
// NewShortIntParams creates parameters for shortint with given message bits
|
||||
func NewShortIntParams(params Parameters, msgBits int) (*ShortIntParams, error) {
|
||||
if msgBits < 1 || msgBits > 4 {
|
||||
return nil, fmt.Errorf("msgBits must be 1-4, got %d", msgBits)
|
||||
}
|
||||
msgSpace := 1 << msgBits
|
||||
return &ShortIntParams{
|
||||
params: params,
|
||||
msgBits: msgBits,
|
||||
msgSpace: msgSpace,
|
||||
scale: float64(params.QLWE()) / float64(2*msgSpace),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ShortIntEncryptor encrypts small integers
|
||||
type ShortIntEncryptor struct {
|
||||
params *ShortIntParams
|
||||
encryptor *rlwe.Encryptor
|
||||
}
|
||||
|
||||
// NewShortIntEncryptor creates a new shortint encryptor
|
||||
func NewShortIntEncryptor(params *ShortIntParams, sk *SecretKey) *ShortIntEncryptor {
|
||||
return &ShortIntEncryptor{
|
||||
params: params,
|
||||
encryptor: rlwe.NewEncryptor(params.params.paramsLWE, sk.SKLWE),
|
||||
}
|
||||
}
|
||||
|
||||
// Encrypt encrypts a small integer value
|
||||
func (enc *ShortIntEncryptor) Encrypt(value int) (*ShortInt, error) {
|
||||
if value < 0 || value >= enc.params.msgSpace {
|
||||
return nil, fmt.Errorf("value %d out of range [0, %d)", value, enc.params.msgSpace)
|
||||
}
|
||||
|
||||
pt := rlwe.NewPlaintext(enc.params.params.paramsLWE, enc.params.params.paramsLWE.MaxLevel())
|
||||
|
||||
// Encode: value * (Q / (2*msgSpace)) centers message in [0, Q/2) range
|
||||
// This leaves room for carry in the upper half
|
||||
// Use pure integer arithmetic to avoid floating-point precision issues in crypto
|
||||
q := enc.params.params.QLWE()
|
||||
msgSpace := uint64(enc.params.msgSpace)
|
||||
// encoded = value * q / (2 * msgSpace)
|
||||
// To avoid overflow, compute as: (value * (q / msgSpace)) / 2
|
||||
// Since q is always divisible by powers of 2 and msgSpace is a power of 2,
|
||||
// this is exact integer arithmetic
|
||||
encoded := (uint64(value) * (q / (2 * msgSpace))) % q
|
||||
pt.Value.Coeffs[0][0] = encoded
|
||||
|
||||
enc.params.params.paramsLWE.RingQ().NTT(pt.Value, pt.Value)
|
||||
|
||||
ct := rlwe.NewCiphertext(enc.params.params.paramsLWE, 1, enc.params.params.paramsLWE.MaxLevel())
|
||||
if err := enc.encryptor.Encrypt(pt, ct); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: ct,
|
||||
msgBits: enc.params.msgBits,
|
||||
msgSpace: enc.params.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ShortIntDecryptor decrypts small integers
|
||||
type ShortIntDecryptor struct {
|
||||
params *ShortIntParams
|
||||
decryptor *rlwe.Decryptor
|
||||
ringQ *ring.Ring
|
||||
}
|
||||
|
||||
// NewShortIntDecryptor creates a new shortint decryptor
|
||||
func NewShortIntDecryptor(params *ShortIntParams, sk *SecretKey) *ShortIntDecryptor {
|
||||
return &ShortIntDecryptor{
|
||||
params: params,
|
||||
decryptor: rlwe.NewDecryptor(params.params.paramsLWE, sk.SKLWE),
|
||||
ringQ: params.params.paramsLWE.RingQ(),
|
||||
}
|
||||
}
|
||||
|
||||
// Decrypt decrypts a shortint to its integer value
|
||||
func (dec *ShortIntDecryptor) Decrypt(si *ShortInt) int {
|
||||
pt := rlwe.NewPlaintext(dec.params.params.paramsLWE, si.ct.Level())
|
||||
dec.decryptor.Decrypt(si.ct, pt)
|
||||
|
||||
if pt.IsNTT {
|
||||
dec.ringQ.INTT(pt.Value, pt.Value)
|
||||
}
|
||||
|
||||
// Decode: round(value * 2*msgSpace / Q)
|
||||
c := pt.Value.Coeffs[0][0]
|
||||
q := dec.params.params.QLWE()
|
||||
|
||||
// Scale and round to nearest integer
|
||||
scaled := float64(c) * float64(2*si.msgSpace) / float64(q)
|
||||
value := int(scaled + 0.5)
|
||||
|
||||
// Handle wrap-around for values near Q
|
||||
if value >= si.msgSpace {
|
||||
value = value % si.msgSpace
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// ShortIntEvaluator performs operations on shortints
|
||||
// SECURITY: This evaluator does NOT require the secret key.
|
||||
// It uses sample extraction and key switching for bootstrapping.
|
||||
type ShortIntEvaluator struct {
|
||||
params *ShortIntParams
|
||||
eval *blindrot.Evaluator
|
||||
bsk *BootstrapKey
|
||||
ringQLWE *ring.Ring
|
||||
ringQBR *ring.Ring
|
||||
|
||||
// Key switching evaluator (BR -> LWE)
|
||||
ksEval *rlwe.Evaluator
|
||||
|
||||
// Precomputed LUT polynomials
|
||||
lutAdd map[int]*ring.Poly // LUT for (a + b) mod msgSpace
|
||||
lutSub map[int]*ring.Poly // LUT for (a - b) mod msgSpace
|
||||
lutMul map[int]*ring.Poly // LUT for (a * b) mod msgSpace
|
||||
lutNeg *ring.Poly // LUT for -a mod msgSpace
|
||||
lutCarry *ring.Poly // LUT for carry bit
|
||||
}
|
||||
|
||||
// NewShortIntEvaluator creates a new shortint evaluator
|
||||
// SECURITY: No secret key is required - bootstrapping uses public key switching.
|
||||
func NewShortIntEvaluator(params *ShortIntParams, bsk *BootstrapKey) *ShortIntEvaluator {
|
||||
// Create key switching evaluator using the key switch key in bootstrap key
|
||||
var ksEval *rlwe.Evaluator
|
||||
if bsk.KSK != nil {
|
||||
ksEval = rlwe.NewEvaluator(params.params.paramsBR, nil)
|
||||
}
|
||||
|
||||
eval := &ShortIntEvaluator{
|
||||
params: params,
|
||||
eval: blindrot.NewEvaluator(params.params.paramsBR, params.params.paramsLWE),
|
||||
bsk: bsk,
|
||||
ringQLWE: params.params.paramsLWE.RingQ(),
|
||||
ringQBR: params.params.paramsBR.RingQ(),
|
||||
ksEval: ksEval,
|
||||
lutAdd: make(map[int]*ring.Poly),
|
||||
lutSub: make(map[int]*ring.Poly),
|
||||
lutMul: make(map[int]*ring.Poly),
|
||||
}
|
||||
|
||||
// Precompute LUTs for each possible second operand
|
||||
eval.precomputeLUTs()
|
||||
|
||||
return eval
|
||||
}
|
||||
|
||||
// precomputeLUTs generates lookup tables for arithmetic operations
|
||||
func (eval *ShortIntEvaluator) precomputeLUTs() {
|
||||
msgSpace := eval.params.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.params.QBR()) / float64(2*msgSpace))
|
||||
ringQ := eval.ringQBR
|
||||
|
||||
// For each possible value of the second operand, create an addition LUT
|
||||
for b := 0; b < msgSpace; b++ {
|
||||
// Addition LUT: f(a) = (a + b) mod msgSpace
|
||||
addPoly := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
// x is normalized to [0, 1) representing a in [0, msgSpace)
|
||||
a := int((x + 1) * float64(msgSpace) / 2) // Map [-1,1] to [0, msgSpace)
|
||||
if a >= msgSpace {
|
||||
a = msgSpace - 1
|
||||
}
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
result := (a + b) % msgSpace
|
||||
// Map back to [-1, 1] range
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, ringQ, -1, 1)
|
||||
eval.lutAdd[b] = &addPoly
|
||||
|
||||
// Subtraction LUT: f(a) = (a - b) mod msgSpace
|
||||
subPoly := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
a := int((x + 1) * float64(msgSpace) / 2)
|
||||
if a >= msgSpace {
|
||||
a = msgSpace - 1
|
||||
}
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
result := (a - b + msgSpace) % msgSpace
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, ringQ, -1, 1)
|
||||
eval.lutSub[b] = &subPoly
|
||||
|
||||
// Multiplication LUT: f(a) = (a * b) mod msgSpace
|
||||
mulPoly := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
a := int((x + 1) * float64(msgSpace) / 2)
|
||||
if a >= msgSpace {
|
||||
a = msgSpace - 1
|
||||
}
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
result := (a * b) % msgSpace
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, ringQ, -1, 1)
|
||||
eval.lutMul[b] = &mulPoly
|
||||
}
|
||||
|
||||
// Negation LUT: f(a) = -a mod msgSpace
|
||||
negPoly := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
a := int((x + 1) * float64(msgSpace) / 2)
|
||||
if a >= msgSpace {
|
||||
a = msgSpace - 1
|
||||
}
|
||||
if a < 0 {
|
||||
a = 0
|
||||
}
|
||||
result := (msgSpace - a) % msgSpace
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, ringQ, -1, 1)
|
||||
eval.lutNeg = &negPoly
|
||||
|
||||
// Carry LUT: f(a+b) = 1 if a+b >= msgSpace, else 0
|
||||
// Used for radix addition
|
||||
carryPoly := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
// x represents sum in [-1, 1] normalized from [0, 2*msgSpace)
|
||||
sum := int((x + 1) * float64(msgSpace)) // Map to [0, 2*msgSpace)
|
||||
if sum >= msgSpace {
|
||||
return 1.0 // Carry = 1
|
||||
}
|
||||
return -1.0 // Carry = 0
|
||||
}, scale, ringQ, -1, 1)
|
||||
eval.lutCarry = &carryPoly
|
||||
}
|
||||
|
||||
// sampleExtractAndKeySwitch extracts the constant coefficient from an RLWE ciphertext
|
||||
// and key-switches it to an LWE ciphertext.
|
||||
//
|
||||
// SECURITY: This does NOT decrypt - uses sample extraction and key switching.
|
||||
func (eval *ShortIntEvaluator) sampleExtractAndKeySwitch(ctBR *rlwe.Ciphertext) (*rlwe.Ciphertext, error) {
|
||||
if eval.bsk.KSK == nil {
|
||||
return nil, fmt.Errorf("bootstrap key does not contain key switching key")
|
||||
}
|
||||
|
||||
levelBR := ctBR.Level()
|
||||
ringQBR := eval.ringQBR.AtLevel(levelBR)
|
||||
NBR := ringQBR.N()
|
||||
qBR := eval.params.params.QBR()
|
||||
|
||||
// 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 an LWE ciphertext in the BR dimension
|
||||
ctLWEBR := rlwe.NewCiphertext(eval.params.params.paramsBR, 1, levelBR)
|
||||
|
||||
// Sample extraction: LWE (b, a) where
|
||||
// b = c0[0]
|
||||
// a = (c1[0], -c1[N-1], -c1[N-2], ..., -c1[1])
|
||||
ctLWEBR.Value[0].Coeffs[0][0] = c0.Coeffs[0][0]
|
||||
ctLWEBR.Value[1].Coeffs[0][0] = c1.Coeffs[0][0]
|
||||
for i := 1; i < NBR; i++ {
|
||||
ctLWEBR.Value[1].Coeffs[0][i] = qBR - c1.Coeffs[0][NBR-i]
|
||||
}
|
||||
|
||||
// Zero out higher coefficients of c0
|
||||
for i := 1; i < NBR; i++ {
|
||||
ctLWEBR.Value[0].Coeffs[0][i] = 0
|
||||
}
|
||||
|
||||
// Convert to NTT for key switching
|
||||
ringQBR.NTT(ctLWEBR.Value[0], ctLWEBR.Value[0])
|
||||
ringQBR.NTT(ctLWEBR.Value[1], ctLWEBR.Value[1])
|
||||
ctLWEBR.IsNTT = true
|
||||
|
||||
// Key switch from SKBR to SKLWE
|
||||
ctLWE := rlwe.NewCiphertext(eval.params.params.paramsLWE, 1, eval.params.params.paramsLWE.MaxLevel())
|
||||
ctLWE.IsNTT = true
|
||||
|
||||
if err := eval.ksEval.ApplyEvaluationKey(ctLWEBR, eval.bsk.KSK, ctLWE); err != nil {
|
||||
return nil, fmt.Errorf("key switching failed: %w", err)
|
||||
}
|
||||
|
||||
// Scale from Q_BR to Q_LWE
|
||||
levelLWE := ctLWE.Level()
|
||||
ringQLWE := eval.ringQLWE.AtLevel(levelLWE)
|
||||
|
||||
if ctLWE.IsNTT {
|
||||
ringQLWE.INTT(ctLWE.Value[0], ctLWE.Value[0])
|
||||
ringQLWE.INTT(ctLWE.Value[1], ctLWE.Value[1])
|
||||
ctLWE.IsNTT = false
|
||||
}
|
||||
|
||||
qLWE := eval.params.params.QLWE()
|
||||
scaleFactor := float64(qLWE) / float64(qBR)
|
||||
|
||||
for i := 0; i < ringQLWE.N(); i++ {
|
||||
scaled0 := uint64(float64(ctLWE.Value[0].Coeffs[0][i]) * scaleFactor)
|
||||
scaled1 := uint64(float64(ctLWE.Value[1].Coeffs[0][i]) * scaleFactor)
|
||||
ctLWE.Value[0].Coeffs[0][i] = scaled0 % qLWE
|
||||
ctLWE.Value[1].Coeffs[0][i] = scaled1 % qLWE
|
||||
}
|
||||
|
||||
ringQLWE.NTT(ctLWE.Value[0], ctLWE.Value[0])
|
||||
ringQLWE.NTT(ctLWE.Value[1], ctLWE.Value[1])
|
||||
ctLWE.IsNTT = true
|
||||
|
||||
return ctLWE, nil
|
||||
}
|
||||
|
||||
// bootstrap performs LUT evaluation via programmable bootstrapping
|
||||
// SECURITY: This does NOT decrypt - uses sample extraction and key switching.
|
||||
func (eval *ShortIntEvaluator) bootstrap(ct *rlwe.Ciphertext, lut *ring.Poly) (*rlwe.Ciphertext, error) {
|
||||
testPolyMap := map[int]*ring.Poly{0: lut}
|
||||
|
||||
results, err := eval.eval.Evaluate(ct, testPolyMap, eval.bsk.BRK)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
|
||||
ctBR, ok := results[0]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bootstrap: no result for slot 0")
|
||||
}
|
||||
|
||||
// Sample extract and key switch (no decryption!)
|
||||
return eval.sampleExtractAndKeySwitch(ctBR)
|
||||
}
|
||||
|
||||
// ScalarAdd adds a plaintext value to a shortint using LWE additive homomorphism
|
||||
func (eval *ShortIntEvaluator) ScalarAdd(si *ShortInt, scalar int) (*ShortInt, error) {
|
||||
scalar = scalar % si.msgSpace
|
||||
if scalar < 0 {
|
||||
scalar += si.msgSpace
|
||||
}
|
||||
|
||||
// LWE is additively homomorphic - we can add scalar * scale to the ciphertext directly
|
||||
result := rlwe.NewCiphertext(eval.params.params.paramsLWE, 1, si.ct.Level())
|
||||
|
||||
// Copy ct to result
|
||||
result.Value[0] = *si.ct.Value[0].CopyNew()
|
||||
result.Value[1] = *si.ct.Value[1].CopyNew()
|
||||
result.IsNTT = si.ct.IsNTT
|
||||
|
||||
// Add scalar * scale to the constant term (b = Value[0])
|
||||
// Use pure integer arithmetic: scale = Q / (2 * msgSpace)
|
||||
q := eval.params.params.QLWE()
|
||||
msgSpace := uint64(eval.params.msgSpace)
|
||||
scalarEncoded := (uint64(scalar) * (q / (2 * msgSpace))) % q
|
||||
|
||||
if result.IsNTT {
|
||||
// Need to add in NTT domain - add to all coefficients
|
||||
for i := range result.Value[0].Coeffs[0] {
|
||||
result.Value[0].Coeffs[0][i] = (result.Value[0].Coeffs[0][i] + scalarEncoded) % q
|
||||
}
|
||||
} else {
|
||||
// Add to constant term only
|
||||
result.Value[0].Coeffs[0][0] = (result.Value[0].Coeffs[0][0] + scalarEncoded) % q
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: result,
|
||||
msgBits: si.msgBits,
|
||||
msgSpace: si.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ScalarSub subtracts a plaintext value from a shortint using LWE additive homomorphism
|
||||
func (eval *ShortIntEvaluator) ScalarSub(si *ShortInt, scalar int) (*ShortInt, error) {
|
||||
// Subtraction is addition of negation
|
||||
negScalar := (si.msgSpace - (scalar % si.msgSpace)) % si.msgSpace
|
||||
return eval.ScalarAdd(si, negScalar)
|
||||
}
|
||||
|
||||
// ScalarMul multiplies a shortint by a plaintext value
|
||||
func (eval *ShortIntEvaluator) ScalarMul(si *ShortInt, scalar int) (*ShortInt, error) {
|
||||
scalar = scalar % si.msgSpace
|
||||
if scalar < 0 {
|
||||
scalar += si.msgSpace
|
||||
}
|
||||
|
||||
lut := eval.lutMul[scalar]
|
||||
result, err := eval.bootstrap(si.ct, lut)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: result,
|
||||
msgBits: si.msgBits,
|
||||
msgSpace: si.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Neg negates a shortint
|
||||
func (eval *ShortIntEvaluator) Neg(si *ShortInt) (*ShortInt, error) {
|
||||
result, err := eval.bootstrap(si.ct, eval.lutNeg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: result,
|
||||
msgBits: si.msgBits,
|
||||
msgSpace: si.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EncryptTrivial creates a trivial encryption of a plaintext value
|
||||
// A trivial ciphertext is a ciphertext where the message is encoded without noise,
|
||||
// effectively embedding plaintext in ciphertext format for homomorphic operations.
|
||||
func (eval *ShortIntEvaluator) EncryptTrivial(value int) (*ShortInt, error) {
|
||||
value = value % eval.params.msgSpace
|
||||
if value < 0 {
|
||||
value += eval.params.msgSpace
|
||||
}
|
||||
|
||||
// Create a trivial ciphertext: (0, m * scale)
|
||||
// This is a "noiseless" encryption that can be used in homomorphic operations
|
||||
ct := rlwe.NewCiphertext(eval.params.params.paramsLWE, 1, eval.params.params.paramsLWE.MaxLevel())
|
||||
|
||||
// Encode the message value using pure integer arithmetic
|
||||
// scale = Q / (2 * msgSpace), so encoded = value * Q / (2 * msgSpace)
|
||||
q := eval.params.params.QLWE()
|
||||
msgSpace := uint64(eval.params.msgSpace)
|
||||
encoded := (uint64(value) * (q / (2 * msgSpace))) % q
|
||||
|
||||
// Set b = encoded (the message), a = 0
|
||||
// In NTT domain, this means setting all coefficients
|
||||
for i := range ct.Value[0].Coeffs[0] {
|
||||
ct.Value[0].Coeffs[0][i] = encoded % q
|
||||
}
|
||||
// a (Value[1]) is already zero from NewCiphertext
|
||||
|
||||
ct.IsNTT = true
|
||||
|
||||
return &ShortInt{
|
||||
ct: ct,
|
||||
msgBits: eval.params.msgBits,
|
||||
msgSpace: eval.params.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// addCiphertexts adds two LWE ciphertexts element-wise
|
||||
func (eval *ShortIntEvaluator) addCiphertexts(ct1, ct2 *rlwe.Ciphertext) *rlwe.Ciphertext {
|
||||
result := rlwe.NewCiphertext(eval.params.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 result
|
||||
}
|
||||
|
||||
// Add adds two shortints (with modular wrap)
|
||||
func (eval *ShortIntEvaluator) Add(a, b *ShortInt) (*ShortInt, error) {
|
||||
if a.msgSpace != b.msgSpace {
|
||||
return nil, fmt.Errorf("mismatched message spaces: %d vs %d", a.msgSpace, b.msgSpace)
|
||||
}
|
||||
|
||||
// Add ciphertexts directly, then bootstrap to reduce and refresh
|
||||
sum := eval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
// Create a modular reduction LUT
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.params.QBR()) / float64(2*msgSpace))
|
||||
modLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
// x represents sum in range [0, 2*msgSpace) normalized to [-1, 1]
|
||||
rawSum := int((x + 1) * float64(msgSpace)) // [0, 2*msgSpace)
|
||||
result := rawSum % msgSpace
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.ringQBR, -1, 1)
|
||||
|
||||
result, err := eval.bootstrap(sum, &modLUT)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: result,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddWithCarry adds two shortints and returns result and carry bit
|
||||
func (eval *ShortIntEvaluator) AddWithCarry(a, b *ShortInt) (*ShortInt, *Ciphertext, error) {
|
||||
if a.msgSpace != b.msgSpace {
|
||||
return nil, nil, fmt.Errorf("mismatched message spaces: %d vs %d", a.msgSpace, b.msgSpace)
|
||||
}
|
||||
|
||||
// Add ciphertexts
|
||||
sum := eval.addCiphertexts(a.ct, b.ct)
|
||||
|
||||
// Get modular result
|
||||
msgSpace := a.msgSpace
|
||||
scale := rlwe.NewScale(float64(eval.params.params.QBR()) / float64(2*msgSpace))
|
||||
|
||||
modLUT := blindrot.InitTestPolynomial(func(x float64) float64 {
|
||||
rawSum := int((x + 1) * float64(msgSpace))
|
||||
result := rawSum % msgSpace
|
||||
return float64(result)*2/float64(msgSpace) - 1
|
||||
}, scale, eval.ringQBR, -1, 1)
|
||||
|
||||
resultCt, err := eval.bootstrap(sum, &modLUT)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Get carry bit using carry LUT
|
||||
carryCt, err := eval.bootstrap(sum, eval.lutCarry)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &ShortInt{
|
||||
ct: resultCt,
|
||||
msgBits: a.msgBits,
|
||||
msgSpace: a.msgSpace,
|
||||
}, &Ciphertext{carryCt},
|
||||
nil
|
||||
}
|
||||
|
||||
// Sub subtracts b from a
|
||||
func (eval *ShortIntEvaluator) Sub(a, b *ShortInt) (*ShortInt, error) {
|
||||
// Negate b and add
|
||||
negB, err := eval.Neg(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eval.Add(a, negB)
|
||||
}
|
||||
Reference in New Issue
Block a user