SP1 Ghost Circuit
The SP1 Ghost Circuit is an alternative implementation of the Ghost Protocol's zero-knowledge proof system, targeting Succinct's SP1 zkVM instead of the Groth16/circom stack. Written in Rust, this circuit compiles to RISC-V and executes inside SP1's general-purpose zero-knowledge virtual machine, enabling fully on-chain proof verification without a trusted relayer.
Motivation
The existing Groth16-based Ghost circuit (written in circom) requires a trusted setup ceremony and produces proofs that are verified by a custom Solidity verifier contract. While efficient, this architecture has limitations:
- Trusted setup: The Groth16 proving key and verification key are generated from a circuit-specific trusted setup. Any change to the circuit requires a new ceremony.
- Relayer dependency: Proof generation currently requires server-side infrastructure (the proof relayer) because mobile and constrained browser environments cannot run the Groth16 prover efficiently.
- Circuit rigidity: Circom circuits are fixed at compile time. Adding new features (e.g., new policy types, different commitment schemes) requires recompiling the entire circuit and redeploying the verifier.
SP1 addresses these constraints by providing a general-purpose zkVM where the program is written in standard Rust. The SP1 prover generates STARK-based proofs that can be wrapped into SNARK proofs for on-chain verification via a universal verifier contract — no per-circuit trusted setup required.
Architecture
┌──────────────────────────────────────────────┐
│ SP1 zkVM │
│ │
│ ┌─────────────┐ ┌──────────────────────┐ │
│ │ RISC-V ELF │───▶│ SP1 Ghost Program │ │
│ └─────────────┘ │ │ │
│ │ 1. Read inputs │ │
│ │ 2. Verify commitment │ │
│ │ 3. Verify Merkle path│ │
│ │ 4. Commit public vals│ │
│ └──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ Public Values │ │
│ │ (on-chain data) │ │
│ └──────────────────┘ │
└──────────────────────────────────────────────┘
│
▼
┌──────────────────┐
│ SP1 Verifier │
│ (Solidity) │
└──────────────────┘
Circuit Inputs
The SP1 Ghost Circuit reads the following inputs from the SP1 stdin interface:
| Input | Type | Description |
|---|---|---|
nullifier_hash | [u8; 32] | Hash of the nullifier, used to prevent double-reveals |
commitment | [u8; 32] | The Poseidon commitment stored in the Merkle tree |
merkle_root | [u8; 32] | The current root of the commitment Merkle tree |
token_id | [u8; 32] | Derived identifier for the token being transacted |
amount | [u8; 32] | The amount bound to this commitment |
path_elements | [[u8; 32]; DEPTH] | Sibling hashes along the Merkle path |
path_indices | [u8; DEPTH] | Left/right indicators for each level of the Merkle path |
secret | [u8; 32] | The user's secret, preimage component of the commitment |
nullifier_secret | [u8; 32] | The nullifier preimage, bound to the commitment |
blinding | [u8; 32] | Blinding factor for hiding the commitment |
policy_id | [u8; 32] | Identifier of the policy bound to this commitment |
policy_params_hash | [u8; 32] | Hash of the policy parameters at commit time |
All values are serialized as 32-byte big-endian field elements in the BN254 scalar field.
Verification Logic
The circuit performs three core verification steps, mirroring the logic of the circom-based Ghost circuit:
1. Commitment Preimage Verification
The circuit recomputes the Poseidon7 commitment from its constituent preimage values and asserts equality with the provided commitment:
let computed_commitment = poseidon7(
secret,
nullifier_secret,
blinding,
token_id,
amount,
policy_id,
policy_params_hash,
);
assert_eq!(computed_commitment, commitment);
This proves that the prover knows the secret values that were used to create the commitment, without revealing them.
2. Merkle Path Verification
The circuit walks the Merkle tree from the leaf (the commitment) to the root, hashing at each level with the provided sibling:
let mut current = commitment;
for i in 0..DEPTH {
let sibling = path_elements[i];
current = if path_indices[i] == 0 {
poseidon2(current, sibling)
} else {
poseidon2(sibling, current)
};
}
assert_eq!(current, merkle_root);
This proves the commitment exists in the on-chain Merkle tree without revealing which leaf position it occupies.
3. Nullifier Verification
The circuit verifies that the nullifier hash is correctly derived from the nullifier secret:
let computed_nullifier = poseidon2(nullifier_secret, nullifier_secret);
assert_eq!(computed_nullifier, nullifier_hash);
This binds the nullifier to the commitment's preimage, ensuring each commitment can only be revealed once.
Public Values
After verification, the circuit commits the following values as public outputs. These are the values that the on-chain verifier contract can read and enforce:
sp1_zkvm::io::commit(&nullifier_hash);
sp1_zkvm::io::commit(&merkle_root);
sp1_zkvm::io::commit(&token_id);
sp1_zkvm::io::commit(&amount);
sp1_zkvm::io::commit(&policy_id);
sp1_zkvm::io::commit(&policy_params_hash);
The private inputs (secret, nullifier_secret, blinding, path_elements, path_indices) are never exposed.
Poseidon Implementation
The SP1 circuit uses a pure-Rust Poseidon implementation compatible with the BN254 scalar field. The hash parameters (round constants, MDS matrix) must match exactly with the circomlibjs implementation used elsewhere in the Specter stack:
- Poseidon2 (PoseidonT3): 2 inputs, used for Merkle node hashing and nullifier derivation
- Poseidon7 (PoseidonT8): 7 inputs, used for commitment construction
Parameter alignment is critical. If the Rust Poseidon produces different outputs than circomlibjs for the same inputs, commitments created by the webapp would fail verification inside the SP1 circuit.
On-Chain Verification Path
SP1 proofs follow this verification flow:
- The SP1 prover generates a STARK proof of correct execution.
- The STARK proof is recursively compressed into a SNARK proof (Groth16 or PLONK) suitable for on-chain verification.
- The SNARK proof is submitted to Succinct's universal verifier contract deployed on Specter.
- The verifier contract checks the proof and exposes the public values to the calling contract (e.g.,
CommitRevealVault).
This eliminates the need for a circuit-specific verifier contract. Any program compiled to the SP1 ELF format can be verified through the same universal verifier, identified by its vkey (verification key hash derived from the program binary).
Comparison with Groth16 Circuit
| Property | Groth16 (circom) | SP1 (Rust) |
|---|---|---|
| Language | Circom DSL | Rust |
| Proof system | Groth16 (BN254) | STARK → SNARK wrap |
| Trusted setup | Per-circuit ceremony required | None (universal) |
| Proof size | ~256 bytes | ~256 bytes (after SNARK wrap) |
| Verification gas | ~200k gas | ~300k gas (universal verifier overhead) |
| Prover time | ~13.5s (server) | Variable (depends on SP1 prover infrastructure) |
| Circuit changes | Recompile + new setup | Recompile only |
| On-chain verifier | GhostRedemptionVerifier (custom) | SP1 universal verifier |
Deployment Status
The SP1 Ghost Circuit is not yet deployed. It exists as a proof-of-concept demonstrating the feasibility of migrating the Ghost Protocol's ZK proof system from circom/Groth16 to a general-purpose zkVM.
Remaining Work
- Poseidon parameter alignment: Ensure the Rust Poseidon implementation produces identical outputs to circomlibjs across all input widths.
- SP1 verifier deployment: Deploy Succinct's universal verifier contract on the Specter chain.
- Integration with CommitRevealVault: Modify the vault contract to accept SP1 proofs alongside (or instead of) Groth16 proofs.
- Performance benchmarking: Measure SP1 proof generation time and compare with the existing ~13.5s Groth16 baseline.
- Client-side proving: Evaluate whether SP1's WASM prover can run in-browser, removing the relayer dependency entirely.
Source Location
The SP1 Ghost Circuit source is located in the Specter monorepo under the circuits directory alongside the circom-based circuits. The Rust program is structured as a standard SP1 project with a program crate (the guest code that runs inside the zkVM) and a script crate (the host code that prepares inputs and invokes the prover).