Skip to main content

Overview

The NullifierRegistry is a critical security contract that tracks nullifiers used in zero-knowledge proofs to prevent double-spending attacks. It maintains a record of all nullifiers that have been used to claim payments, ensuring each proof can only be used once. Contract Location: contracts/registries/NullifierRegistry.sol

Purpose

The NullifierRegistry serves as a double-spend prevention mechanism:
  • Nullifier Tracking: Records nullifiers from ZK proofs to prevent reuse
  • Access Control: Restricts write access to authorized contracts only
  • Global State: Provides a shared nullifier registry across all payment verifiers
  • Security Enforcement: Prevents the same off-chain payment proof from being used multiple times

Key Concepts

What is a Nullifier?

A nullifier is a unique identifier derived from a zero-knowledge proof that represents a specific off-chain payment. Once a nullifier is recorded, any attempt to use the same proof again can be detected and rejected.

Writer Permissions

The registry implements a permission system where only authorized “writer” contracts (typically payment verifiers) can add nullifiers.

State Variables

mapping(bytes32 => bool) public isNullified;
mapping(address => bool) public isWriter;
address[] public writers;
  • isNullified: Tracks whether a specific nullifier has been used
  • isWriter: Maps addresses to their write permission status
  • writers: Array of all addresses with write permissions

Core Functions

Adding Nullifiers

function addNullifier(bytes32 _nullifier) external onlyWriter
Records a nullifier as used. This is called by payment verifier contracts after successfully verifying a proof. Parameters:
  • _nullifier: The nullifier hash from the ZK proof
Requirements:
  • Caller must be a registered writer
  • Nullifier must not already exist
Emits: NullifierAdded(bytes32 nullifier, address indexed writer) Reference: contracts/registries/NullifierRegistry.sol:41

Managing Write Permissions

function addWritePermission(address _newWriter) external onlyOwner
function removeWritePermission(address _removedWriter) external onlyOwner
Grant or revoke write permissions to addresses (typically payment verifier contracts). Parameters:
  • _newWriter / _removedWriter: Address to grant or revoke permissions
Requirements:
  • Only callable by contract owner
  • Writer must not already have/lack permissions (prevents duplicates)
Emits: WriterAdded(address writer) or WriterRemoved(address writer) Reference: contracts/registries/NullifierRegistry.sol:56 and :70

Querying Writers

function getWriters() external view returns(address[] memory)
Returns array of all addresses with write permissions. Reference: contracts/registries/NullifierRegistry.sol:81

Integration with Core Contracts

Payment Verifiers

Payment verifier contracts (e.g., UnifiedPaymentVerifier) are the primary users of the NullifierRegistry:
  1. Verifier receives a ZK proof containing a nullifier
  2. Verifier checks if nullifier has been used: isNullified[nullifier]
  3. If proof is valid and nullifier is unused, verifier calls addNullifier()
  4. Nullifier is permanently marked as used
Reference: contracts/unifiedVerifier/BaseUnifiedPaymentVerifier.sol:64

Orchestrator Registry

Payment verifiers that write to the NullifierRegistry must be authorized by being granted write permissions during deployment or configuration. Reference: contracts/unifiedVerifier/UnifiedPaymentVerifier.sol:98

Access Control

Modifiers

modifier onlyWriter() {
    require(isWriter[msg.sender], "Only addresses with write permissions can call");
    _;
}
Restricts addNullifier() to authorized writer contracts only. Reference: contracts/registries/NullifierRegistry.sol:21

Owner Functions

  • addWritePermission(): Add authorized writers
  • removeWritePermission(): Remove writer access

Writer Functions

  • addNullifier(): Record used nullifiers

Public View Functions

  • isNullified[nullifier]: Check if nullifier has been used (publicly accessible)
  • isWriter[address]: Check if address has write permissions
  • getWriters(): Get list of all writers

Events

event NullifierAdded(bytes32 nullifier, address indexed writer);
event WriterAdded(address writer);
event WriterRemoved(address writer);
These events enable:
  • Tracking which verifier added which nullifiers
  • Monitoring changes to write permissions
  • Auditing nullifier usage patterns

Security Model

Double-Spend Prevention

The registry prevents double-spending through:
  1. Uniqueness Enforcement: Each nullifier can only be added once
  2. Revert on Duplicate: Attempting to add an existing nullifier will revert
  3. Permanent Record: Nullifiers cannot be removed once added

Access Control Security

  1. Restricted Writers: Only authorized contracts can add nullifiers
  2. Owner-Controlled: Only owner can modify writer permissions
  3. No Removal: Nullifiers are permanent (cannot be deleted)

Integration Security

Proper integration requires:
  1. Verifiers must check isNullified[nullifier] before processing
  2. Verifiers must call addNullifier() after successful verification
  3. Writer permissions must only be granted to audited verifier contracts

Usage Pattern

Typical flow in a payment verifier:
// 1. Extract nullifier from proof
bytes32 nullifier = proof.nullifier;

// 2. Check if already used
require(!nullifierRegistry.isNullified(nullifier), "Nullifier already used");

// 3. Verify ZK proof
require(verifyProof(proof), "Invalid proof");

// 4. Record nullifier
nullifierRegistry.addNullifier(nullifier);

// 5. Process payment
// ...

Deployment Considerations

  1. Single Registry: Typically one NullifierRegistry per chain for all payment methods
  2. Writer Setup: Grant write permissions to all payment verifier contracts
  3. Immutable Nullifiers: Once deployed, nullifiers cannot be removed (plan for long-term storage)
  4. Gas Optimization: Uses OpenZeppelin’s AddressArrayUtils for efficient array operations

Build docs developers (and LLMs) love