diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b3b9c8a6..62c058ca 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -25,6 +25,10 @@ jobs: profile: minimal override: true + - name: Test starknet-crypto with pedersen_no_lookup + run: | + cargo test -p starknet-crypto --features pedersen_no_lookup + - name: Run cargo tests uses: nick-fields/retry@v2 with: @@ -53,6 +57,10 @@ jobs: profile: minimal override: true + - name: Test starknet-crypto with pedersen_no_lookup + run: | + cargo test -p starknet-crypto --features pedersen_no_lookup + - name: Run cargo tests uses: nick-fields/retry@v2 with: diff --git a/starknet-crypto/Cargo.toml b/starknet-crypto/Cargo.toml index b0989925..23c46487 100644 --- a/starknet-crypto/Cargo.toml +++ b/starknet-crypto/Cargo.toml @@ -31,6 +31,7 @@ default = ["std", "signature-display"] std = ["starknet-types-core/std"] alloc = ["hex?/alloc", "starknet-types-core/alloc"] signature-display = ["dep:hex", "alloc"] +pedersen_no_lookup = [] [dev-dependencies] criterion = { version = "0.4.0", default-features = false } diff --git a/starknet-crypto/README.md b/starknet-crypto/README.md index 3af6bc0e..32e92fca 100644 --- a/starknet-crypto/README.md +++ b/starknet-crypto/README.md @@ -65,6 +65,14 @@ poseidon_hash_many time: [41.878 µs 41.911 µs 41.945 µs] rfc6979_generate_k time: [11.564 µs 11.566 µs 11.569 µs] ``` +## Binary size optimization + +By default, `starknet-crypto` ships with a Pedersen hash implementation utilizing a lookup table for better performance. To optimize for binary size over performance, the crate offers a `pedersen_no_lookup` feature, which uses a vanilla unoptimized implementation instead. + +> [!WARNING] +> +> Enabling the `pedersen_no_lookup` feature significantly slows down hashing performance by approximately a factor of `10`. Make sure you understand the impact on your use case before turning it on. + ## Credits Most of the code in this crate for the Pedersen hash implementation was inspired and modified from the awesome [`pathfinder` from Equilibrium](https://github.com/eqlabs/pathfinder/blob/b091cb889e624897dbb0cbec3c1df9a9e411eb1e/crates/pedersen/src/lib.rs). diff --git a/starknet-crypto/src/pedersen_hash/default.rs b/starknet-crypto/src/pedersen_hash/default.rs new file mode 100644 index 00000000..21279a76 --- /dev/null +++ b/starknet-crypto/src/pedersen_hash/default.rs @@ -0,0 +1,14 @@ +use starknet_types_core::{ + felt::Felt, + hash::{Pedersen, StarkHash}, +}; + +/// Computes the Starkware version of the Pedersen hash of x and y. All inputs are little-endian. +/// +/// ### Parameters +/// +/// - `x`: The x coordinate. +/// - `y`: The y coordinate. +pub fn pedersen_hash(x: &Felt, y: &Felt) -> Felt { + Pedersen::hash(x, y) +} diff --git a/starknet-crypto/src/pedersen_hash.rs b/starknet-crypto/src/pedersen_hash/mod.rs similarity index 80% rename from starknet-crypto/src/pedersen_hash.rs rename to starknet-crypto/src/pedersen_hash/mod.rs index 275b5d81..c6d01a59 100644 --- a/starknet-crypto/src/pedersen_hash.rs +++ b/starknet-crypto/src/pedersen_hash/mod.rs @@ -1,17 +1,12 @@ -use starknet_types_core::{ - felt::Felt, - hash::{Pedersen, StarkHash}, -}; +#[cfg(not(feature = "pedersen_no_lookup"))] +mod default; +#[cfg(not(feature = "pedersen_no_lookup"))] +pub use default::pedersen_hash; -/// Computes the Starkware version of the Pedersen hash of x and y. All inputs are little-endian. -/// -/// ### Parameters -/// -/// - `x`: The x coordinate. -/// - `y`: The y coordinate. -pub fn pedersen_hash(x: &Felt, y: &Felt) -> Felt { - Pedersen::hash(x, y) -} +#[cfg(feature = "pedersen_no_lookup")] +mod no_lookup; +#[cfg(feature = "pedersen_no_lookup")] +pub use no_lookup::pedersen_hash; #[cfg(test)] mod tests { diff --git a/starknet-crypto/src/pedersen_hash/no_lookup.rs b/starknet-crypto/src/pedersen_hash/no_lookup.rs new file mode 100644 index 00000000..b999a451 --- /dev/null +++ b/starknet-crypto/src/pedersen_hash/no_lookup.rs @@ -0,0 +1,97 @@ +// Size-optimized implementation ported from: +// https://github.com/andrewmilson/sandstorm/blob/9e256c4933aa2d89f794b3ed7c293b32984fe1ce/builtins/src/pedersen/mod.rs#L24-L50 + +use starknet_curve::curve_params::SHIFT_POINT; +use starknet_types_core::{curve::ProjectivePoint, felt::Felt}; + +/// Computes the Starkware version of the Pedersen hash of x and y. All inputs are little-endian. +/// +/// ### Parameters +/// +/// - `x`: The x coordinate. +/// - `y`: The y coordinate. +pub fn pedersen_hash(x: &Felt, y: &Felt) -> Felt { + // Temporarily defining the projective points inline, as `ProjectivePoint::new()` is incorrectly + // not `const`. + // TODO: turn these into consts once upstream is fixed. + let p0_projective: ProjectivePoint = ProjectivePoint::new( + Felt::from_raw([ + 241691544791834578, + 518715844721862878, + 13758484295849329960, + 3602345268353203007, + ]), + Felt::from_raw([ + 368891789801938570, + 433857700841878496, + 13001553326386915570, + 13441546676070136227, + ]), + Felt::ONE, + ); + let p1_projective: ProjectivePoint = ProjectivePoint::new( + Felt::from_raw([ + 253000153565733272, + 10043949394709899044, + 12382025591154462459, + 16491878934996302286, + ]), + Felt::from_raw([ + 285630633187035523, + 5191292837124484988, + 2545498000137298346, + 13950428914333633429, + ]), + Felt::ONE, + ); + let p2_projective: ProjectivePoint = ProjectivePoint::new( + Felt::from_raw([ + 338510149841406402, + 12916675983929588442, + 18195981508842736832, + 1203723169299412240, + ]), + Felt::from_raw([ + 161068411212710156, + 11088962269971685343, + 11743524503750604092, + 12352616181161700245, + ]), + Felt::ONE, + ); + let p3_projective: ProjectivePoint = ProjectivePoint::new( + Felt::from_raw([ + 425493972656615276, + 299781701614706065, + 10664803185694787051, + 1145636535101238356, + ]), + Felt::from_raw([ + 345457391846365716, + 6033691581221864148, + 4428713245976508844, + 8187986478389849302, + ]), + Felt::ONE, + ); + + let processed_x = process_element(x, &p0_projective, &p1_projective); + let processed_y = process_element(y, &p2_projective, &p3_projective); + + // Unwrapping is safe as this never fails + (processed_x + processed_y + SHIFT_POINT) + .to_affine() + .unwrap() + .x() +} + +#[inline(always)] +fn process_element(x: &Felt, p1: &ProjectivePoint, p2: &ProjectivePoint) -> ProjectivePoint { + let x = x.to_biguint(); + let shift = 252 - 4; + let high_part = &x >> shift; + let low_part = x - (&high_part << shift); + let x_high = Felt::from(high_part); + let x_low = Felt::from(low_part); + p1 * x_low + p2 * x_high +}