Skip to main content

Application Safety Rules

These are hard rules. Violating them can cause permanent asset loss.

Important

The application never receives the commitment value during callbacks. If you need to associate state with a commitment, store it off-chain or derive a unique identifier from dataHash plus application data.

Rule 1: onCommit Can Veto, onReveal Cannot Undo

onCommit is called BEFORE the commitment is recorded.
├─ If onCommit reverts → commitment is NOT recorded
├─ If onCommit succeeds → commitment IS recorded
└─ You can use onCommit to lock assets, validate inputs, or reject commits

onReveal is called AFTER proof verification but BEFORE nullifier recording.
├─ If onReveal reverts → THE ENTIRE TX REVERTS, nullifier is NOT spent
├─ If onReveal succeeds → nullifier IS spent, action is final
└─ A revert in onReveal means the user can retry with a new proof/root

Execution order (atomic transaction):

  1. Proof verification
  2. onReveal callback
  3. Nullifier recording

If onReveal reverts, the transaction reverts and the nullifier is not recorded.

Critical

If onReveal can revert, users can be blocked from revealing. Design onReveal to never revert for valid inputs.

Rule 2: Never Lock Assets Without a Recovery Path

Bad pattern:

function onCommit(...) {
token.transferFrom(user, address(this), amount);
// If user loses secrets, committed data is unrecoverable
}

Better pattern:

function onCommit(...) {
token.transferFrom(user, address(this), amount);
lockedUntil[commitment] = block.timestamp + 365 days; // Emergency recovery
}

function emergencyWithdraw(bytes32 commitment) external {
require(block.timestamp > lockedUntil[commitment], "Still locked");
// Allow original committer to recover after timeout
}

Rule 3: dataHash Must Be Deterministic

Your computeDataHash function must return the same value for the same input, every time, on-chain and off-chain.

Forbidden:

  • Using block.timestamp in dataHash
  • Using msg.sender in dataHash (it's the vault, not the user)
  • Using any non-deterministic value

Required:

  • Pure function of the data parameter only
  • Same algorithm on-chain and off-chain

Rule 4: Handle the "Lost Secrets" Scenario

If a user loses their secrets before revealing:

  • The commitment exists forever
  • The nullifier can never be computed
  • Committed data cannot be revealed (unless you have a recovery mechanism)
Design Decision

Is permanent loss acceptable for your use case? If not, implement time-locked recovery.

Rule 5: Never Trust dataHash Alone for Identity

The dataHash does not identify a specific commitment. Multiple commitments can have the same dataHash.

Bad pattern:

mapping(bytes32 => uint256) public amountByDataHash;

function onCommit(..., bytes32 dataHash, ...) {
amountByDataHash[dataHash] = amount; // WRONG: can be overwritten
}

Good pattern:

mapping(bytes32 => mapping(bytes32 => uint256)) public stateByCommitmentAndDataHash;

function onCommit(..., bytes32 dataHash, ...) {
// Track by commitment (passed separately) or use a unique ID
}

Rule 6: Nullifiers Are One-Way

Once a nullifier is spent:

  • It cannot be unspent
  • The commitment can never be revealed again
  • This is enforced by the protocol, not your application
Important

Do not design applications that require "refunds" or "cancellations" after reveal.

Rule 7: Root Staleness Is Your Problem

The protocol accepts the last 100 roots. If a user's proof uses root #1 and the chain is now at root #150, their proof is invalid.

Your application must:

  • Generate proofs with recent roots
  • Retry with a new root if InvalidRoot error occurs
  • Warn users if they're using an old proof

Rule 8: Application State Must Be Idempotent to Commitment

If onCommit is called twice with the same inputs, the second call should either:

  • Succeed with the same effect (idempotent)
  • Revert with a clear error

The vault prevents duplicate commitments, but your application logic should be defensive.

Summary

RuleKey Point
1onReveal reverts = retry possible; design for success
2Always have a recovery path for locked assets (if applicable)
3computeDataHash must be deterministic and match off-chain
4Lost secrets = permanent loss (plan for it)
5dataHash alone doesn't identify a commitment
6Nullifiers are forever spent; no refunds after reveal
7Handle stale roots; regenerate proofs as needed
8Make onCommit idempotent or clearly reject duplicates

Next Steps