The Blind Machine

Allele Frequency with Variance

1.0.0 Ed25519 signed

Application digest — content address of the signed/ payload

b48cdffa32c46d2a5de95010ea12e434593b2af2179fcedf7f8e36ebc7245eec

197 lines · 8.08 KB · sha256:226f17d822cc…27788af0

Raw
#!/usr/bin/env python3"""server.py — the BLIND computation for `allele_frequency_with_variance`.This is the ONLY code that runs server-side. The kit shim`30_compute_encrypted.py` is its sole caller: it maps the argparse CLI(`--context/--inputs/--out`) onto the reserved ``compute`` function below. Theauthor writes only this pure, bytes-in/bytes-out function; all the argv/fileplumbing lives in the (kit-owned) shim.Trust boundary, made structural by the signature:    compute(inputs: list[bytes], public_context: bytes) -> bytesThere is NO secret-context parameter — this function is incapable of receiving asecret key. It runs in the network-isolated sandbox (`--network none`, read-onlyfs, non-root, resource-limited) with the PUBLIC context (relin keys, NO secretkey) and ciphertexts only, so the server never sees a single plaintext genotype.Fold every contributor ciphertext into TWO aggregate ciphertexts — the first andsecond moments:    sum_g[j]   = sum_i  g_ij           (additive path; reuses .add)    sum_g2[j]  = sum_i  g_ij^2         (element-wise SQUARE per contributor,                                        depth 1, THEN sum)Squaring each contributor BEFORE summing is mandatory: ``(sum g)^2 != sum g^2``.This is the ONE ct x ct multiplicative level that separates this protocol fromthe additive flagship — the server SQUARES an encrypted value, which is why thepublic context must carry relinearization keys (keygen). There is NO rotation, soNO Galois keys are needed; the square is element-wise per slot.Written ONCE against an abstract evaluator ``E`` (the cleartext oracle indocs/simulation_mode.md swaps a plaintext evaluator into the same ``aggregate`` soit cannot drift from this encrypted path):    aggregate(inputs, E):          # E provides zero / add / mul        acc_sum = <first>        acc_sq  = E.mul(<first>, <first>)        for ct in rest(inputs):    # square-then-sum, depth 1            acc_sum = E.add(acc_sum, ct)            acc_sq  = E.add(acc_sq, E.mul(ct, ct))        return acc_sum, acc_sqAdditive sentinel rides along both paths: sum path slot L sums to N; square pathslot L sums to ``sum_i 1^2 = N``.Output convention: a SINGLE ``--out`` FILE artifact. Because this protocol emitsTWO result ciphertexts (sum_g, sum_g2) and the hosted worker content-addressesexactly one opaque ``result.bin`` FILE, both moments are packed into that oneartifact as a deterministic, self-describing binary container (magic ``BMCT1\n``,see ``pack_results`` / ``unpack_results``). This is the shared Blind Machinemulti-ciphertext container format (byte-identical to the onegenotype_phenotype_covariance uses).Determinism / verify-by-re-execution: BFV add and multiply are deterministic, sothe same ordered set of input ciphertexts always yields the same resultciphertext bytes (encryption is randomized, the *compute* is not). Re-runningthis stage on the same inputs reproduces bit-identical result digests."""from __future__ import annotationsimport structfrom typing import Iterable, Protocol# Canonical moment order — shared, fixed protocol definition. Decrypt unpacks# against the same names, so the container is deterministic and self-describing.# Names match the plain.json keys decode already consumes ({"sum","sumsq"}).MOMENT_ORDER = ("sum", "sumsq")# Container framing for the moment ciphertexts inside one --out artifact. This is# the shared Blind Machine multi-CipherText container v1 (also used verbatim by# genotype_phenotype_covariance); a fixed MOMENT_ORDER + length-prefixed framing# makes the packed bytes deterministic._CONTAINER_MAGIC = b"BMCT1\n"  # Blind Machine multi-CipherText container v1class Evaluator(Protocol):    """The abstract op interface both engines implement (see simulation_mode).    Extends the flagship's zero/add with ``mul`` (the ct x ct square). A    ``PlaintextEvaluator`` mirror must implement the identical ops so the cleartext    oracle cannot drift from this encrypted path.    """    def zero(self, length: int): ...    def add(self, a, b): ...    def mul(self, a, b): ...class BFVEvaluator:    """The real (encrypted) evaluator: ops on TenSEAL BFV ciphertexts."""    def __init__(self, context) -> None:        self.context = context    def zero(self, length: int):        import tenseal as ts        return ts.bfv_vector(self.context, [0] * length)    def add(self, a, b):        return a + b    def mul(self, a, b):        # ct x ct element-wise product (depth 1). TenSEAL relinearizes the degree-2        # product back down using the relin keys retained in the PUBLIC context        # (keygen). No rotation, so no Galois keys are touched.        return a * b    def load(self, blob: bytes):        import tenseal as ts        return ts.bfv_vector_from(self.context, blob)def aggregate(inputs: Iterable, evaluator: Evaluator, length: int | None = None):    """Fold ``inputs`` into ``(acc_sum, acc_sumsq)`` under the evaluator.    ``acc_sum``   = sum_i x_i                (additive)    ``acc_sumsq`` = sum_i (x_i * x_i)        (square each input, THEN sum; depth 1)    Folds from the first input (no length needed). If ``inputs`` is empty a pair    of zero vectors of ``length`` is returned (requires ``length``).    """    iterator = iter(inputs)    try:        first = next(iterator)    except StopIteration:        if length is None:            raise ValueError("aggregate() needs a length when there are no inputs")        return evaluator.zero(length), evaluator.zero(length)    acc_sum = first    acc_sumsq = evaluator.mul(first, first)    for item in iterator:        acc_sum = evaluator.add(acc_sum, item)        acc_sumsq = evaluator.add(acc_sumsq, evaluator.mul(item, item))    return acc_sum, acc_sumsqdef pack_results(named_blobs: dict[str, bytes]) -> bytes:    """Pack the moment ciphertexts into one deterministic container.    Layout: MAGIC, then for each moment in ``MOMENT_ORDER`` a length-prefixed    name and a length-prefixed blob. Fixed order => byte-deterministic.    """    out = bytearray(_CONTAINER_MAGIC)    out += struct.pack(">B", len(MOMENT_ORDER))    for name in MOMENT_ORDER:        blob = named_blobs[name]        name_bytes = name.encode("utf-8")        out += struct.pack(">B", len(name_bytes)) + name_bytes        out += struct.pack(">Q", len(blob)) + blob    return bytes(out)def unpack_results(blob: bytes) -> dict[str, bytes]:    """Inverse of ``pack_results``: recover ``{name: ciphertext_bytes}``."""    if blob[: len(_CONTAINER_MAGIC)] != _CONTAINER_MAGIC:        raise ValueError("result artifact is not a Blind Machine multi-ciphertext "                         "container (bad magic)")    offset = len(_CONTAINER_MAGIC)    (count,) = struct.unpack_from(">B", blob, offset)    offset += 1    named: dict[str, bytes] = {}    for _ in range(count):        (name_len,) = struct.unpack_from(">B", blob, offset)        offset += 1        name = blob[offset : offset + name_len].decode("utf-8")        offset += name_len        (blob_len,) = struct.unpack_from(">Q", blob, offset)        offset += 8        named[name] = bytes(blob[offset : offset + blob_len])        offset += blob_len    return nameddef compute(inputs: list[bytes], public_context: bytes) -> bytes:    """RESERVED blind entrypoint — fold into the two moments, pack the aggregate.    Deserialize the PUBLIC context and each ciphertext, fold them into the    ``(sum_g, sum_g2)`` moment pair, and return the deterministic ``BMCT1``    container holding ``{"sum": <sum_g>, "sumsq": <sum_g2>}`` (raw serialized    ciphertexts). No secret key is present; defensively refuse a context that    carries one.    """    import tenseal as ts    context = ts.context_from(public_context)    if context.is_private():        # The server must never receive a secret key.        raise ValueError("compute stage received a context holding a secret key")    evaluator = BFVEvaluator(context)    vectors = [evaluator.load(blob) for blob in inputs]    acc_sum, acc_sumsq = aggregate(vectors, evaluator)    named = {"sum": acc_sum.serialize(), "sumsq": acc_sumsq.serialize()}    return pack_results(named)

Inside signed payload digest b48cdffa32c4…c7245eec. Change one byte here and the application becomes a different application.

Keyboard shortcuts

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

Send feedback

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