Segmentation Fault in Concrete's Interoperability with TFHE-rs

Hi,

I am trying to build on the interoperability example between Concrete and TFHE-rs given at Shared key | Concrete. Instead of adding two encrypted values x and y, I want to compute a * x * y , where x and y are “encrypted” and a is “clear”. I have the following code for this and I get a segmentation error which I do not understand. Can someone help me understand what am I doing wrong?

The values I am working with are x=2, y=3, a=4. I have tried to do some debugging and my analysis is as follows: When I try to to use the compiled circuit to encrypt, run, decrypt, I get the expected value 24. On the other hand, when I try to decrypt the ciphertexts read from file (generated using TFHE-rs) corresponding to x and y, I do get correct decryptions. So it seems if there is any issue, it could be one of the following:

  1. The encoding of the clear value a is malformed.
  2. The TFHE-rs encryptions of x and y: while they may be well-formed, and could be decrypted, their LWE noise is high and results in some overflow at time of evaluation.

Python code below:

import os
from functools import partial

import click
import numpy as np
import random

from concrete import fhe
from concrete.fhe import tfhers
from concrete.fhe.values import ClearScalar

########## Params #####################
LWE_DIM = 909
GLWE_DIM = 1
POLY_SIZE = 4096
PBS_BASE_LOG = 15
PBS_LEVEL = 2
MSG_WIDTH = 2
CARRY_WIDTH = 3
ENCRYPTION_KEY_CHOICE = tfhers.EncryptionKeyChoice.BIG
# LWE_NOISE_DISTR = 0
LWE_NOISE_DISTR = 1.0994794733558207e-6
GLWE_NOISE_DISTR = 2.168404344971009e-19
#######################################

assert GLWE_DIM == 1, "glwe dim must be 1"

### Options ###########################
FHEUINT_PRECISION = 8
#######################################


tfhers_params = tfhers.CryptoParams(
    lwe_dimension=LWE_DIM,
    glwe_dimension=GLWE_DIM,
    polynomial_size=POLY_SIZE,
    pbs_base_log=PBS_BASE_LOG,
    pbs_level=PBS_LEVEL,
    lwe_noise_distribution=LWE_NOISE_DISTR,
    glwe_noise_distribution=GLWE_NOISE_DISTR,
    encryption_key_choice=ENCRYPTION_KEY_CHOICE,
)
tfhers_type = tfhers.TFHERSIntegerType(
    is_signed=False,
    bit_width=FHEUINT_PRECISION,
    carry_width=CARRY_WIDTH,
    msg_width=MSG_WIDTH,
    params=tfhers_params,
)

# this partial will help us create TFHERSInteger with the given type instead of calling
# tfhers.TFHERSInteger(tfhers_type, value) every time
tfhers_int = partial(tfhers.TFHERSInteger, tfhers_type)


def compute(tfhers_x, tfhers_y, aa_0):
    ####### TFHE-rs to Concrete #########

    # x and y are supposed to be TFHE-rs values.
    # to_native will use type information from x and y to do
    # a correct conversion from TFHE-rs to Concrete
    concrete_x = tfhers.to_native(tfhers_x)
    concrete_y = tfhers.to_native(tfhers_y)
    a_0 = tfhers.to_native(aa_0)
    ####### TFHE-rs to Concrete #########

    ####### Concrete Computation ########
    concrete_res = (a_0 *concrete_x * concrete_y) % 213
    ####### Concrete Computation ########

    ####### Concrete to TFHE-rs #########
    tfhers_res = tfhers.from_native(
        concrete_res, tfhers_type
    )  # we have to specify the type we want to convert to
    ####### Concrete to TFHE-rs #########
    return tfhers_res


