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:
Zach Kelling
2026-01-03 19:11:38 -08:00
parent 41a570b699
commit 1ed73f84d7
28 changed files with 11359 additions and 0 deletions
Executable
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1003
View File
File diff suppressed because it is too large Load Diff
BIN
View File
Binary file not shown.
+99
View File
@@ -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
}
+96
View File
@@ -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
View File
@@ -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)
}
+406
View File
@@ -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
}
Executable
BIN
View File
Binary file not shown.
+284
View File
@@ -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
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+564
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+242
View File
@@ -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
View File
@@ -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, &notLUT)
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, &ltLUT)
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
View File
File diff suppressed because it is too large Load Diff
+704
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
+83
View File
@@ -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)
}
})
}
+188
View File
@@ -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
View File
@@ -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
}
}
+336
View File
@@ -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
View File
@@ -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)
}
Executable
BIN
View File
Binary file not shown.