Skip to content

Buffer Abstraction#260

Open
zkfriendly wants to merge 23 commits into
worldfnd:mainfrom
zkfriendly:zkfr/buffer-abstraction
Open

Buffer Abstraction#260
zkfriendly wants to merge 23 commits into
worldfnd:mainfrom
zkfriendly:zkfr/buffer-abstraction

Conversation

@zkfriendly

@zkfriendly zkfriendly commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR introduces the buffer abstraction layer. All buffer abstraction traits are defined in the buffer crate, with a CPU implementation that essentially wraps the existing in-memory code paths.

At this moment, the buffer crate still depends on existing algebra and protocol crates. Future work can consolidate the relevant logic directly into the buffer crate as the abstraction settles.

Notes

There is also an experimental Metal implementation, but it is not included in this PR.

zk_whir is not fully ported to the buffer abstraction yet. It uses .as_slice() in places to keep the code working, so behavior is essentially unchanged compared to directly using CPU buffers. Since WHIR v3 is coming and is expected to replace zk_whir, that path is intentionally not fully ported for now.

@zkfriendly

Copy link
Copy Markdown
Collaborator Author

cc @recmo

@shreyas-londhe shreyas-londhe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR @zkfriendly. Really like where this lands. Trait split is the right cut, the CPU backend stays a thin wrapper with no second implementation to drift, and the prover got simpler dropping the Any/Cow recycling for mixed_linear_combination and linear_forms_rlc. Left a few inline comments, but the shape is solid.

Comment thread src/protocols/basecase.rs
Comment on lines +90 to +93
assert!(
!vector.at_index(0).unwrap_or_else(F::one).is_zero(),
"Proof failed"
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (blocking): unwrap_or_else(F::one) makes this pass when the buffer is empty.

F::one() is non-zero, so an out-of-bounds at_index(0) falls back to a value that satisfies the check instead of failing it. The covector lines just below (96, 152) already use .expect("Proof failed"). Use that here too. Same fix at line 146.

/// Build the full node array from leaf hashes, without touching the transcript.
///
/// The returned vector holds the leaf layer first and the root last (at `num_nodes() - 1`).
pub fn build_nodes(&self, leaves: Vec<Hash>) -> Vec<Hash> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (non-blocking): build_nodes leaves the root-send as an unenforced caller duty.

Splitting construction from the transcript write is good, but sending the root is now manual (prover_message(&nodes[num_nodes()-1])). A future caller that forgets it produces a commitment whose root never binds, with no compile error and probably no failing test. Consider funneling Witness creation through a method that takes prover_state and sends the root, or returning a #[must_use] wrapper that only a transcript-taking method unwraps. At minimum, document the obligation here.

Comment thread src/algebra/ntt/mod.rs
Comment on lines +131 to +136
fn interleaved_encode(
&self,
messages: Messages<'_, F>,
masks: &ActiveBuffer<F>,
codeword_length: usize,
) -> ActiveBuffer<F>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: ReedSolomon is a backend-abstraction trait but hardcodes the global ActiveBuffer alias in its signature, which defeats the purpose for the one trait that most needs it.

Give it an associated type Buffer: Buffer<F> and thread it through the registry (type Dyn<F> = dyn ReedSolomon<F, Buffer = ActiveBuffer<F>>); NttEngine then declares type Buffer = CpuBuffer<F>. The payoff is enforcement, not tidiness: today a second backend can't even be expressed here, since the trait forces every impl to speak CpuBuffer. With the associated type, an impl whose buffer doesn't match the build's ActiveBuffer fails to register as a type error instead of sitting as dead code that forces host round-trips if called.

Comment thread src/buffer/cpu/buffer.rs
Comment on lines +171 to +179
fn fold_pair(&mut self, other: &mut Self, weight: F) {
self.fold(weight);
other.fold(weight);
}

fn fold_pair_sumcheck_polynomial(&mut self, other: &mut Self, weight: F) -> (F, F) {
self.fold_pair(other, weight);
self.sumcheck_polynomial(other)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (non-blocking): these two overrides duplicate the trait defaults.

fold_pair and fold_pair_sumcheck_polynomial here have the same bodies as the defaults on Buffer. Drop both from the impl and inherit the defaults, unless an override is planned for a reason.

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.

2 participants