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.