Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

  1. To be compatible with signature verification, a contract acting as an RP MUST conform to the IWIP101 interface below. verifyRpRequest MUST NOT modify state (declared view for solc > 0.5) and MUST allow external calls.
  2. On a valid request, verifyRpRequest MUST return the bytes4 magic value 0x35dbc8de (the function selector for verifyRpRequest). Reverts otherwise.
  3. The contract (implementer) MUST also adhere to ERC-165 and return true from supportsInterface for type(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

  1. If the request is not authorized, verifyRpRequest MUST revert.
  2. When a request is invalid, implementers SHOULD revert with the explicit RpInvalidRequest error.
  3. Callers MUST treat any return value other than the magic value 0x35dbc8de as an invalid request.
  4. RPs MAY define any number of codes in RpInvalidRequest. These codes are intended only for RP debugging, hence the RP can establish any format/definition for this attribute.
  5. It is RECOMMENDED implementers ensure that any upgrades to validation logic are backwards-compatible for the duration of the maximum expiresAt window.

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).

  1. OPRF Nodes MUST use this spec (WIP-101) verification if the signer in the RpRegistry implements ERC-165 for type(IWIP101).interfaceId.
  2. Following the main threat model of this spec, OPRF Nodes MUST pass to verifyRpRequest the same action value that will be used as input to the OPRF computation.
  3. On an explicit RpInvalidRequest revert, OPRF Nodes MUST return the inner code to the requester (for debugging purposes).
  4. OPRF Nodes MAY enforce a timeout on the resolution of verifyRpRequest.
  5. Implementers SHOULD NOT rely on computationally expensive validation logic, so a call resolves within any node-enforced timeout.
  6. OPRF Nodes MAY check whether a contract is deployed at the registered signer address, and MAY return a specific error code if a deployed contract is not WIP-101 compliant.
  7. OPRF Nodes are NOT REQUIRED to verify signature via ECDSA if there is a contract deployed at the signer address.

3.4 Signer Caching

  1. OPRF Nodes MAY cache an RP’s signer for performance. This cache may include the consideration on whether the signer is WIP-101 compliant or not.
  2. A cached signer entry MUST NOT exceed a 30-day TTL.
  3. OPRF Nodes MUST invalidate the signer cache on any RpUpdated event emission from the RpRegistry.

3.5 Request Field Constraints

  1. The maximum length of data is 8192 bits (1024 bytes); the data field MUST NOT exceed this length.
  2. OPRF Nodes MAY enforce this maximum length and reject requests which exceed it.

3.6 Deployment

  1. Implementations of WIP-101 MUST be deployed on World Chain Mainnet (Chain ID: 480).
  2. 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

It is RECOMMENDED to implement the following validations for a Proof request:

  1. Reject a request which version is larger than the maximum known, as future version changes MAY be incompatible with previous versions.
  2. createdAt is not in the future and close to the current time.
  3. expiresAt is 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.
  4. nonce has not been used before. This is RECOMMENDED, however OPRF Nodes will still enforce a nonce is only used once. Nonce tracking within verifyRpRequest is not possible due to the view constraint. 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 the verifyRpRequest function independently, so it’s not a good place to enforce non re-use.
  5. action hash is the expected value for Uniqueness Proofs.
  6. If the RP does not support Session Proofs, the first 8 bits of the action must all equal 0.

7. Security

  1. 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 verified action (§3.3) is what anchors this guarantee.
  2. Implementers are solely responsible for the security of their validation logic. A verifyRpRequest that 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.