diff --git a/Cargo.lock b/Cargo.lock index 50a2c33..562d0e9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,7 +173,7 @@ dependencies = [ "clap 4.5.48", "criterion-plot", "is-terminal", - "itertools 0.10.5", + "itertools", "num-traits", "once_cell", "oorandom", @@ -194,7 +194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" dependencies = [ "cast", - "itertools 0.10.5", + "itertools", ] [[package]] @@ -259,8 +259,8 @@ checksum = "b5d065e1eac4fe9b4313fe81f692c7f589ce7b431719296a3392b85af4b73d38" dependencies = [ "clap 2.34.0", "ctrlc", - "rand 0.8.5", - "rand_chacha 0.3.1", + "rand", + "rand_chacha", ] [[package]] @@ -331,15 +331,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.15" @@ -386,25 +377,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -441,134 +413,6 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" -[[package]] -name = "p3-dft" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b2764a3982d22d62aa933c8de6f9d71d8a474c9110b69e675dea1887bdeffc" -dependencies = [ - "itertools 0.14.0", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", -] - -[[package]] -name = "p3-field" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc13a73509fe09c67b339951ca8d4cc6e61c9bf08c130dbc90dda52452918cc2" -dependencies = [ - "itertools 0.14.0", - "num-bigint", - "p3-maybe-rayon", - "p3-util", - "paste", - "rand 0.9.2", - "serde", - "tracing", -] - -[[package]] -name = "p3-goldilocks" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "552849f6309ffde34af0d31aa9a2d0a549cb0ec138d9792bfbf4a17800742362" -dependencies = [ - "num-bigint", - "p3-dft", - "p3-field", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "paste", - "rand 0.9.2", - "serde", -] - -[[package]] -name = "p3-matrix" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e1e9f69c2fe15768b3ceb2915edb88c47398aa22c485d8163deab2a47fe194" -dependencies = [ - "itertools 0.14.0", - "p3-field", - "p3-maybe-rayon", - "p3-util", - "rand 0.9.2", - "serde", - "tracing", - "transpose", -] - -[[package]] -name = "p3-maybe-rayon" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33f765046b763d046728b3246b690f81dfa7ccd7523b7a1582c74f616fbce6a0" - -[[package]] -name = "p3-mds" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c90541c6056712daf2ee69ec328db8b5605ae8dbafe60226c8eb75eaac0e1f9" -dependencies = [ - "p3-dft", - "p3-field", - "p3-symmetric", - "p3-util", - "rand 0.9.2", -] - -[[package]] -name = "p3-poseidon2" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88e9f053f120a78ad27e9c1991a0ea547777328ca24025c42364d6ee2667d59a" -dependencies = [ - "p3-field", - "p3-mds", - "p3-symmetric", - "p3-util", - "rand 0.9.2", -] - -[[package]] -name = "p3-symmetric" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d5db8f05a26d706dfd8aaf7aa4272ca4f3e7a075db897ec7108f24fad78759" -dependencies = [ - "itertools 0.14.0", - "p3-field", - "serde", -] - -[[package]] -name = "p3-util" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dfee67245d9ce78a15176728da2280032f0a84b5819a39a953e7ec03cfd9bd7" -dependencies = [ - "serde", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - [[package]] name = "plotters" version = "0.3.7" @@ -615,19 +459,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "qp-poseidon-constants" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300d8b01e4a492b202e739ab5f73215b261905911020cc0f74a22109365e3bd7" -dependencies = [ - "p3-field", - "p3-goldilocks", - "p3-poseidon2", - "rand 0.9.2", - "rand_chacha 0.9.0", -] - [[package]] name = "qp-poseidon-core" version = "2.0.2" @@ -635,11 +466,6 @@ dependencies = [ "criterion", "dudect-bencher", "hex", - "p3-field", - "p3-goldilocks", - "p3-poseidon2", - "p3-symmetric", - "qp-poseidon-constants", ] [[package]] @@ -658,17 +484,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_core 0.9.3", + "rand_chacha", + "rand_core", ] [[package]] @@ -678,17 +495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.3", + "rand_core", ] [[package]] @@ -700,12 +507,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "rand_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" - [[package]] name = "rayon" version = "1.11.0" @@ -813,12 +614,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strsim" version = "0.8.0" @@ -855,44 +650,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - [[package]] name = "unicode-ident" version = "1.0.19" diff --git a/Cargo.toml b/Cargo.toml index 48b165f..55018be 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,35 @@ -[workspace] -resolver = "2" -members = ["core"] - -[workspace.package] +[package] +name = "qp-poseidon-core" version = "2.0.2" authors = ["Quantus Network Developers "] homepage = "https://quantus.com" repository = "https://github.com/Quantus-Network/qp-poseidon" license = "MIT-0" edition = "2021" +description = "Self-contained Poseidon2 hash core over Goldilocks" +keywords = [ + "cryptography", + "hash-functions", + "no-std", + "poseidon2", + "quantus-network", +] +categories = ["cryptography", "no-std"] -[workspace.dependencies] -criterion = { version = "0.5", features = ["html_reports"] } +[dependencies] + +[dev-dependencies] hex = "0.4.3" -p3-field = { version = "0.3.0", default-features = false } -p3-goldilocks = { version = "0.3.0", default-features = false } -p3-poseidon2 = { version = "0.3.0", default-features = false } -p3-symmetric = { version = "0.3.0", default-features = false } -qp-poseidon-constants = { version = "1.1.0", default-features = false } +criterion = { version = "0.5", features = ["html_reports"] } +dudect-bencher = "0.6" + +[features] +default = [] +std = [] + +[[bench]] +name = "poseidon_benchmarks" +harness = false + +[[example]] +name = "ct_bench" diff --git a/core/benches/poseidon_benchmarks.rs b/benches/poseidon_benchmarks.rs similarity index 94% rename from core/benches/poseidon_benchmarks.rs rename to benches/poseidon_benchmarks.rs index 83eb776..904cc47 100644 --- a/core/benches/poseidon_benchmarks.rs +++ b/benches/poseidon_benchmarks.rs @@ -1,9 +1,7 @@ use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; -use p3_field::integers::QuotientMap; -use p3_goldilocks::Goldilocks; -use qp_poseidon_constants::create_poseidon; use qp_poseidon_core::{ - hash_bytes, hash_squeeze_twice, hash_to_bytes, serialization::bytes_to_felts, + hash_bytes, hash_squeeze_twice, hash_to_bytes, serialization::bytes_to_felts, Goldilocks, + Poseidon2, }; /// Generate test data of varying sizes for benchmarking @@ -14,7 +12,7 @@ fn generate_test_data(size: usize) -> Vec { /// Generate test field elements for benchmarking fn generate_test_felts(count: usize) -> Vec { (0..count) - .map(|i| Goldilocks::from_int((i * 456) as u64 % (1u64 << 32))) + .map(|i| Goldilocks::from_u64((i * 456) as u64 % (1u64 << 32))) .collect() } @@ -106,7 +104,7 @@ fn bench_initialization(c: &mut Criterion) { group.bench_function("create_poseidon", |b| { b.iter(|| { - let hasher = create_poseidon(); + let hasher = Poseidon2::new(); black_box(hasher) }) }); diff --git a/core/Cargo.toml b/core/Cargo.toml deleted file mode 100644 index 22e42d8..0000000 --- a/core/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "qp-poseidon-core" -version.workspace = true -authors.workspace = true -homepage.workspace = true -repository.workspace = true -license.workspace = true -edition.workspace = true -description = "Poseidon2 hash core over Goldilocks with a Plonky2/Plonky3-agnostic compatibility layer" -keywords = [ - "cryptography", - "hash-functions", - "no-std", - "poseidon2", - "quantus-network", -] -categories = ["cryptography", "no-std"] - -[dependencies] -qp-poseidon-constants.workspace = true - -# Plonky3 stack -p3-goldilocks.workspace = true -p3-poseidon2.workspace = true -p3-field.workspace = true -p3-symmetric.workspace = true - -# Constant-time testing (optional for binaries) -dudect-bencher = { version = "0.6", optional = true } - -[dev-dependencies] -hex.workspace = true -criterion.workspace = true - -[features] -default = [] -std = [] -ct-testing = ["dudect-bencher", "std"] - -[[bin]] -name = "ct_bench" -required-features = ["ct-testing"] - - -[[bench]] -name = "poseidon_benchmarks" -harness = false diff --git a/core/README.md b/core/README.md deleted file mode 100644 index 791e00f..0000000 --- a/core/README.md +++ /dev/null @@ -1,91 +0,0 @@ -# qp-poseidon-core - -Pure Rust implementation of Poseidon2 hashing over the Goldilocks field (p = 2^64 - 2^32 + 1). - -## Features - -- **No-std compatible**: Works in embedded, WASM, and constrained environments -- **Circuit-compatible**: Encoding matches ZK circuit implementations -- **Collision-resistant**: Injective encoding prevents length-extension and padding attacks -- **Goldilocks field**: Built on plonky3's optimized 64-bit prime field arithmetic - -## Usage - -### Hash Arbitrary Bytes - -```rust -use qp_poseidon_core::hash_bytes; - -// Injective encoding - safe for any input length -let hash: [u8; 32] = hash_bytes(b"hello world"); -``` - -### Hash Field Elements - -```rust -use qp_poseidon_core::{hash_to_bytes, hash_to_felts}; -use qp_poseidon_core::serialization::bytes_to_felts; - -let felts = bytes_to_felts(b"data"); - -// Get 32-byte hash -let hash: [u8; 32] = hash_to_bytes(&felts); - -// Get 4 field elements (for chaining) -let hash_felts = hash_to_felts(&felts); -``` - -### Double Hashing (Wormhole Addresses) - -```rust -use qp_poseidon_core::{hash_twice, rehash_to_bytes}; -use qp_poseidon_core::serialization::bytes_to_felts; - -let felts = bytes_to_felts(b"secret"); - -// hash(hash(input)) - used for wormhole address derivation -let address: [u8; 32] = hash_twice(&felts); - -// Re-hash an existing 32-byte digest -let first_hash = hash_to_bytes(&felts); -let second_hash: [u8; 32] = rehash_to_bytes(&first_hash); -``` - -### Mining (64-byte Output) - -```rust -use qp_poseidon_core::hash_squeeze_twice; - -// Two sponge squeezes for 64-byte output -let hash_512: [u8; 64] = hash_squeeze_twice(b"block data"); -``` - -## Encoding Modes - -### Injective Encoding (Default) - -Used by `hash_bytes` and `bytes_to_felts`. Safe for variable-length inputs. - -- 4 bytes per field element -- Terminator byte (0x01) marks end of input -- Guarantees different inputs produce different field element sequences - -### Compact Encoding - -Used by `bytes_to_felts_compact`. Only safe for fixed-size inputs. - -- 8 bytes per field element (full capacity) -- No length marker - trailing zeros collide -- Use only when input size is enforced externally (e.g., ZK circuits) - -## Security Notes - -1. **Always use `hash_bytes` for arbitrary input** - it's collision-resistant -2. **Compact encoding is for circuits only** - where input size is constrained by the circuit -3. **No padding functions** - legacy padded hashing was removed due to audit findings -4. **Timing resistance** - core hashing has no input-dependent branches - -## Related Crates - -- [`qp-poseidon-constants`](https://crates.io/crates/qp-poseidon-constants) - Round constants -- [`plonky3`](https://github.com/0xPolygonZero/plonky3) - Field arithmetic diff --git a/core/bench_config.toml b/core/bench_config.toml deleted file mode 100644 index ab416e7..0000000 --- a/core/bench_config.toml +++ /dev/null @@ -1,27 +0,0 @@ -[criterion] -# Enable HTML reports for better visualization -html_reports_enabled = true -# Set confidence level for statistical analysis -confidence_level = 0.95 -# Set significance level for detecting changes -significance_level = 0.05 -# Number of samples to collect -sample_size = 100 -# Warm up time before measurements -warm_up_time = "3s" -# Measurement time for each benchmark -measurement_time = "5s" -# Number of resamples for bootstrap -nresamples = 100000 - -[measurement] -# Use wall-clock time for measurements -measurement_overhead_correction = true -# Filter outliers -outlier_detection = "auto" - -[output] -# Generate plots and detailed reports -verbose_output = true -# Save raw data for further analysis -save_baseline = "benchmark_baseline" diff --git a/core/src/bin/ct_bench.rs b/examples/ct_bench.rs similarity index 84% rename from core/src/bin/ct_bench.rs rename to examples/ct_bench.rs index e4b78a5..c4b2f0c 100644 --- a/core/src/bin/ct_bench.rs +++ b/examples/ct_bench.rs @@ -8,22 +8,11 @@ //! - Class B: Random data //! //! This ensures the two classes are distinguishable before timing analysis begins. -//! -//! In `cfg(test)` builds the `dudect_bencher::ctbench_main!` macro is gated out -//! (it is `cfg(all(not(test), feature = "dudect-bencher"))`), so every helper -//! would be reported as dead code under `cargo clippy --all-targets`. The -//! binary is only meaningful in non-test mode anyway, so skip the file under -//! `cfg(test)`. - -#![cfg(not(test))] - -#[cfg(feature = "dudect-bencher")] -use dudect_bencher::rand::{Rng, RngCore}; -#[cfg(feature = "dudect-bencher")] -use dudect_bencher::{BenchRng, Class, CtRunner}; -use p3_field::{integers::QuotientMap, PrimeCharacteristicRing, PrimeField64}; -use p3_goldilocks::Goldilocks; -use p3_symmetric::Permutation; + +use dudect_bencher::{ + rand::{Rng, RngCore}, + BenchRng, Class, CtRunner, +}; use qp_poseidon_core::{serialization::bytes_to_felts, *}; // Test sizes in bytes @@ -54,8 +43,7 @@ fn generate_random_byte_input(size: usize, rng: &mut BenchRng) -> Vec { /// Generate a fixed field element input for Left class (same for all samples) fn generate_fixed_felt_input(count: usize, rng: &mut BenchRng) -> Vec { // Always use ZERO for the fixed input - let val = rng.next_u64() % Goldilocks::ORDER_U64; - let felt = Goldilocks::from_int(val); + let felt = Goldilocks::from_u64(rng.next_u64()); vec![felt; count] } @@ -63,8 +51,7 @@ fn generate_fixed_felt_input(count: usize, rng: &mut BenchRng) -> Vec Vec { let mut random_input = Vec::with_capacity(count); for _ in 0..count { - let val = rng.next_u64() % Goldilocks::ORDER_U64; - random_input.push(Goldilocks::from_int(val)); + random_input.push(Goldilocks::from_u64(rng.next_u64())); } random_input } @@ -81,8 +68,6 @@ fn disrupt_cache(rng: &mut BenchRng) { } // Random access pattern to disrupt prefetcher - #[cfg(feature = "dudect-bencher")] - use dudect_bencher::rand::Rng; for _ in 0..100 { let idx = rng.gen_range(0..dummy.len()); sum = sum.wrapping_add(dummy[idx] as u64); @@ -99,7 +84,6 @@ fn disrupt_cache(rng: &mut BenchRng) { } /// Test hash_bytes with small inputs (32 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_bytes_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(SMALL_INPUT_SIZE, rng); @@ -121,7 +105,6 @@ fn test_hash_bytes_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_bytes with medium inputs (256 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_bytes_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(MEDIUM_INPUT_SIZE, rng); @@ -143,7 +126,6 @@ fn test_hash_bytes_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_bytes with large inputs (1KB) -#[cfg(feature = "dudect-bencher")] fn test_hash_bytes_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(LARGE_INPUT_SIZE, rng); @@ -165,7 +147,6 @@ fn test_hash_bytes_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_bytes with extra large inputs (4KB) -#[cfg(feature = "dudect-bencher")] fn test_hash_bytes_xlarge_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(EXTRA_LARGE_INPUT_SIZE, rng); @@ -187,7 +168,6 @@ fn test_hash_bytes_xlarge_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_variable_length_bytes with small inputs (32 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_bytes_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(SMALL_INPUT_SIZE, rng); @@ -209,7 +189,6 @@ fn test_hash_variable_length_bytes_small_ct(runner: &mut CtRunner, rng: &mut Ben } /// Test hash_variable_length_bytes with medium inputs (256 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_bytes_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(MEDIUM_INPUT_SIZE, rng); @@ -231,7 +210,6 @@ fn test_hash_variable_length_bytes_medium_ct(runner: &mut CtRunner, rng: &mut Be } /// Test hash_variable_length_bytes with large inputs (1KB) -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_bytes_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(LARGE_INPUT_SIZE, rng); @@ -253,23 +231,21 @@ fn test_hash_variable_length_bytes_large_ct(runner: &mut CtRunner, rng: &mut Ben } /// Test the core Poseidon2 permutation with fixed state patterns -#[cfg(feature = "dudect-bencher")] fn test_poseidon2_permutation_ct(runner: &mut CtRunner, rng: &mut BenchRng) { - let poseidon = qp_poseidon_constants::create_poseidon(); + let poseidon = Poseidon2::new(); // Generate the fixed state once for all Left class samples - let fixed_value = Goldilocks::from_int(rng.next_u64() % Goldilocks::ORDER_U64); - let fixed_state = [fixed_value; 12]; + let fixed_value = Goldilocks::from_u64(rng.next_u64()); + let fixed_state = [fixed_value; SPONGE_WIDTH]; for _ in 0..10_000 { let class = if rng.gen::() { Class::Left } else { Class::Right }; let state = match class { Class::Left => fixed_state, Class::Right => { - let mut random_state = [Goldilocks::ZERO; 12]; + let mut random_state = [Goldilocks::ZERO; SPONGE_WIDTH]; for slot in &mut random_state { - let val = rng.next_u64() % Goldilocks::ORDER_U64; - *slot = Goldilocks::from_int(val); + *slot = Goldilocks::from_u64(rng.next_u64()); } random_state }, @@ -286,7 +262,6 @@ fn test_poseidon2_permutation_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_squeeze_twice with small inputs (32 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_squeeze_twice_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(SMALL_INPUT_SIZE, rng); @@ -308,7 +283,6 @@ fn test_hash_squeeze_twice_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_squeeze_twice with medium inputs (256 bytes) -#[cfg(feature = "dudect-bencher")] fn test_hash_squeeze_twice_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(MEDIUM_INPUT_SIZE, rng); @@ -330,7 +304,6 @@ fn test_hash_squeeze_twice_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) } /// Test hash_squeeze_twice with large inputs (1KB) -#[cfg(feature = "dudect-bencher")] fn test_hash_squeeze_twice_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(LARGE_INPUT_SIZE, rng); @@ -352,7 +325,6 @@ fn test_hash_squeeze_twice_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test field element absorption with small inputs -#[cfg(feature = "dudect-bencher")] fn test_field_absorption_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(SMALL_INPUT_SIZE, rng); @@ -374,7 +346,6 @@ fn test_field_absorption_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test field element absorption with medium inputs -#[cfg(feature = "dudect-bencher")] fn test_field_absorption_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(MEDIUM_INPUT_SIZE, rng); @@ -396,7 +367,6 @@ fn test_field_absorption_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test field element absorption with large inputs -#[cfg(feature = "dudect-bencher")] fn test_field_absorption_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(LARGE_INPUT_SIZE, rng); @@ -418,7 +388,6 @@ fn test_field_absorption_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test double hashing with small field element inputs -#[cfg(feature = "dudect-bencher")] fn test_double_hash_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(SMALL_FELT_COUNT, rng); @@ -440,7 +409,6 @@ fn test_double_hash_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test double hashing with medium field element inputs -#[cfg(feature = "dudect-bencher")] fn test_double_hash_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(MEDIUM_FELT_COUNT, rng); @@ -462,7 +430,6 @@ fn test_double_hash_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test hash_variable_length with small field element inputs -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_felts_small_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(SMALL_FELT_COUNT, rng); @@ -484,7 +451,6 @@ fn test_hash_variable_length_felts_small_ct(runner: &mut CtRunner, rng: &mut Ben } /// Test hash_variable_length with medium field element inputs -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_felts_medium_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(MEDIUM_FELT_COUNT, rng); @@ -506,7 +472,6 @@ fn test_hash_variable_length_felts_medium_ct(runner: &mut CtRunner, rng: &mut Be } /// Test hash_variable_length with large field element inputs -#[cfg(feature = "dudect-bencher")] fn test_hash_variable_length_felts_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(LARGE_FELT_COUNT, rng); @@ -528,7 +493,6 @@ fn test_hash_variable_length_felts_large_ct(runner: &mut CtRunner, rng: &mut Ben } /// Test double hashing with large field element inputs -#[cfg(feature = "dudect-bencher")] fn test_double_hash_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_felts = generate_fixed_felt_input(LARGE_FELT_COUNT, rng); @@ -550,7 +514,6 @@ fn test_double_hash_large_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Test single byte edge cases with fixed vs random patterns -#[cfg(feature = "dudect-bencher")] fn test_single_byte_edge_cases_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_byte = rng.gen::(); @@ -576,7 +539,6 @@ fn test_single_byte_edge_cases_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } /// Integration test with medium-sized inputs combining multiple operations -#[cfg(feature = "dudect-bencher")] fn test_integrated_operations_ct(runner: &mut CtRunner, rng: &mut BenchRng) { // Generate the fixed input once for all Left class samples let fixed_input = generate_fixed_byte_input(MEDIUM_INPUT_SIZE, rng); @@ -601,53 +563,6 @@ fn test_integrated_operations_ct(runner: &mut CtRunner, rng: &mut BenchRng) { } } -#[cfg(test)] -mod tests { - use super::*; - use dudect_bencher::rand::SeedableRng; - - #[test] - fn test_input_generation_distinguishable() { - let mut rng = BenchRng::seed_from_u64(0); - - // Test byte input generation - let fixed = generate_fixed_byte_input(32, &mut rng); - let random = generate_random_byte_input(32, &mut rng); - assert_eq!(fixed.len(), 32); - assert_eq!(random.len(), 32); - - // Fixed should be all the same value - let first_byte = fixed[0]; - assert!(fixed.iter().all(|&b| b == first_byte)); - - // Test felt input generation - let fixed_felts = generate_fixed_felt_input(8, &mut rng); - let random_felts = generate_random_felt_input(8, &mut rng); - assert_eq!(fixed_felts.len(), 8); - assert_eq!(random_felts.len(), 8); - - // Fixed should be all the same value - let first_felt = fixed_felts[0]; - assert!(fixed_felts.iter().all(|&f| f == first_felt)); - } - - #[test] - fn test_ct_functions_compile() { - // This test just ensures the hash functions compile and work - let input = vec![0u8; 32]; - let hash = hash_bytes(&input); - let _rehash = rehash_to_bytes(&hash); - let _result3 = hash_squeeze_twice(&input); - - // Test field operations - let felts = bytes_to_felts(&input); - let _result4 = hash_to_bytes(&felts); - let _result5 = hash_twice(&felts); - } -} - -// This creates a main function when compiled as a binary -#[cfg(all(not(test), feature = "dudect-bencher"))] dudect_bencher::ctbench_main!( // hash_bytes tests for different input sizes test_hash_bytes_small_ct, diff --git a/src/goldilocks.rs b/src/goldilocks.rs new file mode 100644 index 0000000..d689597 --- /dev/null +++ b/src/goldilocks.rs @@ -0,0 +1,497 @@ +//! Minimal Goldilocks field implementation for Poseidon2. +//! +//! This module provides a self-contained implementation of the Goldilocks prime field +//! (p = 2^64 - 2^32 + 1) with just enough operations for Poseidon2 hashing: +//! - Addition, subtraction, multiplication +//! - S-box computation (x^7) +//! - Conversion to/from canonical u64 + +use core::{ + fmt::{Debug, Display, Formatter}, + hash::{Hash, Hasher}, + hint::unreachable_unchecked, + iter::Sum, + ops::{Add, AddAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +// ============================================================================ +// Compiler hints for optimization +// ============================================================================ + +/// Tell the compiler that condition `p` is always true. +/// If `p` is false at runtime, behavior is undefined. +#[inline(always)] +fn assume(p: bool) { + debug_assert!(p); + if !p { + unsafe { + unreachable_unchecked(); + } + } +} + +/// Force the compiler to emit a branch instruction. +/// This helps when a branch is rarely taken but important to handle. +#[inline(always)] +fn branch_hint() { + #[cfg(any( + target_arch = "aarch64", + target_arch = "arm", + target_arch = "riscv32", + target_arch = "riscv64", + target_arch = "x86", + target_arch = "x86_64", + ))] + unsafe { + core::arch::asm!("", options(nomem, nostack, preserves_flags)); + } +} + +/// The Goldilocks prime: p = 2^64 - 2^32 + 1 +pub const P: u64 = 0xFFFF_FFFF_0000_0001; + +/// Two's complement of ORDER: 2^64 - P = 2^32 - 1 +const NEG_ORDER: u64 = P.wrapping_neg(); + +/// A field element in the Goldilocks prime field. +/// +/// Internal representation may be non-canonical (i.e., value can be >= P). +/// Use `as_canonical_u64()` to get the canonical representation. +#[derive(Copy, Clone, Default)] +#[repr(transparent)] +pub struct Goldilocks { + /// Not necessarily canonical (can be any u64). + pub(crate) value: u64, +} + +impl Goldilocks { + /// The additive identity. + pub const ZERO: Self = Self::new(0); + + /// The multiplicative identity. + pub const ONE: Self = Self::new(1); + + /// Create a new field element from a u64. + /// + /// No reduction is performed since Goldilocks uses a non-canonical internal representation. + #[inline] + pub const fn new(value: u64) -> Self { + Self { value } + } + + /// Create a field element from a u64, reducing if necessary. + #[inline] + pub fn from_u64(x: u64) -> Self { + Self::new(x) + } + + /// Convert to canonical u64 representation (0 <= result < P). + #[inline] + pub fn as_canonical_u64(&self) -> u64 { + let mut c = self.value; + // We only need one conditional subtraction, since 2 * P would not fit in a u64. + if c >= P { + c -= P; + } + c + } + + /// Check if this element is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.value == 0 || self.value == P + } + + /// Compute x/2 in the field (branchless). + #[inline] + pub fn halve(&self) -> Self { + // Branchless halving: x/2 = (x >> 1) + ((x & 1) * (p+1)/2). + // When x is odd, add (p+1)/2 to compensate for the lost bit. + const HALF_P_PLUS_1: u64 = (P + 1) >> 1; // 0x7FFFFFFF80000001 + let lo_bit = self.value & 1; + let half = self.value >> 1; + let mask = 0u64.wrapping_sub(lo_bit); // all-ones when odd, zero when even + Self::new(half.wrapping_add(mask & HALF_P_PLUS_1)) + } + + /// Compute x^2 in the field. + #[inline] + pub fn square(&self) -> Self { + *self * *self + } + + /// Compute x * 2 in the field. + #[inline] + pub fn double(&self) -> Self { + *self + *self + } + + /// Compute x^7 (the S-box for Poseidon2 over Goldilocks). + #[inline] + pub fn exp7(&self) -> Self { + let x2 = self.square(); + let x3 = x2 * *self; + let x4 = x2.square(); + x3 * x4 + } +} + +// ============================================================================ +// Equality and ordering (based on canonical representation) +// ============================================================================ + +impl PartialEq for Goldilocks { + fn eq(&self, other: &Self) -> bool { + self.as_canonical_u64() == other.as_canonical_u64() + } +} + +impl Eq for Goldilocks {} + +impl Hash for Goldilocks { + fn hash(&self, state: &mut H) { + state.write_u64(self.as_canonical_u64()); + } +} + +impl Ord for Goldilocks { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.as_canonical_u64().cmp(&other.as_canonical_u64()) + } +} + +impl PartialOrd for Goldilocks { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +// ============================================================================ +// Display and Debug +// ============================================================================ + +impl Display for Goldilocks { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Display::fmt(&self.as_canonical_u64(), f) + } +} + +impl Debug for Goldilocks { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + Debug::fmt(&self.as_canonical_u64(), f) + } +} + +// ============================================================================ +// Arithmetic operations +// ============================================================================ + +impl Add for Goldilocks { + type Output = Self; + + #[inline(always)] + fn add(self, rhs: Self) -> Self { + let (sum, over) = self.value.overflowing_add(rhs.value); + let (mut sum, over) = sum.overflowing_add(u64::from(over) * NEG_ORDER); + if over { + // NB: self.value > P && rhs.value > P is necessary but not sufficient for + // double-overflow. This assume does two things: + // 1. If compiler knows that either self.value or rhs.value <= P, then it can skip this + // check. + // 2. Hints to the compiler how rare this double-overflow is (thus handled better with + // a branch). + assume(self.value > P && rhs.value > P); + branch_hint(); + sum += NEG_ORDER; // Cannot overflow. + } + Self::new(sum) + } +} + +impl AddAssign for Goldilocks { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Sub for Goldilocks { + type Output = Self; + + #[inline(always)] + fn sub(self, rhs: Self) -> Self { + let (diff, under) = self.value.overflowing_sub(rhs.value); + let (mut diff, under) = diff.overflowing_sub(u64::from(under) * NEG_ORDER); + if under { + // NB: self.value < NEG_ORDER - 1 && rhs.value > P is necessary but not sufficient for + // double-underflow. This assume does two things: + // 1. If compiler knows that either self.value >= NEG_ORDER - 1 or rhs.value <= P, then + // it can skip this check. + // 2. Hints to the compiler how rare this double-underflow is (thus handled better with + // a branch). + assume(self.value < NEG_ORDER - 1 && rhs.value > P); + branch_hint(); + diff -= NEG_ORDER; // Cannot underflow. + } + Self::new(diff) + } +} + +impl SubAssign for Goldilocks { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl Neg for Goldilocks { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + Self::new(P - self.as_canonical_u64()) + } +} + +impl Mul for Goldilocks { + type Output = Self; + + #[inline(always)] + fn mul(self, rhs: Self) -> Self { + reduce128(u128::from(self.value) * u128::from(rhs.value)) + } +} + +impl MulAssign for Goldilocks { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl Sum for Goldilocks { + fn sum>(iter: I) -> Self { + // Accumulate in u128 to avoid repeated reductions + let sum = iter.map(|x| x.value as u128).sum::(); + reduce128(sum) + } +} + +// ============================================================================ +// Reduction from u128 +// ============================================================================ + +/// Reduce a u128 to a Goldilocks field element. +/// +/// The result might not be in canonical form; it could be between P and 2^64. +#[inline(always)] +fn reduce128(x: u128) -> Goldilocks { + let (x_lo, x_hi) = split(x); + let x_hi_hi = x_hi >> 32; + let x_hi_lo = x_hi & NEG_ORDER; + + let (mut t0, borrow) = x_lo.overflowing_sub(x_hi_hi); + if borrow { + branch_hint(); // A borrow is exceedingly rare. It is faster to branch. + t0 -= NEG_ORDER; // Cannot underflow + } + let t1 = x_hi_lo * NEG_ORDER; + let t2 = unsafe { add_no_canonicalize_trashing_input(t0, t1) }; + Goldilocks::new(t2) +} + +#[inline] +#[allow(clippy::cast_possible_truncation)] +const fn split(x: u128) -> (u64, u64) { + (x as u64, (x >> 64) as u64) +} + +/// Fast addition modulo P (result may be non-canonical). +/// +/// # Safety +/// Only correct if x + y < 2^64 + P. +#[inline(always)] +#[cfg(target_arch = "x86_64")] +unsafe fn add_no_canonicalize_trashing_input(x: u64, y: u64) -> u64 { + let res_wrapped: u64; + let adjustment: u64; + unsafe { + core::arch::asm!( + "add {0}, {1}", + "sbb {1:e}, {1:e}", + inlateout(reg) x => res_wrapped, + inlateout(reg) y => adjustment, + options(pure, nomem, nostack), + ); + } + res_wrapped + adjustment +} + +/// Fast addition modulo P (result may be non-canonical). +/// +/// # Safety +/// Only correct if x + y < 2^64 + P. +#[inline(always)] +#[cfg(not(target_arch = "x86_64"))] +unsafe fn add_no_canonicalize_trashing_input(x: u64, y: u64) -> u64 { + let (res_wrapped, carry) = x.overflowing_add(y); + res_wrapped + NEG_ORDER * u64::from(carry) +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_arithmetic() { + let a = Goldilocks::new(5); + let b = Goldilocks::new(3); + + assert_eq!((a + b).as_canonical_u64(), 8); + assert_eq!((a - b).as_canonical_u64(), 2); + assert_eq!((a * b).as_canonical_u64(), 15); + } + + #[test] + fn test_large_values_arithmetic() { + // Test with large values near the field prime + let a = Goldilocks::new(P - 1); // -1 mod P + let b = Goldilocks::new(P - 2); // -2 mod P + + // (-1) + (-2) = -3 mod P = P - 3 + assert_eq!((a + b).as_canonical_u64(), P - 3); + + // (-1) - (-2) = 1 mod P + assert_eq!((a - b).as_canonical_u64(), 1); + + // (-1) * (-2) = 2 mod P + assert_eq!((a * b).as_canonical_u64(), 2); + + // Test with random-looking large values + let x = Goldilocks::new(0xc002e770975b1607); // First round constant + let y = Goldilocks::new(0xbca51a8dfe14593a); // Second round constant + + // Just verify these don't panic and produce canonical results + let sum = x + y; + let diff = x - y; + let prod = x * y; + + assert!(sum.as_canonical_u64() < P); + assert!(diff.as_canonical_u64() < P); + assert!(prod.as_canonical_u64() < P); + } + + #[test] + fn test_exp7_large_values() { + // Test S-box with large values + let x = Goldilocks::new(0xc002e770975b1607); + let y = x.exp7(); + + // Verify it's canonical + assert!(y.as_canonical_u64() < P); + + // Verify x^7 = x * x^2 * x^4 + let x2 = x * x; + let x4 = x2 * x2; + let x7_manual = x * x2 * x4; + assert_eq!(y.as_canonical_u64(), x7_manual.as_canonical_u64()); + } + + #[test] + fn test_against_p3_expected_values() { + // Expected values from p3 field operations + // p3: 13835875475997267463 + 13593300247443167546 = 8982431654025850688 + let a = Goldilocks::new(13835875475997267463); + let b = Goldilocks::new(13593300247443167546); + assert_eq!((a + b).as_canonical_u64(), 8982431654025850688, "add mismatch"); + + // p3: 13835875475997267463 - 13593300247443167546 = 242575228554099917 + assert_eq!((a - b).as_canonical_u64(), 242575228554099917, "sub mismatch"); + + // p3: 13835875475997267463 * 13593300247443167546 = 16746386726560462281 + assert_eq!((a * b).as_canonical_u64(), 16746386726560462281, "mul mismatch"); + + // p3: halve(13835875475997267463) = 16141309772705925892 + assert_eq!(a.halve().as_canonical_u64(), 16141309772705925892, "halve mismatch"); + + // p3: 13835875475997267463^7 = 5716687150516714629 + assert_eq!(a.exp7().as_canonical_u64(), 5716687150516714629, "exp7 mismatch"); + + // More test cases + // p3: 18446744069414584319 * 9223372034707292161 = 18446744069414584320 + let c = Goldilocks::new(18446744069414584319); // -2 mod P + let d = Goldilocks::new(9223372034707292161); // 1/2 mod P + assert_eq!((c * d).as_canonical_u64(), 18446744069414584320, "mul -2 * 1/2 mismatch"); + + // p3: halve(18446744069414584319) = 18446744069414584320 + assert_eq!(c.halve().as_canonical_u64(), 18446744069414584320, "halve -2 mismatch"); + } + + #[test] + fn test_subtraction_underflow() { + let a = Goldilocks::new(3); + let b = Goldilocks::new(5); + let result = a - b; + // 3 - 5 = -2 mod P = P - 2 + assert_eq!(result.as_canonical_u64(), P - 2); + } + + #[test] + fn test_negation() { + let a = Goldilocks::new(5); + let neg_a = -a; + assert_eq!((a + neg_a).as_canonical_u64(), 0); + } + + #[test] + fn test_exp7() { + let a = Goldilocks::new(2); + let result = a.exp7(); + assert_eq!(result.as_canonical_u64(), 128); // 2^7 = 128 + } + + #[test] + fn test_halve() { + let a = Goldilocks::new(10); + let half = a.halve(); + assert_eq!((half + half).as_canonical_u64(), 10); + + // Test odd number + let b = Goldilocks::new(11); + let half_b = b.halve(); + assert_eq!((half_b + half_b).as_canonical_u64(), 11); + } + + #[test] + fn test_canonical_reduction() { + // P should reduce to 0 + let p = Goldilocks::new(P); + assert_eq!(p.as_canonical_u64(), 0); + + // P + 1 should reduce to 1 + let p_plus_1 = Goldilocks::new(P) + Goldilocks::ONE; + assert_eq!(p_plus_1.as_canonical_u64(), 1); + } + + #[test] + fn test_multiplication_large() { + // Test multiplication that requires reduction + let a = Goldilocks::new(1 << 32); + let b = Goldilocks::new(1 << 32); + let result = a * b; + // (2^32)^2 = 2^64 = 2^32 - 1 mod P + assert_eq!(result.as_canonical_u64(), (1u64 << 32) - 1); + } + + #[test] + fn test_sum() { + let values = + [Goldilocks::new(1), Goldilocks::new(2), Goldilocks::new(3), Goldilocks::new(4)]; + let sum: Goldilocks = values.iter().copied().sum(); + assert_eq!(sum.as_canonical_u64(), 10); + } +} diff --git a/core/src/lib.rs b/src/lib.rs similarity index 96% rename from core/src/lib.rs rename to src/lib.rs index 2a59913..8257d7d 100644 --- a/core/src/lib.rs +++ b/src/lib.rs @@ -2,19 +2,18 @@ extern crate alloc; -use qp_poseidon_constants as constants; - +pub mod goldilocks; +pub mod poseidon2; pub mod serialization; +pub use goldilocks::Goldilocks; +pub use poseidon2::{Poseidon2, POSEIDON2_OUTPUT, SPONGE_CAPACITY, SPONGE_RATE, SPONGE_WIDTH}; + use crate::serialization::bytes_to_felts; -use p3_field::PrimeCharacteristicRing; -use p3_goldilocks::{Goldilocks, Poseidon2Goldilocks}; -use p3_symmetric::Permutation; -use qp_poseidon_constants::{POSEIDON2_OUTPUT, SPONGE_RATE, SPONGE_WIDTH}; // Internal state for Poseidon2 hashing struct Poseidon2State { - poseidon2: Poseidon2Goldilocks, + poseidon2: Poseidon2, state: [Goldilocks; SPONGE_WIDTH], buf: [Goldilocks; SPONGE_RATE], buf_len: usize, @@ -23,7 +22,7 @@ struct Poseidon2State { impl Poseidon2State { fn new() -> Self { Self { - poseidon2: constants::create_poseidon(), + poseidon2: Poseidon2::new(), state: [Goldilocks::ZERO; SPONGE_WIDTH], buf: [Goldilocks::ZERO; SPONGE_RATE], buf_len: 0, @@ -166,7 +165,6 @@ mod tests { use super::*; use crate::alloc::string::ToString; use alloc::{format, vec, vec::Vec}; - use p3_field::PrimeField64; #[test] fn test_empty_input() { @@ -236,8 +234,7 @@ mod tests { #[test] fn test_big_preimage() { for overflow in 1..=10 { - let preimage = - (::ORDER_U64 + overflow).to_le_bytes(); + let preimage = (goldilocks::P + overflow).to_le_bytes(); let _hash = hash_bytes(&preimage); } } @@ -507,8 +504,6 @@ mod tests { /// These ensure hash chaining operations remain stable across versions. #[test] fn test_hash_twice_vectors() { - use p3_field::PrimeCharacteristicRing; - // hash_twice test vectors (input felts -> output bytes) // Empty input let empty: Vec = vec![]; diff --git a/src/poseidon2.rs b/src/poseidon2.rs new file mode 100644 index 0000000..73de049 --- /dev/null +++ b/src/poseidon2.rs @@ -0,0 +1,680 @@ +//! Self-contained Poseidon2 permutation for Goldilocks field (WIDTH=12). +//! +//! This implements the Poseidon2 hash function without any external dependencies. +//! Based on the paper: https://eprint.iacr.org/2023/323 + +use crate::goldilocks::Goldilocks; + +// ============================================================================ +// Poseidon2 Parameters for WIDTH=12 +// ============================================================================ + +/// Width of the Poseidon2 sponge (number of field elements in state). +pub const SPONGE_WIDTH: usize = 12; + +/// Rate of the sponge construction (number of field elements absorbed per permutation). +pub const SPONGE_RATE: usize = 8; + +/// Capacity of the sponge (security parameter = WIDTH - RATE). +pub const SPONGE_CAPACITY: usize = SPONGE_WIDTH - SPONGE_RATE; + +/// Number of output field elements in a Poseidon2 hash digest. +pub const POSEIDON2_OUTPUT: usize = 4; + +/// Number of internal (partial) rounds in the Poseidon2 permutation. +pub const INTERNAL_ROUNDS: usize = 22; + +/// Number of external (full) rounds in the Poseidon2 permutation (4 initial + 4 terminal). +pub const EXTERNAL_ROUNDS: usize = 8; + +/// Half external rounds (4 initial, 4 terminal). +pub const HALF_EXTERNAL_ROUNDS: usize = EXTERNAL_ROUNDS / 2; + +// ============================================================================ +// Round Constants (generated with seed 0x3141592653589793) +// ============================================================================ + +/// Internal round constants (22 scalars). +pub const INTERNAL_CONSTANTS: [u64; INTERNAL_ROUNDS] = [ + 0x97f7798a784ad863, + 0xd1d2bf082f60d4f0, + 0x69a377a79f9ad206, + 0xa9d06906a3858e24, + 0x295275001eede5b5, + 0x5874e441117bd746, + 0x8a084bbba8ed86cc, + 0x3defd7645cde6425, + 0x3998cfe6871cc137, + 0x3e52ef8bca48314a, + 0x964a209f85dc9ecc, + 0x3fcc9ee82cc4577e, + 0x8e79b4a5d0096d6d, + 0x8492362ad2392556, + 0xee72f470262574d6, + 0x1e0e18496da2444a, + 0x0f3a74bf215eaac6, + 0x1b061b76a1c0ded3, + 0x192c42d86803d7a6, + 0xf6d49ff997ae0260, + 0x3ec372e7a0fa3786, + 0x5538cdf4f23445d3, +]; + +/// Diagonal values for the internal diffusion matrix. +/// +/// p3-goldilocks 0.3.x uses `state[i] = sum(state) + state[i] * MATRIX_DIAG[i]`. +pub const MATRIX_DIAG: [u64; SPONGE_WIDTH] = [ + 0xc3b6c08e23ba9300, + 0xd84b5de94a324fb6, + 0x0d0c371c5b35b84f, + 0x7964f570e7188037, + 0x5daf18bbd996604b, + 0x6743bc47b9595257, + 0x5528b9362c59bb70, + 0xac45e25b7127b68b, + 0xa2077d7dfbb606b5, + 0xf3faac6faee378ae, + 0x0c6388b51545e883, + 0xd27dbb6944917b60, +]; + +/// Initial external round constants (4 rounds x 12 elements). +pub const INITIAL_EXTERNAL_CONSTANTS: [[u64; SPONGE_WIDTH]; HALF_EXTERNAL_ROUNDS] = [ + [ + 0xc002e770975b1607, + 0xbca51a8dfe14593a, + 0x72938dfbe774f7f9, + 0xe4f2fe29e03234ac, + 0xd5e0ba2f541b6449, + 0xec33b868f3cc46c1, + 0x486dcb55419d475a, + 0x6c1cb2a358cc24f1, + 0xe3f30d509a1436bb, + 0xd9a64f068dca7c29, + 0xe59b3f57aabba1ae, + 0x2a3dd4505b478fdc, + ], + [ + 0xada1f8dc7676ed25, + 0x2711aa8b5509d516, + 0x4ae6acd0c9c92897, + 0x56eb3d6b5256d67a, + 0x1f7a9d55923bf51e, + 0x3600427d397a7f68, + 0xe5076df75b72c3d0, + 0xfcd59aa12c6090ad, + 0xcd895e8c68b57a9e, + 0x41df7ef9d730ae3e, + 0xee3e2b889abe977d, + 0xd29bb7edbeb9c405, + ], + [ + 0x7d5c08eef608e382, + 0x89ae889caaf0802c, + 0xb35a8e976d2af617, + 0xdb14234eafaf5173, + 0x78f04462d48b1c98, + 0x265293b0e47ce88a, + 0x999a649b69b9d32f, + 0x64b0a186698e01d3, + 0xee0b22d0dfae8bb8, + 0x4fd53e50ca04a7ee, + 0x5762bfe181f25047, + 0xf51593e2beb5e3bd, + ], + [ + 0x1e5e2b5760e32477, + 0x622462a1f9aaaeed, + 0xaa284b3ecdb222ae, + 0x63c8e72f542bf3fc, + 0x3ba588cacb43b5e0, + 0x23eda6f3c99150dd, + 0xaad3bea4baac9a5a, + 0xe9da8d699b94184a, + 0xcdb13f4cd93e024c, + 0x902cbd0956f655e3, + 0x5b4e40ffc759532f, + 0xde795c20a2357af7, + ], +]; + +/// Terminal external round constants (4 rounds x 12 elements). +pub const TERMINAL_EXTERNAL_CONSTANTS: [[u64; SPONGE_WIDTH]; HALF_EXTERNAL_ROUNDS] = [ + [ + 0x7b72c539e0ea4c6e, + 0x144573dae2ce9976, + 0x802028b68f35fc88, + 0x6d36c5022c4fe7c2, + 0xa205d0ffa9b9def3, + 0xf6e7e38b1ea6ba2f, + 0x34f7909ae5258d64, + 0xb0464d9d77b97fca, + 0x64ddb9d5de7e00a6, + 0x0ed0d75c27975d97, + 0x1cbb36f11127338b, + 0x6673e505cfd0b6ba, + ], + [ + 0x605f902830872e01, + 0x3fd5eb927e95fe4f, + 0xe81025b5a24c69cd, + 0xf7d0ce75de23f74e, + 0xf39942b6a8585089, + 0x6d808a08f7b71df6, + 0xf8806b6588f49a8b, + 0x57df2d8c2a32107a, + 0x16e7c2074d654a2d, + 0x213de241fcf33835, + 0xb0f2b8905a0976f6, + 0xd8e3cf2bbd355417, + ], + [ + 0xe498691679d9330f, + 0x763b45d2a3821b28, + 0x0908bf65eb0a1f0d, + 0x7691eb2d194b24f4, + 0x0e43551233ae13b2, + 0x93c393dbfc2fe76f, + 0x98f607485d48cdea, + 0xe3d95f30309819c0, + 0x1ef581a93eaf6acf, + 0x0b24c1b7a030fca4, + 0x624370be5670b327, + 0x5f1e28615a11e486, + ], + [ + 0xfe04051f909e042b, + 0x7257e5b147fd3803, + 0xe6ae134bb82f2e78, + 0x5711fd5cf4784511, + 0xf83a42660c08c0bc, + 0x2cd8c96d9a3ce855, + 0x7d2ffb1bb0e17271, + 0x85ae1528caea3811, + 0x52a345d5c7adb0b8, + 0x504c4c51f3faee94, + 0xbce34a649cfccaf9, + 0xe0a3389266fb6dc9, + ], +]; + +// ============================================================================ +// Poseidon2 Permutation +// ============================================================================ + +/// The Poseidon2 permutation for Goldilocks field with WIDTH=12. +#[derive(Clone, Debug)] +pub struct Poseidon2 { + internal_constants: [Goldilocks; INTERNAL_ROUNDS], + matrix_diag: [Goldilocks; SPONGE_WIDTH], + initial_external_constants: [[Goldilocks; SPONGE_WIDTH]; HALF_EXTERNAL_ROUNDS], + terminal_external_constants: [[Goldilocks; SPONGE_WIDTH]; HALF_EXTERNAL_ROUNDS], +} + +impl Default for Poseidon2 { + fn default() -> Self { + Self::new() + } +} + +impl Poseidon2 { + /// Create a new Poseidon2 instance with precomputed constants. + pub fn new() -> Self { + let internal_constants = core::array::from_fn(|i| Goldilocks::new(INTERNAL_CONSTANTS[i])); + let matrix_diag = core::array::from_fn(|i| Goldilocks::new(MATRIX_DIAG[i])); + + let initial_external_constants = core::array::from_fn(|r| { + core::array::from_fn(|i| Goldilocks::new(INITIAL_EXTERNAL_CONSTANTS[r][i])) + }); + + let terminal_external_constants = core::array::from_fn(|r| { + core::array::from_fn(|i| Goldilocks::new(TERMINAL_EXTERNAL_CONSTANTS[r][i])) + }); + + Self { + internal_constants, + matrix_diag, + initial_external_constants, + terminal_external_constants, + } + } + + /// Apply the Poseidon2 permutation to the state in-place. + #[inline] + pub fn permute_mut(&self, state: &mut [Goldilocks; SPONGE_WIDTH]) { + // Initial external layer: first apply linear layer, then rounds + external_linear_layer(state); + for round in 0..HALF_EXTERNAL_ROUNDS { + self.external_round(state, &self.initial_external_constants[round]); + } + + // Internal rounds + for round in 0..INTERNAL_ROUNDS { + self.internal_round(state, self.internal_constants[round]); + } + + // Terminal external rounds + for round in 0..HALF_EXTERNAL_ROUNDS { + self.external_round(state, &self.terminal_external_constants[round]); + } + } + + /// Apply a single external (full) round. + #[inline] + fn external_round( + &self, + state: &mut [Goldilocks; SPONGE_WIDTH], + rc: &[Goldilocks; SPONGE_WIDTH], + ) { + // Add round constants and apply S-box to all elements + for i in 0..SPONGE_WIDTH { + state[i] += rc[i]; + state[i] = state[i].exp7(); + } + // Apply external linear layer + external_linear_layer(state); + } + + /// Apply a single internal (partial) round. + #[inline] + fn internal_round(&self, state: &mut [Goldilocks; SPONGE_WIDTH], rc: Goldilocks) { + // Add round constant and apply S-box only to first element + state[0] += rc; + state[0] = state[0].exp7(); + // Apply internal linear layer + internal_linear_layer(state, &self.matrix_diag); + } +} + +// ============================================================================ +// Linear Layers +// ============================================================================ + +/// Apply the external linear layer (MDS light permutation). +/// +/// This applies M_4 to each consecutive 4 elements, then adds sums. +#[inline] +fn external_linear_layer(state: &mut [Goldilocks; SPONGE_WIDTH]) { + // Apply M_4 to each 4-element chunk + for chunk in state.chunks_exact_mut(4) { + apply_mat4(chunk.try_into().unwrap()); + } + + // Compute sums for the outer circulant + let sums: [Goldilocks; 4] = + core::array::from_fn(|k| (0..SPONGE_WIDTH).step_by(4).map(|j| state[j + k]).sum()); + + // Add sums back + for (i, elem) in state.iter_mut().enumerate() { + *elem += sums[i % 4]; + } +} + +/// Apply the 4x4 MDS matrix: +/// [ 2 3 1 1 ] +/// [ 1 2 3 1 ] +/// [ 1 1 2 3 ] +/// [ 3 1 1 2 ] +#[inline(always)] +fn apply_mat4(x: &mut [Goldilocks; 4]) { + let t01 = x[0] + x[1]; + let t23 = x[2] + x[3]; + let t0123 = t01 + t23; + let t01123 = t0123 + x[1]; + let t01233 = t0123 + x[3]; + // Order matters: overwrite x[0] and x[2] after using x[1] and x[3] + x[3] = t01233 + x[0].double(); // 3*x[0] + x[1] + x[2] + 2*x[3] + x[1] = t01123 + x[2].double(); // x[0] + 2*x[1] + 3*x[2] + x[3] + x[0] = t01123 + t01; // 2*x[0] + 3*x[1] + x[2] + x[3] + x[2] = t01233 + t23; // x[0] + x[1] + 2*x[2] + 3*x[3] +} + +/// Apply the internal linear layer for WIDTH=12. +/// +/// This computes: state[i] = sum + diag[i] * state[i]. +#[inline] +fn internal_linear_layer( + state: &mut [Goldilocks; SPONGE_WIDTH], + matrix_diag: &[Goldilocks; SPONGE_WIDTH], +) { + let sum: Goldilocks = state.iter().copied().sum(); + for i in 0..SPONGE_WIDTH { + state[i] = sum + state[i] * matrix_diag[i]; + } +} + +// ============================================================================ +// Tests +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + + // Expected outputs from p3-goldilocks + qp-poseidon-constants (seed 0x3141592653589793) + const P3_ZERO_RESULT: [u64; 12] = [ + 0xc9bc9432e1686884, + 0x03ecbab0dcdd2189, + 0x5e7ac885b3dc1215, + 0x6ac07513801d191f, + 0xca5c593fb184dcfc, + 0x414dec5f3e455287, + 0x1a17df170127ae41, + 0xe7e592bd0af9b0a5, + 0xc71a9b27edc66a4c, + 0x2728671759ac43c2, + 0xb9969c20f7f672f9, + 0xc5140b586823b92f, + ]; + + const P3_SEQ_RESULT: [u64; 12] = [ + 0x7e9574e2a3d6c48b, + 0x9d7bc16d282d2f2b, + 0x798826626d94a498, + 0x0831011bb22304c7, + 0xbdccb5931fffd16c, + 0xe98687714dacbefc, + 0xc6a1ed29dd75e027, + 0x1aec96681d15f765, + 0xc74b2c710b170a23, + 0x5fb4aff45e9c24fb, + 0x1fb3d228db0127eb, + 0xe201a7e214b16e74, + ]; + + #[test] + fn test_first_few_ops_zero_state() { + // Compare first few operations with zero state against p3 + // Expected from p3: + // After initial MDS: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + // After first RC + S-box: [5716687150516714629, ...] + // After first round MDS: [15870188304392300026, ...] + + let poseidon = Poseidon2::new(); + let mut state = [Goldilocks::ZERO; SPONGE_WIDTH]; + + // Step 1: Apply initial external linear layer + external_linear_layer(&mut state); + let expected_after_mds = [0u64; 12]; + for (i, (actual, exp)) in state.iter().zip(expected_after_mds.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "After initial MDS mismatch at index {}: got {}, expected {}", + i, + actual.as_canonical_u64(), + exp + ); + } + + // Step 2: First external round - add RC and S-box + let rc = &poseidon.initial_external_constants[0]; + for i in 0..SPONGE_WIDTH { + state[i] += rc[i]; + state[i] = state[i].exp7(); + } + let expected_after_sbox = [ + 5716687150516714629u64, + 12606094110489894541, + 7176561780833472673, + 2787933693728327558, + 2372134002172061930, + 5020691920936401247, + 15687823912570289724, + 15071980520517301340, + 751173130886275522, + 17086029608447879298, + 4895459708442877360, + 10576769927437500691, + ]; + for (i, (actual, exp)) in state.iter().zip(expected_after_sbox.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "After first RC + S-box mismatch at index {}: got {}, expected {}", + i, + actual.as_canonical_u64(), + exp + ); + } + + // Step 3: MDS after first round + external_linear_layer(&mut state); + let expected_after_round = [ + 15870188304392300026u64, + 5420667415882104228, + 4061330223075420786, + 3907156211996553271, + 7220184397568305579, + 6276399041015305555, + 10112551490188661599, + 920706293309282174, + 6439956850909369582, + 10360554408705021829, + 3933312188334710888, + 6787120046090971660, + ]; + for (i, (actual, exp)) in state.iter().zip(expected_after_round.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "After first round MDS mismatch at index {}: got {}, expected {}", + i, + actual.as_canonical_u64(), + exp + ); + } + } + + #[test] + fn test_sbox() { + // Compare S-box (x^7) against p3 + // p3 output: S-box(0): 0, S-box(1): 1, S-box(2): 128, S-box(5): 78125, S-box(4294967295): + // 4294967295 + let test_cases = [ + (0u64, 0u64), + (1, 1), + (2, 128), + (5, 78125), + (0xFFFFFFFF, 0xFFFFFFFF), // (2^32-1)^7 mod P = 2^32-1 + ]; + + for (input, expected) in test_cases { + let x = Goldilocks::from_u64(input); + let y = x.exp7(); + assert_eq!( + y.as_canonical_u64(), + expected, + "S-box mismatch for input {}: got {}, expected {}", + input, + y.as_canonical_u64(), + expected + ); + } + } + + #[test] + fn test_first_external_round_zero_state() { + // Compare first external round with zero state against p3 + // p3 output after RC + S-box: [5716687150516714629, 12606094110489894541, + // 7176561780833472673, 2787933693728327558, 2372134002172061930, 5020691920936401247, + // 15687823912570289724, 15071980520517301340, 751173130886275522, 17086029608447879298, + // 4895459708442877360, 10576769927437500691] + + let poseidon = Poseidon2::new(); + let mut state = [Goldilocks::ZERO; SPONGE_WIDTH]; + + // Add round constants and apply S-box (first external round, no linear layer yet) + let rc = &poseidon.initial_external_constants[0]; + for i in 0..SPONGE_WIDTH { + state[i] += rc[i]; + state[i] = state[i].exp7(); + } + + let expected = [ + 5716687150516714629u64, + 12606094110489894541, + 7176561780833472673, + 2787933693728327558, + 2372134002172061930, + 5020691920936401247, + 15687823912570289724, + 15071980520517301340, + 751173130886275522, + 17086029608447879298, + 4895459708442877360, + 10576769927437500691, + ]; + + for (i, (actual, exp)) in state.iter().zip(expected.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "First external round (RC+S-box) mismatch at index {}: got 0x{:x}, expected 0x{:x}", + i, + actual.as_canonical_u64(), + exp + ); + } + } + + #[test] + fn test_internal_linear_layer() { + // Compare against p3's output for internal linear layer + let mut state: [Goldilocks; SPONGE_WIDTH] = + core::array::from_fn(|i| Goldilocks::from_u64((i + 1) as u64)); + let poseidon = Poseidon2::new(); + + internal_linear_layer(&mut state, &poseidon.matrix_diag); + + let expected = [ + 14102670999874605902u64, + 12724564314584031161, + 2820561051427350843, + 16542800896675938601, + 15306463738286039492, + 7752500014678011478, + 6061018803481092188, + 7074894295206835361, + 12845226504185330854, + 9784840285965170449, + 9819782087245364719, + 15989125545469266117, + ]; + + for (i, (actual, exp)) in state.iter().zip(expected.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "Internal linear layer mismatch at index {}: got {}, expected {}", + i, + actual.as_canonical_u64(), + exp + ); + } + } + + #[test] + fn test_external_linear_layer() { + // Compare against p3's output for external linear layer + let mut state: [Goldilocks; SPONGE_WIDTH] = + core::array::from_fn(|i| Goldilocks::from_u64((i + 1) as u64)); + + external_linear_layer(&mut state); + + // Expected from p3: [144, 156, 168, 148, 172, 184, 196, 176, 200, 212, 224, 204] + let expected = [144u64, 156, 168, 148, 172, 184, 196, 176, 200, 212, 224, 204]; + for (i, (actual, exp)) in state.iter().zip(expected.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *exp, + "External linear layer mismatch at index {}: got {}, expected {}", + i, + actual.as_canonical_u64(), + exp + ); + } + } + + #[test] + fn test_poseidon2_matches_p3_zero_input() { + let poseidon = Poseidon2::new(); + + let mut state = [Goldilocks::ZERO; SPONGE_WIDTH]; + poseidon.permute_mut(&mut state); + + for (i, (actual, expected)) in state.iter().zip(P3_ZERO_RESULT.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *expected, + "Mismatch at index {} for zero input: got 0x{:016x}, expected 0x{:016x}", + i, + actual.as_canonical_u64(), + expected + ); + } + } + + #[test] + fn test_poseidon2_matches_p3_sequential_input() { + let poseidon = Poseidon2::new(); + + let mut state: [Goldilocks; SPONGE_WIDTH] = + core::array::from_fn(|i| Goldilocks::from_u64((i + 1) as u64)); + poseidon.permute_mut(&mut state); + + for (i, (actual, expected)) in state.iter().zip(P3_SEQ_RESULT.iter()).enumerate() { + assert_eq!( + actual.as_canonical_u64(), + *expected, + "Mismatch at index {} for sequential input: got 0x{:016x}, expected 0x{:016x}", + i, + actual.as_canonical_u64(), + expected + ); + } + } + + #[test] + fn test_poseidon2_deterministic() { + let poseidon = Poseidon2::new(); + + let mut state1 = [Goldilocks::ZERO; SPONGE_WIDTH]; + let mut state2 = [Goldilocks::ZERO; SPONGE_WIDTH]; + + poseidon.permute_mut(&mut state1); + poseidon.permute_mut(&mut state2); + + assert_eq!(state1, state2, "Permutation should be deterministic"); + } + + #[test] + fn test_poseidon2_non_trivial() { + let poseidon = Poseidon2::new(); + + let mut state = [Goldilocks::ZERO; SPONGE_WIDTH]; + let original = state; + + poseidon.permute_mut(&mut state); + + assert_ne!(state, original, "Permutation should change state"); + } + + #[test] + fn test_apply_mat4() { + // Test that mat4 produces expected output + let mut x = + [Goldilocks::new(1), Goldilocks::new(2), Goldilocks::new(3), Goldilocks::new(4)]; + apply_mat4(&mut x); + + // Matrix multiplication: + // [ 2 3 1 1 ] [ 1 ] [ 2+6+3+4 ] [ 15 ] + // [ 1 2 3 1 ] [ 2 ] = [ 1+4+9+4 ] = [ 18 ] + // [ 1 1 2 3 ] [ 3 ] [ 1+2+6+12] [ 21 ] + // [ 3 1 1 2 ] [ 4 ] [ 3+2+3+8 ] [ 16 ] + assert_eq!(x[0].as_canonical_u64(), 15); + assert_eq!(x[1].as_canonical_u64(), 18); + assert_eq!(x[2].as_canonical_u64(), 21); + assert_eq!(x[3].as_canonical_u64(), 16); + } +} diff --git a/core/src/serialization.rs b/src/serialization.rs similarity index 95% rename from core/src/serialization.rs rename to src/serialization.rs index eeffd68..e88d5c4 100644 --- a/core/src/serialization.rs +++ b/src/serialization.rs @@ -12,9 +12,8 @@ //! The 8-bytes/felt encoding is used only for hash outputs (which are already field elements). use alloc::{string::String, vec::Vec}; -use p3_field::{integers::QuotientMap, PrimeField64}; -use p3_goldilocks::Goldilocks; -use qp_poseidon_constants::POSEIDON2_OUTPUT; + +use crate::{goldilocks::Goldilocks, poseidon2::POSEIDON2_OUTPUT}; const BIT_32_LIMB_MASK: u64 = 0xFFFF_FFFF; @@ -37,7 +36,7 @@ pub const BYTES_PER_FELT: usize = 4; #[inline] fn from_u64(x: u64) -> Goldilocks { - Goldilocks::from_int(x) + Goldilocks::from_u64(x) } #[inline] @@ -259,14 +258,11 @@ pub fn digest_to_bytes(input: &[Goldilocks; POSEIDON2_OUTPUT]) -> BytesDigest { /// /// Each 8-byte chunk becomes one field element. /// Use this to deserialize hash outputs from storage. -pub fn bytes_to_digest(input: &BytesDigest) -> [F; POSEIDON2_OUTPUT] -where - F: p3_field::PrimeCharacteristicRing, -{ +pub fn bytes_to_digest(input: &BytesDigest) -> [Goldilocks; POSEIDON2_OUTPUT] { core::array::from_fn(|i| { let start = i * 8; let bytes: [u8; 8] = input[start..start + 8].try_into().expect("8 bytes"); - F::from_u64(u64::from_le_bytes(bytes)) + Goldilocks::from_u64(u64::from_le_bytes(bytes)) }) } @@ -413,8 +409,8 @@ mod tests { #[test] fn test_malformed_bytes_input_error_cases() { let malformed_cases: Vec> = vec![ - vec![Goldilocks::from_int(0x12345678_i64), Goldilocks::from_int(0x1ABCDEF0_i64)], - vec![Goldilocks::from_int(0x12345678_i64), Goldilocks::from_int(0x00000002_i64)], + vec![Goldilocks::from_u64(0x12345678), Goldilocks::from_u64(0x1ABCDEF0)], + vec![Goldilocks::from_u64(0x12345678), Goldilocks::from_u64(0x00000002)], ]; for malformed_felts in &malformed_cases { @@ -425,8 +421,7 @@ mod tests { #[test] fn test_felt_width_error_handling() { - let invalid_felts = - [Goldilocks::from_int(0x1_0000_0000_i64), Goldilocks::from_int(0xFFFFFFFF_i64)]; + let invalid_felts = [Goldilocks::from_u64(0x1_0000_0000), Goldilocks::from_u64(0xFFFFFFFF)]; let result = try_felts_to_u64(invalid_felts); assert!(result.is_err(), "Expected felt width error for invalid felts"); } @@ -473,7 +468,7 @@ mod tests { // Should be little-endian u64 let expected = u64::from_le_bytes(input); - assert_eq!(felts[0], Goldilocks::from_int(expected)); + assert_eq!(felts[0], Goldilocks::from_u64(expected)); } #[test]