diff --git a/crypto/crypto/src/fiat_shamir/default_transcript.rs b/crypto/crypto/src/fiat_shamir/default_transcript.rs index 8ab3eafc3..819b0f761 100644 --- a/crypto/crypto/src/fiat_shamir/default_transcript.rs +++ b/crypto/crypto/src/fiat_shamir/default_transcript.rs @@ -8,7 +8,7 @@ use math::{ element::FieldElement, traits::{HasDefaultTranscript, IsField, IsSubFieldOf}, }, - traits::ByteConversion, + traits::AsBytes, }; use rand_chacha::{ChaCha20Rng, rand_core::SeedableRng}; @@ -29,7 +29,7 @@ impl Clone for DefaultTranscript { impl DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, { pub fn new(data: &[u8]) -> Self { let mut res = Self { @@ -51,7 +51,7 @@ where impl Default for DefaultTranscript where F: HasDefaultTranscript, - FieldElement: ByteConversion, + FieldElement: AsBytes, { fn default() -> Self { Self::new(&[]) @@ -61,14 +61,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] { @@ -95,7 +95,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 e6daace0d..d60419cf4 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 @@ -102,7 +102,7 @@ where pub fn hash_data_from_slices(a: &[FieldElement], b: &[FieldElement]) -> [u8; NUM_BYTES] { let mut hasher = D::new(); for element in a.iter().chain(b.iter()) { - hasher.update(element.as_bytes()); + element.stream_bytes(&mut |bytes| hasher.update(bytes)); } let mut result_hash = [0_u8; NUM_BYTES]; result_hash.copy_from_slice(&hasher.finalize()); diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index d6bac98df..246a3cb87 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -554,6 +554,17 @@ impl AsBytes for FieldElement { fn as_bytes(&self) -> alloc::vec::Vec { self.to_bytes_be() } + + // One sink call over a stack buffer instead of three (one per limb): each + // sink call lands as its own `Digest::update` on the guest, and dyn dispatch + // here is fully devirtualized by the #[inline(always)] chain, so call count + // — not indirection — is the cost being cut. + #[inline(always)] + fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { + let mut buf = [0u8; 24]; + 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")]