Compressing ciphertexts/keys

This document explains the mechanism and steps to compress ciphertext and keys to reduce the storage needed as well as transmission times.

Most TFHE-rs entities contain random numbers generated by a Pseudo Random Number Generator (PRNG). Since the implemented PRNG is deterministic, storing only the random seed used to generate those numbers preserves all necessary information. When decompressing the entity, using the same PRNG and the same seed will reconstruct the full chain of random values.

In TFHE-rs, compressible entities are prefixed with Compressed. For instance, a compressed FheUint256 is declared as CompressedFheUint256.

In the following example code, we use the bincode crate dependency to serialize in a binary format and compare serialized sizes.

Compressing Ciphertexts

Compressing ciphertexts at encryption time

This example shows how to compress a ciphertext encrypting messages over 16 bits:

use tfhe::prelude::*;
use tfhe::{ConfigBuilder, generate_keys, set_server_key, CompressedFheUint16};

fn main() {
    let config = ConfigBuilder::default().build();
    let (client_key, _) = generate_keys(config);

    let clear = 12_837u16;
    let compressed = CompressedFheUint16::try_encrypt(clear, &client_key).unwrap();
    println!(
        "compressed size  : {}",
        bincode::serialize(&compressed).unwrap().len()
    );
    
    let decompressed = compressed.decompress();
    
    println!(
        "decompressed size: {}",
        bincode::serialize(&decompressed).unwrap().len()
    );

    let clear_decompressed: u16 = decompressed.decrypt(&client_key);
    assert_eq!(clear_decompressed, clear);
}

Compression ciphertexts after some homomorphic computation

You can compress ciphertexts at any time, even after performing multiple homomorphic operations.

To do so, you need to build a list containing all the ciphertexts that have to be compressed. This list might contain ciphertexts of different types, e.g., FheBool, FheUint32, FheInt64,... There is no constraint regarding the size of the list.

There are two possible approaches:

  • Single list: Compressing several ciphertexts into a single list. This generally yields a better compression ratio between output and input sizes;

  • Multiple lists: Using multiple lists. This offers more flexibility, since compression might happen at different times in the code, but could lead to larger outputs.

In more details, the optimal ratio is achieved with a list whose size is equal to the lwe_per_glwe field from the CompressionParameters.

The following example shows how to compress and decompress a list containing 4 messages: one 32-bits integer, one 64-bit integer, one boolean, and one 2-bit integer.

use tfhe::prelude::*;
use tfhe::shortint::parameters::{COMP_PARAM_MESSAGE_2_CARRY_2, PARAM_MESSAGE_2_CARRY_2};
use tfhe::{
    set_server_key, CompressedCiphertextList, CompressedCiphertextListBuilder, FheBool,
    FheInt64, FheUint16, FheUint2, FheUint32,
};

fn main() {
    let config = tfhe::ConfigBuilder::with_custom_parameters(PARAM_MESSAGE_2_CARRY_2, None)
        .enable_compression(COMP_PARAM_MESSAGE_2_CARRY_2)
        .build();

    let ck = tfhe::ClientKey::generate(config);
    let sk = tfhe::ServerKey::new(&ck);

    set_server_key(sk);

    let ct1 = FheUint32::encrypt(17_u32, &ck);

    let ct2 = FheInt64::encrypt(-1i64, &ck);

    let ct3 = FheBool::encrypt(false, &ck);

    let ct4 = FheUint2::encrypt(3u8, &ck);

    let compressed_list = CompressedCiphertextListBuilder::new()
        .push(ct1)
        .push(ct2)
        .push(ct3)
        .push(ct4)
        .build()
        .unwrap();

    let serialized = bincode::serialize(&compressed_list).unwrap();

    println!("Serialized size: {} bytes", serialized.len());

    let compressed_list: CompressedCiphertextList = bincode::deserialize(&serialized).unwrap();

    
    let a: FheUint32 = compressed_list.get(0).unwrap().unwrap();
    let b: FheInt64 = compressed_list.get(1).unwrap().unwrap();
    let c: FheBool = compressed_list.get(2).unwrap().unwrap();
    let d: FheUint2 = compressed_list.get(3).unwrap().unwrap();

    let a: u32 = a.decrypt(&ck);
    assert_eq!(a, 17);
    let b: i64 = b.decrypt(&ck);
    assert_eq!(b, -1);
    let c = c.decrypt(&ck);
    assert!(!c);
    let d: u8 = d.decrypt(&ck);
    assert_eq!(d, 3);

    // Out of bound index 
    assert!(compressed_list.get::<FheBool>(4).unwrap().is_none());

    // Incorrect type
    assert!(compressed_list.get::<FheInt64>(0).is_err());

    // Correct type but wrong number of bits
    assert!(compressed_list.get::<FheUint16>(0).is_err());
}

