diff --git a/crypto/crypto/src/fiat_shamir/default_transcript.rs b/crypto/crypto/src/fiat_shamir/default_transcript.rs index 7c3c0bf99..60922aec2 100644 --- a/crypto/crypto/src/fiat_shamir/default_transcript.rs +++ b/crypto/crypto/src/fiat_shamir/default_transcript.rs @@ -6,7 +6,7 @@ use math::{ element::FieldElement, traits::{HasDefaultTranscript, IsField, IsSubFieldOf}, }, - traits::ByteConversion, + traits::AsBytes, }; use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; use sha3::{Digest, Keccak256}; @@ -28,7 +28,7 @@ impl Clone for DefaultTranscript { impl DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, { pub fn new(data: &[u8]) -> Self { let mut res = Self { @@ -50,7 +50,7 @@ where impl Default for DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, { fn default() -> Self { Self::new(&[]) @@ -60,14 +60,14 @@ where impl IsTranscript for DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, { fn append_bytes(&mut self, new_bytes: &[u8]) { self.hasher.update(new_bytes); } fn append_field_element(&mut self, element: &FieldElement) { - self.append_bytes(&element.to_bytes_be()); + element.stream_bytes(&mut |b| self.hasher.update(b)); } fn state(&self) -> [u8; 32] { @@ -94,7 +94,7 @@ where impl IsStarkTranscript for DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, S: IsField + IsSubFieldOf, { // nothing to implement: sample_z_ood uses the default body diff --git a/crypto/crypto/src/merkle_tree/backends/field_element.rs b/crypto/crypto/src/merkle_tree/backends/field_element.rs index d5d5c32d7..e8f106f5a 100644 --- a/crypto/crypto/src/merkle_tree/backends/field_element.rs +++ b/crypto/crypto/src/merkle_tree/backends/field_element.rs @@ -34,7 +34,7 @@ where fn hash_data(input: &FieldElement) -> [u8; NUM_BYTES] { let mut hasher = D::new(); - hasher.update(input.as_bytes()); + input.stream_bytes(&mut |b| hasher.update(b)); hasher.finalize().into() } diff --git a/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs b/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs index 25ba807c6..870eb5c17 100644 --- a/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs +++ b/crypto/crypto/src/merkle_tree/backends/field_element_vector.rs @@ -39,8 +39,8 @@ where fn hash_data(input: &[FieldElement; 2]) -> [u8; NUM_BYTES] { let mut hasher = D::new(); - hasher.update(input[0].as_bytes()); - hasher.update(input[1].as_bytes()); + input[0].stream_bytes(&mut |b| hasher.update(b)); + input[1].stream_bytes(&mut |b| hasher.update(b)); let mut result_hash = [0_u8; NUM_BYTES]; result_hash.copy_from_slice(&hasher.finalize()); result_hash @@ -86,6 +86,25 @@ where result.copy_from_slice(&hasher.finalize()); result } + + /// Hashes the concatenation of `parts` (each a slice of field elements) as a + /// single leaf, without allocating a buffer to concatenate them first. Byte- + /// identical to hashing `parts.concat()` via [`Self::hash_data`]. + pub fn hash_data_parts(parts: &[&[FieldElement]]) -> [u8; NUM_BYTES] + where + F: IsField, + FieldElement: AsBytes, + { + let mut hasher = D::new(); + for part in parts { + for element in part.iter() { + element.stream_bytes(&mut |b| hasher.update(b)); + } + } + let mut result = [0u8; NUM_BYTES]; + result.copy_from_slice(&hasher.finalize()); + result + } } impl IsMerkleTreeBackend @@ -100,13 +119,7 @@ where type Data = Vec>; fn hash_data(input: &Vec>) -> [u8; NUM_BYTES] { - let mut hasher = D::new(); - for element in input.iter() { - hasher.update(element.as_bytes()); - } - let mut result_hash = [0_u8; NUM_BYTES]; - result_hash.copy_from_slice(&hasher.finalize()); - result_hash + Self::hash_data_parts(&[input.as_slice()]) } fn hash_new_parent(left: &[u8; NUM_BYTES], right: &[u8; NUM_BYTES]) -> [u8; NUM_BYTES] { diff --git a/crypto/crypto/src/merkle_tree/proof.rs b/crypto/crypto/src/merkle_tree/proof.rs index 2bbcfb3c5..518412ec8 100644 --- a/crypto/crypto/src/merkle_tree/proof.rs +++ b/crypto/crypto/src/merkle_tree/proof.rs @@ -30,14 +30,28 @@ pub struct Proof { pub fn verify_merkle_path( merkle_path: &[B::Node], root_hash: &B::Node, - mut index: usize, + index: usize, value: &B::Data, ) -> bool where B: IsMerkleTreeBackend, { - let mut hashed_value = B::hash_data(value); + verify_merkle_path_from_hash::(merkle_path, root_hash, index, B::hash_data(value)) +} +/// Same check as [`verify_merkle_path`], starting from an already-computed leaf +/// hash. Lets callers that can hash their leaf data more efficiently than the +/// backend's `hash_data` (e.g. hashing several slices without concatenating +/// them first) still walk the shared path-verification logic. +pub fn verify_merkle_path_from_hash( + merkle_path: &[B::Node], + root_hash: &B::Node, + mut index: usize, + mut hashed_value: B::Node, +) -> bool +where + B: IsMerkleTreeBackend, +{ for sibling_node in merkle_path.iter() { if index.is_multiple_of(2) { hashed_value = B::hash_new_parent(&hashed_value, sibling_node); diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index 45fd7274b..8eaf5a427 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -537,6 +537,13 @@ impl AsBytes for FieldElement { fn as_bytes(&self) -> alloc::vec::Vec { self.to_bytes_be() } + + #[inline(always)] + fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { + let mut buf = [0u8; 24]; + crate::traits::ByteConversion::write_bytes_be(self, &mut buf); + sink(&buf); + } } impl HasDefaultTranscript for Degree3GoldilocksExtensionField { diff --git a/crypto/math/src/field/goldilocks.rs b/crypto/math/src/field/goldilocks.rs index 082d57325..1d60ee5b2 100644 --- a/crypto/math/src/field/goldilocks.rs +++ b/crypto/math/src/field/goldilocks.rs @@ -488,6 +488,11 @@ impl AsBytes for FieldElement { fn as_bytes(&self) -> alloc::vec::Vec { ByteConversion::to_bytes_be(self) } + + #[inline(always)] + fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { + sink(&self.canonical_u64().to_be_bytes()); + } } // Implement IsPrimeField for the native Goldilocks diff --git a/crypto/math/src/traits.rs b/crypto/math/src/traits.rs index 0e902c6ff..e16b5bfb1 100644 --- a/crypto/math/src/traits.rs +++ b/crypto/math/src/traits.rs @@ -39,6 +39,12 @@ pub trait ByteConversion { pub trait AsBytes { /// Default serialize without args fn as_bytes(&self) -> alloc::vec::Vec; + + /// Streams the byte representation to `sink` without heap-allocating a `Vec`. + /// Default falls back to `as_bytes`; override for zero-allocation hashing/transcript hot paths. + fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { + sink(&self.as_bytes()); + } } #[cfg(feature = "alloc")] diff --git a/crypto/stark/src/verifier.rs b/crypto/stark/src/verifier.rs index 7ab1843a2..78d5017f0 100644 --- a/crypto/stark/src/verifier.rs +++ b/crypto/stark/src/verifier.rs @@ -16,7 +16,7 @@ use crate::{ }, }; use crypto::fiat_shamir::is_transcript::IsStarkTranscript; -use crypto::merkle_tree::proof::verify_merkle_path; +use crypto::merkle_tree::proof::verify_merkle_path_from_hash; use math::field::element::ArchivedFieldElement; #[cfg(not(feature = "test_fiat_shamir"))] use log::error; @@ -369,13 +369,15 @@ where E::BaseType: rkyv::Archive, Field: IsSubFieldOf, { - let mut value = evals(&opening.evaluations).to_vec(); - value.extend_from_slice(evals(&opening.evaluations_sym)); - verify_merkle_path::>( + let leaf_hash = BatchedMerkleTreeBackend::::hash_data_parts(&[ + evals(&opening.evaluations), + evals(&opening.evaluations_sym), + ]); + verify_merkle_path_from_hash::>( opening.proof.merkle_path.as_slice(), root, iota, - &value, + leaf_hash, ) } @@ -426,6 +428,8 @@ where /// Verify opening Open(Hᵢ(D_LDE), 𝜐) and Open(Hᵢ(D_LDE), -𝜐) for all parts Hᵢof the composition /// polynomial, where 𝜐 and -𝜐 are the elements corresponding to the index challenge `iota`. + /// The composition-poly opening has the identical row-pair leaf layout as + /// `verify_opening_pair`, so it's the same check with `E = FieldExtension`. fn verify_composition_poly_opening( deep_poly_openings: &ArchivedDeepPolynomialOpening, composition_poly_merkle_root: &Commitment, @@ -435,14 +439,10 @@ where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let mut value = evals(&deep_poly_openings.composition_poly.evaluations).to_vec(); - value.extend_from_slice(evals(&deep_poly_openings.composition_poly.evaluations_sym)); - - verify_merkle_path::>( - deep_poly_openings.composition_poly.proof.merkle_path.as_slice(), + Self::verify_opening_pair::( + &deep_poly_openings.composition_poly, composition_poly_merkle_root, *iota, - &value, ) } @@ -485,17 +485,21 @@ where FieldElement: AsBytes + Sync + Send, FieldElement: AsBytes + Sync + Send, { - let evaluations = if iota % 2 == 1 { - vec![evaluation_sym.clone(), evaluation.clone()] + let (first, second) = if iota % 2 == 1 { + (evaluation_sym, evaluation) } else { - vec![evaluation.clone(), evaluation_sym.clone()] + (evaluation, evaluation_sym) }; + let leaf_hash = BatchedMerkleTreeBackend::::hash_data_parts(&[ + core::slice::from_ref(first), + core::slice::from_ref(second), + ]); - verify_merkle_path::>( + verify_merkle_path_from_hash::>( auth_path_sym, merkle_root, iota >> 1, - &evaluations, + leaf_hash, ) }