Poseidon Hash Reference
Specter uses the Poseidon hash function throughout its cryptographic protocol. Poseidon is an algebraic hash function designed for efficiency inside arithmetic circuits (ZK-SNARKs), operating natively over prime fields without the bit-decomposition overhead of hash functions like SHA-256 or Keccak.
BN254 Field
All Poseidon operations in Specter use the scalar field of the BN254 (alt-bn128) elliptic curve:
p = 21888242871839275222246405745257275088548364400416034343698204186575808495617
This is a 254-bit prime. All inputs and outputs of Poseidon are elements of this field (integers in the range [0, p-1]).
Field Reduction
Any value used as a Poseidon input must be reduced modulo p. This is critical for values that may exceed the field size:
- BN254 field elements (from other Poseidon outputs): Already in the field, no reduction needed.
- uint256 values (e.g., token amounts): Always less than
2^256, but can exceedp. Must reduce. - keccak256 outputs (256-bit): These are uniformly distributed over
[0, 2^256)and will exceed the ~254-bit field roughly 13% of the time. Must reduce. - Ethereum addresses (160-bit): Always less than
p. No reduction needed. - Leaf indices (uint32): Always less than
p. No reduction needed.
const BN254_FIELD_PRIME = 21888242871839275222246405745257275088548364400416034343698204186575808495617n;
function toField(value) {
const v = BigInt(value);
return ((v % BN254_FIELD_PRIME) + BN254_FIELD_PRIME) % BN254_FIELD_PRIME;
}
The double-modulo pattern ((v % p) + p) % p handles negative values correctly, though in practice Specter inputs are always non-negative.
Poseidon Variants
Specter uses three Poseidon configurations, distinguished by the number of inputs. The "T" number refers to the internal state width (number of inputs + 1).
Poseidon2 (T3) — 2 Inputs
| Property | Value |
|---|---|
| Inputs | 2 |
| State width | T = 3 |
| On-chain | Yes — PoseidonT3 at 0xacaef99b13d5846e3309017586de9f777da41548 |
| Gas cost | ~30,000 gas per hash |
Use cases:
- Merkle tree internal nodes:
hash(left, right)for each level of the CommitmentTree. - Nullifier derivation:
nullifier = Poseidon2(nullifierSecret, leafIndex). - Access tags:
accessTag = Poseidon2(secret, tokenIdHash). - Token ID hashing:
tokenIdHash = Poseidon2(tokenAddress, 0).
On-chain interface:
// PoseidonT3 contract
function poseidon(uint256[2] memory inputs) public pure returns (uint256);
JavaScript usage:
import { buildPoseidon } from "circomlibjs";
const poseidon = await buildPoseidon();
// Merkle node: hash two children
const node = poseidon.F.toObject(poseidon([leftChild, rightChild]));
// Nullifier
const nullifier = poseidon.F.toObject(poseidon([nullifierSecret, BigInt(leafIndex)]));
// Token ID hash
const tokenIdHash = poseidon.F.toObject(poseidon([BigInt(tokenAddress), 0n]));
Poseidon4 (T5) — 4 Inputs
| Property | Value |
|---|---|
| Inputs | 4 |
| State width | T = 5 |
| On-chain | No — off-chain only |
| Gas cost | ~80,000 gas (estimated, not deployed) |
Use cases:
- OpenGhost persistent key commitments:
commitment = Poseidon4(secret, nullifierSecret, dataHash, blinding).
This variant is used exclusively in the data privacy (OpenGhost) subsystem. It is not deployed on-chain because persistent key commitments are computed client-side and only the resulting hash is submitted.
JavaScript usage:
const poseidon = await buildPoseidon();
// OpenGhost commitment (4 inputs)
const commitment = poseidon.F.toObject(
poseidon([
secret,
nullifierSecret,
toField(dataHash), // keccak256 output, must reduce to field
blinding,
])
);
Poseidon7 (T8) — 7 Inputs
| Property | Value |
|---|---|
| Inputs | 7 |
| State width | T = 8 |
| On-chain | No — off-chain only |
| Gas cost | ~179,000+ gas (prohibitively expensive for on-chain use) |
Use cases:
- CommitRevealVault commitments:
commitment = Poseidon7(secret, nullifierSecret, blinding, tokenIdHash, amount, policyId, policyParamsHash).
This is the primary commitment hash for the token privacy system. It takes 7 inputs to bind the commitment to a specific token, amount, and optional policy. The hash is computed client-side and the result is submitted in the commit() transaction.
Why not on-chain? At ~179,000+ gas per invocation, an on-chain PoseidonT8 would make commit transactions prohibitively expensive. Instead, the commitment is computed off-chain, and the ZK circuit proves correct computation during the reveal.
JavaScript usage:
const poseidon = await buildPoseidon();
const commitment = poseidon.F.toObject(
poseidon([
secret,
nullifierSecret,
blinding,
tokenIdHash,
toField(amount),
toField(policyId), // address cast to uint256
toField(policyParamsHash), // keccak256 output, reduce to field
])
);
Comparison Table
| Variant | Inputs | State (T) | On-chain | Gas | Primary Use |
|---|---|---|---|---|---|
| Poseidon2 | 2 | T3 | Yes | ~30,000 | Merkle nodes, nullifiers, token IDs, access tags |
| Poseidon4 | 4 | T5 | No | ~80,000* | OpenGhost persistent key commitments |
| Poseidon7 | 7 | T8 | No | ~179,000+* | CommitRevealVault commitments |
*Estimated gas if deployed on-chain; these variants are off-chain only.
circomlibjs Implementation Notes
The circomlibjs library provides a single poseidon function that automatically selects the correct internal configuration based on the number of inputs:
import { buildPoseidon } from "circomlibjs";
const poseidon = await buildPoseidon();
// 2 inputs → uses T3 configuration internally
poseidon([a, b]);
// 4 inputs → uses T5 configuration internally
poseidon([a, b, c, d]);
// 7 inputs → uses T8 configuration internally
poseidon([a, b, c, d, e, f, g]);
Output Conversion
The poseidon() function returns an internal field representation. Use poseidon.F.toObject() to convert to a JavaScript BigInt:
const rawResult = poseidon([a, b]);
const bigintResult = poseidon.F.toObject(rawResult);
// bigintResult is a BigInt in [0, p-1]
Input Types
The poseidon() function accepts BigInt, number, or string inputs. For consistency and to avoid precision issues, always use BigInt:
// Preferred
poseidon([123n, 456n]);
// Also works but less explicit
poseidon([123, 456]);
poseidon(["123", "456"]);
On-Chain Verification
The on-chain PoseidonT3 contract at 0xacaef99b13d5846e3309017586de9f777da41548 is used by the CommitmentTree to compute Merkle tree internal nodes. You can call it directly for verification:
import { ethers } from "ethers";
const provider = new ethers.JsonRpcProvider("https://testnet.specterchain.com");
const poseidonT3 = new ethers.Contract(
"0xacaef99b13d5846e3309017586de9f777da41548",
["function poseidon(uint256[2] memory) pure returns (uint256)"],
provider
);
const result = await poseidonT3.poseidon([leftChild, rightChild]);
console.log("On-chain Poseidon2:", result.toString());
The on-chain result must match the circomlibjs off-chain result for the same inputs. If they do not match, check that inputs are properly reduced to the BN254 field.