Documentation for World ID
- The Protocol’s primary source of documentation is directly in the codebase and particularly the foundational crates.
- Primitives: https://docs.rs/world-id-primitives
- Core: https://docs.rs/world-id-core
- The developer documentation and generally how to use World ID can be found in: https://docs.world.org/world-id
Supporting Documentation
This folder includes supporting documentation for World ID, particularly point-in-time product and technical specifications for discussion and reference. These docs differ from the primary sources (e.g. crates documentation) because they are static at the time of introduction.
Index
world-id-4-specs: High level product and technical specs of the World ID 4.0 Upgrade.world-id-4-trusted-setup: High level explanation of what a trusted setup is and how to contribute to it for World ID 4.0. After the trusted setup is done it will also contain a step by step guide to verify the provenance of the.zkeyfiles used to power World ID ZK circuits.
Abstract
The World ID v4.0 release introduces account abstraction at its core, transforming a World ID from a single secret into an abstract record in a public registry (called WorldIDRegistry) with multiple authorized keys. This fundamental change enables several key improvements:
- Multi-key support: Users can generate proofs using multiple valid authenticators (such as across multiple devices, multiple platforms or multiple applications) while maintaining the same identity.
- Improved Protocol Resilience: Multi-key support reduces likelihood of loss of all keys, key rotation and revocation are natively supported, and the user can (optionally) define a World ID Recovery Agent (e.g. such as biometric authentication).
- Web-based authenticator provider: A reference web authenticator provider simplifies World ID usage directly in browsers, improving user experience and adoption potential.
- Improved privacy: The protocol enforces one-time use of nullifiers for longer-term actions, preventing tracking while maintaining proof integrity.
These changes collectively increase security, privacy, and usability while enabling more sophisticated applications of human verification in the Age of AI. For more details on what is changing from a technical standpoint, please see Summary: What is Changing?.
Product Specs
At a high level, this release of the Protocol is about Account Abstraction. A World ID is no longer tied to a single secret, and instead is an abstract record created in the public WorldIDRegistry with a defined set of authorized keys which allow the user to interact with their World ID (a similar mental model is how passkeys authorize accounts for specific websites or how smart contract wallets work in the Ethereum ecosystem).
Stemming from the enablement of other Authenticators to exist, a reference open-source, web-based Authenticator will be published which will reduce some of the overhead for using a World ID and simplifies the user experience. The enhanced user experience will enable simpler and more widespread adoption of the Protocol.
Definitions
- Authenticator: A software or hardware agent (e.g., app, device, web client, or service) that controls a set of authorized keypairs for a World ID Account and is functionally capable of interacting with the Protocol, and is therefore permitted to act on that account’s behalf. An Authenticator is the agent of users/holders. Each Authenticator is registered in the
WorldIDRegistrythrough their authorized keypairs. - OPRF Nodes: The OPRF Nodes are a multi-party set (MPC) of nodes responsible for enabling the nullifier generation which users present to RPs to prove uniqueness.
- Recovery Agent: A party designated by the user which has a special permission to recover their World ID Account in case of loss of all Authenticators. The party may be a set of entities represented with abstract rules (e.g. through a smart contract). This designation is optional and done on-chain.
- Action: Maintains definition from previous Protocol version.
- Nullifier: Maintains definition from previous Protocol version. A nullifier is a one-time use identifier that guarantees an action can only be taken once. It’s analogous to a one-time use ticket, a user generates a nullifier through a Proof, and presents it to an RP. A nullifier will always be the same for the same user and the same RP+Action.
- Uniqueness Proof: A statement where a user proves they are performing an action once and they own a particular credential (e.g. I’m a unique person with a PoH credential voting on this proposal once). These proofs cannot be cryptographically associated with any other proof (e.g. not across RP, or even to the same RP).
- Session Proofs: Used with a
sessionIdprovided by an Authenticator and subsequently provided by an RP to receive a statement that cryptographically guarantees the same World ID is being used (e.g. I have an account with an RP and I want to prove I now have a new Credential). These proofs cannot be associated across RPs, across sessions with the same RP, or with any other proof.
Key Product Functionality
This notes the key new features or functionality for this release of World ID (v4.0):
- Multi-key support: A World ID is not bound to a single key. A user can generate proofs on multiple valid authenticators (e.g. devices, platforms). With the important exception of security properties of the Authenticator, a proof proves the same thing to an RP regardless of which authenticator was used.
- A user can add or remove different valid authenticators to manage their World ID (Portability).
- Motivation: Allowing multiple authenticators serves to the Decentralization of the Protocol, with no reliance on a single actor (such as a single Authenticator provider, e.g. World App). Furthermore, abstracting a World ID into a conceptual record vs. a single secret enables the secure and practical existence of multiple Authenticators as well as enabling Recovery in case of loss and rotation in case of compromise.
- Recovery: Regain access to the same World ID through Recovery Agents.
- Motivation: Recovery is a fundamental building block of a Proof-of-Human Protocol (see Whitepaper on why). While we expect authenticator providers to offer robust backup mechanisms, the user must be able to recover their World ID and related state in a contingency scenario.
- Web-based Authenticator Provider: A limited authenticator that allows usage of World ID in the web browser. This serves both as a reference of an authenticator and also for improved UX for certain RP flows. Functionality is limited as enrollment of credentials is out of the scope for this initial release.
- Motivation: A lightweight web-based authenticator enables simpler usage of World ID which provides a better user experience and will enable more growth as interacting with RPs will be significantly simpler.
- Trusted RPs. An authenticator can identify a request comes from a valid RP.
- Motivation: Authenticators need to be able to identify that they’re generating proofs for the right recipient to reduce potential for proof phishing (e.g. a malicious actor asking you for a proof meant for a different RP to know if you’ve performed that action). In addition, this enables future introduction of Protocol fees.
Non-Functional Requirements
- Privacy.
- Assuming non-collusion of nodes of each multi-party system, the user’s privacy cannot be compromised by any single party, neither correlation of multiple actions nor direct identification of a user. For example, it’s impossible to know that a specific user performed a specific action (identification) or that two different Actions were performed by the same user (correlation).
- Addressing the attack vector of collusion of a threshold (or all) nodes is covered in the Other Risk Considerations section.
- Strict requirements for the identifiers handed off by the Protocol are introduced. See details in Tech Specs.
- No human super-cookies. Permanent state or linkable state is as privacy-preserving as possible and is protocol-enforced. Privacy preserving in this context means that it’s not possible to identify a single person, even pseudonymously, across a long period of time without ongoing consent. Any exposed long-living / constant IDs should be protected as secrets.
- Security.
- A World ID is not a single secret that needs to be shared or can’t be rotated.
- A World ID cannot be recovered or an authenticator added without verifiable user intent (through knowledge of a secret key of an authorized authenticator).
- Collusion of all nodes with an multi-party system (MPC) does not allow performing actions on the user’s behalf.
- User Auditability — each user needs to be able to see account management events that have been authorized with their World ID, for example things like adding / removing of authenticators.
- Migration Path.
- There needs to be a clear migration path for all currently active and relevant use cases to the new version of the Protocol.
User Flows (Authenticator)

