Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
f57a71a
fix(prover): pass the disk-spill argument at the four bench call sites
MauroToscano Sep 15, 2026
bcdd3dd
feat(multilinear): a hash parameter on the WHIR path, with keccak as …
MauroToscano Sep 15, 2026
29fbb45
feat(crypto,multilinear): RPX256 on the WHIR path, host side
MauroToscano Sep 16, 2026
c9c452c
chore(crypto): gate the hash-metrics feature in make lint and make test
MauroToscano Sep 16, 2026
60b6765
test(prover): the WHIR byte gate, over a canonically sorted EQ trace
MauroToscano Sep 16, 2026
0502406
feat(math-cuda,multilinear): RPX256 on the WHIR device path
MauroToscano Sep 16, 2026
140e269
fix(multilinear): the four clippy findings in gpu.rs, so the cuda lin…
MauroToscano Sep 16, 2026
2e23db6
feat(multilinear): derive the WHIR query count in integers, not f64
MauroToscano Sep 16, 2026
f2d93e1
feat(prover): LAMBDA_VM_WHIR_HASH selects the hash, keccak by default
MauroToscano Sep 16, 2026
43a1cb7
fix(prover): make the commit-phase bench follow the hash knob, and ma…
MauroToscano Sep 16, 2026
390909c
feat(crypto,math-cuda): dispatch the RPX grind to the device
MauroToscano Sep 16, 2026
a1c338b
bench(prover): print the device commit and grind counts after each co…
MauroToscano Sep 16, 2026
0cbc962
fix(prover): the byte gate must go through the hash dispatch, and assert
MauroToscano Sep 16, 2026
3e38c9d
perf(math-cuda): a commitment keeps the tree it built, so its leaves …
MauroToscano Sep 16, 2026
e14f63c
fix(math-cuda): route `paths` through the tree cache, and count per c…
MauroToscano Sep 16, 2026
8f13724
test(math-cuda): the reservation test owns the globals it asserts on
MauroToscano Sep 16, 2026
2997b97
perf(math-cuda): a commitment does not keep its tree — the retention …
MauroToscano Sep 16, 2026
107bde9
bench(prover): count host fallbacks beside the device commits
MauroToscano Sep 16, 2026
070749e
test(math-cuda): the group guard's margin has to survive the allocator
MauroToscano Sep 16, 2026
3e8c6fa
feat(crypto): the RPX transcript hands out canonical felts, keccak un…
MauroToscano Sep 16, 2026
3f75574
feat(crypto): count Fiat-Shamir absorbs and squeezes on BOTH arms
MauroToscano Sep 16, 2026
a783f2d
feat(stark,prover): the WHIR transcript is the configuration's, check…
MauroToscano Sep 16, 2026
2ab421a
fix(crypto,prover): count state reads apart from squeezes, and both s…
MauroToscano Sep 16, 2026
a850dd2
fix(prover): DECODE rows go in pc order, so the root is a function of…
MauroToscano Sep 16, 2026
c73568f
test(prover): pin the transcript pair, guarded by the guest ELF's sha256
MauroToscano Sep 16, 2026
0b6f99a
fix(prover): the pinned ELF sha is the measured one, and the skip lin…
MauroToscano Sep 17, 2026
8c933bd
feat(prover): pad a WHIR statement to a field element boundary
MauroToscano Sep 17, 2026
d4f983f
refactor(prover): the transcript pair pin is a base plus a per-branch…
MauroToscano Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 47 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ compile-programs compile-recursion-elfs clean-asm clean-rust clean-bench clean-s
clean-recursion-elfs clean test test-asm \
test-rust test-ethrex test-ethrex-offline test-executor test-syscalls test-flamegraph flamegraph-prover test-profile-recursion test-profile-recursion-single test-profile-recursion-multi \
test-profile-recursion-block recursion-profile-block-input \
test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-cuda-integration test-cuda-d1 test-cuda-fallback \
test-fast test-prover test-prover-all test-prover-debug test-disk-spill test-math-cuda test-rpx-host-kat test-cuda-integration test-cuda-d1 test-cuda-fallback \
test-prover-cuda test-prover-comprehensive-cuda \
bench-math-cuda bench-prover bench-prover-cuda build check clippy fmt lint regen-ethrex-fixtures \
update-ethrex-fixture-checksums check-ethrex-fixture-checksums ethrex-real-block-fixture \
Expand Down Expand Up @@ -545,6 +545,20 @@ test-ethrex-crypto:

