Commitment Structure
Every commitment in the Ghost Protocol is a Poseidon hash of a structured preimage. Understanding the fields is essential for generating valid proofs.
Basic commitment (no policy)
commitment = Poseidon5(secret, nullifierSecret, tokenIdHash, amount, blinding)
| Field | Type | Description |
|---|---|---|
secret | Field element | User's private key for this commitment. Only the user knows this. |
nullifierSecret | Field element | Used to derive the nullifier. Must be unique per commitment. |
tokenIdHash | Field element | keccak256(tokenAddress) truncated to fit the BN254 field. Identifies the token type. |
amount | uint256 | The amount committed, in base units (aghost for native GHOST). |
blinding | Field element | Random blinding factor. Prevents commitment grinding attacks. |
Policy commitment
commitment = Poseidon7(secret, nullifierSecret, tokenIdHash, amount, blinding, policyId, policyParamsHash)
| Additional Field | Type | Description |
|---|---|---|
policyId | address (as field) | The reveal policy contract address. |
policyParamsHash | Field element | Poseidon hash of the policy parameters. |
These fields are bound inside the ZK circuit — you cannot change the policy after commit without invalidating the proof.
Derived values
Nullifier
nullifier = Poseidon2(nullifierSecret, leafIndex)
The nullifier uniquely identifies a spent commitment. It is derived from nullifierSecret and the leaf's position in the Merkle tree. Since the nullifierSecret is private, an observer cannot compute which commitment a nullifier corresponds to.
Token ID hash
tokenIdHash = uint256(keccak256(tokenAddress)) % BN254_FIELD_MODULUS
For native GHOST, the token address is address(0).
Field constraints
All field elements must be less than the BN254 scalar field modulus:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
Values exceeding this will cause the circuit to reject the proof.
Generating commitment inputs
Client-side (JavaScript)
import { buildPoseidon } from 'circomlibjs';
const poseidon = await buildPoseidon();
// Generate random field elements
const secret = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), ''));
const nullifierSecret = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), ''));
const blinding = BigInt('0x' + crypto.getRandomValues(new Uint8Array(31)).reduce((s, b) => s + b.toString(16).padStart(2, '0'), ''));
const amount = BigInt('1000000000000000000'); // 1 GHOST
const tokenIdHash = BigInt(0); // native GHOST
// Compute commitment
const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]);
Via commitment relayer
curl -X POST https://relayer.specterchain.com/api/commitment/compute \
-H "Content-Type: application/json" \
-H "X-HMAC-Signature: $HMAC_SIG" \
-d '{
"secret": "123456789",
"nullifierSecret": "987654321",
"blinding": "111222333",
"tokenIdHash": "0",
"amount": "1000000000000000000"
}'
Never share your secret or nullifierSecret. These values are what prove ownership of the commitment. Anyone who has them can reveal your tokens.
Storage
- Commitments are stored as leaves in the CommitmentTree (Merkle tree, depth 20, ~1M capacity)
- Spent nullifiers are stored in the NullifierRegistry (append-only)
- Policy bindings are stored in the CommitRevealVault's
commitmentPoliciesmapping