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 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions .github/workflows/foundry.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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
working-directory: contracts
run: |
forge fmt --check
id: fmt

- name: Run Forge build
working-directory: contracts
run: |
forge build --sizes
id: build

- name: Run Forge tests
working-directory: contracts
run: |
forge test -vvv
id: test
9 changes: 9 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[submodule "contracts/lib/forge-std"]
path = contracts/lib/forge-std
url = https://github.com/foundry-rs/forge-std
[submodule "contracts/lib/world-id-contracts"]
path = contracts/lib/world-id-contracts
url = https://github.com/worldcoin/world-id-contracts
[submodule "contracts/lib/BokkyPooBahsDateTimeLibrary"]
path = contracts/lib/BokkyPooBahsDateTimeLibrary
url = https://github.com/bokkypoobah/BokkyPooBahsDateTimeLibrary
14 changes: 14 additions & 0 deletions contracts/.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
18 changes: 18 additions & 0 deletions contracts/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 contracts/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 contracts/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 contracts/lib/BokkyPooBahsDateTimeLibrary
1 change: 1 addition & 0 deletions contracts/lib/forge-std
Submodule forge-std added at 1eea5b
1 change: 1 addition & 0 deletions contracts/lib/world-id-contracts
Submodule world-id-contracts added at f41cd6
6 changes: 6 additions & 0 deletions contracts/remappings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@world-id-contracts/=lib/world-id-contracts/src/
@account-abstraction/=lib/account-abstraction/contracts/
@BokkyPooBahsDateTimeLibrary/=lib/BokkyPooBahsDateTimeLibrary/contracts/
@helpers/=src/helpers/

openzeppelin-contracts/=lib/world-id-contracts/lib/openzeppelin-contracts/contracts/
31 changes: 31 additions & 0 deletions contracts/src/PBHVerifier.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.
}
}
184 changes: 184 additions & 0 deletions contracts/src/PBHVerifierImplV1.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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/WorldIDImpl.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();

/// @notice Thrown if the World ID instance is set to the zero address
error InvalidWorldId();

///////////////////////////////////////////////////////////////////////////////
/// 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);

event PBHVerifierImplInitialized(uint8 indexed numPbhPerMonth);

///////////////////////////////////////////////////////////////////////////////
/// 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 worldId;

/// @dev Make this configurable
uint8 internal 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;

///////////////////////////////////////////////////////////////////////////////
/// INITIALIZATION ///
///////////////////////////////////////////////////////////////////////////////

/// @notice Constructs the contract.
constructor() {
// When called in the constructor, this is called in the context of the implementation and
// not the proxy. Calling this thereby ensures that the contract cannot be spuriously
// initialized on its own.
_disableInitializers();
}

/// @notice Initializes the contract.
/// @dev Must be called exactly once.
/// @dev This is marked `reinitializer()` to allow for updated initialisation steps when working
/// with upgrades based upon this contract. Be aware that there are only 256 (zero-indexed)
/// initialisations allowed, so decide carefully when to use them. Many cases can safely be
/// replaced by use of setters.
/// @dev This function is explicitly not virtual as it does not make sense to override even when
/// upgrading. Create a separate initializer function instead.
///
/// @param _worldId The World ID instance that will be used for verifying proofs.
/// @param _numPbhPerMonth The number of allowed PBH transactions per month.
///
/// @custom:reverts string If called more than once at the same initialisation number.
/// @custom:reverts InvalidWorldId if `_worldId` is set to the zero address
function initialize(IWorldIDGroups _worldId, uint8 _numPbhPerMonth) public reinitializer(1) {
if (address(_worldId) == address(0)) {
revert InvalidWorldId();
}

// First, ensure that all of the parent contracts are initialised.
__delegateInit();

worldId = _worldId;
numPbhPerMonth = _numPbhPerMonth;

// Say that the contract is initialized.
__setInitialized();

emit PBHVerifierImplInitialized(_numPbhPerMonth);
}

/// @notice Responsible for initialising all of the supertypes of this contract.
/// @dev Must be called exactly once.
/// @dev When adding new superclasses, ensure that any initialization that they need to perform
/// is accounted for here.
///
/// @custom:reverts string If called more than once.
function __delegateInit() internal virtual onlyInitializing {
__WorldIDImpl_init();
}

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

/// @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 virtual onlyProxy onlyInitialized {
// 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);
}
}
12 changes: 12 additions & 0 deletions contracts/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;
}
}
Loading
Loading