Privacy disclosure
What the coordination network keeps private, how, and what it doesn't. It covers contracts,
network-member-client, user-app-core, wallet, the identity, credential and network-member-proof
libraries, account, expo-identity-keystore, issuer-service and indexer. The demos, testbeds,
reference apps and circuits are out of scope.
Network members are public by design: their records, keys and governance votes sit onchain. Users are anonymised: no PII (personally identifiable information) goes onchain, and what directly links a user to their activity stays with the user and the network member that enrolled them.
Each section lists what one party can and can't see, as the threat model does. Cells give the technical reason first, then a plain one where it helps. "Threat model" names the matching section and row.
Contents
- Public chain observer
- Other network members
- The user's own network member
- RPC provider
- Bundler
- Verifier
- Network observer
Public chain observer
Any entity observing the chain. May use primitive or sophisticated chain-analysis techniques.
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
networkId → metaStealthAddress (a network ID and its public receiving address) | yes | Both registries publish this mapping, with no personal data. | Deliveries go to fresh stealth accounts, which only the viewing key links back. The address doesn't show what was received. | IdentityRegistry.sol:33IdentityRegistry.sol:35NetworkMemberRegistry.sol:44NetworkMemberRegistry.sol:68 | 2. Registration and deduplication: Public chain observer, "Reads the networkId => metaStealthAddress entry" |
The account address linked to a user's networkId | no | Both derive from the account public key, so once that key signs onchain anyone can link them. The account would reveal its network ID once it transacts. | In Phase 1 the account key signs only offchain. Spec 01 accepts the link if a later phase signs onchain. Nothing links them today. | network-id.ts:27derive.ts:40 | 2. Registration and deduplication: Public chain observer, "Reads the networkId => metaStealthAddress entry" |
The auth address linked to a network member's networkId | yes | A network member's network ID ends in its auth address. Anyone can see which account acts for a network member. | Accepted by design: network members are known, vetted entities. | network-id.ts:39NetworkMemberRegistry.sol:40 | — |
| The link between a user's old and new meta-stealth address | yes | updateIdentity keeps the network ID, linking the new address to the old. | Recovery doesn't call updateIdentity. It removes the old entry and registers a new network ID, each from a fresh sender.A recovered user's old and new addresses aren't linked by the registry. | IdentityRegistry.sol:36IdentityRegistry.sol:69IdentityRegistry.sol:76IdentityRegistry.sol:82 | 6. Revocation and recovery: Public chain observer, "Pairs the old entry's removal with the new registration" |
| Which network member wrote a registry entry | no | A write from the network member's own address would name it. | A Semaphore proof shows only group membership, and the write comes from a fresh sponsored address. No network member is named. | NetworkMemberGate.sol:65sponsored-relay.ts:208 | 2. Registration and deduplication: Public chain observer, "Attributes every user a network member enrols to it" Guarantees: "Anonymous registry writes" |
| Link between two registry writes | no | Each write publishes a nullifier, and a repeated one would link two writes. | The client picks a fresh random scope for each proof, so nullifiers never repeat. Two writes can't be matched to each other. | NetworkMemberGate.sol:31message.ts:69 | 2. Registration and deduplication: Public chain observer, "Reads the nullifier and scope that every registry write publishes" |
| Network member onchain records | yes | NetworkMemberRegistry publishes each network member's auth address, meta-stealth address, Semaphore commitment and governance signer. | Accepted by design: network members are known, vetted entities. | NetworkMemberRegistry.sol:40NetworkMemberRegistry.sol:48NetworkMemberRegistry.sol:53 | — |
| Join requests | yes | NetworkMemberJoinRequested publishes the applicant's keys, commitment, signer and metadataURI, from its auth address. | Accepted by design: network members can only vet an applicant they can identify. | NetworkMemberRegistry.sol:58NetworkMemberRegistry.sol:131 | — |
| Sponsored operations from one sender | yes | Operations from one sender share its address, and its paymaster proof can be reused with the same nullifier for a period. | Accepted: spec 05 reuses one proof per account to allow gas cost amortisation. The network member client uses a fresh sender for every operation. | sponsored-relay.ts:208SemaphorePaymaster.sol:88 | 2. Registration and deduplication: Sponsored sender, "One membership proof sponsors the same account without limit" |
| Sponsored operations from different senders | no | A fixed proof scope would repeat the nullifier across senders and link them. | Each proof's scope is its own sender, so nullifiers differ between senders. Operations from different addresses can't be matched. | sponsored-relay.ts:230SemaphorePaymaster.sol:88 | 2. Registration and deduplication: Sponsored sender, "One membership proof sponsors the same account without limit" |
| Issuer keys per network member | yes | ApprovedIssuerSet publishes each signing key against its network member's network ID. | Accepted by design: a presentation proves its key is in the set without saying which. | ApprovedIssuerSet.sol:89ApprovedIssuerSet.sol:109ApprovedIssuerSet.sol:110 | Guarantees: "Issuer anonymity" |
| Revoked credential commitments | yes | The registry publishes every revoked commitment. A revokeMany batch shows its entries were revoked together. | Accepted by design: verifiers need the public list to check a credential isn't revoked. | revocation.ts:32IRevocationRegistries.sol:36IRevocationRegistries.sol:44CredentialRevocationRegistry.sol:77 | — |
| Whose credential was revoked | no | Each commitment is to a random secret for that credential, with no user or issuer field. | All network members write to one shared tree through the sponsored path. No entry traces to a user or network member. | revocation.ts:32IRevocationRegistries.sol:36IRevocationRegistries.sol:44CredentialRevocationRegistry.sol:77 | — |
| Stealth address delivery announcements | yes | An announcement carries the stealth address, an ephemeral key, a view tag and its caller. | Each delivery uses a fresh stealth address, ephemeral key and sponsored caller.Two deliveries to one user look unrelated. | buildAnnouncement.ts:16ERC5564Announcer.sol:15deliver.ts:22config.ts:190 | 5. Stealth addresses: Public chain observer, "Reads every announcement and may try to cluster" |
| A user's stealth accounts | no | An observer could try to group them from the announcements. | Linking one to its meta-stealth address needs the viewing key or the shared secret. Nobody outside can list a user's accounts. | scanAnnouncements.ts:40 | 5. Stealth addresses: Public chain observer, "Reads every announcement and may try to cluster" |
| Transfers and balances at stealth addresses | yes | Amounts are public, and accounts spent together in one transaction link to each other. | Accepted for Phase 1: fresh stealth addresses don't hide transaction-graph links. | fetchBalances.ts:11 | Accepted threats: "Onchain graph leaks" |
| Governance (votes on who joins or leaves) | yes | The Safe's owners and approvals are public, and each admission, removal and exit emits the network ID and signer. | Accepted by design: decisions by known network members are meant to be auditable. | NetworkMemberGovernance.sol:73NetworkMemberRegistry.sol:69 | — |
| Personal data | no | Anything identifying written onchain, even hashed, identifies a user permanently. | Only network IDs, keys, commitments and stealth addresses go onchain. Nothing onchain names a user. | IdentityRegistry.sol:33 | Guarantees: "No personal data onchain" |
| Onchain keys and entries recorded today | yes | A future quantum computer could break today's public keys and de-anonymise accounts. | Accepted: the system can move to post-quantum cryptography, but recorded data stays exposed. | — | 2. Registration and deduplication: Public chain observer, "Records the entry now to recover the key" Accepted threats: "Harvest-now-decrypt-later on existing accounts" |
Other network members
Everything the public chain observer sees, plus:
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
| Which other network members serve a user | no | Records, registry writes, sponsored operations or deliveries could each name the network members a user deals with. | Records stay on each network member's server. Registry writes and sponsored operations carry Semaphore proofs, and deliveries go to stealth addresses, so none names a network member. | issuance-store.ts:11NetworkMemberGate.sol:65SemaphorePaymaster.sol:88deliver.ts:22 | — |
The user's own network member
What the network member that enrolled a user sees in normal operation.
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
| A user's meta-stealth address | yes | An individual network member holding a user's account public key can compute the network ID and look up the entry. | Accepted: the threat model gives users no privacy from network members. A network member without the user's account public key can't tell which entry is theirs. | IdentityRegistry.sol:33adapters.ts:147 | 2. Registration and deduplication: Other network member, "Looks up a user's identifier to find their accounts" Design trade-offs: "A user has no privacy from network members" |
| Which user holds which credential | yes | The issuer service links each credential to its user's network ID, programme, country and revocation commitment, so it can revoke it. | Accepted: the network member enrolled the user and is trusted with this. The records stay on its own server. | types.ts:24issuance-store.ts:11credentials.ts:152 | Design trade-offs: "A user has no privacy from network members" |
| What the phone sends at issuance | yes | Its public keys, the signed registration request, and an attestation chain with the OS, patch level and boot key. | The attestation is discarded after checking, and only a commitment to the device key is kept. Phone details aren't stored. | credential-issue.ts:183credentials.ts:241credentials.ts:247types.ts:31 | 3. Credential issuance: User, "The device key is a software key" |
| Each user's full receiving history | yes | The indexer stores each user's viewing key, readable with ENCRYPTION_KEY. | Accepted: a viewing key reveals receipts, never spending authority. | schema.sql:14store.ts:17 | Accepted threats: "Receiving history is visible to the indexer" |
RPC provider
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
| Which users the network member works with | yes | The provider sees identity(networkId) reads at issuance, registration and delivery, and a delivery's sender reads right after. That ties the network ID to the announced stealth address. | Accepted: the network member can run its own node or use a provider it trusts. | credentials.ts:116submit.ts:64deliver.ts:30sponsored-relay.ts:125 | Accepted threats: "RPC nodes are trusted" |
| One user's accounts, grouped | yes | The indexer's /balances reads batch one user's stealth accounts together. | Accepted: the indexer can use its own node, a trusted provider or several endpoints. | fetchBalances.ts:26 | Accepted threats: "RPC nodes are trusted" |
| Which network member a user gets a credential from | no | At issuance the wallet reads issuer keys through its RPC endpoint, and reading only the issuer's would name it. | The user app reads every scanned network member's issuer keys and uses only the issuer's. The provider can't tell which one issues. | credential-issue.ts:92chain-reads.ts:179ApprovedIssuerSet.sol:414 | Accepted threats: "RPC nodes are trusted" |
Bundler
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
| Which network member sent a sponsored operation | partly | The proof binds to the disposable sender, but the issuer service reaches BUNDLER_URL from its own IP. | Accepted as out of scope for the reference deployment: use a bundler the network member runs or trusts. | SemaphorePaymaster.sol:88sponsored-relay.ts:174config.ts:166 | Accepted threats: "Network metadata is visible" |
Verifier
| Data | Visible | How / why | Mitigation | Code | Threat model |
|---|---|---|---|---|---|
| Unrevoked credential from an approved issuer, for its programme and country | no | The proof answers only for the verifier's own time, challenge, audience, programme and country. The verifier learns only that the user qualifies. | Its only other public outputs are the issuer set and revocation roots, so asking for more returns nothing. | zk-proof.ts:7zk-proof.ts:202 | 4. Credential verification: Relying party, "Asks for more than the decision needs" |
| Issuer set and revocation roots used (which list versions the proof used) | yes | Both roots are public outputs, and an old root shows roughly when the wallet last updated. | Accepted: the wallet proves against the current root, and the window defaults to one hour. | zk-proof.ts:172 | Accepted threats: "A superseded root narrows issuer anonymity" |
| Which network member issued the credential | no | Every issuer key is public under its network member's network ID, so a proof that showed its key would name the issuer. | The proof shows the key is in the approved set without saying which. Anonymity grows with the number of issuers. | zk-proof.ts:7ApprovedIssuerSet.sol:89 | Guarantees: "Issuer anonymity" Design trade-offs: "Issuer anonymity is bounded by the anonymity set" |
| Link between two presentations | no | A value repeated across proofs would let a verifier recognise a returning user. | Each proof is fresh. Its public values are the verifier's own and the two current roots, which every holder shares. | zk-proof.ts:7 | Guarantees: "Unlinkable presentation" |
| That the user holds a revoked credential | partly | When the only match is revoked, the wallet can answer CREDENTIAL_REVOKED instead of a proof, which tells the verifier. | The wallet asks for the user's consent first. | presentation-create.ts:100presentation-create.ts:104 | — |
Network observer
IP addresses, timing and sizes of every request. Nothing here hides them.