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.
| Variant | Inputs | Circuit Name | Primary Uses |
|---|---|---|---|
| Poseidon2 | 2 | PoseidonT3 | Merkle tree node hashing, nullifier derivation, access tags, token ID derivation |
| Poseidon4 | 4 | PoseidonT5 | OpenGhost commitments |
| Poseidon7 | 7 | PoseidonT8 | CommitRevealVault 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:
| Input | Description |
|---|---|
secret | Random 31-byte secret known only to the key holder |
nullifierSecret | Separate random secret used in nullifier derivation |
tokenId | Poseidon2(tokenAddress, 0) — field-safe token identifier |
amount | Token amount in base units (e.g., aghost with 18 decimals) |
blinding | Random blinding factor for commitment uniqueness |
policyId | Identifier of the policy contract (0 for no policy) |
policyParamsHash | keccak256(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
| Parameter | Value |
|---|---|
| Depth | 20 |
| Capacity | 2^20 = 1,048,576 commitments |
| Hash function | Poseidon2 (PoseidonT3) |
| Zero value | keccak256("ghost_protocol") % BN254_FIELD |
| Root history | 100-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
| Component | Location | Purpose |
|---|---|---|
| Root history | On-chain (ring buffer) | Stores the last 100 Merkle roots for proof verification |
| Current root | On-chain | The most recent root, updated on each insertion |
| Next leaf index | On-chain | Counter tracking the next available leaf position |
| Filled subtrees | On-chain | Array of 20 hashes enabling O(20) root recomputation |
| Full tree | Off-chain | Complete 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:
- The commitment is placed at position
nextLeafIndex - The path from the new leaf to the root is recomputed (20 Poseidon2 hashes)
- The new root is pushed into the ring buffer
nextLeafIndexis incremented- 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 knowingnullifierSecret, 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:
- Break BN254 elliptic curve operations (Groth16 proofs)
- 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
| Property | Detail |
|---|---|
| Optional | Users can commit without a quantum secret |
| Hash function | keccak256 (256-bit, quantum-resistant against preimage attacks) |
| Storage | On-chain, indexed by commitment |
| Verification | Preimage check at reveal time |
| Key format | V4 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
| Parameter | Value / Algorithm |
|---|---|
| Curve | BN254 |
| Scalar field | p = 21888...5617 (254 bits) |
| Proof system | Groth16 |
| Hash (circuit) | Poseidon (T3, T5, T8 variants) |
| Hash (quantum) | keccak256 |
| Merkle depth | 20 |
| Merkle capacity | ~1,048,576 leaves |
| Root buffer | 100 entries |
| Nullifier derivation | Two-level Poseidon2 |
| Field reduction | keccak256(x) % p |
| On-chain Poseidon gas | ~30,000 per invocation |