Skip to main content

NullifierRegistry

The NullifierRegistry is a simple but critical contract that tracks spent nullifiers. Every withdrawal (reveal) in the Specter protocol produces a unique nullifier derived from the depositor's secret. Recording nullifiers prevents double-spending -- the same commitment cannot be withdrawn twice.

Deployed address: 0xaadb9c3394835B450023daA91Ad5a46beA6e43a1

Storage

mapping(bytes32 => bool) public nullifiers;

Maps each nullifier hash to a boolean indicating whether it has been spent. Once set to true, it is never reset.

Functions

recordNullifier

function recordNullifier(bytes32 nullifier) external

Records a nullifier as spent.

Parameters:

NameTypeDescription
nullifierbytes32The nullifier hash to mark as spent.

Access control: Only authorized contracts (the CommitRevealVault) can call this function.

Reverts if:

  • Caller is not authorized.
  • Nullifier has already been recorded (already spent).

checkAndRecord

function checkAndRecord(bytes32 nullifier) external returns (bool)

Atomically checks whether a nullifier is unspent and, if so, records it as spent. This is the primary function used during the reveal flow.

Parameters:

NameTypeDescription
nullifierbytes32The nullifier hash to check and record.

Returns:

NameTypeDescription
(unnamed)booltrue if the nullifier was successfully recorded (was unspent). false if it was already spent.

Access control: Only authorized contracts can call this function.

Behavior:

  1. Checks nullifiers[nullifier].
  2. If false (unspent): sets nullifiers[nullifier] = true and returns true.
  3. If true (already spent): returns false (or reverts, depending on implementation).

isSpent

function isSpent(bytes32 nullifier) external view returns (bool)

Checks whether a single nullifier has been spent.

Parameters:

NameTypeDescription
nullifierbytes32The nullifier hash to query.

Returns:

NameTypeDescription
(unnamed)booltrue if the nullifier has been recorded (spent), false otherwise.

batchIsSpent

function batchIsSpent(bytes32[] calldata nullifiers_) external view returns (bool[] memory)

Checks the spent status of multiple nullifiers in a single call.

Parameters:

NameTypeDescription
nullifiers_bytes32[]Array of nullifier hashes to query.

Returns:

NameTypeDescription
(unnamed)bool[]Array of booleans, each true if the corresponding nullifier is spent.

Usage: Useful for wallet UIs and indexers that need to check the status of many nullifiers efficiently without making multiple RPC calls.

Usage Examples

Checking Nullifier Status

// Single check
bool spent = NullifierRegistry(registryAddress).isSpent(nullifierHash);

// Batch check
bytes32[] memory toCheck = new bytes32[](3);
toCheck[0] = nullifier1;
toCheck[1] = nullifier2;
toCheck[2] = nullifier3;

bool[] memory results = NullifierRegistry(registryAddress).batchIsSpent(toCheck);
// results[0] = true/false, results[1] = true/false, etc.

Internal Usage by CommitRevealVault

// During reveal(), the vault calls:
bool success = NullifierRegistry(registryAddress).checkAndRecord(nullifier);
require(success, "Nullifier already spent");

Security Notes

  • The nullifier is derived from the depositor's secret inside the ZK circuit: nullifier = Poseidon2(secret, leafIndex). This binding ensures that each commitment produces exactly one nullifier, and the nullifier cannot be predicted without knowledge of the secret.
  • The registry is append-only. There is no mechanism to "unspend" a nullifier.
  • The batchIsSpent function is read-only and can be called by anyone. Only write functions are access-controlled.