Skip to content

Commit

Permalink
Merge pull request #99 from worldcoin/kit/contract-tests
Browse files Browse the repository at this point in the history
Update `PBHExternalNullifier` tests
  • Loading branch information
0xKitsune authored Dec 30, 2024
2 parents 66dd9dd + 5ec753e commit b447dc4
Show file tree
Hide file tree
Showing 13 changed files with 291 additions and 130 deletions.
73 changes: 37 additions & 36 deletions contracts/src/PBHEntryPointImplV1.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";
import {IWorldID} from "./interfaces/IWorldID.sol";
import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {IPBHEntryPoint} from "./interfaces/IPBHEntryPoint.sol";
import {IMulticall3} from "./interfaces/IMulticall3.sol";
Expand Down Expand Up @@ -67,7 +67,7 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
//////////////////////////////////////////////////////////////////////////////

event PBHEntryPointImplInitialized(
IWorldIDGroups indexed worldId, IEntryPoint indexed entryPoint, uint8 indexed numPbhPerMonth, address multicall3
IWorldID indexed worldId, IEntryPoint indexed entryPoint, uint8 indexed numPbhPerMonth, address multicall3
);

/// @notice Emitted once for each successful PBH verification.
Expand Down Expand Up @@ -99,7 +99,7 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
uint256 internal constant _GROUP_ID = 1;

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

/// @dev The EntryPoint where Aggregated PBH Bundles will be proxied to.
IEntryPoint public entryPoint;
Expand Down Expand Up @@ -141,7 +141,7 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
/// @param _numPbhPerMonth The number of allowed PBH transactions per month.
///
/// @custom:reverts string If called more than once at the same initialisation number.
function initialize(IWorldIDGroups _worldId, IEntryPoint _entryPoint, uint8 _numPbhPerMonth, address _multicall3)
function initialize(IWorldID _worldId, IEntryPoint _entryPoint, uint8 _numPbhPerMonth, address _multicall3)
external
reinitializer(1)
{
Expand Down Expand Up @@ -172,6 +172,37 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
/// Functions ///
//////////////////////////////////////////////////////////////////////////////

/// @param pbhPayload The PBH payload containing the proof data.
function verifyPbh(uint256 signalHash, PBHPayload memory pbhPayload)
public
view
virtual
onlyInitialized
onlyProxy
{
// First, we make sure this nullifier has not been used before.
if (nullifierHashes[pbhPayload.nullifierHash]) {
revert InvalidNullifier();
}

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

// If worldId address is set, proceed with on chain verification,
// otherwise assume verification has been done off chain by the builder.
if (address(worldId) != address(0)) {
// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
pbhPayload.root,
_GROUP_ID,
signalHash,
pbhPayload.nullifierHash,
pbhPayload.pbhExternalNullifier,
pbhPayload.proof
);
}
}

/// Execute a batch of PackedUserOperation with Aggregators
/// @param opsPerAggregator - The operations to execute, grouped by aggregator (or address(0) for no-aggregator accounts).
/// @param beneficiary - The address to receive the fees.
Expand Down Expand Up @@ -229,40 +260,10 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
emit PBH(msg.sender, pbhPayload);
}

/// @param pbhPayload The PBH payload containing the proof data.
function verifyPbh(uint256 signalHash, PBHPayload memory pbhPayload)
public
view
virtual
onlyInitialized
onlyProxy
{
// First, we make sure this nullifier has not been used before.
if (nullifierHashes[pbhPayload.nullifierHash]) {
revert InvalidNullifier();
}

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

// If worldId address is set, proceed with on chain verification,
// otherwise assume verification has been done off chain by the builder.
if (address(worldId) != address(0)) {
// We now verify the provided proof is valid and the user is verified by World ID
worldId.verifyProof(
pbhPayload.root,
_GROUP_ID,
signalHash,
pbhPayload.nullifierHash,
pbhPayload.pbhExternalNullifier,
pbhPayload.proof
);
}
}

