Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Trust and honest limits

zero-indexer’s migration-privacy guarantee is designed as a chain of mechanisms, documented here end to end. Their status differs, and each is marked:

  • the Nym 5-hop mixnet that unlinks a shim from the hub, so the hub cannot tell which operator or region a migration came from (deployed: an attested shim and hub pair has run on the public mixnet since 2026-08-14);
  • AWS Nitro attestation over a reproducible StageX build, making the exact software in each enclave checkable rather than asserted (built: both shim and hub run as attested enclaves today, and a real mainnet Orchard-touching transaction has crossed the whole stack);
  • STEVE, Caution’s one-way enclave handshake, by which the shim verifies the hub before handing over a migration (designed; there is no separate encrypt-to-hub-key layer yet);
  • the keymaker / locksmith M-of-N quorum, a governance mechanism (separate from STEVE) that would persist keys across cold boots and upgrades and hand every hub the same shared key (designed);
  • the Auditor Role, which lets any independent third party confirm a public endpoint is really running the reviewed software, without trusting the operator (exercisable today for the application layer; see the PCR caveat below).

The same attestation makes the attested edge protections real: that the operator’s front-end is exactly the reviewed shim code, and its promise not to log requester IPs is verifiable, not merely trusted. See the problem for what that edge protects and review for claims still awaiting sign-off. The architecture owns the two trust-plane diagrams and the three nested encryption layers (the design end-state; today only the outer Nym layer is deployed); the roadmap covers PIR (V3) as the step that removes the hardware trust root.

What is trusted, and what is not. The trust root is the AWS Nitro platform and the people who review the open source and reproduce its build hash; the table under what this trusts has the party-by-party detail. The one entry worth flagging up front, because it changed: the mixnet client is linked in-process rather than run as a sidecar, so it is inside each enclave’s TCB.

The Nym transport (shim to hub)

Status: deployed. An attested shim and hub pair has run this transport on the public mixnet since 2026-08-14, the hub publishing its address at GET /nym-address. The clearnet dial remains in the code but is off at the hub unless ZIH_HTTP_SUBMIT re-opens it. What has not caught up is verifiability: the live pair’s published provenance does not currently reproduce (below).

Nym is used for exactly one hop: shim to hub. Wallets do not speak Nym; a naive wallet reaches the shim over ordinary TLS, and all query and non-migration traffic flows from the shim to the operator’s backing lwd over the local network. Only an isolated migration is routed over Nym, from the shim’s side to the hub’s. This is the same boundary STEVE observes: both live only on the shim-to-hub channel, never on wallet-to-shim.

The mixnet. Nym is a 5-hop mixnet. The encrypted migration is wrapped in a Sphinx packet and relayed through independent mix nodes, each peeling one layer and forwarding, so no node sees both source and destination. Cover traffic (steady dummy packets indistinguishable from real ones) makes the flow unlinkable in time and volume: the hub cannot tell which region or operator a migration came from, and a network observer cannot pick the real migration out of the cover. This breaks the “IP X was active at time T” to “on-chain migration at time T” correlation for naive wallets.

The mixnet client is linked, not a sidecar, and is therefore inside the TCB. Each side links nym-sdk and runs its own client in-process. An earlier design placed nym-sdk TcpProxy binaries outside each enclave as untrusted sidecars, on the argument that a migration was already encrypted to the hub’s key before it entered the tunnel. That design is superseded and the argument does not survive it: there is no encrypt-to-hub-key layer yet, and the client now sits inside the enclave rather than beside it. The honest consequence is that the TCB grew. The mixnet client and its dependency tree are part of what an auditor must review and what the reproducible build must cover.

What protects this hop instead, in the shipped code: every SubmitV1 frame is zero-padded to exactly 64 KiB and every AckV1 to 64 bytes, so frame size reveals nothing about the transaction inside; sends are anonymous with reply SURBs rather than an addressed tunnel; and the client’s sender tag rotates on a configurable interval. Mix nodes and the parent host stay untrusted and see only Sphinx traffic. Nym mainnet uses ticketbook ecash credentials, so the client needs Nyx-RPC egress (rpc.nymtech.net:443) to obtain them, a real operational dependency, not just an IP route.

