Zuzalu Oracle
This is a wrapper contract around the deploy contracts of Semaphore, to be used by Zuzalu.
How
The Zuzalu API offers the latest root for every group. A trusted backend cron service, named zuzalu-updater, reads that API call and updates the on-chain stored root, so that people can generate proofs and verify them on-chain.
Deployed Addresses
Building with Zuzalu Oracle
Understand the Semaphore Protocol
First, make sure you understand how the proof and group system works in Semaphore.
Check out their docs!
As an on-chain application
- User visits the application's website
- The application connects to the oracle and downloads the latest root of each group
- The user wants to perform some on-chain activity that is gated to zuzalu groups members. The application selects the group the user will verify against
- The website calculates a URL on the Zupass API for the group, given the latest root/group combination
- The website invokes Zupass asking for a group membership with a group URL from step 4
- The website extracts the semaphore proof from the returned PCD and uploads it to the smart contract
- The smart contract verifies the semaphore proof, using the oracle, and depending on the return (true|false) it decides what to do
How to integrate in Solidity
Read the smart contract docs!
Install
yarn add zuzalu-oracle
and then just
ZuzaluOracle oracle = ZuzaluOracle(ORACLE_ADDRESS);
uint[8] proof;
// Use the latest root of group Residents
oracle.verify(0, 0, 0, proof, ZuzaluOracle.Groups.Residents);
How to integrate in Typescript
yarn add zuzalu-oracle
- Import it as follows. The example is from zuzalu-updater
import { ZuzaluOracle__factory } from 'zuzalu-oracle';
export default {
async scheduled(
controller: ScheduledController,
env: Env,
ctx: ExecutionContext
): Promise<void> {
const provider = new ethers.JsonRpcProvider(env.ETH_RPC_URL);
const wallet = new ethers.Wallet(env.ETH_PRIVATE_KEY, provider);
const oracle = ZuzaluOracle__factory.connect(env.CONTRACT_ADDRESS, wallet);
const latestRoots = await oracle.getLastRoots();
...
License
MIT
ZuzaluOracle
Inherits: Owned
Authors: Mark Tyneway mark.tyneway@gmail.com, Odysseas.eth odyslam@gmail.com
State Variables
VERSION
string internal constant VERSION = "0.0.2";
$visitorRoots
An array of roots for the "visitors" group
uint256[] $visitorRoots;
$residentRoots
An arry of roots for the "residents" group
uint256[] $residentRoots;
$organizerRoots
An array of roots for the "organizers" group
uint256[] $organizerRoots;
$participantRoots
An array of roots for the "participants" group
uint256[] $participantRoots;
$visitorsToDepth
A mapping of roots to their depth for the "visitors" groups
mapping(uint256 => uint256) public $visitorsToDepth;
$residentsToDepth
A mapping of roots to their depth for the "residents" groups
mapping(uint256 => uint256) public $residentsToDepth;
$organizersToDepth
A mapping of roots to their depth for the "organizers" groups
mapping(uint256 => uint256) public $organizersToDepth;
$participantsToDepth
A mapping of roots to their depth for the "participants" groups
mapping(uint256 => uint256) public $participantsToDepth;
VERIFIER
The address of the Semaphore verifier contract
address public immutable VERIFIER;
Functions
constructor
constructor(address _owner, address _verifier) Owned(_owner);
_initArrays
Initialize the arrays so that the getLastRoots() function does not revert and the backend does not have to implement a special case for the first time the contract is deployed
function _initArrays() internal;
updateGroups
Updates the roots and depths of all the groups
The order of the roots and depths must match the order of the groups. If you don't want to update a group, pass in 0 for the root.
function updateGroups(uint256[4] calldata _roots, uint256[4] calldata _depths) public onlyOwner;
Parameters
Name | Type | Description |
---|---|---|
_roots | uint256[4] | An array of roots for each group |
_depths | uint256[4] | An array of depths for each group |
_update
Updates the roots and depths of a particular group
function _update(uint256[] storage _roots, mapping(uint256 => uint256) storage _toDepth, uint256 _root, uint256 _depth)
internal;
verify
Verifies a Semaphore proof for a particular signal and group. It returns true if the proof is valid, and false otherwise. It will check against historic roots in case the latest root doesn't work. As the latest root may be updated between proof generation and verification, we want to offer a better UX to the users by enabling by default to fall to check up to a limit of previous roots.
It will check up to 2 roots in the past (latest + 2 roots).
The backend updates the root every few hours, so at worse case the user will generate the proof and right away the backend will update the latest root. By checking the historic roots as well, we allow the user to still use the proof for a small extra cost in gas.
function verify(
uint256 _nullifierHash,
uint256 _signal,
uint256 _externalNullifier,
uint256[8] calldata _proof,
Groups _group
) external returns (bool);
Parameters
Name | Type | Description |
---|---|---|
_nullifierHash | uint256 | The hash of the nullifier |
_signal | uint256 | The signal to verify |
_externalNullifier | uint256 | The external nullifier |
_proof | uint256[8] | The proof to verify |
_group | Groups | The group to verify the proof for { None, Visitors, Residents, Organizers, Participants } |
verify
Verifies a Semaphore proof for a particular signal and group. It returns true if the proof is valid, and false otherwise.
It will not check the latest root, but a root in the past, as specified by the _historicRootIndex.
function verify(
uint256 _nullifierHash,
uint256 _signal,
uint256 _externalNullifier,
uint256[8] calldata _proof,
uint256 _historicRootIndex,
Groups _group
) external returns (bool);
Parameters
Name | Type | Description |
---|---|---|
_nullifierHash | uint256 | The hash of the nullifier |
_signal | uint256 | The signal to verify |
_externalNullifier | uint256 | The external nullifier |
_proof | uint256[8] | The proof to verify |
_historicRootIndex | uint256 | The index of the array that contains the roots for the specific group. |
_group | Groups | The group to verify the proof for { None, Visitors, Residents, Organizers, Participants } |
_verifyHistoric
Verifies a Semaphore proof for a particular signal and group. It returns true if the proof is valid, and false otherwise. It will also return false if the index is out of bounds of the array for the specific group.
function _verifyHistoric(
uint256 _nullifierHash,
uint256 _signal,
uint256 _externalNullifier,
uint256[8] calldata _proof,
Groups _group,
uint256 _historicIndex
) internal returns (bool);
Parameters
Name | Type | Description |
---|---|---|
_nullifierHash | uint256 | The hash of the nullifier |
_signal | uint256 | The signal to verify |
_externalNullifier | uint256 | The external nullifier |
_proof | uint256[8] | The proof to verify |
_group | Groups | The group to verify the proof for { None, Visitors, Residents, Organizers, Participants } |
_historicIndex | uint256 | The index of the array that contains the roots for the specific group. |
_getRootAndDepth
Get the root and and the depth from the data structures that are passed as inputs
The function accepts a storage pointer, not the actual data
function _getRootAndDepth(uint256[] storage roots, uint256 index, mapping(uint256 => uint256) storage depths)
internal
view
returns (uint256, uint256);
Parameters
Name | Type | Description |
---|---|---|
roots | uint256[] | The roots array |
index | uint256 | The index of the root to get |
depths | mapping(uint256 => uint256) | The depths mapping |
verifyUnsafe
Verifies a Semaphore proof for a particular signal and group. The group is specified by the root and depth which is not one of the groups defined in the contract. It returns true if the proof is valid, and false otherwise.
function verifyUnsafe(
uint256 _root,
uint256 _depth,
uint256 _nullifierHash,
uint256 _signal,
uint256 _externalNullifier,
uint256[8] calldata _proof
) external returns (bool);
Parameters
Name | Type | Description |
---|---|---|
_root | uint256 | The root of the merkle tree |
_depth | uint256 | The depth of the merkle tree |
_nullifierHash | uint256 | The hash of the nullifier |
_signal | uint256 | The signal to verify |
_externalNullifier | uint256 | The external nullifier |
_proof | uint256[8] | The proof to verify |
_verify
Thin wrapper arround the SemaphoreVerifier contract's verifyProof function
function _verify(
uint256 _root,
uint256 _depth,
uint256 _nullifierHash,
uint256 _signal,
uint256 _externalNullifier,
uint256[8] calldata _proof
) internal returns (bool);
version
function version() public pure returns (string memory);
getLastRoot
Returns the current root of the group provided in the argument
function getLastRoot(Groups _group) external view returns (uint256);
Parameters
Name | Type | Description |
---|---|---|
_group | Groups | The group to get the root for { None, Visitors, Residents, Organizers, Participants } |
Returns
Name | Type | Description |
---|---|---|
<none> | uint256 | root The current root of the group |
getLastRoots
Returns the latest root of all the groups in an array
function getLastRoots() external view returns (uint256[4] memory);
Returns
Name | Type | Description |
---|---|---|
<none> | uint256[4] | roots The latest roots of all the groups [participants, residents, visitors, organizers] |
getLastDepths
function getLastDepths() external view returns (uint256[4] memory);
Events
UpdateGroups
The groups have been updated with new latest roots and depths
event UpdateGroups(uint256[4] roots, uint256[4] depths);
Verify
A new succesful verification has been made for a particular Zuzalu group and signal
event Verify(uint256 indexed signal, uint256 indexed root, Groups indexed _group);
Errors
InvalidGroup
The group is not one of the groups in the Groups enum
error InvalidGroup();
Enums
Groups
The official groups by Zuzalu as defined and used in the backend
enum Groups {
None,
Participants,
Residents,
Visitors,
Organizers
}