Skip to main content

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)
FieldTypeDescription
secretField elementUser's private key for this commitment. Only the user knows this.
nullifierSecretField elementUsed to derive the nullifier. Must be unique per commitment.
tokenIdHashField elementkeccak256(tokenAddress) truncated to fit the BN254 field. Identifies the token type.
amountuint256The amount committed, in base units (aghost for native GHOST).
blindingField elementRandom blinding factor. Prevents commitment grinding attacks.

Policy commitment

commitment = Poseidon7(secret, nullifierSecret, tokenIdHash, amount, blinding, policyId, policyParamsHash)
Additional FieldTypeDescription
policyIdaddress (as field)The reveal policy contract address.
policyParamsHashField elementPoseidon 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"
}'
warning

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 commitmentPolicies mapping