diff --git a/dotnet/src/Serialization.cs b/dotnet/src/Serialization.cs
index 90e37b4b..45dc7f1f 100644
--- a/dotnet/src/Serialization.cs
+++ b/dotnet/src/Serialization.cs
@@ -25,7 +25,11 @@ public enum ComprModeType : byte
ZLIB = 1,
/// Use Zstandard compression.
- ZSTD = 2
+ ZSTD = 2,
+
+ /// Use bit-packing compression. Unlike ZLIB and Zstandard, bit-packing performs no
+ /// integrity checking of the data.
+ BitPack = 3
}
/// Class to provide functionality for serialization.
diff --git a/dotnet/tests/CiphertextTests.cs b/dotnet/tests/CiphertextTests.cs
index de143d0e..80114af9 100644
--- a/dotnet/tests/CiphertextTests.cs
+++ b/dotnet/tests/CiphertextTests.cs
@@ -221,6 +221,44 @@ public void BFVSaveLoadTest()
}
}
+ [TestMethod]
+ public void BFVBitPackSaveLoadTest()
+ {
+ SEALContext context = GlobalContext.BFVContext;
+ KeyGenerator keygen = new KeyGenerator(context);
+ keygen.CreatePublicKey(out PublicKey publicKey);
+
+ Encryptor encryptor = new Encryptor(context, publicKey);
+ Plaintext plain = new Plaintext("2x^3 + 4x^2 + 5x^1 + 6");
+ Ciphertext cipher = new Ciphertext();
+
+ encryptor.Encrypt(plain, cipher);
+
+ Ciphertext loaded = new Ciphertext();
+ long saveSize = 0;
+
+ using (MemoryStream mem = new MemoryStream())
+ {
+ saveSize = cipher.Save(mem, ComprModeType.BitPack);
+
+ mem.Seek(offset: 0, loc: SeekOrigin.Begin);
+
+ loaded.Load(context, mem);
+ }
+
+ Assert.IsTrue(ValCheck.IsValidFor(loaded, context));
+
+ ulong ulongCount = cipher.Size * cipher.PolyModulusDegree * cipher.CoeffModulusSize;
+ for (ulong i = 0; i < ulongCount; i++)
+ {
+ Assert.AreEqual(cipher[i], loaded[i]);
+ }
+
+ // The coefficients are uniformly random modulo the coefficient modulus primes, which are well under
+ // 64 bits, so packing each 64-bit word to its significant bits must beat the unpacked size.
+ Assert.IsTrue(saveSize < cipher.SaveSize(ComprModeType.None));
+ }
+
[TestMethod]
public void BGVSaveLoadTest()
{
diff --git a/dotnet/tests/SerializationTests.cs b/dotnet/tests/SerializationTests.cs
index e0a21976..7bf3654f 100644
--- a/dotnet/tests/SerializationTests.cs
+++ b/dotnet/tests/SerializationTests.cs
@@ -28,7 +28,9 @@ public void IsValidHeader()
invalidHeader.VersionMajor = 0x02;
Assert.IsFalse(Serialization.IsValidHeader(invalidHeader));
invalidHeader.VersionMajor = SEALVersion.Major;
- invalidHeader.ComprMode = (ComprModeType)0x03;
+ invalidHeader.ComprMode = ComprModeType.BitPack;
+ Assert.IsTrue(Serialization.IsValidHeader(invalidHeader));
+ invalidHeader.ComprMode = (ComprModeType)0x04;
Assert.IsFalse(Serialization.IsValidHeader(invalidHeader));
}
diff --git a/native/bench/CMakeLists.txt b/native/bench/CMakeLists.txt
index 0b0c99cb..663db826 100644
--- a/native/bench/CMakeLists.txt
+++ b/native/bench/CMakeLists.txt
@@ -58,6 +58,7 @@ if(SEAL_BUILD_BENCH)
${CMAKE_CURRENT_LIST_DIR}/bench.cpp
${CMAKE_CURRENT_LIST_DIR}/keygen.cpp
${CMAKE_CURRENT_LIST_DIR}/ntt.cpp
+ ${CMAKE_CURRENT_LIST_DIR}/serialize.cpp
${CMAKE_CURRENT_LIST_DIR}/bfv.cpp
${CMAKE_CURRENT_LIST_DIR}/bgv.cpp
${CMAKE_CURRENT_LIST_DIR}/ckks.cpp
diff --git a/native/bench/bench.cpp b/native/bench/bench.cpp
index 89dbb0dc..04423552 100644
--- a/native/bench/bench.cpp
+++ b/native/bench/bench.cpp
@@ -129,6 +129,7 @@ namespace sealbench
// 3. BGV
// 4. CKKS
// 5. Util
+ // 6. Serialize
int n = static_cast(parms.first);
int log_q = static_cast(
bm_env_map.find(parms_ckks)->second->context().key_context_data()->total_coeff_modulus_bit_count());
@@ -224,6 +225,148 @@ namespace sealbench
SEAL_BENCHMARK_REGISTER(UTIL, n, 0, NTTInverseLowLevel, bm_util_ntt_inverse_low_level, bm_env_bfv);
SEAL_BENCHMARK_REGISTER(UTIL, n, 0, NTTForwardLowLevelLazy, bm_util_ntt_forward_low_level_lazy, bm_env_bfv);
SEAL_BENCHMARK_REGISTER(UTIL, n, 0, NTTInverseLowLevelLazy, bm_util_ntt_inverse_low_level_lazy, bm_env_bfv);
+
+ // Serialization cases save and load a ciphertext (per scheme) and the keys under each supported
+ // compression mode, reporting the serialized size in bytes as the counter "size". Keys have the same
+ // size and content shape in every scheme, so they are registered once, under the BFV environment. Note
+ // that the Galois keys here contain two elements; full rotation key sets scale the numbers linearly.
+#define SEAL_BENCHMARK_REGISTER_SERIALIZE(name, func, env, mode) \
+ SEAL_BENCHMARK_REGISTER(SERIALIZE, n, log_q, name, func, env, seal::compr_mode_type::mode)
+
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBFVNone, bm_serialize_save_ct, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBFVNone, bm_serialize_load_ct, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBGVNone, bm_serialize_save_ct, bm_env_bgv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBGVNone, bm_serialize_load_ct, bm_env_bgv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextCKKSNone, bm_serialize_save_ct, bm_env_ckks, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextCKKSNone, bm_serialize_load_ct, bm_env_ckks, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SavePublicKeyNone, bm_serialize_save_pk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadPublicKeyNone, bm_serialize_load_pk, bm_env_bfv, none);
+#ifdef SEAL_USE_ZLIB
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBFVZlib, bm_serialize_save_ct, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBFVZlib, bm_serialize_load_ct, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBGVZlib, bm_serialize_save_ct, bm_env_bgv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBGVZlib, bm_serialize_load_ct, bm_env_bgv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextCKKSZlib, bm_serialize_save_ct, bm_env_ckks, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextCKKSZlib, bm_serialize_load_ct, bm_env_ckks, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SavePublicKeyZlib, bm_serialize_save_pk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadPublicKeyZlib, bm_serialize_load_pk, bm_env_bfv, zlib);
+#endif
+#ifdef SEAL_USE_ZSTD
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBFVZstd, bm_serialize_save_ct, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBFVZstd, bm_serialize_load_ct, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBGVZstd, bm_serialize_save_ct, bm_env_bgv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBGVZstd, bm_serialize_load_ct, bm_env_bgv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextCKKSZstd, bm_serialize_save_ct, bm_env_ckks, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextCKKSZstd, bm_serialize_load_ct, bm_env_ckks, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SavePublicKeyZstd, bm_serialize_save_pk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadPublicKeyZstd, bm_serialize_load_pk, bm_env_bfv, zstd);
+#endif
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBFVBitPack, bm_serialize_save_ct, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBFVBitPack, bm_serialize_load_ct, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextBGVBitPack, bm_serialize_save_ct, bm_env_bgv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextBGVBitPack, bm_serialize_load_ct, bm_env_bgv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveCiphertextCKKSBitPack, bm_serialize_save_ct, bm_env_ckks, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadCiphertextCKKSBitPack, bm_serialize_load_ct, bm_env_ckks, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SavePublicKeyBitPack, bm_serialize_save_pk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadPublicKeyBitPack, bm_serialize_load_pk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSecretKeyNone, bm_serialize_save_sk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSecretKeyNone, bm_serialize_load_sk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBFVNone, bm_serialize_save_seeded_ct, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBFVNone, bm_serialize_load_seeded_ct, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBGVNone, bm_serialize_save_seeded_ct, bm_env_bgv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBGVNone, bm_serialize_load_seeded_ct, bm_env_bgv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextCKKSNone, bm_serialize_save_seeded_ct, bm_env_ckks, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextCKKSNone, bm_serialize_load_seeded_ct, bm_env_ckks, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededPublicKeyNone, bm_serialize_save_seeded_pk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededPublicKeyNone, bm_serialize_load_seeded_pk, bm_env_bfv, none);
+#ifdef SEAL_USE_ZLIB
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSecretKeyZlib, bm_serialize_save_sk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSecretKeyZlib, bm_serialize_load_sk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBFVZlib, bm_serialize_save_seeded_ct, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBFVZlib, bm_serialize_load_seeded_ct, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBGVZlib, bm_serialize_save_seeded_ct, bm_env_bgv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBGVZlib, bm_serialize_load_seeded_ct, bm_env_bgv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextCKKSZlib, bm_serialize_save_seeded_ct, bm_env_ckks, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextCKKSZlib, bm_serialize_load_seeded_ct, bm_env_ckks, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededPublicKeyZlib, bm_serialize_save_seeded_pk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededPublicKeyZlib, bm_serialize_load_seeded_pk, bm_env_bfv, zlib);
+#endif
+#ifdef SEAL_USE_ZSTD
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSecretKeyZstd, bm_serialize_save_sk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSecretKeyZstd, bm_serialize_load_sk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBFVZstd, bm_serialize_save_seeded_ct, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBFVZstd, bm_serialize_load_seeded_ct, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextBGVZstd, bm_serialize_save_seeded_ct, bm_env_bgv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextBGVZstd, bm_serialize_load_seeded_ct, bm_env_bgv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededCiphertextCKKSZstd, bm_serialize_save_seeded_ct, bm_env_ckks, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededCiphertextCKKSZstd, bm_serialize_load_seeded_ct, bm_env_ckks, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededPublicKeyZstd, bm_serialize_save_seeded_pk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededPublicKeyZstd, bm_serialize_load_seeded_pk, bm_env_bfv, zstd);
+#endif
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSecretKeyBitPack, bm_serialize_save_sk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSecretKeyBitPack, bm_serialize_load_sk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ SaveSeededCiphertextBFVBitPack, bm_serialize_save_seeded_ct, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ LoadSeededCiphertextBFVBitPack, bm_serialize_load_seeded_ct, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ SaveSeededCiphertextBGVBitPack, bm_serialize_save_seeded_ct, bm_env_bgv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ LoadSeededCiphertextBGVBitPack, bm_serialize_load_seeded_ct, bm_env_bgv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ SaveSeededCiphertextCKKSBitPack, bm_serialize_save_seeded_ct, bm_env_ckks, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ LoadSeededCiphertextCKKSBitPack, bm_serialize_load_seeded_ct, bm_env_ckks, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededPublicKeyBitPack, bm_serialize_save_seeded_pk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededPublicKeyBitPack, bm_serialize_load_seeded_pk, bm_env_bfv, bitpack);
+ if (bm_env_bfv->context().using_keyswitching())
+ {
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveRelinKeysNone, bm_serialize_save_rlk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadRelinKeysNone, bm_serialize_load_rlk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveGaloisKeysNone, bm_serialize_save_glk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadGaloisKeysNone, bm_serialize_load_glk, bm_env_bfv, none);
+#ifdef SEAL_USE_ZLIB
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveRelinKeysZlib, bm_serialize_save_rlk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadRelinKeysZlib, bm_serialize_load_rlk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveGaloisKeysZlib, bm_serialize_save_glk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadGaloisKeysZlib, bm_serialize_load_glk, bm_env_bfv, zlib);
+#endif
+#ifdef SEAL_USE_ZSTD
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveRelinKeysZstd, bm_serialize_save_rlk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadRelinKeysZstd, bm_serialize_load_rlk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveGaloisKeysZstd, bm_serialize_save_glk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadGaloisKeysZstd, bm_serialize_load_glk, bm_env_bfv, zstd);
+#endif
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveRelinKeysBitPack, bm_serialize_save_rlk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadRelinKeysBitPack, bm_serialize_load_rlk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveGaloisKeysBitPack, bm_serialize_save_glk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadGaloisKeysBitPack, bm_serialize_load_glk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededRelinKeysNone, bm_serialize_save_seeded_rlk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededRelinKeysNone, bm_serialize_load_seeded_rlk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededGaloisKeysNone, bm_serialize_save_seeded_glk, bm_env_bfv, none);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededGaloisKeysNone, bm_serialize_load_seeded_glk, bm_env_bfv, none);
+#ifdef SEAL_USE_ZLIB
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededRelinKeysZlib, bm_serialize_save_seeded_rlk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededRelinKeysZlib, bm_serialize_load_seeded_rlk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededGaloisKeysZlib, bm_serialize_save_seeded_glk, bm_env_bfv, zlib);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededGaloisKeysZlib, bm_serialize_load_seeded_glk, bm_env_bfv, zlib);
+#endif
+#ifdef SEAL_USE_ZSTD
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededRelinKeysZstd, bm_serialize_save_seeded_rlk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededRelinKeysZstd, bm_serialize_load_seeded_rlk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(SaveSeededGaloisKeysZstd, bm_serialize_save_seeded_glk, bm_env_bfv, zstd);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(LoadSeededGaloisKeysZstd, bm_serialize_load_seeded_glk, bm_env_bfv, zstd);
+#endif
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ SaveSeededRelinKeysBitPack, bm_serialize_save_seeded_rlk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ LoadSeededRelinKeysBitPack, bm_serialize_load_seeded_rlk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ SaveSeededGaloisKeysBitPack, bm_serialize_save_seeded_glk, bm_env_bfv, bitpack);
+ SEAL_BENCHMARK_REGISTER_SERIALIZE(
+ LoadSeededGaloisKeysBitPack, bm_serialize_load_seeded_glk, bm_env_bfv, bitpack);
+ }
+#undef SEAL_BENCHMARK_REGISTER_SERIALIZE
}
} // namespace sealbench
diff --git a/native/bench/bench.h b/native/bench/bench.h
index bb801098..6a4f7cb5 100644
--- a/native/bench/bench.h
+++ b/native/bench/bench.h
@@ -425,4 +425,39 @@ namespace sealbench
void bm_ckks_rescale_inplace(benchmark::State &state, std::shared_ptr bm_env);
void bm_ckks_relin_inplace(benchmark::State &state, std::shared_ptr bm_env);
void bm_ckks_rotate(benchmark::State &state, std::shared_ptr bm_env);
+
+ /**
+ Benchmark cases for serialization save/load under a given compression mode. Each case reports the
+ serialized size in bytes as the counter "size".
+ */
+ void bm_serialize_save_ct(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_ct(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_pk(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_pk(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_rlk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_rlk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_glk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_glk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_sk(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_sk(benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_seeded_ct(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_seeded_ct(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_seeded_pk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_seeded_pk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_seeded_rlk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_seeded_rlk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_save_seeded_glk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
+ void bm_serialize_load_seeded_glk(
+ benchmark::State &state, std::shared_ptr bm_env, seal::compr_mode_type compr_mode);
} // namespace sealbench
diff --git a/native/bench/serialize.cpp b/native/bench/serialize.cpp
new file mode 100644
index 00000000..16a17202
--- /dev/null
+++ b/native/bench/serialize.cpp
@@ -0,0 +1,209 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+#include "seal/seal.h"
+#include "bench.h"
+
+using namespace benchmark;
+using namespace sealbench;
+using namespace seal;
+using namespace std;
+
+/**
+This file defines benchmarks for serialization: saving and loading ciphertexts and keys under each supported
+compression mode. Every case reports the serialized object size in bytes as the counter "size". Loading is
+measured through the load() member functions and therefore includes the validity checks they perform.
+*/
+
+namespace sealbench
+{
+ namespace
+ {
+ template
+ void bm_serialize_save(State &state, const T &in, compr_mode_type compr_mode)
+ {
+ vector buf(static_cast(in.save_size(compr_mode)));
+ streamoff size = 0;
+ for (auto _ : state)
+ {
+ size = in.save(buf.data(), buf.size(), compr_mode);
+ }
+ state.counters["size"] = static_cast(size);
+ }
+
+ template
+ void bm_serialize_load_from(State &state, const SEALContext &context, const vector &buf, size_t size)
+ {
+ T out;
+ for (auto _ : state)
+ {
+ out.load(context, buf.data(), size);
+ }
+ state.counters["size"] = static_cast(size);
+ }
+
+ template
+ void bm_serialize_load(State &state, const SEALContext &context, const T &in, compr_mode_type compr_mode)
+ {
+ vector buf(static_cast(in.save_size(compr_mode)));
+ size_t size = static_cast(in.save(buf.data(), buf.size(), compr_mode));
+ bm_serialize_load_from(state, context, buf, size);
+ }
+
+ // Loading a seeded object expands the seed into the full object, so the times below include the PRNG
+ // sampling that regenerates the seeded polynomials.
+ template
+ void bm_serialize_load_seeded(
+ State &state, const SEALContext &context, const Serializable &in, compr_mode_type compr_mode)
+ {
+ vector buf(static_cast(in.save_size(compr_mode)));
+ size_t size = static_cast(in.save(buf.data(), buf.size(), compr_mode));
+ bm_serialize_load_from(state, context, buf, size);
+ }
+
+ void randomize_ct(shared_ptr &bm_env, Ciphertext &ct)
+ {
+ switch (bm_env->parms().scheme())
+ {
+ case scheme_type::bfv:
+ bm_env->randomize_ct_bfv(ct);
+ break;
+ case scheme_type::bgv:
+ bm_env->randomize_ct_bgv(ct);
+ break;
+ case scheme_type::ckks:
+ bm_env->randomize_ct_ckks(ct);
+ break;
+ default:
+ break;
+ }
+ }
+
+ void randomize_pt(shared_ptr &bm_env, Plaintext &pt)
+ {
+ switch (bm_env->parms().scheme())
+ {
+ case scheme_type::bfv:
+ bm_env->randomize_pt_bfv(pt);
+ break;
+ case scheme_type::bgv:
+ bm_env->randomize_pt_bgv(pt);
+ break;
+ case scheme_type::ckks:
+ bm_env->randomize_pt_ckks(pt);
+ break;
+ default:
+ break;
+ }
+ }
+
+ Serializable make_seeded_ct(shared_ptr &bm_env)
+ {
+ Plaintext &pt = bm_env->pt()[0];
+ randomize_pt(bm_env, pt);
+ return bm_env->encryptor()->encrypt_symmetric(pt);
+ }
+ } // namespace
+
+ void bm_serialize_save_ct(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ Ciphertext &ct = bm_env->ct()[0];
+ randomize_ct(bm_env, ct);
+ bm_serialize_save(state, ct, compr_mode);
+ }
+
+ void bm_serialize_load_ct(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ Ciphertext &ct = bm_env->ct()[0];
+ randomize_ct(bm_env, ct);
+ bm_serialize_load(state, bm_env->context(), ct, compr_mode);
+ }
+
+ void bm_serialize_save_pk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_save(state, bm_env->pk(), compr_mode);
+ }
+
+ void bm_serialize_load_pk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_load(state, bm_env->context(), bm_env->pk(), compr_mode);
+ }
+
+ void bm_serialize_save_rlk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_save(state, bm_env->rlk(), compr_mode);
+ }
+
+ void bm_serialize_load_rlk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_load(state, bm_env->context(), bm_env->rlk(), compr_mode);
+ }
+
+ void bm_serialize_save_glk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_save(state, bm_env->glk(), compr_mode);
+ }
+
+ void bm_serialize_load_glk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_load(state, bm_env->context(), bm_env->glk(), compr_mode);
+ }
+
+ void bm_serialize_save_sk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_save(state, bm_env->sk(), compr_mode);
+ }
+
+ void bm_serialize_load_sk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ bm_serialize_load(state, bm_env->context(), bm_env->sk(), compr_mode);
+ }
+
+ void bm_serialize_save_seeded_ct(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = make_seeded_ct(bm_env);
+ bm_serialize_save(state, seeded, compr_mode);
+ }
+
+ void bm_serialize_load_seeded_ct(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = make_seeded_ct(bm_env);
+ bm_serialize_load_seeded(state, bm_env->context(), seeded, compr_mode);
+ }
+
+ void bm_serialize_save_seeded_pk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_public_key();
+ bm_serialize_save(state, seeded, compr_mode);
+ }
+
+ void bm_serialize_load_seeded_pk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_public_key();
+ bm_serialize_load_seeded(state, bm_env->context(), seeded, compr_mode);
+ }
+
+ void bm_serialize_save_seeded_rlk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_relin_keys();
+ bm_serialize_save(state, seeded, compr_mode);
+ }
+
+ void bm_serialize_load_seeded_rlk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_relin_keys();
+ bm_serialize_load_seeded(state, bm_env->context(), seeded, compr_mode);
+ }
+
+ void bm_serialize_save_seeded_glk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_galois_keys(bm_env->galois_elts_all());
+ bm_serialize_save(state, seeded, compr_mode);
+ }
+
+ void bm_serialize_load_seeded_glk(State &state, shared_ptr bm_env, compr_mode_type compr_mode)
+ {
+ auto seeded = bm_env->keygen()->create_galois_keys(bm_env->galois_elts_all());
+ bm_serialize_load_seeded(state, bm_env->context(), seeded, compr_mode);
+ }
+} // namespace sealbench
diff --git a/native/src/seal/serialization.cpp b/native/src/seal/serialization.cpp
index 477584d4..c4f348a2 100644
--- a/native/src/seal/serialization.cpp
+++ b/native/src/seal/serialization.cpp
@@ -4,6 +4,7 @@
#include "seal/dynarray.h"
#include "seal/memorymanager.h"
#include "seal/serialization.h"
+#include "seal/util/bitpack.h"
#include "seal/util/common.h"
#include "seal/util/streambuf.h"
#include "seal/util/ztools.h"
@@ -99,6 +100,9 @@ namespace seal
case compr_mode_type::zlib:
return ztools::zlib_deflate_size_bound(in_size);
#endif
+ case compr_mode_type::bitpack:
+ return bitpack::bitpack_size_bound(in_size);
+
case compr_mode_type::none:
// No compression
return in_size;
@@ -315,6 +319,29 @@ namespace seal
break;
}
#endif
+ case compr_mode_type::bitpack:
+ {
+ // First save_members to a temporary byte stream; set the size of the temporary stream to be right from
+ // the start to avoid extra reallocs.
+ SafeByteBuffer safe_buffer(
+ bitpack::bitpack_size_bound(raw_size - static_cast(sizeof(SEALHeader))), clear_buffers);
+ iostream temp_stream(&safe_buffer);
+ temp_stream.exceptions(ios_base::badbit | ios_base::failbit);
+ save_members(temp_stream);
+
+ auto safe_pool(MemoryManager::GetPool(mm_prof_opt::mm_force_new, clear_buffers));
+
+ // Create temporary aliasing DynArray to wrap safe_buffer
+ DynArray safe_buffer_array(
+ Pointer::Aliasing(safe_buffer.data()), safe_buffer.size(),
+ static_cast(temp_stream.tellp()), false, safe_pool);
+
+ // After bit-packing, write_header_pack_buffer will write the final size to the given header and
+ // write the header to stream, before writing the bit-packed output.
+ bitpack::bitpack_write_header_pack_buffer(
+ safe_buffer_array, reinterpret_cast(&header), stream, safe_pool);
+ break;
+ }
default:
throw invalid_argument("unsupported compression mode");
}
@@ -514,6 +541,45 @@ namespace seal
break;
}
#endif
+ case compr_mode_type::bitpack:
+ {
+ // header.size counts the whole object including the header, so the packed payload is exactly
+ // header.size - sizeof(SEALHeader). Computing it directly keeps it correct on non-seekable streams,
+ // where tellg() returns -1 and cannot be used to measure the header.
+ auto packed_size = header.size - static_cast(sizeof(SEALHeader));
+
+ auto safe_pool = MemoryManager::GetPool(mm_prof_opt::mm_force_new, clear_buffers);
+
+ // Unpack on demand directly into the parser rather than decoding the entire payload up front. This
+ // bounds memory use during loading to what the parser actually reads, so a hostile size claim in the
+ // packed data cannot drive an unbounded allocation.
+ streamoff packed_remaining = 0;
+ {
+ auto unpack_buffer =
+ bitpack::make_bitpack_unpack_buffer(stream, safe_cast(packed_size), safe_pool);
+ istream temp_stream(unpack_buffer.get());
+ temp_stream.exceptions(ios_base::badbit | ios_base::failbit);
+
+ load_members(temp_stream, version);
+
+ if (unpack_buffer->failed())
+ {
+ throw logic_error("stream decompression failed");
+ }
+ packed_remaining = unpack_buffer->remaining();
+ }
+
+ // The parser may not have pulled the whole packed payload. On a seekable stream, where header.size
+ // was confirmed to fit the available input, skip any remainder so the stream ends exactly at
+ // stream_start_pos + header.size, keeping concatenated objects loadable. On a non-seekable stream
+ // header.size is unverified, so skipping is gated off: an inflated header.size must not drive
+ // stream.ignore() past the real data into an over-read or an indefinite block.
+ if (size_verified && packed_remaining > 0)
+ {
+ stream.ignore(packed_remaining);
+ }
+ break;
+ }
default:
throw invalid_argument("unsupported compression mode");
}
diff --git a/native/src/seal/serialization.h b/native/src/seal/serialization.h
index 1ea6730c..273a6fab 100644
--- a/native/src/seal/serialization.h
+++ b/native/src/seal/serialization.h
@@ -32,6 +32,9 @@ namespace seal
// Use Zstandard compression
zstd = 2,
#endif
+ // Use bit-packing compression. Unlike ZLIB and Zstandard, bit-packing performs no integrity
+ // checking of the data.
+ bitpack = 3,
};
/**
@@ -109,7 +112,9 @@ namespace seal
#endif
#ifdef SEAL_USE_ZSTD
case static_cast(compr_mode_type::zstd):
+ /* fall through */
#endif
+ case static_cast(compr_mode_type::bitpack):
return true;
}
return false;
diff --git a/native/src/seal/util/CMakeLists.txt b/native/src/seal/util/CMakeLists.txt
index 7863325f..7651034e 100644
--- a/native/src/seal/util/CMakeLists.txt
+++ b/native/src/seal/util/CMakeLists.txt
@@ -3,6 +3,7 @@
# Source files in this directory
set(SEAL_SOURCE_FILES ${SEAL_SOURCE_FILES}
+ ${CMAKE_CURRENT_LIST_DIR}/bitpack.cpp
${CMAKE_CURRENT_LIST_DIR}/blake2b.c
${CMAKE_CURRENT_LIST_DIR}/blake2xb.c
${CMAKE_CURRENT_LIST_DIR}/clipnormal.cpp
@@ -31,6 +32,7 @@ set(SEAL_SOURCE_FILES ${SEAL_SOURCE_FILES}
# Add header files for installation
install(
FILES
+ ${CMAKE_CURRENT_LIST_DIR}/bitpack.h
${CMAKE_CURRENT_LIST_DIR}/blake2.h
${CMAKE_CURRENT_LIST_DIR}/blake2-impl.h
${CMAKE_CURRENT_LIST_DIR}/clang.h
diff --git a/native/src/seal/util/bitpack.cpp b/native/src/seal/util/bitpack.cpp
new file mode 100644
index 00000000..39f1f2dd
--- /dev/null
+++ b/native/src/seal/util/bitpack.cpp
@@ -0,0 +1,382 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+#include "seal/serialization.h"
+#include "seal/util/bitpack.h"
+#include "seal/util/common.h"
+#include
+#include
+
+using namespace std;
+
+namespace seal
+{
+ namespace util
+ {
+ namespace bitpack
+ {
+ namespace
+ {
+ // Size in bytes of the 64-bit words being packed.
+ constexpr size_t bytes_per_word = sizeof(uint64_t);
+
+ // Slack allowing the packing and unpacking loops to address words through whole-uint64_t reads and
+ // writes near the end of a block without stepping out of bounds.
+ constexpr size_t block_slack = bytes_per_word;
+
+ // Number of whole 64-bit words in a block of block_len bytes read at the given phase.
+ SEAL_NODISCARD inline size_t block_word_count(size_t block_len, size_t phase) noexcept
+ {
+ return (block_len - phase) / bytes_per_word;
+ }
+
+ // Encoded size in bytes of the body of a block of block_len bytes read at the given phase: the
+ // verbatim phase bytes, the packed words, and the verbatim tail bytes.
+ SEAL_NODISCARD inline size_t block_body_size(size_t block_len, size_t phase, int width) noexcept
+ {
+ size_t words = block_word_count(block_len, phase);
+ size_t packed_bytes = (words * static_cast(width) + size_t(7)) >> 3;
+ return block_len - words * bytes_per_word + packed_bytes;
+ }
+ } // namespace
+
+ void bitpack_write_header_pack_buffer(
+ const DynArray &in, void *header_ptr, ostream &out_stream, MemoryPoolHandle pool)
+ {
+ if (!pool)
+ {
+ throw invalid_argument("pool is uninitialized");
+ }
+
+ Serialization::SEALHeader &header = *reinterpret_cast(header_ptr);
+
+ size_t in_size = in.size();
+ const unsigned char *in_data = reinterpret_cast(in.cbegin());
+
+ // Allocates a zero-filled array with block_slack bytes of extra headroom past the size bound.
+ DynArray out(add_safe(bitpack_size_bound(in_size), block_slack), pool);
+ unsigned char *out_data = reinterpret_cast(out.begin());
+
+ // Write the 9-byte stream header: the original byte count and the base-2 log of the block size
+ uint64_t in_size64 = static_cast(in_size);
+ memcpy(out_data, &in_size64, bytes_per_word);
+ size_t out_pos = bytes_per_word;
+ out_data[out_pos++] = static_cast(get_significant_bit_count(bitpack_block_bytes) - 1);
+
+ for (size_t block_start = 0; block_start < in_size;)
+ {
+ size_t block_len = min(bitpack_block_bytes, in_size - block_start);
+ const unsigned char *block_in = in_data + block_start;
+
+ // The word data in the stream need not fall on the stream's own word grid (serialized metadata
+ // is not always a multiple of eight bytes), so choose the phase that minimizes the encoded size
+ // of the block. A phase of bytes_per_word would reproduce the alignment of phase zero, so only
+ // smaller values need to be considered. The clamp to block_len keeps the word count from
+ // underflowing on a block shorter than a word; for such a block every phase encodes zero words
+ // at the same size and the tie-break below settles on phase zero.
+ //
+ // (Note: The current approach scans through the block eight times, one for each possible phase.
+ // An optimization is possible that cuts this down to one scan, using the fact that we could OR
+ // all (phase 0) uint64_ts together and infer the phase from where the zeroes ended up. However,
+ // this has more complicated bookkeeping around the start and end of the block and leads to only a
+ // modest speedup for an already very-fast routine, so it's been left for future work.)
+ size_t phase = 0;
+ int width = 0;
+ size_t body_size = block_len;
+ for (size_t p = 0; p <= min(bytes_per_word - 1, block_len); p++)
+ {
+ size_t words = block_word_count(block_len, p);
+ uint64_t block_or = 0;
+ for (size_t i = 0; i < words; i++)
+ {
+ uint64_t word = 0;
+ memcpy(&word, block_in + p + i * bytes_per_word, bytes_per_word);
+ block_or |= word;
+ }
+ int p_width = get_significant_bit_count(block_or);
+ size_t p_body_size = block_body_size(block_len, p, p_width);
+ if (p == 0 || p_body_size < body_size)
+ {
+ phase = p;
+ width = p_width;
+ body_size = p_body_size;
+ }
+ }
+ size_t words = block_word_count(block_len, phase);
+
+ out_data[out_pos++] = static_cast(width);
+ out_data[out_pos++] = static_cast(phase);
+
+ // Verbatim phase bytes
+ memcpy(out_data + out_pos, block_in, phase);
+ out_pos += phase;
+
+ // Pack the words consecutively starting from the least significant bit. Each word carries at
+ // most width significant bits, so nothing is lost; the read-modify-write below only ever ORs
+ // significant bits into the zero-filled output.
+ unsigned char *packed_out = out_data + out_pos;
+ size_t bit_pos = 0;
+ for (size_t i = 0; i < words; i++)
+ {
+ uint64_t word = 0;
+ memcpy(&word, block_in + phase + i * bytes_per_word, bytes_per_word);
+ size_t byte_index = bit_pos >> 3;
+ int shift = static_cast(bit_pos & size_t(7));
+ uint64_t low_word = 0;
+ memcpy(&low_word, packed_out + byte_index, bytes_per_word);
+ low_word |= word << shift;
+ memcpy(packed_out + byte_index, &low_word, bytes_per_word);
+ if (shift && width > bits_per_uint64 - shift)
+ {
+ packed_out[byte_index + bytes_per_word] =
+ static_cast(word >> (bits_per_uint64 - shift));
+ }
+ bit_pos += static_cast(width);
+ }
+ out_pos += (words * static_cast(width) + size_t(7)) >> 3;
+
+ // Verbatim tail bytes
+ size_t tail = block_len - phase - words * bytes_per_word;
+ memcpy(out_data + out_pos, block_in + block_len - tail, tail);
+ out_pos += tail;
+
+ block_start += block_len;
+ }
+
+ // Populate the header
+ header.compr_mode = compr_mode_type::bitpack;
+ header.size = static_cast(add_safe(sizeof(Serialization::SEALHeader), out_pos));
+
+ auto old_except_mask = out_stream.exceptions();
+ try
+ {
+ // Throw exceptions on ios_base::badbit and ios_base::failbit
+ out_stream.exceptions(ios_base::badbit | ios_base::failbit);
+
+ // Write the header and the data
+ out_stream.write(reinterpret_cast(&header), sizeof(Serialization::SEALHeader));
+ out_stream.write(reinterpret_cast(out_data), safe_cast(out_pos));
+ }
+ catch (...)
+ {
+ out_stream.exceptions(old_except_mask);
+ throw;
+ }
+
+ out_stream.exceptions(old_except_mask);
+ }
+
+ BitUnpackGetBuffer::BitUnpackGetBuffer(istream &in_stream, streamoff in_size, MemoryPoolHandle pool)
+ : pool_(std::move(pool)), in_stream_(in_stream), in_remaining_(in_size),
+ in_stream_except_mask_(in_stream.exceptions())
+ {
+ if (!pool_)
+ {
+ throw invalid_argument("pool is uninitialized");
+ }
+
+ // Unpacking reports failure through failed_ rather than stream exceptions, so clear the mask while
+ // we read; it is restored in the destructor.
+ in_stream_.exceptions(ios_base::goodbit);
+
+ // Start with an empty get area so that the first read triggers underflow(); the buffers are
+ // allocated once the block size has been read from the packed data.
+ setg(nullptr, nullptr, nullptr);
+ }
+
+ BitUnpackGetBuffer::~BitUnpackGetBuffer()
+ {
+ in_stream_.exceptions(in_stream_except_mask_);
+ }
+
+ streamsize BitUnpackGetBuffer::read_packed(unsigned char *dst, streamsize count)
+ {
+ streamsize to_read = min(count, in_remaining_);
+ if (to_read <= 0)
+ {
+ return 0;
+ }
+ in_stream_.read(reinterpret_cast(dst), to_read);
+ streamsize got = in_stream_.gcount();
+ in_remaining_ -= got;
+ return got;
+ }
+
+ size_t BitUnpackGetBuffer::unpack_block()
+ {
+ if (finished_)
+ {
+ return 0;
+ }
+
+ if (!started_)
+ {
+ // The compressed stream begins with its 9-byte header: the original byte count and the base-2
+ // log of the block size
+ unsigned char prologue[bytes_per_word + 1];
+ if (read_packed(prologue, static_cast(sizeof(prologue))) !=
+ static_cast(sizeof(prologue)))
+ {
+ failed_ = true;
+ return 0;
+ }
+ memcpy(&raw_remaining_, prologue, bytes_per_word);
+ int block_log2 = static_cast(prologue[bytes_per_word]);
+ if (block_log2 < bitpack_block_log2_min || block_log2 > bitpack_block_log2_max)
+ {
+ failed_ = true;
+ return 0;
+ }
+ block_bytes_ = size_t(1) << block_log2;
+ in_buf_ = allocate(block_bytes_ + block_slack, pool_);
+ out_buf_ = allocate(block_bytes_, pool_);
+ started_ = true;
+ if (!raw_remaining_)
+ {
+ finished_ = true;
+ return 0;
+ }
+ }
+
+ size_t block_len =
+ static_cast(min(static_cast(block_bytes_), raw_remaining_));
+
+ unsigned char block_header[2];
+ if (read_packed(block_header, 2) != 2)
+ {
+ failed_ = true;
+ return 0;
+ }
+ int width = static_cast(block_header[0]);
+ size_t phase = static_cast(block_header[1]);
+ if (width > bits_per_uint64 || phase > min(bytes_per_word - 1, block_len))
+ {
+ failed_ = true;
+ return 0;
+ }
+ size_t words = block_word_count(block_len, phase);
+ size_t packed_bytes = (words * static_cast(width) + size_t(7)) >> 3;
+ size_t tail = block_len - phase - words * bytes_per_word;
+
+ // Verbatim phase bytes
+ if (read_packed(out_buf_.get(), safe_cast(phase)) != safe_cast(phase))
+ {
+ failed_ = true;
+ return 0;
+ }
+
+ if (read_packed(in_buf_.get(), safe_cast(packed_bytes)) !=
+ safe_cast(packed_bytes))
+ {
+ failed_ = true;
+ return 0;
+ }
+
+ // Unpack the words. The whole-uint64_t reads may pick up bits past the packed data (block_slack
+ // bytes of headroom make them safe); the mask discards everything above the significant bits.
+ uint64_t mask = (width == bits_per_uint64) ? ~uint64_t(0) : ((uint64_t(1) << width) - 1);
+ size_t bit_pos = 0;
+ for (size_t i = 0; i < words; i++)
+ {
+ size_t byte_index = bit_pos >> 3;
+ int shift = static_cast(bit_pos & size_t(7));
+ uint64_t low_word = 0;
+ memcpy(&low_word, in_buf_.get() + byte_index, bytes_per_word);
+ low_word >>= shift;
+ if (shift && width > bits_per_uint64 - shift)
+ {
+ uint64_t high_byte = in_buf_.get()[byte_index + bytes_per_word];
+ low_word |= high_byte << (bits_per_uint64 - shift);
+ }
+ uint64_t word = low_word & mask;
+ memcpy(out_buf_.get() + phase + i * bytes_per_word, &word, bytes_per_word);
+ bit_pos += static_cast(width);
+ }
+
+ // Verbatim tail bytes
+ if (read_packed(out_buf_.get() + block_len - tail, safe_cast(tail)) !=
+ safe_cast(tail))
+ {
+ failed_ = true;
+ return 0;
+ }
+
+ raw_remaining_ -= block_len;
+ if (!raw_remaining_)
+ {
+ finished_ = true;
+ }
+ return block_len;
+ }
+
+ BitUnpackGetBuffer::int_type BitUnpackGetBuffer::underflow()
+ {
+ if (gptr() < egptr())
+ {
+ return traits_type::to_int_type(*gptr());
+ }
+
+ // Pull and unpack blocks until we produce output, reach the end of the data, or fail. unpack_block()
+ // guarantees progress: whenever it makes none it sets failed_ or finished_, so this loop always
+ // terminates.
+ while (!failed_)
+ {
+ size_t produced = unpack_block();
+ if (failed_)
+ {
+ break;
+ }
+ if (produced)
+ {
+ char_type *base = reinterpret_cast(out_buf_.get());
+ setg(base, base, base + produced);
+ total_produced_ += static_cast(produced);
+ return traits_type::to_int_type(*gptr());
+ }
+ if (finished_)
+ {
+ return traits_type::eof();
+ }
+ }
+ return traits_type::eof();
+ }
+
+ streamsize BitUnpackGetBuffer::xsgetn(char_type *s, streamsize count)
+ {
+ streamsize total = 0;
+ while (total < count)
+ {
+ if (gptr() == egptr() && traits_type::eq_int_type(underflow(), traits_type::eof()))
+ {
+ break;
+ }
+ streamsize avail = min(count - total, static_cast(egptr() - gptr()));
+ copy_n(gptr(), avail, s + total);
+
+ // avail is at most the block size (at most 64 KB), which is well within the range of int.
+ gbump(static_cast(avail));
+ total += avail;
+ }
+ return total;
+ }
+
+ BitUnpackGetBuffer::pos_type BitUnpackGetBuffer::seekoff(
+ off_type off, ios_base::seekdir dir, ios_base::openmode which)
+ {
+ // Only a no-op seek to the current input position is supported, i.e. tellg(). The position is the
+ // number of unpacked bytes already consumed from the get area.
+ if (off == 0 && dir == ios_base::cur && (which & ios_base::in))
+ {
+ return pos_type(total_produced_ - static_cast(egptr() - gptr()));
+ }
+ return pos_type(off_type(-1));
+ }
+
+ unique_ptr make_bitpack_unpack_buffer(
+ istream &in_stream, streamoff in_size, MemoryPoolHandle pool)
+ {
+ return make_unique(in_stream, in_size, std::move(pool));
+ }
+ } // namespace bitpack
+ } // namespace util
+} // namespace seal
diff --git a/native/src/seal/util/bitpack.h b/native/src/seal/util/bitpack.h
new file mode 100644
index 00000000..bf5b2c7d
--- /dev/null
+++ b/native/src/seal/util/bitpack.h
@@ -0,0 +1,238 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT license.
+
+#pragma once
+
+#include "seal/dynarray.h"
+#include "seal/memorymanager.h"
+#include "seal/util/common.h"
+#include "seal/util/defines.h"
+#include "seal/util/pointer.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace seal
+{
+ namespace util
+ {
+ namespace bitpack
+ {
+ /**
+ The functions in this namespace implement the compr_mode_type::bitpack encoding of a serialized byte
+ stream. This can be thought of as a compression algorithm designed specifically for the types of byte
+ streams SEAL will most often be serializing.
+
+ The vast majority of data serialized / deserialized by SEAL will be arrays of uint64_t representing
+ polynomial coefficients in RNS form. Each of the uint64_ts in a single array will be residues modulo
+ some prime q, where q < 2^61. Thus, some number of high-order bits of these values will always be zero,
+ while the lower-order bits will (when serializing any cryptographic data) have high entropy. Standard
+ compression algorithms like zlib are designed to operate on whole bytes, so they will fail to compress
+ any zero bits that do not appear as a whole byte, and the high-entropy cryptographic bits are
+ incompressible.
+
+ The optimal way to compress these arrays of polynomial coefficients would be to simply use knowledge
+ of q to append one coefficient after another with no padding bits in between. However, SEAL's serialization
+ API deliberately hides details of the payload's schema from the serializer, which just sees a raw byte
+ stream. While the majority of that byte stream will usually contain these uint64_t arrays, each array
+ may have a different value of q, and the stream may be interspersed with other small data structures such
+ as metadata, parms_ids, etc.
+
+ The bitpack compression scheme works by breaking the byte stream up into blocks of size
+ bitpack_block_bytes (currently 1024), and attempting to compress each block as if it contains a section
+ of a uint64_t array of polynomial coefficients modulo some q, using the strategy described below.
+ On blocks that really do contain such data, it achieves near-optimal compression; on blocks that contain
+ any other kind of data it will probably not compress the data at all, but such blocks are rare enough
+ that the overall performance when applied to real SEAL payloads is closer to optimal than standard
+ compression algorithms can achieve.
+
+ The structure of the output of bitpack compression is as follows. It begins with a 9-byte header,
+ consisting of the original size of the byte array pre-compression (8 bytes), and the log of
+ bitpack_block_bytes used for the stream (1 byte). The remainder of the output is a series of compressed
+ blocks.
+
+ +---------------+------------+---------+---------+-- --+---------+
+ | original size | block size | block 0 | block 1 | ... | block k |
+ | (8 bytes) | (log2, | | | | |
+ | | 1 byte) | | | | |
+ +---------------+------------+---------+---------+-- --+---------+
+
+ The compression algorithm for each block works independently, as follows. The algorithm assumes that
+ the input block contains a series of uint64_t values with some number of high bits that are consistently
+ 0; however, the start of the first uint64_t value may be offset from the start of the stream by some unknown
+ number of bytes between 0 and 7 (this misalignment can be caused by interleaved metadata earlier in the
+ stream, for example). This offset is called the "phase," and the compression algorithm simply tries all
+ possible phase values between 0 and 7 and proceeds with the one that yields the smallest compressed block.
+
+ An example input stream might look like the following. (The annotations below the figure represent where
+ the data came from.)
+
+ 00 00 00 00 00 25 EB 79 2B 00 00 00 00 14 AC 2D 27 00 00 00 ...
+ +-------------------+-------------------------------+---------------------------+
+ | metadata tail | coefficient 0 | coefficient 1 |
+ | (from prev block) | 0x2B79EB25 -> 30 bits | 0x272DAC14 -> 30 bits |
+ +-------------------+-------------------------------+---------------------------+
+
+ Here we can see that the optimal value of "phase" will be 5, since this stream does actually contain an
+ array of uint64_t values and they start on the sixth byte. The algorithm will then cast the remainder of the
+ block as uint64_ts, bitwise-OR those values together, and record the position of the highest-order 1 bit.
+ This is the "width," and the number of uint64_ts in the array is the value of "words". (The width is the
+ algorithm's best guess at the bit size of the coefficient modulus.) The final output of compression is
+ then:
+
+ +-----------+-----------+~~~~~~~~~~~~~+--------------------------------+~~~~~~~~~~~~~+
+ | width | phase | verbatim | packed words | verbatim |
+ | (1 byte, | (1 byte, | (phase | (ceil(words * width / 8) | (tail |
+ | max 64) | max 7) | bytes) | bytes) | bytes) |
+ +-----------+-----------+~~~~~~~~~~~~~+--------------------------------+~~~~~~~~~~~~~+
+
+ Here, the "verbatim" blocks before and after the packed words simply contain the raw data from the array
+ before the packed stream started and after it ends, if the phase value was not 0.
+
+ For the example stream above, this would look like:
+
+ 1E 05 00 00 00 00 00 25 EB 79 2B 05 6B CB ... xx xx xx
+ +-------+-------+~~~~~~~~~~~~~~~~+----------------------------------+~~~~~~~~~~~+
+ | width | phase | verbatim | packed words | verbatim |
+ | 1 B | 1 B | 5 B | 127 words x 30 bits -> 477 B | 3 B |
+ +-------+-------+~~~~~~~~~~~~~~~~+----------------------------------+~~~~~~~~~~~+
+
+ The trailing xx bytes are the start of a coefficient that no longer fit in a whole uint64_t before the
+ block ended; they are carried verbatim, and being the low-order bytes of a coefficient they are
+ high-entropy rather than zero. The total size of the compressed block in this example would be 487
+ bytes.
+
+ Two other scenarios are worth mentioning. First, when the input stream contains something other than the
+ expected content (e.g. metadata), generally this algorithm will not find any value for "width" below 64,
+ and phase will default to 0, meaning the "compressed" output will be two bytes carrying the width (64)
+ and phase (0), followed by the raw input block. Second, if the input contains all 0-bits for whatever
+ reason, the computed width and phase will both be 0, so the compressed output will simply be two bytes
+ containing 0s.
+ */
+
+ // Bounds for the base-2 logarithm of the block size accepted when unpacking.
+ constexpr int bitpack_block_log2_min = 6;
+
+ constexpr int bitpack_block_log2_max = 16;
+
+ // Number of original bytes encoded in each bit-packed block by this encoder.
+ constexpr std::size_t bitpack_block_bytes = 1024;
+
+ static_assert(
+ (bitpack_block_bytes & (bitpack_block_bytes - 1)) == 0 &&
+ bitpack_block_bytes >= (std::size_t(1) << bitpack_block_log2_min) &&
+ bitpack_block_bytes <= (std::size_t(1) << bitpack_block_log2_max),
+ "bitpack_block_bytes must be a power of two within the accepted range");
+
+ /**
+ Bit-packs data in the given buffer, completes the given SEALHeader by writing in the size of the output
+ and setting the compression mode to compr_mode_type::bitpack and finally writes the SEALHeader followed
+ by the bit-packed data in the given stream.
+
+ @param[in] in The buffer to bit-pack
+ @param[out] header_ptr A pointer to a SEALHeader instance matching the output of the encoding
+ @param[out] out_stream The stream to write to
+ @param[in] pool The MemoryPoolHandle pointing to a valid memory pool
+ @throws std::invalid_argument if pool is uninitialized
+ @throws std::runtime_error if I/O operations failed
+ */
+ void bitpack_write_header_pack_buffer(
+ const DynArray &in, void *header_ptr, std::ostream &out_stream, MemoryPoolHandle pool);
+
+ /**
+ A get-only stream buffer that unpacks bit-packed data from an underlying input stream on demand, one
+ block at a time, instead of decoding the entire payload up front into a single growing buffer. Because
+ the parser only pulls as many bytes as its (validated) parameters require, this bounds the memory used
+ during deserialization to a small constant, defending against hostile size claims in the encoded data.
+ The buffer is forward-only: it reports the current read position (so tellg() works, which nested loads
+ rely on) but does not support repositioning.
+ */
+ class BitUnpackGetBuffer final : public std::streambuf
+ {
+ public:
+ BitUnpackGetBuffer(std::istream &in_stream, std::streamoff in_size, MemoryPoolHandle pool);
+
+ ~BitUnpackGetBuffer() override;
+
+ BitUnpackGetBuffer(const BitUnpackGetBuffer ©) = delete;
+
+ BitUnpackGetBuffer &operator=(const BitUnpackGetBuffer &assign) = delete;
+
+ // True if malformed or truncated input was encountered while pulling data.
+ SEAL_NODISCARD bool failed() const noexcept
+ {
+ return failed_;
+ }
+
+ // Number of packed bytes from the bound that have not yet been read from the underlying stream.
+ SEAL_NODISCARD std::streamoff remaining() const noexcept
+ {
+ return in_remaining_;
+ }
+
+ private:
+ // Unpacks the next block into out_buf_, returning the number of bytes produced. Sets finished_ when
+ // all of the original bytes have been produced and failed_ on malformed or truncated input.
+ std::size_t unpack_block();
+
+ // Reads up to count packed bytes from the underlying stream, capped by the remaining bound. Returns
+ // the number of bytes actually read.
+ std::streamsize read_packed(unsigned char *dst, std::streamsize count);
+
+ int_type underflow() override;
+
+ std::streamsize xsgetn(char_type *s, std::streamsize count) override;
+
+ // Supports only tellg() (a no-op seek to the current input position); any other seek fails. This is
+ // enough for the nested loads that verify their size via tellg().
+ pos_type seekoff(
+ off_type off, std::ios_base::seekdir dir,
+ std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override;
+
+ MemoryPoolHandle pool_;
+
+ // Allocated once the block size has been read from the packed data.
+ Pointer in_buf_;
+
+ Pointer out_buf_;
+
+ std::istream &in_stream_;
+
+ std::streamoff in_remaining_;
+
+ std::ios_base::iostate in_stream_except_mask_;
+
+ // Number of original bytes that remain to be produced; valid once started_ is set.
+ std::uint64_t raw_remaining_ = 0;
+
+ // Block size read from the packed data; valid once started_ is set.
+ std::size_t block_bytes_ = 0;
+
+ bool started_ = false;
+
+ bool failed_ = false;
+
+ bool finished_ = false;
+
+ // Total unpacked bytes handed to the get area so far; used to report the read position.
+ std::streamoff total_produced_ = 0;
+ };
+
+ // Creates a streambuf that unpacks in_size bytes of bit-packed data from in_stream on demand.
+ std::unique_ptr make_bitpack_unpack_buffer(
+ std::istream &in_stream, std::streamoff in_size, MemoryPoolHandle pool);
+
+ template
+ SEAL_NODISCARD SizeT bitpack_size_bound(SizeT in_size)
+ {
+ // 9 bytes for the original size and the block size, and a width and a phase byte per block of
+ // bitpack_block_bytes = 1024 original bytes (plus rounding up); the blocks themselves never
+ // exceed their original size.
+ return util::add_safe(in_size, in_size >> 9, SizeT(17));
+ }
+ } // namespace bitpack
+ } // namespace util
+} // namespace seal
diff --git a/native/tests/seal/ciphertext.cpp b/native/tests/seal/ciphertext.cpp
index 032afbb2..e8323e92 100644
--- a/native/tests/seal/ciphertext.cpp
+++ b/native/tests/seal/ciphertext.cpp
@@ -127,6 +127,53 @@ namespace sealtest
ASSERT_TRUE(ctxt.data() != ctxt2.data());
}
+ TEST(CiphertextTest, BFVBitPackSaveLoadCiphertext)
+ {
+ stringstream stream;
+ EncryptionParameters parms(scheme_type::bfv);
+ parms.set_poly_modulus_degree(1024);
+ parms.set_coeff_modulus(CoeffModulus::BFVDefault(1024));
+ parms.set_plain_modulus(0xF0F0);
+
+ SEALContext context(parms, false);
+ KeyGenerator keygen(context);
+ PublicKey pk;
+ keygen.create_public_key(pk);
+ Encryptor encryptor(context, pk);
+
+ Ciphertext ctxt;
+ encryptor.encrypt(Plaintext("Ax^10 + 9x^9 + 8x^8 + 7x^7 + 6x^6 + 5x^5 + 4x^4 + 3x^3 + 2x^2 + 1"), ctxt);
+
+ // A bit-packed round-trip preserves the ciphertext exactly
+ auto bitpack_size = ctxt.save(stream, compr_mode_type::bitpack);
+ Ciphertext ctxt2;
+ ctxt2.load(context, stream);
+ ASSERT_TRUE(ctxt.parms_id() == ctxt2.parms_id());
+ ASSERT_TRUE(
+ is_equal_uint(ctxt.data(), ctxt2.data(), parms.poly_modulus_degree() * parms.coeff_modulus().size() * 2));
+ ASSERT_TRUE(ctxt.data() != ctxt2.data());
+
+ // The first block contains the ciphertext metadata (among them the full-width parms_id hash words) and
+ // packs at up to the full 64 bits, but every later block holds only coefficients smaller than the
+ // coefficient modulus primes and hence packs at their bit width, so the total is guaranteed to beat the
+ // unpacked size.
+ ASSERT_LT(bitpack_size, ctxt.save_size(compr_mode_type::none));
+
+ // Seeded ciphertexts bit-pack too: the same seeded object saved with and without bit-packing must load to
+ // identical data
+ Encryptor sym_encryptor(context, keygen.secret_key());
+ auto seeded = sym_encryptor.encrypt_symmetric(Plaintext("3x^7 + 2"));
+ stringstream seeded_stream;
+ seeded.save(seeded_stream, compr_mode_type::bitpack);
+ seeded.save(seeded_stream, compr_mode_type::none);
+ Ciphertext from_bitpack, from_none;
+ from_bitpack.load(context, seeded_stream);
+ from_none.load(context, seeded_stream);
+ ASSERT_TRUE(from_bitpack.parms_id() == from_none.parms_id());
+ ASSERT_TRUE(is_equal_uint(
+ from_bitpack.data(), from_none.data(), parms.poly_modulus_degree() * parms.coeff_modulus().size() * 2));
+ }
+
TEST(CiphertextTest, LoadZeroSizeRejectsOversizedDynArray)
{
EncryptionParameters parms(scheme_type::bfv);
diff --git a/native/tests/seal/galoiskeys.cpp b/native/tests/seal/galoiskeys.cpp
index 0d931af5..de7858bb 100644
--- a/native/tests/seal/galoiskeys.cpp
+++ b/native/tests/seal/galoiskeys.cpp
@@ -98,6 +98,61 @@ namespace sealtest
galoiskey_save_load(scheme_type::bgv);
}
+ TEST(GaloisKeysTest, GaloisKeysBitPackSaveLoad)
+ {
+ auto galoiskey_bitpack_save_load = [](scheme_type scheme) {
+ EncryptionParameters parms(scheme);
+ parms.set_poly_modulus_degree(256);
+ parms.set_plain_modulus(65537);
+ parms.set_coeff_modulus(CoeffModulus::Create(256, { 60, 50 }));
+ SEALContext context(parms, false, sec_level_type::none);
+ KeyGenerator keygen(context);
+
+ auto compare_keys = [](const GaloisKeys &a, const GaloisKeys &b) {
+ ASSERT_TRUE(a.parms_id() == b.parms_id());
+ ASSERT_EQ(a.data().size(), b.data().size());
+ for (size_t j = 0; j < a.data().size(); j++)
+ {
+ ASSERT_EQ(a.data()[j].size(), b.data()[j].size());
+ for (size_t i = 0; i < a.data()[j].size(); i++)
+ {
+ ASSERT_EQ(a.data()[j][i].data().dyn_array().size(), b.data()[j][i].data().dyn_array().size());
+ ASSERT_TRUE(is_equal_uint(
+ a.data()[j][i].data().data(), b.data()[j][i].data().data(),
+ a.data()[j][i].data().dyn_array().size()));
+ }
+ }
+ };
+
+ // Expanded keys round-trip bit-packed
+ stringstream stream;
+ GaloisKeys keys;
+ GaloisKeys test_keys;
+ keygen.create_galois_keys(keys);
+ auto bitpack_size = keys.save(stream, compr_mode_type::bitpack);
+ test_keys.load(context, stream);
+ compare_keys(keys, test_keys);
+
+ // The key data is uniformly random modulo the coefficient modulus primes, so bit-packing must beat
+ // the unpacked size
+ ASSERT_LT(bitpack_size, keys.save_size(compr_mode_type::none));
+
+ // Seeded keys bit-pack too, with the seeded polynomials regenerated on load: the same seeded object
+ // saved with and without bit-packing must load to identical keys
+ stringstream seeded_stream;
+ auto seeded = keygen.create_galois_keys();
+ seeded.save(seeded_stream, compr_mode_type::bitpack);
+ seeded.save(seeded_stream, compr_mode_type::none);
+ GaloisKeys from_bitpack;
+ GaloisKeys from_none;
+ from_bitpack.load(context, seeded_stream);
+ from_none.load(context, seeded_stream);
+ compare_keys(from_bitpack, from_none);
+ };
+ galoiskey_bitpack_save_load(scheme_type::bfv);
+ galoiskey_bitpack_save_load(scheme_type::bgv);
+ }
+
TEST(GaloisKeysTest, GaloisKeysSeededSaveLoad)
{
auto galoiskey_seeded_save_load = [](scheme_type scheme) {
diff --git a/native/tests/seal/relinkeys.cpp b/native/tests/seal/relinkeys.cpp
index 4ccb5675..99002d0b 100644
--- a/native/tests/seal/relinkeys.cpp
+++ b/native/tests/seal/relinkeys.cpp
@@ -83,6 +83,62 @@ namespace sealtest
relin_keys_save_load(scheme_type::bfv);
relin_keys_save_load(scheme_type::bgv);
}
+
+ TEST(RelinKeysTest, RelinKeysBitPackSaveLoad)
+ {
+ auto relin_keys_bitpack_save_load = [](scheme_type scheme) {
+ EncryptionParameters parms(scheme);
+ parms.set_poly_modulus_degree(256);
+ parms.set_plain_modulus(65537);
+ parms.set_coeff_modulus(CoeffModulus::Create(256, { 60, 50 }));
+ SEALContext context(parms, false, sec_level_type::none);
+ KeyGenerator keygen(context);
+
+ auto compare_keys = [](const RelinKeys &a, const RelinKeys &b) {
+ ASSERT_TRUE(a.parms_id() == b.parms_id());
+ ASSERT_EQ(a.data().size(), b.data().size());
+ for (size_t j = 0; j < a.data().size(); j++)
+ {
+ ASSERT_EQ(a.data()[j].size(), b.data()[j].size());
+ for (size_t i = 0; i < a.data()[j].size(); i++)
+ {
+ ASSERT_EQ(a.data()[j][i].data().dyn_array().size(), b.data()[j][i].data().dyn_array().size());
+ ASSERT_TRUE(is_equal_uint(
+ a.data()[j][i].data().data(), b.data()[j][i].data().data(),
+ a.data()[j][i].data().dyn_array().size()));
+ }
+ }
+ };
+
+ // Expanded keys round-trip bit-packed
+ stringstream stream;
+ RelinKeys keys;
+ RelinKeys test_keys;
+ keygen.create_relin_keys(keys);
+ auto bitpack_size = keys.save(stream, compr_mode_type::bitpack);
+ test_keys.load(context, stream);
+ compare_keys(keys, test_keys);
+
+ // The key data is uniformly random modulo the coefficient modulus primes, so bit-packing must beat
+ // the unpacked size
+ ASSERT_LT(bitpack_size, keys.save_size(compr_mode_type::none));
+
+ // Seeded keys bit-pack too, with the seeded polynomials regenerated on load: the same seeded object
+ // saved with and without bit-packing must load to identical keys
+ stringstream seeded_stream;
+ auto seeded = keygen.create_relin_keys();
+ seeded.save(seeded_stream, compr_mode_type::bitpack);
+ seeded.save(seeded_stream, compr_mode_type::none);
+ RelinKeys from_bitpack;
+ RelinKeys from_none;
+ from_bitpack.load(context, seeded_stream);
+ from_none.load(context, seeded_stream);
+ compare_keys(from_bitpack, from_none);
+ };
+ relin_keys_bitpack_save_load(scheme_type::bfv);
+ relin_keys_bitpack_save_load(scheme_type::bgv);
+ }
+
TEST(RelinKeysTest, RelinKeysSeededSaveLoad)
{
auto relin_keys_seeded_save_load = [](scheme_type scheme) {
diff --git a/native/tests/seal/serialization.cpp b/native/tests/seal/serialization.cpp
index 702794d4..ff003873 100644
--- a/native/tests/seal/serialization.cpp
+++ b/native/tests/seal/serialization.cpp
@@ -79,6 +79,35 @@ namespace sealtest
}
};
+ // A serializable object whose payload is a sequence of 64-bit words, mirroring how real SEAL objects store
+ // coefficient data; used to pin down the exact bit-packed output size.
+ struct word_struct
+ {
+ std::vector words;
+
+ void save_members(ostream &stream)
+ {
+ uint64_t n = static_cast(words.size());
+ stream.write(reinterpret_cast(&n), sizeof(uint64_t));
+ stream.write(reinterpret_cast(words.data()), static_cast(words.size() * 8));
+ }
+
+ void load_members(istream &stream)
+ {
+ uint64_t n = 0;
+ stream.read(reinterpret_cast(&n), sizeof(uint64_t));
+ words.resize(static_cast(n));
+ stream.read(reinterpret_cast(words.data()), static_cast(n * 8));
+ }
+
+ streamoff save_size(compr_mode_type compr_mode) const
+ {
+ size_t raw = sizeof(uint64_t) + words.size() * 8;
+ return static_cast(
+ sizeof(Serialization::SEALHeader) + Serialization::ComprSizeEstimate(raw, compr_mode));
+ }
+ };
+
// A serializable object that, on save, writes a small prefix followed by a large filler, but on load reads
// only the prefix. Modeling a hostile/oversized payload: the loader must not need to inflate the unread filler
// (the decompression-bomb defense), and must leave the stream positioned at the end of the object.
@@ -237,6 +266,22 @@ namespace sealtest
#ifdef SEAL_USE_ZLIB
modes.push_back(compr_mode_type::zlib);
#endif
+#ifdef SEAL_USE_ZSTD
+ modes.push_back(compr_mode_type::zstd);
+#endif
+ modes.push_back(compr_mode_type::bitpack);
+ return modes;
+ }
+
+ // The compression modes that verify the integrity of the compressed data on load. Bit-packing performs no
+ // integrity checking: corrupted packed bits decode to wrong values rather than a detected error, like
+ // compr_mode_type::none.
+ std::vector checksummed_compr_modes()
+ {
+ std::vector modes;
+#ifdef SEAL_USE_ZLIB
+ modes.push_back(compr_mode_type::zlib);
+#endif
#ifdef SEAL_USE_ZSTD
modes.push_back(compr_mode_type::zstd);
#endif
@@ -269,7 +314,9 @@ namespace sealtest
invalid_header.version_major = 0x02;
ASSERT_FALSE(Serialization::IsValidHeader(invalid_header));
invalid_header.version_major = SEAL_VERSION_MAJOR;
- invalid_header.compr_mode = (compr_mode_type)0x03;
+ invalid_header.compr_mode = compr_mode_type::bitpack;
+ ASSERT_TRUE(Serialization::IsValidHeader(invalid_header));
+ invalid_header.compr_mode = (compr_mode_type)0x04;
ASSERT_FALSE(Serialization::IsValidHeader(invalid_header));
}
@@ -430,6 +477,17 @@ namespace sealtest
ASSERT_EQ(st.c, st3.c);
}
#endif
+ {
+ test_struct st3;
+ out_size = Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), stream,
+ compr_mode_type::bitpack, false);
+ in_size = Serialization::Load(bind(&test_struct::load_members, &st3, _1), stream, false);
+ ASSERT_EQ(out_size, in_size);
+ ASSERT_EQ(st.a, st3.a);
+ ASSERT_EQ(st.b, st3.b);
+ ASSERT_EQ(st.c, st3.c);
+ }
}
TEST(SerializationTest, SaveLoadToBuffer)
@@ -510,6 +568,30 @@ namespace sealtest
ASSERT_EQ(st.c, st3.c);
}
#endif
+ {
+ // Reset buffer back to zero
+ memset(buffer, 0, arr_size);
+
+ test_struct st3;
+ ss.seekp(0);
+ test_out_size = Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+ out_size = Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), buffer, arr_size,
+ compr_mode_type::bitpack, false);
+ ASSERT_EQ(test_out_size, out_size);
+ for (size_t i = static_cast(out_size); i < arr_size; i++)
+ {
+ ASSERT_EQ(seal_byte{}, buffer[i]);
+ }
+
+ in_size = Serialization::Load(bind(&test_struct::load_members, &st3, _1), buffer, arr_size, false);
+ ASSERT_EQ(out_size, in_size);
+ ASSERT_EQ(st.a, st3.a);
+ ASSERT_EQ(st.b, st3.b);
+ ASSERT_EQ(st.c, st3.c);
+ }
}
// Round-trips a payload larger than the 256 KB internal decompression buffer to exercise multi-chunk streaming
@@ -624,7 +706,7 @@ namespace sealtest
st.data[i] = static_cast((i * 2654435761ULL) >> 24);
}
- for (auto mode : available_compr_modes())
+ for (auto mode : checksummed_compr_modes())
{
stringstream stream;
Serialization::Save(bind(&large_struct::save_members, &st, _1), st.save_size(mode), stream, mode, false);
@@ -803,4 +885,260 @@ namespace sealtest
ASSERT_LT(buf.consumed(), streamsize(1) << 20);
}
}
+
+ // Bit-packing an all-word payload of bounded-width values must produce exactly the size the format prescribes
+ // (the original size and the block size, then per block a width byte, a phase byte, and the packed words) and
+ // must round-trip.
+ TEST(SerializationTest, BitPackSizeAndRoundTrip)
+ {
+ using namespace placeholders;
+
+ // 1023 values of at most 36 significant bits; with the 8-byte count in front, the serialized stream is
+ // exactly 8192 bytes, i.e. eight full 1024-byte blocks of 128 word-aligned words each (phase 0, no
+ // verbatim bytes).
+ word_struct st;
+ st.words.resize(1023);
+ uint64_t state = 1;
+ for (size_t i = 0; i < st.words.size(); i++)
+ {
+ state = state * 6364136223846793005ULL + 1442695040888963407ULL;
+ st.words[i] = state & ((uint64_t(1) << 36) - 1);
+ }
+
+ // Pin the width of every block to exactly 36 bits. The stream words are the count followed by the values,
+ // so block i (of 128 stream words each) starts at value index 128 * i - 1.
+ st.words[0] |= uint64_t(1) << 35;
+ for (size_t block = 1; block < 8; block++)
+ {
+ st.words[128 * block - 1] |= uint64_t(1) << 35;
+ }
+
+ stringstream stream;
+ auto out_size = Serialization::Save(
+ bind(&word_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), stream,
+ compr_mode_type::bitpack, false);
+
+ // 16 (SEALHeader) + 8 (original size) + 1 (block size) + 8 * (1 width byte + 1 phase byte + 128 * 36 / 8)
+ ASSERT_EQ(16 + 8 + 1 + 8 * (2 + 576), out_size);
+
+ word_struct st2;
+ auto in_size = Serialization::Load(bind(&word_struct::load_members, &st2, _1), stream, false);
+ ASSERT_EQ(out_size, in_size);
+ ASSERT_TRUE(st.words == st2.words);
+ }
+
+ // The encoder must find word data that does not fall on the stream's own word grid: values shifted off the
+ // grid by a 1-byte prefix (as a seal_byte member does in real objects) must still pack at their bit width,
+ // costing only the per-block phase bytes relative to the aligned encoding.
+ TEST(SerializationTest, BitPackMisalignedWords)
+ {
+ using namespace placeholders;
+
+ word_struct aligned;
+ aligned.words.resize(1023);
+ uint64_t state = 12345;
+ for (size_t i = 0; i < aligned.words.size(); i++)
+ {
+ state = state * 6364136223846793005ULL + 1442695040888963407ULL;
+ aligned.words[i] = state & ((uint64_t(1) << 36) - 1);
+ }
+
+ struct prefixed_word_struct
+ {
+ word_struct inner;
+
+ void save_members(ostream &stream)
+ {
+ seal_byte prefix{};
+ stream.write(reinterpret_cast(&prefix), 1);
+ inner.save_members(stream);
+ }
+
+ streamoff save_size(compr_mode_type compr_mode) const
+ {
+ size_t raw = 1 + sizeof(uint64_t) + inner.words.size() * 8;
+ return static_cast(
+ sizeof(Serialization::SEALHeader) + Serialization::ComprSizeEstimate(raw, compr_mode));
+ }
+ };
+ prefixed_word_struct st;
+ st.inner = aligned;
+
+ stringstream aligned_stream;
+ auto aligned_size = Serialization::Save(
+ bind(&word_struct::save_members, &aligned, _1), aligned.save_size(compr_mode_type::bitpack), aligned_stream,
+ compr_mode_type::bitpack, false);
+
+ stringstream prefixed_stream;
+ auto prefixed_size = Serialization::Save(
+ bind(&prefixed_word_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), prefixed_stream,
+ compr_mode_type::bitpack, false);
+
+ // The prefixed stream is 1 original byte longer and spans one more block; realignment costs at most the
+ // per-block phase and tail verbatim bytes plus one extra block header, far below the 8 bits per word
+ // (over 4,000 bytes here) that losing alignment would cost.
+ ASSERT_LE(prefixed_size, aligned_size + 80);
+ }
+
+ // A width byte exceeding 64 is malformed and must be rejected cleanly.
+ TEST(SerializationTest, BitPackTamperedWidthThrows)
+ {
+ using namespace placeholders;
+
+ test_struct st{ 3, ~0, 3.14159 };
+ stringstream ss;
+ Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+
+ // The first block's width byte follows the SEALHeader (16 bytes), the original size (8 bytes), and the
+ // block size (1 byte).
+ string bytes = ss.str();
+ bytes[25] = static_cast(65);
+
+ stringstream tampered(bytes);
+ test_struct st2;
+ ASSERT_ANY_THROW(Serialization::Load(bind(&test_struct::load_members, &st2, _1), tampered, false));
+ }
+
+ // A phase byte exceeding 7 is malformed and must be rejected cleanly.
+ TEST(SerializationTest, BitPackTamperedPhaseThrows)
+ {
+ using namespace placeholders;
+
+ test_struct st{ 3, ~0, 3.14159 };
+ stringstream ss;
+ Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+
+ // The first block's phase byte follows the SEALHeader (16 bytes), the original size (8 bytes), the block
+ // size (1 byte), and the width byte.
+ string bytes = ss.str();
+ bytes[26] = static_cast(8);
+
+ stringstream tampered(bytes);
+ test_struct st2;
+ ASSERT_ANY_THROW(Serialization::Load(bind(&test_struct::load_members, &st2, _1), tampered, false));
+ }
+
+ // A block shorter than a word admits several equivalent encodings: any phase up to the block length splits
+ // the bytes between the verbatim phase prefix and the verbatim tail, with zero packed words. The decoder must
+ // accept all of them (an encoder is free to emit any) and must reject a phase beyond the block length, which
+ // would underflow the word count.
+ TEST(SerializationTest, BitPackTinyBlockPhases)
+ {
+ using namespace placeholders;
+
+ for (unsigned phase = 0; phase <= 4; phase++)
+ {
+ // Hand-craft a stream holding the 3 original bytes { 0xAA, 0xBB, 0xCC } in a single tiny block
+ Serialization::SEALHeader header;
+ header.compr_mode = compr_mode_type::bitpack;
+ header.size = 16 + 8 + 1 + 2 + 3;
+ string blob(reinterpret_cast(&header), sizeof(Serialization::SEALHeader));
+ uint64_t original_size = 3;
+ blob.append(reinterpret_cast(&original_size), sizeof(uint64_t));
+ blob.push_back(static_cast(10)); // block size: 2^10 bytes
+ blob.push_back(static_cast(0)); // width
+ blob.push_back(static_cast(phase));
+ blob.push_back(static_cast(0xAA));
+ blob.push_back(static_cast(0xBB));
+ blob.push_back(static_cast(0xCC));
+
+ stringstream stream(blob);
+ unsigned char loaded[3]{};
+ auto load_fn = [&](istream &in_stream, SEALVersion) {
+ in_stream.read(reinterpret_cast(loaded), 3);
+ };
+ if (phase <= 3)
+ {
+ Serialization::Load(load_fn, stream, false);
+ ASSERT_EQ(0xAA, loaded[0]);
+ ASSERT_EQ(0xBB, loaded[1]);
+ ASSERT_EQ(0xCC, loaded[2]);
+ }
+ else
+ {
+ ASSERT_ANY_THROW(Serialization::Load(load_fn, stream, false));
+ }
+ }
+ }
+
+ // A block size outside the accepted power-of-two range is malformed and must be rejected cleanly.
+ TEST(SerializationTest, BitPackTamperedBlockSizeThrows)
+ {
+ using namespace placeholders;
+
+ test_struct st{ 3, ~0, 3.14159 };
+ stringstream ss;
+ Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+
+ // The block size byte follows the SEALHeader (16 bytes) and the original size (8 bytes).
+ string bytes = ss.str();
+ bytes[24] = static_cast(5);
+
+ stringstream tampered(bytes);
+ test_struct st2;
+ ASSERT_ANY_THROW(Serialization::Load(bind(&test_struct::load_members, &st2, _1), tampered, false));
+
+ bytes[24] = static_cast(17);
+ stringstream tampered2(bytes);
+ ASSERT_ANY_THROW(Serialization::Load(bind(&test_struct::load_members, &st2, _1), tampered2, false));
+ }
+
+ // An understated original size makes the parser read past the end of the unpacked data and must be rejected
+ // cleanly.
+ TEST(SerializationTest, BitPackTamperedSizeThrows)
+ {
+ using namespace placeholders;
+
+ test_struct st{ 3, ~0, 3.14159 };
+ stringstream ss;
+ Serialization::Save(
+ bind(&test_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+
+ // The original size is the 8 bytes following the SEALHeader; understate it below what load_members reads.
+ string bytes = ss.str();
+ uint64_t small_size = 8;
+ memcpy(&bytes[16], &small_size, sizeof(uint64_t));
+
+ stringstream tampered(bytes);
+ test_struct st2;
+ ASSERT_ANY_THROW(Serialization::Load(bind(&test_struct::load_members, &st2, _1), tampered, false));
+ }
+
+ // An overstated original size promises far more data than the (unmodified) SEALHeader.size can back. The
+ // claimed size must act only as a bound on production -- never an allocation -- and the shortfall must be
+ // rejected cleanly once the parser reads past what the real input provides.
+ TEST(SerializationTest, BitPackOverstatedSizeThrows)
+ {
+ using namespace placeholders;
+
+ // Three blocks (1024, 1024, 960 bytes); a decoder believing the overstated size parses the short final
+ // block as a full one and runs out of input partway through it.
+ large_struct st;
+ st.data.resize(3000 - sizeof(uint64_t));
+ for (size_t i = 0; i < st.data.size(); i++)
+ {
+ st.data[i] = static_cast((i * 2654435761ULL) >> 16);
+ }
+
+ stringstream ss;
+ Serialization::Save(
+ bind(&large_struct::save_members, &st, _1), st.save_size(compr_mode_type::bitpack), ss,
+ compr_mode_type::bitpack, false);
+
+ // The original size is the 8 bytes following the SEALHeader; overstate it to 2^63.
+ string bytes = ss.str();
+ uint64_t huge_size = uint64_t(1) << 63;
+ memcpy(&bytes[16], &huge_size, sizeof(uint64_t));
+
+ stringstream tampered(bytes);
+ large_struct st2;
+ ASSERT_ANY_THROW(Serialization::Load(bind(&large_struct::load_members, &st2, _1), tampered, false));
+ }
} // namespace sealtest