Compare commits

...

11 Commits

Author SHA1 Message Date
Ben-PH
f0649e22d0 Use seedable and clean up the setups 2025-05-15 15:58:33 +02:00
Ben-PH
9bb8246b6a test: Inc. new_circom(1) constructed hash in bench 2025-05-06 15:22:54 +02:00
Ben-PH
96bc791047 fix: correct alpha to the default '5' 2025-05-06 15:18:13 +02:00
Ben-PH
a0cefd50bd fix: correct benchmark mislabelling 2025-05-06 15:13:06 +02:00
Ben-PH
63a0472466 fix: make benchmarks actually comparative 2025-05-06 11:49:35 +02:00
Ben-PH
b65433ba99 (ugly as sin)test: generate objects needed to bench_cmp poseidons 2025-05-05 19:32:00 +02:00
Ben-PH
80b393568f style: Make poseidon details easier to grok 2025-05-05 18:21:00 +02:00
Ben-PH
4d93b592bc feat: Add light-poseidon crate dep 2025-05-05 12:43:12 +02:00
Ben-PH
602c923296 build(nix devshell): Add gnuplot and cargo-make 2025-05-04 14:53:22 +02:00
markoburcul
f9bf41b69d makefile: install wasm-pack with cargo 2025-04-23 11:54:55 +02:00
markoburcul
9a5ab32d0d flake: add rust overlay and shell dependencies 2025-04-23 11:54:35 +02:00
9 changed files with 303 additions and 112 deletions

50
Cargo.lock generated
View File

@@ -1226,6 +1226,18 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "light-poseidon"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39e3d87542063daaccbfecd78b60f988079b6ec4e089249658b9455075c78d42"
dependencies = [
"ark-bn254",
"ark-ff 0.5.0",
"num-bigint",
"thiserror 1.0.69",
]
[[package]]
name = "litrs"
version = "0.4.1"
@@ -1415,7 +1427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
dependencies = [
"memchr",
"thiserror",
"thiserror 2.0.12",
"ucd-trie",
]
@@ -1564,13 +1576,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy 0.8.24",
]
[[package]]
@@ -1706,7 +1717,7 @@ dependencies = [
"serde",
"serde_json",
"sled",
"thiserror",
"thiserror 2.0.12",
"tiny-keccak",
"zerokit_utils",
]
@@ -1770,7 +1781,7 @@ dependencies = [
"primitive-types",
"proptest",
"rand 0.8.5",
"rand 0.9.0",
"rand 0.9.1",
"rlp",
"ruint-macro",
"serde",
@@ -2008,13 +2019,33 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
"thiserror-impl 2.0.12",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
@@ -2537,8 +2568,13 @@ dependencies = [
"hex",
"hex-literal",
"lazy_static",
"light-poseidon",
"num-bigint",
"num-traits",
"rand 0.9.1",
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"rln",
"serde",
"sled",
"tiny-keccak",

View File

@@ -26,7 +26,7 @@ endif
[ -s "$$NVM_DIR/nvm.sh" ] && \. "$$NVM_DIR/nvm.sh" && \
nvm install 22.14.0 && \
nvm use 22.14.0'
@curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
@cargo install wasm-pack
@echo "\033[1;32m>>> Now run this command to activate Node.js 22.14.0: \033[1;33msource $$HOME/.nvm/nvm.sh && nvm use 22.14.0\033[0m"
build: .pre-build

23
flake.lock generated
View File

@@ -18,7 +18,28 @@
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1745289264,
"narHash": "sha256-7nt+UJ7qaIUe2J7BdnEEph9n2eKEwxUwKS/QIr091uA=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3b7171858c20d5293360042936058fb0c4cb93a9",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
}
},

View File

