-
Notifications
You must be signed in to change notification settings - Fork 1
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
base: main
Are you sure you want to change the base?
PBHVerifier #74
Changes from 17 commits
8d96b05
57e0963
0d2fd70
42a210d
a4df740
a51e13d
ee0126f
bf756b0
c4c3203
938bac0
8d4469c
2da172d
990ae18
28aa3ce
35d1a28
6ddd845
7af6d9f
cc3d03a
3ec365a
010c3cd
cf16121
495b1f9
248091f
7332400
bf02c01
4374fe3
397d3e6
9ba0c75
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 |
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 |
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 |
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`. |
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 |
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/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// SPDX-License-Identifier: UNLICENSED | ||
pragma solidity ^0.8.13; | ||
|
||
import {Script, console} from "forge-std/Script.sol"; | ||
import {Counter} from "../src/Counter.sol"; | ||
|
||
contract CounterScript is Script { | ||
Counter public counter; | ||
|
||
function setUp() public {} | ||
|
||
function run() public { | ||
vm.startBroadcast(); | ||
|
||
counter = new Counter(); | ||
|
||
vm.stopBroadcast(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
// 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 "@BokkyPooBahsDateTimeLibrary/BokkyPooBahsDateTimeLibrary.sol"; | ||
|
||
contract PBHVerifier { | ||
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 | ||
); | ||
|
||
/////////////////////////////////////////////////////////////////////////////// | ||
/// Structs /// | ||
////////////////////////////////////////////////////////////////////////////// | ||
|
||
struct PBHPayload { | ||
uint256 root; | ||
uint256 nullifierHash; | ||
ExternalNullifier externalNullifier; | ||
uint256[8] proof; | ||
} | ||
|
||
/** | ||
* External Nullifier struct | ||
* @param pbhNonce - A nonce between 0 and numPbhPerMonth. | ||
* @param month - An integer representing the current month. | ||
* @param year - An integer representing the current year. | ||
*/ | ||
struct ExternalNullifier { | ||
uint8 pbhNonce; | ||
uint16 month; | ||
uint8 year; | ||
} | ||
|
||
/////////////////////////////////////////////////////////////////////////////// | ||
/// Vars /// | ||
////////////////////////////////////////////////////////////////////////////// | ||
|
||
/// @dev The World ID instance that will be used for verifying proofs | ||
IWorldIDGroups internal immutable worldId; | ||
|
||
/// @dev The World ID group ID (always 1) | ||
uint256 internal immutable groupId = 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Consider |
||
|
||
/// @dev Make this configurable | ||
uint8 internal immutable numPbhPerMonth; | ||
|
||
/// @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, | ||
groupId, | ||
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); | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.10; | ||
|
||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
// SPDX-License-Identifier: MIT | ||
pragma solidity ^0.8.10; | ||
|
||
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 Utilizes `PBHExternalNullifier` as a custom type for encoded nullifiers. | ||
/// @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 <= 9999, InvalidExternalNullifierYear()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: You can use
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isn't this a tautology ahah? a uint16 has to be <= type(uint16).max :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does it need to be |
||
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()); | ||
} | ||
} |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@0xOsiris could you explain your thoughts here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I mean this is probably up for a wider discussion, but instead of verifying the proof in the
PBHVerifier
we can instead just emit the proof. The builder will pick up the proof(s) when validating the bundle transaction, and verify them off chain. If any proof in the transaction is invalid the bundle will be dropped from the mempool.In this case the
PBHVerifier
is only in charge of storing the external nullifier hashes, and reverting the whole bundle on a rate limit. The benefit would be saving ~200,000 gas / UO, downside is higher trust assumptions in the builderThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah okay I'm into that! I think this could work. Let's chat tomorrow at standup.