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:
| Name | Type | Description |
|---|---|---|
nullifier | bytes32 | The 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:
| Name | Type | Description |
|---|---|---|
nullifier | bytes32 | The nullifier hash to check and record. |
Returns:
| Name | Type | Description |
|---|---|---|
| (unnamed) | bool | true if the nullifier was successfully recorded (was unspent). false if it was already spent. |
Access control: Only authorized contracts can call this function.
Behavior:
- Checks
nullifiers[nullifier]. - If
false(unspent): setsnullifiers[nullifier] = trueand returnstrue. - If
true(already spent): returnsfalse(or reverts, depending on implementation).
isSpent
function isSpent(bytes32 nullifier) external view returns (bool)
Checks whether a single nullifier has been spent.
Parameters:
| Name | Type | Description |
|---|---|---|
nullifier | bytes32 | The nullifier hash to query. |
Returns:
| Name | Type | Description |
|---|---|---|
| (unnamed) | bool | true 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:
| Name | Type | Description |
|---|---|---|
nullifiers_ | bytes32[] | Array of nullifier hashes to query. |
Returns:
| Name | Type | Description |
|---|---|---|
| (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
batchIsSpentfunction is read-only and can be called by anyone. Only write functions are access-controlled.