Nullifiers are mechanisms introduced in typical privacy protocols to prevent double spending and are a kind of “spent tag” designed not to compromise privacy. Exactly one nullifier exists for each UTXO, and once that UTXO is consumed in a transaction, the corresponding nullifier is recorded onchain. The smart contract rejects any transaction that submits the same nullifier again, thereby blocking attempts to reuse the same UTXO.
In Privacy Boost, the nullifier is defined as follows:
Nullifier=Poseidon2(NullifierSecretKey, index)Nullifier=_Poseidon_2(NullifierSecretKey, index)
Here, NullifierSecretKey is the Nullifier Secret Key defined in Section 3.1, and index is the leaf index that the corresponding UTXO occupies in the Merkle tree. Since NullifierSecretKey is deterministically derived from a signature of the spending key, the user can generate nullifiers in a consistent way for multiple UTXOs. The important point is that the Merkle tree structure and UTXO commitment value are not directly included in this hash, so external observers cannot reverse-track which UTXO was spent from a given nullifier, and the nullifier appears simply as a random hash value.
The ZK SNARK circuit takes NullifierSecretKey and the leaf index as private inputs, recomputes the nullifier according to the above formula, and verifies that this value exactly matches the nullifier provided as a public input. The onchain contract maintains a mapping (mapping(uint256 _nullifierHash => bool _spent) nullifierHashes) keyed by this public nullifier.
When a transaction is submitted, the contract first checks whether the nullifier has been used before, and if it already exists, it reverts the transaction. If it has not been used yet, and ZK proof verification succeeds, the contract records the nullifier as spent. In this way, the contract can control double spending solely by checking whether “this nullifier has been seen before or not,” without needing to know how the nullifier was generated.
From a privacy perspective, nullifiers have several important properties. First, since the definition of the nullifier does not include the UTXO commitment, token, or value information, it is difficult for external observers to link a specific nullifier directly to a specific commitment or transaction. Furthermore, the leaf index in the Merkle tree is used only as a private input inside the circuit and is not exposed as a public input, so it is also impossible to know “which position’s UTXO was spent.” Ultimately, the only observable fact is that “a certain nullifier value appeared once at a certain point in time,” which abstracts to merely expressing that “some note was spent.”
In summary, a nullifier can be seen as a value that is cryptographically linked to a UTXO via NullifierSecretKey and the leaf index inside the circuit to prevent double spending, while from the contract’s perspective it is represented as a boolean flag keyed by a hash value, without any publicly visible linkage to the UTXO.