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 representation. This is used internally for calculations, as multiplication (the main bottleneck) is much faster this way.

The core functionality of the Poseidon family of hash functions is the permutation, which takes an array of t >= 2 field elements, and returns the same:

fn permute( [Felt; t] ) -> [Felt; t]

From this one can build all kinds 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 a single one (which is interpreted as a hash. Note that a field element contains about 254 bits of information, which is pretty fine for a cryptographic hash output)

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:

  • Poseidon: about 900k cycles for a single t=3 permutation
  • Poseidon2: about 350k cycles for a single t=3 permutation

Note: Poseidon is about 2.5x slower, simply because there are about 2.5x more field multiplications involved (which absolutely dominate the runtime).

Modern CPUs

On modern 64-bit CPU-s, the 64-bit version would be preferred (TODO: implement it).

32 bit version, running on an M2 macbook pro (single threaded):

  • Poseidon: 320 msec for 10,000 t=3 permutations
  • Poseidon2: 140 msec for 10,000 t=3 permutations

TODO

  • clean up the code and make it more idiomatic
  • implement circomlib-compatible Poseidon
  • benchmark RISC-V cycles
  • add a proper test-suite; in particular, more complete testing of the field operations
  • add more tests for the corner cases specifically
  • add more Poseidon2 state widths (not just t=3)
  • add a 64 bit version
  • implement the sponge construction
  • optimize squaring to use less multiplications (?)
  • investigate further optimization possibilities (?)
Description
Self-contained Rust implementation of Poseidon hashes over BN254
Readme
Languages
Haskell 46.4%
Circom 43.9%
Rust 9.1%
Sage 0.6%