rust-poseidon-bn254-pure
Self-contained (no external dependencies), pure Rust implementation of Poseidon and Poseidon2 hash functions over the BN254 curve's scalar field, using 32 bit limbs internally.
It's primarily intended to be used on 32-bit platforms, eg. 32-bit RISC-V (rv32im)
(though porting to 64 bits shouldn't be a big effort; TODO).
The algebra implementation is based on zikkurat-algebra
and staging-agda.
Compatibility
The Poseidon implementation is compatible with circomlib.
The Poseidon2 implementation is compatible with zkfriendlyhashzoo.
It used to be compatible with the HorizenLabs implementation,
until they changed all their constants in this commit.
We don't think it's worth the pain to follow this change.
Status
Currently, only the following instances are implemented:
- Poseidon permutation with
t=2,3,4,5over BN254's scalar field - Poseidon2 permutation with
t=3over BN254's scalar field
I feel that larger states are unneccesary in practice. As a concrete example,
PSE's RLN circuit uses t=2,3,4.
The proper way to handle larger input is to implement the sponge construction.
Usage
There are three main types:
BigInt<N>is an unsigned big integer consisting ofNwords (so2^(32*N)or2^(64*N)bits);Felt, short for "Field Element", is a prime field element in the standard representation (integers modulop);Montis a field element in the Montgomery represntation. This is used internally for calculations, as the multiplications is much faster this way.
The core functionality of the Poseidon family of hash functions is the permutation,
which takes an array of t field elements, and returns the same:
fn permute( [Felt; t] ) -> [Felt; t]
From this one can build all kind of stuff, including a proper hash function (using
the so-called "sponge construction). The latter is not implemented in circomlib,
instead, what they have is a compression function parametrized by t:
fn compress( [Felt; t-1] ) -> Felt
This takes t-1 field elements and returns one (which is interpreted as a hash).
This is implemented by extending the input with a 0, applying the permutation, and
taking the first element of the output vector (note: in circomlib, the extra 0 is
at the beginning, not at the end, but that doesn't matter at all; just be consistent).
Remark: That extra zero (called the "capacity") is extremely important, without that the whole construction would be totally insecure!
Speed
Some approximate benchmark numbers below.
32-bit RISC-V
On RV32IM (the primary target as of now), we have approximately the following cycle counts:
Poseidon2:
- 350k cycles for a single
t=3permutation
Modern CPUs
On modern 64-bit CPU-s, the 64-bit version is preferred (TODO: implement it).
32 bit version, running on an M2 macbook pro:
- 155 msec for 10k
t=3permutations
TODO
- optimize squaring to use less multiplications (?)
- benchmark RISC-V cycles
- add more Poseidon2 state widths (not just
t=3) - implement
circomlib-compatible Poseidon - add a proper test-suite; in particular, more complete testing of the field operations
- add a 64 bit version
- further optimizations
- implement the sponge construction