Bug[hefloat/bootstrapping]: `bootstrapping.NewEvaluator` modifies `bootstrapping.Parameters`
Opened this issue · 2 comments
Pro7ech commented
Instantiating a bootstrapping.Evaluator
modifies the bootstrapping.Parameters
template that is given as input. Reusing the same bootstrapping.Parameters
to instantiate a second bootstrapping.Evaluator
produces a bootstrapping.Evaluator
that has incorrect internal initialization of the plaintext constants.
This bug is caused by
- The
bootstrapping.Parameters
having some field that are pointer to values - The
bootstrapping.Parameters
being copied in thebootstrapping.Evaluator
without allocating new pointers for the said values - The said values then being modified by the
bootstrapping.Evaluator
qantik commented
Hey, this seems to be quite a serious issue.
However I'm not able to reproduce it on my end, i.e., triggering a change in bootstrapping.Parameters
after reusing the parameters for different bootstrapping.Evaluators
.
Can you give an minimal working example, that showcases this bug?
Pro7ech commented
Yes my bad, I should have added a code snippet. Here is the bootstrapping example that triggers the bug
// Package main implements an example showcasing the basics of the bootstrapping for fixed-point approximate arithmetic over the reals/complexes.
// The bootstrapping is a circuit that homomorphically re-encrypts a ciphertext at level zero to a ciphertext at a higher level, enabling further computations.
// Note that, unlike other bootstrappings (BGV/BFV/TFHE), the this bootstrapping does not reduce the error in the ciphertext, but only enables further computations.
// This example shows how to bootstrap a single ciphertext whose ring degree is the same as the one of the bootstrapping parameters.
// Use the flag -short to run the examples fast but with insecure parameters.
package main
import (
"flag"
"fmt"
"math"
"math/big"
"github.com/tuneinsight/lattigo/v5/core/rlwe"
"github.com/tuneinsight/lattigo/v5/he/hefloat"
"github.com/tuneinsight/lattigo/v5/he/hefloat/bootstrapping"
"github.com/tuneinsight/lattigo/v5/ring"
"github.com/tuneinsight/lattigo/v5/utils"
"github.com/tuneinsight/lattigo/v5/utils/sampling"
)
var flagShort = flag.Bool("short", false, "run the example with a smaller and insecure ring degree.")
func main() {
flag.Parse()
// Default LogN, which with the following defined parameters
// provides a security of 128-bit.
LogN := 16
if *flagShort {
LogN -= 3
}
//==============================
//=== 1) RESIDUAL PARAMETERS ===
//==============================
// First we must define the residual parameters.
// The residual parameters are the parameters used outside of the bootstrapping circuit.
// For this example, we have a LogN=16, logQ = 55 + 10*40 and logP = 3*61, so LogQP = 638.
// With LogN=16, LogQP=638 and H=192, these parameters achieve well over 128-bit of security.
params, err := hefloat.NewParametersFromLiteral(hefloat.ParametersLiteral{
LogN: LogN, // Log2 of the ring degree
LogQ: []int{55, 40, 40, 40, 40, 40, 40, 40, 40, 40, 40}, // Log2 of the ciphertext prime moduli
LogP: []int{61, 61, 61}, // Log2 of the key-switch auxiliary prime moduli
LogDefaultScale: 40, // Log2 of the scale
Xs: ring.Ternary{H: 192},
})
if err != nil {
panic(err)
}
//==========================================
//=== 2) BOOTSTRAPPING PARAMETERSLITERAL ===
//==========================================
// The bootstrapping circuit use its own Parameters which will be automatically
// instantiated given the residual parameters and the bootstrapping parameters.
// !WARNING! The bootstrapping parameters are not ensure to be 128-bit secure, it is the
// responsibility of the user to check that the meet the security requirement and tweak them if necessary.
// Note that the default bootstrapping parameters use LogN=16 and a ternary secret with H=192 non-zero coefficients
// which provides parameters which are at least 128-bit if their LogQP <= 1550.
// For this first example, we do not specify any circuit specific optional field in the bootstrapping parameters literal.
// Thus we expect the bootstrapping to give a precision of 27.25 bits with H=192 (and 23.8 with H=N/2)
// if the plaintext values are uniformly distributed in [-1, 1] for both the real and imaginary part.
// See `he/float/bootstrapping/parameters_literal.go` for detailed information about the optional fields.
btpParametersLit := bootstrapping.ParametersLiteral{
// We specify LogN to ensure that both the residual parameters and the bootstrapping parameters
// have the same LogN. This is not required, but we want it for this example.
LogN: utils.Pointy(LogN),
// In this example we need manually specify the number of auxiliary primes (i.e. #Pi) used by the
// evaluation keys of the bootstrapping circuit, so that the size of LogQP meets the security target.
LogP: []int{61, 61, 61, 61},
// In this example we manually specify the bootstrapping parameters' secret distribution.
// This is not necessary, but we ensure here that they are the same as the residual parameters.
Xs: params.Xs(),
}
//===================================
//=== 3) BOOTSTRAPPING PARAMETERS ===
//===================================
// Now that the residual parameters and the bootstrapping parameters literals are defined, we can instantiate
// the bootstrapping parameters.
// The instantiated bootstrapping parameters store their own hefloat.Parameter, which are the parameters of the
// ring used by the bootstrapping circuit.
// The bootstrapping parameters are a wrapper of hefloat.Parameters, with additional information.
// They therefore has the same API as the hefloat.Parameters and we can use this API to print some information.
btpParams, err := bootstrapping.NewParametersFromLiteral(params, btpParametersLit)
if err != nil {
panic(err)
}
if *flagShort {
// Corrects the message ratio Q0/|m(X)| to take into account the smaller number of slots and keep the same precision
btpParams.Mod1ParametersLiteral.LogMessageRatio += 16 - params.LogN()
}
// We print some information about the residual parameters.
fmt.Printf("Residual parameters: logN=%d, logSlots=%d, H=%d, sigma=%f, logQP=%f, levels=%d, scale=2^%d\n",
btpParams.ResidualParameters.LogN(),
btpParams.ResidualParameters.LogMaxSlots(),
btpParams.ResidualParameters.XsHammingWeight(),
btpParams.ResidualParameters.Xe(), params.LogQP(),
btpParams.ResidualParameters.MaxLevel(),
btpParams.ResidualParameters.LogDefaultScale())
// And some information about the bootstrapping parameters.
// We can notably check that the LogQP of the bootstrapping parameters is smaller than 1550, which ensures
// 128-bit of security as explained above.
fmt.Printf("Bootstrapping parameters: logN=%d, logSlots=%d, H(%d; %d), sigma=%f, logQP=%f, levels=%d, scale=2^%d\n",
btpParams.BootstrappingParameters.LogN(),
btpParams.BootstrappingParameters.LogMaxSlots(),
btpParams.BootstrappingParameters.XsHammingWeight(),
btpParams.EphemeralSecretWeight,
btpParams.BootstrappingParameters.Xe(),
btpParams.BootstrappingParameters.LogQP(),
btpParams.BootstrappingParameters.QCount(),
btpParams.BootstrappingParameters.LogDefaultScale())
//===========================
//=== 4) KEYGEN & ENCRYPT ===
//===========================
// Now that both the residual and bootstrapping parameters are instantiated, we can
// instantiate the usual necessary object to encode, encrypt and decrypt.
// Scheme context and keys
kgen := rlwe.NewKeyGenerator(params)
sk, pk := kgen.GenKeyPairNew()
encoder := hefloat.NewEncoder(params)
decryptor := rlwe.NewDecryptor(params, sk)
encryptor := rlwe.NewEncryptor(params, pk)
fmt.Println()
fmt.Println("Generating bootstrapping evaluation keys...")
evk, _, err := btpParams.GenEvaluationKeys(sk)
if err != nil {
panic(err)
}
fmt.Println("Done")
//========================
//=== 5) BOOTSTRAPPING ===
//========================
btpParams.CoeffsToSlotsParameters.Scaling = new(big.Float).SetFloat64(1)
// Instantiates the bootstrapper
var eval *bootstrapping.Evaluator
if eval, err = bootstrapping.NewEvaluator(btpParams, evk); err != nil {
panic(err)
}
fmt.Println(btpParams.CoeffsToSlotsParameters.Scaling)
if eval, err = bootstrapping.NewEvaluator(btpParams, evk); err != nil {
panic(err)
}
fmt.Println(btpParams.CoeffsToSlotsParameters.Scaling)
// Generate a random plaintext with values uniformely distributed in [-1, 1] for the real and imaginary part.
valuesWant := make([]complex128, params.MaxSlots())
for i := range valuesWant {
valuesWant[i] = sampling.RandComplex128(-1, 1)
}
// We encrypt at level 0
plaintext := hefloat.NewPlaintext(params, 0)
if err := encoder.Encode(valuesWant, plaintext); err != nil {
panic(err)
}
// Encrypt
ciphertext1, err := encryptor.EncryptNew(plaintext)
if err != nil {
panic(err)
}
// Decrypt, print and compare with the plaintext values
fmt.Println()
fmt.Println("Precision of values vs. ciphertext")
valuesTest1 := printDebug(params, ciphertext1, valuesWant, decryptor, encoder)
// Bootstrap the ciphertext (homomorphic re-encryption)
// It takes a ciphertext at level 0 (if not at level 0, then it will reduce it to level 0)
// and returns a ciphertext with the max level of `floatParamsResidualLit`.
// CAUTION: the scale of the ciphertext MUST be equal (or very close) to params.DefaultScale()
// To equalize the scale, the function evaluator.SetScale(ciphertext, parameters.DefaultScale()) can be used at the expense of one level.
// If the ciphertext is is at level one or greater when given to the bootstrapper, this equalization is automatically done.
fmt.Println("Bootstrapping...")
ciphertext2, err := eval.Bootstrap(ciphertext1)
if err != nil {
panic(err)
}
fmt.Println("Done")
//==================
//=== 6) DECRYPT ===
//==================
// Decrypt, print and compare with the plaintext values
fmt.Println()
fmt.Println("Precision of ciphertext vs. Bootstrap(ciphertext)")
printDebug(params, ciphertext2, valuesTest1, decryptor, encoder)
}
func printDebug(params hefloat.Parameters, ciphertext *rlwe.Ciphertext, valuesWant []complex128, decryptor *rlwe.Decryptor, encoder *hefloat.Encoder) (valuesTest []complex128) {
valuesTest = make([]complex128, ciphertext.Slots())
if err := encoder.Decode(decryptor.DecryptNew(ciphertext), valuesTest); err != nil {
panic(err)
}
fmt.Println()
fmt.Printf("Level: %d (logQ = %d)\n", ciphertext.Level(), params.LogQLvl(ciphertext.Level()))
fmt.Printf("Scale: 2^%f\n", math.Log2(ciphertext.Scale.Float64()))
fmt.Printf("ValuesTest: %6.10f %6.10f %6.10f %6.10f...\n", valuesTest[0], valuesTest[1], valuesTest[2], valuesTest[3])
fmt.Printf("ValuesWant: %6.10f %6.10f %6.10f %6.10f...\n", valuesWant[0], valuesWant[1], valuesWant[2], valuesWant[3])
precStats := hefloat.GetPrecisionStats(params, encoder, nil, valuesWant, valuesTest, 0, false)
fmt.Println(precStats.String())
fmt.Println()
return
}