From 15786646591f6867f6433e453a9dda625ac725aa Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 12:01:30 +0100 Subject: [PATCH 01/10] add safe implementation in Rust --- Cargo.lock | 93 +++++- Cargo.toml | 1 + crates/safe/Cargo.toml | 14 + crates/safe/src/lib.rs | 644 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 747 insertions(+), 5 deletions(-) create mode 100644 crates/safe/Cargo.toml create mode 100644 crates/safe/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 2042dd9ea1..2d7bc6eab8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1185,11 +1185,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" dependencies = [ - "ark-ec", + "ark-ec 0.4.2", "ark-ff 0.4.2", "ark-std 0.4.0", ] +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + [[package]] name = "ark-ec" version = "0.4.2" @@ -1197,7 +1208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" dependencies = [ "ark-ff 0.4.2", - "ark-poly", + "ark-poly 0.4.2", "ark-serialize 0.4.2", "ark-std 0.4.0", "derivative", @@ -1207,6 +1218,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint", + "num-integer", + "num-traits", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -1346,6 +1378,21 @@ dependencies = [ "hashbrown 0.13.2", ] +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -1362,7 +1409,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" dependencies = [ - "ark-serialize-derive", + "ark-serialize-derive 0.4.2", "ark-std 0.4.0", "digest 0.10.7", "num-bigint", @@ -1374,6 +1421,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" dependencies = [ + "ark-serialize-derive 0.5.0", "ark-std 0.5.0", "arrayvec", "digest 0.10.7", @@ -1391,6 +1439,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.110", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -2743,7 +2802,7 @@ dependencies = [ name = "e3-compute-provider" version = "0.1.5" dependencies = [ - "ark-bn254", + "ark-bn254 0.4.0", "ark-ff 0.4.2", "hex", "lean-imt", @@ -5220,7 +5279,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" dependencies = [ - "ark-bn254", + "ark-bn254 0.4.0", "ark-ff 0.4.2", "num-bigint", "thiserror 1.0.69", @@ -7064,6 +7123,17 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "safe" +version = "0.1.5" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", + "hex", + "sha3", + "taceo-poseidon2", +] + [[package]] name = "scc" version = "2.4.0" @@ -7695,6 +7765,19 @@ dependencies = [ "libc", ] +[[package]] +name = "taceo-poseidon2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbf106fb8682ee4e057872a18f431828bd467c28d2ead469e4c84dbf6ce5ec6" +dependencies = [ + "ark-bn254 0.5.0", + "ark-ff 0.5.0", + "ark-std 0.5.0", + "num-bigint", + "num-traits", +] + [[package]] name = "tap" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 91f3959161..481036a749 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "crates/net", "crates/program-server", "crates/request", + "crates/safe", "crates/sdk", "crates/sortition", "crates/support-scripts", diff --git a/crates/safe/Cargo.toml b/crates/safe/Cargo.toml new file mode 100644 index 0000000000..499c153e57 --- /dev/null +++ b/crates/safe/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "safe" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "E3 - Enclave SAFE" +repository = "https://github.com/gnosisguild/enclave/crates/safe" + +[dependencies] +sha3 = "0.10.8" +ark-ff = "0.5" +ark-bn254 = "0.5" +taceo-poseidon2 = { version = "0.2", features = ["bn254", "t4"] } +hex = { workspace = true } diff --git a/crates/safe/src/lib.rs b/crates/safe/src/lib.rs new file mode 100644 index 0000000000..06d4024857 --- /dev/null +++ b/crates/safe/src/lib.rs @@ -0,0 +1,644 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! SAFE (Sponge API for Field Elements) +//! +//! This module provides a complete implementation of the SAFE API in Rust as defined in: +//! "SAFE (Sponge API for Field Elements) - A Toolbox for ZK Hash Applications" +//! see https://hackmd.io/bHgsH6mMStCVibM_wYvb2w#22-Sponge-state for more details. +//! +//! SAFE provides a unified interface for cryptographic sponge functions that can be +//! instantiated with various permutations to create hash functions, MACs, authenticated +//! encryption schemes, and other cryptographic primitives for ZK proof systems. +//! +//! This implementation follows the SAFE specification exactly, providing: +//! - Complete API: START, ABSORB, SQUEEZE, FINISH operations. +//! - Full security: Domain separation, tag computation, IO pattern validation. +//! - Poseidon2 integration: Field-friendly permutation for ZK systems. +//! - Specification compliance: All operations follow SAFE spec 2.4 exactly. +//! - Natural API design: Variable-length inputs, automatic length detection from IO patterns. + +use ark_bn254::Fr; +use ark_ff::Zero; +use sha3::{Digest, Keccak256}; +use taceo_poseidon2::bn254::t4::permutation as poseidon2_permutation; + +/// Field type used throughout the SAFE implementation (BN254 scalar field) +pub type Field = Fr; + +/// Rate parameter for the sponge construction (number of field elements that can be absorbed per permutation call). +pub const RATE: usize = 3; + +/// Capacity parameter for the sponge construction (security parameter, typically 1-2 field elements). +pub const CAPACITY: usize = 1; + +/// Total state size (rate + capacity) in field elements. +pub const STATE_SIZE: usize = RATE + CAPACITY; + +// IO Pattern encoding constants (from SAFE spec 2.3). +// +// These constants are used for encoding operation types in the 32-bit word format: +// - MSB set to 1 for ABSORB operations +// - MSB set to 0 for SQUEEZE operations + +/// Flag for ABSORB operations (MSB = 1) +pub const ABSORB_FLAG: u32 = 0x80000000; + +/// Flag for SQUEEZE operations (MSB = 0) +pub const SQUEEZE_FLAG: u32 = 0x00000000; + +/// SAFE Sponge State (following spec 2.2) +/// +/// The sponge state consists of the permutation state, tag, position counters, +/// and IO pattern tracking as defined in the SAFE specification. +/// +/// # Generic Parameters +/// - `L`: The length of the IO pattern array +/// +/// # Fields +/// - `state`: Permutation state V in F^n (rate + capacity elements) +/// - `tag`: Parameter tag T used for instance differentiation +/// - `absorb_pos`: Current absorb position (<= n-c) +/// - `squeeze_pos`: Current squeeze position (<= n-c) +/// - `io_pattern`: Expected IO pattern for validation (encoded 32-bit words) +/// - `io_count`: Current operation count for pattern tracking +#[derive(Clone, Debug)] +pub struct SafeSponge { + /// Permutation state V in F^n (rate + capacity elements). + state: [Field; STATE_SIZE], + /// Parameter tag T used for instance differentiation. + #[allow(dead_code)] + tag: Field, + /// Current absorb position (<= n-c). + absorb_pos: usize, + /// Current squeeze position (<= n-c). + squeeze_pos: usize, + /// Expected IO pattern for validation. + io_pattern: [u32; L], + /// Current operation count for pattern tracking (spec 2.4: io_count). + io_count: usize, +} + +impl SafeSponge { + /// Initializes a new SAFE sponge instance with the given IO pattern and domain separator (following spec 2.4). + /// + /// # Arguments + /// - `io_pattern`: Array of 32-bit encoded operations defining the expected sequence of ABSORB/SQUEEZE calls. + /// Each word has MSB=1 for ABSORB operations, MSB=0 for SQUEEZE operations. + /// - `domain_separator`: 64-byte domain separator for cross-protocol security. + /// + /// # Returns + /// A new `SafeSponge` instance with initialized state + pub fn start(io_pattern: [u32; L], domain_separator: [u8; 64]) -> SafeSponge { + // Compute tag from IO pattern and domain separator (spec 2.3). + let tag = compute_tag(io_pattern, domain_separator); + + let mut state = [Field::zero(); STATE_SIZE]; + // Initialize capacity with tag (spec 2.4). + // Add T to the first 128 bits of the state. + state[0] = tag; + + SafeSponge { + state, + tag, + absorb_pos: 0, + squeeze_pos: 0, + io_pattern, + io_count: 0, + } + } + + /// Absorbs field elements into the sponge state, interleaving permutation calls as needed (following spec 2.4). + /// + /// The number of elements to absorb is automatically validated against the IO pattern. + /// This method accepts variable-length slices, making it natural to use without padding. + /// + /// # Arguments + /// - `input`: Slice of field elements to absorb (variable length, must match IO pattern) + /// + /// # Panics + /// Panics if the operation doesn't match the expected IO pattern. + pub fn absorb(&mut self, input: Vec) { + let length = input.len() as u32; + + // Validate against IO pattern. + assert!(self.io_count < L, "IO pattern exhausted"); + + // Parse expected operation from io_pattern (encoded word) + let expected_encoded_word = self.io_pattern[self.io_count]; + let is_expected_absorb = (expected_encoded_word & ABSORB_FLAG) != 0; + let expected_length = expected_encoded_word & 0x7FFFFFFF; + + // Validate operation type and length + assert!(is_expected_absorb, "Expected ABSORB operation"); + assert!(expected_length == length, "Length mismatch"); + + // Process each element naturally (no unnecessary iterations). + for input in &input { + // If absorb_pos == (n-c) then permute and reset (spec 2.4). + if self.absorb_pos == RATE { + // n-c = RATE. + self.state = self.permute(); + self.absorb_pos = 0; + } + + // Add X[i] to state at absorb_pos (spec 2.4). + // Note: absorb_pos is the rate position, not capacity position. + self.state[self.absorb_pos + CAPACITY] += input; + self.absorb_pos += 1; + } + + // Verify that the encoded word matches the expected pattern. + let encoded_word = ABSORB_FLAG | length; + assert!(encoded_word == expected_encoded_word); + + self.io_count += 1; + + // Force permute at start of next SQUEEZE (spec 2.4). + self.squeeze_pos = RATE; + } + + /// Extracts field elements from the sponge state, interleaving permutation calls as needed (following spec 2.4). + /// + /// The number of elements to squeeze is automatically determined from the IO pattern. + /// + /// # Returns + /// A vector of field elements squeezed from the sponge state. + /// + /// # Panics + /// Panics if the operation doesn't match the expected IO pattern. + pub fn squeeze(&mut self) -> Vec { + // Parse expected operation from io_pattern (encoded word) + let expected_encoded_word = self.io_pattern[self.io_count]; + let is_expected_squeeze = (expected_encoded_word & ABSORB_FLAG) == 0; + let length = (expected_encoded_word & 0x7FFFFFFF) as usize; + + // Validate operation type + assert!(is_expected_squeeze, "Expected SQUEEZE operation"); + + let mut output = Vec::with_capacity(length); + + // SQUEEZE implementation following spec 2.4. + // If length==0, loop won't execute (spec 2.4). + for _ in 0..length { + // If squeeze_pos==(n-c) then permute and reset (spec 2.4). + if self.squeeze_pos == RATE { + // n-c = RATE. + self.state = self.permute(); + self.squeeze_pos = 0; + self.absorb_pos = 0; + } + // Set Y[i] to state element at squeeze_pos (spec 2.4). + output.push(self.state[self.squeeze_pos + CAPACITY]); + self.squeeze_pos += 1; + } + + // Verify that the encoded word matches the expected pattern. + let encoded_word = SQUEEZE_FLAG | (length as u32); + assert!(encoded_word == expected_encoded_word); + + self.io_count += 1; + output + } + + /// Finalizes the sponge instance, verifying that all expected operations have been performed + /// and clearing the internal state for security (following spec 2.4). + /// + /// This function is used to ensure that the sponge instance has been used correctly + /// and to prevent information leakage. + /// + /// # Panics + /// Panics if not all operations in the IO pattern have been performed. + pub fn finish(&mut self) { + // Check that io_count equals the length of the IO pattern expected (spec 2.4). + assert!(self.io_count == L, "IO pattern not completed"); + + // Erase the state and its variables (spec 2.4). + self.state = [Field::zero(); STATE_SIZE]; + self.absorb_pos = 0; + self.squeeze_pos = 0; + self.io_count = 0; + } + + /// Permute the state using Poseidon2 (following spec 2.4). + /// + /// Applies the Poseidon2 permutation to the current state. + /// This is the core cryptographic primitive of the sponge construction. + /// + /// # Returns + /// New state after permutation + fn permute(&self) -> [Field; STATE_SIZE] { + poseidon2_permutation(&self.state) + } +} + +/// Computes a unique tag for a sponge instance based on its IO pattern and domain separator. +/// The tag is used to ensure that distinct instances behave like distinct functions. +/// +/// # Generic Parameters +/// - `L`: The length of the IO pattern array +/// +/// # Arguments +/// - `io_pattern`: Array of 32-bit encoded operations defining the sponge's usage pattern. +/// Each word has MSB=1 for ABSORB operations, MSB=0 for SQUEEZE operations. +/// - `domain_separator`: 64-byte domain separator for cross-protocol security. +/// +/// # Returns +/// A field element representing the 128-bit tag. +pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) -> Field { + // Step 1: Parse and aggregate consecutive operations of the same type + let mut encoded_words = [0; L]; // Support up to L operations. + let mut word_count = 0; + let mut current_absorb_sum = 0; + let mut current_squeeze_sum = 0; + let mut last_was_absorb = false; + + for item in io_pattern.iter().take(L) { + if *item > 0 { + // Parse operation type from MSB and length from lower 31 bits + let is_absorb = (*item & ABSORB_FLAG) != 0; + let length = *item & 0x7FFFFFFF; // Clear MSB to get length + + if is_absorb { + if last_was_absorb { + // Aggregate consecutive ABSORB operations + current_absorb_sum += length; + } else { + // Start new ABSORB sequence + if current_squeeze_sum > 0 { + // Flush previous SQUEEZE sequence + encoded_words[word_count] = SQUEEZE_FLAG | current_squeeze_sum; + word_count += 1; + current_squeeze_sum = 0; + } + current_absorb_sum = length; + } + last_was_absorb = true; + } else { + if !last_was_absorb { + // Aggregate consecutive SQUEEZE operations + current_squeeze_sum += length; + } else { + // Start new SQUEEZE sequence + if current_absorb_sum > 0 { + // Flush previous ABSORB sequence + encoded_words[word_count] = ABSORB_FLAG | current_absorb_sum; + word_count += 1; + current_absorb_sum = 0; + } + current_squeeze_sum = length; + } + last_was_absorb = false; + } + } + } + + // Flush remaining operations + if current_absorb_sum > 0 { + encoded_words[word_count] = ABSORB_FLAG | current_absorb_sum; + word_count += 1; + } + if current_squeeze_sum > 0 { + encoded_words[word_count] = SQUEEZE_FLAG | current_squeeze_sum; + word_count += 1; + } + + // Step 2: Serialize to byte string and append domain separator (following SAFE spec 2.3). + // Create a fixed-size array for SHA256 input (max 256 bytes should be enough). + let mut input_bytes = [0; 256]; + let mut byte_count = 0; + + // Serialize encoded words to bytes (big-endian as per SAFE spec). + for word in encoded_words.iter().take(word_count) { + if byte_count + 4 <= 256 { + let word = *word; + input_bytes[byte_count] = (word >> 24) as u8; + input_bytes[byte_count + 1] = (word >> 16) as u8; + input_bytes[byte_count + 2] = (word >> 8) as u8; + input_bytes[byte_count + 3] = word as u8; + byte_count += 4; + } + } + + // Append domain separator. + for i in 0..64 { + if byte_count < 256 { + input_bytes[byte_count] = domain_separator[i]; + byte_count += 1; + } + } + + // Step 3: Hash with Keccak-256 and truncate to 128 bits. + // Note: Using Keccak-256 (Ethereum's hash) for compatibility with Noir's keccak256. + // The SAFE spec mentions SHA3-256, but Keccak-256 is used here for cross-implementation consistency. + let mut hasher = Keccak256::new(); + hasher.update(&input_bytes[..byte_count]); + let hash_bytes = hasher.finalize(); + + // Convert first 128 bits (16 bytes) to field element. + let mut tag_value: Field = Field::zero(); + for i in 0..16 { + tag_value = tag_value * Field::from(256) + Field::from(hash_bytes[i]); + } + + tag_value +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper function to create a field element from a u64 value + fn field_from_u64(val: u64) -> Field { + Field::from(val) + } + + fn test_domain_separator() -> [u8; 64] { + let mut ds = [0u8; 64]; + ds[0] = 0x41; // 'A' + ds[1] = 0x42; // 'B' + ds[2] = 0x43; // 'C' + ds[3] = 0x44; // 'D' + ds + } + + #[test] + fn test_safe_hashing() { + // Verifies basic hash functionality with a simple ABSORB(3) + SQUEEZE(1) pattern. + let domain_separator = test_domain_separator(); + let elements = vec![field_from_u64(1), field_from_u64(2), field_from_u64(3)]; + + // Pattern: ABSORB(3), SQUEEZE(1) + let io_pattern = [0x80000003, 0x00000001]; + let mut sponge = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(elements.clone()); + let output = sponge.squeeze(); + sponge.finish(); + + assert_eq!(output.len(), 1); + assert!(output[0] != Field::zero()); + + // Test determinism + let mut sponge2 = SafeSponge::start(io_pattern, domain_separator); + sponge2.absorb(elements.clone()); + let output2 = sponge2.squeeze(); + sponge2.finish(); + + assert_eq!(output2.len(), 1); + assert_eq!(output[0], output2[0]); + } + + #[test] + fn test_merkle_node() { + // Verifies SAFE can be used for Merkle tree node hashing with pattern ABSORB(1) + ABSORB(1) + SQUEEZE(1). + let domain_separator = test_domain_separator(); + let left = vec![field_from_u64(123)]; + let right = vec![field_from_u64(456)]; + + // Pattern: ABSORB(1), ABSORB(1), SQUEEZE(1) + let io_pattern = [0x80000001, 0x80000001, 0x00000001]; + let mut sponge = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(left.clone()); + sponge.absorb(right.clone()); + let output = sponge.squeeze(); + sponge.finish(); + + assert_eq!(output.len(), 1); + assert!(output[0] != Field::zero()); + + // Test determinism + let mut sponge2 = SafeSponge::start(io_pattern, domain_separator); + sponge2.absorb(left.clone()); + sponge2.absorb(right.clone()); + let output2 = sponge2.squeeze(); + sponge2.finish(); + + assert_eq!(output[0], output2[0]); + } + + #[test] + fn test_commitment_scheme() { + // Verifies SAFE can be used for commitment schemes with pattern ABSORB(3) + SQUEEZE(1). + let domain_separator = test_domain_separator(); + let values = vec![field_from_u64(10), field_from_u64(20), field_from_u64(30)]; + + // Pattern: ABSORB(3), SQUEEZE(1) + let io_pattern = [0x80000003, 0x00000001]; + let mut sponge = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(values.clone()); + let output = sponge.squeeze(); + sponge.finish(); + + assert_eq!(output.len(), 1); + assert!(output[0] != Field::zero()); + + // Test determinism + let mut sponge2 = SafeSponge::start(io_pattern, domain_separator); + sponge2.absorb(values.clone()); + let output2 = sponge2.squeeze(); + sponge2.finish(); + + assert_eq!(output[0], output2[0]); + } + + #[test] + fn test_domain_separation() { + // Verifies that different domain separators produce different outputs for the same input. + let elements = vec![field_from_u64(1), field_from_u64(2), field_from_u64(3)]; + + let mut domain1 = [0u8; 64]; + domain1[0] = 0x41; + domain1[1] = 0x42; + domain1[2] = 0x43; + domain1[3] = 0x44; + + let mut domain2 = [0u8; 64]; + domain2[0] = 0x41; + domain2[1] = 0x42; + domain2[2] = 0x43; + domain2[3] = 0x45; // Different! + + // Pattern: ABSORB(3), SQUEEZE(1) + let io_pattern = [0x80000003, 0x00000001]; + + let mut sponge1 = SafeSponge::start(io_pattern, domain1); + sponge1.absorb(elements.clone()); + let output1 = sponge1.squeeze(); + sponge1.finish(); + + let mut sponge2 = SafeSponge::start(io_pattern, domain2); + sponge2.absorb(elements.clone()); + let output2 = sponge2.squeeze(); + sponge2.finish(); + + assert_eq!(output1.len(), 1); + assert_eq!(output2.len(), 1); + assert!(output1[0] != output2[0]); // Different domain separators should produce different outputs + } + + #[test] + fn test_multiple_squeeze() { + // Verifies that multiple field elements can be squeezed in a single operation. + let domain_separator = test_domain_separator(); + let elements = vec![field_from_u64(1), field_from_u64(2), field_from_u64(3)]; + + // Pattern: ABSORB(3), SQUEEZE(2) + let io_pattern = [0x80000003, 0x00000002]; + let mut sponge = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(elements.clone()); + let output = sponge.squeeze(); + sponge.finish(); + + assert_eq!(output.len(), 2); + assert!(output[0] != Field::zero()); + assert!(output[1] != Field::zero()); + assert!(output[0] != output[1]); // Different squeeze outputs should be different + } + + #[test] + fn test_zero_length_operations() { + // Verifies that zero-length ABSORB and SQUEEZE operations are handled correctly. + let domain_separator = test_domain_separator(); + + // Pattern: ABSORB(0), SQUEEZE(1) + let io_pattern = [0x80000000, 0x00000001]; + let mut sponge = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(vec![]); + let output = sponge.squeeze(); + sponge.finish(); + + assert_eq!(output.len(), 1); + assert!(output[0] != Field::zero()); + } + + #[test] + fn test_tag_computation() { + // Verifies the tag computation algorithm. + // Pattern: ABSORB(3), ABSORB(3), SQUEEZE(3) + // Should aggregate to: ABSORB(6), SQUEEZE(3) + let io_pattern = [0x80000003, 0x80000003, 0x00000003]; + let domain_separator = test_domain_separator(); + + let tag = compute_tag(io_pattern, domain_separator); + + // Test determinism + let tag2 = compute_tag(io_pattern, domain_separator); + assert_eq!(tag, tag2); + + // Test that different patterns produce different tags + let io_pattern2 = [0x80000003, 0x00000003]; // ABSORB(3), SQUEEZE(3) - different pattern + let tag3 = compute_tag(io_pattern2, domain_separator); + assert!(tag != tag3); + } + + #[test] + fn test_consecutive_absorb_aggregation() { + // Test that consecutive ABSORB operations are properly aggregated + // Pattern: ABSORB(1), ABSORB(1), SQUEEZE(1) should aggregate to ABSORB(2), SQUEEZE(1) + let domain_separator = test_domain_separator(); + + // Test pattern: ABSORB(1), ABSORB(1), SQUEEZE(1) + let io_pattern = [0x80000001, 0x80000001, 0x00000001]; + + // This should aggregate to: ABSORB(2), SQUEEZE(1) = [0x80000002, 0x00000001] + let tag = compute_tag(io_pattern, domain_separator); + + // Test that the aggregated pattern produces the same tag ABSORB(2), SQUEEZE(1) + let aggregated_pattern = [0x80000002, 0x00000001]; + let aggregated_tag = compute_tag(aggregated_pattern, domain_separator); + + // The tags should be identical because the patterns are equivalent after aggregation + assert_eq!( + tag, aggregated_tag, + "Consecutive ABSORB operations should aggregate to the same tag" + ); + + // Test that a different pattern produces a different tag + let different_pattern = [0x80000001, 0x00000001, 0x80000001]; // ABSORB(1), SQUEEZE(1), ABSORB(1) + let different_tag = compute_tag(different_pattern, domain_separator); + + // This should be different because it doesn't have consecutive ABSORB operations + assert!( + tag != different_tag, + "Different patterns should produce different tags" + ); + } + + #[test] + fn test_consecutive_squeeze_aggregation() { + // Test that consecutive SQUEEZE operations are properly aggregated + // Pattern: ABSORB(1), SQUEEZE(1), SQUEEZE(1) should aggregate to ABSORB(1), SQUEEZE(2) + let domain_separator = test_domain_separator(); + + // Test pattern: ABSORB(1), SQUEEZE(1), SQUEEZE(1) + let io_pattern = [0x80000001, 0x00000001, 0x00000001]; + + // This should aggregate to: ABSORB(1), SQUEEZE(2) = [0x80000001, 0x00000002] + let tag = compute_tag(io_pattern, domain_separator); + + // Test that the aggregated pattern produces the same tag ABSORB(1), SQUEEZE(2) + let aggregated_pattern = [0x80000001, 0x00000002]; + let aggregated_tag = compute_tag(aggregated_pattern, domain_separator); + + // The tags should be identical because the patterns are equivalent after aggregation + assert_eq!( + tag, aggregated_tag, + "Consecutive SQUEEZE operations should aggregate to the same tag" + ); + + // Test that a different pattern produces a different tag + let different_pattern = [0x80000001, 0x00000001, 0x80000001]; // ABSORB(1), SQUEEZE(1), ABSORB(1) + let different_tag = compute_tag(different_pattern, domain_separator); + + // This should be different because it doesn't have consecutive SQUEEZE operations + assert!( + tag != different_tag, + "Different patterns should produce different tags" + ); + } + + #[test] + fn test_mixed_consecutive_aggregation() { + // Test that both consecutive ABSORB and SQUEEZE operations are properly aggregated + // Pattern: ABSORB(1), ABSORB(1), SQUEEZE(1), SQUEEZE(1), ABSORB(1) + // Should aggregate to: ABSORB(2), SQUEEZE(2), ABSORB(1) + let domain_separator = test_domain_separator(); + + // Test pattern: ABSORB(1), ABSORB(1), SQUEEZE(1), SQUEEZE(1), ABSORB(1) + let io_pattern = [0x80000001, 0x80000001, 0x00000001, 0x00000001, 0x80000001]; + + // This should aggregate to: ABSORB(2), SQUEEZE(2), ABSORB(1) = [0x80000002, 0x00000002, 0x80000001] + let tag = compute_tag(io_pattern, domain_separator); + + // Test that the aggregated pattern produces the same tag + let aggregated_pattern = [0x80000002, 0x00000002, 0x80000001]; // ABSORB(2), SQUEEZE(2), ABSORB(1) + let aggregated_tag = compute_tag(aggregated_pattern, domain_separator); + + // The tags should be identical because the patterns are equivalent after aggregation + assert_eq!( + tag, aggregated_tag, + "Mixed consecutive operations should aggregate to the same tag" + ); + } + #[test] + fn test_large_io_pattern() { + let domain_separator = test_domain_separator(); + + // Create pattern with 64 alternating ABSORB(1) and SQUEEZE(1) operations + // so we get 64 words = 256 bytes + let mut io_pattern = [0u32; 64]; + for i in 0..64 { + if i % 2 == 0 { + io_pattern[i] = ABSORB_FLAG | 1; // ABSORB(1) + } else { + io_pattern[i] = SQUEEZE_FLAG | 1; // SQUEEZE(1) + } + } + + let tag = compute_tag(io_pattern, domain_separator); + assert!(tag != Field::zero()); + } +} From 0d565fbf0e25fbbb5811a7c517fe9c5c3eabfffa Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 12:01:46 +0100 Subject: [PATCH 02/10] use keccak256 sha3 in safe and fix buffer overflow bug --- circuits/crates/libs/safe/Nargo.toml | 2 +- circuits/crates/libs/safe/src/lib.nr | 44 +++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/circuits/crates/libs/safe/Nargo.toml b/circuits/crates/libs/safe/Nargo.toml index dcc413deb9..cdb3f5a856 100644 --- a/circuits/crates/libs/safe/Nargo.toml +++ b/circuits/crates/libs/safe/Nargo.toml @@ -7,4 +7,4 @@ license = "MIT" [dependencies] poseidon = { tag = "v0.1.1", git = "https://github.com/noir-lang/poseidon" } -sha256 = { tag = "v0.2.0", git = "https://github.com/noir-lang/sha256" } \ No newline at end of file +keccak256 = { tag = "v0.1.1", git = "https://github.com/noir-lang/keccak256" } \ No newline at end of file diff --git a/circuits/crates/libs/safe/src/lib.nr b/circuits/crates/libs/safe/src/lib.nr index 6c5738a91d..485a6d9789 100644 --- a/circuits/crates/libs/safe/src/lib.nr +++ b/circuits/crates/libs/safe/src/lib.nr @@ -4,8 +4,8 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use keccak256::keccak256; use poseidon::poseidon2_permutation; -use sha256::sha256_var; /// SAFE (Sponge API for Field Elements) /// @@ -289,8 +289,12 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) let mut byte_count = 0; // Serialize encoded words to bytes (big-endian as per SAFE spec). - for i in 0..word_count { - if byte_count + 4 <= 256 { + // Note: Noir requires compile-time loop bounds, so we iterate over L (the array size) + // instead of word_count (runtime value). The condition `i < word_count` ensures we only + // process valid encoded words. This is safe because word_count <= L always holds + // (we can have at most L encoded words from L input operations). + for i in 0..L { + if (i < word_count) & (byte_count + 4 <= 256) { let word = encoded_words[i]; input_bytes[byte_count] = (word >> 24) as u8; input_bytes[byte_count + 1] = (word >> 16) as u8; @@ -302,14 +306,15 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) // Append domain separator. for i in 0..64 { - if byte_count + i < 256 { - input_bytes[byte_count + i] = domain_separator[i]; + if byte_count < 256 { + input_bytes[byte_count] = domain_separator[i]; + byte_count += 1; } } - byte_count += 64; - // Step 3: Hash with SHA256 and truncate to 128 bits (following SAFE spec 2.3). - let hash_bytes = sha256_var(input_bytes, byte_count as u64); + // Step 3: Hash with Keccak256 and truncate to 128 bits (following SAFE spec 2.3). + // Note: Noir uses Keccak256 which is equivalent to SHA3-256 for this purpose. + let hash_bytes = keccak256(input_bytes, byte_count as u32); // Convert first 128 bits (16 bytes) to field element. let mut tag_value: Field = 0; @@ -682,3 +687,26 @@ fn test_mixed_consecutive_aggregation() { println(f"Original tag: {tag}"); println(f"Aggregated tag: {aggregated_tag}"); } + +#[test] +fn test_large_io_pattern() { + let domain_separator = [ + 0x41, 0x42, 0x43, 0x44, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, + ]; + + // Create pattern with 64 alternating ABSORB(1) and SQUEEZE(1) operations + // so we get 64 words = 256 bytes + let mut io_pattern = [0u32; 64]; + for i in 0..64 { + if i % 2 == 0 { + io_pattern[i] = ABSORB_FLAG | 1; // ABSORB(1) + } else { + io_pattern[i] = SQUEEZE_FLAG | 1; // SQUEEZE(1) + } + } + + let tag = compute_tag(io_pattern, domain_separator); + assert(tag != 0); +} From 61b23d1eb7bbee105a596989bb9b81aef3d079df Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 12:09:13 +0100 Subject: [PATCH 03/10] fix outdated comment --- crates/safe/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/safe/src/lib.rs b/crates/safe/src/lib.rs index 06d4024857..d39efe2396 100644 --- a/crates/safe/src/lib.rs +++ b/crates/safe/src/lib.rs @@ -307,7 +307,7 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; } // Step 2: Serialize to byte string and append domain separator (following SAFE spec 2.3). - // Create a fixed-size array for SHA256 input (max 256 bytes should be enough). + // Create a fixed-size array for Keccak-256 input (max 256 bytes should be enough). let mut input_bytes = [0; 256]; let mut byte_count = 0; From 406754b71bffe204a96f341a482efa97b9c67bd2 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 12:10:34 +0100 Subject: [PATCH 04/10] another comment --- crates/safe/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/safe/src/lib.rs b/crates/safe/src/lib.rs index d39efe2396..1ab410b7a8 100644 --- a/crates/safe/src/lib.rs +++ b/crates/safe/src/lib.rs @@ -331,9 +331,9 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; } } - // Step 3: Hash with Keccak-256 and truncate to 128 bits. - // Note: Using Keccak-256 (Ethereum's hash) for compatibility with Noir's keccak256. - // The SAFE spec mentions SHA3-256, but Keccak-256 is used here for cross-implementation consistency. + // Step 3: Hash with Keccak256 and truncate to 128 bits. + // Note: Using Keccak-256 (Ethereum's hash) for consistency with the Rust implementation. + // Keccak-256 differs from FIPS SHA3-256 in padding but is used here for cross-implementation compatibility. let mut hasher = Keccak256::new(); hasher.update(&input_bytes[..byte_count]); let hash_bytes = hasher.finalize(); From d128bd722b271ca0ced7e178491a3f4d35b335dc Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 12:31:03 +0100 Subject: [PATCH 05/10] add assertion to validate properly IO pattern exhaustion --- circuits/crates/libs/safe/src/lib.nr | 3 +++ crates/safe/src/lib.rs | 35 ++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/circuits/crates/libs/safe/src/lib.nr b/circuits/crates/libs/safe/src/lib.nr index 485a6d9789..e25a80f1dc 100644 --- a/circuits/crates/libs/safe/src/lib.nr +++ b/circuits/crates/libs/safe/src/lib.nr @@ -156,6 +156,9 @@ impl SafeSponge { /// /// The number of elements to squeeze is automatically determined from the IO pattern. pub fn squeeze(&mut self) -> Vec { + // Validate against IO pattern. + assert(self.io_count < L); + // Parse expected operation from io_pattern (encoded word) let expected_encoded_word = self.io_pattern[self.io_count]; let is_expected_squeeze = (expected_encoded_word & ABSORB_FLAG) == 0; diff --git a/crates/safe/src/lib.rs b/crates/safe/src/lib.rs index 1ab410b7a8..b75a5e2ab4 100644 --- a/crates/safe/src/lib.rs +++ b/crates/safe/src/lib.rs @@ -171,6 +171,9 @@ impl SafeSponge { /// # Panics /// Panics if the operation doesn't match the expected IO pattern. pub fn squeeze(&mut self) -> Vec { + // Validate against IO pattern. + assert!(self.io_count < L, "IO pattern exhausted"); + // Parse expected operation from io_pattern (encoded word) let expected_encoded_word = self.io_pattern[self.io_count]; let is_expected_squeeze = (expected_encoded_word & ABSORB_FLAG) == 0; @@ -641,4 +644,36 @@ mod tests { let tag = compute_tag(io_pattern, domain_separator); assert!(tag != Field::zero()); } + #[test] + fn test_squeeze_io_pattern_exhausted() { + // This test verifies that squeeze properly checks for IO pattern exhaustion + // and provides a clear error message instead of an index-out-of-bounds panic. + + let domain_separator = test_domain_separator(); + + // Create a sponge with exactly one operation (L=1) + let io_pattern: [u32; 1] = [ABSORB_FLAG | 1]; // Only ABSORB(1) + + let mut sponge: SafeSponge<1> = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(vec![field_from_u64(42)]); + + // io_count is now 1, which equals L=1, so this should panic with "IO pattern exhausted" + let _ = sponge.squeeze(); + } + + #[test] + fn test_absorb_io_pattern_exhausted() { + // This test verifies that absorb properly checks for IO pattern exhaustion. + + let domain_separator = test_domain_separator(); + + // Create a sponge with exactly one operation (L=1) + let io_pattern: [u32; 1] = [ABSORB_FLAG | 1]; // Only ABSORB(1) + + let mut sponge: SafeSponge<1> = SafeSponge::start(io_pattern, domain_separator); + sponge.absorb(vec![field_from_u64(42)]); + + // io_count is now 1, which equals L=1, so this should panic with "IO pattern exhausted" + sponge.absorb(vec![field_from_u64(43)]); + } } From 5b04a2752cb5ee811a1edbd36af5b08b291350cf Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 14:35:52 +0100 Subject: [PATCH 06/10] fix to 48 words max --- circuits/crates/libs/safe/src/lib.nr | 79 ++++++++++++++++++++------ crates/safe/src/lib.rs | 84 ++++++++++++++++++++-------- 2 files changed, 124 insertions(+), 39 deletions(-) diff --git a/circuits/crates/libs/safe/src/lib.nr b/circuits/crates/libs/safe/src/lib.nr index e25a80f1dc..ba359d2b1c 100644 --- a/circuits/crates/libs/safe/src/lib.nr +++ b/circuits/crates/libs/safe/src/lib.nr @@ -287,9 +287,17 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) } // Step 2: Serialize to byte string and append domain separator (following SAFE spec 2.3). - // Create a fixed-size array for SHA256 input (max 256 bytes should be enough). - let mut input_bytes = [0; 256]; - let mut byte_count = 0; + // Buffer is 256 bytes: max 192 bytes for IO pattern (48 words) + 64 bytes for domain separator. + // Note: We must use a fixed-size array because Noir's keccak256 requires [u8; N], not Vec. + let max_io_pattern_bytes: u32 = 192; // 256 - 64 (domain separator) + let io_pattern_bytes = word_count * 4; + assert( + io_pattern_bytes <= max_io_pattern_bytes, + "IO pattern too large: max 48 aggregated words supported", + ); + + let mut input_bytes = [0u8; 256]; + let mut byte_count: u32 = 0; // Serialize encoded words to bytes (big-endian as per SAFE spec). // Note: Noir requires compile-time loop bounds, so we iterate over L (the array size) @@ -297,7 +305,7 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) // process valid encoded words. This is safe because word_count <= L always holds // (we can have at most L encoded words from L input operations). for i in 0..L { - if (i < word_count) & (byte_count + 4 <= 256) { + if i < word_count { let word = encoded_words[i]; input_bytes[byte_count] = (word >> 24) as u8; input_bytes[byte_count + 1] = (word >> 16) as u8; @@ -307,17 +315,16 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; 64]) } } - // Append domain separator. + // Append full 64-byte domain separator. for i in 0..64 { - if byte_count < 256 { - input_bytes[byte_count] = domain_separator[i]; - byte_count += 1; - } + input_bytes[byte_count] = domain_separator[i]; + byte_count += 1; } - // Step 3: Hash with Keccak256 and truncate to 128 bits (following SAFE spec 2.3). - // Note: Noir uses Keccak256 which is equivalent to SHA3-256 for this purpose. - let hash_bytes = keccak256(input_bytes, byte_count as u32); + // Step 3: Hash with Keccak-256 and truncate to 128 bits. + // Note: The SAFE spec uses SHA3-256, but we use Keccak-256 for Noir compatibility. + // Keccak-256 differs from SHA3-256 in padding, but both provide equivalent security. + let hash_bytes = keccak256(input_bytes, byte_count); // Convert first 128 bits (16 bytes) to field element. let mut tag_value: Field = 0; @@ -699,10 +706,10 @@ fn test_large_io_pattern() { 0, 0, 0, 0, 0, 0, ]; - // Create pattern with 64 alternating ABSORB(1) and SQUEEZE(1) operations - // so we get 64 words = 256 bytes - let mut io_pattern = [0u32; 64]; - for i in 0..64 { + // Create pattern with 48 alternating ABSORB(1) and SQUEEZE(1) operations + // This is the maximum supported (48 words * 4 bytes = 192 bytes, leaving 64 for domain separator) + let mut io_pattern = [0u32; 48]; + for i in 0..48 { if i % 2 == 0 { io_pattern[i] = ABSORB_FLAG | 1; // ABSORB(1) } else { @@ -713,3 +720,43 @@ fn test_large_io_pattern() { let tag = compute_tag(io_pattern, domain_separator); assert(tag != 0); } + +#[test] +fn test_domain_separator_not_truncated() { + // This test verifies that the domain separator is always included in the tag computation, + // even for large IO patterns. If the domain separator were truncated, different domain + // separators would produce the same tag for large patterns. + + let domain_separator_a = [ + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, 0x41, + 0x41, 0x41, 0x41, 0x41, + ]; // All 'A's + + let domain_separator_b = [ + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, + 0x42, 0x42, 0x42, 0x42, + ]; // All 'B's + + // Create pattern with 48 alternating operations (max supported: 192 bytes of IO pattern) + let mut io_pattern = [0u32; 48]; + for i in 0..48 { + if i % 2 == 0 { + io_pattern[i] = ABSORB_FLAG | 1; + } else { + io_pattern[i] = SQUEEZE_FLAG | 1; + } + } + + let tag_a = compute_tag(io_pattern, domain_separator_a); + let tag_b = compute_tag(io_pattern, domain_separator_b); + + // Tags MUST be different because domain separators are different. + // If they were the same, it would mean the domain separator was truncated/ignored. + assert(tag_a != tag_b, "Domain separator must affect tag even for large IO patterns"); +} diff --git a/crates/safe/src/lib.rs b/crates/safe/src/lib.rs index b75a5e2ab4..fe6ca161a8 100644 --- a/crates/safe/src/lib.rs +++ b/crates/safe/src/lib.rs @@ -310,33 +310,38 @@ pub fn compute_tag(io_pattern: [u32; L], domain_separator: [u8; } // Step 2: Serialize to byte string and append domain separator (following SAFE spec 2.3). - // Create a fixed-size array for Keccak-256 input (max 256 bytes should be enough). - let mut input_bytes = [0; 256]; + // Buffer is 256 bytes: max 192 bytes for IO pattern (48 words) + 64 bytes for domain separator. + // Note: We use a fixed-size array to match Noir's implementation exactly. + let max_io_pattern_bytes = 192; // 256 - 64 (domain separator) + let io_pattern_bytes = word_count * 4; + assert!( + io_pattern_bytes <= max_io_pattern_bytes, + "IO pattern too large: max 48 aggregated words supported" + ); + + let mut input_bytes = [0u8; 256]; let mut byte_count = 0; // Serialize encoded words to bytes (big-endian as per SAFE spec). for word in encoded_words.iter().take(word_count) { - if byte_count + 4 <= 256 { - let word = *word; - input_bytes[byte_count] = (word >> 24) as u8; - input_bytes[byte_count + 1] = (word >> 16) as u8; - input_bytes[byte_count + 2] = (word >> 8) as u8; - input_bytes[byte_count + 3] = word as u8; - byte_count += 4; - } + let word = *word; + input_bytes[byte_count] = (word >> 24) as u8; + input_bytes[byte_count + 1] = (word >> 16) as u8; + input_bytes[byte_count + 2] = (word >> 8) as u8; + input_bytes[byte_count + 3] = word as u8; + byte_count += 4; } - // Append domain separator. - for i in 0..64 { - if byte_count < 256 { - input_bytes[byte_count] = domain_separator[i]; - byte_count += 1; - } + // Append full 64-byte domain separator. + for ds_val in domain_separator { + input_bytes[byte_count] = ds_val; + byte_count += 1; } - // Step 3: Hash with Keccak256 and truncate to 128 bits. - // Note: Using Keccak-256 (Ethereum's hash) for consistency with the Rust implementation. - // Keccak-256 differs from FIPS SHA3-256 in padding but is used here for cross-implementation compatibility. + // Step 3: Hash with Keccak-256 and truncate to 128 bits. + // Note: Using Keccak-256 (Ethereum's hash) for compatibility with Noir's keccak256. + // The SAFE spec mentions SHA3-256, but Keccak-256 is used here for cross-implementation consistency. + // Hash only the first byte_count bytes to match Noir's keccak256(input_bytes, byte_count). let mut hasher = Keccak256::new(); hasher.update(&input_bytes[..byte_count]); let hash_bytes = hasher.finalize(); @@ -630,10 +635,10 @@ mod tests { fn test_large_io_pattern() { let domain_separator = test_domain_separator(); - // Create pattern with 64 alternating ABSORB(1) and SQUEEZE(1) operations - // so we get 64 words = 256 bytes - let mut io_pattern = [0u32; 64]; - for i in 0..64 { + // Create pattern with 48 alternating ABSORB(1) and SQUEEZE(1) operations + // This is the maximum supported (48 words * 4 bytes = 192 bytes, leaving 64 for domain separator) + let mut io_pattern = [0u32; 48]; + for i in 0..48 { if i % 2 == 0 { io_pattern[i] = ABSORB_FLAG | 1; // ABSORB(1) } else { @@ -644,7 +649,39 @@ mod tests { let tag = compute_tag(io_pattern, domain_separator); assert!(tag != Field::zero()); } + + #[test] + fn test_domain_separator_not_truncated() { + // This test verifies that the domain separator is always included in the tag computation, + // even for large IO patterns. If the domain separator were truncated, different domain + // separators would produce the same tag for large patterns. + + let domain_separator_a = [0x41u8; 64]; // All 'A's + let domain_separator_b = [0x42u8; 64]; // All 'B's + + // Create pattern with 48 alternating operations (max supported: 192 bytes of IO pattern) + let mut io_pattern = [0u32; 48]; + for i in 0..48 { + if i % 2 == 0 { + io_pattern[i] = ABSORB_FLAG | 1; + } else { + io_pattern[i] = SQUEEZE_FLAG | 1; + } + } + + let tag_a = compute_tag(io_pattern, domain_separator_a); + let tag_b = compute_tag(io_pattern, domain_separator_b); + + // Tags MUST be different because domain separators are different. + // If they were the same, it would mean the domain separator was truncated/ignored. + assert_ne!( + tag_a, tag_b, + "Domain separator must affect tag even for large IO patterns" + ); + } + #[test] + #[should_panic(expected = "IO pattern exhausted")] fn test_squeeze_io_pattern_exhausted() { // This test verifies that squeeze properly checks for IO pattern exhaustion // and provides a clear error message instead of an index-out-of-bounds panic. @@ -662,6 +699,7 @@ mod tests { } #[test] + #[should_panic(expected = "IO pattern exhausted")] fn test_absorb_io_pattern_exhausted() { // This test verifies that absorb properly checks for IO pattern exhaustion. From f677c43ee0eff307765bfce29a28ba5878091607 Mon Sep 17 00:00:00 2001 From: 0xjei Date: Thu, 4 Dec 2025 14:42:55 +0100 Subject: [PATCH 07/10] add missing safe crate in docker --- crates/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/Dockerfile b/crates/Dockerfile index 540bc53bdc..482f5fd2bf 100644 --- a/crates/Dockerfile +++ b/crates/Dockerfile @@ -65,6 +65,7 @@ COPY crates/multithread/Cargo.toml ./multithread/Cargo.toml COPY crates/net/Cargo.toml ./net/Cargo.toml COPY crates/program-server/Cargo.toml ./program-server/Cargo.toml COPY crates/request/Cargo.toml ./request/Cargo.toml +COPY crates/safe/Cargo.toml ./safe/Cargo.toml COPY crates/sdk/Cargo.toml ./sdk/Cargo.toml COPY crates/sortition/Cargo.toml ./sortition/Cargo.toml COPY crates/support-scripts/Cargo.toml ./support-scripts/Cargo.toml From 3efef8b6509ea7c4f474b21700262de81babe73a Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 4 Dec 2025 15:37:51 +0000 Subject: [PATCH 08/10] chore: update crisp verifier circuit --- .../contracts/CRISPVerifier.sol | 110 +++++++++--------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol b/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol index a580ff9ddd..ddd2110b88 100644 --- a/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol +++ b/examples/CRISP/packages/crisp-contracts/contracts/CRISPVerifier.sol @@ -8,7 +8,7 @@ pragma solidity >=0.8.21; uint256 constant N = 262144; uint256 constant LOG_N = 18; uint256 constant NUMBER_OF_PUBLIC_INPUTS = 2066; -uint256 constant VK_HASH = 0x063c39e8bdbf5641b7e7da911a54f2e808b084120de6fec9cd1223ce6ef0da85; +uint256 constant VK_HASH = 0x1928c21699c7e0ea8193b35638ffffb6e95f2a8d572fa86a4f16f9df74c379fd; library HonkVerificationKey { function loadVerificationKey() internal pure returns (Honk.VerificationKey memory) { Honk.VerificationKey memory vk = Honk.VerificationKey({ @@ -16,116 +16,116 @@ library HonkVerificationKey { logCircuitSize: uint256(18), publicInputsSize: uint256(2066), ql: Honk.G1Point({ - x: uint256(0x246e058a5ff7d94b72e2d8f1d273265644d4795cfb2a427bab835b180a5509fb), - y: uint256(0x221fc7eff2803fca12db033baa14646582610126778228307901d0db50748c5e) + x: uint256(0x04e9b837eb4859cf6d88b9cffdc3cb147c3924ba2c2a70d822f587ffdc922c7d), + y: uint256(0x1a985ef8bd818ab1a4465f284ae97817916527b7ba8fb601b02c336fdf677523) }), qr: Honk.G1Point({ - x: uint256(0x08011cd886ba5fc7884098df37d58eecfa75f404fead14dbbe1708d9fba3a702), - y: uint256(0x0a76efd12734f504e19bf349e90d20fc19400dd97c238195c67b118c14a35dc9) + x: uint256(0x0025964752de7d9ce88a9105961e89c5d54508867be3d862ccd5d7df3282f71c), + y: uint256(0x262f9a1cbe392f2d7802bdb647edce2906220ed344b999a15d453c4cbc72fff1) }), qo: Honk.G1Point({ - x: uint256(0x03250bf94bfb59a296a7227fe1f7a6873ca063c4c581a7b725d6ab685c53440c), - y: uint256(0x0663423bdefc067e3f531e2760b73340a76365a28d7753db11af572e71a7f395) + x: uint256(0x199b3105035a711288994d4a46db5df5ce7f77173d9b39e2c6c7dee7a045ab2d), + y: uint256(0x2384d70aa447ed87273b69abef7e65cf56e9e1f1781cc5d47627971cd8a20a6d) }), q4: Honk.G1Point({ - x: uint256(0x1db2b3316d4bba2799898fc14a80ee1a12fd9af0b6cf261b86a0e725737dd091), - y: uint256(0x0988609ce0b437fd1d5586ad60e05cc16f891e702fda15bfbbd79cf6ea306582) + x: uint256(0x2040a147ace2b079e7da66f09c4a8a790c25155e9112f6d359edb1e7a1c14bd9), + y: uint256(0x01f4c95f4a958654488f52a471557b7a9248d688eba93dd73e7b42b5f65d395b) }), qm: Honk.G1Point({ - x: uint256(0x1a1ddaefb0b108d39e35111ea8cab1f89d6fb1e6ca3d4d28ae1b6ad3acdb4c4f), - y: uint256(0x110c07c930f3fd905e11cf35db1b4e8d267f016d94ffc9f0b3833dd27641fd4d) + x: uint256(0x249e1e9e7b76099e6ae659cff1012d9ea22fa00702d2c201588ef753a39027a1), + y: uint256(0x0fe03c280de4af4b50cb49b818a4ceb6b16d71123933670a8fb97db1f91f1cb9) }), qc: Honk.G1Point({ - x: uint256(0x24a1d61ec991a93d44d50980c43acae82f4013d66126daf568a854cbc88c5a88), - y: uint256(0x20327007b5abc8edee984da35d5c8b8f8565165d4708436627ba34a90ded8431) + x: uint256(0x0b87629543c1951caf6237bd2c13fa9e8d0d9dbcf683448e36b83efdb37ba822), + y: uint256(0x2b160fd59507419036359b4dad926d33bb7212ebfa265020a16d9860b77a4326) }), qLookup: Honk.G1Point({ - x: uint256(0x205057a47479c3744023a35ca3d08d79c3499d9af48e264ecb31823713bbbca8), - y: uint256(0x19b2541dcaae69df644bec1bb8ce13455719c73fcadac3763d81a6b1c70560f3) + x: uint256(0x056cab9e0cc90d6187f1504470e987376fb9d964f5e69f79d3dc50a3aba8b070), + y: uint256(0x2a0690805846bbbba0fe533d4ec11edc41678b77983bcba8f10a71ece5298fee) }), qArith: Honk.G1Point({ - x: uint256(0x06294dfc20c077df81e702e90386bca302a58eb9a8e42d116e88b4bcc7605f67), - y: uint256(0x06a35b9ed28d64b5c79f08a6ed5698a501e9a45e71cd515d3a89455ea711489d) + x: uint256(0x1be80044ada3428b16f44b8deb186189654facebd948778500a35ff70d539f80), + y: uint256(0x16c4b4c9d038147adf1ee72f02840837732841340f45216ee0c7b26c44e6c598) }), qDeltaRange: Honk.G1Point({ - x: uint256(0x21cb2b3ddedbbb4529c61e4586aa3913a975644620c112f1c892f1b0930633d8), - y: uint256(0x2b2d51a0dc545770c23b83eceb33b488e8217ed4af94bbecd3233520f1a09e36) + x: uint256(0x0e4f6b5ad66313663f1f68b7bdbd37fd9ae4fb0a30a7a26bc09b3624340acb94), + y: uint256(0x0f76c694a09644e9b7e9ab35d64c7fd37d6299e442b7d2c3ff159e04ede31b7d) }), qElliptic: Honk.G1Point({ - x: uint256(0x0b33cc711a57329b7a8532a68b487c08afc55c25843549a9f80a1a954c946c62), - y: uint256(0x068fcecd26322a8e6c9699ef0aaa0f2a0d309c5cb2bca51f14287d121c4694fa) + x: uint256(0x1ba257d8ac16fc3c4b3694a6f31b1c5f5ac6beb4e9eb38297180c454f66ec30e), + y: uint256(0x2ff820c29d349cff8bac8e6a969c845b9ceb741103c1d07ac891d513a6bd0901) }), qMemory: Honk.G1Point({ - x: uint256(0x12924d915fd97341729ab4a38d431bdb22555119117c62a2ee78941855604328), - y: uint256(0x253f6f540fea2a3f6d3b9f9d24d142b1e942c383fe5b45aea1306992c99fb094) + x: uint256(0x160f9a682f385226627287244e1b6fffd5ad050d682304707917ff5999819b1b), + y: uint256(0x1e9891b752101a03dfaf1bbc35822d7bb037c71ba669c063374d84f4bb47b56a) }), qNnf: Honk.G1Point({ - x: uint256(0x23916c7db7b5ec14f6c4a90328e16fad9e03f0d36a7c22750a82bdb74925d008), - y: uint256(0x0d2d9f771a28305ef54fd924dbab1b7fd294e39f2e64b1884fec624a151a78cc) + x: uint256(0x1f0fa4f6a3a36136a0e0ebd90abd6ac2e2574079c13c04a6371dc01b0ea5a9c7), + y: uint256(0x0cc5647711047db7fff873f47fa26d7d3d67c07ec5917a306646b5cfcd8e87e1) }), qPoseidon2External: Honk.G1Point({ - x: uint256(0x277f77603f20d5d41e1149588bc2942ba2938ac9d9444622709ed99f1e207a4e), - y: uint256(0x214d57941547e7acf0f4db0ba32b3a80ccf3bc7195673d43a58e36b735df60e6) + x: uint256(0x127934fc35e05404db26f9f8c6822686cde6713bbd81c53e3662078f16db8f85), + y: uint256(0x154ef0a5e35291de0be60d5f445a17f8e76cd641f7d131790b4ab2009c075705) }), qPoseidon2Internal: Honk.G1Point({ - x: uint256(0x09a0b5fbddaa5b0077595c630a0403f3de09ae31815145dae74c04ac7013cdd2), - y: uint256(0x1c8f6eebb7992fbbd7b84ef11585538f876749bedd8e61b70fd86a22f3f0c47d) + x: uint256(0x21c3559c733cc1fb3e84d2b4a5e5147c25143a7a846505773bf9fade3c363004), + y: uint256(0x1deac820a1292780994b6b49e4ec046f0674a1f5e1e09fa72c4dfe863a8da4b7) }), s1: Honk.G1Point({ - x: uint256(0x20dbaae9012430dfd99a8e15c41e5ce5e33abe943d4e6bcae2568196a6e2bf16), - y: uint256(0x164b7d011804aa4dbb49c6d96cb28e3b16af2c3532c2407f0ce12c52be461956) + x: uint256(0x0485a119a5e9393108425df7916013414c2397f9b633ae60a1dc852ece837a96), + y: uint256(0x04ec4a89caab0203e7d3ed4876bf3ff1a5fad1ffb11c44ea4fb722a52147d3c8) }), s2: Honk.G1Point({ - x: uint256(0x1da71ffed08aa0fa2b5a58ab3fc27ffb7e2cf131547842780a24963048e2d5aa), - y: uint256(0x00ec65400c01cf5af83108cda478482094334d2ba594e370e47bb0bef1e30491) + x: uint256(0x013f7d7e0f3d0416b38be435440a072e8f55964c09a8126276be2fc6f4fd6f9f), + y: uint256(0x1595883ac9ceeb4fad5fe4ec277c354f87b25b8f4737ef1f4aab5ce1efa7d4ab) }), s3: Honk.G1Point({ - x: uint256(0x076d8ef37e5f6a8a967907da958538f3302bc2360c31614a5e1832a1f3ceb421), - y: uint256(0x1c83f5d0dda79e6f11f5969084b6dcddd1432a426dca69f83a1b1cee21c9e208) + x: uint256(0x1edfd7bd7cb7d31d82d7e5711cf8144e54def3b2a868882ea55a9c9bb67ec8ab), + y: uint256(0x0ff877cf80a59cefdda50fd9846b591c74cfecb8e72f9b2c1c2a9e6ea2e15ef3) }), s4: Honk.G1Point({ - x: uint256(0x0fbf35404645e90f9aa8a9a37507098b6a6332c214ba9b9d65f6e05a5ef82b26), - y: uint256(0x0a6e47c369491fe21fc51c7b254dbaad6a9b6fba5f5fc1e1d66b11a05aca1eb5) + x: uint256(0x20487f6814f872cef8047d4e2a8ceebfcdb02173231ef2e3b5fdb81a7a4d02cd), + y: uint256(0x14c15fece64e528803e437e2b6ba8557cf484c02b43589880178ef3a7035eb84) }), t1: Honk.G1Point({ - x: uint256(0x08a5ba822823e5f21f5585f7d90f070aaad388561d817362c819850cccf82580), - y: uint256(0x2d296fb3ec6c283d6f822a7e7f9edbe350516a4f9cba53be9dc8ac6240d0559c) + x: uint256(0x1f16b037f0b4c96ea2a30a118a44e139881c0db8a4d6c9fde7db5c1c1738e61f), + y: uint256(0x00c7781bda34afc32dedfb0d0f6b16c987c83375da000e180b44ad25685fc2ae) }), t2: Honk.G1Point({ - x: uint256(0x201b4ffc4068dd22cc3a99a1ef5bc10e2be7841ed934ad5ea5247f992687c29b), - y: uint256(0x28351d4eacb149a545035052b1b2081b7e8c3ffa751c5bc31483b653f95cb6ca) + x: uint256(0x29345f914a28707887bee191c3a928191de584827a6d1a78ccce1d7629ca9dc0), + y: uint256(0x1920cebd0b33ac9713424e3bc03d53d79dc72f6afc24c90e56593094a213444c) }), t3: Honk.G1Point({ - x: uint256(0x0d1a271b6b84d9a2d8953885c3b2d13d10aa96a483eeb4c7a41d65c19d69d638), - y: uint256(0x2a40aaa4bc03f75cbc60cc97a07b3e8885d4c99101b026f18219c82ee71443c4) + x: uint256(0x261c990958bc2ef77a45467d9639ab2c68cf787ff7bce55ce3074dfdaedc8f8f), + y: uint256(0x23c1c05424a40360a61e46f4deab04988a6f5b71dda351e0da608cff1f332ee0) }), t4: Honk.G1Point({ - x: uint256(0x18216d5e69c40817c81feefd02de1aa548f7bf9d9ce4d671e96b22f368709ed5), - y: uint256(0x1e5e5f5acbdcd05a0ebffacea7a5426da9ec26a79cbb95692c9e9a499ff0155a) + x: uint256(0x2b651d2fd644b2972d72ec439dc69d3339d0b052a296bfc48c6a08396aaca078), + y: uint256(0x2d7e8c1ecb92e2490049b50efc811df63f1ca97e58d5e82852dbec0c29715d71) }), id1: Honk.G1Point({ - x: uint256(0x0fcc20437825949a4e696438aea909760b3db2d273bf5b17f5fbfe3a70f036fb), - y: uint256(0x210603917536ed64abdd44a319ea9341a3f30a789789230a7e9cf6214b4bce7e) + x: uint256(0x0fd29f94279ade1203624e978608aa8d4cdd56a7b2f83291f86e990871748c9d), + y: uint256(0x0eba8e7e417161e496fa362877b6463f892dced77d5d486611447ba378a5c7a6) }), id2: Honk.G1Point({ - x: uint256(0x1ecfabef38de6cd4881366a1b917f8ff9f99a024f4b5087c0c0f566d2c3ea36b), - y: uint256(0x1dd57e4a465cfe220a85cafe9c22d18ea7500a73930a72a173882333424658fd) + x: uint256(0x23cfb55bc0f52f50da0a01ca6fefad0af11ca20e6ad94e002afaab2b57f4e7c7), + y: uint256(0x10c9ad32e742902c5d75712b94a91faec6a2a0acded64159ca96f7cf3314b081) }), id3: Honk.G1Point({ - x: uint256(0x0c44be4c332daa7e6c1989b113af431bbbd1177a9e3b296182a038c4898c58b8), - y: uint256(0x07c0db6714c5c1aae2d785402c39e9d18b09e367033cfcb2aecbb6991da057b1) + x: uint256(0x1031f8cc78cc9d0e75484eef90799dafa081faa2b2a15288f7e4b6e4df21642f), + y: uint256(0x180f20c9501bae17e5ba23fada7a49bb85004de66bb62df79cb2558690e92ef7) }), id4: Honk.G1Point({ - x: uint256(0x24443689861aa88435f69edc9782024e9207b659aab7df6694958fb85f6d4d5a), - y: uint256(0x19fa9eb82c3e56a745b7900324b2a205ab358a1dac89439011fc099408d21c96) + x: uint256(0x284a583a1744d54bd7abd4b196d05620f8d17901400c8d203b45dd26e9bae43e), + y: uint256(0x0252954267d54f35fc76a5e93f5350d55c3cb488e2817a6dc191ba40999a4800) }), lagrangeFirst: Honk.G1Point({ x: uint256(0x0000000000000000000000000000000000000000000000000000000000000001), y: uint256(0x0000000000000000000000000000000000000000000000000000000000000002) }), lagrangeLast: Honk.G1Point({ - x: uint256(0x11dc324ee0b909fa5ec049c5832d17f8ba51b9f2990874ea77384948fcaa6800), - y: uint256(0x243280a04dd209d1241eb79b76c07ccc3bf501dd5b79fa218d4669cfb30316f8) + x: uint256(0x205b95c6359cfde0becec8f8b0fcd3a7a24b84725ea23c684f4f04d1add85998), + y: uint256(0x1db6e6ea5150485a98eebd181652c378c6a799a1d38553dc70bfddd39251c03b) }) }); return vk; From 5f210d7ce5fc3a9d95a572b27a93e0890289afd8 Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 4 Dec 2025 15:53:45 +0000 Subject: [PATCH 09/10] chore(crisp): publish version 0.4.2 - Updated @crisp-e3/sdk to 0.4.2 - Updated @crisp-e3/contracts to 0.4.2 - Updated @crisp-e3/zk-inputs to 0.4.2 - Published to npm --- examples/CRISP/client/package.json | 2 +- examples/CRISP/packages/crisp-contracts/package.json | 2 +- examples/CRISP/packages/crisp-sdk/package.json | 2 +- examples/CRISP/packages/crisp-zk-inputs/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/CRISP/client/package.json b/examples/CRISP/client/package.json index 128fec51ae..20fcf48c5b 100644 --- a/examples/CRISP/client/package.json +++ b/examples/CRISP/client/package.json @@ -18,7 +18,7 @@ "deploy": "gh-pages -d dist" }, "dependencies": { - "@crisp-e3/sdk": "0.4.1", + "@crisp-e3/sdk": "0.4.2", "@emotion/babel-plugin": "^11.11.0", "@emotion/react": "^11.11.4", "@phosphor-icons/react": "^2.1.4", diff --git a/examples/CRISP/packages/crisp-contracts/package.json b/examples/CRISP/packages/crisp-contracts/package.json index 3134d06c6d..65dd8a3dc6 100644 --- a/examples/CRISP/packages/crisp-contracts/package.json +++ b/examples/CRISP/packages/crisp-contracts/package.json @@ -1,6 +1,6 @@ { "name": "@crisp-e3/contracts", - "version": "0.4.1", + "version": "0.4.2", "type": "module", "files": [ "contracts", diff --git a/examples/CRISP/packages/crisp-sdk/package.json b/examples/CRISP/packages/crisp-sdk/package.json index f64cfd1330..374fef3503 100644 --- a/examples/CRISP/packages/crisp-sdk/package.json +++ b/examples/CRISP/packages/crisp-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@crisp-e3/sdk", - "version": "0.4.1", + "version": "0.4.2", "type": "module", "author": { "name": "gnosisguild", diff --git a/examples/CRISP/packages/crisp-zk-inputs/package.json b/examples/CRISP/packages/crisp-zk-inputs/package.json index 0c984be69c..2414e727e5 100644 --- a/examples/CRISP/packages/crisp-zk-inputs/package.json +++ b/examples/CRISP/packages/crisp-zk-inputs/package.json @@ -2,7 +2,7 @@ "name": "@crisp-e3/zk-inputs", "type": "module", "description": "Core logic to pre-compute CRISP ZK inputs (WASM/JavaScript bindings).", - "version": "0.4.1", + "version": "0.4.2", "license": "LGPL-3.0-only", "repository": { "type": "git", From ae85ffe1b05481502c7ef498fbd24768450d07ad Mon Sep 17 00:00:00 2001 From: Cedoor Date: Thu, 4 Dec 2025 15:58:04 +0000 Subject: [PATCH 10/10] fix: add pnpm-lock.yaml to git operations --- examples/CRISP/scripts/publish.ts | 16 ++++++++++++++-- pnpm-lock.yaml | 2 +- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/examples/CRISP/scripts/publish.ts b/examples/CRISP/scripts/publish.ts index 1c46d30fc7..480af1d249 100644 --- a/examples/CRISP/scripts/publish.ts +++ b/examples/CRISP/scripts/publish.ts @@ -239,10 +239,22 @@ class CRISPPublisher { console.log('\nšŸ“ Performing git operations...') try { - // Add all changes + // Get git repository root + const gitRoot = execSync('git rev-parse --show-toplevel', { + cwd: this.crispDir, + encoding: 'utf-8', + }).trim() + + // Add all changes from CRISP directory console.log(' Adding changes...') execSync('git add .', { cwd: this.crispDir }) + // Explicitly add the lock file from root + const lockFilePath = join(gitRoot, 'pnpm-lock.yaml') + if (existsSync(lockFilePath)) { + execSync(`git add ${lockFilePath}`, { cwd: gitRoot }) + } + // Create commit message const commitMessage = `chore(crisp): publish version ${this.newVersion} @@ -254,7 +266,7 @@ class CRISPPublisher { // Commit changes console.log(' Committing changes...') execSync(`git commit -m "${commitMessage}"`, { - cwd: this.crispDir, + cwd: gitRoot, stdio: 'pipe', }) console.log(` āœ“ Committed with message: "chore(crisp): publish version ${this.newVersion}"`) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 085db3c2ca..a25b71ab11 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,7 +143,7 @@ importers: examples/CRISP/client: dependencies: '@crisp-e3/sdk': - specifier: 0.4.1 + specifier: 0.4.2 version: link:../packages/crisp-sdk '@emotion/babel-plugin': specifier: ^11.11.0