def ccompilee():
    compiler = fhe.Compiler(compute, {"tfhers_x": "encrypted", "tfhers_y": "encrypted", "aa_0" : "clear"})

    val = 10
    inputset = [(tfhers_int(random.randint(1, val)), tfhers_int(random.randint(1, val)), tfhers_int(random.randint(1, val))) for _ in range(10)]
    circuit = compiler.compile(inputset)

    print("Testing compiler...")
    encrypted_x, encrypted_y, encrypted_a0 = circuit.encrypt(tfhers_type.encode(2), tfhers_type.encode(3), tfhers_type.encode(4))
    print("encrypted_x: {}".format(encrypted_x))
    print("encrypted_y: {}".format(encrypted_y))
    print("encrypted_a0: {}".format(encrypted_a0))
    # run
    encrypted_result = circuit.run(encrypted_x, encrypted_y, encrypted_a0)
    # decrypt
    result = circuit.decrypt(encrypted_result)
    # decode
    decoded = tfhers_type.decode(result)
    print("Testing compiler... DONE, decoded value: {}".format(decoded))

    tfhers_bridge = tfhers.new_bridge(circuit=circuit)
    return circuit, tfhers_bridge


@click.group()
def cli():
    pass


@cli.command()
@click.option("-s", "--secret-key", type=str, required=False)
@click.option("-o", "--output-secret-key", type=str, required=True)
@click.option("-k", "--concrete-keyset-path", type=str, required=True)
def keygen(output_secret_key: str, secret_key: str, concrete_keyset_path: str):
    """Concrete Key Generation"""

    circuit, tfhers_bridge = ccompilee()

    if os.path.exists(concrete_keyset_path):
        print(f"removing old keyset at '{concrete_keyset_path}'")
        os.remove(concrete_keyset_path)

    if secret_key:
        print(f"partial keygen from sk at '{secret_key}'")
        # load the initial secret key to use for keygen
        with open(
            secret_key,
            "rb",
        ) as f:
            buff = f.read()
        input_idx_to_key = {0: buff, 1: buff}
        tfhers_bridge.keygen_with_initial_keys(input_idx_to_key_buffer=input_idx_to_key)
    else:
        print("full keygen")
        circuit.keygen()

    print(f"saving Concrete keyset")
    circuit.client.keys.save(concrete_keyset_path)
    print(f"saved Concrete keyset to '{concrete_keyset_path}'")

    sk: bytes = tfhers_bridge.serialize_input_secret_key(input_idx=0)
    print(f"writing secret key of size {len(sk)} to '{output_secret_key}'")
    with open(output_secret_key, "wb") as f:
        f.write(sk)


@cli.command()
@click.option("-c1", "--rust-ct-1", type=str, required=True)
@click.option("-c2", "--rust-ct-2", type=str, required=True)
@click.option("-o", "--output-rust-ct", type=str, required=False)
@click.option("-k", "--concrete-keyset-path", type=str, required=True)
@click.option("-a0", "--a-0", type=int, required=True)
def run(rust_ct_1: str, rust_ct_2: str, output_rust_ct: str, concrete_keyset_path: str, a_0: int):
    """Run circuit"""
    circuit, tfhers_bridge = ccompilee()

    if not os.path.exists(concrete_keyset_path):
        raise RuntimeError("cannot find keys, you should run keygen before")
    print(f"loading keys from '{concrete_keyset_path}'")
    circuit.client.keys.load(concrete_keyset_path)

    # read tfhers int from file
    with open(rust_ct_1, "rb") as f:
        buff = f.read()
    # import fheuint8 and get its description
    tfhers_uint8_x = tfhers_bridge.import_value(buff, input_idx=0)

    # read tfhers int from file
    with open(rust_ct_2, "rb") as f:
        buff = f.read()
    # import fheuint8 and get its description
    tfhers_uint8_y = tfhers_bridge.import_value(buff, input_idx=1)

    encrypted_x, encrypted_y = tfhers_uint8_x, tfhers_uint8_y

    print("Testing well-formedness of ciphertexts...")
    plain_x = tfhers_type.decode(circuit.decrypt(encrypted_x))
    plain_y = tfhers_type.decode(circuit.decrypt(encrypted_y))
    print("Testing well-formedness of ciphertexts... DONE, x: {}, y: {}".format(plain_x, plain_y))

    print(f"Homomorphic evaluation...")
    _, _, encrypted_a0 = circuit.encrypt(None, None, tfhers_type.encode(a_0))
    print("encrypted_a0: {}".format(encrypted_a0))
    print("Running FHE circuit...")
    encrypted_result = circuit.run(encrypted_x, encrypted_y, encrypted_a0)
    print("Running FHE circuit... DONE")
    if output_rust_ct:
        print("exporting Rust ciphertexts")
        # export fheuint8
        buff = tfhers_bridge.export_value(encrypted_result, output_idx=0)
        # write it to file
        with open(output_rust_ct, "wb") as f:
            f.write(buff)
    else:
        print("Decrypting the result...")
        result = circuit.decrypt(encrypted_result)
        decoded = tfhers_type.decode(result)
        print(f"Concrete decryption result: raw({result}), decoded({decoded})")


