diff --git a/core/rlwe/keygenerator.go b/core/rlwe/keygenerator.go index 9fc49c02d..0450f87d3 100644 --- a/core/rlwe/keygenerator.go +++ b/core/rlwe/keygenerator.go @@ -279,12 +279,13 @@ func (kgen KeyGenerator) genEvaluationKey(skIn ring.Poly, skOut ringqp.Poly, evk // For a compressed evaluation key, a seed is created and stored in the EvaluationKey struct // struct while an uncompressed key uses an ephemeral seed. if evk.IsCompressed() { - evk.Seed = make([]byte, 32) - if n, err := kgen.prng.Read(evk.Seed); n != 32 || err != nil { + var seed [32]byte + if n, err := kgen.prng.Read(seed[:]); n != 32 || err != nil { panic(fmt.Errorf("unable to sample evaluation key seed")) } + evk.Seed = &seed - sampler, err := sampling.NewKeyedPRNG(evk.Seed) + sampler, err := sampling.NewKeyedPRNG(seed[:]) if err != nil { panic(fmt.Errorf("sampling.NewKeyedPRNG: %w", err)) } diff --git a/core/rlwe/keys.go b/core/rlwe/keys.go index 35585d527..ac1f2c6f5 100644 --- a/core/rlwe/keys.go +++ b/core/rlwe/keys.go @@ -292,7 +292,7 @@ func (p *PublicKey) isEncryptionKey() {} // is used to bring it back to its original key. type EvaluationKey struct { GadgetCiphertext - Seed []byte + Seed *[32]byte // Must be != nil iff EvaluationKey.IsCompressed() = true } type EvaluationKeyParameters struct { @@ -362,7 +362,11 @@ func (evk EvaluationKey) Expand(params ParameterProvider, buffer *GadgetCipherte return fmt.Errorf("evaluation key is not compressed") } - prng, err := sampling.NewKeyedPRNG(evk.Seed) + if evk.Seed == nil { + return fmt.Errorf("seed is missing") + } + + prng, err := sampling.NewKeyedPRNG((*evk.Seed)[:]) if err != nil { panic(fmt.Errorf("sampling.NewKeyedPRNG: %s", err)) } @@ -417,6 +421,115 @@ func (evk EvaluationKey) Expand(params ParameterProvider, buffer *GadgetCipherte return nil } +// BinarySize returns the serialized size of the object in bytes. +func (evk EvaluationKey) BinarySize() (size int) { + if evk.Seed != nil { + return evk.GadgetCiphertext.BinarySize() + len(*evk.Seed) + } + return evk.GadgetCiphertext.BinarySize() +} + +// WriteTo writes the object on an [io.Writer]. It implements the [io.WriterTo] +// interface, and will write exactly object.BinarySize() bytes on w. +// +// Unless w implements the [buffer.Writer] interface (see lattigo/utils/buffer/writer.go), +// it will be wrapped into a [bufio.Writer]. Since this requires allocations, it +// is preferable to pass a [buffer.Writer] directly: +// +// - When writing multiple times to a [io.Writer], it is preferable to first wrap the +// io.Writer in a pre-allocated [bufio.Writer]. +// - When writing to a pre-allocated var b []byte, it is preferable to pass +// buffer.NewBuffer(b) as w (see lattigo/utils/buffer/buffer.go). +func (evk EvaluationKey) WriteTo(w io.Writer) (n int64, err error) { + switch w := w.(type) { + case buffer.Writer: + + var inc int64 + + if inc, err = evk.GadgetCiphertext.WriteTo(w); err != nil { + return n + inc, err + } + + n += inc + + if evk.IsCompressed() { + + // Sanity check, should not happen unless evk has been manually modified + if evk.Seed == nil { + return n + inc, fmt.Errorf("writing compressed evaluation key: the seed is nil") + } + + if inc, err = buffer.Write(w, (*evk.Seed)[:]); err != nil { + return n + inc, err + } + + n += inc + } + + if err = w.Flush(); err != nil { + return n, err + } + return + + default: + return evk.WriteTo(bufio.NewWriter(w)) + } +} + +// ReadFrom reads on the object from an [io.Writer]. It implements the +// [io.ReaderFrom] interface. +// +// Unless r implements the [buffer.Reader] interface (see see lattigo/utils/buffer/reader.go), +// it will be wrapped into a [bufio.Reader]. Since this requires allocation, it +// is preferable to pass a [buffer.Reader] directly: +// +// - When reading multiple values from a [io.Reader], it is preferable to first +// first wrap io.Reader in a pre-allocated [bufio.Reader]. +// - When reading from a var b []byte, it is preferable to pass a buffer.NewBuffer(b) +// as w (see lattigo/utils/buffer/buffer.go). +func (evk *EvaluationKey) ReadFrom(r io.Reader) (n int64, err error) { + switch r := r.(type) { + case buffer.Reader: + + var inc int64 + + if inc, err = evk.GadgetCiphertext.ReadFrom(r); err != nil { + return n + inc, err + } + + n += inc + + if evk.IsCompressed() { + var seed [32]byte + if inc, err = buffer.Read(r, seed[:]); err != nil { + return n + inc, err + } + + evk.Seed = &seed + + n += inc + } + + return + default: + return evk.ReadFrom(bufio.NewReader(r)) + } +} + +// MarshalBinary encodes the object into a binary form on a newly allocated slice of bytes. +func (evk EvaluationKey) MarshalBinary() (p []byte, err error) { + buf := buffer.NewBufferSize(evk.BinarySize()) + _, err = evk.WriteTo(buf) + return buf.Bytes(), err +} + +// UnmarshalBinary decodes a slice of bytes generated by +// [EvaluationKey.MarshalBinary] or [EvaluationKey.WriteTo] on the object. +func (evk *EvaluationKey) UnmarshalBinary(p []byte) (err error) { + _, err = evk.ReadFrom(buffer.NewBuffer(p)) + return +} + // CopyNew creates a deep copy of the target [EvaluationKey] and returns it. func (evk EvaluationKey) CopyNew() *EvaluationKey { return &EvaluationKey{GadgetCiphertext: *evk.GadgetCiphertext.CopyNew()} diff --git a/core/rlwe/rlwe_test.go b/core/rlwe/rlwe_test.go index f76dddf35..59aa75353 100644 --- a/core/rlwe/rlwe_test.go +++ b/core/rlwe/rlwe_test.go @@ -41,7 +41,7 @@ func testString(params Parameters, levelQ, levelP, bpw2 int, opname string) stri func TestRLWEConstSerialization(t *testing.T) { // Note: changing nbIteration will change the expected value const nbIteration = 10 - const expected = "XRdlwx5vEX9qdGY3CeeAxzGHa0gbXghzpLhV0eIgVk8=" + const expected = "/mTt2kB+03NdOMoI1msW+glCZmrF1sxEGQkFsC6P1SA=" var err error defaultParamsLiteral := testInsecure seedKeyGen := []byte{'l', 'a', 't'} @@ -77,16 +77,20 @@ func TestRLWEConstSerialization(t *testing.T) { hash.Write(pkBytes) // Add marshalled GaloisKey to the hash input - galEl := params.GaloisElement(-1) + galEl1 := params.GaloisElement(-1) galEl2 := params.GaloisElement(3) - galKey := detTC.kgen.GenGaloisKeysNew([]uint64{galEl, galEl2}, sk) - galKeyBytes, err := galKey[0].MarshalBinary() + galKey1 := detTC.kgen.GenGaloisKeyNew(galEl1, sk) + galKey2 := detTC.kgen.GenGaloisKeyNew(galEl2, sk, EvaluationKeyParameters{Compressed: true}) + galKeyBytes, err := galKey1.MarshalBinary() + require.Nil(t, err) + hash.Write(galKeyBytes) + galKeyBytes, err = galKey2.MarshalBinary() require.Nil(t, err) hash.Write(galKeyBytes) // Add marshalled MemEvaluationKeySet to the hash input relinKey := detTC.kgen.GenRelinearizationKeyNew(sk) - evk := NewMemEvaluationKeySet(relinKey, galKey...) + evk := NewMemEvaluationKeySet(relinKey, galKey1, galKey2) evkBytes, err := evk.MarshalBinary() require.Nil(t, err) hash.Write(evkBytes) @@ -1238,6 +1242,10 @@ func testWriteAndRead(tc *TestContext, bpw2 int, t *testing.T) { buffer.RequireSerializerCorrect(t, tc.kgen.GenEvaluationKeyNew(sk, sk)) }) + t.Run(testString(params, levelQ, levelP, bpw2, "WriteAndRead/EvaluationKey/Compressed=True"), func(t *testing.T) { + buffer.RequireSerializerCorrect(t, tc.kgen.GenEvaluationKeyNew(sk, sk, EvaluationKeyParameters{Compressed: true})) + }) + t.Run(testString(params, levelQ, levelP, bpw2, "WriteAndRead/RelinearizationKey"), func(t *testing.T) { buffer.RequireSerializerCorrect(t, tc.kgen.GenRelinearizationKeyNew(tc.sk)) })