test: compile-programs test-syscalls test-ethrex-crypto
cargo test
# The hash counters compile to nothing unless the feature is on, so their
# own tests only execute here. See the `lint` target for why an instrument
# nobody runs is worth a line in the build.
cargo test -p crypto --features hash-metrics
# The transcript counters answer "which sponge ran". Their own integration
# binary, because the counters are process-global and a parallel neighbour's
# reset lands inside another test's measurement window — moving them out of
# the lib binary left four of five failing until they also took a lock.
cargo test -p crypto --features hash-metrics --test transcript_counters
# And the system test that reads them through a real prove: it is the one
# that says the PROVER picked the configuration's sponge, which the
# type-level test next to it cannot observe.
cargo test -p lambda-vm-prover --features hash-metrics --test whir_transcript_configuration
$(MAKE) test-rpx-host-kat

# === Quick test shortcuts ===

Expand All @@ -553,6 +567,28 @@ test: compile-programs test-syscalls test-ethrex-crypto
test-fast: compile-recursion-elfs
cargo test -p lambda-vm-prover -p stark -p executor -F stark/parallel

# ★ The RPX device kernel's arithmetic, checked WITHOUT a GPU.
#
# `kernels/rpx.cu` is compiled as ordinary host C++ through `cuda_host_shim.h`,
# so its field primitives, MDS, S-boxes, cubic extension, seven-round schedule,
# leaf sponge, Merkle parent and every leaf kernel's read pattern are pinned in
# seconds on a laptop. That matters here because GPU CI runs only on
# merge_group, so without this the two WHIR coset kernels — which exist nowhere
# else — would reach a GPU unchecked.
#
# ⚠ Necessary, never sufficient: it cannot tell you whether nvcc accepts the
# file, nor anything about execution rather than arithmetic (grid indexing,
# register pressure, local-memory spills). Those still belong to the GPU tests.
HOST_KAT_DIR := crypto/math-cuda/tests/host_kat
HOST_KAT_CXXFLAGS := -std=c++17 -O2 -Wall -Wno-unknown-pragmas \
-I$(HOST_KAT_DIR) -Icrypto/math-cuda/kernels

test-rpx-host-kat:
@mkdir -p target/host_kat
$(CXX) $(HOST_KAT_CXXFLAGS) \
-o target/host_kat/rpx_host_kat $(HOST_KAT_DIR)/rpx_host_kat.cpp
./target/host_kat/rpx_host_kat

# Prover tests only
test-prover: compile-recursion-elfs
cargo test -p lambda-vm-prover
Expand Down Expand Up @@ -690,6 +726,16 @@ lint:
# cubin stubs when nvcc is absent, so this checks on a GPU-less host (CI lint runner, dev laptop)
# too — no GPU required. Catches cuda-gated breakage that the non-cuda passes above miss.
cargo clippy --workspace --all-targets --features lambda-vm-prover/cuda -- -D warnings -A clippy::op_ref
# `hash-metrics` is host-only and off by default, so no pass above compiles it.
# Without this line the feature can rot untouched — which is how its Merkle
# counters stayed keccak-only after a second hash arrived, reporting ZERO for
# the arm whose whole purpose was to change the hashing. Lints, does not run:
# its tests are in the `test` target.
cargo clippy -p crypto --all-targets --features hash-metrics -- -D warnings -A clippy::op_ref
# The prover's own `hash-metrics` passthrough gates the per-arm transcript
# line and the system test that reads it; without this line neither compiles
# in any pass, which is how an instrument rots.
cargo clippy -p lambda-vm-prover --all-targets --features hash-metrics -- -D warnings -A clippy::op_ref

flamegraph-prover:
cd crypto/stark && samply record cargo bench --bench profile_prover --features parallel
101 changes: 84 additions & 17 deletions crypto/crypto/src/fiat_shamir/default_transcript.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::fiat_shamir::is_transcript::{IsStarkTranscript, IsTranscript};
use crate::fiat_shamir::transcript_hash::{KeccakTranscriptHash, TranscriptHash};

