Skip to main content

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 exceed p. 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

PropertyValue
Inputs2
State widthT = 3
On-chainYes — 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

PropertyValue
Inputs4
State widthT = 5
On-chainNo — 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

PropertyValue
Inputs7
State widthT = 8
On-chainNo — 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

VariantInputsState (T)On-chainGasPrimary Use
Poseidon22T3Yes~30,000Merkle nodes, nullifiers, token IDs, access tags
Poseidon44T5No~80,000*OpenGhost persistent key commitments
Poseidon77T8No~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.