Poseidon Hash on Kusama

Poseidon is a cryptographic hash function designed for zero-knowledge proof systems. This guide covers implementing and using Poseidon on Kusama/Polkadot networks.

Why Poseidon?

Hash FunctionConstraints (BN254)Gas CostZK-Friendly
Poseidon~240Low
Keccak-256~25,000High
SHA-256~20,000High

Poseidon is 100x more efficient in ZK circuits than traditional hashes.

Security: $1M Ethereum Foundation Bounty

The Ethereum Foundation has placed a $1 million bounty on breaking Poseidon through the Poseidon Initiative.

The Challenge

  • Reward: $1,000,000 USD
  • Task: Find a collision or preimage attack against Poseidon
  • Status: UNCLAIMED - No one has successfully broken Poseidon
  • Significance: Demonstrates confidence in Poseidon's security for critical infrastructure

This bounty reinforces Poseidon's position as the trusted hash function for:

  • ZK proof systems (Groth16, PLONK, Halo2)
  • Privacy protocols (Kusama Shield, Aztec)
  • Layer 2 rollups (StarkNet, Polygon zkEVM)
  • Kusama privacy applications

Mathematical Background

BN254 Curve

Poseidon on Kusama uses the BN254 (alt_bn128) curve:

  • Field: Prime field 𝔽p where p = 21888242871839275222246405745257275088696311157297823662689037894645226208583
  • Scalar Field: 𝔽r where r = 21888242871839275222246405745257275088548364400416034343698204186575808495617

Poseidon Parameters

For t=3 (2 inputs + 1 output):

ParameterValue
State Size (t)3
Full Rounds (R_F)8
Partial Rounds (R_P)57
S-Boxx⁵
MDS Matrix3×3 Cauchy matrix

Round Structure

┌─────────────────────────────────────────────────┐
│              Poseidon Permutation                │
├─────────────────────────────────────────────────┤
│  Round 0-3:  Full Round (S-Box all states)      │
│  Round 4-60: Partial Round (S-Box first state)  │
│  Round 61-64: Full Round (S-Box all states)     │
└─────────────────────────────────────────────────┘

Each round:

  1. Add round constants
  2. Apply S-Box (x⁵)
  3. Multiply by MDS matrix

Implementation in Rust (PolkaVM)

Field Element Structure

#![allow(unused)]
fn main() {
/// BN254 scalar field element in Montgomery form
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct Fr([u64; 4]);

const MODULUS: [u64; 4] = [
    0x43e1f593f0000001,
    0x2833e84879b97091,
    0xb85045b68181585d,
    0x30644e72e131a029,
];

/// -MODULUS^(-1) mod 2^64
const INV: u64 = 0xc2e1f593efffffff;
}

Montgomery Multiplication

#![allow(unused)]
fn main() {
/// CIOS Montgomery multiplication: computes a*b*R^(-1) mod p
fn mont_mul(a: &[u64; 4], b: &[u64; 4]) -> Fr {
    let mut t = [0u64; 6];

    for i in 0..4 {
        let mut c: u64 = 0;
        for j in 0..4 {
            let uv = (t[j] as u128) + (a[j] as u128) * (b[i] as u128) + (c as u128);
            t[j] = uv as u64;
            c = (uv >> 64) as u64;
        }
        // ... reduction steps
    }
    Fr(result)
}
}

S-Box Implementation

#![allow(unused)]
fn main() {
fn sbox(x: &Fr) -> Fr {
    let x2 = x.mul(&x);      // x²
    let x4 = x2.mul(&x2);    // x⁴
    x4.mul(x)                // x⁵
}
}

Full Poseidon Function

#![allow(unused)]
fn main() {
const T: usize = 3;
const NROUNDS_F: usize = 8;
const NROUNDS_P: usize = 57;

pub fn poseidon(inputs: &[Fr; 2]) -> Fr {
    let total_rounds = NROUNDS_F + NROUNDS_P;
    let mut state = [Fr::zero(), inputs[0], inputs[1]];

    for r in 0..total_rounds {
        // Add round constants
        for i in 0..T {
            state[i] = state[i].add(&ROUND_CONSTANTS[r * T + i]);
        }

        // Apply S-Box
        if r < NROUNDS_F / 2 || r >= total_rounds - NROUNDS_F / 2 {
            // Full round: S-Box all states
            for i in 0..T {
                state[i] = sbox(&state[i]);
            }
        } else {
            // Partial round: S-Box first state only
            state[0] = sbox(&state[0]);
        }

        // MDS matrix multiplication
        let mut new_state = [Fr::zero(); T];
        for i in 0..T {
            for j in 0..T {
                new_state[i] = new_state[i].add(&state[j].mul(&MDS[i][j]));
            }
        }
        state = new_state;
    }

    state[0]  // Return first state as output
}
}