if __name__ == "__main__":
    cli()

Segmentation fault below.

python example.py run -k $TDIR/concrete_keyset -c1 $TDIR/tfhers_ct_1 -c2 $TDIR/tfhers_ct_2 -o $TDIR/tfhers_ct_out -a0 4
tfhers_int(120): {} TFHEInteger(dtype=tfhers<uint8, 3, 2, params=crypto_params<lwe_dim=909, glwe_dim=1, poly_size=4096, pbs_base_log=15, pbs_level=2, lwe_noise_distribution=1.0994794733558207e-06, glwe_noise_distribution=2.168404344971009e-19, encryption_key_choice=BIG>>, shape=(), value=120)
loading keys from '/tmp/interop/concrete_keyset'
Testing well-formedness of ciphertexts...
Testing well-formedness of ciphertexts... DONE, x: 2, y: 3
Homomorphic evaluation...
encrypted_a0: <concrete.fhe.compilation.value.Value object at 0x12e932020>
Running FHE circuit...
PLEASE submit a bug report to https://github.com/llvm/llvm-project/issues/ and include the crash backtrace.
Stack dump without symbol names (ensure you have llvm-symbolizer in your PATH or set the environment var `LLVM_SYMBOLIZER_PATH` to point to it):
0  _mlir.cpython-310-darwin.so         0x00000001172054a4 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) + 56
1  _mlir.cpython-310-darwin.so         0x00000001172043fc llvm::sys::RunSignalHandlers() + 72
2  _mlir.cpython-310-darwin.so         0x0000000117205b44 llvm::sys::PrintStackTraceOnErrorSignal(llvm::StringRef, bool) + 784
3  libsystem_platform.dylib            0x0000000186c9aa84 _sigtramp + 56
4  libConcretelangRuntime.dylib        0x000000011a0cfde8 tfhe::core_crypto::algorithms::lwe_keyswitch::keyswitch_lwe_ciphertext::hc400126bd5cbe271 + 1992
5  libConcretelangRuntime.dylib        0x000000011a0cfde8 tfhe::core_crypto::algorithms::lwe_keyswitch::keyswitch_lwe_ciphertext::hc400126bd5cbe271 + 1992
6  libConcretelangRuntime.dylib        0x000000011a0aed38 concrete_cpu_keyswitch_lwe_ciphertext_u64 + 108
7  sharedlib.dylib                     0x00000001197ae0c4 concrete_compute + 3904
8  _concretelang.cpython-310-darwin.so 0x000000011829cad0 concretelang::serverlib::ServerCircuit::call(concretelang::keysets::ServerKeyset const&, std::__1::vector<concretelang::protocol::Message<concreteprotocol::Value>, std::__1::allocator<concretelang::protocol::Message<concreteprotocol::Value>>>&) + 992
9  _concretelang.cpython-310-darwin.so 0x0000000117e4b048 PyInit__concretelang + 540808
10 _concretelang.cpython-310-darwin.so 0x0000000117dd1434 PyInit__concretelang + 42100
11 Python                              0x000000010247ebe8 cfunction_call + 60
12 Python                              0x0000000102436d20 _PyObject_MakeTpCall + 136
13 Python                              0x000000010243a140 method_vectorcall + 604
14 Python                              0x00000001025113fc call_function + 128
15 Python                              0x000000010250bb6c _PyEval_EvalFrameDefault + 23108
16 Python                              0x0000000102505264 _PyEval_Vector + 396
17 Python                              0x00000001025113fc call_function + 128
18 Python                              0x000000010250a710 _PyEval_EvalFrameDefault + 17896
19 Python                              0x0000000102505264 _PyEval_Vector + 396
20 Python                              0x0000000102439f60 method_vectorcall + 124
21 Python                              0x00000001024377e8 PyVectorcall_Call + 176
22 Python                              0x000000010250694c _PyEval_EvalFrameDefault + 2084
23 Python                              0x0000000102505264 _PyEval_Vector + 396
24 Python                              0x000000010243a008 method_vectorcall + 292
25 Python                              0x000000010250694c _PyEval_EvalFrameDefault + 2084
26 Python                              0x0000000102505264 _PyEval_Vector + 396
27 Python                              0x00000001025113fc call_function + 128
28 Python                              0x000000010250a710 _PyEval_EvalFrameDefault + 17896
29 Python                              0x0000000102505264 _PyEval_Vector + 396
30 Python                              0x00000001024377e8 PyVectorcall_Call + 176
31 Python                              0x000000010250694c _PyEval_EvalFrameDefault + 2084
32 Python                              0x0000000102505264 _PyEval_Vector + 396
33 Python                              0x0000000102439f60 method_vectorcall + 124
34 Python                              0x00000001024377e8 PyVectorcall_Call + 176
35 Python                              0x000000010250694c _PyEval_EvalFrameDefault + 2084
36 Python                              0x0000000102505264 _PyEval_Vector + 396
37 Python                              0x00000001025113fc call_function + 128
38 Python                              0x000000010250a710 _PyEval_EvalFrameDefault + 17896
39 Python                              0x0000000102505264 _PyEval_Vector + 396
40 Python                              0x00000001025113fc call_function + 128
41 Python                              0x000000010250a710 _PyEval_EvalFrameDefault + 17896
42 Python                              0x0000000102505264 _PyEval_Vector + 396
43 Python                              0x000000010243a06c method_vectorcall + 392
44 Python                              0x000000010250694c _PyEval_EvalFrameDefault + 2084
45 Python                              0x0000000102505264 _PyEval_Vector + 396
46 Python                              0x0000000102437078 _PyObject_FastCallDictTstate + 96
47 Python                              0x000000010249cc90 slot_tp_call + 200
48 Python                              0x0000000102436d20 _PyObject_MakeTpCall + 136
49 Python                              0x000000010251148c call_function + 272
50 Python                              0x000000010250a2e0 _PyEval_EvalFrameDefault + 16824
51 Python                              0x0000000102505264 _PyEval_Vector + 396
52 Python                              0x00000001025050c4 PyEval_EvalCode + 104
53 Python                              0x0000000102553178 run_eval_code_obj + 84
54 Python                              0x00000001025530dc run_mod + 112
55 Python                              0x0000000102552f00 pyrun_file + 148
56 Python                              0x0000000102552950 _PyRun_SimpleFileObject + 268
57 Python                              0x000000010255230c _PyRun_AnyFileObject + 216
58 Python                              0x000000010256d384 pymain_run_file_obj + 220
59 Python                              0x000000010256ccc8 pymain_run_file + 72
60 Python                              0x000000010256c69c Py_RunMain + 996
61 Python                              0x000000010256d6c8 Py_BytesMain + 40
62 dyld                                0x0000000186913f28 start + 2236
[1]    81217 segmentation fault  python example.py run -k $TDIR/concrete_keyset -c1 $TDIR/tfhers_ct_1 -c2  -o 

Hello @nikhilvanjani61,

From which version of TFHE-rs are you generated keyset and ciphertexts and what concrete-python version are you using, I think your issue can come from incompatibility between version. Before concrete-python v2.9 we using TFHE-rs version which had retro-compatibility break and unsafe serialization framework for keys because it was not available.
With concrete v2.9 we are using TFHE-rs 0.10 as backend and the safe serialization, the retro compatibility should be ensured between version of TFHE-rs > 0.10.0 even if it must be the case when TFHE-rs will be above 1.0 (should be done shortly).

So please try with concrete 2.9.0 and TFHE-rs (>=) 0.10, and take care to well clean all your previously generated keys/ciphertexts, if you have still troubles let me known and if possible give me the rust part too.

Cheers!