---
title: "BinFHE: Boolean Circuits and Arbitrary Functions on Encrypted Bits"
author: "Balasubramanian Narasimhan"
date: "`r Sys.Date()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{BinFHE: Boolean Circuits and Arbitrary Functions on Encrypted Bits}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
library(openfhe.R)
knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>"
)
```

The other vignettes in this package focus on **CKKS** —
approximate real-number arithmetic with batched slots,
suitable for distributed statistics, regression, and the
bulk of the headline "R optimizers run unchanged over an
encrypted channel" workflows. CKKS is where most of the
audience-A use cases live.

But CKKS has blind spots. It is not well suited to:

- **Comparisons and branching** — "is `x > 0`?" doesn't
  translate cleanly into a polynomial approximation unless
  you're willing to burn levels on a Chebyshev sign
  approximation.
- **Arbitrary non-polynomial functions** on bounded integer
  domains — a piecewise step function, a bit-extraction, a
  custom lookup.
- **Exact per-bit logic** — parity, comparators, small
  arbitrary-function evaluation.

OpenFHE's **BinFHE** subsystem is the tool for those jobs.
BinFHE implements the FHEW/TFHE family: single-bit
ciphertexts with *every gate evaluation* performing an
in-place bootstrap. That means there's no depth budget to
manage — a BinFHE circuit can be arbitrarily deep — but
each gate is comparatively expensive because the bootstrap
is on the critical path. It's the right shape for short
circuits with logical or arbitrary-function operations,
not for long chains of multiplications.

This vignette walks through the BinFHE operations end-to-end.
Every section runs real code and checks real output. The
examples use the **TOY** paramset for speed — in production
you'd use **STD128** (or STD128_3 / STD128_4 for multi-input
gates).

## Context and key setup

A BinFHE context is built with a parameter set and a
bootstrap method. `BinFHEParamSet` carries 44 values
covering the full C++ header range — **TOY** for tests,
**STD128** / **STD128Q** for 128-bit classical / quantum
security, **STD192** / **STD256** for higher security, and
the **LMKCDEY** family for the newer bootstrap algorithm.
`BinFHEMethod` chooses between **GINX** (default),
**AP** (Ducas-Micciancio), and **LMKCDEY** (the fastest
variant in the current OpenFHE release).

```{r setup-toy}
library(openfhe.R)

## TOY paramset — fast but zero security. For demos only.
ctx <- bin_fhe_context(BinFHEParamSet$TOY, BinFHEMethod$GINX)

## The secret key generates bootstrap keys that every gate
## will use internally.
sk <- bin_key_gen(ctx)
bin_bt_key_gen(ctx, sk)
```

`bin_bt_key_gen()` accepts an optional `keygen_mode`
argument (`KeygenMode$SYM_ENCRYPT` default, or
`KeygenMode$PUB_ENCRYPT`) that controls how the bootstrap
keys are generated. Most users leave it at the default.

## Two-input boolean gates

The standard 2-input gates are `AND`, `OR`, `NAND`, `NOR`,
`XOR`, `XNOR`, and the fast variants `XOR_FAST` /
`XNOR_FAST`. All of them go through the same
`eval_bin_gate(ctx, gate, ct1, ct2)` entry point.

```{r binary-gates}
ct0 <- bin_encrypt(ctx, sk, 0L)
ct1 <- bin_encrypt(ctx, sk, 1L)

truth <- function(gate_name, gate, ct_a, ct_b) {
  r <- eval_bin_gate(ctx, gate, ct_a, ct_b)
  bin_decrypt(ctx, sk, r)
}

## Full truth table for AND / OR / XOR.
grid <- expand.grid(a = c(0L, 1L), b = c(0L, 1L))
grid$AND <- mapply(function(a, b) truth("AND", BinGate$AND,
                                         bin_encrypt(ctx, sk, a),
                                         bin_encrypt(ctx, sk, b)),
                    grid$a, grid$b)
grid$OR  <- mapply(function(a, b) truth("OR", BinGate$OR,
                                         bin_encrypt(ctx, sk, a),
                                         bin_encrypt(ctx, sk, b)),
                    grid$a, grid$b)
grid$XOR <- mapply(function(a, b) truth("XOR", BinGate$XOR,
                                         bin_encrypt(ctx, sk, a),
                                         bin_encrypt(ctx, sk, b)),
                    grid$a, grid$b)