Where the performance numbers come from. In the V2 rehearsal (2026-07-30), nym-proxy built from nymtech/nym carried real CompactTxStreamer gRPC over the live Nym mainnet mixnet against a live testnet node, end to end. Throughput is roughly 10x slower than clearnet (unary calls ~9 to 10 seconds, GetBlockRange ~19 blocks per second, latency-bound, first one or two calls warming up before steady state). Fine for migrations, which are not time-sensitive. One caveat on how far that carries: the rehearsal used the superseded proxy pair rather than the shipped in-process driver, so it bounds the mixnet’s cost rather than the current code path. Gateway backpressure, not credentials, turned out to be the throughput constraint, which is why the shipped clients pin their entry gateways.

TEE attestation (AWS Nitro)

Both binaries, the shim and the hub, run inside AWS Nitro enclaves. The enclave makes operator-blindness real and checkable. Nitro provides hardware memory isolation (parent instance and host cannot inspect enclave memory) and a hardware root of trust that signs a statement about the software inside. The design additionally assumes the enclave resists state rollback (rewind/replay) and memory-access-pattern observation; hardening for both is an open item (see review). See the problem for the detection-not-prevention framing this rests on.

Reproducible build, root hash. Both binaries are static-musl and built reproducibly with StageX (SOURCE_DATE_EPOCH=1); the recipes are zeronym/shim/deploy/ and zeronym/hub/deploy/. A reproducible build means anyone with the source produces a bit-identical image and the same measurement hash, so the chain runs: rebuild from source, get the same hash, match it against the hash bound into the attestation. An attestation alone proves only that some binary runs inside a genuine enclave; without reproducibility the design collapses into trusting whoever compiled it, which is exactly the party it refuses to trust.

Status: the source-to-hash link is proven for the recipe, and currently drifted for the deployed pair. The published hashes live in deploy/EXPECTED_SHA256 (machine-readable; they move when the source does, so this book quotes no literal).

Cross-machine agreement has been demonstrated: a native x86_64 CI runner, a local arm64 macOS host building linux/amd64 under emulation, and a third-party operator’s native x86_64 box all landed on the then-published hash from the same commit. Those differ in machine and in how the compiler was executed, so the agreement also rules out codegen that varies with runtime CPU feature detection, which two native builds would not have caught. Read that as a property of the recipe, not of any current value: each hash in force today is two cold builds on a single x86_64 runner, self-consistent but not cross-machine.

Three honest qualifications, all live as of 2026-08-14.

  • The check does not run on every push. Both reproduce workflows fire on pull requests touching the recipe and on manual dispatch, so a change landed directly on main outruns them.
  • Both published hashes are currently stale against the tip of main, and both jobs report DOES NOT REPRODUCE. The cause is known and deliberate: several recent changes each noted that they need a re-baseline before an attested cut, and that re-baseline has not happened yet. This is the third time the tripwire has caught a drift, which is the tripwire working, but it means the published values do not describe the current source.
  • The deployed pair’s provenance does not check out. The live shim’s PROVENANCE cites a source commit that is not public, so an auditor cannot even check out the tree it names; the live hub’s quotes a hash that its own cited commit does not produce. Until both are corrected, the enclaves are attested and running but not independently verifiable.

