Skip to main content
A full adder computes the sum of three input bits (a, b, and a carry-in) and produces two output bits (sum and carry-out). It’s the basic building block for arbitrary-width integer arithmetic. This example shows how to compose TFHE gates into a real circuit, all on encrypted bits.

The math

For input bits a, b, cin:
sum  = a XOR b XOR cin
cout = (a AND b) OR (cin AND (a XOR b))
Truth table:
abcinsumcout
00000
00110
01010
01101
10010
10101
11001
11111

Implementation

import wavis_fhe as wv

keys = wv.keygen("fast_128")


def full_adder(ct_a, ct_b, ct_cin):
    """Compute (sum, cout) from three encrypted bits."""

    # Intermediate XOR (used twice — compute once, reuse)
    ct_a_xor_b = keys.xor(ct_a, ct_b)

    # Sum: three-input XOR
    ct_sum = keys.xor(ct_a_xor_b, ct_cin)

    # Carry-out: (a AND b) OR (cin AND (a XOR b))
    ct_ab = keys.and_(ct_a, ct_b)
    ct_cin_and_xor = keys.and_(ct_cin, ct_a_xor_b)
    ct_cout = keys.or_(ct_ab, ct_cin_and_xor)

    return ct_sum, ct_cout


# Test all 8 input combinations
print(f"{'a':>3} {'b':>3} {'cin':>4} | {'sum':>4} {'cout':>5}")
print("-" * 28)

for a in [False, True]:
    for b in [False, True]:
        for cin in [False, True]:
            ct_a = keys.encrypt(a)
            ct_b = keys.encrypt(b)
            ct_cin = keys.encrypt(cin)

            ct_sum, ct_cout = full_adder(ct_a, ct_b, ct_cin)

            sum_bit = keys.decrypt(ct_sum)
            cout_bit = keys.decrypt(ct_cout)

            # Verify against integer arithmetic
            expected = int(a) + int(b) + int(cin)
            assert sum_bit == bool(expected & 1)
            assert cout_bit == bool(expected & 2)

            print(f"  {int(a)}   {int(b)}   {int(cin)}  |   {int(sum_bit)}    {int(cout_bit)}")

print("\nAll 8 combinations verified ✓")
Output:
  a   b  cin |  sum  cout
----------------------------
  0   0   0  |   0    0
  0   0   1  |   1    0
  0   1   0  |   1    0
  0   1   1  |   0    1
  1   0   0  |   1    0
  1   0   1  |   0    1
  1   1   0  |   0    1
  1   1   1  |   1    1

All 8 combinations verified ✓

Performance

The adder uses 5 gate evaluations:
GateCountLatency (fast_128 CPU)
XOR256 ms × 2 = 112 ms
AND214 ms × 2 = 28 ms
OR114 ms
Total5~154 ms
Each input requires 8 such adders to perform 8-bit integer addition (~1.2 s). For practical multi-bit arithmetic, prefer CKKS — see Performance.

Building an N-bit ripple-carry adder

def ripple_adder(bits_a, bits_b):
    """Add two N-bit numbers represented as lists of encrypted bits (LSB first)."""
    assert len(bits_a) == len(bits_b)

    cin = keys.encrypt(False)
    sum_bits = []

    for a, b in zip(bits_a, bits_b):
        s, cout = full_adder(a, b, cin)
        sum_bits.append(s)
        cin = cout

    sum_bits.append(cin)   # final carry — produces N+1-bit result
    return sum_bits


def encrypt_int(value: int, n_bits: int):
    """Encrypt an integer as a list of bits (LSB first)."""
    return [keys.encrypt(bool((value >> i) & 1)) for i in range(n_bits)]


def decrypt_int(bits):
    """Recover an integer from encrypted bits (LSB first)."""
    return sum(int(keys.decrypt(b)) << i for i, b in enumerate(bits))


# 4-bit addition: 5 + 3 = 8
a = encrypt_int(5, 4)
b = encrypt_int(3, 4)
c = ripple_adder(a, b)
print(decrypt_int(c))   # 8
This 4-bit adder takes ~620 ms on CPU. Wider adders scale linearly.

Optimizing with batching

For a serial pipeline like a ripple-carry adder, the gates happen sequentially (each carry depends on the previous one). Batching doesn’t help here. But for independent additions (e.g., adding 100 pairs of numbers in parallel), use keys.batch_* to amortize bootstrapping-key DRAM reads on a local GPU:
keys_gpu = wv.keygen_gpu("fast_128")

# 100 independent NANDs, all on GPU
pairs = [(keys_gpu.encrypt(a), keys_gpu.encrypt(b)) for a, b in many_pairs]
results = keys_gpu.batch_nand(pairs)   # ~5.2 ms/gate amortized
See GPU Batch Example.

Why this matters

A 1-bit full adder doesn’t sound like much, but it’s the cryptographic proof point: arbitrary boolean circuits can be evaluated on encrypted data. Once you have a full adder, you have addition, subtraction (two’s complement), multiplication (shifts + adds), comparison, and via Turing completeness, anything else. In practice, large-circuit FHE is slow — but for targeted privacy-preserving operations (a private credit-score formula, a small ML inference, a multi-party comparison), it’s exactly the right primitive.

Next Steps

Server Mode

Move the gates to api.wavis.xyz

GPU Batch

Local GPU acceleration walkthrough