grid
```

Every row of the truth table matches the standard boolean
definitions. Nothing exotic is happening here — the
interesting part is that the inputs are encrypted, the
gate evaluation does a bootstrap internally, and the
output is decryptable with the same secret key.

## Negation

The only 1-input gate is `eval_not(ctx, ct)`:

```{r not}
bin_decrypt(ctx, sk, eval_not(ctx, ct0))
bin_decrypt(ctx, sk, eval_not(ctx, ct1))
```

## Multi-input gates

OpenFHE exposes wider boolean gates (3-input and 4-input)
that are more efficient than composing 2-input gates. The
supported wide gates are **AND3**, **OR3**, **AND4**,
**OR4**, **MAJORITY** (3-input majority vote), and
**CMUX** (3-input conditional mux). All of them go through
the same `eval_bin_gate()` entry point, but instead of
passing two `Ciphertext` arguments you pass a **list** of
ciphertexts.

Multi-input gates have two setup constraints the 2-input
gates don't share:

1. **Parameter set**: you need a paramset designed for the
   input width. **STD128_3** for 3-input gates, **STD128_4**
   for 4-input. TOY with the default plaintext modulus
   produces garbled decryption because the TOY parameters
   are tuned for 2-input only.
2. **Plaintext modulus**: the encryption call must use
   `p = 2 * num_inputs`. For 3-input gates that's
   `p = 6`; for 4-input, `p = 8`. The `SMALL_DIM` output
   mode is also required.

Here's a 3-input **AND3** example using the right setup:

```{r multi-input-setup}
ctx3 <- bin_fhe_context(BinFHEParamSet$STD128_3)
sk3 <- bin_key_gen(ctx3)
bin_bt_key_gen(ctx3, sk3)

p3 <- 6L
encrypt_bit_3 <- function(b) {
  bin_encrypt(ctx3, sk3, as.integer(b),
              output = BinFHEOutput$SMALL_DIM,
              p = p3)
}
```

```{r and3}
## 1 AND 1 AND 0 = 0
cts_110 <- list(encrypt_bit_3(1L),
                encrypt_bit_3(1L),
                encrypt_bit_3(0L))
bin_decrypt(ctx3, sk3,
            eval_bin_gate(ctx3, BinGate$AND3, cts_110),
            p = p3)

## 1 AND 1 AND 1 = 1
cts_111 <- list(encrypt_bit_3(1L),
                encrypt_bit_3(1L),
                encrypt_bit_3(1L))
bin_decrypt(ctx3, sk3,
            eval_bin_gate(ctx3, BinGate$AND3, cts_111),
            p = p3)
```

Same entry point, different shape: the third argument
is a list instead of a single ciphertext, and the second
argument (`ct2`) is left unset (defaults to `NULL`). The
dispatcher detects the list shape and routes to the
vector-form C++ overload.

Majority on three bits:

```{r majority, eval = FALSE}
## Majority uses p = 4 (not 2 * num_inputs = 6) per the
## upstream boolean-multi-input example's encoding.
p_maj <- 4L
encrypt_bit_maj <- function(b) {
  bin_encrypt(ctx3, sk3, as.integer(b),
              output = BinFHEOutput$SMALL_DIM,
              p = p_maj)
}

cts <- list(encrypt_bit_maj(1L),
            encrypt_bit_maj(1L),
            encrypt_bit_maj(0L))
bin_decrypt(ctx3, sk3,
            eval_bin_gate(ctx3, BinGate$MAJORITY, cts),
            p = p_maj)
## 1 (majority of two 1s and one 0)
```

## Arbitrary function evaluation

For functions that don't fit into the fixed gate repertoire,
BinFHE supports **arbitrary function evaluation** via a
lookup table. The workflow is:

1. Define a function from integers in `[0, p)` to integers
   in `[0, p)` in plain R.
2. Build a `BinFHEContext` with `arb_func = TRUE`, which
   selects a paramset and ring dimension large enough for
   the functional bootstrap.
3. Generate a lookup table via `generate_lut_via_function()`.
4. Call `eval_func(ctx, ct, lut)` on an encrypted input —
   the output is a ciphertext of `f(input)`.

```{r arb-func}
## Build an arb-func context. arb_func = TRUE picks a wider
## paramset; log_q and n control the LARGE_DIM modulus and
## dimension of the functional-bootstrap path.
ctx_f <- bin_fhe_context(BinFHEParamSet$TOY,
                          arb_func = TRUE)
sk_f <- bin_key_gen(ctx_f)
bin_bt_key_gen(ctx_f, sk_f)

p <- get_max_plaintext_space(ctx_f)

## Example function: squared value mod p. The LUT maps
## input i in [0, p) to f(i) = i^2 mod p.
f_square <- function(x, plaintext_modulus) {
  (x * x) %% plaintext_modulus
}
lut <- generate_lut_via_function(f_square, p)

## Encrypt a value in the LARGE_DIM / functional-bootstrap
## path and evaluate.
ct_input <- bin_encrypt(ctx_f, sk_f, 3L,
                        output = BinFHEOutput$LARGE_DIM,
                        p = p)
