mirror of
https://github.com/luxfi/fhe.git
synced 2026-07-26 23:16:08 +00:00
Adds the canonical noise-flooding threshold-decryption primitives that
upstream consumers (notably luxfi/threshold/protocols/tfhe) need to
implement true M-of-N FHE decryption without any party holding the
master key.
- ShareLWESecretKey: Shamir-splits the LWE secret coefficient-by-
coefficient over Z_q. No party ever sees the master.
- PartialDecryptLWE: party j computes d_j = c_1 · s_j + e_j with
fresh smudging noise from a discrete Gaussian.
- CombineLWE: integer-Lagrange (Bendlin-Damgård denominator clearing)
+ Δ^{-1} mod q to recover the plaintext polynomial.
- SmudgingSigma: tight noise calibration against the worst-case
integer Lagrange numerator, so Λ_max · σ · √t · 6 ≤ Q/16.
Decomplecting note: this LSSS-over-Z_q lives next to the existing
LSSS-over-RFC-3526-prime in pkg/threshold. The two are different
primitives — large-prime LSSS shares high-entropy secrets where the
secret space need not equal the FHE modulus; Z_q LSSS composes
directly with lattice arithmetic. Both stay in this package.
Decryption follows Bendlin-Damgård (TCC 2010, §4.2) and Asharov-Jain-
López-Alt-Tromer-Vaikuntanathan-Wichs (EUROCRYPT 2012, §4). Noise
flooding is the simulation-soundness mechanism.
Verified by round-trip tests at (t, n) ∈ {(2, 3), (5, 9), (11, 21)},
below-threshold negative test, share-doesn't-equal-master regression
guard, dedup guard, cross-parameter guard, and combine-determinism
test. All 8 new threshold tests pass.
Also adds public accessors:
- fhe.Parameters.ParamsLWE/ParamsBR — expose underlying rlwe.Parameters
so out-of-tree threshold callers can drive lattice primitives
against the same parameter set the encryptor uses.
- fhe.BitCiphertext.Bits / NewBitCiphertextFromBits — iterate per-bit
for threshold decryption without round-tripping MarshalBinary.
References:
- Bendlin, Damgård. Threshold Decryption and Zero-Knowledge Proofs
for Lattice-Based Cryptosystems. TCC 2010.
- Asharov, Jain, López-Alt, Tromer, Vaikuntanathan, Wichs. Multiparty
Computation with Low Communication, Computation and Interaction
via Threshold FHE. EUROCRYPT 2012.
- Mouchet, Troncoso-Pastoriza, Bossuat, Hubaux. Multiparty
Homomorphic Encryption from Ring-Learning-with-Errors. PETS 2021.
586 lines
20 KiB
Go
586 lines
20 KiB
Go
// 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 (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
|
|
"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"
|
|
"github.com/luxfi/lattice/v7/utils/sampling"
|
|
"golang.org/x/crypto/hkdf"
|
|
)
|
|
|
|
// 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=0x3FFFFFFFFED001 (prime, 54-bit, Q ≡ 1 mod 4096)
|
|
PN11QP54 = ParametersLiteral{
|
|
LogNLWE: 11, // Same as BR
|
|
LogNBR: 11,
|
|
QLWE: 0x3FFFFFFFFED001, // NTT-friendly prime ~2^54
|
|
QBR: 0x3FFFFFFFFED001, // Q ≡ 1 (mod 2*2048)
|
|
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()
|
|
}
|
|
|
|
// ParamsLWE returns the underlying rlwe.Parameters used for LWE bit ciphertexts.
|
|
// Exposed so out-of-tree consumers (notably pkg/threshold) can drive lattice
|
|
// primitives directly against the same parameter set used by Encryptor /
|
|
// Decryptor — no duplicate parameter construction, no ad-hoc Q recomputation.
|
|
func (p Parameters) ParamsLWE() rlwe.Parameters {
|
|
return p.paramsLWE
|
|
}
|
|
|
|
// ParamsBR returns the underlying rlwe.Parameters used for blind-rotation /
|
|
// bootstrapping. Exposed for the same reason as ParamsLWE.
|
|
func (p Parameters) ParamsBR() rlwe.Parameters {
|
|
return p.paramsBR
|
|
}
|
|
|
|
// 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
|
|
// TestPolyCMPCOMBINE is the test polynomial for comparison combine:
|
|
// output = isLess OR (isEqual AND bitLt)
|
|
// Uses weighted sum: 2*isLess + isEqual + bitLt >= 0
|
|
TestPolyCMPCOMBINE *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
|
|
// prngLWE and prngBR are non-nil only when the generator was constructed
|
|
// via NewKeyGeneratorFromSeed. They drive deterministic sampling of the
|
|
// secret-key coefficients in GenSecretKey.
|
|
prngLWE sampling.PRNG
|
|
prngBR sampling.PRNG
|
|
}
|
|
|
|
// 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]
|
|
}
|
|
}
|
|
|
|
// keygenHKDFInfoLWE is the HKDF info string for the LWE secret-key stream.
|
|
// Domain-separated from BR to ensure the two PRNG streams never collide.
|
|
const keygenHKDFInfoLWE = "LUX_FHE_KEYGEN_v1:LWE"
|
|
|
|
// keygenHKDFInfoBR is the HKDF info string for the blind-rotation secret-key
|
|
// stream. Domain-separated from LWE.
|
|
const keygenHKDFInfoBR = "LUX_FHE_KEYGEN_v1:BR"
|
|
|
|
// keygenHKDFSalt is a fixed salt used for HKDF-SHA256 extract. Treated as a
|
|
// network constant — changing it invalidates all keys derived from prior seeds.
|
|
var keygenHKDFSalt = []byte("LUX_FHE_KEYGEN_v1")
|
|
|
|
// NewKeyGeneratorFromSeed creates a key generator that deterministically
|
|
// derives the secret-key material from `seed`. All validators using the same
|
|
// seed produce identical secret keys (and therefore identical public/bootstrap
|
|
// keys), which is required for consensus.
|
|
//
|
|
// Derivation pipeline:
|
|
//
|
|
// prk = HKDF-Extract(SHA-256, salt=keygenHKDFSalt, ikm=seed)
|
|
// keyLWE = HKDF-Expand(prk, info="LUX_FHE_KEYGEN_v1:LWE", L=32)
|
|
// keyBR = HKDF-Expand(prk, info="LUX_FHE_KEYGEN_v1:BR", L=32)
|
|
//
|
|
// keyLWE and keyBR seed two independent blake2b-based KeyedPRNG streams which
|
|
// drive `ring.NewSampler` to fill the secret-key polynomial coefficients
|
|
// according to the parameter set's secret distribution `Xs`.
|
|
//
|
|
// WARNING: The seed is a network parameter. Changing it invalidates all
|
|
// existing ciphertexts. Use a domain-separated constant
|
|
// (e.g. "LUX_FHE_KEYGEN_v1").
|
|
func NewKeyGeneratorFromSeed(params Parameters, seed []byte) (*KeyGenerator, error) {
|
|
if len(seed) == 0 {
|
|
return nil, fmt.Errorf("fhe: NewKeyGeneratorFromSeed: empty seed")
|
|
}
|
|
|
|
// HKDF-SHA256 extract once, expand to two domain-separated 32-byte keys.
|
|
prk := hkdf.Extract(sha256.New, seed, keygenHKDFSalt)
|
|
keyLWE, err := hkdfExpand32(prk, keygenHKDFInfoLWE)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fhe: HKDF-Expand LWE: %w", err)
|
|
}
|
|
keyBR, err := hkdfExpand32(prk, keygenHKDFInfoBR)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fhe: HKDF-Expand BR: %w", err)
|
|
}
|
|
|
|
prngLWE, err := sampling.NewKeyedPRNG(keyLWE)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fhe: NewKeyedPRNG LWE: %w", err)
|
|
}
|
|
prngBR, err := sampling.NewKeyedPRNG(keyBR)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("fhe: NewKeyedPRNG BR: %w", err)
|
|
}
|
|
|
|
return &KeyGenerator{
|
|
params: params,
|
|
kgenLWE: rlwe.NewKeyGenerator(params.paramsLWE),
|
|
kgenBR: rlwe.NewKeyGenerator(params.paramsBR),
|
|
ringQBR: params.paramsBR.RingQ(),
|
|
scaleBR: float64(params.QBR()) / 8.0,
|
|
// Stash the per-stream PRNGs; GenSecretKey consumes them when set so
|
|
// the resulting secret key is fully deterministic for a given seed.
|
|
prngLWE: prngLWE,
|
|
prngBR: prngBR,
|
|
}, nil
|
|
}
|
|
|
|
// hkdfExpand32 expands `prk` to a 32-byte key using HKDF-SHA256 with the
|
|
// given info string.
|
|
func hkdfExpand32(prk []byte, info string) ([]byte, error) {
|
|
r := hkdf.Expand(sha256.New, prk, []byte(info))
|
|
out := make([]byte, 32)
|
|
if _, err := io.ReadFull(r, out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// sampleSecretKeyDeterministic fills `sk` in-place with secret-key
|
|
// coefficients drawn from the parameter set's `Xs` distribution using `prng`
|
|
// as the source of randomness. The polynomial is left in NTT + Montgomery
|
|
// form, matching the convention used by `rlwe.KeyGenerator.GenSecretKeyNew`.
|
|
func sampleSecretKeyDeterministic(params rlwe.Parameters, prng sampling.PRNG, sk *rlwe.SecretKey) error {
|
|
ringQP := params.RingQP()
|
|
|
|
// RingQ is always present; sample Xs into sk.Value.Q at level Q.
|
|
samplerQ, err := ring.NewSampler(prng, ringQP.RingQ, params.Xs(), false)
|
|
if err != nil {
|
|
return fmt.Errorf("fhe: ring.NewSampler Q: %w", err)
|
|
}
|
|
samplerQ.AtLevel(sk.LevelQ()).Read(sk.Value.Q)
|
|
ringQP.RingQ.AtLevel(sk.LevelQ()).NTT(sk.Value.Q, sk.Value.Q)
|
|
ringQP.RingQ.AtLevel(sk.LevelQ()).MForm(sk.Value.Q, sk.Value.Q)
|
|
|
|
// RingP is optional (only when special primes P are configured).
|
|
if ringQP.RingP != nil && sk.LevelP() >= 0 {
|
|
samplerP, err := ring.NewSampler(prng, ringQP.RingP, params.Xs(), false)
|
|
if err != nil {
|
|
return fmt.Errorf("fhe: ring.NewSampler P: %w", err)
|
|
}
|
|
samplerP.AtLevel(sk.LevelP()).Read(sk.Value.P)
|
|
ringQP.RingP.AtLevel(sk.LevelP()).NTT(sk.Value.P, sk.Value.P)
|
|
ringQP.RingP.AtLevel(sk.LevelP()).MForm(sk.Value.P, sk.Value.P)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenSecretKey generates a new secret key pair.
|
|
//
|
|
// When the generator was constructed via NewKeyGeneratorFromSeed the secret
|
|
// key coefficients are sampled from the seeded blake2b stream so the result
|
|
// is fully deterministic. Otherwise the standard cryptographically random
|
|
// sampler is used.
|
|
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()
|
|
if kg.prngBR != nil {
|
|
if err := sampleSecretKeyDeterministic(kg.params.paramsBR, kg.prngBR, sk); err != nil {
|
|
panic(fmt.Sprintf("fhe: deterministic BR sample: %v", err))
|
|
}
|
|
}
|
|
return &SecretKey{
|
|
SKLWE: sk,
|
|
SKBR: sk,
|
|
}
|
|
}
|
|
// Different dimensions require separate keys.
|
|
skLWE := kg.kgenLWE.GenSecretKeyNew()
|
|
skBR := kg.kgenBR.GenSecretKeyNew()
|
|
if kg.prngLWE != nil {
|
|
if err := sampleSecretKeyDeterministic(kg.params.paramsLWE, kg.prngLWE, skLWE); err != nil {
|
|
panic(fmt.Sprintf("fhe: deterministic LWE sample: %v", err))
|
|
}
|
|
}
|
|
if kg.prngBR != nil {
|
|
if err := sampleSecretKeyDeterministic(kg.params.paramsBR, kg.prngBR, skBR); err != nil {
|
|
panic(fmt.Sprintf("fhe: deterministic BR sample: %v", err))
|
|
}
|
|
}
|
|
return &SecretKey{
|
|
SKLWE: skLWE,
|
|
SKBR: skBR,
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
|
|
// CMPCOMBINE: computes isLess OR (isEqual AND bitLt) in one bootstrap
|
|
// Uses weighted encoding: 2*isLess + isEqual + bitLt
|
|
// With Q/8 encoding, weighted sum ranges:
|
|
// - (F,F,F): 2*(-1/8) + (-1/8) + (-1/8) = -4/8 = -0.5 → FALSE
|
|
// - (F,F,T): 2*(-1/8) + (-1/8) + (+1/8) = -2/8 = -0.25 → FALSE
|
|
// - (F,T,F): 2*(-1/8) + (+1/8) + (-1/8) = -2/8 = -0.25 → FALSE
|
|
// - (F,T,T): 2*(-1/8) + (+1/8) + (+1/8) = 0 → TRUE
|
|
// - (T,F,F): 2*(+1/8) + (-1/8) + (-1/8) = 0 → TRUE
|
|
// - (T,F,T): 2*(+1/8) + (-1/8) + (+1/8) = +2/8 = +0.25 → TRUE
|
|
// - (T,T,F): 2*(+1/8) + (+1/8) + (-1/8) = +2/8 = +0.25 → TRUE
|
|
// - (T,T,T): 2*(+1/8) + (+1/8) + (+1/8) = +4/8 = +0.5 → TRUE
|
|
// Threshold at -0.125 (midpoint between -0.25 and 0) for maximum noise margin
|
|
testPolyCMPCOMBINE := blindrot.InitTestPolynomial(func(x float64) float64 {
|
|
if x > -0.125 {
|
|
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,
|
|
TestPolyCMPCOMBINE: &testPolyCMPCOMBINE,
|
|
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
|
|
}
|