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 Function | Constraints (BN254) | Gas Cost | ZK-Friendly |
|---|---|---|---|
| Poseidon | ~240 | Low | ✓ |
| Keccak-256 | ~25,000 | High | ✗ |
| SHA-256 | ~20,000 | High | ✗ |
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):
| Parameter | Value |
|---|---|
| State Size (t) | 3 |
| Full Rounds (R_F) | 8 |
| Partial Rounds (R_P) | 57 |
| S-Box | x⁵ |
| MDS Matrix | 3×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:
- Add round constants
- Apply S-Box (x⁵)
- 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:
| Input | Expected 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
| Operation | PolkaVM Gas | EVM 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