deploy/README.md carries the post-mortems. Building it settled the three requirements the PoC had surfaced:

  • A context spanning the vendored subtrees (solved). The crate path-depends on zebra/zebra-chain and zaino/packages/zaino-proto, so a per-crate context cannot work. An assemble script composes a repo-shaped context with git archive HEAD, which both resolves the path deps unchanged and structurally prevents the build from dirtying a vendored subtree.
  • zaino-proto pinned to default-features = false (solved, and double-locked). Its heavy default lets its build.rs find protoc and regenerate its committed protos inside the vendored subtree, and pulls a second zebra-chain from crates.io. Beyond the feature pin, the image simply ships no protoc, so the regeneration path cannot execute whatever happens to features. The tripwire stays cheap: git status --porcelain zaino/ zebra/ must be empty after a build.
  • A committed lockfile (done), pinning a parser that is not yet the node’s (still open). Cargo.lock pins the classifier’s parse. It is not identical to the node’s. The shim and hub are their own workspaces and now agree with each other (orchard 0.15.5, halo2_proofs 0.3.5, pinned across both lockfiles so a parse skew cannot surface as the shim rejecting a correct hub answer), but zebra’s binaries resolve orchard 0.15.3 and halo2_proofs 0.3.2. The lockfiles pin that remaining gap rather than closing it; closing it means the shim and hub joining a repo-root workspace, or all three pinning the same versions. The differential classifier test that would catch a real divergence is still deferred.

The second link is now demonstrated too. Both binaries run inside Nitro enclaves (Shielded Labs’ shims since 2026-08-01; the first third-party shim, in that operator’s own AWS account, on 2026-08-10), each serving a nonce-bound COSE_Sign1 attestation that chains to the AWS Nitro root. caution verify exercises the rebuild-and-compare loop against the published deploy context (app_sources in the manifest), and the application-layer measurement (PCR2) reproduces. One honest caveat, as of Caution platform 8e31ea7: the framework measurements (PCR0/1) do not reproduce, because Caution’s builder fetches its framework from a floating branch archive rather than the pinned commit, so caution verify reports FAILED on healthy enclaves until their fix lands. What an auditor can conclude today: a genuine, unmodified enclave whose application layer reproduces against the assembled deploy context, but which cannot yet be tied back to a public commit of this repository (the provenance defects above).

The hub reached that point later than the shim, and the parity is recent. Its assembler only gained an --app-source flag on 2026-08-13, and before that the hub manifest recorded no application source at all, so caution verify refused outright rather than reporting a mismatch: no hub enclave before that date was independently verifiable in any degree. The deploy script now also pushes the assembled tree to a public repository on an attested deploy, which is what gives an auditor something to clone.

In-enclave keygen and NSM binding. Each enclave generates its key at boot, inside the enclave (the private key never leaves) and binds the public key into the attestation produced by the Nitro Security Module (NSM). The attestation is a COSE_Sign1 document, signed up to the AWS Nitro root, carrying the enclave’s PCRs (the StageX root hash) alongside the bound public key. Verifying it proves both: the software is exactly the reviewed build, and the public key was born inside that software rather than handed in by the host.

The shim binds its TLS public key (see the shim for the ACME cert model), so an auditor can check the certificate a wallet sees is keyed to an enclave-born key. The hub binds its hub public key, the key shims encrypt migrations to. Attestation binding is achievable three ways on Caution’s platform (confirmed at V2): via the STEVE handshake, via the pubkey injected through metadata.json into user_data (which implies a persisted key), or via a new runtime arbitrary_data field Caution would add. The specific mechanism, and how /attestation is delivered for a zero-ingress service, are open for Caution (see review).

STEVE

Status: design. STEVE is not yet integrated; the shipped shim-to-hub hop is plain TLS terminating inside the hub enclave, with no separate encrypt-to-hub-key layer. What follows is the design.

STEVE (“Secure Transport Encryption Via Enclave”) is a Distrust protocol integrated into Caution, documented at distrust.co/blog/steve.html with source at git.distrust.co/public/steve. It is a second encryption layer that terminates inside the enclave, so that even when an outer transport terminates outside it, the plaintext exists only within the TCB.

Scope. Used only on the shim-to-hub channel, carrying the submit and lookup exchanges. Not used on wallet-to-shim (naive wallets speak plain TLS, which terminates inside the shim enclave on its own). STEVE is the middle of the architecture’s three layers.

