Poseidon Hash Function
Poseidon is the core hash function used throughout the Specter data protocol. It was chosen because it is an algebraic hash function designed specifically for efficient computation inside zero-knowledge circuits. Every commitment, nullifier, token identifier, access tag, and Merkle tree node in the protocol is computed using a variant of Poseidon.
Why Poseidon?
Traditional hash functions like SHA-256 and Keccak-256 are designed for hardware and software efficiency on standard processors. They use bitwise operations (XOR, AND, rotations, shifts) that are cheap on CPUs but expensive to represent in the arithmetic circuits used by ZK proof systems.
ZK circuits operate over finite fields — they represent computation as systems of polynomial equations over large prime numbers. Each bitwise operation in SHA-256 or Keccak must be decomposed into field arithmetic, which multiplies the constraint count dramatically.
Poseidon takes the opposite approach. It operates natively on finite field elements using only field multiplications and additions. Its round function is a simple power map (S-box: x^5) followed by a linear mixing layer — operations that translate directly into a small number of R1CS constraints.
| Hash Function | Approximate R1CS Constraints | Circuit Efficiency |
|---|---|---|
| SHA-256 | ~25,000 per hash | Baseline |
| Keccak-256 | ~150,000 per hash | 6x worse than SHA-256 |
| Poseidon (t=3) | ~3,000 per hash | ~8x better than SHA-256 |
This ~8x improvement in constraint count translates directly to faster proof generation, smaller proof sizes, and lower verification gas costs. For a protocol that computes multiple hashes per proof (commitment hash, nullifier hash, 20 Merkle tree node hashes), the cumulative savings are substantial.
BN254 Scalar Field
All Poseidon computations in Specter operate over the BN254 scalar field:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
This is a ~254-bit prime. Every input, output, and intermediate value in a Poseidon hash must be a valid element of this field — meaning it must be a non-negative integer less than p.
The BN254 curve was chosen because it is the curve supported by Ethereum's ecPairing precompile (0x08), which is required for on-chain Groth16 proof verification. Using the same field for both hashing and proving avoids costly field conversions.
When values from other domains (e.g., Keccak-256 hashes, which produce 256-bit outputs) need to enter the Poseidon field, they are reduced modulo p:
fieldValue = keccak256(data) % p
This reduction is performed identically on-chain (in Solidity) and off-chain (in circuits and client code) to ensure consistency.
Poseidon Variants
Specter uses three Poseidon configurations, differing in the number of inputs they accept. The naming convention uses "T" (for "t-value"), which equals the number of inputs plus one (the internal state width).
PoseidonT3 (2 inputs)
output = PoseidonT3(input0, input1)
The workhorse of the protocol. Used for all binary operations:
| Use | Input 0 | Input 1 | Output |
|---|---|---|---|
| Merkle tree nodes | Left child hash | Right child hash | Parent node hash |
| Nullifier (inner) | nullifierSecret | commitment | Inner hash |
| Nullifier (outer) | Inner hash | leafIndex | Final nullifier |
| Token ID | tokenAddress | 0 | tokenId |
| Access tag | nullifierSecret | sessionNonce | accessTag |
On-chain deployment. PoseidonT3 is the only Poseidon variant deployed on-chain, as a Solidity library at approximately 55 KB of bytecode. This large size is why Specter increases the EVM's MaxCodeSize to 64 KB. The on-chain library costs approximately 30,000 gas per hash invocation and is used by the CommitmentTree for computing Merkle tree nodes during root verification.
PoseidonT5 (4 inputs)
output = PoseidonT5(input0, input1, input2, input3)
Used for generic data commitments — the data-agnostic commitment format:
| Use | Input 0 | Input 1 | Input 2 | Input 3 | Output |
|---|---|---|---|---|---|
| Data commitment | secret | nullifierSecret | dataHash | blinding | commitment |
PoseidonT5 is computed off-chain only (in the browser, in the client application, or inside ZK circuits). It is never executed on-chain because:
- On-chain deployment would require ~179,000+ gas per hash
- The smart contracts only need to verify ZK proofs that attest to the commitment's correctness — they never recompute the commitment themselves
PoseidonT8 (7 inputs)
output = PoseidonT8(input0, input1, input2, input3, input4, input5, input6)
Used for token commitments — the full commitment format with token and policy binding:
| Use | Input 0 | Input 1 | Input 2 | Input 3 | Input 4 | Input 5 | Input 6 | Output |
|---|---|---|---|---|---|---|---|---|
| Token commitment | secret | nullifierSecret | tokenId | amount | blinding | policyId | policyParamsHash | commitment |
Like PoseidonT5, PoseidonT8 is computed off-chain only. The on-chain contracts receive only the final commitment hash and verify it through ZK proofs.
All Hashes in the Protocol
The following table catalogs every Poseidon hash computation in the Specter protocol:
| Hash Purpose | Variant | Inputs | Computed Where |
|---|---|---|---|
| Token commitment | PoseidonT8 (7 inputs) | secret, nullifierSecret, tokenId, amount, blinding, policyId, policyParamsHash | Off-chain (client + circuit) |
| Data commitment | PoseidonT5 (4 inputs) | secret, nullifierSecret, dataHash, blinding | Off-chain (client + circuit) |
| Nullifier (inner hash) | PoseidonT3 (2 inputs) | nullifierSecret, commitment | Off-chain (circuit) |
| Nullifier (outer hash) | PoseidonT3 (2 inputs) | innerHash, leafIndex | Off-chain (circuit) |
| Token ID | PoseidonT3 (2 inputs) | tokenAddress, 0 | Off-chain (client) |
| Access tag | PoseidonT3 (2 inputs) | nullifierSecret, sessionNonce | Off-chain (circuit) |
| Merkle tree nodes | PoseidonT3 (2 inputs) | leftChild, rightChild | Off-chain (indexer) + On-chain (tree contract) |
| Change commitment | PoseidonT8 (7 inputs) | secret, nullifierSecret, tokenId, changeAmount, newBlinding, policyId, policyParamsHash | Off-chain (circuit) |
On-Chain vs. Off-Chain
The design principle is clear: minimize on-chain Poseidon computation. Only PoseidonT3 is deployed on-chain, and only for Merkle tree operations that the smart contracts must perform directly (root tracking, proof verification during root updates). All multi-input hashes (4-input and 7-input commitments) are computed off-chain and verified on-chain through ZK proofs — the chain never sees the hash inputs, only the final hash output and a proof that it was correctly computed.
Security Properties
Poseidon's security has been analyzed extensively in the academic literature. Key properties relevant to Specter:
Collision resistance. It is computationally infeasible to find two distinct inputs that produce the same Poseidon hash. This ensures that each commitment is unique and that Merkle tree nodes cannot be forged.
Preimage resistance. Given a Poseidon hash output, it is computationally infeasible to recover the inputs. This ensures that observing a commitment hash on-chain reveals nothing about the underlying data (secret, nullifierSecret, dataHash, blinding, etc.).
Algebraic soundness. Poseidon's round structure (full rounds + partial rounds with the x^5 S-box over a large prime field) provides security margins against algebraic attacks including Grobner basis attacks, interpolation attacks, and differential/linear cryptanalysis. The number of rounds is chosen to provide a security margin of at least 2x above the theoretical minimum.
Field-native operations. Because Poseidon operates directly on BN254 field elements, there are no bit-decomposition or domain-conversion steps that could introduce subtle vulnerabilities. Every operation is a native field multiplication or addition.