Skip to content

Refactor/merkle hash agnostic#506

Merged
MauroToscano merged 1 commit into
perf/merkle-zero-alloc-hashingfrom
refactor/merkle-hash-agnostic
Apr 17, 2026
Merged

Refactor/merkle hash agnostic#506
MauroToscano merged 1 commit into
perf/merkle-zero-alloc-hashingfrom
refactor/merkle-hash-agnostic

Conversation

@MauroToscano

Copy link
Copy Markdown
Contributor

No description provided.

…ytes

Add hash_bytes() inherent method to FieldElementVectorBackend that
surfaces the generic digest parameter D. The prover now calls
BatchedMerkleTreeBackend::<E>::hash_bytes() instead of hardcoding
sha3::Keccak256, so changing the config.rs type alias automatically
propagates the hash function to the commit hot paths.

Also adds missing debug_assert for power-of-two in
commit_columns_bit_reversed for consistency with
commit_composition_polynomial.
@MauroToscano

Copy link
Copy Markdown
Contributor Author

/bench

@github-actions

Copy link
Copy Markdown

Codex Code Review

No actionable issues found in this PR diff.

I checked for security regressions (unsafe/panics/memory/crypto consistency), logic bugs, and significant performance/simplicity concerns in the changed code paths. The WriteBytes serialization matches existing AsBytes behavior for the touched field types, and the hash_bytes optimization appears behavior-preserving while reducing allocation overhead.

Residual risk/testing gap: I could not run Rust tests in this environment because cargo/rustup attempted to write under a read-only path (/home/runner/.rustup/tmp/...).

@github-actions

github-actions Bot commented Apr 17, 2026

Copy link
Copy Markdown

Benchmark — fib_iterative_8M (median of 10)

Table parallelism: 32 (auto = cores / 3)

Metric main PR Δ
Peak heap 67681 MB 69433 MB +1752 MB (+2.6%) ⚪
Prove time 35.298s 33.903s -1.395s (-4.0%) ⚪

✅ No significant change.

⚠️ Heap spread: 6.6% (67806 MB / 69437 MB / 69624 MB / 67432 MB / 70796 MB / 69955 MB / 67467 MB / 66226 MB / 69433 MB / 69438 MB)
Consider re-running /bench

Commit: ec2ddf7 · Baseline: built from main · Runner: self-hosted bench

@claude

claude Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

PR #506 Review: merkle-hash-agnostic refactor

Summary: Introduces a WriteBytes trait for zero-allocation byte serialization and refactors the STARK prover's Merkle tree commitment building to write field elements directly into a buffer before hashing, eliminating per-element Vec allocations.


Medium

WriteBytes has no compile-time enforcement that BYTE_LEN matches what write_bytes_be actually writes.

write_bytes_be receives &mut [u8] with no length bound. The doc says buf[..BYTE_LEN] but the type system doesn't enforce it. If an implementor declares BYTE_LEN = 8 but writes 16 bytes (or vice versa), the caller's slice arithmetic col_idx * byte_len..(col_idx + 1) * byte_len silently truncates/overlaps output, producing a bad Merkle commitment with no error in release builds (only debug_assert guards). Signature should be fn write_bytes_be(&self, buf: &mut [u8; Self::BYTE_LEN]), or at minimum use assert! instead of debug_assert!.

hash_bytes is an inherent method, not part of IsMerkleTreeBackend.

BatchedMerkleTreeBackend::<E>::hash_bytes is called directly on the concrete type. If BatchedMerkleTreeBackend is ever changed to a different backend (e.g., Poseidon), this call silently breaks compilation. Consider either adding hash_bytes to the IsMerkleTreeBackend trait (with a default) or documenting the coupling explicitly.


Low

Duplicate debug_assert! silently existed before this PR.

The original code had two identical debug_assert!(num_rows.is_power_of_two(), ...) at the same site in commit_composition_polynomial. This PR removes one but the duplicate suggests it was never caught. The end state is correct.

Missing test: non-canonical component values for the degree-3 extension.

The Goldilocks tests cover a non-canonical base field element (from_raw(GOLDILOCKS_PRIME + 5)). The extension field tests only cover canonical values. A test with non-canonical components (e.g., each component from_raw(GOLDILOCKS_PRIME + 1)) would confirm that write_bytes_be and as_bytes agree in that case too.


Overall the refactor is clean and correct for the current configuration. The main concern is the WriteBytes contract — the type signature doesn't prevent silent misuse, and the only protection (debug_assert) is stripped in release builds.

Comment thread crypto/math/src/traits.rs
Comment thread crypto/crypto/src/merkle_tree/backends/field_element_vector.rs
@MauroToscano

Copy link
Copy Markdown
Contributor Author

/bench 10

@MauroToscano
MauroToscano changed the base branch from main to perf/merkle-zero-alloc-hashing April 17, 2026 17:15
@MauroToscano
MauroToscano merged commit a41bdf8 into perf/merkle-zero-alloc-hashing Apr 17, 2026
13 checks passed
@MauroToscano
MauroToscano deleted the refactor/merkle-hash-agnostic branch April 17, 2026 17:18
MauroToscano added a commit that referenced this pull request Apr 17, 2026
…#503)

* perf: eliminate per-element Vec<u8> allocation in Merkle tree hashing

Add WriteBytes trait for zero-allocation byte serialization and use it
in the prover's Merkle commitment hot paths.

Problem: as_bytes() returns Vec<u8> (heap allocation) for every field
element. For CPU table commitment (2^20 rows × 74 cols), that's ~77M
allocations of 8-byte Vecs. Benchmarks show this as a 1.3-1.7x slowdown.

Solution:
- WriteBytes trait: write_bytes_be(&self, buf: &mut [u8]) writes to
  caller-provided buffer, zero heap allocations per element.
- Implemented for Goldilocks (8 bytes) and cubic extension (24 bytes).
- commit_columns_bit_reversed: single buffer per row, write all columns
  directly, hash once. Eliminates both the row Vec and per-element Vec.
- commit_composition_polynomial: same approach for composition pairs.

The AsBytes trait and FieldElementVectorBackend are unchanged —
only the prover hot paths are optimized. Verifier and generic code
still use as_bytes() for backward compatibility.

Benchmarked in lambdaworks (~/dev/lambdaworks bench/merkle-comparison):
- 16 cols: 1.3-1.5x speedup, beats Plonky3 by 1.3-1.4x
- 64 cols: 1.6-1.8x speedup, beats Plonky3 by 1.5-1.6x

* add debug_asserts, fix comments, remove dead code, add tests

* fix lint

* refactor: make prover leaf-hashing hash-agnostic via backend's hash_bytes (#506)

Add hash_bytes() inherent method to FieldElementVectorBackend that
surfaces the generic digest parameter D. The prover now calls
BatchedMerkleTreeBackend::<E>::hash_bytes() instead of hardcoding
sha3::Keccak256, so changing the config.rs type alias automatically
propagates the hash function to the commit hot paths.

Also adds missing debug_assert for power-of-two in
commit_columns_bit_reversed for consistency with
commit_composition_polynomial.

* refactor: fold WriteBytes into ByteConversion trait

Consolidate serialization traits by adding BYTE_LEN and write_bytes_be
to ByteConversion instead of having a separate WriteBytes trait.
Goldilocks and cubic extension override write_bytes_be with zero-alloc
implementations; other types use the default which delegates to
to_bytes_be.

---------

Co-authored-by: Nicole <nicole.graus@lambdaclass.com>
Co-authored-by: Mauro Toscano <12560266+MauroToscano@users.noreply.github.com>
Co-authored-by: MauroFab <maurotoscano2@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant