Skip to main content

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:

InputTypeDescription
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:

  1. The SP1 prover generates a STARK proof of correct execution.
  2. The STARK proof is recursively compressed into a SNARK proof (Groth16 or PLONK) suitable for on-chain verification.
  3. The SNARK proof is submitted to Succinct's universal verifier contract deployed on Specter.
  4. 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

PropertyGroth16 (circom)SP1 (Rust)
LanguageCircom DSLRust
Proof systemGroth16 (BN254)STARK → SNARK wrap
Trusted setupPer-circuit ceremony requiredNone (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 changesRecompile + new setupRecompile only
On-chain verifierGhostRedemptionVerifier (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).