Skip to main content

Ghost Protocol Reference

Ghost Protocol is Specter's core cryptographic primitive — a commit/reveal privacy system built on zero-knowledge proofs, Poseidon hashing, and Groth16 verification over the BN254 elliptic curve. This page is the authoritative reference for every cryptographic component in the protocol.

Finite Field

All arithmetic in Ghost Protocol operates within the BN254 scalar field:

p = 21888242871839275222246405745257275088548364400416034343698204186575808495617

This is a 254-bit prime. Every value that enters a ZK circuit — secrets, nullifiers, token identifiers, policy hashes — must be a valid element of this field (i.e., in the range [0, p)).

Field Reduction

When Ghost Protocol derives circuit inputs from Ethereum-native operations (e.g., keccak256), the 256-bit hash output is reduced modulo p before use:

uint256 fieldElement = uint256(keccak256(abi.encodePacked(data))) % BN254_FIELD;

This reduction is critical. A raw keccak256 output can exceed p, which would cause the circuit to reject the input or — worse — create a soundness gap between on-chain and off-chain computations.

Poseidon Hash Variants

Ghost Protocol uses three Poseidon hash configurations, each tuned to a specific input width. Poseidon is an algebraic hash function designed for efficiency inside arithmetic circuits (R1CS / Groth16), where it is orders of magnitude cheaper than SHA-256 or keccak256.

VariantInputsCircuit NamePrimary Uses
Poseidon22PoseidonT3Merkle tree node hashing, nullifier derivation, access tags, token ID derivation
Poseidon44PoseidonT5OpenGhost commitments
Poseidon77PoseidonT8CommitRevealVault commitments with policy binding

Poseidon2 (PoseidonT3)

The workhorse of the protocol. Two-input Poseidon is used whenever exactly two field elements need to be hashed together:

Poseidon2(a, b) → field element

Applications:

  • Merkle tree nodes: node = Poseidon2(leftChild, rightChild)
  • Token ID derivation: tokenId = Poseidon2(tokenAddress, 0)
  • Nullifier (inner): innerNullifier = Poseidon2(nullifierSecret, commitment)
  • Nullifier (outer): nullifier = Poseidon2(innerNullifier, leafIndex)
  • Access tags: accessTag = Poseidon2(nullifierSecret, sessionNonce)

On-chain deployment: PoseidonT3 (the Poseidon2 implementation) is the only Poseidon variant deployed as an on-chain Solidity contract. A single invocation costs approximately 30,000 gas. The on-chain contract is used for Merkle tree insertions and nullifier verification.

Poseidon4 (PoseidonT5)

Four-input Poseidon is used for OpenGhost commitments, which store arbitrary encrypted data rather than token values:

Poseidon4(secret, nullifierSecret, dataHash, blinding) → commitment

This variant is computed off-chain only — the commitment is generated in the client and submitted to the contract as a precomputed field element. No PoseidonT5 contract is deployed on-chain.

Poseidon7 (PoseidonT8)

Seven-input Poseidon is used for CommitRevealVault commitments, which bind token value and policy information into a single commitment:

Poseidon7(secret, nullifierSecret, tokenId, amount, blinding, policyId, policyParamsHash) → commitment

This is the most complex commitment structure in the protocol, encoding:

InputDescription
secretRandom 31-byte secret known only to the key holder
nullifierSecretSeparate random secret used in nullifier derivation
tokenIdPoseidon2(tokenAddress, 0) — field-safe token identifier
amountToken amount in base units (e.g., aghost with 18 decimals)
blindingRandom blinding factor for commitment uniqueness
policyIdIdentifier of the policy contract (0 for no policy)
policyParamsHashkeccak256(policyParams) % BN254_FIELD (0 for no policy)

Like Poseidon4, this variant is computed off-chain only. The resulting commitment is a single field element submitted to the contract.

Why Off-Chain for Higher Variants?

Deploying PoseidonT5 or PoseidonT8 as Solidity contracts would be expensive — both in deployment cost and per-call gas. Since the ZK circuit already verifies the commitment preimage, the on-chain contract only needs to store and compare the commitment hash. The commitment is computed in the client (JavaScript/TypeScript), and the ZK proof guarantees it was computed correctly.

Merkle Tree

Ghost Protocol stores all commitments in an append-only Merkle tree that provides set membership proofs without revealing which specific leaf a user is proving knowledge of.

Tree Parameters

ParameterValue
Depth20
Capacity2^20 = 1,048,576 commitments
Hash functionPoseidon2 (PoseidonT3)
Zero valuekeccak256("ghost_protocol") % BN254_FIELD
Root history100-root ring buffer on-chain

Structure

                    Root
/ \
H(0,1) H(2,3) ← Poseidon2(left, right)
/ \ / \
L0 L1 L2 L3 ← Leaf commitments

Each internal node is computed as:

node = Poseidon2(leftChild, rightChild)

Empty leaves are initialized with the zero value. As new commitments are appended, the tree is updated from the leaf to the root, recalculating only the nodes along the insertion path (20 hashes per insertion).

On-Chain vs Off-Chain