One-way handshake: the client (shim) verifies the enclave (hub), not the reverse:

  1. The client checks the hub’s attestation and PCRs against the AWS Nitro root.
  2. It extracts the enclave’s Ed25519 identity key from the attested material.
  3. It sends an ephemeral X25519 public key.
  4. It receives the enclave’s ephemeral key plus an Ed25519 signature over that ephemeral key.
  5. It verifies the signature against the attested Ed25519 identity key.
  6. Both sides derive a session key by X25519 ECDH followed by HKDF-SHA256.
  7. Payloads are CBOR encoded and encrypted with AES-256-GCM.

STEVE runs as a reverse proxy on :8080: on the hub side the STEVE server decrypts inbound migrations inside the enclave; on the shim side the STEVE client performs the verification before any migration is sent. The Rust SDK is still in development (the JS SDK ships today), which is why implementing the handshake directly from standard primitives is a fallback (see review).

Two open STEVE items go to Caution: the exact wire form over Nym (does a STEVE session carry gRPC / h2, or a raw framed byte stream we frame ourselves), and mutual vs one-way (one-way suffices for privacy; making the hub also verify the shim’s attestation would additionally gate abuse). Both in review.

The keymaker quorum (separate from STEVE)

Status: design. The quorum is not built. At launch a single trusted entity (Caution) stands up the hub, and today a shim dials one hub with one key. What follows is the design.

Enclaves are diskless and ephemeral: a key generated in-enclave at boot is lost on every restart, and a software upgrade changes the measurement so a KMS-seal-to-PCR scheme would refuse to unseal the old key. zero-indexer needs long-lived keys anyway (a stable TLS key and address for the shim, one hub key every hub instance shares), so it uses a keymaker / locksmith M-of-N quorum.

What it is. An M-of-N key-custody mechanism spread across 3 to 4 organizations: the proposed consortium of Caution, Nym, Shielded Labs, and the Zcash Foundation. It reconstitutes a key inside a fresh, attested enclave across both cold boots and software upgrades (strictly better than sealing to PCRs, which breaks on upgrade). Private key material is only ever reassembled inside an attested enclave; no single org holds it.

What it persists. For the shim: the TLS keypair, giving the shim’s public URL a stable key and address across boots and upgrades, so the endpoint does not churn its identity and the ACME cert can renew against a stable key. For the hub: a single shared hub key across all instances. A shim encrypts a migration to “the hub key,” and any attested hub instance can decrypt, dedup, and publish it. Per-hub keys would force re-encryption on failover and strand a migration whose hub died mid-flight. With one shared key provisioned to every attested hub by the quorum, running two or more hubs with shim failover (see the hub) costs nothing in key management; a rare double-publish is a harmless on-chain duplicate, deduped by payload hash.

Governance trajectory. The quorum is also the long-term trust-distribution goal: the consortium collectively attests to key state and software integrity, so no single party (not even Caution) unilaterally controls the hub key. For launch this is staged: a single trusted entity (Caution) stands up the hub, with the multi-org quorum to follow. The consortium’s several organizations are the natural operators of the standby hubs, where decentralization of the hub itself begins.

The Auditor Role

The drop-in model creates a verification problem: a wallet connects to a familiar public URL (say zec.rocks:443) and needs to know it is really fronted by the reviewed shim enclave, not an operator who quietly kept the plaintext. The Auditor Role answers this without trusting the operator, and passes the assurance on: in practice a wallet developer acts as the trust proxy, audits once, and every user of that wallet inherits the result.

Steps for a public lwd endpoint:

  1. Fetch the endpoint’s TLS public key and certificate directly over HTTPS.
  2. Load its attestation. POST a nonce to /attestation and receive a COSE_Sign1 document, proving the private key was generated inside the enclave and carrying the root hash of the software there.
  3. Verify the PCRs against the AWS Nitro root. Confirm the attestation chains to the genuine Nitro hardware root of trust and the measured software matches expectations.
  4. Reproduce the build. Run the reproducible StageX build from source, obtain the hash, and confirm it equals the attested root hash, tying “the reviewed source” to “the software actually running.” This is the step the other three are worthless without. It has now been exercised end to end (above), with the caveat noted there: today it proves the application layer, and the framework measurements follow when Caution’s pinning fix lands.
  5. Check Certificate Transparency. Confirm no other currently-valid certificate exists for the domain. Otherwise an operator could present the attested, enclave-born cert to auditors while serving a different, non-enclave shadow cert to real users and MITM them. All Let’s Encrypt certificates are CT-logged, and the shim obtains its cert via in-enclave ACME precisely so this check is meaningful (see the shim). CT closes the cert-substitution gap the public-URL drop-in would otherwise leave open.

