The Blind Machine

Carrier Count

Per-variant carrier counts over a sealed cohort (additive BFV).

Repository Files

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

1.0.0 · sha256:8691074bf4bb…886fb43c

carrier_count

16 files · 51.7 KB — package root with signed payload and support files

applications/carrier_count

Name Size
signed/ signed payload
tests/ support tests
129 Bytes
README.md support doc
8.47 KB
SECURITY.md support doc
4.37 KB

carrier_count — Blind Machine curated protocol

tenseal-BFV, minimal (additive-only) params. A coordinate-wise homomorphic
vector add over the flagship’s exact published coordinate definition — but with
a different client-side encoding. It is the registry-composability proof:
one published coordinate definition and one additive primitive drive a second
registered protocol without touching the trust loop. See
docs/protocol_catalog.md §2.

What it computes

Each contributor starts from the same alt-allele dosage vector g ∈ {0,1,2}^L
as the flagship, over the same fixed, published coordinate definition (ordered
variants (chrom,pos,ref,alt)). Encoding then thresholds each coordinate to a
carrier indicator locally, before encryption:

c[j] = 1 if g[j] >= 1 else 0        (missing call -> 0)

The cohort aggregate released is the per-coordinate carrier count — how many
participants carry at least one alt allele at each coordinate:

carrier_count[j] = Σ_i c_i[j]         (integer, exact, in [0, N])
carrier_rate[j]  = carrier_count[j] / N   (derived post-decrypt; people, not alleles → no ×2)

The contrast with the flagship is the whole point. Same raw genotypes, same
homomorphic circuit — but the flagship sums dosages (sum_g / 2N allele
frequency) while this sums thresholded indicators (n / N carrier rate). The
tests/vectors/ fixtures are byte-identical to the flagship’s so you can diff
the two released statistics from one input set.

Exactness: BFV is exact in Z_t with plaintext modulus t > max sum = N
(a carrier indicator is ≤ 1). tolerance: 0 — the encrypted result equals the
cleartext sum bit-for-bit. The default t = 1032193 stays exact for N up to
~1M (a larger margin than the flagship’s 2N ceiling).

Append-1 sentinel: encryption appends a trailing 1 slot to every
contribution, so the homomorphic sum’s last slot decrypts to exactly N, the
contributor count. It is an integrity/corruption check, not a MAC — it says
nothing about whether contributors are distinct or genuine (see SECURITY.md).
Note a free cross-check this protocol admits: every carrier_count[j] must lie
in [0, N] (a headcount can’t exceed the cohort), and 50_decode.py asserts it.

Security levels (--security {128,192,256})

00_keygen.py takes --security (default 128) to certify the context at a
chosen HE security level. It selects the coeff-modulus chain only; the ring
N = 8192 and plaintext modulus t = 1032193 are fixed (functions of the value
envelope and depth-0 circuit, not of security). Downstream stages read the
context and stay security-agnostic.

--security coeff_mod_bit_sizes Σ bits achieved (N=8192 caps 118/152/218)
128 (default) [60, 60, 60] 180 128 (band 153–218)
192 [50, 50, 50] 150 192 (band 119–152)
256 [45, 45, 28] 118 256 (band ≤118)

At fixed N, security level == the q-band and a smaller coeff modulus is
more secure. This protocol’s depth-0 noise floor sits in the 256 band, so
certifying 128/192 spends surplus modulus — the 128-bit chain is intentionally
larger (bigger, slower ciphertexts) than the 256-bit chain. That inversion is
correct RLWE behaviour, not a bug. The chain uses the PGS-safe 3-prime layout so
the SECURITY table is byte-identical across all four additive protocols. All
three levels decrypt bit-exact vs the cleartext oracle (parametrized in
tests/test_local_loop.py); the security column is computed from (N, Σbits)
against the HomomorphicEncryption.org table, never read back from SEAL.

(cd signed && uv --project env run python 00_keygen.py --out-dir "$D" --security 256)