ComponentLocationPurpose
Root historyOn-chain (ring buffer)Stores the last 100 Merkle roots for proof verification
Current rootOn-chainThe most recent root, updated on each insertion
Next leaf indexOn-chainCounter tracking the next available leaf position
Filled subtreesOn-chainArray of 20 hashes enabling O(20) root recomputation
Full treeOff-chainComplete tree maintained by the relayer and client for proof generation

The ring buffer of 100 historical roots is critical for concurrency. If the tree updates between when a user generates their proof and when the proof is verified on-chain, the proof's root may no longer be the current root — but it will still be valid if it matches any root in the history buffer.

Insertion

When a new commitment is inserted:

  1. The commitment is placed at position nextLeafIndex
  2. The path from the new leaf to the root is recomputed (20 Poseidon2 hashes)
  3. The new root is pushed into the ring buffer
  4. nextLeafIndex is incremented
  5. An event is emitted with the commitment and leaf index
// Simplified on-chain insertion
function _insert(uint256 commitment) internal returns (uint256 index) {
uint256 currentHash = commitment;
for (uint256 i = 0; i < DEPTH; i++) {
if (index % 2 == 0) {
filledSubtrees[i] = currentHash;
currentHash = PoseidonT3.hash([currentHash, zeros[i]]);
} else {
currentHash = PoseidonT3.hash([filledSubtrees[i], currentHash]);
}
index /= 2;
}
roots[currentRootIndex] = currentHash;
currentRootIndex = (currentRootIndex + 1) % ROOT_HISTORY_SIZE;
}

Nullifiers

Nullifiers prevent double-spending. Each commitment has exactly one valid nullifier, but an observer cannot link a nullifier to its commitment without knowing the secret preimage.

Two-Level Derivation

Ghost Protocol uses a two-level nullifier derivation scheme:

Step 1:  innerNullifier = Poseidon2(nullifierSecret, commitment)
Step 2: nullifier = Poseidon2(innerNullifier, leafIndex)

Why two levels?

  • Level 1 binds the nullifier to the commitment content via the nullifierSecret. Without knowing nullifierSecret, an observer cannot predict the nullifier for a given commitment.
  • Level 2 binds the nullifier to the commitment's position in the tree. This ensures that even if the same commitment value appears in two different leaves (unlikely but possible), each instance has a distinct nullifier.

Nullifier Registry

The NullifierRegistry contract maintains a mapping of spent nullifiers:

mapping(uint256 => bool) public nullifiers;

During reveal, the contract checks nullifiers[nullifier] == false, then sets it to true. Once spent, a nullifier can never be reused. This is the mechanism that prevents double-spending of committed assets.

Nullifier Privacy

The nullifier is computed inside the ZK circuit and output as a public signal. The verifier checks that it was correctly derived from the commitment, but since the commitment itself is hidden behind the Merkle proof, an observer learns only that some commitment was spent — not which one.

Quantum Resistance Layer

Ghost Protocol includes an optional quantum defense mechanism that protects committed assets against future quantum computers capable of breaking elliptic curve cryptography.

Threat Model

A sufficiently powerful quantum computer could:

  1. Break BN254 elliptic curve operations (Groth16 proofs)
  2. Recover secret preimages from Poseidon hashes (algebraic attacks)

If this happens, an attacker could forge ZK proofs and drain committed assets. The quantum layer adds a second authentication factor based on hash preimage knowledge, which remains secure against known quantum algorithms (Grover's algorithm provides only quadratic speedup against 256-bit hashes).

Mechanism

At commit time, the user optionally generates a quantumSecret (32 random bytes) and stores its keccak256 hash:

quantumCommitment = keccak256(quantumSecret)

This commitment is stored on-chain alongside the Poseidon commitment.

At reveal time, if a quantum commitment exists for the nullified leaf, the user must provide the quantumSecret preimage:

if (quantumCommitments[commitment] != bytes32(0)) {
require(
keccak256(abi.encodePacked(quantumSecret)) == quantumCommitments[commitment],
"Invalid quantum preimage"
);
}

Properties

PropertyDetail
OptionalUsers can commit without a quantum secret
Hash functionkeccak256 (256-bit, quantum-resistant against preimage attacks)
StorageOn-chain, indexed by commitment
VerificationPreimage check at reveal time
Key formatV4 phantom key format includes the quantum secret

Limitations

The quantum layer protects against preimage forgery — an attacker cannot reveal without knowing the quantum secret. However, it does not upgrade the ZK proof system itself. Full post-quantum ZK proofs (via SP1 zkVM with post-quantum signature schemes) are planned for a future protocol version.

Cryptographic Parameter Summary

ParameterValue / Algorithm
CurveBN254
Scalar fieldp = 21888...5617 (254 bits)
Proof systemGroth16
Hash (circuit)Poseidon (T3, T5, T8 variants)
Hash (quantum)keccak256
Merkle depth20
Merkle capacity~1,048,576 leaves
Root buffer100 entries
Nullifier derivationTwo-level Poseidon2
Field reductionkeccak256(x) % p
On-chain Poseidon gas~30,000 per invocation