@@ -4,9 +4,13 @@
inputs = {
# Version 24.11
nixpkgs.url = "github:NixOS/nixpkgs?rev=f44bd8ca21e026135061a0a57dcf3d0775b67a49";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs }:
outputs = { self, nixpkgs, rust-overlay }:
let
stableSystems = [
"x86_64-linux" "aarch64-linux"
@@ -15,7 +19,8 @@
"i686-windows"
];
forAllSystems = nixpkgs.lib.genAttrs stableSystems;
pkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
overlays = [ (import rust-overlay) ];
pkgsFor = forAllSystems (system: import nixpkgs { inherit system overlays; });
in rec
{
packages = forAllSystems (system: let
@@ -29,9 +34,21 @@
pkgs = pkgsFor.${system};
in {
default = pkgs.mkShell {
inputsFrom = [
packages.${system}.default
buildInputs = with pkgs; [
git
cmake
rustup
cargo-make
gnuplot
xz
wasm-pack
rust-bin.stable.latest.default
];
# Shared library liblzma.so.5 used by wasm-pack
shellHook = ''
xz_lib=$(nix-store -q --references $(which xz) | grep xz)
export LD_LIBRARY_PATH=$xz_lib/lib:$LD_LIBRARY_PATH
'';
};
});
};

View File

@@ -24,6 +24,7 @@ sled = "0.34.7"
serde = "1.0"
lazy_static = "1.5.0"
hex = "0.4"
light-poseidon = "0.3.0"
[dev-dependencies]
ark-bn254 = { version = "0.5.0", features = ["std"] }
@@ -31,6 +32,10 @@ num-traits = "0.2.19"
hex-literal = "1.0.0"
tiny-keccak = { version = "2.0.2", features = ["keccak"] }
criterion = { version = "0.4.0", features = ["html_reports"] }
rand = "0.9.1"
rand_chacha = "0.9.0"
rand_core = "0.9.3"
rln = { path = "../rln", default-features = false }
[features]
default = []

View File

@@ -2,7 +2,14 @@ use ark_bn254::Fr;
use criterion::{
black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion, Throughput,
};
use zerokit_utils::Poseidon;
use light_poseidon::{
PoseidonHasher as LPoseidonHasher, PoseidonParameters as LPoseidonParameters,
};
use rand::RngCore;
use rand_chacha::ChaCha8Rng;
use rand_core::SeedableRng;
use rln::utils::bytes_le_to_fr;
use zerokit_utils::{Poseidon, RoundParameters};
const ROUND_PARAMS: [(usize, usize, usize, usize); 8] = [
(2, 8, 56, 0),
@@ -15,42 +22,81 @@ const ROUND_PARAMS: [(usize, usize, usize, usize); 8] = [
(9, 8, 63, 0),
];
struct U256Stream {
rng: ChaCha8Rng,
}
impl U256Stream {
fn seeded_stream(seed: u64) -> Self {
let rng = ChaCha8Rng::seed_from_u64(seed);
Self { rng }
}
}
impl Iterator for U256Stream {
type Item = [u8; 32];
fn next(&mut self) -> Option<Self::Item> {
let mut res = [0; 32];
self.rng.fill_bytes(&mut res);
Some(res)
}
}
pub fn poseidon_benchmark(c: &mut Criterion) {
let hasher = Poseidon::<Fr>::from(&ROUND_PARAMS);
let mut group = c.benchmark_group("poseidon Fr");
for size in [10u32, 100, 1000].iter() {
// group.measurement_time(std::time::Duration::from_secs(30));
for size in [1u32, 2].iter() {
group.throughput(Throughput::Elements(*size as u64));
let vals = U256Stream::seeded_stream(*size as u64)
.take(*size as usize)
.map(|b| bytes_le_to_fr(&b).0)
.collect::<Vec<_>>();
let RoundParameters {
t,
n_rounds_full,
n_rounds_partial,
skip_matrices: _,
ark_consts,
mds,
} = hasher.select_params(&vals).unwrap();
group.bench_with_input(BenchmarkId::new("Array hash", size), size, |b, &size| {
group.bench_function(BenchmarkId::new("Array hash light", size), |b| {
b.iter_batched(
// Setup: create values for each benchmark iteration
// setup
|| {
let mut values = Vec::with_capacity(size as usize);
for i in 0..size {
values.push([Fr::from(i)]);
}
values
// this needs to be done here due to move/copy/etc issues.
let l_params = LPoseidonParameters {
ark: ark_consts.clone(),
mds: mds.clone(),
full_rounds: *n_rounds_full,
partial_rounds: *n_rounds_partial,
width: *t,
alpha: 5,
};
light_poseidon::Poseidon::<Fr>::new(l_params)
},
// Actual benchmark
|values| {
for v in values.iter() {
let _ = hasher.hash(black_box(&v[..]));
}
},
|mut light_hasher| black_box(light_hasher.hash(&vals)),
BatchSize::SmallInput,
)
});
group.bench_function(BenchmarkId::new("Array hash ift", size), |b| {
b.iter(|| black_box(hasher.hash(&vals)))
});
group.bench_function(BenchmarkId::new("Array hash light_circom", size), |b| {
b.iter_batched(
// setup
|| light_poseidon::Poseidon::<Fr>::new_circom(*size as usize).unwrap(),
// Actual benchmark
|mut light_hasher_circom| black_box(light_hasher_circom.hash(&vals)),
BatchSize::SmallInput,
)
});
}
// Benchmark single hash operation separately
group.bench_function("Single hash", |b| {
let input = [Fr::from(u64::MAX)];
b.iter(|| {
let _ = hasher.hash(black_box(&input[..]));
})
});
group.finish();
}

View File

@@ -225,37 +225,43 @@ pub fn find_poseidon_ark_and_mds<F: PrimeField>(
partial_rounds,
);
let mut ark = Vec::<F>::with_capacity((full_rounds + partial_rounds) as usize);
for _ in 0..(full_rounds + partial_rounds) {
let values = lfsr.get_field_elements_rejection_sampling::<F>(rate);
for el in values {
ark.push(el);
let ark = {
let mut res = Vec::<F>::with_capacity((full_rounds + partial_rounds) as usize);
for _ in 0..(full_rounds + partial_rounds) {
let values = lfsr.get_field_elements_rejection_sampling::<F>(rate);
for el in values {
res.push(el);
}
}
}
res
};
let mut mds = Vec::<Vec<F>>::with_capacity(rate);
mds.resize(rate, vec![F::zero(); rate]);
let mds = {
let mut res = Vec::<Vec<F>>::with_capacity(rate);
res.resize(rate, vec![F::zero(); rate]);
// Note that we build the MDS matrix generating 2*rate elements. If the matrix built is not secure (see checks with algorithm 1, 2, 3 in reference implementation)
// it has to be skipped. Since here we do not implement such algorithm we allow to pass a parameter to skip generations of elements giving unsecure matrixes.
// At the moment, the skip_matrices parameter has to be generated from the reference implementation and passed to this function
for _ in 0..skip_matrices {
let _ = lfsr.get_field_elements_mod_p::<F>(2 * (rate));
}
// a qualifying matrix must satisfy the following requirements
// - there is no duplication among the elements in x or y
// - there is no i and j such that x[i] + y[j] = p
// - the resultant MDS passes all the three tests
let xs = lfsr.get_field_elements_mod_p::<F>(rate);
let ys = lfsr.get_field_elements_mod_p::<F>(rate);
for i in 0..(rate) {
for (j, ys_item) in ys.iter().enumerate().take(rate) {
mds[i][j] = (xs[i] + ys_item).inverse().unwrap();
// Note that we build the MDS matrix generating 2*rate elements. If the matrix built is not secure (see checks with algorithm 1, 2, 3 in reference implementation)
// it has to be skipped. Since here we do not implement such algorithm we allow to pass a parameter to skip generations of elements giving unsecure matrixes.
// At the moment, the skip_matrices parameter has to be generated from the reference implementation and passed to this function
for _ in 0..skip_matrices {
let _ = lfsr.get_field_elements_mod_p::<F>(2 * (rate));
}
}
// a qualifying matrix must satisfy the following requirements
// - there is no duplication among the elements in x or y
// - there is no i and j such that x[i] + y[j] = p
// - the resultant MDS passes all the three tests
let xs = lfsr.get_field_elements_mod_p::<F>(rate);
let ys = lfsr.get_field_elements_mod_p::<F>(rate);
for i in 0..(rate) {
for (j, ys_item) in ys.iter().enumerate().take(rate) {
res[i][j] = (xs[i] + ys_item).inverse().unwrap();
}
}
res
};
(ark, mds)
}

View File

@@ -8,49 +8,80 @@ use ark_ff::PrimeField;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoundParameters<F: PrimeField> {
// confirm: Is this "rate"? does this correlate with light-poseidon "width" parameter?
pub t: usize,
pub n_rounds_f: usize,
pub n_rounds_p: usize,
pub n_rounds_full: usize,
pub n_rounds_partial: usize,
pub skip_matrices: usize,
pub c: Vec<F>,
pub m: Vec<Vec<F>>,
pub ark_consts: Vec<F>,
pub mds: Vec<Vec<F>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoundParameVec<F: PrimeField> {
pub inner: Vec<RoundParameters<F>>,
}
// Dev artifact: helps grok internal params against light-poseidon approach to params
// /// Parameters for the Poseidon hash algorithm.
// pub struct PoseidonParameters<F: PrimeField> {
// /// Round constants.
// pub ark: Vec<F>,
// /// MDS matrix.
// pub mds: Vec<Vec<F>>,
// /// Number of full rounds (where S-box is applied to all elements of the
// /// state).
// pub full_rounds: usize,
// /// Number of partial rounds (where S-box is applied only to the first
// /// element of the state).
// pub partial_rounds: usize,
// /// Number of prime fields in the state.
// pub width: usize,
// /// Exponential used in S-box to power elements of the state.
// pub alpha: u64,
// }
pub struct Poseidon<F: PrimeField> {
round_params: Vec<RoundParameters<F>>,
}
impl<F: PrimeField> Poseidon<F> {
// Loads round parameters and generates round constants
// poseidon_params is a vector containing tuples (t, RF, RP, skip_matrices)
// where: t is the rate (input length + 1), RF is the number of full rounds, RP is the number of partial rounds
// and skip_matrices is a (temporary) parameter used to generate secure MDS matrices (see comments in the description of find_poseidon_ark_and_mds)
// TODO: implement automatic generation of round parameters
pub fn from(poseidon_params: &[(usize, usize, usize, usize)]) -> Self {
impl<F: PrimeField> RoundParameVec<F> {
fn make_param_vec(poseidon_params: &[(usize, usize, usize, usize)]) -> Self {
let mut read_params = Vec::<RoundParameters<F>>::with_capacity(poseidon_params.len());
for &(t, n_rounds_f, n_rounds_p, skip_matrices) in poseidon_params {
for &(t, n_rounds_full, n_rounds_partial, skip_matrices) in poseidon_params {
let (ark, mds) = find_poseidon_ark_and_mds::<F>(
1, // is_field = 1
0, // is_sbox_inverse = 0
F::MODULUS_BIT_SIZE as u64,
t,
n_rounds_f as u64,
n_rounds_p as u64,
n_rounds_full as u64,
n_rounds_partial as u64,
skip_matrices,
);
let rp = RoundParameters {
t,
n_rounds_p,
n_rounds_f,
n_rounds_partial,
n_rounds_full,
skip_matrices,
c: ark,
m: mds,
ark_consts: ark,
mds,
};
read_params.push(rp);
}
Self { inner: read_params }
}
}
impl<F: PrimeField> Poseidon<F> {
// Loads round parameters and generates round constants
// poseidon_params is a vector containing tuples (t, n_rounds_full, n_rounds_partial, skip_matrices)
// where t is the rate (input length + 1)
// and skip_matrices is a (temporary) parameter used to generate secure MDS matrices (see comments in the description of find_poseidon_ark_and_mds)
// TODO: implement automatic generation of round parameters
pub fn from(poseidon_params: &[(usize, usize, usize, usize)]) -> Self {
let param_vec = RoundParameVec::make_param_vec(poseidon_params);
// dbg!(&param_vec.inner);
Poseidon {
round_params: read_params,
round_params: param_vec.inner,
}
}
@@ -92,38 +123,29 @@ impl<F: PrimeField> Poseidon<F> {
}
}
pub fn hash(&self, inp: &[F]) -> Result<F, String> {
pub fn select_params(&self, inp: &[F]) -> Result<&RoundParameters<F>, String> {
if inp.is_empty() {
return Err("Attempt to hash empty data input".to_string());
}
// Note that the rate t becomes input length + 1; hence for length N we pick parameters with T = N + 1
let t = inp.len() + 1;
self.round_params
.iter()
.find(|el| el.t == t)
.ok_or("No parameters found for inputs length".to_string())
}
// We seek the index (Poseidon's round_params is an ordered vector) for the parameters corresponding to t
let param_index = self.round_params.iter().position(|el| el.t == t);
pub fn hash(&self, inp: &[F]) -> Result<F, String> {
let params = self.select_params(inp)?;
let mut state = Vec::with_capacity(inp.len() + 1);
state.push(F::ZERO);
state.extend_from_slice(inp);
let mut state_2 = vec![F::ZERO; inp.len() + 1];
if inp.is_empty() || param_index.is_none() {
return Err("No parameters found for inputs length".to_string());
}
let param_index = param_index.unwrap();
let mut state = vec![F::ZERO; t];
let mut state_2 = state.clone();
state[1..].clone_from_slice(inp);
for i in 0..(self.round_params[param_index].n_rounds_f
+ self.round_params[param_index].n_rounds_p)
{
self.ark(
&mut state,
&self.round_params[param_index].c,
i * self.round_params[param_index].t,
);
self.sbox(
self.round_params[param_index].n_rounds_f,
self.round_params[param_index].n_rounds_p,
&mut state,
i,
);
self.mix_2(&state, &self.round_params[param_index].m, &mut state_2);
for i in 0..(params.n_rounds_full + params.n_rounds_partial) {
self.ark(&mut state, &params.ark_consts, i * params.t);
self.sbox(params.n_rounds_full, params.n_rounds_partial, &mut state, i);
self.mix_2(&state, &params.mds, &mut state_2);
std::mem::swap(&mut state, &mut state_2);
}
@@ -140,3 +162,41 @@ where
Self::from(&[])
}
}
// WIP artifact
#[cfg(test)]
mod test {
use ark_bn254::Fr;
use super::*;
const ROUND_PARAMS: [(usize, usize, usize, usize); 8] = [
(2, 8, 56, 0),
(3, 8, 57, 0),
(4, 8, 56, 0),
(5, 8, 60, 0),
(6, 8, 60, 0),
(7, 8, 63, 0),
(8, 8, 64, 0),
(9, 8, 63, 0),
];
// #[test]
// fn see_params() {
// let mut param_vec = RoundParameVec::<Fr>::make_param_vec(&ROUND_PARAMS);
// let stats /* (rate, fulls, partual, sm, ark_n, mds_n) */ = param_vec.inner.into_iter().map(|RoundParameters { rate, n_rounds_full, n_rounds_partial, skip_matrices, ark_consts, mds }| (rate, n_rounds_full, n_rounds_partial, skip_matrices, ark_consts.len(), mds.len())).collect::<Vec<_>>();
// println!("r f p s cl ml");
// for s in stats.iter() {
// println!("{:?}", s);
// }
// panic!();
// }
// #[test]
// fn see_data() {
// let size = 10;
// let mut param_vec = RoundParameVec::<Fr>::make_param_vec(&ROUND_PARAMS);
// let mut values = Vec::with_capacity(size as usize);
// for i in 0..size {
// values.push([Fr::from(u128::MAX - i)]);
// }
// panic!("{:?}", values);
// }
}

View File

@@ -3533,8 +3533,8 @@ mod test {
let poseidon_hasher = Poseidon::<Fr>::from(&ROUND_PARAMS);
let poseidon_parameters = poseidon_hasher.get_parameters();
for i in 0..poseidon_parameters.len() {
assert_eq!(loaded_c[i], poseidon_parameters[i].c);
assert_eq!(loaded_m[i], poseidon_parameters[i].m);
assert_eq!(loaded_c[i], poseidon_parameters[i].ark_consts);
assert_eq!(loaded_m[i], poseidon_parameters[i].mds);
}
} else {
unreachable!();