Concrete probes against a running enclave (the node IP here is an ephemeral testnet enclave):

curl -sk -X POST -H 'Content-Type: application/json' \
  -d '{"nonce":"00112233445566778899aabbccddeeff"}' https://<node-ip>/attestation
grpcurl -plaintext \
  -import-path <zaino>/packages/zaino-proto/lightwallet-protocol/walletrpc \
  -proto service.proto <node-ip>:8137 \
  cash.z.wallet.sdk.rpc.CompactTxStreamer/GetLightdInfo

The hub is audited the same way, and the shim’s STEVE handshake is the shim performing exactly this audit of the hub on every session before it trusts a hub with a migration. Independent auditors run the same steps out of band; reproducibility is what lets the consortium and third parties confirm that the running hub is the reviewed hub.

What this trusts, and what it does not

This table is the designed end-state; on the deployed hop the STEVE and hub-key layers are absent and each row’s why still holds by a shorter path.

PartyTrusted?Why
AWS Nitro manufacturer + platformYes (the trust root)Hardware root of trust signs the attestation; memory isolation blocks host peeking
Reviewers who reproduce the build hashYesThe StageX hash is only meaningful if someone recomputes it; CI does on every change, and a third party has matched it (above)
The in-enclave mixnet clientYesLinked in-process rather than run as a sidecar, so nym-sdk and its dependencies are inside the TCB and must be reviewed and reproduced like the rest
Operator running the shimNoSees only TLS that terminates in the enclave; Orchard-touching transactions never reach its backing lwd (see the shim)
Hub host (Caution at launch)NoSees only ciphertext (TLS terminating in-enclave today, STEVE in the design); cleartext exists only inside the attested hub enclave
Nym mix nodesNoSee only Sphinx packets, fixed-size and anonymously sent, and no node sees both source and destination
Indexer and nodes the hub broadcasts throughNoReceive only the final, wallet-signed, batched transactions, unlinked from any source IP

The honest consequence: this privacy trusts AWS and the hardware, not mathematics. That limit is developed under honest limits below; PIR (V3) is what removes the trust root (roadmap).

Honest limits

None of the limits below is a bug to be quietly patched later: each is a structural consequence of the design, and stating it plainly is required for launch and any blog post. The robust, volume-independent win is IP unlinking for the broadcast: the hub, not the requesting wallet, publishes the migration, so the on-chain transaction carries no link to the wallet’s IP. (The Nym hop additionally unlinks operator from hub.) The properties below are weaker, conditional, or out of scope. One scope reminder: the system protects the migration broadcast, not general queries. Which addresses a wallet looks up still pass through the shim to the operator’s backing lwd as today (query privacy is the deferred vision, see the roadmap); the one lookup that no longer does is GetTransaction, now answered by the hub so a migration’s follow-up query never reaches the operator.

The operator learns that a client migrated, not the amount. The shim forwards all traffic to the backing lwd untouched, with exactly one exception: an Orchard-touching transaction (a migration in the code’s terms, see the shim) is the one request it does not forward, diverting it to the hub instead (over TLS today; encrypted to the hub key and routed over Nym in the design). That asymmetry is observable, so an operator watching its own host’s traffic can infer that a given source IP submitted a migration, and roughly when, because that one request behaves differently from everything else. It does not learn which on-chain transaction or what amount: the hub batches the migration with every other operator’s clients and publishes them simultaneously, unordered. The residual is “IP X migrated something, around time T,” not “IP X migrated amount Y into transaction Z.” This inherent residual drives a firm decision: shim-side batching and shim-to-hub cover traffic are rejected. Both were proposed to hide the fact of a migration from the operator (batch a second time inside the shim, or send dummy shim-to-hub traffic), but a migration is by construction the one request the shim withholds from the backing lwd, so traffic analysis at the operator recovers the fact regardless of padding or delay. The genuine defense is hub batching (hides the on-chain transaction and amount), not a doomed attempt to hide the fact. A future Nym-aware wallet talking to the hub directly would sidestep it, but near-term wallets are naive TLS clients, so the residual stands.

