The Blind Machine

Cohort Histogram

1.0.0 Ed25519 signed

Application digest — content address of the signed/ payload

9f030d1c07b87761e9aed5ca7214a5cac8ed3f18cde2c71d8032bcbfb1a3a953
cohort_histogram / README.md

150 lines · 8.51 KB · sha256:96be69fdcca0…07915ebc

# `cohort_histogram` — Blind Machine curated protocol> tenseal-BFV, **minimal (additive-only) params** — the exact same additive> circuit as the flagship, pointed at a **non-genomic** aggregate. Each> contributor is one-hot over a fixed, published bucket definition; the cohort> result is the per-bucket count vector. It exists to prove the additive> primitive is **generic**: genomics is the demo domain, not the only one.> See `docs/protocol_catalog.md` §3.## What it computesEach contributor falls into **exactly one** bucket of a fixed, published,ordered bucket definition (the v1 demo uses demographic age bands`["0-9", "10-19", …, "90+"]`, `B = 10`). They map their raw value to a bucketindex per that definition and encode it as a one-hot vector `h ∈ {0,1}^B` with`sum(h) = 1`. The cohort aggregate released is the per-bucket sum:```counts[b] = Σ_i h_i[b]          (integer, exact)   — the histogramsum(counts) == N                (one-hot invariant; cross-checks the sentinel)```**Exactness:** BFV is exact in `Z_t` with plaintext modulus `t > max bucketcount`. Since each contribution is one-hot, the largest bucket count is `N`, so`t = 1032193` is exact for `N` up to ~1M. `tolerance: 0` — the encryptedhistogram equals the cleartext histogram **bit-for-bit** at every security level.## HE security level (`--security {128,192,256}`)`00_keygen.py` accepts `--security {128,192,256}` (default **128**, matching theprior behaviour). It is the ONLY security-dependent knob: it selects the`coeff_mod_bit_sizes` chain (the RLWE ciphertext modulus). `poly_modulus_degree`(N=8192) and `plain_modulus` (t=1032193) are **fixed** — they are functions of thevalue envelope and the depth-0 additive circuit, not of security. The choice flowsdownstream automatically because stages 20/40 `context_from(...)` the serializedcontext; no other stage changes.| `--security` | `coeff_mod_bit_sizes` | Σ bits | achieved (N=8192 band) ||---|---|---|---|| `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 the security level is the q-band**: a *smaller* Σ is *more* secure.So — the intentional inversion — the **128-bit** chain uses a *larger* coeffmodulus (Σ=180, bigger/slower ciphertexts) than the **256-bit** chain (Σ=118).The depth-0 additive noise floor for a bounded-int histogram sits deep in the 256band, so certifying 128/192 spends *surplus* modulus. This is correct RLWEbehaviour, not a bug; all three decrypt bit-exact. The benchmark's `security`column is computed by the harness as `achieved(N, Σ) = strictest level whose capcovers Σ`, never read back from SEAL (SEAL always validates at tc128).These 3-prime chains are byte-identical across all four additive protocols(afc / carrier / cohort / pgs) so the `SECURITY` table stays one shared contract.The `tests/` local-loop proves bit-exactness (and `achieved == requested`) at allthree levels.**Append-1 sentinel:** encryption appends a trailing `1` slot to everycontribution, so the homomorphic sum's last slot decrypts to **exactly N**, thecontributor count. For a one-hot protocol this is doubly strong: the first `B`slots must *also* total `N`, so decode asserts `sum(counts) == sentinel` — a freeintegrity cross-check the flagship's dosage vectors don't have. It is anintegrity/corruption check, **not a MAC** — it says nothing about whethercontributors are distinct or genuine (see `SECURITY.md`).## Reuse relationship to the flagship`cohort_histogram` is a near-free clone of `allele_frequency_count`: `00_keygen`,`20_encrypt`, `30_compute_encrypted`, and `40_decrypt` are the flagship'sadditive stages verbatim (same BFV params, same append-1 sentinel, same abstract`zero`/`add` evaluator fold, same `is_private()` server guard). Only two stageschange:- `10_encode.py` — index → one-hot (a contributor holds a single bucket index in  `[0, B)`; encode builds `[0]*B` with one `1`, and rejects an out-of-range or  missing bucket — a one-hot histogram has no "missing → 0" escape hatch).- `50_decode.py` — emits `{counts, n_contributors}`, drops the frequency/×2  denominator, and adds the `sum(counts) == N` integrity assertion.## 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 B --out encoded.json` (raw is a single bucket index in `[0,B)`; emits one-hot) || `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 B+1) || `50_decode.py` | local (researcher) | PRIVATE → RELEASED | `--plain plain.json --length B --out result.json` (splits sentinel→N, counts; asserts `sum(counts)==N`) |Inter-stage formats: contexts and ciphertexts are TenSEAL's raw serialized bytes(binary); `raw.json` is a single JSON integer (bucket index); `encoded`/`plain`are JSON int lists; the released result is JSON with `n_contributors`,`buckets_length`, `counts`. **Bucket labels are not re-emitted** — the orderedbucket definition lives in the digest-folded `manifest.yml`; the consumer mapscount position `b` to its published label.`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/cohort_histogramD=/tmp/ch && mkdir -p "$D"R() { (cd signed && uv --project env run python "$@"); }R 00_keygen.py --out-dir "$D"              # add --security 192 or 256 to certify a stronger levelfor i in 00 01 02 03 04; do  R 10_encode.py  --raw ../tests/vectors/contributor_$i.json --length 10 --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_*.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 10 --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 histogram **exactly**, that the sentinel decrypts to**exactly N** (including that dropping one upload yields N−1 and decrements thatcontributor's bucket), and that the one-hot integrity check (`sum(counts) == N`)rejects a tampered aggregate. Skips with a clear reason only if TenSEAL cannot beimported.## Bucket definition & synthetic dataFor the synthetic v1 demo the `B = 10` bucket definition (ordered age bands) isenumerated in `manifest.yml`'s `input.buckets.labels`. The invariant that mattersis that every contributor encodes their membership against the **same** published,ordered definition and that definition is folded into the bundle SHA-256. Alldata here is synthetic bucket indices; no real personal data is used anywhere.

Packaged support file for application digest 9f030d1c07b8…b1a3a953. 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.