Skip to content

Commit fbf794d

Browse files
Merge branch 'main' into perf/batch-byte-range-checks
2 parents aab7f13 + 64bad0e commit fbf794d

7 files changed

Lines changed: 497 additions & 471 deletions

File tree

crypto/crypto/src/merkle_tree/backends/field_element_vector.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,23 @@ impl<F, D: Digest, const NUM_BYTES: usize> Default for FieldElementVectorBackend
7171
}
7272
}
7373

74+
impl<F, D: Digest, const NUM_BYTES: usize> FieldElementVectorBackend<F, D, NUM_BYTES>
75+
where
76+
[u8; NUM_BYTES]: From<Output<D>>,
77+
{
78+
/// Hash raw bytes using the same digest (`D`) as this backend's leaf hashing.
79+
/// Enables callers to pre-serialize field elements into a byte buffer and hash
80+
/// once, avoiding per-element allocations while staying consistent with the
81+
/// backend's hash function.
82+
pub fn hash_bytes(data: &[u8]) -> [u8; NUM_BYTES] {
83+
let mut hasher = D::new();
84+
hasher.update(data);
85+
let mut result = [0u8; NUM_BYTES];
86+
result.copy_from_slice(&hasher.finalize());
87+
result
88+
}
89+
}
90+
7491
impl<F, D: Digest, const NUM_BYTES: usize> IsMerkleTreeBackend
7592
for FieldElementVectorBackend<F, D, NUM_BYTES>
7693
where

crypto/math/src/field/extensions_goldilocks.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ use crate::field::{
1212
use crate::traits::{AsBytes, ByteConversion};
1313

1414
impl ByteConversion for [FpE; 2] {
15+
const BYTE_LEN: usize = 16;
16+
1517
#[cfg(feature = "alloc")]
1618
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
1719
unimplemented!()
@@ -38,6 +40,8 @@ impl ByteConversion for [FpE; 2] {
3840
}
3941

4042
impl ByteConversion for [FpE; 3] {
43+
const BYTE_LEN: usize = 24;
44+
4145
#[cfg(feature = "alloc")]
4246
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
4347
let mut bytes = ByteConversion::to_bytes_be(&self[2]);
@@ -470,6 +474,17 @@ impl Fp3E {
470474
// =====================================================
471475

472476
impl ByteConversion for FieldElement<Degree3GoldilocksExtensionField> {
477+
const BYTE_LEN: usize = 24;
478+
479+
#[inline(always)]
480+
fn write_bytes_be(&self, buf: &mut [u8]) {
481+
debug_assert!(buf.len() >= 24);
482+
let components = self.value();
483+
components[0].write_bytes_be(&mut buf[0..8]);
484+
components[1].write_bytes_be(&mut buf[8..16]);
485+
components[2].write_bytes_be(&mut buf[16..24]);
486+
}
487+
473488
#[cfg(feature = "alloc")]
474489
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
475490
let mut byte_slice = ByteConversion::to_bytes_be(&self.value()[0]);
@@ -554,3 +569,27 @@ impl HasDefaultTranscript for Degree3GoldilocksExtensionField {
554569
fn mul_by_7(a: &FpE) -> FpE {
555570
FpE::from_raw(mul_by_7_raw(*a.value()))
556571
}
572+
573+
#[cfg(test)]
574+
mod tests {
575+
use super::*;
576+
use crate::traits::ByteConversion;
577+
578+
#[test]
579+
fn write_bytes_be_matches_as_bytes() {
580+
let cases = [
581+
FieldElement::<Degree3GoldilocksExtensionField>::zero(),
582+
FieldElement::<Degree3GoldilocksExtensionField>::one(),
583+
FieldElement::<Degree3GoldilocksExtensionField>::new([
584+
FpE::from(1u64),
585+
FpE::from(2u64),
586+
FpE::from(3u64),
587+
]),
588+
];
589+
for elem in &cases {
590+
let mut buf = [0u8; 24];
591+
elem.write_bytes_be(&mut buf);
592+
assert_eq!(&buf[..], elem.as_bytes().as_slice());
593+
}
594+
}
595+
}

crypto/math/src/field/goldilocks.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,14 @@ impl GoldilocksElement {
434434
// =====================================================
435435

436436
impl ByteConversion for FieldElement<GoldilocksField> {
437+
const BYTE_LEN: usize = 8;
438+
439+
#[inline(always)]
440+
fn write_bytes_be(&self, buf: &mut [u8]) {
441+
debug_assert!(buf.len() >= 8);
442+
buf[..8].copy_from_slice(&self.canonical_u64().to_be_bytes());
443+
}
444+
437445
#[cfg(feature = "alloc")]
438446
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
439447
self.canonical_u64().to_be_bytes().to_vec()
@@ -543,3 +551,32 @@ impl HasDefaultTranscript for GoldilocksField {
543551
}
544552
}
545553
}
554+
555+
#[cfg(test)]
556+
mod tests {
557+
use super::*;
558+
use crate::traits::ByteConversion;
559+
560+
#[test]
561+
fn write_bytes_be_matches_as_bytes() {
562+
let cases = [
563+
FieldElement::<GoldilocksField>::from(0u64),
564+
FieldElement::<GoldilocksField>::from(1u64),
565+
FieldElement::<GoldilocksField>::from(GOLDILOCKS_PRIME - 1),
566+
];
567+
for elem in &cases {
568+
let mut buf = [0u8; 8];
569+
elem.write_bytes_be(&mut buf);
570+
assert_eq!(&buf[..], elem.as_bytes().as_slice());
571+
}
572+
}
573+
574+
#[test]
575+
fn write_bytes_be_matches_as_bytes_noncanonical() {
576+
// Values stored as-is via from_raw are non-canonical (>= p) until serialized.
577+
let elem = FieldElement::<GoldilocksField>::from_raw(GOLDILOCKS_PRIME + 5);
578+
let mut buf = [0u8; 8];
579+
elem.write_bytes_be(&mut buf);
580+
assert_eq!(&buf[..], elem.as_bytes().as_slice());
581+
}
582+
}

crypto/math/src/field/test_fields/u32_test_field.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use crate::traits::ByteConversion;
1111
pub struct U32Field<const MODULUS: u32>;
1212

1313
impl ByteConversion for u32 {
14+
const BYTE_LEN: usize = 4;
15+
1416
#[cfg(feature = "alloc")]
1517
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
1618
unimplemented!()

crypto/math/src/traits.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ use crate::errors::ByteConversionError;
55
/// for getting an element from its byte representation in big-endian or
66
/// little-endian order.
77
pub trait ByteConversion {
8-
/// Returns the byte representation of the element in big-endian order.}
8+
/// Byte length of the big-endian representation.
9+
const BYTE_LEN: usize;
10+
11+
/// Returns the byte representation of the element in big-endian order.
912
#[cfg(feature = "alloc")]
1013
fn to_bytes_be(&self) -> alloc::vec::Vec<u8>;
1114

@@ -22,6 +25,14 @@ pub trait ByteConversion {
2225
fn from_bytes_le(bytes: &[u8]) -> Result<Self, ByteConversionError>
2326
where
2427
Self: Sized;
28+
29+
/// Write big-endian bytes into `buf[..BYTE_LEN]`.
30+
/// Override for zero-allocation performance in hot paths.
31+
#[cfg(feature = "alloc")]
32+
fn write_bytes_be(&self, buf: &mut [u8]) {
33+
let bytes = self.to_bytes_be();
34+
buf[..bytes.len()].copy_from_slice(&bytes);
35+
}
2536
}
2637

2738
/// Serialize function without args
@@ -47,6 +58,8 @@ impl AsBytes for u64 {
4758
}
4859

4960
impl ByteConversion for u64 {
61+
const BYTE_LEN: usize = 8;
62+
5063
#[cfg(feature = "alloc")]
5164
fn to_bytes_be(&self) -> alloc::vec::Vec<u8> {
5265
self.to_be_bytes().to_vec()

crypto/stark/src/prover.rs

Lines changed: 39 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ use std::sync::Arc;
44
use std::time::Instant;
55

66
use crypto::fiat_shamir::is_transcript::IsStarkTranscript;
7-
use crypto::merkle_tree::traits::IsMerkleTreeBackend;
87
use math::fft::cpu::bit_reversing::{in_place_bit_reverse_permute, reverse_index};
98
use math::fft::cpu::bowers_fft::LayerTwiddles;
109
use math::fft::errors::FFTError;
@@ -63,6 +62,9 @@ impl<
6362
FieldExtension: Send + Sync + IsField,
6463
PI,
6564
> IsStarkProver<Field, FieldExtension, PI> for Prover<Field, FieldExtension, PI>
65+
where
66+
FieldElement<Field>: math::traits::ByteConversion,
67+
FieldElement<FieldExtension>: math::traits::ByteConversion,
6668
{
6769
}
6870

@@ -352,22 +354,10 @@ pub trait IsStarkProver<
352354
Field: IsSubFieldOf<FieldExtension> + IsFFTField + Send + Sync,
353355
FieldExtension: Send + Sync + IsField,
354356
PI,
355-
>
357+
> where
358+
FieldElement<Field>: math::traits::ByteConversion,
359+
FieldElement<FieldExtension>: math::traits::ByteConversion,
356360
{
357-
/// Returns the Merkle tree and the commitment to the vectors `vectors`.
358-
fn batch_commit_extension(
359-
vectors: &[Vec<FieldElement<FieldExtension>>],
360-
) -> Option<(BatchedMerkleTree<FieldExtension>, Commitment)>
361-
where
362-
FieldElement<Field>: AsBytes + Sync + Send,
363-
FieldElement<FieldExtension>: AsBytes + Sync + Send,
364-
{
365-
let tree = BatchedMerkleTree::build(vectors)?;
366-
367-
let commitment = tree.root;
368-
Some((tree, commitment))
369-
}
370-
371361
/// Builds a Merkle tree commitment from column-major LDE evaluations with
372362
/// bit-reverse permutation, without cloning the full evaluation matrix.
373363
///
@@ -379,28 +369,41 @@ pub trait IsStarkProver<
379369
columns: &[Vec<FieldElement<E>>],
380370
) -> Option<(BatchedMerkleTree<E>, Commitment)>
381371
where
382-
FieldElement<E>: AsBytes + Sync + Send,
372+
FieldElement<E>: AsBytes + Sync + Send + math::traits::ByteConversion,
383373
E: IsField,
384374
{
375+
use math::traits::ByteConversion;
376+
385377
if columns.is_empty() || columns[0].is_empty() {
386378
return None;
387379
}
388380

389381
let num_rows = columns[0].len();
390382
let num_cols = columns.len();
383+
let byte_len = <FieldElement<E> as ByteConversion>::BYTE_LEN;
384+
385+
debug_assert!(
386+
num_rows.is_power_of_two(),
387+
"num_rows must be a power of two for reverse_index"
388+
);
391389

392390
#[cfg(feature = "parallel")]
393391
let iter = (0..num_rows).into_par_iter();
394392
#[cfg(not(feature = "parallel"))]
395393
let iter = 0..num_rows;
396394

395+
// One allocation per row (was one per field element): write all columns
396+
// into a single buffer, then hash once.
397397
let hashed_leaves: Vec<Commitment> = iter
398398
.map(|row_idx| {
399399
let br_idx = reverse_index(row_idx, num_rows as u64);
400-
let row: Vec<FieldElement<E>> = (0..num_cols)
401-
.map(|col_idx| columns[col_idx][br_idx].clone())
402-
.collect();
403-
BatchedMerkleTreeBackend::<E>::hash_data(&row)
400+
let total_bytes = num_cols * byte_len;
401+
let mut buf = vec![0u8; total_bytes];
402+
for col_idx in 0..num_cols {
403+
columns[col_idx][br_idx]
404+
.write_bytes_be(&mut buf[col_idx * byte_len..(col_idx + 1) * byte_len]);
405+
}
406+
BatchedMerkleTreeBackend::<E>::hash_bytes(&buf)
404407
})
405408
.collect();
406409

@@ -690,8 +693,10 @@ pub trait IsStarkProver<
690693
) -> Option<(BatchedMerkleTree<FieldExtension>, Commitment)>
691694
where
692695
FieldElement<Field>: AsBytes + Sync + Send,
693-
FieldElement<FieldExtension>: AsBytes + Sync + Send,
696+
FieldElement<FieldExtension>: AsBytes + Sync + Send + math::traits::ByteConversion,
694697
{
698+
use math::traits::ByteConversion;
699+
695700
let num_parts = lde_composition_poly_parts_evaluations.len();
696701
if num_parts == 0 {
697702
return None;
@@ -702,18 +707,15 @@ pub trait IsStarkProver<
702707
}
703708

704709
let num_leaves = num_rows / 2;
705-
debug_assert!(
706-
num_rows.is_power_of_two(),
707-
"num_rows must be a power of two for reverse_index to be correct"
708-
);
709710
debug_assert!(
710711
num_rows.is_power_of_two(),
711712
"num_rows must be a power of two for reverse_index"
712713
);
713714

714-
// Skip the transpose + merge by computing leaf data inline.
715-
// Each leaf = row_pair[2*i] ++ row_pair[2*i+1] after bit-reverse.
716-
// Build the merged leaf data and hash in one pass.
715+
let byte_len = <FieldElement<FieldExtension> as ByteConversion>::BYTE_LEN;
716+
717+
// One allocation per leaf (was one per field element): write all parts
718+
// into a single buffer. Each leaf = row_pair[2*i] ++ row_pair[2*i+1] after bit-reverse.
717719
#[cfg(feature = "parallel")]
718720
let iter = (0..num_leaves).into_par_iter();
719721
#[cfg(not(feature = "parallel"))]
@@ -723,14 +725,18 @@ pub trait IsStarkProver<
723725
.map(|leaf_idx| {
724726
let br_0 = reverse_index(2 * leaf_idx, num_rows as u64);
725727
let br_1 = reverse_index(2 * leaf_idx + 1, num_rows as u64);
726-
let mut leaf = Vec::with_capacity(2 * num_parts);
728+
let total_bytes = 2 * num_parts * byte_len;
729+
let mut buf = vec![0u8; total_bytes];
730+
let mut offset = 0;
727731
for part in lde_composition_poly_parts_evaluations.iter() {
728-
leaf.push(part[br_0].clone());
732+
part[br_0].write_bytes_be(&mut buf[offset..offset + byte_len]);
733+
offset += byte_len;
729734
}
730735
for part in lde_composition_poly_parts_evaluations.iter() {
731-
leaf.push(part[br_1].clone());
736+
part[br_1].write_bytes_be(&mut buf[offset..offset + byte_len]);
737+
offset += byte_len;
732738
}
733-
BatchedMerkleTreeBackend::<FieldExtension>::hash_data(&leaf)
739+
BatchedMerkleTreeBackend::<FieldExtension>::hash_bytes(&buf)
734740
})
735741
.collect();
736742

0 commit comments

Comments
 (0)