diff --git a/crates/frontend/src/circuits/hash_based_sig/chain.rs b/crates/frontend/src/circuits/hash_based_sig/chain.rs index d4395ce8a..c6ace5daf 100644 --- a/crates/frontend/src/circuits/hash_based_sig/chain.rs +++ b/crates/frontend/src/circuits/hash_based_sig/chain.rs @@ -1,8 +1,8 @@ use binius_core::Word; -use super::tweak::ChainTweak; +use super::tweak::verify_chain_tweak; use crate::{ - circuits::multiplexer::multi_wire_multiplex, + circuits::{keccak::Keccak, multiplexer::multi_wire_multiplex}, compiler::{CircuitBuilder, Wire}, }; @@ -41,7 +41,7 @@ use crate::{ /// /// # Returns /// -/// A vector of `ChainTweak` hashers that need to be populated with witness values. +/// A vector of `Keccak` hashers that need to be populated with witness values. /// The number of hashers equals the maximum chain length supported. #[allow(clippy::too_many_arguments)] pub fn verify_chain( @@ -53,7 +53,7 @@ pub fn verify_chain( coordinate: Wire, max_chain_len: u64, end_hash: [Wire; 4], -) -> Vec { +) -> Vec { assert!( param_len <= param.len() * 8, "param_len {} exceeds maximum capacity {} of param wires", @@ -74,7 +74,7 @@ pub fn verify_chain( let (position_plus_one, _) = builder.iadd_cin_cout(position, one, zero); let next_hash = std::array::from_fn(|_| builder.add_witness()); - let hasher = ChainTweak::new( + let keccak = verify_chain_tweak( builder, param.to_vec(), param_len, @@ -84,7 +84,7 @@ pub fn verify_chain( next_hash, ); - hashers.push(hasher); + hashers.push(keccak); // Conditionally select the hash based on whether position + 1 < max_chain_len // If position + 1 < max_chain_len, use next_hash, otherwise keep current_hash @@ -106,7 +106,7 @@ mod tests { use proptest::{prelude::*, strategy::Just}; use sha3::{Digest, Keccak256}; - use super::*; + use super::{super::tweak::build_chain_tweak, *}; use crate::{constraint_verifier::verify_constraints, util::pack_bytes_into_wires_le}; proptest! { @@ -147,38 +147,41 @@ mod tests { w[chain_index] = Word::from_u64(chain_index_val); w[coordinate] = Word::from_u64(coordinate_val); + // Populate param wires (they're reused for all hashers) + pack_bytes_into_wires_le(&mut w, ¶m, ¶m_bytes); + + // Track current hash through the chain let mut current_hash: [u8; 32] = signature_chunk_bytes; - for (step, hasher) in hashers.iter().enumerate() { - let hash_position = step as u64 + coordinate_val + 1; - hasher.populate_param(&mut w, ¶m_bytes); - hasher.populate_hash(&mut w, ¤t_hash); - hasher.populate_chain_index(&mut w, chain_index_val); - hasher.populate_position(&mut w, hash_position); + for (step, keccak) in hashers.iter().enumerate() { + // Calculate position for this step + let position = step as u64 + coordinate_val; + let position_plus_one = position + 1; - let message = ChainTweak::build_message( + // Build the message for this hash using current_hash + let message = build_chain_tweak( ¶m_bytes, ¤t_hash, chain_index_val, - hash_position, + position_plus_one, ); - hasher.populate_message(&mut w, &message); - // The circuit always computes the hash, even if it won't be used in the final result - // This is because the constraint system verifies all hash computations + // Populate the Keccak circuit + keccak.populate_message(&mut w, &message); + + // Compute the hash let digest: [u8; 32] = Keccak256::digest(&message).into(); - hasher.populate_digest(&mut w, digest); + keccak.populate_digest(&mut w, digest); - // Only update current_hash if this hash is actually selected by the multiplexer - // (when hash_position < max_chain_len) - if hash_position < max_chain_len { + // Update current_hash for next iteration if this hash is selected + // The multiplexer in the circuit selects next_hash when position_plus_one < max_chain_len + if position_plus_one < max_chain_len { current_hash = digest; } } pack_bytes_into_wires_le(&mut w, &end_hash, ¤t_hash); pack_bytes_into_wires_le(&mut w, &signature_chunk, &signature_chunk_bytes); - pack_bytes_into_wires_le(&mut w, ¶m, ¶m_bytes); circuit.populate_wire_witness(&mut w).unwrap(); let cs = circuit.constraint_system(); diff --git a/crates/frontend/src/circuits/hash_based_sig/tweak.rs b/crates/frontend/src/circuits/hash_based_sig/tweak.rs deleted file mode 100644 index b8eb464da..000000000 --- a/crates/frontend/src/circuits/hash_based_sig/tweak.rs +++ /dev/null @@ -1,421 +0,0 @@ -use binius_core::Word; - -use crate::{ - circuits::{ - concat::{Concat, Term}, - keccak::Keccak, - }, - compiler::{CircuitBuilder, Wire, circuit::WitnessFiller}, - util::pack_bytes_into_wires_le, -}; - -/// A circuit to verify chain-specific Keccak-256 tweaking for hash-based -/// signatures. -/// -/// This circuit computes Keccak-256 of a message that's been tweaked with -/// chain-specific parameters: `Keccak256(param || 0x00 || hash || chain_index || position)` -pub struct ChainTweak { - /// The Keccak-256 hasher that computes the final digest - pub keccak: Keccak, - /// The cryptographic parameter wires (padded to multiple of 8 bytes) - pub param_wires: Vec, - /// The actual parameter length in bytes (before padding) - pub param_len: usize, - /// The hash value to be tweaked (32 bytes as 4x64-bit LE-packed wires) - pub hash: [Wire; 4], - /// Index of this chain (as 64-bit value in wire) - pub chain_index: Wire, - /// Position within the chain (as 64-bit value in wire) - pub position: Wire, - /// The tweaked Keccak-256 digest (32 bytes as 4x64-bit wires) - pub digest: [Wire; 4], -} - -/// Fixed overhead in the message beyond the parameter length: -/// - 1 byte: tweak_byte -/// - 32 bytes: hash value -/// - 8 bytes: chain_index -/// - 8 bytes: position -const FIXED_MESSAGE_OVERHEAD: usize = 1 + 32 + 8 + 8; - -const CHAIN_TWEAK: u8 = 0x00; - -impl ChainTweak { - /// Creates a new chain-tweaked Keccak-256 circuit. - /// - /// # Arguments - /// - /// * `builder` - Circuit builder for constructing constraints - /// * `param_wires` - The cryptographic parameter wires - /// * `param_len` - The actual parameter length in bytes - /// * `hash` - The hash value to be tweaked (32 bytes as 4x64-bit LE-packed wires) - /// * `chain_index` - Index of this chain (as 64-bit value in wire) - /// * `position` - Position within the chain (as 64-bit value in wire) - /// * `digest` - Output: The computed Keccak-256 digest (32 bytes as 4x64-bit wires) - /// - /// # Returns - /// - /// A `ChainTweak` instance that verifies the tweaked hash. - pub fn new( - builder: &CircuitBuilder, - param_wires: Vec, - param_len: usize, - hash: [Wire; 4], - chain_index: Wire, - position: Wire, - digest: [Wire; 4], - ) -> Self { - let message_len = param_len + FIXED_MESSAGE_OVERHEAD; - let tweak_byte = builder.add_constant_64(CHAIN_TWEAK as u64); - assert_eq!(param_wires.len(), param_len.div_ceil(8)); - - // Create the message wires for Keccak (LE-packed) - let n_message_words = message_len.div_ceil(8); - let message_le: Vec = (0..n_message_words) - .map(|_| builder.add_witness()) - .collect(); - let len = builder.add_witness(); - - // Keccak digest is 25 words (full state), but we only use first 4 for 256-bit output - let keccak_digest: [Wire; 25] = std::array::from_fn(|i| { - if i < 4 { - digest[i] - } else { - builder.add_witness() - } - }); - - let keccak = Keccak::new(builder, message_len, len, keccak_digest, message_le.clone()); - - let mut terms = Vec::new(); - - let param_term = Term { - len: builder.add_constant_64(param_len as u64), - data: param_wires.clone(), - max_len: param_wires.len() * 8, - }; - terms.push(param_term); - - let tweak_term = Term { - len: builder.add_constant_64(1), - data: vec![tweak_byte], - max_len: 8, - }; - terms.push(tweak_term); - - let hash_term = Term { - len: builder.add_constant_64(32), - data: hash.to_vec(), - max_len: 32, - }; - terms.push(hash_term); - - let chain_index_term = Term { - len: builder.add_constant_64(8), - data: vec![chain_index], - max_len: 8, - }; - terms.push(chain_index_term); - - let position_term = Term { - len: builder.add_constant_64(8), - data: vec![position], - max_len: 8, - }; - terms.push(position_term); - - // Create the concatenation circuit to verify message structure - // message = param || tweak_byte || hash || chain_index || position - let _message_structure_verifier = - Concat::new(builder, message_len.next_multiple_of(8), len, message_le, terms); - - ChainTweak { - keccak, - param_wires, - param_len, - hash, - chain_index, - position, - digest, - } - } - - /// Populate the parameter wires. - pub fn populate_param(&self, w: &mut WitnessFiller, param_bytes: &[u8]) { - assert_eq!(param_bytes.len(), self.param_len); - pack_bytes_into_wires_le(w, &self.param_wires, param_bytes); - } - - /// Populate the hash wires (32 bytes as 4x64-bit LE-packed). - pub fn populate_hash(&self, w: &mut WitnessFiller, hash_bytes: &[u8; 32]) { - for (i, bytes) in hash_bytes.chunks(8).enumerate() { - let word = u64::from_le_bytes(bytes.try_into().unwrap()); - w[self.hash[i]] = Word::from_u64(word); - } - } - - /// Populate the chain index wire. - pub fn populate_chain_index(&self, w: &mut WitnessFiller, chain_index: u64) { - w[self.chain_index] = Word::from_u64(chain_index); - } - - /// Populate the position wire. - pub fn populate_position(&self, w: &mut WitnessFiller, position: u64) { - w[self.position] = Word::from_u64(position); - } - - /// Populate the message wires with the complete concatenated message. - pub fn populate_message(&self, w: &mut WitnessFiller, message: &[u8]) { - let expected_len = self.param_len + FIXED_MESSAGE_OVERHEAD; - assert_eq!( - message.len(), - expected_len, - "Message length {} doesn't match expected length {}", - message.len(), - expected_len - ); - // this populates both the message wires (shared with Concat) and the - // padded_message wires (Keccak-specific padding) - self.keccak.populate_message(w, message); - self.keccak.populate_len(w, expected_len); - } - - /// Populate the digest wires. - pub fn populate_digest(&self, w: &mut WitnessFiller, digest: [u8; 32]) { - self.keccak.populate_digest(w, digest); - } - - /// Build the tweaked message from components. - pub fn build_message( - param_bytes: &[u8], - hash_bytes: &[u8; 32], - chain_index_value: u64, - position_value: u64, - ) -> Vec { - let mut message = Vec::new(); - message.extend_from_slice(param_bytes); - message.push(CHAIN_TWEAK); - message.extend_from_slice(hash_bytes); - message.extend_from_slice(&chain_index_value.to_le_bytes()); - message.extend_from_slice(&position_value.to_le_bytes()); - message - } -} - -#[cfg(test)] -mod tests { - use sha3::{Digest, Keccak256}; - - use super::*; - use crate::{ - compiler::{CircuitBuilder, circuit::Circuit}, - constraint_verifier::verify_constraints, - }; - - /// Helper struct to encapsulate test circuit setup - struct TestCircuit { - circuit: Circuit, - tweaked_keccak: ChainTweak, - } - - impl TestCircuit { - /// Create a new test circuit with specified param length - fn new(param_len: usize) -> Self { - let builder = CircuitBuilder::new(); - - let hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout()); - let chain_index = builder.add_inout(); - let position = builder.add_inout(); - let digest: [Wire; 4] = std::array::from_fn(|_| builder.add_inout()); - - let num_param_wires = param_len.div_ceil(8); - let param_wires: Vec = - (0..num_param_wires).map(|_| builder.add_inout()).collect(); - - let tweaked_keccak = ChainTweak::new( - &builder, - param_wires, - param_len, - hash, - chain_index, - position, - digest, - ); - - let circuit = builder.build(); - - Self { - circuit, - tweaked_keccak, - } - } - - /// Populate witness and verify constraints with given test data - fn populate_and_verify( - &self, - param_bytes: &[u8], - hash_bytes: &[u8; 32], - chain_index_val: u64, - position_val: u64, - message: &[u8], - digest: [u8; 32], - ) -> Result<(), Box> { - let mut w = self.circuit.new_witness_filler(); - - self.tweaked_keccak.populate_param(&mut w, param_bytes); - self.tweaked_keccak.populate_hash(&mut w, hash_bytes); - self.tweaked_keccak - .populate_chain_index(&mut w, chain_index_val); - self.tweaked_keccak.populate_position(&mut w, position_val); - self.tweaked_keccak.populate_message(&mut w, message); - self.tweaked_keccak.populate_digest(&mut w, digest); - - self.circuit.populate_wire_witness(&mut w)?; - let cs = self.circuit.constraint_system(); - verify_constraints(cs, &w.into_value_vec())?; - Ok(()) - } - } - - #[test] - fn test_chain_tweak_basic() { - let test_circuit = TestCircuit::new(32); - - let param_bytes = b"test_parameter_32_bytes_long!!!!"; - let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; - let chain_index_val = 123u64; - let position_val = 456u64; - - let message = - ChainTweak::build_message(param_bytes, hash_bytes, chain_index_val, position_val); - - let expected_digest = Keccak256::digest(&message); - - test_circuit - .populate_and_verify( - param_bytes, - hash_bytes, - chain_index_val, - position_val, - &message, - expected_digest.into(), - ) - .unwrap(); - } - - #[test] - fn test_chain_tweak_with_18_byte_param() { - // Test with 18-byte param as per SPEC_1 and SPEC_2 - let test_circuit = TestCircuit::new(18); - - let param_bytes: &[u8; 18] = b"test_param_18bytes"; - let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; - let chain_index_val = 123u64; - let position_val = 456u64; - - let message = - ChainTweak::build_message(param_bytes, hash_bytes, chain_index_val, position_val); - - let expected_digest = Keccak256::digest(&message); - - test_circuit - .populate_and_verify( - param_bytes, - hash_bytes, - chain_index_val, - position_val, - &message, - expected_digest.into(), - ) - .unwrap(); - } - - #[test] - fn test_chain_tweak_wrong_digest() { - let test_circuit = TestCircuit::new(32); - - let param_bytes = b"test_parameter_32_bytes_long!!!!"; - let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; - let chain_index_val = 123u64; - let position_val = 456u64; - - let message = - ChainTweak::build_message(param_bytes, hash_bytes, chain_index_val, position_val); - - // Populate with WRONG digest - this should cause verification to fail - let wrong_digest = [0u8; 32]; - - let result = test_circuit.populate_and_verify( - param_bytes, - hash_bytes, - chain_index_val, - position_val, - &message, - wrong_digest, - ); - - assert!(result.is_err(), "Expected error for wrong digest"); - } - - #[test] - fn test_chain_tweak_wrong_param() { - let test_circuit = TestCircuit::new(32); - - let correct_param_bytes = b"correct_parameter_32_bytes!!!!!!"; - let wrong_param_bytes = b"wrong___parameter_32_bytes!!!!!!"; - let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; - let chain_index_val = 123u64; - let position_val = 456u64; - - // Message built with correct param - let message = ChainTweak::build_message( - correct_param_bytes, - hash_bytes, - chain_index_val, - position_val, - ); - - let expected_digest = Keccak256::digest(&message); - - // Populate with WRONG param but correct digest - let result = test_circuit.populate_and_verify( - wrong_param_bytes, - hash_bytes, - chain_index_val, - position_val, - &message, - expected_digest.into(), - ); - - assert!(result.is_err(), "Expected error for mismatched param"); - } - - #[test] - fn test_chain_tweak_wrong_chain_index() { - let test_circuit = TestCircuit::new(32); - - let param_bytes = b"test_parameter_32_bytes_long!!!!"; - let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; - let correct_chain_index = 123u64; - let wrong_chain_index = 999u64; - let position_val = 456u64; - - // Message built with correct chain_index - let message = - ChainTweak::build_message(param_bytes, hash_bytes, correct_chain_index, position_val); - - let expected_digest = Keccak256::digest(&message); - - // Populate with WRONG chain_index but correct digest - let result = test_circuit.populate_and_verify( - param_bytes, - hash_bytes, - wrong_chain_index, - position_val, - &message, - expected_digest.into(), - ); - - assert!(result.is_err(), "Expected error for mismatched chain_index"); - } -} diff --git a/crates/frontend/src/circuits/hash_based_sig/tweak/base.rs b/crates/frontend/src/circuits/hash_based_sig/tweak/base.rs new file mode 100644 index 000000000..658011fbc --- /dev/null +++ b/crates/frontend/src/circuits/hash_based_sig/tweak/base.rs @@ -0,0 +1,72 @@ +use crate::{ + circuits::{ + concat::{Concat, Term}, + keccak::Keccak, + }, + compiler::{CircuitBuilder, Wire}, +}; + +/// Verify a tweaked Keccak-256 circuit with custom terms. +/// +/// This function provides the common setup for both message and chain tweaking, +/// which both follow the pattern: `Keccak256(param || tweak_byte || additional_data)` +/// +/// # Arguments +/// * `builder` - Circuit builder for constructing constraints +/// * `param_wires` - The cryptographic parameter wires +/// * `param_len` - The actual parameter length in bytes +/// * `tweak_byte` - The tweak byte value (MESSAGE_TWEAK or CHAIN_TWEAK) +/// * `additional_terms` - Additional concatenation terms after param and tweak +/// * `total_message_len` - Total length of the concatenated message +/// * `digest` - Output digest wires +/// +/// # Returns +/// A `Keccak` instance that computes the tweaked hash +pub(super) fn verify_tweaked_keccak( + builder: &CircuitBuilder, + param_wires: Vec, + param_len: usize, + tweak_byte: u8, + additional_terms: Vec, + total_message_len: usize, + digest: [Wire; 4], +) -> Keccak { + // Create the message wires for Keccak (LE-packed) + let n_message_words = total_message_len.div_ceil(8); + let message_le: Vec = (0..n_message_words) + .map(|_| builder.add_witness()) + .collect(); + let len = builder.add_constant_64(total_message_len as u64); + + // Keccak digest is 25 words (full state), but we only use first 4 for 256-bit output + let keccak_digest: [Wire; 25] = std::array::from_fn(|i| { + if i < 4 { + digest[i] + } else { + builder.add_witness() + } + }); + + let keccak = Keccak::new(builder, total_message_len, len, keccak_digest, message_le.clone()); + + let mut terms = Vec::new(); + let param_term = Term { + len: builder.add_constant_64(param_len as u64), + data: param_wires, + max_len: param_len.div_ceil(8) * 8, + }; + terms.push(param_term); + + let tweak_wire = builder.add_constant_64(tweak_byte as u64); + let tweak_term = Term { + len: builder.add_constant_64(1), + data: vec![tweak_wire], + max_len: 8, + }; + terms.push(tweak_term); + terms.extend(additional_terms); + + let _message_structure_verifier = + Concat::new(builder, total_message_len.next_multiple_of(8), len, message_le, terms); + keccak +} diff --git a/crates/frontend/src/circuits/hash_based_sig/tweak/chain.rs b/crates/frontend/src/circuits/hash_based_sig/tweak/chain.rs new file mode 100644 index 000000000..f30394de0 --- /dev/null +++ b/crates/frontend/src/circuits/hash_based_sig/tweak/chain.rs @@ -0,0 +1,401 @@ +use super::base::verify_tweaked_keccak; +use crate::{ + circuits::{concat::Term, keccak::Keccak}, + compiler::{CircuitBuilder, Wire}, +}; + +pub const CHAIN_TWEAK: u8 = 0x00; + +/// Fixed overhead in the message beyond the parameter length: +/// - 1 byte: tweak_byte +/// - 32 bytes: hash value +/// - 8 bytes: chain_index +/// - 8 bytes: position +pub const FIXED_MESSAGE_OVERHEAD: usize = 1 + 32 + 8 + 8; + +/// A circuit that verifies a chain-tweaked Keccak-256 computation. +/// +/// This circuit verifies Keccak-256 of a message that's been tweaked with +/// chain-specific parameters: `Keccak256(param || 0x00 || hash || chain_index || position)` +/// +/// # Arguments +/// +/// * `builder` - Circuit builder for constructing constraints +/// * `param_wires` - The cryptographic parameter wires, where each wire holds 8 bytes as a 64-bit +/// LE-packed value +/// * `param_len` - The actual parameter length in bytes +/// * `hash` - The hash value to be tweaked (32 bytes as 4x64-bit LE-packed wires) +/// * `chain_index` - Index of this chain (as 64-bit LE-packed value in wire) +/// * `position` - Position within the chain (as 64-bit LE-packed value in wire) +/// * `digest` - Output: The computed Keccak-256 digest (32 bytes as 4x64-bit LE-packed wires) +/// +/// # Returns +/// +/// A `Keccak` circuit that needs to be populated with the tweaked message and digest +pub fn verify_chain_tweak( + builder: &CircuitBuilder, + param_wires: Vec, + param_len: usize, + hash: [Wire; 4], + chain_index: Wire, + position: Wire, + digest: [Wire; 4], +) -> Keccak { + let message_len = param_len + FIXED_MESSAGE_OVERHEAD; + assert_eq!(param_wires.len(), param_len.div_ceil(8)); + + // Build additional terms for hash, chain_index, and position + let mut additional_terms = Vec::new(); + + let hash_term = Term { + len: builder.add_constant_64(32), + data: hash.to_vec(), + max_len: 32, + }; + additional_terms.push(hash_term); + + let chain_index_term = Term { + len: builder.add_constant_64(8), + data: vec![chain_index], + max_len: 8, + }; + additional_terms.push(chain_index_term); + + let position_term = Term { + len: builder.add_constant_64(8), + data: vec![position], + max_len: 8, + }; + additional_terms.push(position_term); + + verify_tweaked_keccak( + builder, + param_wires, + param_len, + CHAIN_TWEAK, + additional_terms, + message_len, + digest, + ) +} + +/// Build the tweaked message from components. +/// +/// Constructs the complete message for Keccak-256 hashing by concatenating: +/// `param || 0x00 || hash || chain_index || position` +/// +/// This function is typically used when populating witness data for the +/// `verify_chain_tweak` circuit. +/// +/// # Arguments +/// +/// * `param_bytes` - The cryptographic parameter bytes +/// * `hash_bytes` - The 32-byte hash value to be tweaked +/// * `chain_index_value` - The chain index as a u64 (will be encoded as little-endian) +/// * `position_value` - The position within the chain as a u64 (will be encoded as little-endian) +/// +/// # Returns +/// +/// A vector containing the complete tweaked message ready for hashing +pub fn build_chain_tweak( + param_bytes: &[u8], + hash_bytes: &[u8; 32], + chain_index_value: u64, + position_value: u64, +) -> Vec { + let mut message = Vec::new(); + message.extend_from_slice(param_bytes); + message.push(CHAIN_TWEAK); + message.extend_from_slice(hash_bytes); + message.extend_from_slice(&chain_index_value.to_le_bytes()); + message.extend_from_slice(&position_value.to_le_bytes()); + message +} + +#[cfg(test)] +mod tests { + use binius_core::Word; + use proptest::prelude::*; + use sha3::{Digest, Keccak256}; + + use super::*; + use crate::{ + compiler::{CircuitBuilder, circuit::Circuit}, + constraint_verifier::verify_constraints, + util::pack_bytes_into_wires_le, + }; + + /// Helper struct for ChainTweak testing + struct ChainTestCircuit { + circuit: Circuit, + keccak: Keccak, + param_wires: Vec, + param_len: usize, + hash: [Wire; 4], + chain_index: Wire, + position: Wire, + } + + impl ChainTestCircuit { + fn new(param_len: usize) -> Self { + let builder = CircuitBuilder::new(); + + let hash: [Wire; 4] = std::array::from_fn(|_| builder.add_inout()); + let chain_index = builder.add_inout(); + let position = builder.add_inout(); + let digest: [Wire; 4] = std::array::from_fn(|_| builder.add_inout()); + + let num_param_wires = param_len.div_ceil(8); + let param_wires: Vec = + (0..num_param_wires).map(|_| builder.add_inout()).collect(); + + let keccak = verify_chain_tweak( + &builder, + param_wires.clone(), + param_len, + hash, + chain_index, + position, + digest, + ); + + let circuit = builder.build(); + + Self { + circuit, + keccak, + param_wires, + param_len, + hash, + chain_index, + position, + } + } + + /// Populate witness and verify constraints with given test data + fn populate_and_verify( + &self, + param_bytes: &[u8], + hash_bytes: &[u8; 32], + chain_index_val: u64, + position_val: u64, + message: &[u8], + digest: [u8; 32], + ) -> Result<(), Box> { + let mut w = self.circuit.new_witness_filler(); + + // Populate param + assert_eq!(param_bytes.len(), self.param_len); + pack_bytes_into_wires_le(&mut w, &self.param_wires, param_bytes); + + // Populate hash, chain_index, position + pack_bytes_into_wires_le(&mut w, &self.hash, hash_bytes); + w[self.chain_index] = Word::from_u64(chain_index_val); + w[self.position] = Word::from_u64(position_val); + + // Populate message for Keccak + let expected_len = self.param_len + FIXED_MESSAGE_OVERHEAD; + assert_eq!( + message.len(), + expected_len, + "Message length {} doesn't match expected length {}", + message.len(), + expected_len + ); + self.keccak.populate_message(&mut w, message); + + // Populate digest + self.keccak.populate_digest(&mut w, digest); + + self.circuit.populate_wire_witness(&mut w)?; + let cs = self.circuit.constraint_system(); + verify_constraints(cs, &w.into_value_vec())?; + Ok(()) + } + } + + #[test] + fn test_chain_tweak_basic() { + let test_circuit = ChainTestCircuit::new(32); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; + let chain_index_val = 123u64; + let position_val = 456u64; + + let message = build_chain_tweak(param_bytes, hash_bytes, chain_index_val, position_val); + + let expected_digest = Keccak256::digest(&message); + + test_circuit + .populate_and_verify( + param_bytes, + hash_bytes, + chain_index_val, + position_val, + &message, + expected_digest.into(), + ) + .unwrap(); + } + + #[test] + fn test_chain_tweak_with_18_byte_param() { + // Test with 18-byte param as per SPEC_1 and SPEC_2 + let test_circuit = ChainTestCircuit::new(18); + + let param_bytes: &[u8; 18] = b"test_param_18bytes"; + let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; + let chain_index_val = 123u64; + let position_val = 456u64; + + let message = build_chain_tweak(param_bytes, hash_bytes, chain_index_val, position_val); + + let expected_digest = Keccak256::digest(&message); + + test_circuit + .populate_and_verify( + param_bytes, + hash_bytes, + chain_index_val, + position_val, + &message, + expected_digest.into(), + ) + .unwrap(); + } + + #[test] + fn test_chain_tweak_wrong_digest() { + let test_circuit = ChainTestCircuit::new(32); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; + let chain_index_val = 123u64; + let position_val = 456u64; + + let message = build_chain_tweak(param_bytes, hash_bytes, chain_index_val, position_val); + + // Populate with WRONG digest - this should cause verification to fail + let wrong_digest = [0u8; 32]; + + let result = test_circuit.populate_and_verify( + param_bytes, + hash_bytes, + chain_index_val, + position_val, + &message, + wrong_digest, + ); + + assert!(result.is_err(), "Expected error for wrong digest"); + } + + #[test] + fn test_chain_tweak_wrong_param() { + let test_circuit = ChainTestCircuit::new(32); + + let correct_param_bytes = b"correct_parameter_32_bytes!!!!!!"; + let wrong_param_bytes = b"wrong___parameter_32_bytes!!!!!!"; + let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; + let chain_index_val = 123u64; + let position_val = 456u64; + + // Message built with correct param + let message = + build_chain_tweak(correct_param_bytes, hash_bytes, chain_index_val, position_val); + + let expected_digest = Keccak256::digest(&message); + + // Populate with WRONG param but correct digest + let result = test_circuit.populate_and_verify( + wrong_param_bytes, + hash_bytes, + chain_index_val, + position_val, + &message, + expected_digest.into(), + ); + + assert!(result.is_err(), "Expected error for mismatched param"); + } + + #[test] + fn test_chain_tweak_wrong_chain_index() { + let test_circuit = ChainTestCircuit::new(32); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let hash_bytes = b"hash_value_32_bytes_long!!!!!!!!"; + let correct_chain_index = 123u64; + let wrong_chain_index = 999u64; + let position_val = 456u64; + + // Message built with correct chain_index + let message = build_chain_tweak(param_bytes, hash_bytes, correct_chain_index, position_val); + + let expected_digest = Keccak256::digest(&message); + + // Populate with WRONG chain_index but correct digest + let result = test_circuit.populate_and_verify( + param_bytes, + hash_bytes, + wrong_chain_index, + position_val, + &message, + expected_digest.into(), + ); + + assert!(result.is_err(), "Expected error for mismatched chain_index"); + } + + proptest! { + #[test] + fn test_chain_tweak_property_based( + param_len in 1usize..=100, + chain_index in 0u64..=1000, + position in 0u64..=1000, + ) { + use rand::SeedableRng; + use rand::prelude::StdRng; + + let mut rng = StdRng::seed_from_u64(0); + + // Generate random param bytes + let mut param_bytes = vec![0u8; param_len]; + rng.fill_bytes(&mut param_bytes); + + // Generate random hash + let mut hash_bytes = [0u8; 32]; + rng.fill_bytes(&mut hash_bytes); + + // Create circuit + let test_circuit = ChainTestCircuit::new(param_len); + + // Build message and compute digest + let message = build_chain_tweak( + ¶m_bytes, + &hash_bytes, + chain_index, + position, + ); + + // Verify message structure + prop_assert_eq!(message.len(), param_len + FIXED_MESSAGE_OVERHEAD); + prop_assert_eq!(message[param_len], CHAIN_TWEAK); + + let expected_digest: [u8; 32] = Keccak256::digest(&message).into(); + + // Verify circuit + test_circuit + .populate_and_verify( + ¶m_bytes, + &hash_bytes, + chain_index, + position, + &message, + expected_digest, + ) + .unwrap(); + } + } +} diff --git a/crates/frontend/src/circuits/hash_based_sig/tweak/message.rs b/crates/frontend/src/circuits/hash_based_sig/tweak/message.rs new file mode 100644 index 000000000..aab8d6d4a --- /dev/null +++ b/crates/frontend/src/circuits/hash_based_sig/tweak/message.rs @@ -0,0 +1,422 @@ +use super::base::verify_tweaked_keccak; +use crate::{ + circuits::{concat::Term, keccak::Keccak}, + compiler::{CircuitBuilder, Wire}, +}; + +pub const MESSAGE_TWEAK: u8 = 0x02; + +/// A circuit that verifies a message-tweaked Keccak-256 computation. +/// +/// This circuit verifies Keccak-256 of a message that's been tweaked with +/// message-specific parameters: `Keccak256(param || 0x02 || nonce || message)` +/// +/// # Arguments +/// +/// * `builder` - Circuit builder for constructing constraints +/// * `param_wires` - The cryptographic parameter wires (typically public key material), where each +/// wire holds 8 bytes as a 64-bit LE-packed value +/// * `param_len` - The actual parameter length in bytes +/// * `nonce_wires` - Random nonce wires to ensure uniqueness, where each wire holds 8 bytes as a +/// 64-bit LE-packed value +/// * `nonce_len` - The actual nonce length in bytes +/// * `message_wires` - The message content wires, where each wire holds 8 bytes as a 64-bit +/// LE-packed value +/// * `message_len` - The actual message length in bytes +/// * `digest` - Output: The computed Keccak-256 digest (32 bytes as 4x64-bit LE-packed wires) +/// +/// # Returns +/// +/// A `Keccak` circuit that needs to be populated with the tweaked message and digest +#[allow(clippy::too_many_arguments)] +pub fn verify_message_tweak( + builder: &CircuitBuilder, + param_wires: Vec, + param_len: usize, + nonce_wires: Vec, + nonce_len: usize, + message_wires: Vec, + message_len: usize, + digest: [Wire; 4], +) -> Keccak { + let total_message_len = param_len + 1 + nonce_len + message_len; // +1 for tweak byte + + let mut additional_terms = Vec::new(); + + let nonce_term = Term { + len: builder.add_constant_64(nonce_len as u64), + data: nonce_wires.clone(), + max_len: nonce_wires.len() * 8, + }; + additional_terms.push(nonce_term); + + let message_term = Term { + len: builder.add_constant_64(message_len as u64), + data: message_wires.clone(), + max_len: message_wires.len() * 8, + }; + additional_terms.push(message_term); + + verify_tweaked_keccak( + builder, + param_wires, + param_len, + MESSAGE_TWEAK, + additional_terms, + total_message_len, + digest, + ) +} + +/// Build the tweaked message from components. +/// +/// Constructs the complete message for Keccak-256 hashing by concatenating: +/// `param || 0x02 || nonce || message` +/// +/// This function is typically used when populating witness data for the +/// `verify_message_tweak` circuit. +/// +/// # Arguments +/// +/// * `param_bytes` - The cryptographic parameter bytes +/// * `nonce_bytes` - The random nonce bytes +/// * `message_bytes` - The message content bytes +/// +/// # Returns +/// +/// A vector containing the complete tweaked message ready for hashing +pub fn build_message_tweak( + param_bytes: &[u8], + nonce_bytes: &[u8], + message_bytes: &[u8], +) -> Vec { + let mut message = Vec::new(); + message.extend_from_slice(param_bytes); + message.push(MESSAGE_TWEAK); // TWEAK_MESSAGE + message.extend_from_slice(nonce_bytes); + message.extend_from_slice(message_bytes); + message +} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use sha3::{Digest, Keccak256}; + + use super::*; + use crate::{ + compiler::{CircuitBuilder, circuit::Circuit}, + constraint_verifier::verify_constraints, + util::{pack_bytes_into_wires_le, pack_bytes_into_wires_le_padded}, + }; + + /// Helper struct for MessageTweak testing + struct MessageTestCircuit { + circuit: Circuit, + keccak: Keccak, + param_wires: Vec, + param_len: usize, + nonce_wires: Vec, + nonce_len: usize, + message_wires: Vec, + message_len: usize, + } + + impl MessageTestCircuit { + fn new(param_len: usize, nonce_len: usize, message_len: usize) -> Self { + let builder = CircuitBuilder::new(); + + let num_param_wires = param_len.div_ceil(8); + let param_wires: Vec = + (0..num_param_wires).map(|_| builder.add_inout()).collect(); + + let num_nonce_wires = nonce_len.div_ceil(8); + let nonce_wires: Vec = (0..num_nonce_wires) + .map(|_| builder.add_witness()) + .collect(); + + let num_message_wires = message_len.div_ceil(8); + let message_wires: Vec = (0..num_message_wires) + .map(|_| builder.add_witness()) + .collect(); + + let digest: [Wire; 4] = std::array::from_fn(|_| builder.add_inout()); + + let keccak = verify_message_tweak( + &builder, + param_wires.clone(), + param_len, + nonce_wires.clone(), + nonce_len, + message_wires.clone(), + message_len, + digest, + ); + + let circuit = builder.build(); + + Self { + circuit, + keccak, + param_wires, + param_len, + nonce_wires, + nonce_len, + message_wires, + message_len, + } + } + + /// Populate witness and verify constraints with given test data + fn populate_and_verify( + &self, + param_bytes: &[u8], + nonce_bytes: &[u8], + message_bytes: &[u8], + full_message: &[u8], + digest: [u8; 32], + ) -> Result<(), Box> { + let mut w = self.circuit.new_witness_filler(); + + assert_eq!(param_bytes.len(), self.param_len); + pack_bytes_into_wires_le(&mut w, &self.param_wires, param_bytes); + + assert_eq!(nonce_bytes.len(), self.nonce_len); + pack_bytes_into_wires_le_padded(&mut w, &self.nonce_wires, nonce_bytes); + + assert_eq!(message_bytes.len(), self.message_len); + pack_bytes_into_wires_le_padded(&mut w, &self.message_wires, message_bytes); + + self.keccak.populate_message(&mut w, full_message); + self.keccak.populate_digest(&mut w, digest); + + self.circuit.populate_wire_witness(&mut w)?; + let cs = self.circuit.constraint_system(); + verify_constraints(cs, &w.into_value_vec())?; + Ok(()) + } + } + + #[test] + fn test_message_tweak_basic() { + let test_circuit = MessageTestCircuit::new(32, 16, 64); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let nonce_bytes = b"random_nonce_16b"; + let message_bytes = b"This is a test message that is exactly 64 bytes long!!!!!!!!!!!."; // 64 bytes + + let full_message = build_message_tweak(param_bytes, nonce_bytes, message_bytes); + + let expected_digest = Keccak256::digest(&full_message); + + test_circuit + .populate_and_verify( + param_bytes, + nonce_bytes, + message_bytes, + &full_message, + expected_digest.into(), + ) + .unwrap(); + } + + #[test] + fn test_message_tweak_with_18_byte_param() { + // Test with 18-byte param as commonly used in XMSS + let test_circuit = MessageTestCircuit::new(18, 8, 32); + + let param_bytes: &[u8; 18] = b"test_param_18bytes"; + let nonce_bytes = b"nonce_8b"; + let message_bytes = b"message_that_is_32_bytes_long!!!"; + + let full_message = build_message_tweak(param_bytes, nonce_bytes, message_bytes); + + let expected_digest = Keccak256::digest(&full_message); + + test_circuit + .populate_and_verify( + param_bytes, + nonce_bytes, + message_bytes, + &full_message, + expected_digest.into(), + ) + .unwrap(); + } + + #[test] + fn test_message_tweak_variable_lengths() { + // Test with various non-aligned lengths + let test_circuit = MessageTestCircuit::new(13, 7, 29); + + let param_bytes = b"param_13bytes"; + let nonce_bytes = b"nonce7b"; + let message_bytes = b"msg_that_is_29_bytes_long!!!!"; + + let full_message = build_message_tweak(param_bytes, nonce_bytes, message_bytes); + + let expected_digest = Keccak256::digest(&full_message); + + test_circuit + .populate_and_verify( + param_bytes, + nonce_bytes, + message_bytes, + &full_message, + expected_digest.into(), + ) + .unwrap(); + } + + #[test] + fn test_message_tweak_wrong_digest() { + let test_circuit = MessageTestCircuit::new(32, 16, 64); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let nonce_bytes = b"random_nonce_16b"; + let message_bytes = b"This is a test message that is exactly 64 bytes long!!!!!!!!!!!."; // 64 bytes + + let full_message = build_message_tweak(param_bytes, nonce_bytes, message_bytes); + + // Populate with WRONG digest - this should cause verification to fail + let wrong_digest = [0u8; 32]; + + let result = test_circuit.populate_and_verify( + param_bytes, + nonce_bytes, + message_bytes, + &full_message, + wrong_digest, + ); + + assert!(result.is_err(), "Expected error for wrong digest"); + } + + #[test] + fn test_message_tweak_wrong_param() { + let test_circuit = MessageTestCircuit::new(32, 16, 64); + + let correct_param_bytes = b"correct_parameter_32_bytes!!!!!!"; + let wrong_param_bytes = b"wrong___parameter_32_bytes!!!!!!"; + let nonce_bytes = b"random_nonce_16b"; + let message_bytes = b"This is a test message that is exactly 64 bytes long!!!!!!!!!!!."; // 64 bytes + + // Build message with correct param + let full_message = build_message_tweak(correct_param_bytes, nonce_bytes, message_bytes); + + let expected_digest = Keccak256::digest(&full_message); + + // Populate with WRONG param but correct digest + let result = test_circuit.populate_and_verify( + wrong_param_bytes, + nonce_bytes, + message_bytes, + &full_message, + expected_digest.into(), + ); + + assert!(result.is_err(), "Expected error for mismatched param"); + } + + #[test] + fn test_message_tweak_wrong_nonce() { + let test_circuit = MessageTestCircuit::new(32, 16, 64); + + let param_bytes = b"test_parameter_32_bytes_long!!!!"; + let correct_nonce = b"correct_nonce16b"; + let wrong_nonce = b"wrong___nonce16b"; + let message_bytes = b"This is a test message that is exactly 64 bytes long!!!!!!!!!!!."; // 64 bytes + + // Build message with correct nonce + let full_message = build_message_tweak(param_bytes, correct_nonce, message_bytes); + + let expected_digest = Keccak256::digest(&full_message); + + // Populate with WRONG nonce but correct digest + let result = test_circuit.populate_and_verify( + param_bytes, + wrong_nonce, + message_bytes, + &full_message, + expected_digest.into(), + ); + + assert!(result.is_err(), "Expected error for mismatched nonce"); + } + + #[test] + fn test_message_tweak_ensures_tweak_byte() { + // This test verifies that the MESSAGE_TWEAK byte (0x02) is correctly inserted + let test_circuit = MessageTestCircuit::new(8, 8, 16); + + let param_bytes = b"param_8b"; + let nonce_bytes = b"nonce_8b"; + let message_bytes = b"message_16_bytes"; + + let full_message = build_message_tweak(param_bytes, nonce_bytes, message_bytes); + + // Verify the tweak byte is at the correct position + assert_eq!(full_message[8], MESSAGE_TWEAK); + assert_eq!(full_message.len(), 8 + 1 + 8 + 16); // param + tweak + nonce + message + + let expected_digest = Keccak256::digest(&full_message); + + test_circuit + .populate_and_verify( + param_bytes, + nonce_bytes, + message_bytes, + &full_message, + expected_digest.into(), + ) + .unwrap(); + } + + proptest! { + #[test] + fn test_message_tweak_property_based( + param_len in 1usize..=100, + nonce_len in 1usize..=50, + message_len in 1usize..=200, + ) { + use rand::SeedableRng; + use rand::prelude::StdRng; + + let mut rng = StdRng::seed_from_u64(0); + + // Generate random data of specified lengths + let mut param_bytes = vec![0u8; param_len]; + rng.fill_bytes(&mut param_bytes); + + let mut nonce_bytes = vec![0u8; nonce_len]; + rng.fill_bytes(&mut nonce_bytes); + + let mut message_bytes = vec![0u8; message_len]; + rng.fill_bytes(&mut message_bytes); + + // Create circuit + let test_circuit = MessageTestCircuit::new(param_len, nonce_len, message_len); + + // Build full message and compute digest + let full_message = + build_message_tweak(¶m_bytes, &nonce_bytes, &message_bytes); + + // Verify message structure + prop_assert_eq!(full_message.len(), param_len + 1 + nonce_len + message_len); + prop_assert_eq!(full_message[param_len], MESSAGE_TWEAK); + + let expected_digest: [u8; 32] = Keccak256::digest(&full_message).into(); + + // Verify circuit + test_circuit + .populate_and_verify( + ¶m_bytes, + &nonce_bytes, + &message_bytes, + &full_message, + expected_digest, + ) + .unwrap(); + } + } +} diff --git a/crates/frontend/src/circuits/hash_based_sig/tweak/mod.rs b/crates/frontend/src/circuits/hash_based_sig/tweak/mod.rs new file mode 100644 index 000000000..c4a6bb656 --- /dev/null +++ b/crates/frontend/src/circuits/hash_based_sig/tweak/mod.rs @@ -0,0 +1,7 @@ +//! Tweaked Keccak-256 circuits for hash-based signatures. +mod base; +mod chain; +mod message; + +pub use chain::{CHAIN_TWEAK, FIXED_MESSAGE_OVERHEAD, build_chain_tweak, verify_chain_tweak}; +pub use message::{MESSAGE_TWEAK, build_message_tweak, verify_message_tweak}; diff --git a/crates/frontend/src/util.rs b/crates/frontend/src/util.rs index 88e6d8db2..4e8596178 100644 --- a/crates/frontend/src/util.rs +++ b/crates/frontend/src/util.rs @@ -37,6 +37,28 @@ pub fn pack_bytes_into_wires_le(w: &mut WitnessFiller, wires: &[Wire], bytes: &[ } } +/// Packs bytes into wires with zero padding to fill the wire capacity. +/// +/// This is useful when you have a fixed number of wires but variable-length data. +/// The bytes are padded with zeros to fill wires.len() * 8 bytes. +pub fn pack_bytes_into_wires_le_padded(w: &mut WitnessFiller, wires: &[Wire], bytes: &[u8]) { + let wire_capacity = wires.len() * 8; + assert!( + bytes.len() <= wire_capacity, + "bytes length {} exceeds wire capacity {}", + bytes.len(), + wire_capacity + ); + + if bytes.len() == wire_capacity { + pack_bytes_into_wires_le(w, wires, bytes); + } else { + let mut padded = vec![0u8; wire_capacity]; + padded[..bytes.len()].copy_from_slice(bytes); + pack_bytes_into_wires_le(w, wires, &padded); + } +} + /// Returns a BigUint from u64 limbs with little-endian ordering pub fn num_biguint_from_u64_limbs(limbs: I) -> num_bigint::BigUint where