Proof Relayer
The proof relayer (ghost-proof-relayer, port 3003) is the ZK proof generation service for the Specter network. It loads the Groth16 circuit files (proving key, WASM witness generator), accepts proof requests containing private inputs, generates Groth16 proofs, and returns them to the client for on-chain submission. The service averages approximately 13.5 seconds per proof and has generated 4 proofs on the current testnet deployment.
Why Server-Side Proving
Groth16 proof generation is computationally intensive. The Ghost Protocol circuit has thousands of R1CS constraints, and the proving process involves multi-scalar multiplications over the BN254 curve. On constrained devices, this is prohibitively slow or impossible:
- Mobile browsers: The
snarkjsWASM prover can take 30–60 seconds on mid-range phones, with frequent out-of-memory crashes on devices with limited RAM. - React Native: WASM execution inside React Native's JavaScript engine (Hermes or JSC) is significantly slower than V8/SpiderMonkey and often fails to complete.
- Older desktops: Even on desktop browsers, proof generation can take 15–30 seconds and freezes the UI thread.
The proof relayer centralizes this computation on a server with adequate CPU and memory, reducing the client's role to assembling inputs and submitting the returned proof on-chain.
Circuit Files
The proof relayer loads two files at startup:
| File | Description |
|---|---|
ghost_redemption.zkey | The Groth16 proving key, generated from the trusted setup ceremony. Contains the toxic waste-derived parameters needed for proof construction. |
ghost_redemption.wasm | The compiled witness generator. Takes circuit inputs and computes the full witness (all intermediate signals). |
These files must correspond to the same circuit compilation and trusted setup. A mismatch between the .zkey and .wasm will produce invalid proofs that fail on-chain verification.
The circuit files are loaded into memory once at startup. Subsequent proof requests reuse the loaded artifacts, avoiding repeated disk I/O.
API
POST /prove
Generates a Groth16 proof from the provided private and public inputs.
Request:
{
"nullifier_hash": "0x...",
"commitment": "0x...",
"merkle_root": "0x...",
"token_id": "0x...",
"amount": "0x...",
"path_elements": ["0x...", "0x...", "..."],
"path_indices": [0, 1, 0, "..."],
"secret": "0x...",
"nullifier_secret": "0x...",
"blinding": "0x...",
"policy_id": "0x...",
"policy_params_hash": "0x..."
}
Response (success):
{
"proof": {
"pi_a": ["0x...", "0x...", "1"],
"pi_b": [["0x...", "0x..."], ["0x...", "0x..."], ["1", "0"]],
"pi_c": ["0x...", "0x...", "1"],
"protocol": "groth16",
"curve": "bn128"
},
"publicSignals": [
"nullifier_hash",
"merkle_root",
"token_id",
"amount",
"policy_id",
"policy_params_hash"
]
}
Response (error):
{
"error": "Invalid merkle root: commitment not found in tree",
"code": "INVALID_INPUT"
}
The returned proof object and publicSignals array are formatted for direct use with the on-chain GhostRedemptionVerifier.verifyProof() function.
GET /health
Returns service status and metrics.
{
"status": "healthy",
"circuitLoaded": true,
"proofsGenerated": 4,
"avgProofTimeMs": 13500,
"commitmentCount": 131,
"merkleRoot": "0x...",
"uptime": 864000
}
Local Merkle Tree State
The proof relayer maintains its own local copy of the commitment Merkle tree, identical to the one maintained by the root updater. This is necessary because proof generation requires the Merkle path (sibling hashes from leaf to root) for the commitment being revealed, and these path elements are not stored on-chain.
When a client requests a proof, the relayer:
- Locates the commitment in its local tree by scanning leaf values.
- Extracts the Merkle path (path elements and path indices) from the local tree.
- Combines the client-provided private inputs with the extracted Merkle path.
- Generates the witness and computes the Groth16 proof.
The local tree is kept in sync by listening for the same Committed events as the root updater. If the relayer's tree falls out of sync, proof generation will fail because the computed root will not match the on-chain root, and the proof will be rejected by the verifier contract.
Proof Generation Pipeline
The proof generation process follows these steps:
Client request
│
▼
Input validation
│
▼
Merkle path extraction (from local tree)
│
▼
Witness computation (ghost_redemption.wasm)
│
▼
Groth16 proof generation (ghost_redemption.zkey)
│
▼
Proof + public signals returned to client
Performance
| Metric | Value |
|---|---|
| Average proof time | ~13.5 seconds |
| Peak memory usage | ~1.5 GB during proof generation |
| Proofs generated (testnet) | 4 |
| Concurrent proof capacity | 1 (sequential processing) |
Proof generation is CPU-bound and memory-intensive. The service processes proof requests sequentially to avoid memory exhaustion. Concurrent requests are queued and processed in order.
Input Validation
Before generating a proof, the service validates:
- Field element range: All inputs must be valid BN254 scalar field elements (less than the field modulus).
- Commitment existence: The provided commitment must exist in the local Merkle tree.
- Root consistency: The Merkle root derived from the local tree must match the current on-chain root.
- Commitment preimage: The Poseidon7 hash of
(secret, nullifier_secret, blinding, token_id, amount, policy_id, policy_params_hash)must equal the provided commitment.
If any validation fails, the service returns an error without attempting proof generation, saving the ~13.5 seconds that would be wasted on an invalid proof.
Security Considerations
The proof relayer receives the user's private inputs (secret, nullifier secret, blinding factor). This means:
- The relayer operator can see private inputs. Users who use the proof relayer are trusting the operator not to log or exfiltrate their secrets.
- Compromise of the relayer could expose user secrets. The relayer should be hardened against unauthorized access.
- Users with sufficient compute should generate proofs locally. The relayer exists as a convenience for constrained clients, not as the primary proving path.
In the future, the SP1 Ghost Circuit may enable client-side proving in more environments, reducing reliance on the proof relayer.
Configuration
| Parameter | Default | Description |
|---|---|---|
PORT | 3003 | HTTP server port |
ZKEY_PATH | — | Path to ghost_redemption.zkey |
WASM_PATH | — | Path to ghost_redemption.wasm |
RPC_URL | — | Specter chain RPC for Merkle tree sync |
COMMITMENT_TREE_ADDRESS | — | Address of CommitmentTree contract |
START_BLOCK | 0 | Block to begin historical event sync |