mirror of
https://github.com/luxfi/lattice.git
synced 2026-09-24 00:25:44 +00:00
504 lines
14 KiB
Go
504 lines
14 KiB
Go
package rlwe
|
|
|
|
import (
|
|
"fmt"
|
|
"math/big"
|
|
"math/bits"
|
|
|
|
"github.com/luxfi/lattice/v5/ring"
|
|
"github.com/luxfi/lattice/v5/utils"
|
|
)
|
|
|
|
// Trace maps X -> sum((-1)^i * X^{i*n+1}) for n <= i < N
|
|
// Monomial X^k vanishes if k is not divisible by (N/n), otherwise it is multiplied by (N/n).
|
|
// Ciphertext is pre-multiplied by (N/n)^-1 to remove the (N/n) factor.
|
|
// Examples of full Trace for [0 + 1X + 2X^2 + 3X^3 + 4X^4 + 5X^5 + 6X^6 + 7X^7]
|
|
//
|
|
// 1.
|
|
//
|
|
// [1 + 2X + 3X^2 + 4X^3 + 5X^4 + 6X^5 + 7X^6 + 8X^7]
|
|
// + [1 - 6X - 3X^2 + 8X^3 + 5X^4 + 2X^5 - 7X^6 - 4X^7] {X-> X^(i * 5^1)}
|
|
// = [2 - 4X + 0X^2 +12X^3 +10X^4 + 8X^5 - 0X^6 + 4X^7]
|
|
//
|
|
// 2.
|
|
//
|
|
// [2 - 4X + 0X^2 +12X^3 +10X^4 + 8X^5 - 0X^6 + 4X^7]
|
|
// + [2 + 4X + 0X^2 -12X^3 +10X^4 - 8X^5 + 0X^6 - 4X^7] {X-> X^(i * 5^2)}
|
|
// = [4 + 0X + 0X^2 - 0X^3 +20X^4 + 0X^5 + 0X^6 - 0X^7]
|
|
//
|
|
// 3.
|
|
//
|
|
// [4 + 0X + 0X^2 - 0X^3 +20X^4 + 0X^5 + 0X^6 - 0X^7]
|
|
// + [4 + 0X + 0X^2 - 0X^3 -20X^4 + 0X^5 + 0X^6 - 0X^7] {X-> X^(i * -1)}
|
|
// = [8 + 0X + 0X^2 - 0X^3 + 0X^4 + 0X^5 + 0X^6 - 0X^7]
|
|
//
|
|
// The method will return an error if the input and output ciphertexts degree is not one.
|
|
func (eval Evaluator) Trace(ctIn *Ciphertext, logN int, opOut *Ciphertext) (err error) {
|
|
|
|
if ctIn.Degree() != 1 || opOut.Degree() != 1 {
|
|
return fmt.Errorf("ctIn.Degree() != 1 or opOut.Degree() != 1")
|
|
}
|
|
|
|
params := eval.GetRLWEParameters()
|
|
|
|
level := utils.Min(ctIn.Level(), opOut.Level())
|
|
|
|
opOut.Resize(opOut.Degree(), level)
|
|
|
|
*opOut.MetaData = *ctIn.MetaData
|
|
|
|
gap := 1 << (params.LogN() - logN - 1)
|
|
|
|
if logN == 0 {
|
|
gap <<= 1
|
|
}
|
|
|
|
if gap > 1 {
|
|
|
|
ringQ := params.RingQ().AtLevel(level)
|
|
|
|
if ringQ.Type() == ring.ConjugateInvariant {
|
|
gap >>= 1 // We skip the last step that applies phi(5^{-1})
|
|
}
|
|
|
|
NInv := new(big.Int).SetUint64(uint64(gap))
|
|
NInv.ModInverse(NInv, ringQ.ModulusAtLevel[level])
|
|
|
|
// pre-multiplication by (N/n)^-1
|
|
ringQ.MulScalarBigint(ctIn.Value[0], NInv, opOut.Value[0])
|
|
ringQ.MulScalarBigint(ctIn.Value[1], NInv, opOut.Value[1])
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.NTT(opOut.Value[0], opOut.Value[0])
|
|
ringQ.NTT(opOut.Value[1], opOut.Value[1])
|
|
opOut.IsNTT = true
|
|
}
|
|
|
|
buff, err := NewCiphertextAtLevelFromPoly(level, []ring.Poly{eval.BuffQP[3].Q, eval.BuffQP[4].Q})
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
buff.IsNTT = true
|
|
|
|
for i := logN; i < params.LogN()-1; i++ {
|
|
|
|
if err = eval.Automorphism(opOut, params.GaloisElement(1<<i), buff); err != nil {
|
|
return err
|
|
}
|
|
|
|
ringQ.Add(opOut.Value[0], buff.Value[0], opOut.Value[0])
|
|
ringQ.Add(opOut.Value[1], buff.Value[1], opOut.Value[1])
|
|
}
|
|
|
|
if logN == 0 && ringQ.Type() == ring.Standard {
|
|
|
|
if err = eval.Automorphism(opOut, ringQ.NthRoot()-1, buff); err != nil {
|
|
return err
|
|
}
|
|
|
|
ringQ.Add(opOut.Value[0], buff.Value[0], opOut.Value[0])
|
|
ringQ.Add(opOut.Value[1], buff.Value[1], opOut.Value[1])
|
|
}
|
|
|
|
if !ctIn.IsNTT {
|
|
ringQ.INTT(opOut.Value[0], opOut.Value[0])
|
|
ringQ.INTT(opOut.Value[1], opOut.Value[1])
|
|
opOut.IsNTT = false
|
|
}
|
|
|
|
} else {
|
|
if ctIn != opOut {
|
|
opOut.Copy(ctIn)
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// GaloisElementsForTrace returns the list of Galois elements requored for the for the `Trace` operation.
|
|
// Trace maps X -> sum((-1)^i * X^{i*n+1}) for 2^{LogN} <= i < N.
|
|
func GaloisElementsForTrace(params ParameterProvider, logN int) (galEls []uint64) {
|
|
|
|
p := params.GetRLWEParameters()
|
|
|
|
galEls = []uint64{}
|
|
for i, j := logN, 0; i < p.LogN()-1; i, j = i+1, j+1 {
|
|
galEls = append(galEls, p.GaloisElement(1<<i))
|
|
}
|
|
|
|
if logN == 0 {
|
|
switch p.RingType() {
|
|
case ring.Standard:
|
|
galEls = append(galEls, p.GaloisElementOrderTwoOrthogonalSubgroup())
|
|
case ring.ConjugateInvariant:
|
|
panic("cannot GaloisElementsForTrace: Galois element GaloisGen^-1 is undefined in ConjugateInvariant Ring")
|
|
default:
|
|
panic("cannot GaloisElementsForTrace: invalid ring type")
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Expand expands a RLWE Ciphertext encrypting sum ai * X^i to 2^logN ciphertexts,
|
|
// each encrypting ai * X^0 for 0 <= i < 2^LogN. That is, it extracts the first 2^logN
|
|
// coefficients, whose degree is a multiple of 2^logGap, of ct and returns a map
|
|
// with the extracted ciphertext at their respective index.
|
|
//
|
|
// The method will return an error if:
|
|
// - The input ciphertext degree is not one
|
|
// - The ring type is not ring.Standard
|
|
func (eval Evaluator) Expand(ct *Ciphertext, logN, logGap int) (cts map[int]*Ciphertext, err error) {
|
|
|
|
if ct.Degree() != 1 {
|
|
return nil, fmt.Errorf("cannot Expand: ct.Degree() != 1")
|
|
}
|
|
|
|
params := eval.GetRLWEParameters()
|
|
|
|
if params.RingType() != ring.Standard {
|
|
return nil, fmt.Errorf("cannot Expand: method is only supported for ring.Type = ring.Standard (X^{-2^{i}} does not exist in the sub-ring Z[X + X^{-1}])")
|
|
}
|
|
|
|
level := ct.Level()
|
|
|
|
ringQ := params.RingQ().AtLevel(level)
|
|
|
|
// Compute X^{-2^{i}} from 1 to LogN
|
|
xPow2 := GenXPow2(ringQ, logN, true)
|
|
|
|
cts = map[int]*Ciphertext{}
|
|
cts[0] = ct.CopyNew()
|
|
cts[0].LogDimensions = ring.Dimensions{Rows: 0, Cols: 0}
|
|
|
|
if ct := cts[0]; !ct.IsNTT {
|
|
ringQ.NTT(ct.Value[0], ct.Value[0])
|
|
ringQ.NTT(ct.Value[1], ct.Value[1])
|
|
ct.IsNTT = true
|
|
}
|
|
|
|
// Multiplies by 2^{-logN} mod Q
|
|
NInv := new(big.Int).SetUint64(1 << logN)
|
|
NInv.ModInverse(NInv, ringQ.ModulusAtLevel[level])
|
|
|
|
ringQ.MulScalarBigint(cts[0].Value[0], NInv, cts[0].Value[0])
|
|
ringQ.MulScalarBigint(cts[0].Value[1], NInv, cts[0].Value[1])
|
|
|
|
gap := 1 << logGap
|
|
|
|
tmp, err := NewCiphertextAtLevelFromPoly(level, []ring.Poly{eval.BuffCt.Value[0], eval.BuffCt.Value[1]})
|
|
|
|
// Sanity check, this error should not happen unless the
|
|
// evaluator's buffer thave been improperly tempered with.
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
*tmp.MetaData = *ct.MetaData
|
|
|
|
for i := 0; i < logN; i++ {
|
|
|
|
n := 1 << i
|
|
|
|
galEl := uint64(ringQ.N()/n + 1)
|
|
|
|
for j := 0; j < n; j += gap {
|
|
|
|
c0 := cts[j]
|
|
|
|
// X -> X^{N/n + 1}
|
|
//[a, b, c, d] -> [a, -b, c, -d]
|
|
if err = eval.Automorphism(c0, galEl, tmp); err != nil {
|
|
return
|
|
}
|
|
|
|
if j+n/gap > 0 {
|
|
|
|
c1 := cts[j].CopyNew()
|
|
|
|
// Zeroes odd coeffs: [a, b, c, d] + [a, -b, c, -d] -> [2a, 0, 2b, 0]
|
|
ringQ.Add(c0.Value[0], tmp.Value[0], c0.Value[0])
|
|
ringQ.Add(c0.Value[1], tmp.Value[1], c0.Value[1])
|
|
|
|
// Zeroes even coeffs: [a, b, c, d] - [a, -b, c, -d] -> [0, 2b, 0, 2d]
|
|
ringQ.Sub(c1.Value[0], tmp.Value[0], c1.Value[0])
|
|
ringQ.Sub(c1.Value[1], tmp.Value[1], c1.Value[1])
|
|
|
|
// c1 * X^{-2^{i}}: [0, 2b, 0, 2d] * X^{-n} -> [2b, 0, 2d, 0]
|
|
ringQ.MulCoeffsMontgomery(c1.Value[0], xPow2[i], c1.Value[0])
|
|
ringQ.MulCoeffsMontgomery(c1.Value[1], xPow2[i], c1.Value[1])
|
|
|
|
cts[j+n] = c1
|
|
|
|
} else {
|
|
|
|
// Zeroes odd coeffs: [a, b, c, d] + [a, -b, c, -d] -> [2a, 0, 2b, 0]
|
|
ringQ.Add(c0.Value[0], tmp.Value[0], c0.Value[0])
|
|
ringQ.Add(c0.Value[1], tmp.Value[1], c0.Value[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, ct := range cts {
|
|
if ct != nil && !ct.IsNTT {
|
|
ringQ.INTT(ct.Value[0], ct.Value[0])
|
|
ringQ.INTT(ct.Value[1], ct.Value[1])
|
|
ct.IsNTT = false
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// GaloisElementsForExpand returns the list of Galois elements required
|
|
// to perform the `Expand` operation with parameter `logN`.
|
|
func GaloisElementsForExpand(params ParameterProvider, logN int) (galEls []uint64) {
|
|
galEls = make([]uint64, logN)
|
|
|
|
NthRoot := params.GetRLWEParameters().RingQ().NthRoot()
|
|
|
|
for i := 0; i < logN; i++ {
|
|
galEls[i] = uint64(NthRoot/(2<<i) + 1)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Pack packs a batch of RLWE ciphertexts, packing the batch of ciphertexts into a single ciphertext.
|
|
// The number of key-switching operations is inputLogGap - log2(gap) + len(cts), where log2(gap) is the
|
|
// minimum distance between two keys of the map cts[int]*Ciphertext.
|
|
//
|
|
// Input:
|
|
//
|
|
// cts: a map of Ciphertext, where the index in the map is the future position of the first coefficient
|
|
// of the indexed ciphertext in the final ciphertext (see example). Ciphertexts can be in or out of the NTT domain.
|
|
// logGap: all coefficients of the input ciphertexts that are not a multiple of X^{2^{logGap}} will be zeroed
|
|
// during the merging (see example). This is equivalent to skipping the first 2^{logGap} steps of the
|
|
// algorithm, i.e. having as input ciphertexts that are already partially packed.
|
|
// zeroGarbageSlots: if set to true, slots which are not multiples of X^{2^{logGap}} will be zeroed during the procedure.
|
|
// this will greatly increase the noise and increase the number of key-switching operations to inputLogGap + len(cts).
|
|
//
|
|
// Output: a ciphertext packing all input ciphertexts
|
|
//
|
|
// Example: we want to pack 4 ciphertexts into one, and keep only coefficients which are a multiple of X^{4}.
|
|
//
|
|
// To do so, we must set logGap = 2.
|
|
// Here the `X` slots are treated as garbage slots that we want to discard during the procedure.
|
|
//
|
|
// input: map[int]{
|
|
// 0: [x00, X, X, X, x01, X, X, X], with logGap = 2
|
|
// 1: [x10, X, X, X, x11, X, X, X],
|
|
// 2: [x20, X, X, X, x21, X, X, X],
|
|
// 3: [x30, X, X, X, x31, X, X, X],
|
|
// }
|
|
//
|
|
// Step 1:
|
|
// map[0]: 2^{-1} * (map[0] + X^2 * map[2] + phi_{5^2}(map[0] - X^2 * map[2]) = [x00, X, x20, X, x01, X, x21, X]
|
|
// map[1]: 2^{-1} * (map[1] + X^2 * map[3] + phi_{5^2}(map[1] - X^2 * map[3]) = [x10, X, x30, X, x11, X, x31, X]
|
|
// Step 2:
|
|
// map[0]: 2^{-1} * (map[0] + X^1 * map[1] + phi_{5^4}(map[0] - X^1 * map[1]) = [x00, x10, x20, x30, x01, x11, x21, x22]
|
|
func (eval Evaluator) Pack(cts map[int]*Ciphertext, inputLogGap int, zeroGarbageSlots bool) (ct *Ciphertext, err error) {
|
|
|
|
params := eval.GetRLWEParameters()
|
|
|
|
if params.RingType() != ring.Standard {
|
|
return nil, fmt.Errorf("cannot Pack: procedure is only supported for ring.Type = ring.Standard (X^{2^{i}} does not exist in the sub-ring Z[X + X^{-1}])")
|
|
}
|
|
|
|
if len(cts) < 2 {
|
|
return nil, fmt.Errorf("cannot Pack: #cts must be at least 2")
|
|
}
|
|
|
|
keys := utils.GetSortedKeys(cts)
|
|
|
|
gap := keys[1] - keys[0]
|
|
level := cts[keys[0]].Level()
|
|
|
|
for i, key := range keys[1:] {
|
|
level = utils.Min(level, cts[key].Level())
|
|
|
|
if i < len(keys)-1 {
|
|
gap = utils.Min(gap, keys[i+1]-keys[i])
|
|
}
|
|
}
|
|
|
|
logN := params.LogN()
|
|
ringQ := params.RingQ().AtLevel(level)
|
|
|
|
logStart := logN - inputLogGap
|
|
logEnd := logN
|
|
|
|
if !zeroGarbageSlots {
|
|
if gap > 0 {
|
|
logEnd -= bits.Len64(uint64(gap - 1))
|
|
}
|
|
}
|
|
|
|
if logStart >= logEnd {
|
|
return nil, fmt.Errorf("cannot Pack: gaps between ciphertexts is smaller than inputLogGap > N")
|
|
}
|
|
|
|
xPow2 := GenXPow2(ringQ.AtLevel(level), params.LogN(), false) // log(N) polynomial to generate, quick
|
|
|
|
NInv := new(big.Int).SetUint64(uint64(1 << (logEnd - logStart)))
|
|
NInv.ModInverse(NInv, ringQ.ModulusAtLevel[level])
|
|
|
|
for _, key := range keys {
|
|
|
|
ct := cts[key]
|
|
|
|
if ct.Degree() != 1 {
|
|
return nil, fmt.Errorf("cannot Pack: cts[%d].Degree() != 1", key)
|
|
}
|
|
|
|
if !ct.IsNTT {
|
|
ringQ.NTT(ct.Value[0], ct.Value[0])
|
|
ringQ.NTT(ct.Value[1], ct.Value[1])
|
|
ct.IsNTT = true
|
|
}
|
|
|
|
ringQ.MulScalarBigint(ct.Value[0], NInv, ct.Value[0])
|
|
ringQ.MulScalarBigint(ct.Value[1], NInv, ct.Value[1])
|
|
}
|
|
|
|
tmpa := &Ciphertext{}
|
|
tmpa.Value = []ring.Poly{ringQ.NewPoly(), ringQ.NewPoly()}
|
|
tmpa.MetaData = &MetaData{}
|
|
tmpa.MetaData.IsNTT = true
|
|
|
|
for i := logStart; i < logEnd; i++ {
|
|
|
|
t := 1 << (logN - 1 - i)
|
|
|
|
for jx, jy := 0, t; jx < t; jx, jy = jx+1, jy+1 {
|
|
|
|
a := cts[jx]
|
|
b := cts[jy]
|
|
|
|
if b != nil {
|
|
|
|
//X^(N/2^L)
|
|
ringQ.MulCoeffsMontgomery(b.Value[0], xPow2[len(xPow2)-i-1], b.Value[0])
|
|
ringQ.MulCoeffsMontgomery(b.Value[1], xPow2[len(xPow2)-i-1], b.Value[1])
|
|
|
|
if a != nil {
|
|
|
|
// tmpa = phi(a - b * X^{N/2^{i}}, 2^{i-1})
|
|
ringQ.Sub(a.Value[0], b.Value[0], tmpa.Value[0])
|
|
ringQ.Sub(a.Value[1], b.Value[1], tmpa.Value[1])
|
|
|
|
// a = a + b * X^{N/2^{i}}
|
|
ringQ.Add(a.Value[0], b.Value[0], a.Value[0])
|
|
ringQ.Add(a.Value[1], b.Value[1], a.Value[1])
|
|
|
|
} else {
|
|
// if ct[jx] == nil, then simply re-assigns
|
|
cts[jx] = cts[jy]
|
|
|
|
// Required for correctness, since each log step is expected
|
|
// to double the values, which are pre-scaled by N^{-1} mod Q
|
|
// Maybe this can be omitted by doing an individual pre-scaling.
|
|
ringQ.Add(cts[jx].Value[0], cts[jx].Value[0], cts[jx].Value[0])
|
|
ringQ.Add(cts[jx].Value[1], cts[jx].Value[1], cts[jx].Value[1])
|
|
}
|
|
}
|
|
|
|
if a != nil {
|
|
|
|
var galEl uint64
|
|
|
|
if i == 0 {
|
|
galEl = ringQ.NthRoot() - 1
|
|
} else {
|
|
galEl = params.GaloisElement(1 << (i - 1))
|
|
}
|
|
|
|
if b != nil {
|
|
if err = eval.Automorphism(tmpa, galEl, tmpa); err != nil {
|
|
return
|
|
}
|
|
} else {
|
|
if err = eval.Automorphism(a, galEl, tmpa); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// a + b * X^{N/2^{i}} + phi(a - b * X^{N/2^{i}}, 2^{i-1})
|
|
ringQ.Add(a.Value[0], tmpa.Value[0], a.Value[0])
|
|
ringQ.Add(a.Value[1], tmpa.Value[1], a.Value[1])
|
|
}
|
|
}
|
|
}
|
|
|
|
return cts[0], nil
|
|
}
|
|
|
|
// GaloisElementsForPack returns the list of Galois elements required to perform the `Pack` operation.
|
|
func GaloisElementsForPack(params ParameterProvider, logGap int) (galEls []uint64) {
|
|
|
|
p := params.GetRLWEParameters()
|
|
|
|
// Sanity check
|
|
if logGap > p.LogN() || logGap < 0 {
|
|
panic(fmt.Errorf("cannot GaloisElementsForPack: logGap > logN || logGap < 0"))
|
|
}
|
|
|
|
galEls = make([]uint64, 0, logGap)
|
|
for i := 0; i < logGap; i++ {
|
|
galEls = append(galEls, p.GaloisElement(1<<i))
|
|
}
|
|
|
|
switch p.RingType() {
|
|
case ring.Standard:
|
|
if logGap == p.LogN() {
|
|
galEls = append(galEls, p.GaloisElementOrderTwoOrthogonalSubgroup())
|
|
}
|
|
default:
|
|
panic("cannot GaloisElementsForPack: invalid ring type")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func GenXPow2(r *ring.Ring, logN int, div bool) (xPow []ring.Poly) {
|
|
|
|
// Compute X^{-n} from 0 to LogN
|
|
xPow = make([]ring.Poly, logN)
|
|
|
|
moduli := r.ModuliChain()[:r.Level()+1]
|
|
BRC := r.BRedConstants()
|
|
|
|
var idx int
|
|
for i := 0; i < logN; i++ {
|
|
|
|
idx = 1 << i
|
|
|
|
if div {
|
|
idx = r.N() - idx
|
|
}
|
|
|
|
xPow[i] = r.NewPoly()
|
|
|
|
if i == 0 {
|
|
|
|
for j := range moduli {
|
|
xPow[i].Coeffs[j][idx] = ring.MForm(1, moduli[j], BRC[j])
|
|
}
|
|
|
|
r.NTT(xPow[i], xPow[i])
|
|
|
|
} else {
|
|
r.MulCoeffsMontgomery(xPow[i-1], xPow[i-1], xPow[i]) // X^{n} = X^{1} * X^{n-1}
|
|
}
|
|
}
|
|
|
|
if div {
|
|
r.Neg(xPow[0], xPow[0])
|
|
}
|
|
|
|
return
|
|
}
|