Compressing keys

Compressing server keys

This example shows how to compress the server keys:

use tfhe::prelude::*;
use tfhe::{
    generate_keys, set_server_key, ClientKey, CompressedServerKey, ConfigBuilder, FheUint8,
};

fn main() {
    let config = ConfigBuilder::default().build();

    let cks = ClientKey::generate(config);
    let compressed_sks = CompressedServerKey::new(&cks);

    println!(
        "compressed size  : {}",
        bincode::serialize(&compressed_sks).unwrap().len()
    );

    let sks = compressed_sks.decompress();

    println!(
        "decompressed size: {}",
        bincode::serialize(&sks).unwrap().len()
    );

    set_server_key(sks);

    let clear_a = 12u8;
    let a = FheUint8::try_encrypt(clear_a, &cks).unwrap();

    let c = a + 234u8;
    let decrypted: u8 = c.decrypt(&cks);
    assert_eq!(decrypted, clear_a.wrapping_add(234));
}

Compressed public keys

This example shows how to compress the classical public keys:

It is not currently recommended to use the CompressedPublicKey to encrypt ciphertexts without first decompressing them. If the resulting PublicKey is too large to fit in memory, it may result in significant slowdowns.

This issue has been identified and will be addressed in future releases.

use tfhe::prelude::*;
use tfhe::{ConfigBuilder, generate_keys, set_server_key, FheUint8, CompressedPublicKey};

fn main() {
    let config = ConfigBuilder::default().build();
    let (client_key, _) = generate_keys(config);

    let compressed_public_key = CompressedPublicKey::new(&client_key);

    println!("compressed size  : {}", bincode::serialize(&compressed_public_key).unwrap().len());

    let public_key = compressed_public_key.decompress();

    println!("decompressed size: {}", bincode::serialize(&public_key).unwrap().len());


    let a = FheUint8::try_encrypt(213u8, &public_key).unwrap();
    let clear: u8 = a.decrypt(&client_key);
    assert_eq!(clear, 213u8);
}

Compressed compact public key

This example shows how to use compressed compact public keys:

use tfhe::prelude::*;
use tfhe::{
    generate_keys, set_server_key, CompactCiphertextList, CompressedCompactPublicKey,
    ConfigBuilder, FheUint8,
};

fn main() {
    let config = ConfigBuilder::default()
        .use_custom_parameters(
            tfhe::shortint::parameters::PARAM_MESSAGE_2_CARRY_2_COMPACT_PK_KS_PBS,
            None,
        )
        .build();
    let (client_key, _) = generate_keys(config);

    let public_key_compressed = CompressedCompactPublicKey::new(&client_key);

    println!(
        "compressed size  : {}",
        bincode::serialize(&public_key_compressed).unwrap().len()
    );

    let public_key = public_key_compressed.decompress();

    println!(
        "decompressed size: {}",
        bincode::serialize(&public_key).unwrap().len()
    );

    let compact_list = CompactCiphertextList::builder(&public_key)
        .push(255u8)
        .build();
    let expanded = compact_list.expand().unwrap();
    let a: FheUint8 = expanded.get(0).unwrap().unwrap();

    let clear: u8 = a.decrypt(&client_key);
    assert_eq!(clear, 255u8);
}

Last updated