From 32c3b8659c2ffe3d33759fdb501a3fdea4eee47a Mon Sep 17 00:00:00 2001 From: Filip Date: Sun, 19 Jul 2026 15:51:31 +0700 Subject: [PATCH] light-poseidon-python --- Cargo.toml | 1 + light-poseidon-python/Cargo.toml | 18 ++++ light-poseidon-python/Makefile | 24 ++++++ light-poseidon-python/README.md | 57 ++++++++++++ light-poseidon-python/examples/basic.py | 36 ++++++++ .../examples/onchain_compare.py | 68 +++++++++++++++ light-poseidon-python/pyproject.toml | 30 +++++++ light-poseidon-python/src/lib.rs | 86 +++++++++++++++++++ 8 files changed, 320 insertions(+) create mode 100644 light-poseidon-python/Cargo.toml create mode 100644 light-poseidon-python/Makefile create mode 100644 light-poseidon-python/README.md create mode 100644 light-poseidon-python/examples/basic.py create mode 100644 light-poseidon-python/examples/onchain_compare.py create mode 100644 light-poseidon-python/pyproject.toml create mode 100644 light-poseidon-python/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index da88a83..736f7bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "light-poseidon", + "light-poseidon-python", "xtask", ] resolver = "2" diff --git a/light-poseidon-python/Cargo.toml b/light-poseidon-python/Cargo.toml new file mode 100644 index 0000000..cb470f1 --- /dev/null +++ b/light-poseidon-python/Cargo.toml @@ -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"] \ No newline at end of file diff --git a/light-poseidon-python/Makefile b/light-poseidon-python/Makefile new file mode 100644 index 0000000..91d3151 --- /dev/null +++ b/light-poseidon-python/Makefile @@ -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 \ No newline at end of file diff --git a/light-poseidon-python/README.md b/light-poseidon-python/README.md new file mode 100644 index 0000000..56d2d7e --- /dev/null +++ b/light-poseidon-python/README.md @@ -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 +``` \ No newline at end of file diff --git a/light-poseidon-python/examples/basic.py b/light-poseidon-python/examples/basic.py new file mode 100644 index 0000000..898d15c --- /dev/null +++ b/light-poseidon-python/examples/basic.py @@ -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]}...") \ No newline at end of file diff --git a/light-poseidon-python/examples/onchain_compare.py b/light-poseidon-python/examples/onchain_compare.py new file mode 100644 index 0000000..9832dc3 --- /dev/null +++ b/light-poseidon-python/examples/onchain_compare.py @@ -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() \ No newline at end of file diff --git a/light-poseidon-python/pyproject.toml b/light-poseidon-python/pyproject.toml new file mode 100644 index 0000000..92f3058 --- /dev/null +++ b/light-poseidon-python/pyproject.toml @@ -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" diff --git a/light-poseidon-python/src/lib.rs b/light-poseidon-python/src/lib.rs new file mode 100644 index 0000000..d90328e --- /dev/null +++ b/light-poseidon-python/src/lib.rs @@ -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, +} + +#[pymethods] +impl Hasher { + #[new] + fn new(nr_inputs: usize) -> PyResult { + let inner = Poseidon::::new_circom(nr_inputs) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?; + Ok(Self { inner }) + } + + fn hash(&mut self, inputs: Vec) -> PyResult { + let fr_inputs: Vec = 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) -> PyResult> { + let fr_inputs: Vec = 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>) -> PyResult { + 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>) -> PyResult { + 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) -> PyResult { + let mut hasher = Poseidon::::new_circom(inputs.len()) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?; + let fr_inputs: Vec = 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>) -> PyResult { + let mut hasher = Poseidon::::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::()?; + m.add("__version__", "0.1.0")?; + Ok(()) +} \ No newline at end of file