Solidity Integration

Interface

interface IPoseidon {
    function hash(uint256[2] memory inputs) external pure returns (uint256);
}

Usage in Contracts

contract MyZKContract {
    IPoseidon public constant POSEIDON = 
        IPoseidon(0x1d165f6fE5A30422E0E2140e91C8A9B800380637);

    function computeCommitment(uint256 value, uint256 secret) 
        external pure returns (uint256) 
    {
        return POSEIDON.hash([value, secret]);
    }

    function computeNullifier(uint256 nullifier, uint256 secret) 
        external pure returns (uint256) 
    {
        return POSEIDON.hash([nullifier, secret]);
    }
}

Circom Integration

Using circomlib

pragma circom 2.0.0;

include "circomlib/poseidon.circom";

template PoseidonExample() {
    signal input a;
    signal input b;
    signal output out;

    component poseidon = Poseidon(2);  // 2 inputs
    poseidon.inputs[0] <== a;
    poseidon.inputs[1] <== b;
    out <== poseidon.out;
}

component main {public [a, b]} = PoseidonExample();

Custom Poseidon in Circom

pragma circom 2.0.0;

include "poseidon_bn254.circom";

template MyHash() {
    signal input in[2];
    signal output out;

    component poseidon = Poseidon(2);
    
    for (let i = 0; i < 2; i++) {
        poseidon.inputs[i] <== in[i];
    }
    
    out <== poseidon.out;
}

Test Vectors

Verify your implementation against known values:

InputExpected Output
[0, 0]5520371747610818048552497760483731695918538905235353263918705622436011791040
[1, 2]3240460917331939313458239639914504962640333851982787431747809795119847651274
[123, 456]10422317022970317265083564129867363010880980031113186224756990573079674352133

Testing with cast

# Deploy contract first, then test:
cast call <CONTRACT_ADDRESS> \
  "hash(uint256[2]):(uint256)" \
  "[0,0]" \
  --rpc-url https://testnet-passet-hub-eth-rpc.polkadot.io

Applications

1. Commitment Scheme

#![allow(unused)]
fn main() {
// Commitment = Poseidon(value, secret)
let commitment = poseidon(&[value, secret]);
}

2. Nullifier Hash

#![allow(unused)]
fn main() {
// Prevents double-spending in privacy pools
let nullifier_hash = poseidon(&[nullifier, secret]);
}

3. Merkle Tree

#![allow(unused)]
fn main() {
// Internal node hash
let node_hash = poseidon(&[left_child, right_child]);
}

4. UTXO Commitment

#![allow(unused)]
fn main() {
// Full commitment for shielded pools
// Poseidon(value, asset, Poseidon(nullifier, secret))
let inner = poseidon(&[nullifier, secret]);
let commitment = poseidon(&[value, poseidon(&[asset, inner])]);
}

Gas Costs on Kusama

OperationPolkaVM GasEVM Gas
Poseidon hash~2,000~15,000
Keccak-256~50,000~87,000
Storage write~20,000~20,000

PolkaVM is 7.5x cheaper for Poseidon operations!

Security Considerations

Input Validation

Always validate inputs are in the valid field range:

require(input < SNARK_SCALAR_FIELD, "Input out of range");
#![allow(unused)]
fn main() {
const SNARK_SCALAR_FIELD: u256 = u256::from_str_radix(
    "21888242871839275222246405745257275088548364400416034343698204186575808495617",
    10
).unwrap();
}

Non-Zero Check

require(commitment != 0, "Invalid commitment");

Domain Separation

Use different prefixes for different use cases:

#![allow(unused)]
fn main() {
let tx_commitment = poseidon(&[TX_DOMAIN, value, secret]);
let identity_commitment = poseidon(&[ID_DOMAIN, value, secret]);
}

Resources


Previous: PolkaVM Smart Contracts | Next: Asset Hub Integration