/// @notice Sets the number of PBH transactions allowed per month.
/// @param _numPbhPerMonth The number of allowed PBH transactions per month.
function setNumPbhPerMonth(uint8 _numPbhPerMonth) external virtual onlyOwner onlyProxy onlyInitialized {
// TODO: require(_numPbhPerMonth > 0, "PBHEntryPointImplV1: numPbhPerMonth must be greater than 0");
numPbhPerMonth = _numPbhPerMonth;
emit NumPbhPerMonthSet(_numPbhPerMonth);
}
Expand All @@ -271,7 +272,7 @@ contract PBHEntryPointImplV1 is IPBHEntryPoint, WorldIDImpl, ReentrancyGuard {
/// @notice Sets the World ID instance that will be used for verifying proofs.
/// @param _worldId The World ID instance that will be used for verifying proofs.
function setWorldId(address _worldId) external virtual onlyOwner onlyProxy onlyInitialized {
worldId = IWorldIDGroups(_worldId);
worldId = IWorldID(_worldId);
emit WorldIdSet(_worldId);
}
}
8 changes: 7 additions & 1 deletion contracts/src/helpers/PBHExternalNullifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import "@BokkyPooBahsDateTimeLibrary/BokkyPooBahsDateTimeLibrary.sol";
/// - Bits 16-31: Month
/// - Bits 8-15: Nonce
/// - Bits 0-7: Version

//TODO: move this to a lib dir
library PBHExternalNullifier {
/// @notice Thrown when the provided external nullifier doesn't
/// contain the correct leading zeros
Expand Down Expand Up @@ -46,6 +48,8 @@ library PBHExternalNullifier {
return (uint256(year) << 24) | (uint256(month) << 16) | (uint256(pbhNonce) << 8) | uint256(version);
}

// TODO: should we provide an encodeV1 helper function?

/// @notice Decodes an encoded PBHExternalNullifier into its constituent components.
/// @param externalNullifier The encoded external nullifier to decode.
/// @return version The 8-bit version extracted from the external nullifier.
Expand All @@ -63,6 +67,8 @@ library PBHExternalNullifier {
version = uint8(externalNullifier & 0xFF);
}

// TODO: revisit, maybe move this function or update the PBH Entrypoint to update the ext nullifier lib for forward compatibility

/// @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.
Expand All @@ -76,6 +82,6 @@ library PBHExternalNullifier {
require(version == V1, InvalidExternalNullifierVersion());
require(year == BokkyPooBahsDateTimeLibrary.getYear(block.timestamp), InvalidExternalNullifierYear());
require(month == BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp), InvalidExternalNullifierMonth());
require(pbhNonce < numPbhPerMonth, InvalidPbhNonce());
require(pbhNonce <= numPbhPerMonth, InvalidPbhNonce());
}
}
4 changes: 2 additions & 2 deletions contracts/src/interfaces/IPBHEntryPoint.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";
import {IWorldID} from "./IWorldID.sol";
import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {IMulticall3} from "./IMulticall3.sol";

