Application Safety Rules
These are hard rules. Violating them can cause permanent asset loss.
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):
- Proof verification
onRevealcallback- Nullifier recording
If onReveal reverts, the transaction reverts and the nullifier is not recorded.
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.timestampin dataHash - Using
msg.senderin dataHash (it's the vault, not the user) - Using any non-deterministic value
Required:
- Pure function of the
dataparameter 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)
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
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
InvalidRooterror 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
| Rule | Key Point |
|---|---|
| 1 | onReveal reverts = retry possible; design for success |
| 2 | Always have a recovery path for locked assets (if applicable) |
| 3 | computeDataHash must be deterministic and match off-chain |
| 4 | Lost secrets = permanent loss (plan for it) |
| 5 | dataHash alone doesn't identify a commitment |
| 6 | Nullifiers are forever spent; no refunds after reveal |
| 7 | Handle stale roots; regenerate proofs as needed |
| 8 | Make onCommit idempotent or clearly reject duplicates |
Next Steps
- IGhostApplication Interface - Full interface reference
- One-Time Access Token Tutorial - See these rules in practice