Other Considerations
- The privacy and security implications of Authenticators is improved (key revocation, etc.) but the user must trust their Authenticator(s). The Authenticator must handle the user’s Credentials (or previously Personal Custody Packages) and keys correctly.
Technical Specs
Tip
Please note these are the high level technical specifications at the time of introduction of World ID 4.0. The most up-to-date technical documentation reflecting the current state of the Protocol can always be found in the developer documentation and the Protocol repository.
Summary: What is Changing?
- A World ID is now a record on an on-chain registry and more importantly a single World ID can have multiple public keys.
- This also means identity commitments (
identityCommitment) no longer exist. Instead, the identification mechanism is the knowledge of a secret key corresponding to a public key registered in a specific leaf index in theWorldIDRegistry. - Also implies that the on-chain trees of identity commitments is gone in favor of a single
WorldIDRegistry.
- This also means identity commitments (
- Creating a World ID now occurs through on-chain registration (vs. as an offline keypair generation previously), and issuing Credentials is now done without on-chain interaction. Credentials are now issued by the Issuer signing them. Previously, the Issuer would add the user’s identity commitment to the relevant on-chain tree.
- Nullifiers are enforced one-time use. Previously there was no enforcement of nullifiers being one-time use and they could become pseudonymous identifiers for an RP, now Authenticators will not issue a nullifier more than once.
- [For RPs only]. When RPs require users to prove they are still the same World ID that originally performed an action, they will be able to store an identifier (a
sessionId) and provide it to the user for subsequent proofs. With Proof of Human, this allows RPs to establish they are interacting with the same World ID, potentially with different credentials too. See Session Proofs for further details. - [For Issuers only]. Authentication based on using nullifiers from ZKPs as identifiers is no longer supported. A new authentication mechanism is introduced for issuers.
- Access to a World ID can be recovered. A user can designate a Recovery Agent for their account which will allow for recovery in case of access to all Authenticators is lost.
- [Recovery Agent Scope]. Users may designate the PoH AMPC system as their Recovery Agent to recover their World ID. In the future, other Recovery Agents are expected to be available.
High level overview
Diagram of components for the World ID 4.0 Protocol.
-
The World ID Registry is introduced. This public contract is the source of truth for World IDs, a World ID conceptually becomes an account, represented in a leaf of the Merkle tree in this contract. Each account is identified with its index in the tree. The registry also contains a list of authenticator public keys authorized to act on behalf of the holder. The OPRF Nodes, as well as RPs use this to verify proofs (through an Merkle inclusion proof within the ZKPs).
-
Similarly, a Relying Party Registry is introduced. This registry contains a list of authorized Relying Parties with their accompanying authorized public keys. The registry permits RPs to authenticate requests for proofs to Authenticators.
-
The multi-party set of OPRF Nodes is introduced. This set of nodes are now responsible for generating the nullifiers that users present to RPs to prove uniqueness. The nullifiers are generated through a Verified Threshold Oblivious Pseudorandom Function (vOPRF) with participation of the OPRF nodes. Nodes verify requests for nullifiers are properly validated by both RPs and users (see Uniqueness Proofs), and only then will generate the required output to compute the user’s nullifier. The users then construct the final nullifier and prove its computation in the proof they present to RPs.
- A multi-party OPRF is necessary because it prevents nullifiers from being guessable, i.e. nullifiers are deterministic but appear random (recall that PRF outputs under a uniformly random key are computationally indistinguishable from a uniformly random function). This could theoretically be accomplished with a regular hash function, but then nullifiers could be brute forced by computing the hash for all possible
leafIndexes (which are public on-chain). To prevent this, secret entropy is required (in World ID ≤ 3.0, the user provided this entropy). Since this is not available anymore, the entropy now comes from the OPRF nodes. - Additionally, to prevent brute forcing even with involvement of OPRF nodes, OPRF nodes require authentication before computing each hash. They authenticate the user through a ZKP that proves knowledge of an Authenticator secret key authorized in the
WorldIDRegistryfor the particularleafIndexfor which they are generating a nullifier. - Importantly, the OPRF nodes compute the keyed-hash function $H_k(x’)$ on a blinded input, hence they cannot learn which user is actually performing a request. Furthermore, the OPRF nodes output a proof that attests to the proper computation of $H_k$ given a committed $k_{pk}$, so neither users nor RPs need to blindly trust the OPRF nodes.
- Similar to how OPRF Nodes are used to generate the nullifiers presented to RPs, these nodes also generate a blinding factor for each credential so there cannot be correlation of World IDs from malicious issuers.
- More information on the OPRF Nodes can be found in the paper: “A Nullifier Protocol based on a Verifiable, Threshold OPRF”.
- Details about the nature, number, and diversity requirements of OPRF nodes must be established before the production network is live.
- A multi-party OPRF is necessary because it prevents nullifiers from being guessable, i.e. nullifiers are deterministic but appear random (recall that PRF outputs under a uniformly random key are computationally indistinguishable from a uniformly random function). This could theoretically be accomplished with a regular hash function, but then nullifiers could be brute forced by computing the hash for all possible
-
Protocol differences at a glance:
World ID ≤3.0 World ID 4.0 (2025) What is a World ID? A secret. An entry in public registry. Proof Generation Semaphore proofs generated on the client. Conceptually the same but with new ZK-circuits. Users generate a query proof for OPRF nodes, which provide computations that enable the nullifier generation. A final Uniqueness Proof is generated and presented to RPs. How does the Relying Party enforce uniqueness? Storing a nullifier hash. Conceptually the same. Post-compromise privacy If the user’s World ID secret leaks, all past activity could be identified.
Furthermore, the secret cannot be rotated.If a user’s authenticator secret leaks, no past activity can be identified on its own. Collusion with an RP would be required to compromise some past activity.
Secrets can be easily rotated.Recoverability Cannot be recovered if the secret is lost. Can be optionally recovered through a Recovery Agent. Credential Issuance On-chain Merkle Tree containing idComms.
Credential-equivalent held self-custodially.Signed message from the Issuer.
Credential held self-custodially.Signal (proof messages) An RP can include an arbitrary signal to which the user commits. Conceptually the same. Secret Security Secret must be stored with software protections and loaded into memory. Conceptually possible to store keys in hardware-based key stores. Initial keys will have the same characteristics as the previous protocol version due to ZK proving limitations.
Technical Details
Uniqueness Proofs
A Uniqueness Proof is how a user primarily proves they are performing an action once and they own a Credential that meets the RP’s requirements (e.g. the user has a unique government-issued document). While fundamentally the user is proving uniqueness per World ID Account when performing an action, if the request is not bound to a Credential that offers a measure of uniqueness (e.g. PoH or unique government-issued document), the proof will lack value.
sequenceDiagram
actor User
RP ->> RP: Generate & sign proof request
RP ->> User: Send proof request
User ->> Indexer: Query leafIndex and inclusion proof in WorldIDRegistry
Indexer --> World Chain: Indexes the WorldIDRegistry tree
Indexer ->> User: leafIndex & inclusion proof
User ->> User: Generate Query Proof $$\pi_{R}$$
User ->> OPRF Node i: blindedLeafIndex + $$\pi_{R}$$ + request context
OPRF Node i ->> OPRF Node i: check validity of request
OPRF Node i ->> World Chain: Verify Merkle root (usually cached)
OPRF Node i ->> OPRF Node i: Verify $$\pi_{R}$$
OPRF Node i ->> User: blinded result
User ->> User: Unblind each response and reconstruct OPRF evaluation
User ->> User: Compute blinded subject for credential
User ->> User: Generate final Proof ($$\pi_{F}$$)
alt
User <<->> Oblivious Nullifier Pool: check if nullifier exists and insert
end
User ->> RP: $$\pi_{F}$$ + nullifier
alt
RP ->> Indexer: Verify ZKP
else
RP ->> World Chain: Verify ZKP
end
RP ->> RP: Verify expected nonce
RP ->> RP: Verify nullifier uniqueness
-
The nullifier is computed by the OPRF Nodes. Computing it requires output from a threshold number of nodes to be valid.
- Importantly, the input to the OPRF Nodes is blinded so that no OPRF node can see the raw
leafIndex(i.e. OPRF nodes only know that the request is from an authorized authenticator). - Importantly, the nullifier is credential independent, so the action can only be performed once regardless of which credentials are available at the time.
- Further information on how the nullifier is computed can be found in the TACEO OPRF Whitepaper.
- Importantly, the input to the OPRF Nodes is blinded so that no OPRF node can see the raw
-
Nullifiers have the following properties, which in combination make them amenable for use by an RP to enforce anonymous per-action uniqueness:
Property Description Deterministic Given the same context ( leafIndex[blinded],rpId,action), the nullifier is always the same. Assuming honest behavior of OPRF nodes never rotating their base key. Note that the credential is intentionally not included in this context. This means that the action can be performed only once, regardless of which credentials are available at the time.Unguessable For a fixed context, the nullifier output is uniformly distributed over the output space. Even with the full set of possible users, computing candidate nullifiers is infeasible without collusion of a threshold of OPRF nodes such that the OPRF key is known. Authenticated It’s probabilistically impossible to claim ownership over a nullifier if you don’t know a secret value (a secret key whose public counterpart is registered in the WorldIDRegistry).Anonymous A nullifier hides which user generated it. To preserve anonymity, each nullifier must only be used once (otherwise repeated use makes it pseudonymous). This is the responsibility of Authenticators. Unlinkable For any two nullifiers with different contexts, the probability that an adversary can correctly distinguish whether they were derived from the same user is at most negligibly better than random guessing. Pre-image resistance For any given nullifier, and knowing the public context ( rpId,action), it is computationally infeasible to find the pre-image or theleafIndex. -
The authenticator generates two types of different zero-knowledge proofs to be able to deliver a Uniqueness Proof to an RP,
- The query proof $\pi_1$ which proves to the OPRF Nodes that the request is properly authorized by the user. This ZKP proves the request is signed by a public key which is registered for the particularly provided blinded
leafIndexin theWorldIDRegistry. - A final Uniqueness Proof $\pi_2$ which ensures at least the following constraints:
- The same constraints of the query proof are evaluated.
- Correct OPRF evaluation on
leafIndex, i.e. the generated nullifier is correct for the committed public keys from each OPRF node. - Request is signed by a public key that is registered for the
leafIndexin theWorldIDRegistry(user authentication). - The Credential was issued for this World ID, i.e. the Credential’s
submatches the blindedleafIndexof the user. - The Credential used in the proof is signed by the Issuer (through the committed key in the
CredentialSchemaIssuerRegistry). - Credential is not expired.
- Credential meets the minimum genesis_issued_at constraint provided by the RP.
- Signal and nonce provided by the RP as public inputs are committed.
- Potential future constraints may include: integrity attestation of device, enforcing the expiration of actions, credential specific checks, etc.
- The query proof $\pi_1$ which proves to the OPRF Nodes that the request is properly authorized by the user. This ZKP proves the request is signed by a public key which is registered for the particularly provided blinded
-
Oblivious Nullifier Pool. The Oblivious Nullifier Pool is a separate service which offers Private Intersection Retrieval and keeps track of used nullifiers. Its function is simply to keep a flat list of used nullifiers such that an authenticator can query if a nullifier has been used before sharing it (and the related $\pi_2$) with an RP if it has been used before. The list is flat (as the nullifier is already unique per-RP-per-action-per-user) relying on the collision-resistance property of the hash function used in the Protocol.
- This system ensures that nullifiers can’t be misused to create long running identifiers. As their name suggests, a nullifier is one-time use.
- The term oblivious is used to refer to the fact that this map is queried in a way where the servers serving such requests cannot learn which records where accessed and hence be able to compromise the user’s privacy.
- The main limitation of the nullifier pool is performance at scale. One option is to shard the pool, making trade-offs of anonymity set size vs. performance. This is still in research.
- Initially, this pool will only be used for actions that have a running period longer than a predefined threshold. This is to solve for scaling issues as this system grows.
-
Blinded subjects. To prevent correlation of users even among issuers, or in case of leaked credentials, the subjects of the credentials are blinded.
- When requesting a new credential from an issuer, the user generates a blinding factor using the OPRF nodes, $\texttt{subjectBlindingFactor}=H_k(\texttt{issuerSchemaId} \mid\mid \texttt{leafIndex})$.
- The user then hashes the blinding factor with their
leafIndexto compute thesubclaim of the credential. This value is what issuers include in the credential. - When a proof is presented, the
subjectBlindingFactoris used within the Uniqueness Proof circuit to ensure the credential is issued to the right user. The blinding factor acts as entropy to prevent correlation, but the rightleafIndexas provided in the circuit input must match correctly.
Registries
- World ID Registry
- Each user can grant access to multiple different keys to interact with their World ID. The Authenticator proves control inside a ZKP to prevent long-lived identifiers.
- In order to not leak the user, this needs to be in some structure that allows inclusion proofs. This is accomplished with Incremental Merkle Trees.
- Each Authenticator registers two keys in the registry. This is done to enable performant operations both on-chain and on zero-knowledge circuits.
- An on-chain key which is an elliptic curve key on the
secp256k1curve is used to authorize on-chain operations on the contract (e.g. adding an authenticator, removing an authenticator, etc.). The public key is simply represented as an Ethereum address. - An off-chain key which is an elliptic curve key on the
BabyJubJubcurve is used to sign requests for zero-knowledge proofs. The public key (represented as a curve point) is emitted on-chain and committed to in the contract.
- An on-chain key which is an elliptic curve key on the
- Relying Party Registry
- Each RP needs to commit to their authorized public key on the public registry, such that this can be verified in the request proof $\pi_1$ by each queried OPRF node.
- Registering an RP is a public action that anyone can take, but this requires paying a one-time registration fee (see Registration Fees below).
- In order to allow for decentralized application creation and registration, the RP Registry will be extended and restrictions further lifted in the future, but for this initial version the following applies:
- At launch, only one authorized key is allowed per RP. This will be extended in the future.
- Credential Schema Issuer Registry
- It’s a simple registry where Issuers register for each of their credential types a schema and an authorized signatory and get issued an
issuerSchemaId. This ID represents the combination of an (issuer, schema). For example: (Tools For Humanity, Orb credential). - The
issuerSchemaIdis included in the credential and is verified as part of all Proofs. When generating and verifying proofs, the signature of a credential is verified against the public key registered in the contract. - Registering an Issuer Schema also requires paying a one-time registration fee (see Registration Fees below).
- It’s a simple registry where Issuers register for each of their credential types a schema and an authorized signatory and get issued an
Registration Fees
Both the Relying Party Registry and the Credential Schema Issuer Registry charge a one-time registration fee. The fee infrastructure serves as a permissionless rate-limiting mechanism — an alternative to restricting registration to authorized callers — ensuring that registration remains open to anyone while preventing abuse.
Why the fee exists. Registering an RP or an Issuer Schema triggers the initialization of an OPRF key via a multi-round distributed key generation ceremony across the OPRF Nodes. This is a computationally expensive operation with real infrastructure cost. The registration fee is sized to cover the cost of OPRF key generation and storage for at least approximately one year.
How it works.
- The fee is paid in a configurable ERC-20 token via
safeTransferFromat the time of registration, before OPRF key generation begins.
Future: per-request fees. The registration fee described here covers only the one-time cost of onboarding. A separate per-request fee — enforced by OPRF Nodes as a proof-of-payment requirement during nullifier generation — may be introduced in a future Protocol release (4.1 or 4.2). See Future Proofing Notes for details.
Recovery
Recovery is introduced for the case where access to all authenticators (or their keys) is lost. Recovery enables users to gain back control of their World ID by registering a new Authenticator and revoking lost ones. A Recovery Agent may be designated by each user, with a special role to perform the recoverAccount operation. This special role can recover a user’s World ID and nothing else, and conversely only these agents can perform a recoverAccount operation.
Important
This section defines Recovery as a Protocol primitive. Each Recovery Agent will provide its own set of specifications for how they provide World ID recovery.
- A
recoverAccountoperation is a function defined in theWorldIDRegistrycontract which receives a signed message from the Recovery Agent with a new authenticator public key. The operation results in the new authenticator being registered and all the old authenticators being revoked. The historical state of authenticators is naturally available on-chain. - The signed message supports ERC-1271, which means it can come from a smart contract that sets its own rulesets for how recovery can be performed. For example, a Recovery Agent may require signatures from multiple parties to allow a recovery. Furthermore, this enables that a Recovery Agent can be defined as an abstract entity governed by its own smart contract.
- Importantly, recovery will only be possible for users in the new Protocol. Fundamentally, there’s no way to recover previously lost World IDs because it’s a secret that has been lost. Some fallbacks are being introduced to mitigate this, see Migration Considerations for details.
- Designating a Recovery Agent is entirely optional and can be updated at any time.
- Once a World ID account has been recovered, new Credentials can be requested to each relevant Issuer. Each Issuer can define their own set of rules for what is needed to recover their Credential.
- Future Consideration. In the future, a new feature could be introduced where a World ID is used as a signer on something else. For example, a World ID signature could be used as a factor for recovering a cryptocurrency wallet. While this functionality builds on top of the Recovery primitive, it is independent of this primitive and not in its scope.
Session Proofs
RPs can create sessions for their app to ensure that it’s still the same World ID interacting with them across multiple interactions. Session Proofs intentionally allow the RP to link multiple interactions in their app to the same World ID. Sessions require the RP to store a sessionId. A sessionId can be created as part of a request for a Uniqueness Proof (see Binding Uniqueness Proofs to a Session), which binds the sessionId to a nullifier, or standalone without uniqueness binding. Potential use cases include:
- Credential upgrade: A user verified previously with one credential and now wants to prove using another one (e.g. unlocking additional benefits). Important Note. While this can be used to prove a new Credential belongs to the same World ID, the implications must be carefully considered when it comes to uniqueness. Uniqueness sets are independent, e.g. users may have both a PoH and a government document Credential, but this doesn’t mean that by accepting both as an RP you can get guarantees that only a single human is behind each. A user may choose to obtain a PoH Credential and a document Credential in different World IDs.
- Credential expiration check: A user previously enrolled with one Credential; periodically,the RP wants to make sure the user’s Credential is still valid (for example not expired).
- (Future). RP-level Face Auth: Currently, Face Auth only ensures that the whoever produces the proof is the same person that received the Credential. However, for some applications an RP may want to make sure the same person is behind multiple interactions.
Session Proofs use the same zero-knowledge circuits as Uniqueness Proofs, but authenticators MUST clearly distinguish them to users since they involve a reusable identifier that can link interactions. Instead of a nullifier, Session Proofs return a sessionNullifier which is required for verification but does not provide the same uniqueness guarantee (see below on sessionNullifier).
Session Proofs without uniqueness binding work in the following manner:
- An RP requests an authenticator to create a session.
- The authenticator provides a
sessionId, together with an initial session proof that proves that thesessionIdis well formed. A unique identifier bound to the user’s World ID for that RP. - The RP stores this
sessionIdalongside their account for the user. - For subsequent interactions, the RP includes the
sessionIdin proof requests. The user can then generate a Session Proof to prove they have the same World ID. Different proofs over time with the samesessionIdmay use different credentials. - A
sessionIdis generated as outlined below, whereris computationally indistinguishable from random.
sequenceDiagram participant o as OPRF participant a as Authenticator participant rp as RP critical initial/enrollment rp ->> a: Initial session request a->>a: Generate oprf_seed locally (CSPRNG) a->>o: r=OPRF(rpPublicKey, DS_C || leafIndex || oprf_seed) a->>a: Compute C = H(DS_C || leafIndex || r) a->>a: sessionId = encode(C, oprf_seed) a ->> rp: sessionId + proof (see below) end rp->>a: session proof request (incl. sessionId) alt [Recover r from sessionId.oprf_seed] a->>o: r=OPRF(rpPublicKey, DS_C || leafIndex || sessionId.oprf_seed) end a->>a: C'=H(DS_C || leafIndex || r) as public output of proof a->>a: check if sessionId == C' (in ZK-circuit) a->>rp: proof + sessionNullifier rp->>rp: verify proof (checking sessionId == C' in verifier contract)
Recovering r for subsequent Session Proofs. The OPRF is deterministic: the same input and key always produce the same output. This means r can be re-derived at any time by calling the OPRF nodes with the original oprf_seed (stored in sessionId). Caching r is an optimization, not a requirement. The OPRF call to derive r and the OPRF call to derive the nullifier can be made in parallel.
Session Nullifiers
- A
sessionNullifieris used for verifying Session Proofs. It must be passed to the verification contract. Internally, thesessionNullifierimplements custom encoding on the Authenticator and on theWorldIDVerifiercontract. - The raison d’être is simply to allow usage of the same ZK circuit as for Uniqueness Proofs. Reducing the number of circuits is currently a priority because of the size of the circuits needed to be bundled in Authenticator clients. As World ID moves to a different proving system, this type will no longer be required.
- Session Proofs use a randomized
actionas circuit input. This randomizedactionensures the circuit’s nullifier output is unique per proof, preserving the one-time use property. It is verified internally within the circuit. It does not affectrderivation.
Binding Uniqueness Proofs to a Session
- A Uniqueness Proof request may set the
sessionIdfield to"create"to atomically mint a session and bind the proof to it. The protocol verifies in-circuit that the session and the nullifier belong to the same World ID. The flow is outlined below. Binding a Uniqueness Proof to an already existingsessionIdis not supported. A session is either created together with the uniqueness proof, or it carries no uniqueness binding at all. - The blinding factor
rof the mintedsessionIdis returned to the Authenticator for caching; as for session proofs it can always be re-derived from theoprf_seed. - Verifiers MUST check bound proofs against the session’s commitment. With the session commitment set to
0the proof is valid but unbound. On-chain, the dedicatedverifyWithSession()entry point does this (it rejectssessionId == 0). The convenienceverify()entry point pins the signal to0and rejects bound proofs, so binding is explicit in both directions. - Binding one
sessionIdto Uniqueness Proofs under different actions intentionally links those actions to the same World ID; Authenticators MUST clearly surface this to users.
sequenceDiagram participant rp as RP participant a as Authenticator participant o as OPRF Nodes participant v as Verifier rp->>a: Signed Uniqueness Proof request (action + sessionId = "create") a->>a: Generate oprf_seed a->>o: Derive session blinding factor r a->>a: sessionId = encode(H(DS_C || leafIndex || r), oprf_seed) par Session binding a->>a: Constrain sessionId.commitment to the user's leafIndex and Uniqueness a->>o: Derive nullifier for (leafIndex, rpId, action) end a->>a: Generate final proof with sessionId.commitment as a public signal a->>rp: proof + nullifier + sessionId rp->>v: verifyWithSession(..., sessionId.commitment, proof) v->>v: Verify the non-zero session commitment and proof v-->>rp: Valid session-bound Uniqueness Proof rp->>rp: Verify nullifier uniqueness
Web-based Authenticator Provider
To allow for an improved user experience, a reference browser-based Authenticator provider is being introduced. This app provides (currently limited) World ID functionality but without leaving the browser.
- At a high-level, it allows usage of a World ID. The user can generate proofs in their browser, and this is particularly useful for when working on other devices (such as desktop) or on non-native apps.
- Whenever an RP requires a user’s World ID proof, they can simply redirect the user to the web app (handled automatically by common SDKs like ID Kit). The user authenticates with their passkey, generates the proof in their browser and passes it back to the RP.
- Further documentation on the architecture of the reference web-based Authenticator provider will be published in the https://github.com/worldcoin/web-authenticator repository.
- Credential Enrollment will not be supported in the initial release, but this may be introduced in the future.
Migration Considerations
At a high level, every user and RP will need to migrate to the new Protocol. Detailed migration paths will be published in the Developer docs.
Privacy Considerations
-
The Protocol, via the Oblivious Nullifier Pool enforces that nullifiers cannot be generated more than once (as long as authenticators are properly implemented), which prevents long running user tracking, increasing the privacy from the previous protocol version.
-
In adversarial scenarios, these are the most relevant privacy considerations,
Attack scenario World ID ≤3.0 World ID 4.0 (2025) Compromised user’s secret ⚠️ Potentially reveals all past activity if the attacker knows the public app IDs and actions. ✅ Cannot reveal past activity on its own Malicious RP ✅ A malicious RP on its own doesn’t reveal past activity ✅ A malicious RP on its own doesn’t reveal past activity Malicious OPRF node N/A ✅ Cannot compromise user’s past activity Collusion of threshold OPRF nodes N/A ⚠️ Could reveal user’s past activity (but requires having nullifiers to match against, e.g. through RP collusion) Compromised user’s secret + RP collusion ⚠️ Potentially reveals all past activity. Offline attack. ⚠️ Can reveal user’s past activity for that RP. Online attack (requires calls to OPRF nodes). Malicious Authenticator ⚠️ Potentially reveals all past activity if the attacker knows the public app IDs and actions. ⚠️ A malicious or compromised authenticator could reveal user’s activity. This could likely translate in users putting their trust in OSS authenticators. User coercion ⚠️ Potentially reveals all past activity. Offline attack. ⚠️ Can force reveal of non-expired nullifiers which in combination with RPs or other information may reveal past activity.
Other Risk Considerations
- Loss of State. The OPRF nodes work with an
m-of-nthreshold. If $n-m+1$ nodes loses their keys, the state of the Protocol will be lost, there won’t be a way to generate the correct nullifiers and the state of “consumed” or performed actions will be lost. - Censoring by OPRF nodes. OPRF nodes could reject generating nullifiers for users. It’s difficult for this to be a targeted attack because the
leafIndexis blinded, but they could censor specific RPs or actions. - Compromised OPRF nodes. Should a threshold of nodes’ key materials be compromised, or a threshold of nodes collude maliciously, users’ past and/or ongoing activity could be compromised, i.e. these nodes could arbitrary compute any nullifier because the
leafIndexes are known and the domain space is relatively low. To be useful, this would require matching nullifiers to actual usage with RPs, or knowing a user’sleafIndex, at least the second not being straightforward. This is a risk that will be progressively mitigated, and the following strategies are being researched for upcoming minor Protocol version updates:- Provable secure environments for OPRF nodes. Running OPRF nodes in attestable secure environments backed by secure hardware where the key material is securely stored and the execution provable. Given the nature of the computations of OPRF nodes, it is quite feasible that a plurality of enviroments and hardware providers could be supported. Authenticators would be able to verify attestations from OPRF nodes on every proof request.
- Increase on the number of OPRF nodes. Increasing the number of OPRF nodes and the threshold required to generate nullifiers reduces the risk of compromise. It is also very feasible to significantly extend the number of nodes over time. The cost of running a node is relatively low.
- Strict key management policies. RPs will be able to specify their policies for action duration such that keys are destroyed after their utility period has elapsed.
- Authenticator Risk. Aside from having access to the user’s credentials, an Authenticator must learn of a user’s raw
leafIndexto be able to generate Proofs. A malicious Authenticator can misuse this to track the user, even though that tracking cannot be correlated to nullifiers provided to RPs on its own. Different strategies to mitigate Authenticator risk are being explored. - Recovery Agent Risk. Should a user designate a Recovery Agent, this entity has a special permission that allows it to gain access to the user’s World ID, which could be misused. Beyond the explicit risk of a malicious Recovery Agent compromising a user’s World ID, users need to consider the different risks associated with different Recovery Agents based on how they perform authentication.
Future Proofing Notes (World ID 4.x future releases and beyond)
This is not a comprehensive list, but it outlines general topics that may be the target of upcoming Protocol releases which are not currently covered on this release.
- Face Auth. The signature on the credential by the issuer will allow for the introduction of a provable (still with camera sensor trust) FaceAuth check during proof generation.
- Authenticator policies and permission set. In this release, the
WorldIDRegistrycontains only a flat list of public keys, but this could be upgraded to be a more complex data structure that codifies explicit permissions for each key such that each authenticator can have a different policy for each action (e.g. add an authenticator vs. generating a proof), and this can be publicly verified. - Client-side proving. With the introduction of client-side proving and later potentially other types of proofs (e.g. age attestation), it will be possible to combine Uniqueness Proofs with additional attestations.
- Issuers having the ability to revoke Credentials.
- Establishing trust between protocol parties (e.g. identifying a trustworthy Authenticator).
- Ability to merge multiple World IDs, which translates into an ability to rotate identifiers with each Issuer.
- On the RP registry. In the future, the RP trust could also be anchored in other existing mechanisms, such as their domain name (DNS). Furthermore, actions could also be publicly registered for auditability and enforcement.
- World ID as signer (or biometric recovery). Similar to how recovering a World ID is possible because PoH AMPC allows for the addition of a new authenticator, it’ll be conceptually possible that a World ID could be a recovery signer for other accounts.
- Man-in-the-middle (MITM) proof phishing protection. While the attack vector of proof phishing is reduced with the introduction of the RP registry, it may still be possible to MITM proofs. Further protection mechanisms are required to mitigate this.
- In the future, credentials could be authenticator-specific, where issuers sign them for use with a specific authenticator only.
- Per-request fee gates may be introduced in the future (targeting 4.1 or 4.2) such that the OPRF Nodes receive a proof of payment as part of the request to generate a nullifier. This is distinct from the one-time registration fee already in place (see Registration Fees).
- Supporting cryptographic operations that enables secrets that can live in secure hardware and never be exported.
World ID 4.0 Trusted Setup
How a trusted setup works
World ID 4.0 uses a Groth16 Phase 2 trusted setup ceremony for its zk-SNARK circuits. In this process, multiple participants each add their own randomness contribution (often called “toxic waste”) to circuit-specific setup artifacts (.zkey), and each contribution is publicly verifiable as part of a transcript. Security relies on at least one participant contributing honestly and permanently deleting their secret randomness.
Why we are doing this for World ID 4.0
The World ID 4.0 ceremony is for the Circom circuits part of the oprf-service stack:
OPRFQueryProofOPRFNullifierProofOPRFKeyGenProof13OPRFKeyGenProof25OPRFKeyGenProof37
These circuits require Groth16 proving keys, which are produced via a Phase 2 setup. The trusted setup is therefore a required part of preparing production-grade proving artifacts for the World ID 4.0 protocol.
How to contribute
Note
The World ID 4.0 trusted setup ceremony is closed. New contributions are no longer accepted.
The @worldcoin/world-id-trusted-setup-cli package was built as a fork of PSE’s p0tion for organizing a trusted setup ceremony with minimum work needed to be done by the contributors. During the active ceremony, contributors used the following flow:
- Authenticate with GitHub device-flow OAuth (used by the CLI and for ceremony identity/attestation flow):
npx @worldcoin/world-id-trusted-setup-cli auth
- Run the contribution command to join an open ceremony and compute/upload your contribution (<10 min on average):
npx @worldcoin/world-id-trusted-setup-cli contribute
If you had any issues contributing to the ceremony, please contact our ceremony coordinator dcbuilder on Telegram.
How it was
Note
If you want to learn more about the trusted setup, watch the rendered explainer video: World Chain post.
Contribution totals (excluding the coordinator finalization contribution) are documented in this summary tweet by @dcbuilder:
OPRFKeyGenProof13:113OPRFKeyGenProof25:111OPRFKeyGenProof37:105OPRFNullifierProof:104OPRFQueryProof:104
Full ceremony artifacts, verification transcripts, the explainer video, manim source, and contribution attestations
are stored on the trusted-setup orphan branch to keep the main branch lightweight.
WIP-100: Cryptographic Primitives for World ID Protocol
1. Abstract
This spec establishes the default cryptographic primitives used throughout the World ID Protocol (the “Protocol”) such as the default elliptic curve, field, signature scheme and hashing. It includes particularly important constraints and validations which are security-critical.
2. Motivation
The Protocol is heavily reliant on different cryptographic primitives to uphold privacy and security. The correct definition and implementation of such primitives is essential for the Protocol operation. Furthermore, the spec documentation permits multiple implementations to exist in an interoperable way.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
3.1 Field
Unless otherwise noted, all circuits MUST be arithmetised over $F_q$, the scalar field of BN254, which is also the base field of BabyJubJub. All arithmetic MUST be done over $F_q$, except where a scalar of the BabyJubJub group is required, in which case arithmetic MUST be done over $F_r$, BabyJubJub’s own scalar field.
-
$F_q$ (
Fq) is the prime field with modulus$q = 21888242871839275222246405745257275088548364400416034343698204186575808495617$
-
$F_r$ (
Fr) is the prime field with modulus $r$, the order of BabyJubJub’s large prime-order subgroup (see Curve):$r = 2736030358979909402780800718157159386076813972158567259200215660948447373041$
Throughout the Protocol and this spec, the arkworks crates naming convention is generally used. This non-normative table is included for reference for the avoidance of doubt.
| Attribute | World ID Protocol | ark-babyjubjub | OPRF paper | EIP-2494 |
|---|---|---|---|---|
| BN254 scalar field, i.e. BabyJubJub’s base field | $F_q$ | Fq | $\mathbb{F}_p$ | $\mathbb{F}_r$ |
| Modulus of the Base Field | $q$ | Fq::MODULUS | $p$ | $r$ |
| BabyJubJub’s scalar field | $F_r$ | Fr | $\mathbb{F}_q$ | — |
| Prime subgroup order (modulus of Scalar Field) | $r$ | Fr::MODULUS | $q$ | $l$ |
| Curve order | $n$ | — | $n$ | $n$ |
| Cofactor | $h$ | COFACTOR | $h$ | $h$ |
3.2 Curve
BabyJubJub is defined in EIP-2494, which specifies three isomorphic forms, the Protocol uses the twisted Edwards form $E$ over $F_q$ given by
$$168700,x^2 + y^2 = 1 + 168696,x^2 y^2$$
All group operations, including key generation, signatures and scalar multiplication, MUST use this form. In particular the reduced twisted Edwards form, used by the ark-ed-on-bn254 crate, is a different curve and MUST NOT be substituted; ark-babyjubjub implements the form above.
$E$ has order
$$n = 21888242871839275222246405745257275088614511777268538073601725287587578984328$$
which factors as $n = h \cdot r$ with cofactor $h = 8$.
The base point of the Protocol is $G$ (ark_babyjubjub::EdwardsConfig::GENERATOR), of order $r$:
$$G = \left(5299619240641551281634865583518297030282874472190772894086521144482721001553,\ 16950150798460657717958625567821834550301663161624707787222815936182638968203\right)$$
3.3 Hashing
Unless otherwise noted, hashing is done via a Poseidon2 permutation over $F_q$. $P_t$ denotes the Poseidon2 permutation on $F_q^t$ with the $x^5$ S-box and the round counts below; it is a permutation, not a hash. The hash constructions built from it are defined in the subsections that follow.
| $t$ | $R_F$ (external rounds) | $R_P$ (internal rounds) |
|---|---|---|
| 2 | 8 | 56 |
| 3 | 8 | 56 |
| 4 | 8 | 56 |
| 8 | 8 | 57 |
| 16 | 8 | 57 |
Round constants and the external/internal matrices MUST be those produced for BN254 at the given $t$ by the reference Poseidon2 parameter generation script. Widths not listed above MUST NOT be used.
Any use of Poseidon2 MUST follow the mode and specifications of this section (“Hashing”) unless otherwise noted (e.g. Merkle Inclusion Proof).
3.3.1 Domain separators
A domain separator ds is the $F_q$ element derived from an ASCII label by big-endian octet-string-to-integer conversion, reduced mod $q$. Labels MUST be at most 31 bytes, so that distinct labels cannot collide under the reduction.
Each label MUST be bound to exactly one (construction, $t$, $k$) triple. For example, EdDSA Signature is allocated to the challenge hash at $t = 8$, $k = 5$, giving
$$\texttt{ds}_{\texttt{EdDSA Signature}} = 360302137480307891234917541314130533$$
3.3.2 Fixed-arity hash $H_t$
$H_t$ is the hash used wherever the number of inputs is known in advance. For a domain separator ds and inputs $m_1, \dots, m_k$ with $k \le t - 1$:
$$H_t(\texttt{ds}; m_1, \dots, m_k) := P_t([\texttt{ds}, m_1, \dots, m_k, 0, \dots, 0])[1]$$
That is, ds MUST occupy the capacity element at state index 0, the inputs occupy state indices 1..k, any remaining state elements are zero, and the digest is state element 1 of the permutation output.
$k$ MUST be constant for a given ds. The zero padding is not length-encoded, so $H_t(\texttt{ds}; m_1)$ and $H_t(\texttt{ds}; m_1, 0)$ are the same digest; binding $k$ to ds is what prevents cross-arity collisions. A construction whose input count varies at runtime MUST NOT use $H_t$.
Caution
For emphasis, failing to follow the requirement on a constant $k$, or failing to place a fixed
dsat state index0is catastrophic. A varying $k$ gives trivial collisions and second pre-images. An input-controlled state index0allows an attacker to invert the permutation and produce pre-images for any digest.
3.3.3 Variable-length hash $H_\text{var}$
$H_\text{var}$ is the hash used for byte strings whose length is not fixed, where $H_t$ cannot be applied. It is a sponge over $P_{16}$: rate 15 across state indices 0..14, capacity at index 15, squeezing from index 0. Note this is the opposite convention to $H_t$, which places the capacity at index 0; $H_\text{var}$ places it last to follow SAFE.
The input is split into 31-byte chunks, each converted to an integer in big-endian order (the final chunk is not padded); the chunks are absorbed 15 at a time, applying $P_{16}$ after each group. The capacity is initialized with a SHA3-256 tag over the domain separator label and the input and output lengths, following SAFE. Because the length is bound into the capacity, inputs of different lengths cannot collide by padding, the weakness $H_t$ avoids by fixing $k$ instead.
3.4 Hash to Curve
Some constructions, in particular the OPRF evaluation of the OPRF paper, require a map $H_1: F_q \mapsto E$ that takes a field element to a curve point whose discrete logarithm with respect to $G$ is unknown. The Protocol follows RFC 9380 (“Hashing to Elliptic Curves”) instantiated for BabyJubJub, with the RFC’s expand_message step replaced by Poseidon2.
The default is $\mathrm{EncodeToCurve}$, RFC 9380’s encode_to_curve. Its output lies in the prime-order subgroup and has unknown discrete logarithm, but is not uniformly distributed in that subgroup: Elligator 2 reaches only about half of the curve points. This is sufficient wherever the requirement is an unknown discrete logarithm rather than uniformity, which covers every present use in the Protocol.
$\mathrm{EncodeToCurve}$ composes four steps, each defined below:
$$\mathrm{EncodeToCurve}(x) := \mathrm{ClearCofactor}\bigl(\mathrm{RationalMap}\bigl(\mathrm{Elligator2}\bigl(\mathrm{HashToField}(x)\bigr)\bigr)\bigr)$$
3.4.1 Notation
$\texttt{sgn0}$ is RFC 9380 §4.1 sgn0: the least significant bit of the canonical integer representative of its argument. $\texttt{inv0}$ is RFC 9380 §4 inv0: $\texttt{inv0}(0) = 0$ and $\texttt{inv0}(y) = y^{-1}$ otherwise. $\texttt{isSquare}$ is RFC 9380 §4 is_square and is true for zero. $\texttt{sqrt}$ returns an arbitrary one of the two roots; the sign is fixed explicitly in step 19 of Elligator 2, so implementations MUST NOT rely on $\texttt{sqrt}$ selecting a canonical root. $\texttt{CMOV}(f, g, c)$ returns $g$ if the condition $c$ holds and $f$ otherwise.
$E_M$ is the Montgomery form of BabyJubJub from EIP-2494, birationally equivalent to $E$:
$$K,t^2 = s^3 + J,s^2 + s, \qquad J = \frac{2(a+d)}{a-d} = 168698, \qquad K = \frac{4}{a-d} = 1$$
Since $K = 1$, the RFC’s constants reduce to $c_1 = J/K = 168698$ and $c_2 = 1/K^2 = 1$, and the final scalings by $K$ are no-ops. Implementations MAY drop them.
3.4.2 Hash to field
The label OPRF_HashToField_BabyJubJub is allocated to this construction at $t = 3$, $k = 1$. Writing $\texttt{ds}_\text{H2F}$ for the resulting domain separator,
$$\texttt{ds}_\text{H2F} = 32627786498498119128812045057993354633158048678109587794777765218$$
$$\mathrm{HashToField}(x) := H_3(\texttt{ds}_\text{H2F};\ x)$$
RFC 9380 §5 obtains a field element by expanding to $\lceil (\log_2 q + k)/8 \rceil$ bytes and reducing mod $q$, to bound the distance from uniform. Poseidon2 is a permutation over $F_q$, so $H_3$ already outputs an $F_q$ element with no modular bias, and the oversampling step is omitted.
3.4.3 Map to curve (Elligator 2)
$\mathrm{Elligator2}: F_q \mapsto E_M$ is RFC 9380 §6.7.1 with $Z = 5$, the value returned by the RFC’s find_z_ell2 for $F_q$ (RFC 9380 App. H.3). $Z$ is a non-square in $F_q$.
Given $u$, return $(s, t)$:
$$ \begin{aligned} &1..2.\ \ &&\texttt{tv1} = Z \cdot u^2 \ &3..4. &&\texttt{tv1} = \texttt{CMOV}(\texttt{tv1},\ 0,\ \texttt{tv1} = -1) \ &5..7. &&x_1 = -c_1 \cdot \texttt{inv0}(\texttt{tv1} + 1) \ &8..11. &&g_{x_1} = \bigl((x_1 + c_1),x_1 + c_2\bigr),x_1 \ &12. &&x_2 = -x_1 - c_1 \ &13. &&g_{x_2} = \texttt{tv1} \cdot g_{x_1} \ &14. &&e_2 = \texttt{isSquare}(g_{x_1}) \ &15..16. &&x = \texttt{CMOV}(x_2,\ x_1,\ e_2), \quad y^2 = \texttt{CMOV}(g_{x_2},\ g_{x_1},\ e_2) \ &17..19. &&y = \texttt{sqrt}(y^2), \quad y = \texttt{CMOV}(y,\ -y,\ e_2 \oplus \texttt{sgn0}(y)) \ &20..22. &&(s, t) = (x \cdot K,\ y \cdot K) \end{aligned} $$
Step 3 handles $\texttt{tv1} = -1$, for which $x_1$ is undefined; steps 14–16 select the branch that yields a square.
3.4.4 Rational map
$\mathrm{RationalMap}: E_M \mapsto E$ is RFC 9380 App. D.1. Given $(s, t)$, return $(v, w)$:
$$\texttt{tv} = \texttt{inv0}\bigl((s+1),t\bigr), \qquad v = \texttt{tv} \cdot (s+1) \cdot s, \qquad w = \texttt{CMOV}\bigl(\texttt{tv} \cdot t \cdot (s-1),\ 1,\ \texttt{tv} = 0\bigr)$$
The $\texttt{tv} = 0$ branch maps the exceptional points of the rational map to the identity $(0, 1)$ of $E$, so the map is total.
3.4.5 Clear cofactor
$$\mathrm{ClearCofactor}(Q) := h \cdot Q = 8 \cdot Q$$
Implementations SHOULD compute this as three doublings. The result lies in the prime-order subgroup, and the output of $\mathrm{EncodeToCurve}$ MAY therefore be consumed by templates that require a subgroup element without a further subgroup check.
Note that $\mathrm{ClearCofactor}$ can return the identity — for the $\mathrm{RationalMap}$ exceptional points above, and for any $Q$ of order dividing 8. Callers for which the identity is not an acceptable $H_1$ output MUST reject it explicitly.
3.5 Signatures
The default signature scheme for the World ID Protocol is an Edwards-curve Digital Signature Algorithm (EdDSA) over the twisted Edwards form $E$ of BabyJubJub, referred to as BabyJubJub-EdDSA-Poseidon2. It is the scheme given in the OPRF paper.
It follows the structure of RFC-8032 but is not an RFC 8032 ciphersuite: RFC 8032 defines only Ed25519 and Ed448, each with a fixed curve and a fixed hash function, and this scheme replaces both.
BabyJubJub-EdDSA-Poseidon2uses:- $F_q$ as its finite field;
- $E$ as its elliptic curve;
- the base point $G$ as defined in Curve, of order $r$;
- Poseidon2 as its challenge hash, as defined in Hashing;
- BLAKE3 in XOF mode for the derivation of the secret scalar $sk$ and for deterministic nonce derivation. Implementations MUST NOT substitute Poseidon2 here, nor SHA-512 as RFC 8032 would prescribe. Implementors SHOULD follow the recommendations in RFC 8032 for key generation.
- Message. Messages are elements of $F_q$. A message not natively encodable in $F_q$ MUST first be lowered to $F_q$ with a construction from Hashing.
- Challenge. $e = H_8(\texttt{ds}_{\texttt{EdDSA Signature}}; R_x, R_y, pk_x, pk_y, M)$, where $pk$ is the signer’s public key. $e$ MAY be used as a full-field scalar without reduction, since reduction mod $r$ is a no-op on the prime-order subgroup in which $pk$ is required to lie.
- Signature. The signature is $\sigma = (R, s)$. $R$ is $r’ \cdot G$, where $r’$ is RECOMMENDED to be derived by hashing the deterministic nonce concatenated by the message as in RFC 8032, and $s = r’ + e * sk$.
- Public Key. The public key $pk$ is derived as $sk \cdot G$.
3.5.1 Verification
A signature $\sigma = (R, s)$ on a message $M$ under a public key $pk$ is valid iff all of the following hold.
- Canonical encoding. $pk$ and $R$ MUST decode from canonical field elements, per Public Key Representation.
- Canonical Scalar. All $s$ scalars MUST fulfill $s \in {0, \dots, r-1}$ to prevent signature malleability (i.e. a reduced scalar).
- $pk$ in the prime-order subgroup. $pk$ MUST satisfy the curve equation, MUST lie in the prime-order subgroup, and MUST NOT be the identity. This also rejects the small-order points.
- $R$ on curve. $R$ MUST be a point satisfying the BabyJubJub curve equation because addition is only complete for on-curve points. The point is NOT REQUIRED to lie in the prime-order subgroup for security, although conforming clients MUST produce points that lie in the prime-order subgroup.
- Cofactored verification equation. The signature MUST meet the cofactored verification equation $h\left(sG - R - e \cdot pk\right) = \mathcal{O}$, with $h = 8$. See Chalkias, K. et al.. Cofactorless verification MUST NOT be used; the two accept different sets of signatures.
3.5.2 Encoding
A signature is encoded as 64 bytes: $R$ in the compressed point encoding of Public Key Representation, followed by $s$ as a canonical little-endian $F_r$ element.
3.6 Discrete Logarithm Equality Proof
A discrete logarithm equality (DLogEq) proof is a Chaum–Pedersen sigma protocol, made non-interactive by the Fiat–Shamir transform with Poseidon2 as the challenge hash. It is the scheme given in the OPRF paper.
It proves knowledge of a scalar $x \in F_r$ such that
$$A = x \cdot D \quad\text{and}\quad C = x \cdot B$$
for public points $A, B, C, D \in E$, without revealing $x$. This is what gives the OPRF public verifiability: the OPRF node publishes $A = x \cdot D$ as its public key and proves that the same $x$ produced the evaluation $C = x \cdot B$ on the client’s blinded query $B$.
$D$ is a parameter of the scheme, not fixed by it. Every present use in the Protocol instantiates $D = G$, and an implementation MAY hard-code it, but the challenge hash MUST bind $D$ regardless, so that a prover cannot move the statement to a different base.
3.6.1 Proving
Given the witness $x$ and the base $B$, the prover:
- samples a nonce $\rho \in F_r$ uniformly at random;
- computes the commitments $R_1 = \rho \cdot D$ and $R_2 = \rho \cdot B$;
- computes the challenge $e$ as defined in Challenge;
- computes the response $s = \rho + e \cdot x \in F_r$.
The proof is $\pi = (e, s)$. $\rho$ MUST be freshly sampled per proof and MUST NOT be reused or derived from public data: two proofs over distinct statements that share a $\rho$ reveal $x$.
3.6.2 Challenge
The label DLOG Equality Proof is allocated to this construction at $t = 16$ with $k = 12$ inputs, giving
$$\texttt{ds}_{\texttt{DLOG Equality Proof}} = 1523098184080632582082867317389990410064981862$$
$$e = H_{16}(\texttt{ds}{\texttt{DLOG Equality Proof}};\ A_x, A_y,\ B_x, B_y,\ C_x, C_y,\ D_x, D_y,\ R{1,x}, R_{1,y},\ R_{2,x}, R_{2,y})$$
The operand order is normative: the four statement points as $A, B, C, D$, then the two commitments as $R_1, R_2$, each point contributing $x$ then $y$.
$e$ is an element of $F_q$. As in Signatures, it MAY be used as a full-field scalar without reduction, since reduction mod $r$ is a no-op on the prime-order subgroup in which all four points are required to lie.
3.6.3 Verification
A proof $\pi = (e, s)$ for the statement $(A, B, C, D)$ is valid iff all of the following hold.
- Canonical Scalar. $s \in {0, \dots, r-1}$, as in Signatures.
- Statement points in the prime-order subgroup. Each of $A$, $B$, $C$, $D$ MUST satisfy the curve equation, MUST lie in the prime-order subgroup, and MUST NOT be the identity.
- Recomputed commitments. $R_1 = s \cdot D - e \cdot A$ and $R_2 = s \cdot B - e \cdot C$.
- Non-degenerate commitments. Neither $R_1$ nor $R_2$ is the identity.
- Challenge. $e$ equals the challenge recomputed over $(A, B, C, D, R_1, R_2)$ per Challenge, compared as $F_q$ elements.
A verifier MAY discharge check 2 for a given point outside the proof system, and a circuit implementing this verification MAY therefore omit it for a point that is a public input of the enclosing proof. In that case the check becomes an obligation on the enclosing verifier and MUST be documented as such at the circuit boundary.
4. Public Key Representation
The PublicKey type is introduced to represent public keys of used elliptic curves. Two representations are defined and MUST NOT be conflated:
- In-circuit: an affine point, i.e. a structure with two
Fieldelements $(x, y)$. Both coordinates MUST be carried explicitly; a circuit MUST NOT decompress a point. - Wire: a compressed point of 32 bytes, holding $y$ as a canonical little-endian $F_q$ element with the sign of $x$ in the most significant bit of the final byte. The sign bit is set when $x > -x$, comparing canonical integer representatives. Note: This is not equivalent to
sgn0.
Decompression recovers $x$ from $y$ via the curve equation and selects the root indicated by the sign bit. Deserialization MUST reject a non-canonical $y$, i.e. one that is not less than $q$. The identity point of the used BabyJubJub curve is (0,1) and is represented using the above rules.
This compressed encoding is not the iden3 BabyJubJub packing, and the two are not interchangeable.
5. Merkle Inclusion Proof
The Protocol uses Merkle trees to store membership and prove inclusion. The most important use of it is to store World ID account membership in the WorldIDRegistry. An account’s position in the tree is its leaf index.
5.1 Parameters
| Parameter | Value |
|---|---|
| Arity | 2 |
| Depth | 30 |
| Node compression | $\mathrm{Compress}$, below |
| Empty leaf | $0$ |
Leaf index 0 is reserved and MUST NOT be assigned.
5.2 Node compression
Nodes MUST be combined with the Davies–Meyer compression function
$$\mathrm{Compress}(x_L, x_R) := P_2([x_L, x_R])[0] + x_L$$
$t = 2$ leaves no state element free for a capacity, so $\mathrm{Compress}$ carries no domain separator and is the one exception to Domain separators. $\mathrm{Compress}$ MUST NOT be used outside a fixed-depth Merkle tree; see Security.
5.3 Inclusion proof
An inclusion proof for a leaf at index $i$ consists of $i$ together with the 30 sibling nodes $s_0, \dots, s_{29}$, ordered from the leaf level upwards. The root is recomputed as
$$v_0 = \mathrm{leaf}, \qquad v_{j+1} = \begin{cases} \mathrm{Compress}(v_j,\ s_j) & \text{if bit } j \text{ of } i \text{ is } 0 \ \mathrm{Compress}(s_j,\ v_j) & \text{otherwise} \end{cases}$$
where bit $j$ is the $j$-th least significant bit of $i$. The proof is valid if and only if $v_{30}$ equals the tree root. Verifiers MUST apply exactly 30 compressions and ensure the depth of the Merkle tree is 30 and $\forall i$, $0 < i < 2^{30}$ MUST hold.
6. Security
- Poseidon2 in compression mode. For a security argument of using Poseidon2 with state size 2 as a 2-1 compression mode, we refer to both section 3.1 of the Poseidon2 paper as well as the treatment of the construction in section 4.2 of the Griffin paper.
- Security Target. The Protocol defines a target security of 128 bits. BabyJubJub is the binding constraint, with $r \approx 2^{251}$. Poseidon2 and BLAKE3 are both parameterized for 128 bits.
- Assumptions. The discrete logarithm problem is hard in the order-$r$ subgroup of $E$; Poseidon2 over $F_q$, at the widths in Hashing, is collision- and preimage-resistant and behaves as a random oracle under Fiat–Shamir; BLAKE3 is a secure pseudorandom function and extendable-output function.
7. Backwards Compatibility
This spec is introduced after the initial release of the World ID 4.0 protocol, it is documenting design decisions previously defined in the 4.0 Specs and from the OPRF Whitepaper, so it’s not breaking backwards compatibility. From adoption onwards, the definitions in this spec become binding for the Query Proof and Nullifier Proof used in the World ID Protocol.
Appendix A1. Test Vectors
Field elements are decimal integers. Byte strings are hexadecimal; wrapped lines are concatenated. For ascending inputs of length $n$, byte $i$ is $i \bmod 251$.
A1.1 Domain separators
label = EdDSA Signature
ds = 360302137480307891234917541314130533
label = OPRF_HashToField_BabyJubJub
ds = 32627786498498119128812045057993354633158048678109587794777765218
label = DLOG Equality Proof
ds = 1523098184080632582082867317389990410064981862
label = CLAIMS_HASH_V1
ds = 1364962988938129392107510493566513
A1.2 Poseidon2 permutations
Each width uses the input state $[0, 1, \dots, t-1]$. All output elements are listed in state order.
t = 2
output[0] = 13120422956170837922441672802975889424559262309139960702680326932494325745547
output[1] = 5923567162677888564808904842769941181302763723060647224839027357562627386465
t = 3
output[0] = 5297208644449048816064511434384511824916970985131888684874823260532015509555
output[1] = 21816030159894113985964609355246484851575571273661473159848781012394295965040
output[2] = 13940986381491601233448981668101586453321811870310341844570924906201623195336
t = 4
output[0] = 786823568102245344938517132468097745676732687098822989626730198331658606391
output[1] = 16105493617470833344375945651585194737369509580406730765188791202038211593826
output[2] = 2169165722086073256768101917994796590773204847633762971322389403847680713675
output[3] = 20837792685223053096472825292260687493226094382304778455120670180090619921530
t = 8
output[0] = 13163567864211573827878829467860137302577760599598440387954761704438999762399
output[1] = 20455256474176316209572707628365862887207812418465031548192789068192434065861
output[2] = 21622031586696647398529562584873094656572287904668581566093346191656615936784
output[3] = 18320622048765136384409419776996464874987888500923344182439589703061890523284
output[4] = 19915468795157938233689963601267136400922725821760118753901600546477081024243
output[5] = 12383970660639123649548441396659012498414420037083153473614822644813849243474
output[6] = 9133088157465982496917058916696585316057943251337470087079495488316110895778
output[7] = 5020935059501715015422969097649999023750915432550677386523662686145648636517
t = 16
output[0] = 7129053404014098913941583447102076532611276040718594073862066403012892177215
output[1] = 5458683216916715697310099658604278457911373519210593239261146303695981710820
output[2] = 11764907654416682971926471140388165312909351793032868507449176373009888376893
output[3] = 17363012907147515824232626923071954964539976031233523938322583063167173991942
output[4] = 16754602647566413012759386310550362661092317428428132757066277153406453157400
output[5] = 10442131742273378767812305849732860137449534508695657144865044457198204305243
output[6] = 13315916208806700309353847107954103794241355430909228633658159683794835480566
output[7] = 14675611827802190925530581036356245293764500457751312643178429199155385431971
output[8] = 3800671750689110886099899395588427301982955036566905831860793275457528754896
output[9] = 863058427093450397617252284543198432424871511785791089866952153042503171268
output[10] = 16110421480974327191214802248220528120081914075253666769021797524181818259452
output[11] = 3050248777345249982082587219460801555485024010345812479213241978893548171998
output[12] = 8005144369031495385854140476761376792991595443174132540148616210767138457404
output[13] = 193712991007063517677674367979478243863141973963118958643316643360558925992
output[14] = 6765341258738133397733055933640609905610288576122407133007925535267189590216
output[15] = 6411743912316957490668095751870764077217660758836562678571866082387292213586
A1.3 Hash to field
This also tests $H_3$ with one input and one zero-padding element.
label = OPRF_HashToField_BabyJubJub
input = 1
output = 10409509318069101293316722480711595867625349071847922195261332397736272636435
A1.4 Variable-length hash
The label is CLAIMS_HASH_V1. Inputs are ascending bytes. Lengths 31 and 32 cover the chunk boundary; lengths 465 and 466 cover the permutation boundary. Empty input is invalid.
length = 31
digest = 12658306072862357948815044357476041793480954961275831694284480429814652585452
length = 32
digest = 7105501883046959224351664232028428089815911608118705425730076057190813037341
length = 465
digest = 13435504409435728907439603956434602577345452839959452028063546759536561347478
length = 466
digest = 15525893232164838192982873493581731321396161293199355856124601873305423262450
A1.5 Merkle tree
Compress(0, 0) = 15621590199821056450610068202457788725601603091791048810523422053872049975191
Compress(1, 2) = 6588139247708940112588203339651261153905233202198520634825199962343944922547
Define the empty-subtree roots by $z_0 = 0$ and $z_{j+1} = \mathrm{Compress}(z_j, z_j)$. Then $z_{30}$ is the empty tree root. The inclusion vector uses $s_j = z_j$ as the sibling path.
z_2 = 15180302612178352054084191513289999058431498575847349863917170755410077436260
z_30 = 15633048765234690365876053958277790002681834789256406938121001738966131111330
leaf = 42
index = 1
root = 21449849969959258301263701183300082902700436494585373070244441970359881620262
With the same root and sibling path, leaf 43 is invalid.
A1.6 Point encoding
$G$ is defined in Curve. The encodings of $G$ and $-G$ exercise both values of the sign bit.
G = 8b7d2d877a253c4b7733e1b91f05e0fcedf96bd11c2e572549b2a0f703727925
-G = 8b7d2d877a253c4b7733e1b91f05e0fcedf96bd11c2e572549b2a0f7037279a5
identity = 0100000000000000000000000000000000000000000000000000000000000000
A1.7 Signature
The deterministic signing procedure for this vector is:
- Expand the 32-byte seed with BLAKE3 XOF to 64 bytes.
- Copy the first 32 bytes to
a; seta[0] &= 0xf8anda[31] = (a[31] & 0x7f) | 0x40. Interpretaas a little-endian integer and reduce modulo $r$ to obtain $sk$. - Interpret the last 32 bytes as a little-endian integer and reduce modulo $r$ to obtain the nonce secret.
- Encode the nonce secret and message as 32-byte little-endian integers, concatenate them in that order, and expand with BLAKE3 XOF to 64 bytes. Interpret the result as a little-endian integer and reduce modulo $r$ to obtain $r’$.
- Compute $pk = skG$, $R = r’G$, the challenge $e$, and $s = (r’ + e \cdot sk) \bmod r$.
seed = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
message = 42
pk.x = 5743127613665812714027674485677763488557233128677239431720756512332055304469
pk.y = 6713952740716875958406247477052754272136198788949552789678520356076812341435
pk = bb9015f1309156fdfddcda5e5922f1ae565f6e5b2e78d4e35f884e036cf6d70e
R.x = 4471796480099078564607791866308147318722757043758328908648489482326053658209
R.y = 16743942090139358314901240752948685648557595749372215854958283055038688657910
s = 1707871037059365895665483114747551425359126542652147321192782334073340268810
signature = f601e481eec3c9fac20942eed731b5c815194fd1416a3f0c7948ffdf41bc0425
0a91c15ded66fde2ecec499b86fd56a7aad62d409391584a9c126b8de99ec603
e = 14790550837438996768258768677587167620081417436968300740169708289833632123084
The signature verifies for message 42 and fails for message 43. The challenge $e$ also satisfies the cofactored verification equation in Signatures.
The following verification-only vector has $pk = G$ and $R = G + (0, -1)$. It MUST be accepted by the cofactored equation; the cofactorless equation rejects it. Signers MUST still produce $R$ in the prime-order subgroup.
message = 42
e = 14107477924306369449585376693150860508714550631015857449267997894818346434634
s = 427326129406822435681373102365063578330480770223021153266919590076109569430
A1.8 EncodeToCurve
input = 2
output.x = 20419487629862769727627645368371427138888560013459229812372534124769971005828
output.y = 7631771008065059170045692780763031490103103796767657363493053446444837523289
input = 42
output.x = 1368536874988764403285491466492470225763829673979223271328990939656695174872
output.y = 5918944744409897789209151589310931911112404737084812644826989226820698253694
Inputs 2 and 42 exercise the square and non-square branches of Elligator 2, respectively. Both outputs are non-identity points in the prime-order subgroup.
A1.9 DLogEq proof
$D = G$ and $B = \mathrm{EncodeToCurve}(42)$ from the vector above. The fixed nonce is for this test only.
witness = 12345
nonce = 67890
A.x = 19099552327547260981542886231210125691902505931204088720746463491300185142606
A.y = 13276557205153692030187527501273228448057533426731746626187331221465573305487
C.x = 1704386023042037258303736539892861539707201163487540094949081990693465165451
C.y = 8768369809996482248064559070370438612828016885307150244744623529899299197157
R1.x = 7560514331452906482367540963526316341247740678202978210835422163029445477658
R1.y = 11610694160704858701950599566691828874575930603037515361903555914794501850944
R2.x = 3490405731880058419043956475882448859018810273867088182397477099060627470353
R2.y = 11594420891490965176904143623534974685648090668464922826887222168496451873187
e = 16671088874615503773909688665365363469394641783242006990890243365077833605068
s = 388554659608811743276554153883008978905828355147065499832325343644454488330
The proof $(e, s)$ verifies for $(A, B, C, D)$. Swapping $A$ and $C$ is invalid.
WIP-101: RP Request Authorization Method for Smart Contracts
1. Abstract
This spec introduces a method to verify Relying Party (RP) proof requests in the World ID Protocol through on-chain smart contracts. As some RPs may not have a backend and be simply a contract, this allows OPRF Nodes to answer “Does the RP authorize this request?” in order to process relevant OPRF requests such as for nullifier generation.
2. Motivation
Context: Proof requests from Relying Parties in the World ID Protocol MUST be signed in order to be recognized. This serves to ensure proofs are authorized for the intended recipient and reduce the attack surface in post-compromise scenarios for Authenticators.
A Relying Party MAY be a Smart Contract without a backend. When this is the case, having a private key to sign the request is impractical or even impossible in a secure way. Hence, we need a way for Relying Parties to authorize the request in a way compatible with Smart Contracts.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
In this document, “contract” and “smart contract” are used interchangeably.
This specification introduces both concerns of “Authorization” and “Validation” of Relying Party (RP) Proof requests. When an RP requests a proof without a smart contract, they construct the proof request and sign the data they constructed. If the RP does not have a backend or other trusted environment, they cannot control the inputs to the Proof request. Hence, the contract SHOULD implement validation rules for the request. For example, an RP should determine what is the maximum expiration period they should allow for Proof requests and enforce it during verification.
3.1 Contract Interface
- To be compatible with signature verification, a contract acting as an RP MUST conform to the
IWIP101interface below.verifyRpRequestMUST NOT modify state (declaredviewfor solc > 0.5) and MUST allow external calls. - On a valid request,
verifyRpRequestMUST return thebytes4magic value0x35dbc8de(the function selector forverifyRpRequest). Reverts otherwise. - The contract (implementer) MUST also adhere to ERC-165 and return
truefromsupportsInterfacefortype(IWIP101).interfaceId.
/**
* @dev Interface of the WIP-101 standard for World ID,
* RP Request Authorization Method for Smart Contracts.
*/
interface IWIP101 is IERC165 {
/**
* @dev The RP request is not valid. The code may be used to provide additional debugging information.
*/
error RpInvalidRequest(uint256 code);
/**
* @notice Verifies a World ID Proof Request is authorized by the RP.
* @dev Should return whether the RP request is valid and should be honored. The `rpId` is implicit in this request,
* any contract implementing this interface will be pointed to in the `RpRegistry`.
* @param version The version determines the format of the request.
* @param nonce Unique nonce for this request
* @param createdAt Creation timestamp of the request
* @param expiresAt Expiration timestamp specified for the request
* @param action Provided action for the request. Importantly, this is already a hashed
* action as a field element.
* @param data Arbitrary data useful for the verification.
* @return magicValue The expected magic value when the request is valid. Reverts otherwise.
*
* MUST return the bytes4 magic value 0x35dbc8de when function passes (function selector for verifyRpRequest).
* MUST NOT modify state (view modifier for solc > 0.5)
* MUST allow external calls
*/
function verifyRpRequest(
uint8 version,
uint256 nonce,
uint64 createdAt,
uint64 expiresAt,
uint256 action,
bytes calldata data
) external view returns (bytes4 magicValue);
}
3.2 Request Validation and Errors
- If the request is not authorized,
verifyRpRequestMUST revert. - When a request is invalid, implementers SHOULD revert with the explicit
RpInvalidRequesterror. - Callers MUST treat any return value other than the magic value
0x35dbc8deas an invalid request. - RPs MAY define any number of
codes inRpInvalidRequest. These codes are intended only for RP debugging, hence the RP can establish any format/definition for this attribute. - It is RECOMMENDED implementers ensure that any upgrades to validation logic are backwards-compatible for the duration of the maximum
expiresAtwindow.
3.3 OPRF Node Behavior
The verifyRpRequest method will be used by OPRF Nodes to verify an incoming proof request before generating the required output to construct a nullifier. When a contract compliant with this spec is deployed, its address can be set as a signer in the RpRegistry contract (e.g. through the updateRp function).
- OPRF Nodes MUST use this spec (WIP-101) verification if the
signerin theRpRegistryimplements ERC-165 fortype(IWIP101).interfaceId. - Following the main threat model of this spec, OPRF Nodes MUST pass to
verifyRpRequestthe sameactionvalue that will be used as input to the OPRF computation. - On an explicit
RpInvalidRequestrevert, OPRF Nodes MUST return the innercodeto the requester (for debugging purposes). - OPRF Nodes MAY enforce a timeout on the resolution of
verifyRpRequest. - Implementers SHOULD NOT rely on computationally expensive validation logic, so a call resolves within any node-enforced timeout.
- OPRF Nodes MAY check whether a contract is deployed at the registered
signeraddress, and MAY return a specific error code if a deployed contract is not WIP-101 compliant. - OPRF Nodes are NOT REQUIRED to verify signature via ECDSA if there is a contract deployed at the
signeraddress.
3.4 Signer Caching
- OPRF Nodes MAY cache an RP’s
signerfor performance. This cache may include the consideration on whether thesigneris WIP-101 compliant or not. - A cached
signerentry MUST NOT exceed a 30-day TTL. - OPRF Nodes MUST invalidate the
signercache on anyRpUpdatedevent emission from theRpRegistry.
3.5 Request Field Constraints
- The maximum length of
datais 8192 bits (1024 bytes); thedatafield MUST NOT exceed this length. - OPRF Nodes MAY enforce this maximum length and reject requests which exceed it.
3.6 Deployment
- Implementations of WIP-101 MUST be deployed on World Chain Mainnet (Chain ID:
480). - OPRF Nodes MUST only verify contract execution on chain ID
480.
Note: For RPs using other chains, proof verification MAY occur elsewhere, it’s only the request authorization that MUST happen on World Chain.
3.7 Additional Context (non-normative)
Version.
The version of the ProofRequest which is passed as version attribute to the verifyRpRequest function is currently defined in the RequestVersion enum.
Invalidating Signer Cache.
Note that RPs can trigger a cache invalidation of the signer by doing a no-op (or an actual) update with the updateRp method in the RpRegistry (see §3.4). This is useful if for example the signer is an ERC-1967 proxy and the implementation was updated in a way material to determine compliance or non-compliance with this spec (e.g. a bug in the implementation of the ERC-165 interface).
Authorizing Sessions.
Implementers may also authorize requests for Session Proofs or starting a session. For these scenarios, the action value will have the relevant prefix. For example, requests for a Session Proof will have a prefix of 0x02 in the most significant byte of the action, i.e. uint8(action >> 248) == uint8(2). More details on the different prefixes can be found in the SessionId definition.
Implementers MAY encode different handling depending on the type of request. For example:
- If the RP does not support sessions, reject proofs if the prefix does not equal
0x00. - If the RP supports sessions, the actions are randomly generated, so as long as the prefix matches one of the expected values (e.g.
0x01,0x02), the action check can be considered fulfilled.
4. Rationale
The verifyRpRequest allows an RP to verify all the inputs that would otherwise be signed, so any rules that an RP would normally use when constructing an off-chain request can be encoded in the RP’s contract for pure on-chain interactions.
The rationale behind the arbitrary data is that the action is passed to this function already hashed into the field so nothing on the pre-image can be validated. Validating the content of the pre-image is something particularly important because it scopes the uniqueness space, i.e. it’s an input to the nullifier of a proof. An example of this is data could include the pre-image of the action in order to perform validations over the raw action data.
This spec is inspired by ERC-1271.
5. Reference Implementation
contract WIP101Example is IWIP101, ERC165 {
// bytes4(keccak256("verifyRpRequest(uint8,uint256,uint64,uint64,uint256,bytes)"))
bytes4 internal constant MAGICVALUE = 0x35dbc8de;
/**
* @dev The RP request is not valid. The code may be used to provide additional debugging information.
*/
error RpInvalidRequest(uint256 code);
/// @inheritdoc IWIP101
function verifyRpRequest(
uint8 version,
uint256 nonce,
uint64 createdAt,
uint64 expiresAt,
uint256 action,
bytes calldata data
) external view returns (bytes4 magicValue) {
if (version > 1) {
revert RpInvalidRequest(0);
}
if (createdAt > block.timestamp || createdAt < block.timestamp - 15 minutes) {
revert RpInvalidRequest(1);
}
if (expiresAt < block.timestamp || expiresAt > block.timestamp + 15 minutes) {
revert RpInvalidRequest(2);
}
if (uint8(action >> 248) != uint8(0)) {
// This RP does not support Session Proofs
revert RpInvalidRequest(3);
}
// note the bitshift by 8 for the action to fit into the field; this is the same
// method as in `FieldElement::from_arbitrary_raw_bytes` which is used in common SDKs
uint256 expected_action = uint256(keccak256(abi.encodePacked("vote"))) >> 8;
if (action != expected_action) {
revert RpInvalidRequest(4);
}
return MAGICVALUE;
}
/// @inheritdoc ERC165
function supportsInterface(bytes4 interfaceId) public view virtual override(ERC165, IERC165) returns (bool) {
return interfaceId == type(IWIP101).interfaceId || super.supportsInterface(interfaceId);
}
}
It is expected that under normal circumstances, the RP would still construct a Proof request. The key distinction with authorization through this specification versus authorization through an ECDSA signature is that the proof request would be constructed in an untrusted environment (for example the user’s web browser). Hence, why this specification also covers validation of the request. The following diagram shows a non-normative example of how such request flow can occur. Note particularly how the proof request is constructed by the RP but does not need to be assumed correct because it is verified by the WIP-101 compliant contract.
sequenceDiagram participant c as RP Client (e.g. Web App) participant a as Authenticator participant o as OPRF Nodes participant v as RP WIP101 Verifier c->>c: generate rnd nonce c->>c: encode RP custom `data` c->>c: construct proof request c->>a: proof request (unsigned) a->>o: nullifier request o->>v: verifyRpRequest() v->>v: internal verifications v->>o: is valid o->>a: nullifier a->>c: proof response c->>v: submit proof and perform action on-chain v->>v: invalidate nonce
6. Recommended Validations
It is RECOMMENDED to implement the following validations for a Proof request:
- Reject a request which
versionis larger than the maximum known, as future version changes MAY be incompatible with previous versions. createdAtis not in the future and close to the current time.expiresAtis not in the past and is not too far in the future. While “too far” is relative to each RP’s use case, it’s unlikely that a Proof request should be valid for more than 15 minutes. Note that sub-minute precision SHOULD generally NOT be relied upon on-chain.noncehas not been used before. This is RECOMMENDED, however OPRF Nodes will still enforce a nonce is only used once. Nonce tracking withinverifyRpRequestis not possible due to theviewconstraint. Implementers relying on nonce uniqueness SHOULD track used nonces in a separate transaction (e.g., when the proof is consumed on-chain). The rationale for this is that a nonce isn’t considered “consumed” until the proof is verified. Furthermore, each OPRF Node will call theverifyRpRequestfunction independently, so it’s not a good place to enforce non re-use.actionhash is the expected value for Uniqueness Proofs.- If the RP does not support Session Proofs, the first 8 bits of the action must all equal
0.
7. Security
- The main threat model protected with this spec is authorization of RP Requests such that an authenticator (malicious or not) cannot generate
nullifiers (or other OPRF outputs) for use with an RP without that RP’s authorization. This for example helps protect users in post-compromise scenarios. If an RP implements proper authorization logic, even with a compromised authenticator, an attacker would be unable to compute a user’s nullifier. Binding the OPRF input to the verifiedaction(§3.3) is what anchors this guarantee. - Implementers are solely responsible for the security of their validation logic. A
verifyRpRequestthat unconditionally returns the magic value is equivalent to having no authorization and exposes the RP to unbounded nullifier generation for any action as well as users in a post-compromise scenario.
8. Backwards Compatibility
This World Improvement Proposal (WIP) introduces a new interface; no existing contracts or other previously existing functionality is affected.
WIP-102: Simplified Optimistic Recovery Agent Update
1. Abstract
This spec proposes replacing the current Recovery Agent update mechanism (initiateRecoveryAgentUpdate, cancelRecoveryAgentUpdate, executeRecoveryAgentUpdate), that always requires two transactions, with a new flow (updateRecoveryAgent, revertRecoveryAgentUpdate) that only requires a second transaction when the update needs to be reverted. The proposal preserves the security delay through a revert window.
2. Motivation
In the current contract implementation the user has to first call initiateRecoveryAgentUpdate. This initiates a configurable (e.g. 14 days) period during which the update operation can be cancelled (with a cancelRecoveryAgentUpdate call). After the period is over anyone can call a permissionless executeRecoveryAgentUpdate transaction to finalize the update.
This introduces the following issues:
- The updater has to remember to execute the call after the delay, or someone needs to develop and maintain an auto-executing service.
- The new Recovery Agent isn’t set on-chain until the delay elapses, which can be problematic e.g. if the user wants to register a Recovery Agent after registering their World ID. The Recovery Agent cannot be sure this has been set up properly for a World ID.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
3.1 Method Changes
The existing 3 methods are replaced with 2 new methods:
| Current | Proposed |
|---|---|
initiateRecoveryAgentUpdate | updateRecoveryAgent |
cancelRecoveryAgentUpdate | revertRecoveryAgentUpdate |
executeRecoveryAgentUpdate | removed |
3.2 Mechanism
Instead of delaying the Recovery Agent update on-chain, the updateRecoveryAgent function updates the address immediately, records the previous Recovery Agent in a mapping, and sets a delay on the account. During this delay period an account MUST NOT yet be recoverable with the new Recovery Agent.
- The previous Recovery Agent remains valid until
invalidAfterelapses and the user MAY revert the update during this window, restoring the previous Recovery Agent. This is to maintain the security mechanism when updating a Recovery Agent to mitigate takeover attacks. - The new Recovery Agent SHALL NOT be used until after
invalidAfter. OnceinvalidAfterelapses without a revert, the previous Recovery Agent becomes invalid and the new Recovery Agent becomes the sole valid Recovery Agent.
3.3 Storage
This change requires storing the previousRecoveryAgent address and an invalidAfter timestamp per leafIndex in the WorldIDRegistry. The data stored is equivalent to what is currently stored in the contract but with inverted semantics:
Current:
struct PendingRecoveryAgentUpdate {
address newRecoveryAgent;
uint256 executeAfter;
}
mapping(uint256 => PendingRecoveryAgentUpdate) internal _pendingRecoveryAgentUpdates;
Proposed:
struct PreviousRecoveryAgentUpdate {
address prevRecoveryAgent;
uint256 invalidAfter;
}
mapping(uint256 => PreviousRecoveryAgentUpdate) internal _prevRecoveryAgentUpdates;
3.4 updateRecoveryAgent
updateRecoveryAgent uses the same authorization as initiateRecoveryAgentUpdate.
function updateRecoveryAgent(
uint64 leafIndex,
address newRecoveryAgent,
bytes memory signature, // authenticator signature
uint256 nonce
) external virtual onlyProxy onlyInitialized {
updateRecoveryAgent SHALL NOT be called again while the previous Recovery Agent is still valid (i.e. invalidAfter has not elapsed) and the call MUST revert if it does. While allowing a second call to overwrite the pending update (keeping previousRecoveryAgent and resetting invalidAfter) would be well-defined, it adds contract complexity. The same result is achieved more explicitly by reverting the current update first.
3.5 revertRecoveryAgentUpdate
revertRecoveryAgentUpdate can be executed by any Admin Authenticator (as defined in WIP-104) up until invalidAfter, matching the authorization model of the current cancelRecoveryAgentUpdate. Reverting a Recovery Agent update is a management operation, so a Proving Authenticator MUST NOT be able to authorize it.
function revertRecoveryAgentUpdate(
uint64 leafIndex,
bytes memory signature, // authenticator signature
uint256 nonce
) external virtual onlyProxy onlyInitialized {
3.6 Interaction with recoverAccount
When recoverAccount is called, any active Recovery Agent update (i.e. invalidAfter has not yet elapsed) MUST be cleared automatically. This handles the attack scenario where an attacker compromises an authenticator, removes the user’s authenticators to lock them out, and initiates a Recovery Agent update. When the user recovers their account via the Recovery Agent, the malicious update is reverted as part of the recovery.
4. Rationale
This solution preserves the original security properties while enabling better ergonomics and user experience. Notably:
- No follow-up transaction required. The update takes effect immediately, removing the need for an execution step after the delay period.
- Equivalent security. The revert window ensures that an attacker who compromises an authenticator cannot immediately replace the Recovery Agent. Any Admin Authenticator retains the ability to revert the change during the delay period.
5. Security
- The delay period MUST be preserved to maintain the security guarantee that a compromised authenticator cannot unilaterally replace the Recovery Agent.
- During the delay period, both the previous and new Recovery Agents exist, but recovery MUST only be possible with the previous Recovery Agent until
invalidAfterelapses. - Any Admin Authenticator MAY revert the update at any point before
invalidAfter, restoring the original state.
6. Backwards Compatibility
This WIP replaces the existing 3-step Recovery Agent update mechanism. Contracts implementing this spec MUST remove the initiateRecoveryAgentUpdate, cancelRecoveryAgentUpdate, and executeRecoveryAgentUpdate methods in favor of the new interface. Any pending Recovery Agent updates at the time of upgrade SHOULD be migrated or invalidated.
WIP-103: Proof of Ownership
1. Abstract
This spec introduces a new ZK circuit (“Ownership Proof”) to the World ID Protocol that allows a user to prove they own a specific registered leaf without revealing their leaf_index or any private key material to the verifier. The circuit takes a domain-separated commitment, H(domain_separator, leaf_index, commitment_blinder), and proves that the user controls a secret key registered in the WorldIDRegistry for that leaf, without exposing the commitment_blinder value (e.g. a credential blinding factor) as a public input. The initial use case is Issuer Authentication, where a user proves ownership of a credential sub to an Issuer for operations like re-issuance, renewal, or deletion.
2. Motivation
There are use cases that require users to prove ownership for their World ID and a specific commitment. At the time of writing there is a well-known use case for Issuer Authentication.
2.1 Issuer Authentication
Issuers may require users to authenticate with their World ID. Some examples of such use cases:
- Re-issuance. Allowing the user to get a new copy of their credential due to loss.
- Status information. Providing the user with information on their current enrollment state.
- Cancellation/deletion/unregistration. Allowing the user to unregister, delete or cancel their existing credential.
For the cases above, Issuers need to know the legitimate owner of a Credential is requesting the operation. Issuers do not know the user’s leaf_index by design, so a mechanism to authenticate based on the Credential’s subject (sub) is required.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
The terms Authenticator, Issuer and Credential are as defined in the World ID Protocol.
This specification introduces a new circuit to the Protocol: “Ownership Proof”. The Ownership Proof is a new circuit which allows users to prove ownership (as defined below) using a specific commitment.
A non-normative high-level description of what the circuit proves:
- It proves the user’s
leaf_indexhashed with acommitment_blinderand a specific domain separator produces anexpected_commitment. An example, is the Credential’s blindedsub.- It proves the user’s
leaf_indexis validly registered in theWorldIDRegistryand the user can produce a signature with a secret key ($sk$) registered as an authenticator for the particularleaf_index.- It proves the user authorized the operation through a signature with a registered authenticator secret key and that such operation is only valid for a specific
nonceandcontextprovided.
The Ownership Proof circuit MUST introduce the following inputs. The circuit MUST NOT introduce any additional public inputs which are not specified in this or a future spec.
| Input | Visibility | Type | Description |
|---|---|---|---|
expected_commitment | Public | Field element | The expected output of the hashed commitment (e.g. the sub of a credential) |
merkle_root | Public | Field element | The root hash of the Merkle tree used for inclusion (representing the WorldIDRegistry) |
depth | Public | Field element | The depth of the WorldIDRegistry’s Merkle tree used for inclusion |
nonce | Public | Field element | One-time use value to prevent replays. See Nonce section. |
context | Public | Field element | A verifier-supplied context for which the proof is valid. |
user_pk | Private | [PublicKey; NUM_KEYS] | The list of authenticator public keys registered in the WorldIDRegistry. |
pk_index | Private | Field element | The index in user_pk of the key used for signature. |
query_s | Private | Field element | The s component from the EdDSA signature on the message (see constraints). |
query_r | Private | [Field element; 2] | The r component from the EdDSA signature on the message (see constraints). |
leaf_index | Private | Field element | The index of the user’s World ID in the WorldIDRegistry. |
siblings | Private | [Field element; MAX_DEPTH] | The sequence of all siblings from the leaf_index to the merkle_root . |
commitment_blinder | Private | Field element | The blinding value that is hashed with the leaf_index to output the expected_commitment. The provenance of the value depends on the specific use case (see Use Cases). |
For the inputs above, a PublicKey is as defined in the WIP-100 spec (an EdDSA public key represented as its coordinate points $(x,y)$ with each coordinate being a Field element). For reference, user_pk, pk_index, leaf_index (named mt_index), siblings introduce the same constraints that the Nullifier Proof circuit.
3.1 Circuit constants
The Field ($F$), the Curve (BabyJubJub), and the Hashing ($H_t$) definitions MUST follow the requirements of WIP-100. For example, in this spec H_3 refers to the hash constructed from a Poseidon2 permutation of 3-elements.
| Constant | Value | Description |
|---|---|---|
NUM_KEYS | 7 | Length of the authenticator public key list. Fixed by the Nullifier Proof circuit and the WorldIDRegistry. |
MAX_DEPTH | 30 | For reference, defined in WIP-100. Maximum supported WorldIDRegistry Merkle tree depth; also the length of siblings. |
MERKLE_LEAF_DS | 105702839725298824521994315 (b"World ID PK") | Domain separator for the authenticator key list commitment. |
DS_EDDSA | 360302137480307891234917541314130533 (b"EdDSA Signature") | For reference, defined in WIP-100. Domain separator for the EdDSA challenge hash. |
DS_WIP_103 | 95972389630003 (b"WIP103") | Domain separator for the Ownership Proof signed message. MUST NOT be reused outside of the Ownership Proof signed-message construction. |
3.2 Circuit constraints
The Ownership Proof circuit MUST implement the following constraints (order is not relevant):
- Expected commitment. Compute the commitment as:
H_3(domain_separator, leaf_index, commitment_blinder). The computed commitment MUST equal theexpected_commitment. Thedomain_separatoris dependent on the Use Case and all possible domain separators MUST be hardcoded in the circuit. - Signature verification. Compute the message of the signature as:
message = H_4(DS_WIP_103, expected_commitment, nonce, context). The circuit MUST verify(query_r, query_s)as an EdDSA-Poseidon2 signature bypkover themessagein order to prove authorization by an authenticator over the particularnonceandcontext. The signature computation and verification MUST follow all the requirements from WIP-100. - Key selection. The circuit MUST select exactly one public key
pk = user_pk[pk_index]and MUST constrain:pk_index < NUM_KEYS, enforced as a comparison over the full field $F$ and not as a truncating cast to a smaller integer type. An out-of-rangepk_indexMUST cause proving to fail.- The selected
pkequals thepk_index-th element ofuser_pk, for the samepk_indexwitness.
- Key validity.
user_pkMUST be a fixed-length array ofNUM_KEYSentries inWorldIDRegistryslot order. Slots with no active authenticator, whether never used or removed, MUST be encoded as the neutral element $O = (0,1)$ of the BabyJubJub curve. The circuit MUST constrain the selected $\texttt{pk} = (A_x, A_y)$ as follows: 1) the point MUST be on the curve; 2) the point MUST be in the prime-order subgroup; and 3) the point MUST NOT be the neutral element $O$. Constraint (3) is particularly important because $O$ is used to signal an omitted element from the array, without it, the circuit would not have transitive proof ofskknowledge. - Leaf index commitment. The circuit MUST compute
offchainSignerCommitmentas designated in the Query Proof, with $H_{16}$ using the domain separatorMERKLE_LEAF_DSand each key inuser_pkencoded as $(x, y)$ points sequentially, with the last position (index15) equal to0. The digest is output state element1. All keys MUST be included in order, including empty slots with the neutral element $O$. - Merkle inclusion proof. The circuit MUST recompute
merkle_rootfromoffchainSignerCommitmentas the leaf value at positionleaf_indexfollowing the definitions of WIP-100. - Leaf-index binding. The
leaf_indexhashed into the commitment in constraint 1 and the position proven in constraint #6 MUST be the same witness. The circuit MUST NOT accept two independent values. This binding is essential, otherwise the circuit is only proving the prover knows theskof some leaf, not the one being proven in theexpected_commitment. - Verifier-supplied input validation. The
nonceandcontextMUST NOT equal0. These are usually intended as sentinel values and could surface an incorrect use of them.
3.3 Nonce
The nonce is a security mechanism to prevent replay attacks. Recipients of an Ownership Proof (also called “verifiers” in this spec) are responsible for its generation and verification.
- Provenance. Verifiers MUST generate and provide the nonce. Authenticators MUST NOT generate nonces.
- Single Use. Verifiers MUST enforce that a nonce may only be used one time. Verifiers MUST invalidate any nonce after first use.
- Entropy. Verifiers MUST generate the nonce from a uniformly random distribution using a CSPRNG over the whole field $F$.
- Expiration. Nonces MUST have a defined TTL and not be valid after expiration. Verifiers MAY define the mechanism to accomplish this.
- The
0element is NOT a valid nonce.
3.4 Context
The context is the mechanism by which a verifier binds a specific proof to a specific requested operation. The verifier MUST constrain the use of Ownership Proofs to specific operations and assign such operations with a unique context. For example, renewing a credential and deleting a credential are two different contexts. The verifier MUST provide the context when requesting a proof and MUST verify the provided context is valid for the explicit operation being executed.
It is RECOMMENDED that Verifiers use contexts shorter than 31 bytes or use a hash function to avoid collisions.
The 0 element is NOT a valid context.
4. Use Cases
- Authenticators MUST restrict Ownership Proofs subject only to the use cases outlined in this spec or a future spec which references this one.
- If future use cases are added, the
commitment_blinderIS REQUIRED to always be a high entropy value to prevent finding a user’sleaf_index. Unless otherwise noted, it should be randomly generated from a CSPRNG uniformly distributed over the entire $F$.
4.1 WIP-103-001: Issuer Authentication
- Use case: Authenticating to an Issuer where the Issuer has a
subas identifier. - Domain Separator:
b"H_CS(id, r)"namedDS_C_CS. commitment_blindervalue: The credential’s blinding factor as stored (or re-derived) by the Authenticator.- Use conditions: Can only be used to prove ownership with the specific Issuer for which the
subwas generated. The Authenticator MUST ensure this proof is only provided to the Issuer it corresponds. The Authenticator knows this from the blinding factor used as thecommitment_blinderinput. Furthermore, providing this proof MUST come from a user initiated action and SHALL NOT be requested by Issuers. Acceptable examples include: when a user requests a Credential re-issuance, when a user requests a Credential renewal, when a user requests a Credential deletion. - Verification: Issuers MUST ensure the
expected_commitmentpublic input of the circuit matches the expected identifier (i.e. Credential’ssub).
5. Rationale
A keen observer would notice the Ownership Proof circuit is very similar to the Query Proof circuit. Using this circuit is not an option because the verifying party would need to learn the commitment_blinder being verified to ensure the hash validity (it’s a public input). For example, an Issuer does not need to learn the credential blinding factor, which would defeat the purpose of the blinding factor because the leaf_index could be easily brute forced.
6. Reference Verification
An Issuer will be able to know a specific user is the legitimate owner of a sub by performing the following validations:
- Ensuring the received zero-knowledge proof verifies correctly with the correct verifying keys.
- The root hash (public input) matches a root hash for a valid Merkle tree of the
WorldIDRegistry, as well as the expected depth. - The
expected_commitmentpublic input matches the expectedsubbeing verified. This value MAY also be used for lookups when thesubis not known in advance. - The
noncepublic input is one the Issuer itself generated, has not expired, and has not been used before. The nonce MUST be invalidated at this point. See Nonce. - The
contextpublic input is the context the Issuer assigns to the operation it is about to execute. A proof carrying any other context MUST be rejected, even if every other check passes. See Context.
Validations 4 and 5 are what make the proof unreplayable and single-purpose. The circuit constrains the nonce and context to be bound in the authenticator’s signature, but it cannot know which values the verifier intended, so a verifier that accepts the values supplied alongside the proof without comparing them to its own gains no replay or cross-context protection.
It is strongly RECOMMENDED that implementations verify the Ownership Proof using a proof format and verification method supported by ProveKit.
7. Security
commitment_blinderis never revealed. Unlike the Query Proof circuit, thecommitment_blindervalue is a private input. For Issuer Authentication (WIP-103-001), this means the credential blinding factor is never exposed to the Issuer. If it were, the Issuer could brute-force theleaf_index(the domain of possible values is small) and break the unlinkability property of the blinding factor.- Domain separation prevents cross-context replay. Each use case defines its own domain separator, and the
domain_separatoris committed inside the hash. A proof generated for one use case cannot be replayed in another context. Authenticators MUST enforce that only domain separators defined in this spec or a referencing spec are used. - Authenticator-gated usage. Authenticators MUST restrict generation of Ownership Proofs to user-initiated actions for the defined use cases. Issuers SHALL NOT be able to request this proof; the Authenticator must initiate it. This prevents an Issuer from using the proof as an oracle to test whether a given
subbelongs to a particular user. - Nonce and context are bound into the signed message. The signature verified by the circuit is over
nonce,contextandexpected_commitment, not only over the bare commitment. This means every Ownership Proof requires a fresh signature from the authenticator signing key for that specific(nonce, context)tuple (in addition to theexpected_commitment), anchoring proof generation to an active signing event by the Authenticator. - For note, proofs for the same
expected_commitment(e.g. the samesub) are linkable because they use the same public input. This is intended as theexpected_commitmentis what its getting proven. This proof however prevents cross-verifier linking as thecommitment_blinderdiffers.
8. Backwards Compatibility
This World Improvement Proposal (WIP) introduces a new interface; no existing contracts or other previously existing functionality is affected.
WIP-104: Proving and Admin Authenticators with Fixed Permission Sets
1. Abstract
This spec introduces a distinction between two classes of Authenticators in the World ID Protocol: Proving Authenticators and Admin Authenticators. A Proving Authenticator holds only an off-chain public key and is limited to proof generation and presentation. An Admin Authenticator additionally holds an on-chain management key (represented as an address registered in _authenticatorAddressToPackedAccountData) and can perform all management operations on the World ID. The authenticator type is encoded in the existing pubkey bitmap of the WorldIDRegistry. In addition, this spec deprecates the updateAuthenticator method.
2. Motivation
Prior to this spec, every authenticator registered for a World ID was required to have a corresponding on-chain management key. This means every authenticator has equal permissions and can perform all management operations (inserting, removing or updating other authenticators, initiating recovery agent updates, etc.). There are multiple use cases where a user may want to have an authenticator capable of generating proofs on their behalf but not managing their World ID. An example of this is when delegating proof generation to some CLI tools, agents or non fully-featured authenticators.
Requiring a management key for authenticators that don’t need it expands the attack surface (and additionally increases storage costs on-chain). Separating the two roles allows:
- A smaller attack surface for Proving Authenticators, limiting the blast radius in case of compromise.
- Reduced storage costs for authenticators that only generate proofs.
- Flexibility for future authenticator form factors that lack the ability to hold an Ethereum key pair.
3. Specification
The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.
The terms Authenticator, Issuer and Credential are as defined in the World ID Protocol.
3.1 Terminology
This spec introduces the following explicit terminology:
- Signing Key: This is an EdDSA over BabyJubJub keypair which authenticators use to sign operations off-chain related to proof generation. The signatures from this key are used in ZK circuits (e.g. Query Circuit or WIP-103 Proof of Ownership). Previously this may have been referred to as off-chain signer.
- Management Key: An ECDSA over
secp256k1keypair (curve used by World Chain) used to sign operations on-chain that change the state of a World ID in theWorldIDRegistry. The public key is represented as an Ethereum address. Previously this may have been referred to as on-chain signer. - Management Operations: Any operation which results in a state change to a user’s World ID in the
WorldIDRegistry. Examples: inserting an authenticator, removing an authenticator, initiating a Recovery Agent update.
3.2 Authenticator Classes
A distinction is introduced between two classes of Authenticators:
- Proving Authenticator: Holds only a Signing Key and is capable of proof generation and presentation. A Proving Authenticator SHALL NOT perform any management operations on the World ID, i.e. it cannot perform any state changes on the
WorldIDRegistryfor the user. A Proving Authenticator has no entry in_authenticatorAddressToPackedAccountData. - Admin Authenticator: Holds both an Signing Key and a Management Key. An Admin Authenticator is capable of all operations for a user’s World ID, including management operations and proof generation.
Prior to the introduction of this spec, all registered authenticators MUST be considered Admin Authenticators.
3.3 Bitmap Encoding
The WorldIDRegistry stores a 96-bit pubkey bitmap per account in _leafIndexToRecoveryAddressPacked. This spec splits the bitmap into two halves:
- Bits [0-47]: Occupancy. Bit
nis set if pubkeyIdnis in use. - Bits [48-95]: Authenticator Class. Bit
48 + nis set ifpubkeyIdnis a Proving Authenticator.
An occupied slot with its class bit clear is an Admin Authenticator. An occupied slot with its class bit set is a Proving Authenticator. The state (occupancy=0, class=1) is unused and MUST NOT occur; an unoccupied slot MUST have both bits clear.
As a consequence of this encoding, the effective hard limit on authenticators per account is reduced from 96 to 48. This limit MUST be exposed as a separate constant, MAX_AUTHENTICATORS_V2_HARD_LIMIT, and the registry MUST reject setMaxAuthenticators calls with a value greater than it.
3.4 insertAuthenticator
The insertAuthenticator function MUST accept address(0) as the newAuthenticatorAddress parameter. When address(0) is provided:
- The occupancy bit for the given
pubkeyIdMUST be set. - The class bit MUST be set, marking the slot as a Proving Authenticator.
- No entry SHALL be written to
_authenticatorAddressToPackedAccountData. - Address validation (
_validateNewAuthenticatorAddress) MUST be skipped.
When a non-zero address is provided, the function behaves as in V1 (occupancy bit set, class bit clear, address mapping written).
The operation MUST be authorized by an existing Admin Authenticator (no changes).
3.5 removeAuthenticator
The removeAuthenticator function MUST use the bitmap class bit to determine the removal path:
- If the class bit is set (Proving Authenticator), the caller MUST pass
address(0)asauthenticatorAddress. No address mapping validation or cleanup is performed. Both the occupancy and class bits MUST be cleared. - If the class bit is clear (Admin Authenticator), the caller MUST pass the correct non-zero
authenticatorAddress. Full V1 validation applies: the address mapping is checked for leaf index, pubkeyId, and recovery counter consistency, then deleted. Both the occupancy and class bits MUST be cleared.
If an incorrect authenticatorAddress is passed for the authenticator class being removed (non-zero for a Proving Authenticator, or zero for an Admin Authenticator) the call MUST revert with AuthenticatorClassMismatch.
The operation MUST be authorized by an existing Admin Authenticator (no changes).
3.6 updateAuthenticator
The updateAuthenticator function is deprecated in this version. Calls MUST revert with MethodUnsupported. Key rotation can be accomplished with a removeAuthenticator followed by insertAuthenticator.
3.7 Account Creation
Account creation (_registerAccount) is unaffected. _registerAccount rejects address(0), so every new account starts with at least one Admin Authenticator.
3.8 Manageability Invariant
An Admin Authenticator MUST NOT be removed if it is the only Admin Authenticator remaining on the account. Such a call MUST revert with UnmanageableNotAllowed. This guarantees that an account always retains at least one Admin Authenticator capable of managing it.
4. Rationale
Introducing two classes of authenticators allows users to have more control over the permissions they assign to different authenticators. This follows common security practices of least privilege.
4.1 Split bitmap over separate storage
The authenticator class could be stored in a separate mapping (e.g. mapping(uint64 => uint256)), but this would add a new cold SSTORE (20k gas) per account on first use. The split-bitmap approach reuses the existing 96-bit bitmap with no additional storage cost. The trade-off is halving the maximum authenticator count from 96 to 48, which is acceptable given practical account sizes.
4.2 updateAuthenticator disabled
Supporting all type of transitions in a single function adds significant surface area. The same outcomes are achievable via removeAuthenticator + insertAuthenticator. Furthermore, these operations are expected to seldom occur.
4.3 Authorization model
Proving Authenticators cannot sign management operations because they have no entry in _authenticatorAddressToPackedAccountData, which is required by _recoverAccountDataFromSignature. This is enforced structurally rather than with an explicit permission check, and the structural check covers every call site that resolves a signer through _recoverAccountDataFromSignature (e.g. insertAuthenticator, removeAuthenticator, initiateRecoveryAgentUpdate, cancelRecoveryAgentUpdate).
5. Security
- Proving Authenticators cannot escalate privileges. The lack of an
_authenticatorAddressToPackedAccountDataentry means_recoverAccountDataFromSignaturewill always revert for a Proving Authenticator’s address. There is no code path through which a Proving Authenticator can authorize a management operation. - Class enforcement on removal. The bitmap class bit determines the required removal path. A caller cannot use
address(0)to remove an Admin Authenticator (which would leave its mapping stale) or a non-zero address to remove a Proving Authenticator. - Bitmap backward compatibility. V1 accounts have all type bits at zero, correctly classifying existing authenticators as Admin. This holds because
_maxAuthenticatorswas never set above 48 in V1. ThesetMaxAuthenticatorsoverride enforces this new hard limit going forward. - Account creation unchanged. The
_registerAccountfunction retains its V1 check that rejectsaddress(0), ensuring every new account starts with at least one Admin Authenticator.
6. Backwards Compatibility
- All existing authenticators are classified as Admin Authenticators (class bit defaults to 0). No migration is required.
- The maximum authenticator count per account is reduced from 96 to 48. Accounts with authenticators at pubkeyId >= 48 are incompatible.
updateAuthenticatoris disabled. Callers relying on this function MUST switch toremoveAuthenticator+insertAuthenticator.
7. Alternatives Considered
With the introduction of this spec, we also considered completely decoupling Signing Keys from Management Keys, which means that each key is treated independently and there is no explicit concept of an Authenticator encoded in the WorldIDRegistry. This was eventually dismissed. Implementing such a system requires one of two options.
A general note applies to both options: practical accounts are expected to hold a small number of authenticators (the V1 default for _maxAuthenticators is 7 and has never been raised), so the 48-slot cap imposed by the chosen encoding is not a binding constraint. While at this time the use of Management-only keys does not have an application it is quite likely the the future direction is for Management Keys to exist on alternative constructs (examples: passkeys, zkLogin alternatives, etc). These constructs could provide recovery alternatives to users. Not pursued with this spec because at this time there are off-protocol mechanisms (e.g. authenticator-managed backups) which address most of the same user pain points.
Option 1: New storage layout. This would truly reflect the separation between each key, but changing the storage layout requires the introduction of at least a new mapping (to store the separate Management Keys) which would cost at least 20,000 additional gas per account on first use, plus a migration path or lazy-init logic for V1 accounts, both of which expand the surface area. The runtime gain (decoupled lifecycles) does not justify these costs given the manageability invariant already binds at least one Management Key to each account.
Option 2: Re-structured bitmap. This entails reusing the same 96-bit bitmap but claiming the unused (occupancy=0, class=1) state to mean a Management-only Authenticator (a slot with a Management Key but no Signing Key). The main concrete benefit is that inserting or removing a Management Key would not require updating the Merkle tree, but this operation is rarely expected to occur. In exchange, the bitmap encoding becomes a four-state enum (instead of a flag on an occupied slot) and removeAuthenticator must distinguish three classes. The added complexity is not justified.