Zuzalu Oracle

zuzalu logo

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

  1. User visits the application's website
  2. The application connects to the oracle and downloads the latest root of each group
  3. 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
  4. The website calculates a URL on the Zupass API for the group, given the latest root/group combination
  5. The website invokes Zupass asking for a group membership with a group URL from step 4
  6. The website extracts the semaphore proof from the returned PCD and uploads it to the smart contract
  7. 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

  1. yarn add zuzalu-oracle
  2. 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

Git Source

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

NameTypeDescription
_rootsuint256[4]An array of roots for each group
_depthsuint256[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

NameTypeDescription
_nullifierHashuint256The hash of the nullifier
_signaluint256The signal to verify
_externalNullifieruint256The external nullifier
_proofuint256[8]The proof to verify
_groupGroupsThe 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

NameTypeDescription
_nullifierHashuint256The hash of the nullifier
_signaluint256The signal to verify
_externalNullifieruint256The external nullifier
_proofuint256[8]The proof to verify
_historicRootIndexuint256The index of the array that contains the roots for the specific group.
_groupGroupsThe 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

NameTypeDescription
_nullifierHashuint256The hash of the nullifier
_signaluint256The signal to verify
_externalNullifieruint256The external nullifier
_proofuint256[8]The proof to verify
_groupGroupsThe group to verify the proof for { None, Visitors, Residents, Organizers, Participants }
_historicIndexuint256The 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

NameTypeDescription
rootsuint256[]The roots array
indexuint256The index of the root to get
depthsmapping(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

NameTypeDescription
_rootuint256The root of the merkle tree
_depthuint256The depth of the merkle tree
_nullifierHashuint256The hash of the nullifier
_signaluint256The signal to verify
_externalNullifieruint256The external nullifier
_proofuint256[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

NameTypeDescription
_groupGroupsThe group to get the root for { None, Visitors, Residents, Organizers, Participants }

Returns

NameTypeDescription
<none>uint256root 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

NameTypeDescription
<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
}