mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
358 lines
11 KiB
Go
358 lines
11 KiB
Go
package rlwe
|
|
|
|
import (
|
|
"github.com/luxfi/lattice/v5/ring"
|
|
"github.com/luxfi/lattice/v5/ring/ringqp"
|
|
"github.com/luxfi/lattice/v5/utils"
|
|
)
|
|
|
|
// InnerSum applies an optimized inner sum on the Ciphertext (log2(n) + HW(n) rotations with double hoisting).
|
|
// The operation assumes that `ctIn` encrypts Slots/`batchSize` sub-vectors of size `batchSize` and will add them together (in parallel) in groups of `n`.
|
|
// It outputs in opOut a Ciphertext for which the "leftmost" sub-vector of each group is equal to the sum of the group.
|
|
//
|
|
// The inner sum is computed in a tree fashion. Example for batchSize=2 & n=4 (garbage slots are marked by 'x'):
|
|
//
|
|
// 1) [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}]
|
|
//
|
|
// 2. [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}]
|
|
// +
|
|
// [{c, d}, {e, f}, {g, h}, {x, x}, {c, d}, {e, f}, {g, h}, {x, x}] (rotate batchSize * 2^{0})
|
|
// =
|
|
// [{a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}, {a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}]
|
|
//
|
|
// 3. [{a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}, {a+c, b+d}, {x, x}, {e+g, f+h}, {x, x}] (rotate batchSize * 2^{1})
|
|
// +
|
|
// [{e+g, f+h}, {x, x}, {x, x}, {x, x}, {e+g, f+h}, {x, x}, {x, x}, {x, x}] =
|
|
// =
|
|
// [{a+c+e+g, b+d+f+h}, {x, x}, {x, x}, {x, x}, {a+c+e+g, b+d+f+h}, {x, x}, {x, x}, {x, x}]
|
|
func (eval Evaluator) InnerSum(ctIn *Ciphertext, batchSize, n int, opOut *Ciphertext) (err error) {
|
|
|
|
params := eval.GetRLWEParameters()
|
|
|
|
levelQ := ctIn.Level()
|
|
levelP := params.PCount() - 1
|
|
|
|
ringQP := params.RingQP().AtLevel(ctIn.Level(), levelP)
|
|
|
|
ringQ := ringQP.RingQ
|
|
|
|
opOut.Resize(opOut.Degree(), levelQ)
|
|
*opOut.MetaData = *ctIn.MetaData
|
|
|
|
ctInNTT, err := NewCiphertextAtLevelFromPoly(levelQ, eval.BuffCt.Value[:2])
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
ctInNTT.MetaData = &MetaData{}
|
|
ctInNTT.IsNTT = true
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.NTT(ctIn.Value[0], ctInNTT.Value[0])
|
|
ringQ.NTT(ctIn.Value[1], ctInNTT.Value[1])
|
|
} else {
|
|
ctInNTT.Value[0].CopyLvl(levelQ, ctIn.Value[0])
|
|
ctInNTT.Value[1].CopyLvl(levelQ, ctIn.Value[1])
|
|
}
|
|
|
|
if n == 1 {
|
|
if ctIn != opOut {
|
|
opOut.Value[0].CopyLvl(levelQ, ctIn.Value[0])
|
|
opOut.Value[1].CopyLvl(levelQ, ctIn.Value[1])
|
|
}
|
|
} else {
|
|
|
|
// BuffQP[0:2] are used by AutomorphismHoistedLazy
|
|
|
|
// Accumulator mod QP (i.e. opOut Mod QP)
|
|
accQP := &Element[ringqp.Poly]{Value: []ringqp.Poly{eval.BuffQP[2], eval.BuffQP[3]}}
|
|
accQP.MetaData = ctInNTT.MetaData
|
|
|
|
// Buffer mod QP (i.e. to store the result of lazy gadget products)
|
|
cQP := &Element[ringqp.Poly]{Value: []ringqp.Poly{eval.BuffQP[4], eval.BuffQP[5]}}
|
|
cQP.MetaData = ctInNTT.MetaData
|
|
|
|
// Buffer mod Q (i.e. to store the result of gadget products)
|
|
cQ, err := NewCiphertextAtLevelFromPoly(levelQ, []ring.Poly{cQP.Value[0].Q, cQP.Value[1].Q})
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
cQ.MetaData = ctInNTT.MetaData
|
|
|
|
state := false
|
|
copy := true
|
|
// Binary reading of the input n
|
|
for i, j := 0, n; j > 0; i, j = i+1, j>>1 {
|
|
|
|
// Starts by decomposing the input ciphertext
|
|
eval.DecomposeNTT(levelQ, levelP, levelP+1, ctInNTT.Value[1], true, eval.BuffDecompQP)
|
|
|
|
// If the binary reading scans a 1 (j is odd)
|
|
if j&1 == 1 {
|
|
|
|
k := n - (n & ((2 << i) - 1))
|
|
k *= batchSize
|
|
|
|
// If the rotation is not zero
|
|
if k != 0 {
|
|
|
|
rot := params.GaloisElement(k)
|
|
|
|
// opOutQP = opOutQP + Rotate(ctInNTT, k)
|
|
if copy {
|
|
if err = eval.AutomorphismHoistedLazy(levelQ, ctInNTT, eval.BuffDecompQP, rot, accQP); err != nil {
|
|
return err
|
|
}
|
|
copy = false
|
|
} else {
|
|
if err = eval.AutomorphismHoistedLazy(levelQ, ctInNTT, eval.BuffDecompQP, rot, cQP); err != nil {
|
|
return err
|
|
}
|
|
ringQP.Add(accQP.Value[0], cQP.Value[0], accQP.Value[0])
|
|
ringQP.Add(accQP.Value[1], cQP.Value[1], accQP.Value[1])
|
|
}
|
|
|
|
// j is even
|
|
} else {
|
|
|
|
state = true
|
|
|
|
// if n is not a power of two, then at least one j was odd, and thus the buffer opOutQP is not empty
|
|
if n&(n-1) != 0 {
|
|
|
|
// opOut = opOutQP/P + ctInNTT
|
|
eval.BasisExtender.ModDownQPtoQNTT(levelQ, levelP, accQP.Value[0].Q, accQP.Value[0].P, opOut.Value[0]) // Division by P
|
|
eval.BasisExtender.ModDownQPtoQNTT(levelQ, levelP, accQP.Value[1].Q, accQP.Value[1].P, opOut.Value[1]) // Division by P
|
|
|
|
ringQ.Add(opOut.Value[0], ctInNTT.Value[0], opOut.Value[0])
|
|
ringQ.Add(opOut.Value[1], ctInNTT.Value[1], opOut.Value[1])
|
|
|
|
} else {
|
|
opOut.Value[0].CopyLvl(levelQ, ctInNTT.Value[0])
|
|
opOut.Value[1].CopyLvl(levelQ, ctInNTT.Value[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
if !state {
|
|
|
|
rot := params.GaloisElement((1 << i) * batchSize)
|
|
|
|
// ctInNTT = ctInNTT + Rotate(ctInNTT, 2^i)
|
|
if err = eval.AutomorphismHoisted(levelQ, ctInNTT, eval.BuffDecompQP, rot, cQ); err != nil {
|
|
return err
|
|
}
|
|
ringQ.Add(ctInNTT.Value[0], cQ.Value[0], ctInNTT.Value[0])
|
|
ringQ.Add(ctInNTT.Value[1], cQ.Value[1], ctInNTT.Value[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.INTT(opOut.Value[0], opOut.Value[0])
|
|
ringQ.INTT(opOut.Value[1], opOut.Value[1])
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// InnerFunction applies an user defined function on the Ciphertext with a tree-like combination requiring log2(n) + HW(n) rotations.
|
|
//
|
|
// InnerFunction with f = eval.Add(a, b, c) is equivalent to InnerSum (although slightly slower).
|
|
//
|
|
// The operation assumes that `ctIn` encrypts Slots/`batchSize` sub-vectors of size `batchSize` and will add them together (in parallel) in groups of `n`.
|
|
// It outputs in opOut a Ciphertext for which the "leftmost" sub-vector of each group is equal to the pair-wise recursive evaluation of function over the group.
|
|
//
|
|
// The inner function is computed in a tree fashion. Example for batchSize=2 & n=4 (garbage slots are marked by 'x'):
|
|
//
|
|
// 1) [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}]
|
|
//
|
|
// 2. [{a, b}, {c, d}, {e, f}, {g, h}, {a, b}, {c, d}, {e, f}, {g, h}]
|
|
// f
|
|
// [{c, d}, {e, f}, {g, h}, {x, x}, {c, d}, {e, f}, {g, h}, {x, x}] (rotate batchSize * 2^{0})
|
|
// =
|
|
// [{f(a, c), f(b, d)}, {f(c, e), f(d, f)}, {f(e, g), f(f, h)}, {x, x}, {f(a, c), f(b, d)}, {f(c, e), f(d, f)}, {f(e, g), f(f, h)}, {x, x}]
|
|
//
|
|
// 3. [{f(a, c), f(b, d)}, {x, x}, {f(e, g), f(f, h)}, {x, x}, {f(a, c), f(b, d)}, {x, x}, {f(e, g), f(f, h)}, {x, x}] (rotate batchSize * 2^{1})
|
|
// +
|
|
// [{f(e, g), f(f, h)}, {x, x}, {x, x}, {x, x}, {f(e, g), f(f, h)}, {x, x}, {x, x}, {x, x}] =
|
|
// =
|
|
// [{f(f(a,c),f(e,g)), f(f(b, d), f(f, h))}, {x, x}, {x, x}, {x, x}, {f(f(a,c),f(e,g)), f(f(b, d), f(f, h))}, {x, x}, {x, x}, {x, x}]
|
|
func (eval Evaluator) InnerFunction(ctIn *Ciphertext, batchSize, n int, f func(a, b, c *Ciphertext) (err error), opOut *Ciphertext) (err error) {
|
|
|
|
params := eval.GetRLWEParameters()
|
|
|
|
levelQ := utils.Min(ctIn.Level(), opOut.Level())
|
|
|
|
ringQ := params.RingQ().AtLevel(levelQ)
|
|
|
|
opOut.Resize(opOut.Degree(), levelQ)
|
|
*opOut.MetaData = *ctIn.MetaData
|
|
|
|
P0 := params.RingQ().NewPoly()
|
|
P1 := params.RingQ().NewPoly()
|
|
P2 := params.RingQ().NewPoly()
|
|
P3 := params.RingQ().NewPoly()
|
|
|
|
ctInNTT := NewCiphertext(params, 1, levelQ)
|
|
|
|
*ctInNTT.MetaData = *ctIn.MetaData
|
|
ctInNTT.IsNTT = true
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.NTT(ctIn.Value[0], ctInNTT.Value[0])
|
|
ringQ.NTT(ctIn.Value[1], ctInNTT.Value[1])
|
|
} else {
|
|
ctInNTT.Copy(ctIn)
|
|
}
|
|
|
|
if n == 1 {
|
|
opOut.Copy(ctIn)
|
|
} else {
|
|
|
|
// Accumulator mod Q
|
|
accQ, err := NewCiphertextAtLevelFromPoly(levelQ, []ring.Poly{P0, P1})
|
|
*accQ.MetaData = *ctInNTT.MetaData
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Buffer mod Q
|
|
cQ, err := NewCiphertextAtLevelFromPoly(levelQ, []ring.Poly{P2, P3})
|
|
*cQ.MetaData = *ctInNTT.MetaData
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
state := false
|
|
copy := true
|
|
// Binary reading of the input n
|
|
for i, j := 0, n; j > 0; i, j = i+1, j>>1 {
|
|
|
|
// If the binary reading scans a 1 (j is odd)
|
|
if j&1 == 1 {
|
|
|
|
k := n - (n & ((2 << i) - 1))
|
|
k *= batchSize
|
|
|
|
// If the rotation is not zero
|
|
if k != 0 {
|
|
|
|
rot := params.GaloisElement(k)
|
|
|
|
// opOutQ = f(opOutQ, Rotate(ctInNTT, k), opOutQ)
|
|
if copy {
|
|
if err = eval.Automorphism(ctInNTT, rot, accQ); err != nil {
|
|
return err
|
|
}
|
|
copy = false
|
|
} else {
|
|
if err = eval.Automorphism(ctInNTT, rot, cQ); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = f(accQ, cQ, accQ); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// j is even
|
|
} else {
|
|
|
|
state = true
|
|
|
|
// if n is not a power of two, then at least one j was odd, and thus the buffer opOutQ is not empty
|
|
if n&(n-1) != 0 {
|
|
|
|
opOut.Copy(accQ)
|
|
|
|
if err = f(opOut, ctInNTT, opOut); err != nil {
|
|
return err
|
|
}
|
|
|
|
} else {
|
|
opOut.Copy(ctInNTT)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !state {
|
|
|
|
// ctInNTT = f(ctInNTT, Rotate(ctInNTT, 2^i), ctInNTT)
|
|
if err = eval.Automorphism(ctInNTT, params.GaloisElement((1<<i)*batchSize), cQ); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = f(ctInNTT, cQ, ctInNTT); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.INTT(opOut.Value[0], opOut.Value[0])
|
|
ringQ.INTT(opOut.Value[1], opOut.Value[1])
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GaloisElementsForInnerSum returns the list of Galois elements necessary to apply the method
|
|
// `InnerSum` operation with parameters `batch` and `n`.
|
|
func GaloisElementsForInnerSum(params ParameterProvider, batch, n int) (galEls []uint64) {
|
|
|
|
rotIndex := make(map[int]bool)
|
|
|
|
var k int
|
|
for i := 1; i < n; i <<= 1 {
|
|
|
|
k = i
|
|
k *= batch
|
|
rotIndex[k] = true
|
|
|
|
k = n - (n & ((i << 1) - 1))
|
|
k *= batch
|
|
rotIndex[k] = true
|
|
}
|
|
|
|
rotations := make([]int, len(rotIndex))
|
|
var i int
|
|
for j := range rotIndex {
|
|
rotations[i] = j
|
|
i++
|
|
}
|
|
|
|
return params.GetRLWEParameters().GaloisElements(rotations)
|
|
}
|
|
|
|
// Replicate applies an optimized replication on the Ciphertext (log2(n) + HW(n) rotations with double hoisting).
|
|
// It acts as the inverse of a inner sum (summing elements from left to right).
|
|
// The replication is parameterized by the size of the sub-vectors to replicate "batchSize" and
|
|
// the number of times 'n' they need to be replicated.
|
|
// To ensure correctness, a gap of zero values of size batchSize * (n-1) must exist between
|
|
// two consecutive sub-vectors to replicate.
|
|
// This method is faster than Replicate when the number of rotations is large and it uses log2(n) + HW(n) instead of 'n'.
|
|
func (eval Evaluator) Replicate(ctIn *Ciphertext, batchSize, n int, opOut *Ciphertext) (err error) {
|
|
return eval.InnerSum(ctIn, -batchSize, n, opOut)
|
|
}
|
|
|
|
// GaloisElementsForReplicate returns the list of Galois elements necessary to perform the
|
|
// `Replicate` operation with parameters `batch` and `n`.
|
|
func GaloisElementsForReplicate(params ParameterProvider, batch, n int) (galEls []uint64) {
|
|
return GaloisElementsForInnerSum(params, -batch, n)
|
|
}
|