Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PBHVerifier #74

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "pbh-verifier/lib/forge-std"]
path = pbh-verifier/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "pbh-verifier/lib/world-id-contracts"]
path = pbh-verifier/lib/world-id-contracts
url = https://github.com/worldcoin/world-id-contracts
[submodule "pbh-verifier/lib/BokkyPooBahsDateTimeLibrary"]
path = pbh-verifier/lib/BokkyPooBahsDateTimeLibrary
url = https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary
45 changes: 45 additions & 0 deletions pbh-verifier/.github/workflows/test.yml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also move this workflow to the root .github/workflows

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additionally can we simply emit the proof instead of verifying on chain after checking the uniqueness of the nullifier hash for the builder to verify during transaction validation.

@0xOsiris could you explain your thoughts here?

Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

env:
FOUNDRY_PROFILE: ci

jobs:
check:
strategy:
fail-fast: true

name: Foundry project
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
with:
version: nightly

- name: Show Forge version
run: |
forge --version

- name: Run Forge fmt
run: |
forge fmt --check
id: fmt

- name: Run Forge build
run: |
forge build --sizes
id: build

- name: Run Forge tests
run: |
forge test -vvv
id: test
14 changes: 14 additions & 0 deletions pbh-verifier/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Compiler files
cache/
out/

# Ignores development broadcast logs
!/broadcast
/broadcast/*/31337/
/broadcast/**/dry-run/

# Docs
docs/

# Dotenv file
.env
17 changes: 17 additions & 0 deletions pbh-verifier/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM ghcr.io/foundry-rs/foundry:latest

WORKDIR /world-id

COPY . .

# Fetch libs
RUN forge install

# Build the project
RUN forge build

# RUN ls script; exit 1
RUN ./script/generate_anvil_state.sh

ENTRYPOINT ["anvil", "--host", "0.0.0.0", "--load-state", "state.json"]
0xForerunner marked this conversation as resolved.
Show resolved Hide resolved
CMD []
0xForerunner marked this conversation as resolved.
Show resolved Hide resolved
18 changes: 18 additions & 0 deletions pbh-verifier/LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
MIT License

Copyright 2023 Worldcoin Foundation

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
7 changes: 7 additions & 0 deletions pbh-verifier/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# PBH Validator

As mentioned previously, Stage 1 of 4337 PBH features a PBHSignatureAggregator and PBHValidator contract, allowing a user to include a World ID proof encoded in the UserOp signature. The signature aggregator will call the PBHValidator for each UserOp included in the bundle, verifying the associated proof.

The `PBHValidator` contract will extract the proof data from the signature, validate proof inputs and verify the proof. The `signal` for the proof will consist of the `userOpHash`. Upon successful verification of the proof, the `PBHValidator` will bump the PBH nonce for the `nullifierHash` associated with the proof. The PBH nonce is used to ensure that a given World ID user does not use more than `n` transactions a month.

If the UserOp successfully clears all of these checks, a `PBH` event will be emitted indicating to the builder that this UserOp is a valid PBH user operation. The builder will only consider a “PBH” bundle for priority inclusion if all UserOps in the bundle emit a PBH event, and `aggregator` is specified as the `PBHSignatureAggregator`.
5 changes: 5 additions & 0 deletions pbh-verifier/foundry.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
via_ir = true
1 change: 1 addition & 0 deletions pbh-verifier/lib/BokkyPooBahsDateTimeLibrary
1 change: 1 addition & 0 deletions pbh-verifier/lib/forge-std
Submodule forge-std added at d3db4e
1 change: 1 addition & 0 deletions pbh-verifier/lib/world-id-contracts
Submodule world-id-contracts added at 583534
4 changes: 4 additions & 0 deletions pbh-verifier/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@world-id-contracts/=lib/world-id-contracts/src/
@account-abstraction/=lib/account-abstraction/contracts/
@BokkyPooBahsDateTimeLibrary/=lib/BokkyPooBahsDateTimeLibrary/contracts/
@helpers/=src/helpers/
149 changes: 149 additions & 0 deletions pbh-verifier/src/PBHVerifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {ByteHasher} from "./helpers/ByteHasher.sol";
import {PBHExternalNullifier} from "./helpers/PBHExternalNullifier.sol";
import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";
import {WorldIDImpl} from "@world-id-contracts/abstract/WorldIDProxy.sol";
import "@BokkyPooBahsDateTimeLibrary/BokkyPooBahsDateTimeLibrary.sol";

