3.6 KiB

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,5 over BN254's scalar field
  • Poseidon2 permutation with t=3 over 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 of N words (so 2^(32*N) or 2^(64*N) bits);
  • Felt, short for "Field Element", is a prime field element in the standard representation (integers modulo p);
  • Mont is 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=3 permutation

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=3 permutations

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