The Blind Machine

Carrier Count

1.0.0 Ed25519 signed

Application digest — content address of the signed/ payload

8691074bf4bbb434cbebb0c61acf6a40cd01987f0a9035a07c4cc484886fb43c
carrier_count / README.md

154 lines · 8.47 KB · sha256:c9c498135d2e…3df5f25f

# `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 computesEach contributor starts from the same alt-allele **dosage vector** `g ∈ {0,1,2}^L`as the flagship, over the same fixed, published coordinate definition (orderedvariants `(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 manyparticipants 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, samehomomorphic circuit — but the flagship sums *dosages* (`sum_g / 2N` allelefrequency) while this sums *thresholded indicators* (`n / N` carrier rate). The`tests/vectors/` fixtures are byte-identical to the flagship's so you can diffthe 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 thecleartext 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 everycontribution, so the homomorphic sum's last slot decrypts to **exactly N**, thecontributor count. It is an integrity/corruption check, **not a MAC** — it saysnothing about whether contributors are distinct or genuine (see `SECURITY.md`).Note a free cross-check this protocol admits: every `carrier_count[j]` must liein `[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 achosen HE security level. It selects the coeff-modulus chain **only**; the ring`N = 8192` and plaintext modulus `t = 1032193` are fixed (functions of the valueenvelope and depth-0 circuit, not of security). Downstream stages read thecontext 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, socertifying 128/192 spends *surplus* modulus — the 128-bit chain is intentionally**larger** (bigger, slower ciphertexts) than the 256-bit chain. That inversion iscorrect RLWE behaviour, not a bug. The chain uses the PGS-safe 3-prime layout sothe `SECURITY` table is byte-identical across all four additive protocols. Allthree 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.```bash(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 contractThe 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 visiblewithout 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 swapsa `PlaintextEvaluator` for the same `compute` and cannot drift from this encryptedpath. 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```bashcd protocols/carrier_countD=/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"doneR 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)```bashuv --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 thedistinguishing encoding thresholds dosage → {0,1}, and that the sentinel decryptsto **exactly N** (including that dropping one upload yields N−1). Skips with aclear reason only if TenSEAL cannot be imported.## Coordinate definition & synthetic dataFor the synthetic v1 demo the `L=1000` coordinate list is generateddeterministically from `manifest.yml`'s `input.coordinates.seed` — the **same**seed (`blind-v1-demo-coordinates`) as the flagship, because this protocol reusesthat exact coordinate definition. The invariant that matters is that everycontributor encodes against the same published definition and that definition isfolded into the bundle SHA-256. All data here is synthetic integer vectors; noreal genomic data is used anywhere.

Packaged support file for application digest 8691074bf4bb…886fb43c. It ships in the archive for review, but is outside the signed payload digest.

Keyboard shortcuts

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

Send feedback

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