---
title: "Allele Frequency with Variance"
description: "Allele counts plus server-derived variance (multiplicative BFV, depth 1)."
last_updated: 2026-07-09
canonical: "https://blindmachine.org/applications/allele_frequency_with_variance"
---

# Allele Frequency with Variance

Allele counts plus server-derived variance (multiplicative BFV, depth 1).

Only versions that pass the configured Ed25519 trust gate appear below.

## Trusted published versions

### 1.0.0

- Summary: 
- Payload digest: `b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec`
- Environment lock: `df1559d9c292f359ceb1c0ccb75619688a210e98c4838fdee620dc2ffd048c8d`
- Minimum contributors: 30
- Runs per project: 1
- Aggregate-only: yes
- Published: 2026-07-09T03:57:43Z
- Metadata: [JSON](https://blindmachine.org/api/v1/applications/allele_frequency_with_variance/versions/b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec)
- Signed bundle: [download](https://blindmachine.org/api/v1/applications/allele_frequency_with_variance/versions/b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec/bundle)
- Signature: [download](https://blindmachine.org/api/v1/applications/allele_frequency_with_variance/versions/b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec/signature)

Install and verify the pinned version:

```sh
blind applications install allele_frequency_with_variance@b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec
blind explain allele_frequency_with_variance@b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec
```

## Bundled README

# `allele_frequency_with_variance` — Blind Machine multiplicative protocol

&gt; tenseal-BFV, **multiplication-supporting params** (depth 1). The server SQUARES
&gt; an encrypted value (ciphertext × ciphertext) to derive the second moment, so it
&gt; exercises exactly **one** BFV multiplicative level. Same published coordinate
&gt; definition and same contributor payload as the flagship
&gt; (`allele_frequency_count`) — which makes it the controlled "money comparison"
&gt; row: hold the inputs fixed, add one multiplicative level, measure the premium.
&gt; See `docs/protocol_catalog.md` §5. The additive **client-precompute benchmark
&gt; variant** (client pre-squares, server only sums) is documented in
&gt; `BENCHMARK.md`; it is not a separate registry protocol.

## What it computes

Each contributor holds an alt-allele **dosage vector** `g ∈ {0,1,2}^L` over a
fixed, published coordinate definition (ordered variants `(chrom,pos,ref,alt)`),
identical to the flagship; missing calls encoded as 0. The cohort aggregate
released is **two** per-coordinate integer vectors — the first and second moments:

```
sum_g[j]   = Σ_i g_i[j]              (integer, exact — additive path)
sum_g2[j]  = Σ_i g_i[j]²             (integer, exact — server squares under encryption)

mean[j]     = sum_g[j] / N                          (mean dosage; derived post-decrypt)
variance[j] = sum_g2[j] / N − (sum_g[j] / N)²       (population variance E[g²]−E[g]²)
frequency[j]= mean[j] / 2                            (2 alleles per diploid coordinate)
```

**The square is server-side.** The client sends only one ciphertext of `g`. The
server computes `Σ_i enc(g_ij)²` under encryption — squaring **each contributor
first, then summing**, because `(Σ g)² ≠ Σ g²`. That per-contributor square is
the ct × ct multiply; it needs relinearization keys in the public context. This
is the whole point versus the additive benchmark variant, where the client would
also encrypt `g²` and the server would only add (`BENCHMARK.md`, `docs/spec.md`).

**Exactness:** BFV is exact in `Z_t`. The largest value is `max sum_g2 = 4N`
(`g² ≤ 4`), and the plaintext modulus `t = 786433` exceeds it for `N` up to ~196k.
`tolerance: 0` — both encrypted integer vectors equal the cleartext moments
**bit-for-bit**. `mean`/`variance`/`frequency` are real-valued derivations of the
two exact integer aggregates.

**Append-1 sentinel:** encryption appends a trailing `1` slot to every
contribution, so **both** result vectors' last slot decrypts to **exactly N**
(sum path: `Σ 1 = N`; square path: `Σ 1² = N`). `50_decode.py` cross-checks that
the two sentinels agree. It is an integrity/corruption check, **not a MAC** — it
says nothing about whether contributors are distinct or genuine (see `SECURITY.md`).

## Crypto approach — why multiplication-supporting params

| parameter | value | why |
|-----------|-------|-----|
| `poly_modulus_degree` | **16384** (fixed, all levels) | multiplication-supporting ring; 16384 slots ≫ L+1. The depth-1 noise floor (~200 bits) can't fit under the 152/118 caps at n=8192, so 8192 is out for 192/256 — N stays 16384 across all three levels (vary the chain, fix N). The larger ring is the dominant cost vs the flagship's 8192. |
| `coeff_mod_bit_sizes` | **selected by `--security`** (see table below) | this is the ONLY knob security moves; depth-1 needs ≥2 interior 40/60-bit primes between the two 60-bit special primes. |
| `plain_modulus` | **786433** (fixed, all levels) | 20-bit NTT batching prime, `≡ 1 (mod 32768)` — **required** at n=16384 (the flagship's `1032193` is invalid here). Exact for `max sum_g2 = 4N`. A function of the value envelope + depth, not of security. |
| relinearization keys | **yes** | ct × ct raises ciphertext degree to 3; TenSEAL relinearizes back to 2 using relin keys, which `00_keygen.py` generates (a secret key exists at context creation) and **retains through `make_context_public()`**. |
| Galois keys | **no** | the square is element-wise per slot; there is no rotation, so no Galois keys are generated. |

### `--security {128,192,256}` — the coeff-modulus chain

`00_keygen.py --security L` (default **128**) selects `coeff_mod_bit_sizes` from
the authoritative per-level table; `poly_modulus_degree` and `plain_modulus` are
fixed. The benchmark's `security` column is the achieved level computed from
`(N, Σ coeff_mod_bit_sizes)` against the HomomorphicEncryption.org caps at
N=16384 (256 ≤ 237, 192 = 238–305, 128 = 306–438), and **achieved == requested**
for every row (verified bit-exact against the cleartext oracle, TenSEAL 0.3.16):

| `--security` | `coeff_mod_bit_sizes` | Σ bits | achieved | ciphertext cost |
|--------------|-----------------------|--------|----------|-----------------|
| `128` (default) | `[60, 60, 60, 60, 60, 60]` | 360 | **128** | largest |
| `192` | `[60, 60, 60, 60]` | 240 | **192** | middle |
| `256` | `[60, 40, 40, 60]` | 200 | **256** | smallest |

**Intentional inversion:** at fixed N, security level == the q-band and *smaller*
Σ ⇒ *more* secure, so the **128-bit** cell uses a **larger** coeff modulus (bigger,
slower ciphertexts) than the **256-bit** cell. This is correct RLWE behaviour, not
a bug — the depth-1 noise floor for this payload already sits in the 256 band, so
certifying 128/192 spends *surplus* modulus. (`[60,40,40,60]=200` technically meets
all three targets; we publish cap-tracking chains so the `security` column reads a
distinct, honest 128/192/256.) The chain flows unchanged into every downstream
stage — they all `ts.context_from(...)`, so `10`–`50` stay security-agnostic.

**§3 escape hatch (not in the default table):** a *quantized-trait* /
oversized-cohort deployment whose grown `t` breaches the 256 cap at N=16384 moves
to **N=32768** via the explicit overrides — e.g.
`--security 256 --poly-modulus-degree 32768 --plain-modulus 537133057
--coeff-mod-bit-sizes 60 50 50 50 60`. This is a per-deployment override, not the
default binary payload.

## Stage lifecycle & I/O contract

The author's logic lives in three pure-function files, grouped by role: `server.py`
(`compute`, the only server-side function), `local_project_owner.py`
(`keygen`/`decrypt`/`decode`), and `local_data_owner.py` (`encode`/`encrypt`) —
these are what sibling `tests/` import. The six numbered files are materialized into `signed/` at run time and are
**kit-owned shims** (thin argparse wrappers; do not edit) that map each stage's CLI
(`python NN_*.py --help`) onto those functions, keeping the lifecycle visible
without opening a subdirectory.

| stage | runs | trust in → out | I/O |
|-------|------|----------------|-----|
| `00_keygen.py` | local (researcher) | — → PRIVATE + PUBLIC context | `--out-dir DIR` → `secret_context.tenseal` (never upload), `public_context.tenseal` (uploadable; **relin keys retained**) |
| `10_encode.py` | local (data owner) | RAW → ENCODED | `--raw raw.json --length L --out encoded.json` (validate {0,1,2}, null→0, pad to L) — verbatim flagship |
| `20_encrypt.py` | local (data owner) | ENCODED → ENCRYPTED | `--context public_context.tenseal --encoded encoded.json --out cipher.bin` (appends sentinel, BFV-encrypts a SINGLE ciphertext) — verbatim flagship |
| `30_compute_encrypted.py` | **SERVER** | ENCRYPTED → ENCRYPTED × 2 | `--context public_context.tenseal --inputs c0.bin c1.bin … --out result.bin` (**squares under encryption**; packs `sum_g` + `sum_g2` into ONE deterministic container; **no secret key present**) |
| `40_decrypt.py` | local (researcher) | ENCRYPTED × 2 → PRIVATE | `--context secret_context.tenseal --result result.bin --out plain.json` (unpacks both moments, each length L+1) |
| `50_decode.py` | local (researcher) | PRIVATE → RELEASED | `--plain plain.json --length L --out result.json` (splits both sentinels→N, sum_g, sum_g2, mean, variance, frequency) |

**`--out` on the compute stage is a single FILE.** Although this protocol emits
two result ciphertexts (`sum_g`, `sum_g2`), `30_compute_encrypted.py` packs them
into ONE self-describing, deterministic binary container written at the `--out`
FILE path — magic `BMCT1\n` (Blind Machine multi-CipherText container v1), a
uint8 count then, in fixed `MOMENT_ORDER = (sum, sumsq)`, each moment as a
length-prefixed name + length-prefixed raw ciphertext (`pack_results` /
`unpack_results`). This is the SAME container format
`genotype_phenotype_covariance` uses (each bundle carries its own verbatim copy —
bundles are self-contained). The single-file output is what the hosted worker
content-addresses (one opaque `result.bin`, SHA-256'd), so the flag convention
(`--context` / `--inputs` / `--out`) matches the flagship's exactly. Fixed field
order + length prefixes + no timestamps/maps make the packed bytes deterministic,
giving verify-by-re-execution.

Inter-stage formats: contexts and ciphertexts are TenSEAL's raw serialized bytes
(binary); raw/encoded are JSON int lists; `plain.json` is
`{"sum": [L+1 ints], "sumsq": [L+1 ints]}`; the released result is JSON with
`n_contributors`, `sum_g`, `sum_g2`, `mean`, `variance`, `allele_frequency`.

`server.py`'s `compute` is written **once** against an abstract evaluator `E`
(`zero`/`add`/**`mul`**), so `docs/simulation_mode.md`'s cleartext correctness
oracle swaps a `PlaintextEvaluator` for the same `compute` and cannot drift from
this encrypted path. Determinism gives verify-by-re-execution: the same ordered
ciphertexts in → bit-identical result digests out (compute is deterministic;
encryption is not).

## Run the full loop by hand

```bash
cd protocols/allele_frequency_with_variance
D=/tmp/afv && mkdir -p "$D"
R() { (cd signed && uv --project env run python "$@"); }

R 00_keygen.py --out-dir "$D"          # add --security {128,192,256} (default 128)
for i in 00 01 02 03; do
  R 10_encode.py  --raw ../tests/vectors/contributor_$i.json --length 16 --out "$D/enc_$i.json"
  R 20_encrypt.py --context "$D/public_context.tenseal" --encoded "$D/enc_$i.json" --out "$D/c_$i.bin"
done
R 30_compute_encrypted.py --context "$D/public_context.tenseal" \
  --inputs "$D/c_00.bin" "$D/c_01.bin" "$D/c_02.bin" "$D/c_03.bin" --out "$D/result.bin"
R 40_decrypt.py --context "$D/secret_context.tenseal" \
  --result "$D/result.bin" --out "$D/plain.json"
R 50_decode.py  --plain "$D/plain.json" --length 16 --out "$D/result.json"
cat "$D/result.json"
```

## Test (local-loop equivalence)

```bash
uv --project signed/env run --group dev python -m pytest tests/
```

Proves keygen → encode → encrypt (≥3 synthetic contributors) → compute (**server
squares**) → decrypt → decode equals the cleartext first- and second-moment
oracle **exactly** (both `sum_g` and `sum_g2`), and that the sentinel decrypts to
**exactly N** in both paths (including that dropping one upload yields N−1). A
**parametrized case runs the full loop at each `--security` level (128, 192,
256)** and asserts bit-exact moments + sentinel==N at every level, plus that the
shipped chain lands in the requested q-band (achieved == requested). One test
guards the mandatory square-then-sum (`Σ g² ≠ (Σ g)²`); one runs the **additive
client-precompute benchmark variant** and asserts a bit-identical `sum_g2`. Skips
with a clear reason only if TenSEAL cannot be imported.

## Coordinate definition & synthetic data

For the synthetic v1 demo the `L=1000` coordinate list is generated
deterministically from `manifest.yml`'s `input.coordinates.seed` — the **same
seed as the flagship**, which is what makes protocol 5 the controlled
multiplicative comparison. The invariant that matters is not a separate
coordinate file — it is that every contributor encodes against the **same**
published definition and that definition is folded into the bundle SHA-256. All
data here is synthetic integer vectors; no real genomic data is used anywhere.
