Reveal Flow
The reveal operation proves knowledge of a commitment's preimage using a Groth16 ZK proof and mints fresh tokens to the recipient.
The reveal function
interface ICommitRevealVault {
function reveal(
address token,
uint256[8] calldata proof,
uint256[] calldata publicInputs,
bytes32 commitment,
bytes calldata quantumProof,
bytes32 changeQuantumCommitment,
bytes calldata policyParams
) external;
}
Parameters
| Parameter | Description |
|---|---|
token | Token address (or address(0) for native GHOST) |
proof | Groth16 proof (8 uint256 values = 3 EC points) |
publicInputs | 8 public circuit inputs (see below) |
commitment | The original commitment being revealed |
quantumProof | Quantum resistance proof (empty bytes if unused) |
changeQuantumCommitment | Quantum commitment for change (zero hash if unused) |
policyParams | ABI-encoded policy parameters (empty if no policy) |
Public inputs (8 values)
| Index | Field | Description |
|---|---|---|
| 0 | root | Merkle tree root the proof was generated against |
| 1 | nullifier | Derived nullifier (prevents double-reveal) |
| 2 | withdrawAmount | Amount being withdrawn (in aghost) |
| 3 | recipient | Address receiving the minted tokens |
| 4 | changeCommitment | New commitment for remaining funds |
| 5 | tokenId | Token identifier hash |
| 6 | policyId | Policy contract address (0 if none) |
| 7 | policyParamsHash | Hash of policy parameters (0 if none) |
Step-by-step reveal process
1. Generate the ZK proof
Using the proof relayer:
curl -X POST https://relayer.specterchain.com/api/proof/generate \
-H "Content-Type: application/json" \
-H "X-HMAC-Signature: $HMAC_SIG" \
-d '{
"secret": "0x...",
"nullifierSecret": "0x...",
"amount": "1000000000000000000",
"blinding": "0x...",
"tokenIdHash": "0x...",
"recipient": "0xRECIPIENT_ADDRESS",
"withdrawAmount": "1000000000000000000",
"newBlinding": "0x..."
}'
Or generate client-side with snarkjs (see Generating Proofs).
2. Submit the reveal transaction
const vault = new ethers.Contract(
'0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
['function reveal(address,uint256[8],uint256[],bytes32,bytes,bytes32,bytes)'],
signer
);
const tx = await vault.reveal(
ethers.ZeroAddress, // native GHOST
proof, // 8 uint256 values from proof generation
publicInputs, // 8 public inputs
originalCommitment, // the commitment being revealed
'0x', // no quantum proof
ethers.ZeroHash, // no quantum change commitment
'0x', // no policy params
{ gasPrice: 1_000_000_000n }
);
await tx.wait();
What happens on-chain
- Root check — verifies
publicInputs[0](root) is in the commitment tree's history window (last 100 roots) - Nullifier check — verifies
publicInputs[1](nullifier) has not been spent - Proof verification — calls the Groth16ProofVerifier to verify the proof against public inputs
- Policy validation — if the commitment has a policy, calls
policy.validate()via staticcall with 100K gas cap - Nullifier registration — marks the nullifier as spent in the NullifierRegistry
- Token minting — calls the Ghostmint precompile to mint fresh tokens to the recipient
- Change commitment — if
changeCommitment != 0, inserts it into the tree for the remaining balance - Event emission — emits
Revealed(commitment, token, amount, recipient)
Partial reveals and change
You don't have to reveal the full committed amount. For example, if you committed 10 GHOST, you can reveal 3 GHOST and create a change commitment for 7 GHOST:
withdrawAmount= 3 GHOSTchangeCommitment= Poseidon hash of a new commitment with 7 GHOST- The circuit verifies:
withdrawAmount + changeAmount == originalAmount
The change commitment goes back into the tree and can be revealed later with a new proof.
Common errors
| Error | Cause |
|---|---|
InvalidRoot | The root used in the proof is no longer in the history window. Generate a fresh proof. |
NullifierAlreadySpent | This commitment has already been revealed. |
InvalidProof | The proof is invalid — check inputs match the commitment preimage exactly. |
PolicyValidationFailed | The reveal policy's validate() returned false. |
InvalidAmount | Withdraw amount doesn't match the proof's public inputs. |