Skip to main content

Ghost Protocol Integration Guide

End-to-end walkthrough: commit GHOST tokens, wait for the Merkle root update, generate a ZK proof, and reveal tokens to a fresh address.

Prerequisites

  • A funded wallet on Specter testnet (get GHOST from faucet)
  • Node.js 20+ for client-side proof generation
  • The commitment secrets must be stored securely — if you lose them, the committed tokens are unrecoverable

Step 1: Generate commitment secrets

import crypto from 'crypto';

function randomFieldElement() {
// Generate a random 31-byte value (fits within BN254 field)
return BigInt('0x' + crypto.randomBytes(31).toString('hex'));
}

const secret = randomFieldElement();
const nullifierSecret = randomFieldElement();
const blinding = randomFieldElement();
const amount = BigInt('1000000000000000000'); // 1 GHOST
const tokenIdHash = BigInt(0); // native GHOST = address(0)

// SAVE THESE VALUES SECURELY — you need them for reveal
console.log('secret:', secret.toString());
console.log('nullifierSecret:', nullifierSecret.toString());
console.log('blinding:', blinding.toString());

Step 2: Compute the commitment hash

import { buildPoseidon } from 'circomlibjs';

const poseidon = await buildPoseidon();
const F = poseidon.F;

const commitment = poseidon([secret, nullifierSecret, tokenIdHash, amount, blinding]);
const commitmentHex = '0x' + F.toString(commitment, 16).padStart(64, '0');
console.log('commitment:', commitmentHex);

Step 3: Submit the commit transaction

import { ethers } from 'ethers';

const provider = new ethers.JsonRpcProvider('https://testnet.specterchain.com');
const signer = new ethers.Wallet(PRIVATE_KEY, provider);

const vault = new ethers.Contract(
'0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
['function commitNative(bytes32,bytes32) payable'],
signer
);

const tx = await vault.commitNative(
commitmentHex,
ethers.ZeroHash, // no quantum commitment
{ value: amount, gasPrice: 1_000_000_000n }
);
const receipt = await tx.wait();
console.log('Commit tx:', receipt.hash);

Step 4: Wait for root update

The root updater relayer processes new commitments and updates the on-chain Merkle root. Poll until it's updated:

const tree = new ethers.Contract(
'0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87',
['function getLastRoot() view returns (bytes32)'],
provider
);

// Wait for root to update (typically 5-15 seconds)
let previousRoot = await tree.getLastRoot();
let newRoot;
do {
await new Promise(r => setTimeout(r, 5000));
newRoot = await tree.getLastRoot();
} while (newRoot === previousRoot);

console.log('Root updated:', newRoot);

Step 5: Build the Merkle proof

Fetch all commitments and build the tree locally to get the Merkle path:

// Fetch all CommitmentAdded events
const treeContract = new ethers.Contract(
'0xB7E37E652F3024bAaaf84b12ae301f8E1feC4D87',
['event CommitmentAdded(bytes32 indexed commitment, uint256 index)'],
provider
);

const events = await treeContract.queryFilter('CommitmentAdded');
const leaves = events.map(e => e.args.commitment);

// Find your leaf index
const leafIndex = leaves.findIndex(l => l === commitmentHex);

Step 6: Generate the ZK proof

Using the proof relayer API:

const proofResponse = await fetch('https://relayer.specterchain.com/api/proof/generate', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-HMAC-Signature': hmacSignature,
},
body: JSON.stringify({
secret: secret.toString(),
nullifierSecret: nullifierSecret.toString(),
amount: amount.toString(),
blinding: blinding.toString(),
tokenIdHash: '0',
recipient: RECIPIENT_ADDRESS,
withdrawAmount: amount.toString(),
newBlinding: '0', // full reveal, no change
}),
});

const { proof, publicInputs } = await proofResponse.json();

Step 7: Submit the reveal transaction

const revealSigner = new ethers.Wallet(RECIPIENT_PRIVATE_KEY, provider);

const revealVault = new ethers.Contract(
'0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
['function reveal(address,uint256[8],uint256[],bytes32,bytes,bytes32,bytes)'],
revealSigner
);

const tx = await revealVault.reveal(
ethers.ZeroAddress, // native GHOST
proof, // 8 uint256 values
publicInputs, // 8 public inputs
commitmentHex, // original commitment
'0x', // no quantum proof
ethers.ZeroHash, // no quantum change commitment
'0x', // no policy params
{ gasPrice: 1_000_000_000n }
);

const receipt = await tx.wait();
console.log('Reveal tx:', receipt.hash);
console.log('Tokens minted to:', RECIPIENT_ADDRESS);

What just happened

  1. You destroyed 1 GHOST from your original address (commit)
  2. You created 1 GHOST at a fresh recipient address (reveal)
  3. No on-chain observer can link the two operations
  4. The anonymity set is every commitment in the Merkle tree

Security reminders

  • Store secrets securelysecret, nullifierSecret, and blinding are the only way to reveal committed tokens
  • Use different addresses — reveal to a fresh address for maximum privacy
  • Don't reuse secrets — every commitment must have unique secret and nullifierSecret values
  • Verify the root — ensure the root used in your proof is still in the on-chain history window (last 100 roots)