/// @title PBH Verifier Implementation Version 1
/// @author Worldcoin
/// @notice An implementation of a batch-based identity manager for the WorldID protocol.
/// @dev This is the implementation delegated to by a proxy.
contract PBHVerifierImplV1 is WorldIDImpl {

///////////////////////////////////////////////////////////////////////////////
/// A NOTE ON IMPLEMENTATION CONTRACTS ///
///////////////////////////////////////////////////////////////////////////////

// This contract is designed explicitly to operate from behind a proxy contract. As a result,
// there are a few important implementation considerations:
//
// - All updates made after deploying a given version of the implementation should inherit from
// the latest version of the implementation. This prevents storage clashes.
// - All functions that are less access-restricted than `private` should be marked `virtual` in
// order to enable the fixing of bugs in the existing interface.
// - Any function that reads from or modifies state (i.e. is not marked `pure`) must be
// annotated with the `onlyProxy` and `onlyInitialized` modifiers. This ensures that it can
// only be called when it has access to the data in the proxy, otherwise results are likely to
// be nonsensical.
// - This contract deals with important data for the PBH system. Ensure that all newly-added
// functionality is carefully access controlled using `onlyOwner`, or a more granular access
// mechanism.
// - Do not assign any contract-level variables at the definition site unless they are
// `constant`.
//
// Additionally, the following notes apply:
//
// - Initialisation and ownership management are not protected behind `onlyProxy` intentionally.
// This ensures that the contract can safely be disposed of after it is no longer used.
// - Carefully consider what data recovery options are presented as new functionality is added.
// Care must be taken to ensure that a migration plan can exist for cases where upgrades
// cannot recover from an issue or vulnerability.

///////////////////////////////////////////////////////////////////////////////
/// !!!!! DATA: DO NOT REORDER !!!!! ///
///////////////////////////////////////////////////////////////////////////////

// To ensure compatibility between upgrades, it is exceedingly important that no reordering of
// these variables takes place. If reordering happens, a storage clash will occur (effectively a
// memory safety error).

using ByteHasher for bytes;

///////////////////////////////////////////////////////////////////////////////
/// ERRORS ///
//////////////////////////////////////////////////////////////////////////////

/// @notice Thrown when attempting to reuse a nullifier
error InvalidNullifier();

///////////////////////////////////////////////////////////////////////////////
/// Events ///
//////////////////////////////////////////////////////////////////////////////

/// @notice Emitted when a verifier is updated in the lookup table.
///
/// @param nullifierHash The nullifier hash that was used.
event PBH(
uint256 indexed nullifierHash
);

///////////////////////////////////////////////////////////////////////////////
/// Vars ///
//////////////////////////////////////////////////////////////////////////////

/// @dev The World ID group ID (always 1)
uint256 internal immutable GROUP_ID = 1;

/// @dev The World ID instance that will be used for verifying proofs
IWorldIDGroups internal immutable worldId;

/// @dev Make this configurable
uint8 internal immutable numPbhPerMonth;

///////////////////////////////////////////////////////////////////////////////
/// Mappings ///
//////////////////////////////////////////////////////////////////////////////

/// @dev Whether a nullifier hash has been used already. Used to guarantee an action is only performed once by a single person
mapping(uint256 => bool) internal nullifierHashes;

///////////////////////////////////////////////////////////////////////////////
/// Functions ///
//////////////////////////////////////////////////////////////////////////////

/// @param _worldId The WorldID instance that will verify the proofs
constructor(
IWorldIDGroups _worldId,
uint8 _numPbhPerMonth
) {
worldId = _worldId;
numPbhPerMonth = _numPbhPerMonth;
}

/// @param root The root of the Merkle tree (returned by the JS widget).
/// @param sender The root of the Merkle tree (returned by the JS widget).
/// @param nonce The root of the Merkle tree (returned by the JS widget).
/// @param callData The root of the Merkle tree (returned by the JS widget).
/// @param nullifierHash The nullifier hash for this proof, preventing double signaling (returned by the JS widget).
/// @param proof The zero-knowledge proof that demonstrates the claimer is registered with World ID (returned by the JS widget).
function verifyPbhProof(
uint256 root,
address sender,
uint256 nonce,
bytes memory callData,
uint256 pbhExternalNullifier,
uint256 nullifierHash,
uint256[8] memory proof
) external {
// First, we make sure this person hasn't done this before
if (nullifierHashes[nullifierHash]) revert InvalidNullifier();

// We now generate the signal hash from the sender, nonce, and calldata
uint256 signalHash = abi.encodePacked(
sender,
nonce,
callData
).hashToField();

// Verify the external nullifier
PBHExternalNullifier.verify(pbhExternalNullifier, numPbhPerMonth);

// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
root,
GROUP_ID,
signalHash,
nullifierHash,
pbhExternalNullifier,
proof
);

// We now record the user has done this, so they can't do it again (proof of uniqueness)
nullifierHashes[nullifierHash] = true;

emit PBH(nullifierHash);
}
}
31 changes: 31 additions & 0 deletions pbh-verifier/src/PBHVerifierProxy.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {WorldIDProxy} from "@world-id-contracts/abstract/WorldIDProxy.sol";

