Generating Proofs
ZK proofs can be generated client-side (in the browser or Node.js using snarkjs) or server-side via the proof relayer API.
Client-side with snarkjs
Installation
npm install snarkjs circomlibjs
Generating a redemption proof
import * as snarkjs from 'snarkjs';
import { buildPoseidon } from 'circomlibjs';
// Prepare circuit inputs
const input = {
// Public inputs
root: merkleRoot,
nullifier: computedNullifier,
withdrawAmount: withdrawAmount.toString(),
recipient: BigInt(recipientAddress).toString(),
changeCommitment: changeCommitment || '0',
tokenId: tokenIdHash.toString(),
policyId: '0',
policyParamsHash: '0',
// Private inputs
secret: secret.toString(),
nullifierSecret: nullifierSecret.toString(),
amount: amount.toString(),
blinding: blinding.toString(),
pathElements: merkleProof.pathElements,
pathIndices: merkleProof.pathIndices,
newBlinding: newBlinding.toString(),
};
// Generate proof
const { proof, publicSignals } = await snarkjs.groth16.fullProve(
input,
'/path/to/redemption.wasm', // compiled circuit
'/path/to/redemption_final.zkey' // proving key
);
// Format proof for on-chain submission (8 uint256 values)
const calldata = await snarkjs.groth16.exportSolidityCallData(proof, publicSignals);
Circuit artifacts
Proof generation requires two files from the trusted setup:
| File | Description | Typical size |
|---|---|---|
redemption.wasm | Compiled circuit (WebAssembly) | ~5 MB |
redemption_final.zkey | Proving key | ~50 MB |
These files should be hosted on a CDN and fetched by the client. They are not secret — only the private inputs are.
Server-side via proof relayer
The proof relayer at https://relayer.specterchain.com generates proofs server-side. This is useful when:
- Client devices lack the compute power for proof generation
- You want consistent proof generation times
- The WASM circuit files are too large for client download
API endpoint
POST https://relayer.specterchain.com/api/proof/generate
Request
{
"secret": "123456789",
"nullifierSecret": "987654321",
"amount": "1000000000000000000",
"blinding": "111222333",
"tokenIdHash": "0",
"recipient": "0xRecipientAddress",
"withdrawAmount": "1000000000000000000",
"newBlinding": "0"
}
Response
{
"proof": [
"0x...", "0x...", "0x...", "0x...",
"0x...", "0x...", "0x...", "0x..."
],
"publicInputs": [
"0x...", "0x...", "0x...", "0x...",
"0x...", "0x...", "0x...", "0x..."
]
}
Rate limits
| Limit | Value |
|---|---|
| Requests per minute per IP | 5 |
| Max concurrent proofs | 2 |
Proof generation is CPU-intensive. The proof relayer has strict rate limits to prevent denial of service. For high-throughput applications, generate proofs client-side.
Authentication
The proof relayer requires HMAC authentication. Include the signature in the request headers:
X-HMAC-Signature: <computed-hmac>
Proof format for on-chain submission
The Groth16 proof consists of 3 elliptic curve points (A, B, C) encoded as 8 uint256 values:
proof[0], proof[1] → Point A (G1)
proof[2], proof[3] → Point B x-coordinate (G2)
proof[4], proof[5] → Point B y-coordinate (G2)
proof[6], proof[7] → Point C (G1)
These map directly to the uint256[8] proof parameter in the reveal() function.