From 67f5e7563839be0d5b17a333d09e38e3fcf975c7 Mon Sep 17 00:00:00 2001 From: MauroFab Date: Thu, 16 Jul 2026 11:38:37 -0300 Subject: [PATCH 1/3] test(math): pin stream_bytes byte parity with as_bytes #828 rewired the Merkle backends and DefaultTranscript::append_field_element from as_bytes()/to_bytes_be() to the new AsBytes::stream_bytes. That swap is only sound because stream_bytes emits byte-identical output, but nothing enforces it: stream_bytes is a defaulted trait method, so an override that disagrees with as_bytes compiles cleanly and then silently changes Merkle roots and Fiat-Shamir challenges rather than failing a test. Adds a parity sweep over both Goldilocks fields, including non-canonical u64s around the modulus where a raw-value override would diverge from as_bytes, and pins the ext3 byte layout that math-cuda's keccak_leaves_ext3 kernel mirrors component-by-component. The GPU parity tests only run on a CUDA host, so this keeps the CPU half of that contract honest on a GPU-less runner. Documents the invariant on the trait method itself, where an implementor would look for it. Also corrects the stream_bytes comment in extensions_goldilocks.rs: it described cutting three sink calls down to one, but as_bytes() already produced a single 24-byte Vec and a single Digest::update. The per-element allocation, not the call count, is what the override removes. --- .../math/src/field/extensions_goldilocks.rs | 12 +- crypto/math/src/traits.rs | 8 + crypto/math/tests/stream_bytes_parity.rs | 175 ++++++++++++++++++ 3 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 crypto/math/tests/stream_bytes_parity.rs diff --git a/crypto/math/src/field/extensions_goldilocks.rs b/crypto/math/src/field/extensions_goldilocks.rs index 246a3cb87..4dc365330 100644 --- a/crypto/math/src/field/extensions_goldilocks.rs +++ b/crypto/math/src/field/extensions_goldilocks.rs @@ -555,10 +555,14 @@ impl AsBytes for FieldElement { 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. + // Same 24 bytes as `as_bytes`, staged in a stack buffer so the guest skips + // the per-element `Vec`; `#[inline(always)]` is what lets the `dyn` sink + // devirtualize at the call site. Emitting them in one call rather than one + // per limb keeps it to a single `Digest::update`. + // + // The layout is load-bearing beyond this crate: `math-cuda`'s + // `keccak_leaves_ext3` kernel reads components in order 0,1,2 to match + // `write_bytes_be`, and CPU/GPU leaf parity depends on the two agreeing. #[inline(always)] fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { let mut buf = [0u8; 24]; diff --git a/crypto/math/src/traits.rs b/crypto/math/src/traits.rs index e16b5bfb1..758e5163c 100644 --- a/crypto/math/src/traits.rs +++ b/crypto/math/src/traits.rs @@ -42,6 +42,14 @@ pub trait AsBytes { /// 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. + /// + /// An override must stream exactly the bytes `as_bytes` would return, in + /// order; splitting them across several `sink` calls is fine, but the + /// concatenation must be identical. Merkle leaf hashes and the Fiat-Shamir + /// transcript take their input through here, so an override that disagrees + /// with `as_bytes` silently changes commitments and challenges rather than + /// failing to compile. `math/tests/stream_bytes_parity.rs` pins this for the + /// Goldilocks fields. fn stream_bytes(&self, sink: &mut dyn FnMut(&[u8])) { sink(&self.as_bytes()); } diff --git a/crypto/math/tests/stream_bytes_parity.rs b/crypto/math/tests/stream_bytes_parity.rs new file mode 100644 index 000000000..ac8a54971 --- /dev/null +++ b/crypto/math/tests/stream_bytes_parity.rs @@ -0,0 +1,175 @@ +//! `AsBytes::stream_bytes` must emit exactly the bytes `as_bytes` returns. +//! +//! Nothing in the type system enforces it: `stream_bytes` is a defaulted trait +//! method, so an override that disagrees with `as_bytes` compiles cleanly and +//! then silently changes every Merkle leaf hash and Fiat-Shamir challenge that +//! flows through it — the transcript and the Merkle backends stream their input +//! rather than calling `as_bytes`. A divergence would surface as proofs that no +//! longer verify against previously committed roots, not as a test failure, so +//! it is pinned here. +//! +//! `ext3_stream_bytes_matches_gpu_kernel_contract` additionally pins the ext3 +//! byte layout that `crypto/math-cuda/src/merkle.rs` mirrors: the GPU +//! `keccak_leaves_ext3` kernel reads three canonical u64s per column in +//! component order 0,1,2 to match `write_bytes_be`. CPU/GPU leaf parity depends +//! on the two staying in agreement, and the GPU parity tests only run on a CUDA +//! host, so this keeps the CPU half honest on a GPU-less runner. + +use math::field::element::FieldElement; +use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; +use math::field::goldilocks::GoldilocksField; +use math::traits::{AsBytes, ByteConversion}; + +type Fp = FieldElement; +type Fp3 = FieldElement; + +fn streamed(e: &T) -> Vec { + let mut out = Vec::new(); + e.stream_bytes(&mut |b| out.extend_from_slice(b)); + out +} + +/// Deterministic LCG: keeps the sweep reproducible and dependency-free. +fn lcg(state: &mut u64) -> u64 { + *state = state + .wrapping_mul(6364136223846793005) + .wrapping_add(1442695040888963407); + *state +} + +const GOLDILOCKS_P: u64 = 0xFFFF_FFFF_0000_0001; + +/// Values around the modulus matter: both encodings reduce through +/// `canonical_u64`, so a non-canonical `u64` is where a raw-value override would +/// diverge from `as_bytes`. +fn edge_values() -> Vec { + vec![ + 0, + 1, + 2, + u32::MAX as u64, + 1u64 << 32, + GOLDILOCKS_P - 1, + GOLDILOCKS_P, // 0 in the field + GOLDILOCKS_P + 1, // 1 in the field + u64::MAX - 1, + u64::MAX, + ] +} + +#[test] +fn goldilocks_stream_bytes_matches_as_bytes_and_to_bytes_be() { + let mut state = 0x1234_5678_9abc_def0u64; + let mut values = edge_values(); + values.extend((0..2000).map(|_| lcg(&mut state))); + + for v in values { + let e = Fp::from(v); + let s = streamed(&e); + assert_eq!(s.len(), 8, "goldilocks stream must be 8 bytes (v={v:#x})"); + // The Merkle backends stream instead of calling `as_bytes`. + assert_eq!(s, e.as_bytes(), "stream != as_bytes (v={v:#x})"); + // `DefaultTranscript::append_field_element` streams instead of + // appending `to_bytes_be`. + assert_eq!( + s, + ByteConversion::to_bytes_be(&e), + "stream != to_bytes_be (v={v:#x})" + ); + } +} + +#[test] +fn ext3_stream_bytes_matches_as_bytes_and_to_bytes_be() { + let mut state = 0x0fed_cba9_8765_4321u64; + let mut triples: Vec<[u64; 3]> = Vec::new(); + for v in edge_values() { + triples.push([v, v, v]); + triples.push([v, 0, 1]); + } + triples.extend((0..2000).map(|_| [lcg(&mut state), lcg(&mut state), lcg(&mut state)])); + + for t in triples { + let e = Fp3::new([Fp::from(t[0]), Fp::from(t[1]), Fp::from(t[2])]); + let s = streamed(&e); + assert_eq!(s.len(), 24, "ext3 stream must be 24 bytes (t={t:?})"); + assert_eq!(s, e.as_bytes(), "stream != as_bytes (t={t:?})"); + assert_eq!( + s, + ByteConversion::to_bytes_be(&e), + "stream != to_bytes_be (t={t:?})" + ); + } +} + +#[test] +fn ext3_stream_bytes_matches_gpu_kernel_contract() { + let mut state = 0xdead_beef_cafe_babeu64; + + for _ in 0..1000 { + let e = Fp3::new([ + Fp::from(lcg(&mut state)), + Fp::from(lcg(&mut state)), + Fp::from(lcg(&mut state)), + ]); + + // What the CUDA kernel builds: canonical u64 per component, big-endian, + // component order 0,1,2. + let mut expected = Vec::new(); + for component in e.value() { + expected.extend_from_slice(&component.canonical_u64().to_be_bytes()); + } + assert_eq!(streamed(&e), expected, "ext3 stream != canonical-BE 0,1,2"); + + let mut buf = [0u8; 24]; + ByteConversion::write_bytes_be(&e, &mut buf); + assert_eq!(streamed(&e), buf, "ext3 stream != write_bytes_be"); + } +} + +/// Keccak absorption means `update(a); update(b)` == `update(a || b)`, so a +/// digest can only move if the concatenated stream moves. Pins the multi-element +/// hash paths (`hash_data`, `hash_data_from_slices`) against the old +/// `as_bytes`-per-element input. +#[test] +fn concatenated_stream_matches_concatenated_as_bytes() { + let mut state = 0xa5a5_5a5a_a5a5_5a5au64; + let elements: Vec = (0..256) + .map(|_| { + Fp3::new([ + Fp::from(lcg(&mut state)), + Fp::from(lcg(&mut state)), + Fp::from(lcg(&mut state)), + ]) + }) + .collect(); + + let mut via_as_bytes = Vec::new(); + let mut via_stream = Vec::new(); + for e in &elements { + via_as_bytes.extend_from_slice(&e.as_bytes()); + e.stream_bytes(&mut |b| via_stream.extend_from_slice(b)); + } + + assert_eq!( + via_as_bytes, via_stream, + "concatenated hasher input stream changed" + ); +} + +/// The default `stream_bytes` body forwards to `as_bytes`; a type that does not +/// override it must still round-trip identically. +#[test] +fn default_stream_bytes_impl_matches_as_bytes() { + struct Unoverridden(Vec); + impl AsBytes for Unoverridden { + fn as_bytes(&self) -> Vec { + self.0.clone() + } + } + + for bytes in [vec![], vec![0u8], vec![1, 2, 3, 4, 5], vec![0xff; 64]] { + let v = Unoverridden(bytes.clone()); + assert_eq!(streamed(&v), bytes); + } +} From 38bbf88a7337170cb49dfe733073b5b1e3c76e7a Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Thu, 16 Jul 2026 18:33:28 -0300 Subject: [PATCH 2/3] convert to proptest --- crypto/math/tests/stream_bytes_parity.rs | 244 ++++++++++++----------- 1 file changed, 131 insertions(+), 113 deletions(-) diff --git a/crypto/math/tests/stream_bytes_parity.rs b/crypto/math/tests/stream_bytes_parity.rs index ac8a54971..96b0ba596 100644 --- a/crypto/math/tests/stream_bytes_parity.rs +++ b/crypto/math/tests/stream_bytes_parity.rs @@ -14,11 +14,17 @@ //! component order 0,1,2 to match `write_bytes_be`. CPU/GPU leaf parity depends //! on the two staying in agreement, and the GPU parity tests only run on a CUDA //! host, so this keeps the CPU half honest on a GPU-less runner. +//! +//! Each check is a plain function shared by two tests: a deterministic `#[test]` +//! over hand-picked edge cases (always runs, no reliance on proptest landing on +//! them) and a `proptest!` sweep over arbitrary input for everything else. use math::field::element::FieldElement; use math::field::extensions_goldilocks::Degree3GoldilocksExtensionField; use math::field::goldilocks::GoldilocksField; use math::traits::{AsBytes, ByteConversion}; +use proptest::collection::vec; +use proptest::prelude::*; type Fp = FieldElement; type Fp3 = FieldElement; @@ -29,147 +35,159 @@ fn streamed(e: &T) -> Vec { out } -/// Deterministic LCG: keeps the sweep reproducible and dependency-free. -fn lcg(state: &mut u64) -> u64 { - *state = state - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - *state +fn fp3(t: [u64; 3]) -> Fp3 { + Fp3::new([Fp::from(t[0]), Fp::from(t[1]), Fp::from(t[2])]) } const GOLDILOCKS_P: u64 = 0xFFFF_FFFF_0000_0001; /// Values around the modulus matter: both encodings reduce through -/// `canonical_u64`, so a non-canonical `u64` is where a raw-value override would -/// diverge from `as_bytes`. -fn edge_values() -> Vec { - vec![ - 0, - 1, - 2, - u32::MAX as u64, - 1u64 << 32, - GOLDILOCKS_P - 1, - GOLDILOCKS_P, // 0 in the field - GOLDILOCKS_P + 1, // 1 in the field - u64::MAX - 1, - u64::MAX, - ] +/// `canonical_u64`, so a non-canonical `u64` is where a raw-value override +/// would diverge from `as_bytes`. +const EDGE_VALUES: [u64; 10] = [ + 0, + 1, + 2, + u32::MAX as u64, + 1u64 << 32, + GOLDILOCKS_P - 1, + GOLDILOCKS_P, // 0 in the field + GOLDILOCKS_P + 1, // 1 in the field + u64::MAX - 1, + u64::MAX, +]; + +fn check_goldilocks_stream_bytes(v: u64) { + let e = Fp::from(v); + let s = streamed(&e); + assert_eq!(s.len(), 8, "goldilocks stream must be 8 bytes (v={v:#x})"); + // The Merkle backends stream instead of calling `as_bytes`. + assert_eq!(s, e.as_bytes(), "stream != as_bytes (v={v:#x})"); + // `DefaultTranscript::append_field_element` streams instead of + // appending `to_bytes_be`. + assert_eq!( + s, + ByteConversion::to_bytes_be(&e), + "stream != to_bytes_be (v={v:#x})" + ); } #[test] -fn goldilocks_stream_bytes_matches_as_bytes_and_to_bytes_be() { - let mut state = 0x1234_5678_9abc_def0u64; - let mut values = edge_values(); - values.extend((0..2000).map(|_| lcg(&mut state))); - - for v in values { - let e = Fp::from(v); - let s = streamed(&e); - assert_eq!(s.len(), 8, "goldilocks stream must be 8 bytes (v={v:#x})"); - // The Merkle backends stream instead of calling `as_bytes`. - assert_eq!(s, e.as_bytes(), "stream != as_bytes (v={v:#x})"); - // `DefaultTranscript::append_field_element` streams instead of - // appending `to_bytes_be`. - assert_eq!( - s, - ByteConversion::to_bytes_be(&e), - "stream != to_bytes_be (v={v:#x})" - ); +fn goldilocks_stream_bytes_matches_as_bytes_and_to_bytes_be_edge_cases() { + for v in EDGE_VALUES { + check_goldilocks_stream_bytes(v); } } +fn check_ext3_stream_bytes(t: [u64; 3]) { + let e = fp3(t); + let s = streamed(&e); + assert_eq!(s.len(), 24, "ext3 stream must be 24 bytes (t={t:?})"); + assert_eq!(s, e.as_bytes(), "stream != as_bytes (t={t:?})"); + assert_eq!( + s, + ByteConversion::to_bytes_be(&e), + "stream != to_bytes_be (t={t:?})" + ); +} + #[test] -fn ext3_stream_bytes_matches_as_bytes_and_to_bytes_be() { - let mut state = 0x0fed_cba9_8765_4321u64; - let mut triples: Vec<[u64; 3]> = Vec::new(); - for v in edge_values() { - triples.push([v, v, v]); - triples.push([v, 0, 1]); - } - triples.extend((0..2000).map(|_| [lcg(&mut state), lcg(&mut state), lcg(&mut state)])); - - for t in triples { - let e = Fp3::new([Fp::from(t[0]), Fp::from(t[1]), Fp::from(t[2])]); - let s = streamed(&e); - assert_eq!(s.len(), 24, "ext3 stream must be 24 bytes (t={t:?})"); - assert_eq!(s, e.as_bytes(), "stream != as_bytes (t={t:?})"); - assert_eq!( - s, - ByteConversion::to_bytes_be(&e), - "stream != to_bytes_be (t={t:?})" - ); +fn ext3_stream_bytes_matches_as_bytes_and_to_bytes_be_edge_cases() { + for v in EDGE_VALUES { + check_ext3_stream_bytes([v, v, v]); + check_ext3_stream_bytes([v, 0, 1]); } } -#[test] -fn ext3_stream_bytes_matches_gpu_kernel_contract() { - let mut state = 0xdead_beef_cafe_babeu64; - - for _ in 0..1000 { - let e = Fp3::new([ - Fp::from(lcg(&mut state)), - Fp::from(lcg(&mut state)), - Fp::from(lcg(&mut state)), - ]); - - // What the CUDA kernel builds: canonical u64 per component, big-endian, - // component order 0,1,2. - let mut expected = Vec::new(); - for component in e.value() { - expected.extend_from_slice(&component.canonical_u64().to_be_bytes()); - } - assert_eq!(streamed(&e), expected, "ext3 stream != canonical-BE 0,1,2"); +fn check_ext3_stream_bytes_gpu_kernel_contract(t: [u64; 3]) { + let e = fp3(t); - let mut buf = [0u8; 24]; - ByteConversion::write_bytes_be(&e, &mut buf); - assert_eq!(streamed(&e), buf, "ext3 stream != write_bytes_be"); + // What the CUDA kernel builds: canonical u64 per component, big-endian, + // component order 0,1,2. + let mut expected = Vec::new(); + for component in e.value() { + expected.extend_from_slice(&component.canonical_u64().to_be_bytes()); } + assert_eq!( + streamed(&e), + expected, + "ext3 stream != canonical-BE 0,1,2 (t={t:?})" + ); + + let mut buf = [0u8; 24]; + ByteConversion::write_bytes_be(&e, &mut buf); + assert_eq!(streamed(&e), buf, "ext3 stream != write_bytes_be (t={t:?})"); } -/// Keccak absorption means `update(a); update(b)` == `update(a || b)`, so a -/// digest can only move if the concatenated stream moves. Pins the multi-element -/// hash paths (`hash_data`, `hash_data_from_slices`) against the old -/// `as_bytes`-per-element input. #[test] -fn concatenated_stream_matches_concatenated_as_bytes() { - let mut state = 0xa5a5_5a5a_a5a5_5a5au64; - let elements: Vec = (0..256) - .map(|_| { - Fp3::new([ - Fp::from(lcg(&mut state)), - Fp::from(lcg(&mut state)), - Fp::from(lcg(&mut state)), - ]) - }) - .collect(); - - let mut via_as_bytes = Vec::new(); - let mut via_stream = Vec::new(); - for e in &elements { - via_as_bytes.extend_from_slice(&e.as_bytes()); - e.stream_bytes(&mut |b| via_stream.extend_from_slice(b)); +fn ext3_stream_bytes_matches_gpu_kernel_contract_edge_cases() { + for v in EDGE_VALUES { + check_ext3_stream_bytes_gpu_kernel_contract([v, v, v]); + check_ext3_stream_bytes_gpu_kernel_contract([v, 0, 1]); } - - assert_eq!( - via_as_bytes, via_stream, - "concatenated hasher input stream changed" - ); } /// The default `stream_bytes` body forwards to `as_bytes`; a type that does not /// override it must still round-trip identically. +struct Unoverridden(Vec); +impl AsBytes for Unoverridden { + fn as_bytes(&self) -> Vec { + self.0.clone() + } +} + +fn check_default_stream_bytes_impl(bytes: Vec) { + let v = Unoverridden(bytes.clone()); + assert_eq!(streamed(&v), bytes); +} + #[test] -fn default_stream_bytes_impl_matches_as_bytes() { - struct Unoverridden(Vec); - impl AsBytes for Unoverridden { - fn as_bytes(&self) -> Vec { - self.0.clone() +fn default_stream_bytes_impl_matches_as_bytes_edge_cases() { + for bytes in [vec![], vec![0u8], vec![1, 2, 3, 4, 5], vec![0xff; 64]] { + check_default_stream_bytes_impl(bytes); + } +} + +proptest! { + #![proptest_config(ProptestConfig::with_cases(1024))] + + #[test] + fn goldilocks_stream_bytes_matches_as_bytes_and_to_bytes_be(v in any::()) { + check_goldilocks_stream_bytes(v); + } + + #[test] + fn ext3_stream_bytes_matches_as_bytes_and_to_bytes_be(a in any::(), b in any::(), c in any::()) { + check_ext3_stream_bytes([a, b, c]); + } + + #[test] + fn ext3_stream_bytes_matches_gpu_kernel_contract(a in any::(), b in any::(), c in any::()) { + check_ext3_stream_bytes_gpu_kernel_contract([a, b, c]); + } + + // Keccak absorption means `update(a); update(b)` == `update(a || b)`, so a + // digest can only move if the concatenated stream moves. Pins the multi-element + // hash paths (`hash_data`, `hash_data_from_slices`) against the old + // `as_bytes`-per-element input. + #[test] + fn concatenated_stream_matches_concatenated_as_bytes( + triples in vec((any::(), any::(), any::()), 0..64) + ) { + let elements: Vec = triples.into_iter().map(|(a, b, c)| fp3([a, b, c])).collect(); + + let mut via_as_bytes = Vec::new(); + let mut via_stream = Vec::new(); + for e in &elements { + via_as_bytes.extend_from_slice(&e.as_bytes()); + e.stream_bytes(&mut |b| via_stream.extend_from_slice(b)); } + + prop_assert_eq!(via_as_bytes, via_stream, "concatenated hasher input stream changed"); } - for bytes in [vec![], vec![0u8], vec![1, 2, 3, 4, 5], vec![0xff; 64]] { - let v = Unoverridden(bytes.clone()); - assert_eq!(streamed(&v), bytes); + #[test] + fn default_stream_bytes_impl_matches_as_bytes(bytes in vec(any::(), 0..64)) { + check_default_stream_bytes_impl(bytes); } } From 091e6856694615021a224089c3e9e22347655aa9 Mon Sep 17 00:00:00 2001 From: Mario Rugiero Date: Thu, 16 Jul 2026 18:40:51 -0300 Subject: [PATCH 3/3] test(math): merge stream_bytes edge-case tests into one Four near-identical `_edge_cases` wrappers, one per property, added pure boilerplate on top of the shared check_* helpers. Fold them into a single edge_cases test that runs all the checks over EDGE_VALUES. --- crypto/math/tests/stream_bytes_parity.rs | 32 ++++++------------------ 1 file changed, 8 insertions(+), 24 deletions(-) diff --git a/crypto/math/tests/stream_bytes_parity.rs b/crypto/math/tests/stream_bytes_parity.rs index 96b0ba596..3a012cf76 100644 --- a/crypto/math/tests/stream_bytes_parity.rs +++ b/crypto/math/tests/stream_bytes_parity.rs @@ -72,13 +72,6 @@ fn check_goldilocks_stream_bytes(v: u64) { ); } -#[test] -fn goldilocks_stream_bytes_matches_as_bytes_and_to_bytes_be_edge_cases() { - for v in EDGE_VALUES { - check_goldilocks_stream_bytes(v); - } -} - fn check_ext3_stream_bytes(t: [u64; 3]) { let e = fp3(t); let s = streamed(&e); @@ -91,14 +84,6 @@ fn check_ext3_stream_bytes(t: [u64; 3]) { ); } -#[test] -fn ext3_stream_bytes_matches_as_bytes_and_to_bytes_be_edge_cases() { - for v in EDGE_VALUES { - check_ext3_stream_bytes([v, v, v]); - check_ext3_stream_bytes([v, 0, 1]); - } -} - fn check_ext3_stream_bytes_gpu_kernel_contract(t: [u64; 3]) { let e = fp3(t); @@ -119,14 +104,6 @@ fn check_ext3_stream_bytes_gpu_kernel_contract(t: [u64; 3]) { assert_eq!(streamed(&e), buf, "ext3 stream != write_bytes_be (t={t:?})"); } -#[test] -fn ext3_stream_bytes_matches_gpu_kernel_contract_edge_cases() { - for v in EDGE_VALUES { - check_ext3_stream_bytes_gpu_kernel_contract([v, v, v]); - check_ext3_stream_bytes_gpu_kernel_contract([v, 0, 1]); - } -} - /// The default `stream_bytes` body forwards to `as_bytes`; a type that does not /// override it must still round-trip identically. struct Unoverridden(Vec); @@ -142,7 +119,14 @@ fn check_default_stream_bytes_impl(bytes: Vec) { } #[test] -fn default_stream_bytes_impl_matches_as_bytes_edge_cases() { +fn edge_cases() { + for v in EDGE_VALUES { + check_goldilocks_stream_bytes(v); + check_ext3_stream_bytes([v, v, v]); + check_ext3_stream_bytes([v, 0, 1]); + check_ext3_stream_bytes_gpu_kernel_contract([v, v, v]); + check_ext3_stream_bytes_gpu_kernel_contract([v, 0, 1]); + } for bytes in [vec![], vec![0u8], vec![1, 2, 3, 4, 5], vec![0xff; 64]] { check_default_stream_bytes_impl(bytes); }