Why we built it
Every ZK privacy system needs a hash function. Not SHA-256, that's catastrophically expensive inside a circuit. You need an algebraic hash: one designed from the ground up to be cheap in arithmetic constraints. Poseidon2 is the current state of the art.
The problem: if you're building ZK infrastructure in Go, there was no standalone Poseidon2 library. Rust has multiple. TypeScript has one. Solidity has one. Go had nothing.
So we built poseidon2-go, a pure-Go implementation of Poseidon2 over the BN254 scalar field. Today we're releasing it under the Apache 2.0 license.
What it does
poseidon2-go gives you four functions:
// Hash arbitrary-length inputs
func Hash(inputs []fr.Element) fr.Element
// Optimized 2-element hash (Merkle trees, commitments)
func Hash2(a, b fr.Element) fr.Element
// Hash to 32-byte output
func HashToBytes(inputs []fr.Element) [32]byte
// Raw permutation on a 4-element state
func Permute(s *[4]fr.Element)
That's the entire public API. One import, no configuration, no options structs. You call Hash2 to build a Merkle tree. You call Hash when you have more than two inputs. You call Permute when you know what you're doing.
The parameters
| Parameter | Value |
|---|---|
| Field | BN254 Fr |
| State width (t) | 4 |
| Full rounds | 8 (4 + 4) |
| Partial rounds | 56 |
| S-box | x⁵ |
| Rate | 3 |
| Capacity | 1 |
These match Aztec's Barretenberg prover, which means circuits proved in Noir will accept hashes computed by this library. Same constants, same sponge construction, same outputs.
Cross-implementation compatibility
This was the hardest part. Getting the permutation right is straightforward. It's just matrix multiplications and exponentiations over a field. Getting the sponge construction right across implementations is where things break.
Different implementations encode the IV differently, absorb into different state indices, and handle padding differently. We matched Noir's FieldSponge mode exactly:
- IV:
len(inputs) << 64, placed instate[3](the capacity element) - Absorption: into
state[0..2](rate = 3), permuting when the rate buffer fills - Squeeze:
state[0]after the final permutation - No padding marker (fixed-length mode)
Every test vector is verified against three independent implementations:
- Noir stdlib (Barretenberg): Aztec's ZK prover
- @zkpassport/poseidon2: TypeScript reference
- Poseidon2.sol: Ethereum Solidity on-chain implementation
If those three agree and we match, it's correct.
Performance
Zero heap allocations. The entire hot path operates on stack-allocated fr.Element values.
| Benchmark | Time | Allocations |
|---|---|---|
| Permute | 12.1 µs | 0 |
| Hash2 (Merkle node) | 9.7 µs | 0 |
| Hash10 | 39.8 µs | 0 |
| Hash100 | 395 µs | 0 |
Hash2 is faster than Permute because it skips the sponge loop. Two elements fit in the rate, so it's a single permutation with optimized state setup.
These numbers are from an Apple M1 Pro. Your mileage will vary, but the zero-allocation property holds everywhere. The garbage collector never sees this code.
Why Go matters for ZK infrastructure
Most ZK tooling is written in Rust. That makes sense for provers, where you need every cycle. But the ecosystem around a prover (indexers, relayers, backend services, chain integrations) is often written in Go.
If your relayer needs to verify a Merkle proof before submitting a transaction, it needs to recompute Poseidon2 hashes. If your indexer watches for note commitments, it needs Poseidon2. If your backend derives nullifiers for a wallet sync service, it needs Poseidon2.
Without a Go library, these services either shell out to a Rust binary, maintain FFI bindings, or reimplement the hash function from scratch. All three options are fragile. A native Go library eliminates the problem.
How we use it
Inside NIX Protocol, poseidon2-go powers:
- Note commitment verification: every UTXO note is committed with Poseidon2
- Merkle tree construction: the on-chain Merkle tree uses Poseidon2 for internal nodes
- Nullifier derivation: spent notes produce nullifiers via Poseidon2
- Backend services: relayers and indexers that interact with the privacy pool
The same hash function runs in the Noir circuit (Barretenberg), the Solidity contract (poseidon2-evm), and now the Go backend (poseidon2-go). Same inputs, same outputs, every time.
Get started
Install:
go get github.com/nixprotocol/poseidon2-go
Use it:
import (
"github.com/consensys/gnark-crypto/ecc/bn254/fr"
poseidon2 "github.com/nixprotocol/poseidon2-go"
)
var a, b fr.Element
a.SetUint64(1)
b.SetUint64(2)
digest := poseidon2.Hash2(a, b)
One dependency: gnark-crypto for BN254 field arithmetic. That's it.
Open source, open standard
We released poseidon2-go under the Apache 2.0 license. Use it in your project, fork it, vendor it, do whatever you need.
The ZK ecosystem moves faster when implementations are open and interoperable. If you're building privacy infrastructure, a rollup, a bridge, or anything that touches Poseidon2 on BN254, this library exists so you don't have to write it yourself.
GitHub: github.com/nixprotocol/poseidon2-go
Docs & integration support: nixprotocol.com/docs