Files
zeekay eaf864890e feat: release v7.0.0 with GPU acceleration and module update
- Update module path to github.com/luxfi/lattice/v7
- Add GPU acceleration with Metal (macOS) and CUDA backends
- Enhance threshold FHE operations
- Revert to vanilla Apache 2.0 license (remove tfhe)
- Update Go version to 1.25.5
- Merge upstream lattigo improvements
2025-12-30 15:37:37 -08:00

236 lines
7.3 KiB
Go

package rlwe
import (
"fmt"
"github.com/luxfi/lattice/v7/ring"
"github.com/luxfi/lattice/v7/utils"
)
// Evaluator is a struct that holds the necessary elements to execute general homomorphic
// operation on RLWE ciphertexts, such as automorphisms, key-switching and relinearization.
type Evaluator struct {
params Parameters
EvaluationKeySet
automorphismIndex map[uint64][]uint64
BasisExtender *ring.BasisExtender
Decomposer *ring.Decomposer
pool *BufferPool
}
// NewEvaluator creates a new [Evaluator].
func NewEvaluator(params ParameterProvider, evk EvaluationKeySet) (eval *Evaluator) {
eval = new(Evaluator)
p := params.GetRLWEParameters()
eval.params = *p
eval.pool = NewPool(p.RingQP())
if p.RingP() != nil {
eval.BasisExtender = ring.NewBasisExtender(p.RingQ(), p.RingP())
}
eval.Decomposer = ring.NewDecomposer(p.RingQ(), p.RingP())
eval.EvaluationKeySet = evk
var AutomorphismIndex map[uint64][]uint64
if !utils.IsNil(evk) {
if galEls := evk.GetGaloisKeysList(); len(galEls) != 0 {
AutomorphismIndex = make(map[uint64][]uint64)
N := p.N()
NthRoot := p.RingQ().NthRoot()
var err error
for _, galEl := range galEls {
if AutomorphismIndex[galEl], err = ring.AutomorphismNTTIndex(N, NthRoot, galEl); err != nil {
// Sanity check, this error should not happen.
panic(err)
}
}
}
}
eval.automorphismIndex = AutomorphismIndex
return
}
func (eval *Evaluator) GetRLWEParameters() *Parameters {
return &eval.params
}
// CheckAndGetGaloisKey returns an error if the [GaloisKey] for the given Galois element is missing or the [EvaluationKey] interface is nil.
func (eval Evaluator) CheckAndGetGaloisKey(galEl uint64) (evk *GaloisKey, err error) {
if eval.EvaluationKeySet != nil {
if evk, err = eval.GetGaloisKey(galEl); err != nil {
return nil, fmt.Errorf("%w: key for galEl %d = 5^{%d} key is missing", err, galEl, eval.params.SolveDiscreteLogGaloisElement(galEl))
}
} else {
return nil, fmt.Errorf("evaluation key interface is nil")
}
if eval.automorphismIndex == nil {
eval.automorphismIndex = map[uint64][]uint64{}
}
if _, ok := eval.automorphismIndex[galEl]; !ok {
if eval.automorphismIndex[galEl], err = ring.AutomorphismNTTIndex(eval.params.N(), eval.params.RingQ().NthRoot(), galEl); err != nil {
// Sanity check, this error should not happen.
panic(err)
}
}
return
}
// CheckAndGetRelinearizationKey returns an error if the [RelinearizationKey] is missing or the [EvaluationKey] interface is nil.
func (eval Evaluator) CheckAndGetRelinearizationKey() (evk *RelinearizationKey, err error) {
if eval.EvaluationKeySet != nil {
if evk, err = eval.GetRelinearizationKey(); err != nil {
return nil, fmt.Errorf("%w: relineariztion key is missing", err)
}
} else {
return nil, fmt.Errorf("evaluation key interface is nil")
}
return
}
// InitOutputBinaryOp initializes the output [Element] opOut for receiving the result of a binary operation over
// op0 and op1. The method also performs the following checks:
//
// 1. Inputs are not nil
// 2. MetaData are not nil
// 3. op0.Degree() + op1.Degree() != 0 (i.e at least one [Element] is a ciphertext)
// 4. op0.IsNTT == op1.IsNTT == DefaultNTTFlag
// 5. op0.IsBatched == op1.IsBatched
//
// The opOut metadata are initilized as:
// IsNTT <- DefaultNTTFlag
// IsBatched <- op0.IsBatched
// LogDimensions <- max(op0.LogDimensions, op1.LogDimensions)
//
// The method returns max(op0.Degree(), op1.Degree(), opOut.Degree()) and min(op0.Level(), op1.Level(), opOut.Level())
func (eval Evaluator) InitOutputBinaryOp(op0, op1 *Element[ring.Poly], opInTotalMaxDegree int, opOut *Element[ring.Poly]) (degree, level int, err error) {
if op0 == nil || op1 == nil || opOut == nil {
return 0, 0, fmt.Errorf("op0, op1 and opOut cannot be nil")
}
if op0.MetaData == nil || op1.MetaData == nil || opOut.MetaData == nil {
return 0, 0, fmt.Errorf("op0, op1 and opOut MetaData cannot be nil")
}
degree = utils.Max(op0.Degree(), op1.Degree())
degree = utils.Max(degree, opOut.Degree())
level = utils.Min(op0.Level(), op1.Level())
level = utils.Min(level, opOut.Level())
totDegree := op0.Degree() + op1.Degree()
if totDegree == 0 {
return 0, 0, fmt.Errorf("op0 and op1 cannot be both plaintexts")
}
if totDegree > opInTotalMaxDegree {
return 0, 0, fmt.Errorf("op0 and op1 total degree cannot exceed %d but is %d", opInTotalMaxDegree, totDegree)
}
if op0.El().IsNTT != op1.El().IsNTT || op0.El().IsNTT != eval.params.NTTFlag() {
return 0, 0, fmt.Errorf("op0.El().IsNTT or op1.El().IsNTT != %t", eval.params.NTTFlag())
} else {
opOut.El().IsNTT = op0.El().IsNTT
}
if op0.El().IsBatched != op1.El().IsBatched {
return 0, 0, fmt.Errorf("op1.El().IsBatched != opOut.El().IsBatched")
} else {
opOut.El().IsBatched = op0.El().IsBatched
}
opOut.El().LogDimensions.Rows = utils.Max(op0.El().LogDimensions.Rows, op1.El().LogDimensions.Rows)
opOut.El().LogDimensions.Cols = utils.Max(op0.El().LogDimensions.Cols, op1.El().LogDimensions.Cols)
return
}
// InitOutputUnaryOp initializes the output [Element] opOut for receiving the result of a unary operation over
// op0. The method also performs the following checks:
//
// 1. Input and output are not nil
// 2. Inoutp and output Metadata are not nil
// 3. op0.IsNTT == DefaultNTTFlag
//
// The method will also update the metadata of opOut:
//
// IsNTT <- NTTFlag
// IsBatched <- op0.IsBatched
// LogDimensions <- op0.LogDimensions
//
// The method returns max(op0.Degree(), opOut.Degree()) and min(op0.Level(), opOut.Level()).
func (eval Evaluator) InitOutputUnaryOp(op0, opOut *Element[ring.Poly]) (degree, level int, err error) {
if op0 == nil || opOut == nil {
return 0, 0, fmt.Errorf("op0 and opOut cannot be nil")
}
if op0.MetaData == nil || opOut.MetaData == nil {
return 0, 0, fmt.Errorf("op0 and opOut MetaData cannot be nil")
}
if op0.El().IsNTT != eval.params.NTTFlag() {
return 0, 0, fmt.Errorf("op0.IsNTT() != %t", eval.params.NTTFlag())
} else {
opOut.El().IsNTT = op0.El().IsNTT
}
opOut.El().IsBatched = op0.El().IsBatched
opOut.El().LogDimensions = op0.El().LogDimensions
return utils.Max(op0.Degree(), opOut.Degree()), utils.Min(op0.Level(), opOut.Level()), nil
}
// WithKey creates a shallow copy of the receiver [Evaluator] for which the new [EvaluationKey] is evaluationKey
// and where the temporary buffers are shared. The receiver and the returned evaluators cannot be used concurrently.
func (eval Evaluator) WithKey(evk EvaluationKeySet) *Evaluator {
var AutomorphismIndex map[uint64][]uint64
if galEls := evk.GetGaloisKeysList(); len(galEls) != 0 {
AutomorphismIndex = make(map[uint64][]uint64)
N := eval.params.N()
NthRoot := eval.params.RingQ().NthRoot()
var err error
for _, galEl := range galEls {
if AutomorphismIndex[galEl], err = ring.AutomorphismNTTIndex(N, NthRoot, galEl); err != nil {
// Sanity check, this error should not happen.
panic(err)
}
}
}
return &Evaluator{
params: eval.params,
Decomposer: eval.Decomposer,
BasisExtender: eval.BasisExtender,
EvaluationKeySet: evk,
automorphismIndex: AutomorphismIndex,
pool: eval.pool,
}
}
func (eval Evaluator) AutomorphismIndex(galEl uint64) []uint64 {
return eval.automorphismIndex[galEl]
}
func (eval Evaluator) ModDownQPtoQNTT(levelQ, levelP int, p1Q, p1P, p2Q ring.Poly) {
eval.BasisExtender.ModDownQPtoQNTT(levelQ, levelP, p1Q, p1P, p2Q)
}