ct_out <- eval_func(ctx_f, ct_input, lut)
bin_decrypt(ctx_f, sk_f, ct_out, p = p)
## 9 (if p > 9, otherwise 9 mod p)
```

The LUT evaluation runs one functional bootstrap per
`eval_func()` call — so it's not cheap — but it gives you
arbitrary univariate functions over the plaintext domain
without any polynomial approximation.

## The sign function

`eval_sign(ctx, ct)` evaluates the sign of an encrypted
value: it returns 1 if the input is in the "positive" half
of the encoding range and 0 if it's in the "negative" half.
The primary use case is threshold comparisons (convert
`x > threshold` into `eval_sign(ct - threshold_ct)`).

Unlike the other functional-bootstrap primitives in this
vignette, `eval_sign()` requires a **large-Q** context: the
`arb_func` flag must be `FALSE` and an explicit `log_q`
must be supplied. Only **TOY** and **STD128** are available
in this configuration — the remaining parameter sets raise
an error. The encryption path uses the `LARGE_DIM` output
mode with both an explicit plaintext modulus `p` and an
explicit ciphertext modulus `mod = Q`.

Every earlier section of this vignette runs on **TOY**,
which the OpenFHE headers describe as carrying no security
at all; it is there so that a tour of the gates builds
quickly. This section uses **STD128** instead, so that at
least one worked example runs at parameters you could
actually deploy. That choice is not free — generating the
bootstrap keys below is the dominant cost of building this
vignette, and it is a cost a real deployment pays once per
key. Take the pairing as the lesson: reach for **TOY**
while you are learning the API, and for **STD128** or
higher the moment anything real is encrypted.

This setup mirrors the upstream `eval-sign.py` example.

```{r sign-setup}
log_q <- 17L
ctx_s <- bin_fhe_context(
  paramset           = BinFHEParamSet$STD128,
  method             = BinFHEMethod$GINX,
  arb_func           = FALSE,
  log_q              = log_q,
  n                  = 0L,
  time_optimization  = FALSE
)
sk_s <- bin_key_gen(ctx_s)
bin_bt_key_gen(ctx_s, sk_s)

Q <- bitwShiftL(1L, log_q)                           # 131072
q <- 4096
factor <- bitwShiftL(1L, log_q - as.integer(log2(q))) # 32
p_s <- get_max_plaintext_space(ctx_s) * factor
```

Now encrypt eight values centered on `p_s / 2` and test
each one's sign:

```{r sign}
center <- p_s %/% 2
for (i in 0:7) {
  msg <- center + i - 3
  ct  <- bin_encrypt(ctx_s, sk_s, msg,
                     output = BinFHEOutput$LARGE_DIM,
                     p = p_s, mod = Q)
  ct_sign <- eval_sign(ctx_s, ct)
  ## Decrypt with p = 2 (sign bit is a single bit).
  result <- bin_decrypt(ctx_s, sk_s, ct_sign, p = 2L)
  cat(sprintf("msg = center%+d => sign bit %d\n", i - 3, result))
}
```

The sign flips exactly at `i = 3` (msg = center) — inputs
in the lower half of `[0, p_s)` return 0, inputs in the
upper half return 1.

The optional `scheme_switch` argument to `eval_sign()`
controls whether the output encoding is compatible with
the CKKS<->FHEW scheme-switching pipeline. Most users
can ignore it (default `FALSE`).

## Floor: bit-level rounding

`eval_floor(ctx, ct, roundbits)` performs the LWE
equivalent of `floor(ct / 2^roundbits)` via functional
bootstrapping. Used as a primitive in bit-extraction or
quantization pipelines. Like `eval_func`, `eval_floor`
works with an `arb_func = TRUE` context and the
`LARGE_DIM` encryption mode — so the previous `ctx_f`
from the LUT section is ready to use.

```{r floor}
ct_five <- bin_encrypt(ctx_f, sk_f, 5L,
                       output = BinFHEOutput$LARGE_DIM,
                       p = p)
ct_floor <- eval_floor(ctx_f, ct_five, roundbits = 2L)
class(ct_floor)
## Returns an LWECiphertext holding floor(5 / 2^2) = 1 in
## the rounded encoding.
```

## When to use BinFHE vs CKKS

The two schemes solve different problems.

| Property | CKKS | BinFHE |
|---|---|---|
| Data type | real numbers (approximate) | single bits |
| Batching | yes (thousands of slots per ciphertext) | no (one bit per ciphertext) |
| Per-op cost | cheap multiply / add, expensive bootstrap | every gate is a bootstrap internally |
| Depth budget | yes — bootstrap refreshes, see [ckks-bootstrapping](ckks-bootstrapping.html) | no — gates self-refresh |
| Good at | polynomial arithmetic, statistics, batched linear algebra | comparisons, branching, per-bit logic, arbitrary univariate functions |
| Audience-A fit in this package | **primary** — cox / cvxr / mle / encrypted-regression etc. | **secondary** — comparators, thresholding, custom LUT evaluation |

In practice the two schemes are complementary. A hybrid workflow — use
CKKS for the heavy lifting, then scheme-switch to BinFHE for a
comparison or a LUT evaluation — exists in the OpenFHE C++ library
(via `eval_sign(scheme_switch = TRUE)` and the `EvalSchemeSwitching*`
family). The scheme-switching pipeline itself is not yet exposed in
`openfhe.R`'s R interface but a future version could if a concrete use
case demands it.

## Further reading

Besides the package help, more details can be found in the [OpenFHE BinFHE documentation](https://openfhe-development.readthedocs.io/).