The anonymity set is the cross-operator batch, and at launch volumes it is one. What hides which on-chain migration belongs to a client is the batch: the set of migrations from all shims that the hub publishes together in one flush. An operator never sees other operators’ clients, so the mixing happens at the hub, across the whole network. That makes batch size the key privacy metric for the timing property, and a singular batch is not a thin-period risk but the expected case. Sampling 144 blocks at mainnet tip 3,433,105 on 2026-08-01 gives 0.77 Orchard-touching transactions per block network-wide, roughly 37 an hour across every wallet in existence. Only the fraction flowing through participating shims reaches the hub, so with one to a few operators arrivals into the flush window are Poisson with a mean well under one, and the modal published batch is zero or one. For a batch of one the anonymity set is the transaction itself, and the shuffle, the simultaneous publish, Nym and the TEE are all irrelevant to that transaction’s timing. During the acute Orchard to Ironwood window density is natural, which is the whole reason to batch that mass, non-time-sensitive event; outside it the property thins out.

There is no version of this that code fixes. Holding longer needs a window the wallet expiry does not permit (the hub has the budget: the rest of it is spent on mining time and delivery lag); decoys reach only the orchard_value_balance == 0 subclass, because covering a genuine exit means spending a real legacy Orchard note and NU6.3 makes that stock finite, so they cost real on-chain value and are a last resort rather than the primary mechanism; and refusing to publish a lone migration would route it to a direct broadcast, a strictly worse leak. The batch is also publicly identifiable by construction: simultaneous submission to a small set of known nodes on a published cadence is exactly what an observer clusters on, so batch membership and size are readable from public data by anyone.

So the property is real but conditional, and the condition is adoption. State the threshold rather than the mechanism. The system should say what migration rate through participating shims is needed for a target batch size, publish the achieved distribution once running (the hub measures and exports it rather than asserting it), and decline to claim timing unlinkability until the measurement supports it. The single largest lever is not ours: transaction expiry is a wallet default rather than a consensus rule, so the flush interval is capped by whichever wallet in scope is least generous. Taking Brave out of scope for v1 already bought a doubling; raising the floor further, epoch-canonically, is what would make batch size a function of adoption instead of a fixed loss. That is a conversation with wallet teams, not a hub setting.

Broadcast is delayed, and the shim accepts before the chain does. The shim returns an accepted response to the wallet as soon as the migration is dispatched to the mixnet, which is earlier than it sounds: submit is dispatch-only, so at that moment the shim does not know the hub received the frame, only that it went out. The transaction is in no mempool until the hub flushes, up to roughly 25 minutes later (blocks target about 75 seconds; the flush is a strict 20-block cadence). A wallet polling in the mempool or confirmed? is answered by the hub, not the operator: the shim routes every GetTransaction to the hub, which reports a queued-but-unflushed migration at height 0 (the mempool sentinel) and serves its confirmed detail from the hub’s own indexer after flush. The one narrow gap is flush-in-flight: for the few seconds between the queue draining and the broadcast reaching the indexer a lookup can miss, though a resubmit in that window is harmless (deduped before flush, already-known after). Migrations are not time-sensitive, so the delay is acceptable by design. The batched class is wider than migrations: every Orchard-touching transaction is delayed, including a deshield straight to transparent. That costs little, because ordinary commerce lives in Ironwood (architecture has the argument). The real cost is UX, since the pending gap can confuse a UI or trigger a resend. A sharp edge, widened by the dispatch-only choice: because the shim returns success before the hub has even acknowledged receipt, both an invalid migration and one whose frame never arrived receive a false success and fail silently. The mitigation is stateless pre-validation (parseable, not already expired) at the shim and again at the hub, plus the wallet’s own confirmation-via-sync, which is what the design now leans on for delivery as well as validity. Surfacing a late hub-side failure to the client is out of near-term scope.