Expand All @@ -25,7 +25,7 @@ interface IPBHEntryPoint {

function pbhMulticall(IMulticall3.Call3[] calldata calls, PBHPayload calldata pbhPayload) external;

function initialize(IWorldIDGroups worldId, IEntryPoint entryPoint, uint8 _numPbhPerMonth, address _multicall3)
function initialize(IWorldID worldId, IEntryPoint entryPoint, uint8 _numPbhPerMonth, address _multicall3)
external;

function validateSignaturesCallback(bytes32 hashedOps) external view;
Expand Down
38 changes: 38 additions & 0 deletions contracts/src/interfaces/IWorldID.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

interface IWorldID {
///////////////////////////////////////////////////////////////////////////////
/// ERRORS ///
///////////////////////////////////////////////////////////////////////////////

/// @notice Thrown when attempting to validate a root that has expired.
error ExpiredRoot();

/// @notice Thrown when attempting to validate a root that has yet to be added to the root
/// history.
error NonExistentRoot();

/// @notice Verifies a WorldID zero knowledge proof.
/// @dev Note that a double-signaling check is not included here, and should be carried by the
/// caller.
/// @dev It is highly recommended that the implementation is restricted to `view` if possible.
///
/// @param groupId The group identifier for the group to verify a proof for.
/// @param root The of the Merkle tree
/// @param signalHash A keccak256 hash of the Semaphore signal
/// @param nullifierHash The nullifier hash
/// @param externalNullifierHash A keccak256 hash of the external nullifier
/// @param proof The zero-knowledge proof
///
/// @custom:reverts string If the `proof` is invalid.
/// @custom:reverts NoSuchGroup If the provided `groupId` references a group that does not exist.
function verifyProof(
uint256 root,
uint256 groupId,
uint256 signalHash,
uint256 nullifierHash,
uint256 externalNullifierHash,
uint256[8] calldata proof
) external view;
}
8 changes: 4 additions & 4 deletions contracts/test/PBHEntryPointConstruction.t.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import {Setup} from "./Setup.sol";
import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";
import {TestSetup} from "./TestSetup.sol";
import {IWorldID} from "../src/interfaces/IWorldID.sol";
import {IEntryPoint} from "@account-abstraction/contracts/interfaces/IEntryPoint.sol";
import {PBHEntryPointImplV1} from "../src/PBHEntryPointImplV1.sol";
import {IPBHEntryPoint} from "../src/interfaces/IPBHEntryPoint.sol";
Expand All @@ -13,7 +13,7 @@ import {PBHEntryPoint} from "../src/PBHEntryPoint.sol";
/// @author Worldcoin
/// @dev This test suite tests both the proxy and the functionality of the underlying implementation
/// so as to test everything in the context of how it will be deployed.
contract PBHEntryPointConstruction is Setup {
contract PBHEntryPointConstruction is TestSetup {
/// @notice Taken from Initializable.sol
event Initialized(uint8 version);

Expand All @@ -28,7 +28,7 @@ contract PBHEntryPointConstruction is Setup {
}

/// @notice Tests that it is possible to properly construct and initialise a router.
function testCanConstructRouterWithDelegate(IWorldIDGroups dummy, IEntryPoint entryPoint) public {
function testCanConstructRouterWithDelegate(IWorldID dummy, IEntryPoint entryPoint) public {
// Setup
vm.expectEmit(true, true, true, true);
emit Initialized(1);
Expand Down
111 changes: 111 additions & 0 deletions contracts/test/PBHEntryPointImplV1.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";
import {MockWorldIDGroups} from "./mocks/MockWorldIDGroups.sol";
import {CheckInitialized} from "@world-id-contracts/utils/CheckInitialized.sol";
import {WorldIDImpl} from "@world-id-contracts/abstract/WorldIDImpl.sol";
import {ByteHasher} from "@helpers/ByteHasher.sol";
import {IPBHEntryPoint} from "../src/interfaces/IPBHEntryPoint.sol";
import {PBHEntryPointImplV1} from "../src/PBHEntryPointImplV1.sol";

import "@BokkyPooBahsDateTimeLibrary/BokkyPooBahsDateTimeLibrary.sol";
import "@helpers/PBHExternalNullifier.sol";
import {TestSetup} from "./TestSetup.sol";

/// @title PBHVerifer Verify Tests
/// @notice Contains tests for the pbhVerifier
/// @author Worldcoin
contract PBHEntryPointImplV1Test is TestSetup {
using ByteHasher for bytes;

event PBH(address indexed sender, IPBHEntryPoint.PBHPayload payload);
event NumPbhPerMonthSet(uint8 indexed numPbhPerMonth);
event WorldIdSet(address indexed worldId);

/// @notice Test payload for the PBHVerifier
IPBHEntryPoint.PBHPayload public testPayload = IPBHEntryPoint.PBHPayload({
root: 1,
pbhExternalNullifier: getValidPBHExternalNullifier(),
nullifierHash: 1,
proof: [uint256(0), 0, 0, 0, 0, 0, 0, 0]
});

uint256 internal nonce = 1;
address internal sender = address(0x123);
bytes internal testCallData = hex"deadbeef";

// TODO: move this to test utils
function getValidPBHExternalNullifier() public view returns (uint256) {
uint8 month = uint8(BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp));
uint16 year = uint16(BokkyPooBahsDateTimeLibrary.getYear(block.timestamp));
return PBHExternalNullifier.encode(PBHExternalNullifier.V1, 0, month, year);
}

// TODO:
function test_verifyPbh() public {
uint256 signalHash = abi.encodePacked(sender, nonce, testCallData).hashToField();

pbhEntryPoint.verifyPbh(signalHash, testPayload);

// TODO: update to use mock work id
// Expect revert when proof verification fails
MockWorldIDGroups(address(worldIDGroups)).setVerifyProofSuccess(false);
vm.expectRevert("Proof verification failed");
pbhEntryPoint.verifyPbh(signalHash, testPayload);

// Now expect success
MockWorldIDGroups(address(worldIDGroups)).setVerifyProofSuccess(true);
pbhEntryPoint.verifyPbh(signalHash, testPayload);
}

// TODO:
function test_verifyPbh_RevertIfInvalidNullifier() public {}

/// @notice Test that setNumPBHPerMonth works as expected
function testSetNumPBHPerMonth() public {
uint256 signalHash = abi.encodePacked(sender, nonce, testCallData).hashToField();

MockWorldIDGroups(address(worldIDGroups)).setVerifyProofSuccess(true);
uint8 month = uint8(BokkyPooBahsDateTimeLibrary.getMonth(block.timestamp));
uint16 year = uint16(BokkyPooBahsDateTimeLibrary.getYear(block.timestamp));

// Value starts at 30, make sure 30 reverts.
testPayload.pbhExternalNullifier = PBHExternalNullifier.encode(PBHExternalNullifier.V1, 30, month, year);

testPayload.nullifierHash = 0;
vm.expectRevert(PBHExternalNullifier.InvalidPbhNonce.selector);
pbhEntryPoint.verifyPbh(signalHash, testPayload);

// Increase numPbhPerMonth from non owner, expect revert
vm.prank(address(123));
vm.expectRevert("Ownable: caller is not the owner");
pbhEntryPoint.setNumPbhPerMonth(40);

// Increase numPbhPerMonth from owner
vm.prank(thisAddress);
vm.expectEmit(true, false, false, false);
emit NumPbhPerMonthSet(40);
pbhEntryPoint.setNumPbhPerMonth(40);

// Try again, it should work
testPayload.pbhExternalNullifier = PBHExternalNullifier.encode(PBHExternalNullifier.V1, 30, month, year);
testPayload.nullifierHash = 1;
pbhEntryPoint.verifyPbh(signalHash, testPayload);
}

function testSetWorldId() public {
vm.expectEmit(true, false, false, false);
emit WorldIdSet(address(0x123));
pbhEntryPoint.setWorldId(address(0x123));
}

function test_FailSetWorldId_NotOwner(address naughty) public {
if (naughty == thisAddress) {
return;
}
vm.prank(naughty);
vm.expectRevert("Ownable: caller is not the owner");
pbhEntryPoint.setWorldId(address(0x123));
}
}
4 changes: 2 additions & 2 deletions contracts/test/PBHEntryPointOwnershipManagement.t.sol
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.21;

import {Setup} from "./Setup.sol";
import {TestSetup} from "./TestSetup.sol";
import {IWorldIDGroups} from "@world-id-contracts/interfaces/IWorldIDGroups.sol";

import {CheckInitialized} from "@world-id-contracts/utils/CheckInitialized.sol";
Expand All @@ -15,7 +15,7 @@ import {WorldIDImpl} from "@world-id-contracts/abstract/WorldIDImpl.sol";
/// @author Worldcoin
/// @dev This test suite tests both the proxy and the functionality of the underlying implementation
/// so as to test everything in the context of how it will be deployed.
contract PBHVerifierRouting is Setup {
contract PBHVerifierRouting is TestSetup {
address internal pbhEntryPointAddress;

function setUp() public override {
Expand Down
Loading

0 comments on commit b447dc4

Please sign in to comment.