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 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 1050 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 DIRsecret_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

v1

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

aggregate only published July 06, 2026

Application digest — content address of the signed payload

d4fde2a35e21065509cf496d967d3f6a9900dd91e3bdaf17c4324c357bc43ab9

Environment lock — the sealed, pinned dependency set

df1559d9c292f359ceb1c0ccb75619688a210e98c4838fdee620dc2ffd048c8d
Minimum N
30
Runs per project
1
Ed25519 signature
6fa22f5f354371c40c08064b70d1c53fcc473...

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@d4fde2a35e21065509cf496d967d3f6a9900dd91e3bdaf17c4324c357bc43ab9
blind explain allele_frequency_with_variance@d4fde2a35e21065509cf496d967d3f6a9900dd91e3bdaf17c4324c357bc43ab9

View repository status · 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.