2026-01-03 19:11:38 -08:00
|
|
|
// Copyright (c) 2025, Lux Industries Inc
|
|
|
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
|
|
|
|
|
|
package fhe
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
|
|
"github.com/luxfi/lattice/v7/core/rgsw/blindrot"
|
|
|
|
|
"github.com/luxfi/lattice/v7/core/rlwe"
|
|
|
|
|
"github.com/luxfi/lattice/v7/ring"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-25 13:58:30 -08:00
|
|
|
// scaleCiphertext scales a ciphertext by a scalar value
|
|
|
|
|
func (eval *ShortIntEvaluator) scaleCiphertext(ct *rlwe.Ciphertext, scalar float64) *rlwe.Ciphertext {
|
|
|
|
|
result := ct.CopyNew()
|
|
|
|
|
// Scale by multiplying each coefficient
|
|
|
|
|
for i := range result.Value {
|
|
|
|
|
coeffs := result.Value[i].Coeffs
|
|
|
|
|
for j := range coeffs[0] {
|
|
|
|
|
coeffs[0][j] = uint64(float64(coeffs[0][j]) * scalar)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-03 19:11:38 -08:00
|
|
|
// 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)
|
|
|
|
|
}
|