The Blind Machine

Allele Frequency with Variance

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

Repository Files

Browse the package root. signed/ is the digest-verified payload; root docs and tests/ ship as review support.

1.0.0 · sha256:b48cdffa32c4…c7245eec

allele_frequency_with_variance

17 files · 72.8 KB — package root with signed payload and support files

applications/allele_frequency_with_variance

Name Size
signed/ signed payload
—
tests/ support tests
—
129 Bytes
BENCHMARK.md support doc
4.75 KB
README.md support doc
11.6 KB
SECURITY.md support doc
5.63 KB

allele_frequency_with_variance — Blind Machine multiplicative protocol

tenseal-BFV, multiplication-supporting params (depth 1). The server SQUARES
an encrypted value (ciphertext × ciphertext) to derive the second moment, so it
exercises exactly one BFV multiplicative level. Same published coordinate
definition and same contributor payload as the flagship
(allele_frequency_count) — which makes it the controlled “money comparison”
row: hold the inputs fixed, add one multiplicative level, measure the premium.
See docs/protocol_catalog.md §5. The additive client-precompute benchmark
variant
(client pre-squares, server only sums) is documented in
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

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)

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.

Published Versions

1.0.0

aggregate only published July 09, 2026

Application digest — content address of the signed payload

b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec

Environment lock — the sealed, pinned dependency set

df1559d9c292f359ceb1c0ccb75619688a210e98c4838fdee620dc2ffd048c8d
Minimum N
30
Runs per project
1
Ed25519 signature
b933be1a387d57e1b03c7c5b0cde9edd25c39...

Manifest, coordinate definition, and params travel inside the signed payload — inspect them from the CLI (the digest above pins them all):

blind applications install allele_frequency_with_variance@b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec
blind explain allele_frequency_with_variance@b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec

View pinned repository · Version metadata (JSON) · the same record the CLI reads

Keyboard shortcuts

  • Cmd/Ctrl+K
    Focus global search
  • ?
    Open keyboard shortcuts

Send feedback

We'll only use this to respond to your feedback.