The shim is stateless; durability lives in the hub. The shim keeps no per-migration state, so a restart or a second shim instance loses nothing and leaks nothing. (An earlier design cached diverted txids in the shim to answer follow-up lookups; a restart forgot them, and the next poll fell through to the operator, the exact IP-to-migration link this system exists to prevent. It was deleted.) Recognising nothing, the shim routes every follow-up GetTransaction to the hub, so the only in-flight durable copy of an accepted migration is the hub’s queue, held until the next flush. Every failure path fails closed: an unreachable hub is an error to the wallet, never a fall-back to the operator’s indexer (the leak) or to a direct broadcast (off by default), and the wallet retries while the hub collapses identical resubmissions. The queue is RAM in a diskless enclave, deliberately, to keep the enclave TCB small, and it is not retained past flush, so a hub panic would lose an in-flight batch. That is why the hub code forbids panics outright rather than recovering from one; there is no confirmation-tracking layer on either side, and after flush a batch is on the network like any other submission. The queue and dedup engineering lives with the hub.

The trust root is AWS and the hardware, not math. The operator-blindness and hub-blindness guarantees are real and verifiable, but they rest on a hardware root of trust, not cryptography that stands alone. A wallet (or a developer acting as trust proxy) checks the shim’s and hub’s attestations against the AWS Nitro root, confirms the reproducible-build hash matches, and thereby knows the attested software is running and that keys were born inside the enclaves. That chain is only as strong as the AWS Nitro platform and the physical boundary of the hardware. If you do not trust AWS and the hardware, the near-term system gives no purely mathematical fallback. This is the specific gap the deferred PIR layer (V3) closes: PIR makes query privacy hold via math, hardware-independent, and closes the access-pattern and IO-pattern leaks a Nitro enclave structurally has when its state is parent-mediated at mainnet scale. TEE and PIR are complementary, not equivalent, with distinct failure modes (a TEE fails on hardware-manufacturer or physical-boundary compromise; PIR fails on cryptographic or software flaws), so the endgame uses both, defense in depth, with PIR as the trust-root-removal step (see the roadmap).

Hub resilience, and the concentration of trust. The hub must hold every migration in order to publish it, which makes a single hub both a liveness risk (an outage would stall migrations) and a concentration of trust (all cross-operator batching converges on it, run by Caution at launch). The design bounds this without eliminating the central point: two or more attested hubs on a shared quorum-provisioned key, queues idempotent by construction so a double delivery is a harmless on-chain duplicate, and a shim that fails closed (an unreachable hub errors to the wallet and never falls back to the operator, and direct broadcast stays off by default because it is a strictly worse leak than a retry). Everything there except fail-closed is designed rather than built; the engineering is in the hub.

The governing principle: an outage degrades privacy at the margin (a lost batch), while liveness rests on the wallet resubmitting rather than on any buffer inside the shim. No hub operator sees cleartext, since the hubs are attested enclaves. But the trust distribution is a goal, not a shipped property: at launch a single entity stands the hub up, consortium to follow.

How to report these limits. Lead with the robust, volume-independent claim (IP unlinking for the migration broadcast: the hub, not the wallet, publishes, and both ends are attested enclaves) and qualify the conditional ones: the batch-timing anonymity depends on migration density and can be size one; the operator can infer the fact of a migration though not its amount; the broadcast is delayed; the trust root is AWS and the hardware, not math. Do not describe a mechanism as shipped when it is only built: Nym is in the binaries but not in any attested deploy, and STEVE and the keymaker quorum have no code at all. Report the achieved batch size rather than an assumed one. Underclaiming here is cheap; overclaiming is the failure mode this chapter exists to prevent.