Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"light-poseidon",
"light-poseidon-python",
"xtask",
]
resolver = "2"
18 changes: 18 additions & 0 deletions light-poseidon-python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "light-poseidon-python"
version = "0.1.0"
edition = "2021"

[lib]
name = "light_poseidon_python"
crate-type = ["cdylib"]

[dependencies]
light-poseidon = { path = "../light-poseidon" }
ark-bn254 = "0.5.0"
ark-ff = "0.5.0"
hex = "0.4.3"

[dependencies.pyo3]
version = "0.23"
features = ["extension-module"]
24 changes: 24 additions & 0 deletions light-poseidon-python/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.PHONY: install build test test-onchain clean publish

install:
python3 -m venv .venv
.venv/bin/pip install maturin
.venv/bin/maturin develop

build:
.venv/bin/maturin build --release

test: install
.venv/bin/python examples/basic.py

test-onchain: install
.venv/bin/pip install requests
.venv/bin/python examples/onchain_compare.py

clean:
rm -rf .venv
rm -rf target
rm -rf dist

publish: build
.venv/bin/maturin publish
57 changes: 57 additions & 0 deletions light-poseidon-python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# light-poseidon Python Bindings

Python bindings for [light-poseidon](https://github.com/Lightprotocol/light-poseidon), originally by [Mike Rostecki](https://github.com/vadorovsky). This package is a fork providing Python access to the Poseidon hash implementation for the BN254 curve.

## Compatibility

This package is made to be compatible with [Kusama Shield's PoseidonPolkaVM](https://codeberg.org/KusamaShield/PoseidonPolkaVM).

## Installation

```bash
pip install light-poseidon
```

## Usage

```python
from light_poseidon_python import poseidon_hash, poseidon_hash_bytes, Hasher

# One-shot function with uint64 inputs
result = poseidon_hash([1, 2])
print(result) # 0x115cc0f5e7d690413df64c6b9662e9cf2a3617f2743245519e19607a4417189a

# Byte inputs (32 bytes each, big-endian)
a = b"\x01" * 32
b = b"\x02" * 32
result = poseidon_hash_bytes([a, b])

# Reusable Hasher class
hasher = Hasher(2) # 2 inputs
result = hasher.hash([1, 2])

# Byte variants
hasher.hash_bytes_be([a, b]) # big-endian
hasher.hash_bytes_le([a, b]) # little-endian
```

## Supported arities

1-12 inputs are supported.

## Development

```bash
# Using Makefile
make install # Create venv and build
make test # Run basic tests
make build # Build wheel
make publish # Publish to PyPI

# Or manually
cd light-poseidon-python
python3 -m venv .venv
source .venv/bin/activate
pip install maturin
maturin develop
```
36 changes: 36 additions & 0 deletions light-poseidon-python/examples/basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Basic usage of the light_poseidon_python library."""

from light_poseidon_python import poseidon_hash, poseidon_hash_bytes, Hasher

# --- One-shot function ---

print("=== poseidon_hash (uint64 inputs) ===")
print(f"hash([0, 0]): {poseidon_hash([0, 0])}")
print(f"hash([1, 2]): {poseidon_hash([1, 2])}")
print(f"hash([123, 456]): {poseidon_hash([123, 456])}")
print()

# --- Byte inputs ---

print("=== poseidon_hash_bytes (32-byte inputs) ===")
a = b"\x00" * 32 # 0 as 32 bytes big-endian
b = b"\x01" * 32 # 1 as 32 bytes big-endian
print(f"hash_bytes([0x00..00, 0x01..01]): {poseidon_hash_bytes([a, b])}")
print()

# --- Hasher class (reusable) ---

print("=== Hasher class ===")
hasher = Hasher(2) # 2 inputs
print(f"hasher.hash([1, 2]): {hasher.hash([1, 2])}")
print(f"hasher.hash_bytes_be([...]): {hasher.hash_bytes_be([b, a])}")
print(f"hasher.hash_bytes_le([...]): {hasher.hash_bytes_le([b, a])}")
print()

# --- Different arities ---

print("=== Different arities (1-12 inputs) ===")
for n in range(1, 13):
h = Hasher(n)
result = h.hash([i + 1 for i in range(n)])
print(f" arity={n:2d}: {result[:18]}...")
68 changes: 68 additions & 0 deletions light-poseidon-python/examples/onchain_compare.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""
Compare local Poseidon hash against the on-chain contract on Paseo Asset Hub.

Requires: requests (pip install requests)
"""

import requests
from light_poseidon_python import poseidon_hash

CONTRACT = "0x1d165f6fE5A30422E0E2140e91C8A9B800380637"
RPC_URL = "https://paseo-assethub-rpc.laissez-faire.trade"
SELECTOR = "561558fe"


def call_on_chain(a: int, b: int) -> str:
"""Call hash(uint256[2]) on the deployed contract."""
calldata = f"0x{SELECTOR}{a:064x}{b:064x}"
payload = {
"jsonrpc": "2.0",
"id": 1,
"method": "eth_call",
"params": [{"to": CONTRACT, "data": calldata}, "latest"],
}
resp = requests.post(RPC_URL, json=payload, timeout=30)
resp.raise_for_status()
result = resp.json()
if "error" in result:
raise RuntimeError(f"RPC error: {result['error']}")
return result["result"]


def main():
test_cases = [
(0, 0),
(1, 2),
(123, 456),
(255, 256),
(1000000, 999999),
]

print(f"Contract: {CONTRACT}")
print(f"RPC: {RPC_URL}")
print()

all_pass = True
for a, b in test_cases:
local = poseidon_hash([a, b])
on_chain = call_on_chain(a, b)
passed = local.lower() == on_chain.lower()
status = "PASS" if passed else "FAIL"
if not passed:
all_pass = False

print(f"hash([{a}, {b}])")
print(f" local: {local}")
print(f" on-chain: {on_chain}")
print(f" {status}")
print()

if all_pass:
print("All tests passed - local matches on-chain!")
else:
print("MISMATCH detected!")
exit(1)


if __name__ == "__main__":
main()
30 changes: 30 additions & 0 deletions light-poseidon-python/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"

[project]
name = "light-poseidon"
version = "0.1.1"
description = "Python bindings for light-poseidon (Poseidon hash for BN254)"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "Apache-2.0"}
authors = [
{name = "Mike Rostecki", email = "vadorovsky@protonmail.com"},
{name = "flipchan", email = "flipchan@protonmail.com"},
]
keywords = ["cryptography", "hash", "poseidon", "zero-knowledge", "zksnark", "bn254"]
classifiers = [
"License :: OSI Approved :: Apache Software License",
"Programming Language :: Rust",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]

[tool.maturin]
features = ["pyo3/extension-module"]
module-name = "light_poseidon_python"
86 changes: 86 additions & 0 deletions light-poseidon-python/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use ark_bn254::Fr;
use ark_ff::{BigInteger, PrimeField};
use light_poseidon::{Poseidon, PoseidonBytesHasher, PoseidonHasher as _};
use pyo3::prelude::*;

#[pyclass]
struct Hasher {
inner: Poseidon<Fr>,
}

#[pymethods]
impl Hasher {
#[new]
fn new(nr_inputs: usize) -> PyResult<Self> {
let inner = Poseidon::<Fr>::new_circom(nr_inputs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(Self { inner })
}

fn hash(&mut self, inputs: Vec<u64>) -> PyResult<String> {
let fr_inputs: Vec<Fr> = inputs.into_iter().map(Fr::from).collect();
let result = self
.inner
.hash(&fr_inputs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(format!("0x{}", hex::encode(result.into_bigint().to_bytes_be())))
}

fn hash_raw(&mut self, inputs: Vec<u64>) -> PyResult<Vec<u8>> {
let fr_inputs: Vec<Fr> = inputs.into_iter().map(Fr::from).collect();
let result = self
.inner
.hash(&fr_inputs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(result.into_bigint().to_bytes_be().to_vec())
}

fn hash_bytes_be(&mut self, inputs: Vec<Vec<u8>>) -> PyResult<String> {
let byte_refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
let result = self
.inner
.hash_bytes_be(&byte_refs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(format!("0x{}", hex::encode(result)))
}

fn hash_bytes_le(&mut self, inputs: Vec<Vec<u8>>) -> PyResult<String> {
let byte_refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
let result = self
.inner
.hash_bytes_le(&byte_refs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(format!("0x{}", hex::encode(result)))
}
}

#[pyfunction]
fn poseidon_hash(inputs: Vec<u64>) -> PyResult<String> {
let mut hasher = Poseidon::<Fr>::new_circom(inputs.len())
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
let fr_inputs: Vec<Fr> = inputs.into_iter().map(Fr::from).collect();
let result = hasher
.hash(&fr_inputs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(format!("0x{}", hex::encode(result.into_bigint().to_bytes_be())))
}

#[pyfunction]
fn poseidon_hash_bytes(inputs: Vec<Vec<u8>>) -> PyResult<String> {
let mut hasher = Poseidon::<Fr>::new_circom(inputs.len())
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
let byte_refs: Vec<&[u8]> = inputs.iter().map(|v| v.as_slice()).collect();
let result = hasher
.hash_bytes_be(&byte_refs)
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
Ok(format!("0x{}", hex::encode(result)))
}

#[pymodule]
fn light_poseidon_python(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(poseidon_hash, m)?)?;
m.add_function(wrap_pyfunction!(poseidon_hash_bytes, m)?)?;
m.add_class::<Hasher>()?;
m.add("__version__", "0.1.0")?;
Ok(())
}