# Operations

Last updated

Last updated

The structure and operations related to short integers are described in this section.

How a shortint is represented

In `shortint`

, the encrypted data is stored in an LWE ciphertext.

Conceptually, the message stored in an LWE ciphertext is divided into a **carry buffer** and a **message buffer**.

The message buffer is the space where the actual message is stored. This represents the modulus of the input messages (denoted by `MessageModulus`

in the code). When doing computations on a ciphertext, the encrypted message can overflow the message modulus. The part of the message which exceeds the message modulus is stored in the carry buffer. The size of the carry buffer is defined by another modulus, called `CarryModulus`

.

Together, the message modulus and the carry modulus form the plaintext space that is available in a ciphertext. This space cannot be overflowed, otherwise the computation may result in an incorrect output.

In order to ensure the correctness of the computation, we track the maximum value encrypted in a ciphertext via an associated attribute called the **degree**. When the degree reaches a defined threshold, the carry buffer may be emptied to safely resume the computations. In `shortint`

the carry modulus is considered useful as a means to do more computations.

Types of operations

The operations available via a `ServerKey`

may come in different variants:

operations that take their inputs as encrypted values

scalar operations that take at least one non-encrypted value as input

For example, the addition has two variants:

`ServerKey::unchecked_add`

, which takes two encrypted values and adds them.`ServerKey::unchecked_scalar_add`

, which takes an encrypted value and a clear value (a so-called scalar) and adds them.

Each operation may come in different 'flavors':

`unchecked`

: always does the operation, without checking if the result may exceed the capacity of the plaintext space. Using this operation might have an impact on the correctness of the following operations;`checked`

: checks are done before computing the operation, returning an error if operation cannot be done safely;`smart`

: always does the operation. If the operation cannot be computed safely, the smart operation will clear the carry to make the operation possible. Some of those will require a mutable reference as input: this is to allow the modification of the carry, but this will not change the underlying encrypted value;`default`

: always does the operation and always clears the carry. Could be**slower**than smart, but it ensures that the timings are consistent from one call to another.

Not all operations have these 4 flavors, as some of them are implemented in a way that the operation is always possible without ever exceeding the plaintext space capacity.

If you don't know which flavor to use, you should use the `default`

one.

How to use operation types

Let's try to do a circuit evaluation using the different flavors of operations that we have already introduced. For a very small circuit, the `unchecked`

flavour may be enough to do the computation correctly. Otherwise,`checked`

and `smart`

are the best options.

Let's do a scalar multiplication, a subtraction, and a multiplication.

During this computation, the carry buffer has been overflowed and, as all the operations were `unchecked`

, the output may be incorrect.

If we redo this same circuit with the `checked`

flavor, a panic will occur:

The `checked`

flavor permits manual management of the overflow of the carry buffer by raising an error if correctness is not guaranteed.

Using the `smart`

flavor will output the correct result all the time. However, the computation may be slower as the carry buffer may be cleaned during the computations.

The main advantage of the default flavor is to ensure predictable timings as long as this is the only kind of operation which is used.

Using `default`

could **slow-down** computations.

#List of available operations

Certain operations can only be used if the parameter set chosen is compatible with the bivariate programmable bootstrapping, meaning the carry buffer is larger than or equal to the message buffer. These operations are marked with a star (*).

The list of implemented operations for shortint is:

addition between two ciphertexts

addition between a ciphertext and an unencrypted scalar

comparisons

`<`

,`<=`

,`>`

,`>=`

,`==`

,`!=`

between a ciphertext and an unencrypted scalardivision of a ciphertext by an unencrypted scalar

LSB multiplication between two ciphertexts returning the result truncated to fit in the

`message buffer`

multiplication of a ciphertext by an unencrypted scalar

bitwise shift

`<<`

,`>>`

subtraction of a ciphertext by another ciphertext

subtraction of a ciphertext by an unencrypted scalar

negation of a ciphertext

bitwise and, or and xor (*)

comparisons

`<`

,`<=`

,`>`

,`>=`

,`==`

,`!=`

between two ciphertexts (*)division between two ciphertexts (*)

MSB multiplication between two ciphertexts returning the part overflowing the

`message buffer`

(*)

Public key encryption.

TFHE-rs supports both private and public key encryption methods. The only difference between both lies in the encryption step: in this case, the encryption method is called using `public_key`

instead of `client_key`

.

Here is a small example on how to use public encryption:

Arithmetic operations.

Classical arithmetic operations are supported by shortint:

bitwise operations

Short homomorphic integer types support some bitwise operations.

A simple example on how to use these operations:

comparisons

Short homomorphic integer types support comparison operations.

A simple example on how to use these operations:

univariate function evaluations

A simple example on how to use this operation to homomorphically compute the hamming weight (i.e., the number of bits equal to one) of an encrypted number.

bi-variate function evaluations

Using the shortint types offers the possibility to evaluate bi-variate functions, or functions that take two ciphertexts as input. This requires choosing a parameter set such that the carry buffer size is at least as large as the message (i.e., PARAM_MESSAGE_X_CARRY_Y with X <= Y).

Here is a simple code example: