Skip to main content

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 snarkjs WASM 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:

FileDescription
ghost_redemption.zkeyThe Groth16 proving key, generated from the trusted setup ceremony. Contains the toxic waste-derived parameters needed for proof construction.
ghost_redemption.wasmThe 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:

  1. Locates the commitment in its local tree by scanning leaf values.
  2. Extracts the Merkle path (path elements and path indices) from the local tree.
  3. Combines the client-provided private inputs with the extracted Merkle path.
  4. 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

MetricValue
Average proof time~13.5 seconds
Peak memory usage~1.5 GB during proof generation
Proofs generated (testnet)4
Concurrent proof capacity1 (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:

  1. Field element range: All inputs must be valid BN254 scalar field elements (less than the field modulus).
  2. Commitment existence: The provided commitment must exist in the local Merkle tree.
  3. Root consistency: The Merkle root derived from the local tree must match the current on-chain root.
  4. 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

ParameterDefaultDescription
PORT3003HTTP server port
ZKEY_PATHPath to ghost_redemption.zkey
WASM_PATHPath to ghost_redemption.wasm
RPC_URLSpecter chain RPC for Merkle tree sync
COMMITMENT_TREE_ADDRESSAddress of CommitmentTree contract
START_BLOCK0Block to begin historical event sync