/// @title PBH Verifier
/// @author Worldcoin
/// @notice An implementation of an on chain PBH verifier.
contract PBHVerifier is WorldIDProxy {
///////////////////////////////////////////////////////////////////////////////
/// !!!! DO NOT ADD MEMBERS HERE !!!! ///
///////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////
/// CONSTRUCTION ///
///////////////////////////////////////////////////////////////////////////////

/// @notice Constructs a new instance of the PBH Verifier.
/// @dev This constructor is only called once, and can be called with the encoded call necessary
/// to initialize the logic contract.
///
/// @param _logic The initial implementation (delegate) of the contract that this acts as a proxy
/// for.
/// @param _data If this is non-empty, it is used as the data for a `delegatecall` to `_logic`.
/// This is usually an encoded function call, and allows for initialising the storage of
/// the proxy in a way similar to a traditional solidity constructor.
constructor(address _logic, bytes memory _data) payable WorldIDProxy(_logic, _data) {
// !!!! DO NOT PUT PROGRAM LOGIC HERE !!!!
// It should go in the `initialize` function of the delegate instead.
}
}
12 changes: 12 additions & 0 deletions pbh-verifier/src/helpers/ByteHasher.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

library ByteHasher {
/// @dev Creates a keccak256 hash of a bytestring.
/// @param value The bytestring to hash
/// @return The hash of the specified value
/// @dev `>> 8` makes sure that the result is included in our field
function hashToField(bytes memory value) internal pure returns (uint256) {
return uint256(keccak256(abi.encodePacked(value))) >> 8;
}
}
61 changes: 61 additions & 0 deletions pbh-verifier/src/helpers/PBHExternalNullifier.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import "@BokkyPooBahsDateTimeLibrary/BokkyPooBahsDateTimeLibrary.sol";

/// @title PBHExternalNullifierLib
/// @notice Library for encoding, decoding, and verifying PBH external nullifiers.
/// External nullifiers are used to uniquely identify actions or events
/// within a specific year and month using a nonce.
/// @dev The encoding format is as follows:
/// - Bits 32-255: Empty
/// - Bits 16-31: Year
/// - Bits 8-15: Month
/// - Bits 0-7: Nonce
library PBHExternalNullifier {
/// @notice Thrown when the provided external nullifier year doesn't
/// match the current year
error InvalidExternalNullifierYear();

/// @notice Thrown when the provided external nullifier month doesn't
/// match the current month
error InvalidExternalNullifierMonth();

/// @notice Thrown when the provided external
/// nullifier pbhNonce >= numPbhPerMonth
error InvalidPbhNonce();

/// @notice Encodes a PBH external nullifier using the provided year, month, and nonce.
/// @param pbhNonce An 8-bit nonce value (0-255) used to uniquely identify the nullifier within a month.
/// @param month An 8-bit 1-indexed value representing the month (1-12).
/// @param year A 16-bit value representing the year (e.g., 2024).
/// @return The encoded PBHExternalNullifier.
function encode(uint8 pbhNonce, uint8 month, uint16 year) internal pure returns (uint256) {
require(month > 0 && month < 13, InvalidExternalNullifierMonth());
require(year < 10000, InvalidExternalNullifierYear());
return (uint32(year) << 16) | (uint32(month) << 8) | uint32(pbhNonce);
}

/// @notice Decodes an encoded PBHExternalNullifier into its constituent components.
/// @param externalNullifier The encoded external nullifier to decode.
/// @return pbhNonce The 8-bit nonce extracted from the external nullifier.
/// @return month The 8-bit month extracted from the external nullifier.
/// @return year The 16-bit year extracted from the external nullifier.
function decode(uint256 externalNullifier) internal pure returns (uint8 pbhNonce, uint8 month, uint16 year) {
year = uint16(externalNullifier >> 16);
month = uint8((externalNullifier >> 8) & 0xFF);
pbhNonce = uint8(externalNullifier & 0xFF);
}

/// @notice Verifies the validity of a PBHExternalNullifier by checking its components.
/// @param externalNullifier The external nullifier to verify.
/// @param numPbhPerMonth The maximum allowed value for the `pbhNonce` in the nullifier.
/// @dev This function ensures the external nullifier matches the current year and month,
/// and that the nonce does not exceed `numPbhPerMonth`.
function verify(uint256 externalNullifier, uint8 numPbhPerMonth) public view {
(uint8 pbhNonce, uint8 month, uint16 year) = PBHExternalNullifier.decode(externalNullifier);
require(year == BokkyPooBahsDateTimeLibrary.getYear(block.timestamp), InvalidExternalNullifierYear());
require(month == BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp), InvalidExternalNullifierMonth());
require(pbhNonce <= numPbhPerMonth, InvalidPbhNonce());
}
}
Loading
Loading