use crate::hash::platform_keccak::PlatformKeccak256 as Keccak256;
use core::marker::PhantomData;
use digest::Digest;
use math::{
Expand All @@ -16,8 +16,8 @@ use math::{
/// per squeeze).
const SQUEEZE_LEN: usize = 32;

/// Keccak-sponge Fiat-Shamir transcript with a Plonky3-style duplex output
/// buffer.
/// Sponge Fiat-Shamir transcript with a Plonky3-style duplex output buffer,
/// over the hash `T` names.
///
/// Challenges are derived by squeezing the sponge and rejection-sampling field
/// coordinates directly from those bytes — there is **no CSPRNG**. Earlier this
Expand All @@ -28,19 +28,25 @@ const SQUEEZE_LEN: usize = 32;
/// free. The output buffer amortizes one squeeze across up to `SQUEEZE_LEN / 8`
/// 64-bit candidates, so a cubic-extension element (3 coordinates) usually costs
/// a single squeeze.
pub struct DefaultTranscript<F: HasDefaultTranscript> {
hasher: Keccak256,
///
/// `T` defaults to [`KeccakTranscriptHash`], so `DefaultTranscript::<F>::new(..)`
/// still names exactly the transcript this system has always produced: every
/// method body below is hash-agnostic, and the parameter only decides which
/// `digest::Digest` the sponge is. Nothing about the keccak configuration's
/// bytes moves.
pub struct DefaultTranscript<F: HasDefaultTranscript, T: TranscriptHash = KeccakTranscriptHash> {
hasher: T::Digest,
/// Duplex output buffer: bytes squeezed from the sponge, consumed 8 at a
/// time by field/`u64` sampling. Positions `[out_pos, SQUEEZE_LEN)` are the
/// bytes not yet handed out; `out_pos == SQUEEZE_LEN` means "empty, squeeze
/// to refill". Absorbing new data invalidates it (see `append_bytes`) so a
/// squeeze can never reflect input appended after it was produced.
out_buf: [u8; SQUEEZE_LEN],
out_pos: usize,
phantom: PhantomData<F>,
phantom: PhantomData<(F, T)>,
}

impl<F: HasDefaultTranscript> Clone for DefaultTranscript<F> {
impl<F: HasDefaultTranscript, T: TranscriptHash> Clone for DefaultTranscript<F, T> {
fn clone(&self) -> Self {
Self {
hasher: self.hasher.clone(),
Expand All @@ -51,14 +57,15 @@ impl<F: HasDefaultTranscript> Clone for DefaultTranscript<F> {
}
}

impl<F> DefaultTranscript<F>
impl<F, T> DefaultTranscript<F, T>
where
F: HasDefaultTranscript,
T: TranscriptHash,
FieldElement<F>: AsBytes,
{
pub fn new(data: &[u8]) -> Self {
let mut res = Self {
hasher: Keccak256::new(),
hasher: T::Digest::new(),
out_buf: [0u8; SQUEEZE_LEN],
// Empty: the first sample forces a squeeze.
out_pos: SQUEEZE_LEN,
Expand All @@ -69,12 +76,34 @@ where
}

/// Raw squeeze: finalize the current sponge state, advance the hash chain by
/// absorbing the (reversed) output, and return it. Also invalidates the
/// duplex output buffer, so interleaving raw `sample()` calls with buffered
/// field/`u64` sampling can never reuse stale squeeze bytes.
/// absorbing the output, and return it. Also invalidates the duplex output
/// buffer, so interleaving raw `sample()` calls with buffered field/`u64`
/// sampling can never reuse stale squeeze bytes.
///
/// ★ The byte order is the configuration's, via
/// [`TranscriptHash::REVERSES_SQUEEZE`] — `true` for keccak, which is the
/// convention every proof on this branch has been produced under, and
/// `false` for an algebraic sponge, whose squeeze is already four canonical
/// felts and whose consumer is a field-native verifier that would otherwise
/// spend rows undoing the reversal.
///
/// ⚠ The returned bytes and the chained bytes are the SAME value, and that
/// is deliberate: a replaying verifier reproducing this chain would
/// otherwise have two byte conventions to carry instead of none. Whichever
/// order the constant selects applies to both.
///
/// The constant is associated, so each configuration monomorphises to
/// straight-line code — the keccak arm keeps the instruction sequence it
/// had before this became a choice.
pub fn sample(&mut self) -> [u8; 32] {
// ★ Hash-agnostic, and deliberately here rather than inside a digest:
// a counter that lives in keccak reads ZERO for an algebraic
// transcript, which is indistinguishable from "no transcript ran".
crate::hash_metrics::count_transcript_squeeze::<T::Digest>();
let mut result_hash: [u8; 32] = self.hasher.finalize_reset().into();
result_hash.reverse();
if T::REVERSES_SQUEEZE {
result_hash.reverse();
}
self.hasher.update(result_hash);
self.out_pos = SQUEEZE_LEN;
result_hash
Expand All @@ -83,6 +112,15 @@ where
/// Next 64-bit candidate from the duplex output buffer, refilling with one
/// squeeze when fewer than 8 bytes remain. Big-endian, matching the byte
/// order `sample_u64` used when it read directly from `sample()`.
///
/// ★ `SQUEEZE_LEN` is 32 and every read is 8, so `out_pos` only ever takes
/// the values `0, 8, 16, 24, 32` and a candidate is always a whole 8-byte
/// group — never two halves of adjacent ones. That is what lets a
/// configuration whose squeeze is four canonical felts promise
/// `CANDIDATES_PER_COORDINATE = Some(1)`: the felt boundaries and the read
/// boundaries are the same boundaries. `append_bytes` invalidates the
/// buffer wholesale rather than partially, so the alignment survives
/// interleaved absorbs.
fn next_sample_u64(&mut self) -> u64 {
if self.out_pos + 8 > SQUEEZE_LEN {
self.out_buf = self.sample();
Expand All @@ -95,37 +133,65 @@ where
}
}

impl<F> Default for DefaultTranscript<F>
impl<F, T> crate::fiat_shamir::transcript_hash::HasTranscriptHash for DefaultTranscript<F, T>
where
F: HasDefaultTranscript,
T: TranscriptHash,
{
type Hash = T;
}

impl<F, T> Default for DefaultTranscript<F, T>
where
F: HasDefaultTranscript,
T: TranscriptHash,
FieldElement<F>: AsBytes,
{
fn default() -> Self {
Self::new(&[])
}
}

impl<F> IsTranscript<F> for DefaultTranscript<F>
impl<F, T> IsTranscript<F> for DefaultTranscript<F, T>
where
F: HasDefaultTranscript,
T: TranscriptHash,
FieldElement<F>: AsBytes,
{
fn append_bytes(&mut self, new_bytes: &[u8]) {
// Absorbing new input invalidates any buffered squeeze output: a
// subsequent challenge must depend on this input, so drop the bytes
// squeezed before it.
self.out_pos = SQUEEZE_LEN;
crate::hash_metrics::count_transcript_absorb::<T::Digest>();
self.hasher.update(new_bytes);
}

fn append_field_element(&mut self, element: &FieldElement<F>) {
// Absorb, same invalidation as `append_bytes` (the field element's bytes
// are streamed straight into the sponge with no intermediate `Vec`).
//
// ⚠ Counted PER `update` rather than once per call, because that is the
// unit `absorb_calls` has always used and the dimension a block-
// absorption change moves. Today the degree-3 extension writes one
// 24-byte buffer and calls the sink once, so the two happen to agree —
// a field or a serialisation that streams in pieces would not, and the
// counter should follow the sponge rather than the argument list.
self.out_pos = SQUEEZE_LEN;
element.stream_bytes(&mut |b| self.hasher.update(b));
let hasher = &mut self.hasher;
element.stream_bytes(&mut |b| {
crate::hash_metrics::count_transcript_absorb::<T::Digest>();
hasher.update(b);
});
}

fn state(&self) -> [u8; 32] {
// ★ Counted, and NOT as a squeeze. This finalizes a CLONE: no reset and
// no re-absorb, so the chain does not advance and a counter hooked to
// `sample` cannot see it. There is one per grind check — 2,996 on a
// block proof against 182,734 squeezes — so a counter that reported
// only their sum could be checked against neither.
crate::hash_metrics::count_transcript_state::<T::Digest>();
self.hasher.clone().finalize().into()
}

Expand All @@ -145,9 +211,10 @@ where
}
}

impl<F, S> IsStarkTranscript<F, S> for DefaultTranscript<F>
impl<F, T, S> IsStarkTranscript<F, S> for DefaultTranscript<F, T>
where
F: HasDefaultTranscript,
T: TranscriptHash,
FieldElement<F>: AsBytes,
S: IsField + IsSubFieldOf<F>,
{
Expand Down
1 change: 1 addition & 0 deletions crypto/crypto/src/fiat_shamir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@

pub mod default_transcript;
pub mod is_transcript;
pub mod transcript_hash;
Loading
Loading