Reuse map (vs the flagship allele_frequency_count)

stage change
00_keygen.py verbatim — same BFV params (8192 / 1032193) + shared --security table, no relin/Galois
10_encode.py one-line threshold — emit 1 if dosage >= 1 else 0 instead of the dosage
20_encrypt.py verbatim — append-1 sentinel + bfv_vector
30_compute_encrypted.py verbatim — pure additive fold (--context/--inputs/--out)
40_decrypt.py verbatim — one BFV integer vector, length L+1
50_decode.py math only — carrier rate = n/N (drop the diploid ×2), bound-check [0,N]

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 [--security {128,192,256}]secret_context.tenseal (never upload), public_context.tenseal (uploadable)
10_encode.py local (data owner) RAW → ENCODED --raw raw.json --length L --out encoded.json (validate dosage {0,1,2}, null→0, threshold to {0,1}, pad to L)
20_encrypt.py local (data owner) ENCODED → ENCRYPTED --context public_context.tenseal --encoded encoded.json --out cipher.bin (appends sentinel, BFV-encrypts)
30_compute_encrypted.py SERVER ENCRYPTED → ENCRYPTED --context public_context.tenseal --inputs c0.bin c1.bin … --out result.bin (homomorphic sum; no secret key present)
40_decrypt.py local (researcher) ENCRYPTED → PRIVATE --context secret_context.tenseal --result result.bin --out plain.json (length L+1)
50_decode.py local (researcher) PRIVATE → RELEASED --plain plain.json --length L --out result.json (splits sentinel→N, carrier counts, rates)

Inter-stage formats: contexts and ciphertexts are TenSEAL’s raw serialized bytes
(binary); raw/encoded/plain are JSON int lists; the released result is JSON with
n_contributors, carrier_counts, carrier_rates.

server.py’s compute is written once against an abstract evaluator E
(zero/add), 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 →
a bit-identical result digest out (compute is deterministic; encryption is not).

Run the full loop by hand

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

R 00_keygen.py --out-dir "$D"
for i in 00 01 02; 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" --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 → decrypt
→ decode equals the cleartext carrier-count aggregate exactly, that the
distinguishing encoding thresholds dosage → {0,1}, and that the sentinel decrypts
to exactly N (including that dropping one upload yields N−1). 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 (blind-v1-demo-coordinates) as the flagship, because this protocol reuses
that exact coordinate definition. The invariant that matters 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

8691074bf4bbb434cbebb0c61acf6a40cd01987f0a9035a07c4cc484886fb43c

Environment lock — the sealed, pinned dependency set

28a71f683860071c200fd61b679b6a9e2b3ea063ed0b016ea85ac8eb8ab9084a
Minimum N
20
Runs per project
1
Ed25519 signature
b87e8a952f9fba14c41cf8d111c1531b28424...

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

blind applications install carrier_count@8691074bf4bbb434cbebb0c61acf6a40cd01987f0a9035a07c4cc484886fb43c
blind explain carrier_count@8691074bf4bbb434cbebb0c61acf6a40cd01987f0a9035a07c4cc484886fb43c

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

v1

Per-variant carrier counts over a sealed cohort (additive BFV).

aggregate only published July 06, 2026

Application digest — content address of the signed payload

27dcb262b1d6f9f503970c9e14ca065d4765b1ceeb4d95bc8708dde95d757b7a

Environment lock — the sealed, pinned dependency set

28a71f683860071c200fd61b679b6a9e2b3ea063ed0b016ea85ac8eb8ab9084a
Minimum N
20
Runs per project
1
Ed25519 signature
1deda3e7d9b48ec3db8094e86bc7c1576cd11...

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

blind applications install carrier_count@27dcb262b1d6f9f503970c9e14ca065d4765b1ceeb4d95bc8708dde95d757b7a
blind explain carrier_count@27dcb262b1d6f9f503970c9e14ca065d4765b1ceeb4d95bc8708dde95d757b7a

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.