---
title: "Privacy-Preserving Count Aggregation"
author: "Balasubramanian Narasimhan"
date: '`r Sys.Date()`'
bibliography: homomorphing.bib
output:
  html_document:
  fig_caption: yes
  theme: cerulean
  toc: yes
  toc_depth: 2
vignette: >
  %\VignetteIndexEntry{Privacy-Preserving Count Aggregation}
  %\VignetteEngine{knitr::rmarkdown}
  \usepackage[utf8]{inputenc}
---

```{r echo=F}
knitr::opts_chunk$set(
  message = FALSE,
  warning = FALSE,
  error = FALSE,
  tidy = FALSE,
  cache = FALSE
)
```

## The problem

Multiple research sites each hold patient data and want to know
the *total count* of patients matching a criterion (say
"`age < 50` and `sex == 'F'` and `biomarker < 0.2`") without
revealing the individual per-site counts to anyone, including the
aggregator running the aggregation.

Here we use OpenFHE's **BFV** scheme via the `openfhe.R` package.
BFV operates on *integer vectors* with both addition and
multiplication — well-suited to counting and other exact
integer-valued aggregates.

This is the BFV companion to the CKKS-based real-valued
master/worker vignettes (`vignette("mle")`, `vignette("cox")`).
For exact integer aggregation BFV is the right choice; for
real-valued sufficient statistics CKKS is the right choice.

## Setup

```{r}
library(openfhe.R)

set.seed(42)
site_data <- lapply(c(1000, 500, 1500), function(n) {
    data.frame(
        age       = sample(40:70, n, replace = TRUE),
        sex       = sample(c("M", "F"), n, replace = TRUE),
        biomarker = runif(n, 0, 1)
    )
})
```

## The aggregator sets up encryption

```{r}
## BFV: exact integer arithmetic mod plaintext_modulus.
cc   <- fhe_context("BFV",
                    plaintext_modulus    = 65537L,
                    multiplicative_depth = 1L)
keys <- key_gen(cc)

pk <- keys@public
sk <- keys@secret
```

In a real deployment the aggregator distributes the public key
(and the serialized context) to each site. The secret key stays
with the aggregator. We simulate this in one R session by
sharing `cc` and `pk` locally.

## Each site computes locally and encrypts

```{r}
site_encrypt <- function(site_df, cc, pk) {
    count <- sum(site_df$age < 50 &
                 site_df$sex == "F" &
                 site_df$biomarker < 0.2)
    pt    <- make_packed_plaintext(cc, as.integer(count))
    encrypt(pk, pt, cc = cc)
}

ct_site1 <- site_encrypt(site_data[[1]], cc, pk)
ct_site2 <- site_encrypt(site_data[[2]], cc, pk)
ct_site3 <- site_encrypt(site_data[[3]], cc, pk)
```

These encrypted counts are opaque to the aggregator — it learns
nothing about any individual site's count.

## The aggregator aggregates

```{r}
## Homomorphic addition: the aggregator never decrypts intermediate values.
ct_total <- ct_site1 + ct_site2 + ct_site3

## Only the aggregator holds the secret key.
result      <- decrypt(ct_total, sk, cc = cc)
total_count <- get_packed_value(result)[1]
total_count
```

## Verification

```{r}
true_count <- sum(sapply(site_data, function(df) {
    sum(df$age < 50 & df$sex == "F" & df$biomarker < 0.2)
}))
true_count

stopifnot(total_count == true_count)
```

The aggregated count matches the cleartext computation exactly.
BFV is an *exact* scheme over the integers: unlike the real-valued
arithmetic used elsewhere in this package, it introduces no
approximation error at all.

## The details of what happened

1. The aggregator created an encryption context and distributed
   the **public key** to all sites.
2. Each site computed its local count in the clear, encrypted
   it, and sent the encrypted count to the aggregator.
3. The aggregator added the encrypted counts using `+`, which
   gives the same answer as adding the counts themselves.
4. Only the aggregator, holding the **secret key**, could
   decrypt the total.

No site revealed its individual count. The aggregator never
saw any patient-level data. The total is exact.

## Serializing for a real distributed protocol

In a real deployment each site needs the aggregator's context
and public key. `openfhe.R` provides serialization:

```{r}
tdir <- tempdir()
fhe_serialize(cc, file.path(tdir, "context.bin"))
fhe_serialize(pk, file.path(tdir, "pubkey.bin"))

cc_remote <- fhe_deserialize(file.path(tdir, "context.bin"), "CryptoContext")
pk_remote <- fhe_deserialize(file.path(tdir, "pubkey.bin"), "PublicKey")

ct <- encrypt(pk_remote,
              make_packed_plaintext(cc_remote, 42L),
              cc = cc_remote)

fhe_serialize(ct, file.path(tdir, "site_count.bin"))
ct_received <- fhe_deserialize(file.path(tdir, "site_count.bin"), "Ciphertext")
result      <- decrypt(ct_received, sk, cc = cc)
get_packed_value(result)[1]
```
