Skip to main content

binius_circuits/
slice.rs

1// Copyright 2025 Irreducible Inc.
2use binius_core::word::Word;
3use binius_frontend::{CircuitBuilder, Wire};
4
5use crate::{
6	multiplexer::rotate_left_dynamic,
7	shift::{var_sll_bytes, var_srl_bytes},
8};
9
10/// Asserts that two byte sequences (packed little-endian into 64-bit words) are equal in their
11/// first `len_bytes` bytes. Bytes past `len_bytes` are ignored on both sides.
12///
13/// For each word index `i`, the saturating difference `diff = len_bytes - i*8` selects between:
14/// - full-word `assert_eq` when `diff > 8` (whole word is in range),
15/// - partial comparison via `var_sll_bytes` shifting both sides left by `8 - diff` bytes so only
16///   the low `diff` bytes remain (1 ≤ diff ≤ 8 — note that the `diff = 8` case picks the shifted
17///   form with `shift = 0`, which is identical to the original word), and
18/// - skipped comparison (both sides forced to zero) when the diff is 0.
19///
20/// # Panics
21///
22/// Panics if `actual.len() != expected.len()`.
23pub fn assert_slice_eq(
24	b: &CircuitBuilder,
25	name: impl Into<String>,
26	len_bytes: Wire,
27	actual: &[Wire],
28	expected: &[Wire],
29) {
30	assert_eq!(
31		actual.len(),
32		expected.len(),
33		"assert_slice_eq: actual and expected must have the same word count"
34	);
35	let name = name.into();
36	let zero = b.add_constant(Word::ZERO);
37	let eight = b.add_constant_64(8);
38	for (i, (&a, &e)) in actual.iter().zip(expected).enumerate() {
39		let start_byte = b.add_constant_64((i * 8) as u64);
40		let (diff_raw, borrow) = b.isub_bin_bout(len_bytes, start_byte, zero);
41		let diff = b.select(borrow, zero, diff_raw);
42
43		// `eight_minus_diff` is the byte shift amount; the same sub's borrow_out tells us
44		// whether `diff > 8` (i.e. take the full word) without a separate compare.
45		let (eight_minus_diff, diff_gt_8) = b.isub_bin_bout(eight, diff, zero);
46		let a_shifted = var_sll_bytes(b, a, eight_minus_diff);
47		let e_shifted = var_sll_bytes(b, e, eight_minus_diff);
48		let a_part = b.select(diff_gt_8, a, a_shifted);
49		let e_part = b.select(diff_gt_8, e, e_shifted);
50
51		// `var_sll_bytes` has precondition `shift < 8`, which the `diff == 0` case violates
52		// (then `eight_minus_diff = 8`). The shift result is unused in that case because both
53		// sides are forced to zero here.
54		let diff_eq_0 = b.icmp_eq(diff, zero);
55		let a_cmp = b.select(diff_eq_0, zero, a_part);
56		let e_cmp = b.select(diff_eq_0, zero, e_part);
57
58		b.assert_eq(format!("{name}[{i}]"), a_cmp, e_cmp);
59	}
60}
61
62/// Extracts a slice from an input byte array and returns it as a vector of packed 64-bit words.
63///
64/// Returns the bytes from `input` starting at `offset` for `len_slice` bytes, packed into
65/// `max_n_words` little-endian 64-bit words. Bytes past `len_slice` are not constrained — they
66/// hold whatever raw bytes happen to follow in `input` (and may be nonzero garbage). Callers that
67/// need to compare against an expected slice should use [`assert_slice_eq`], which masks the
68/// comparison to the first `len_slice` bytes.
69///
70/// # Limitations
71/// All size and offset values must fit within 32 bits. Specifically:
72/// - `len_input` must be < 2^32
73/// - `len_slice` must be < 2^32
74/// - `offset` must be < 2^32
75/// - `offset + len_slice` must be < 2^32
76///
77/// These limitations are enforced by the circuit constraints.
78///
79/// # Arguments
80/// * `b` - Circuit builder
81/// * `len_input` - Actual input size in bytes
82/// * `len_slice` - Actual slice size in bytes
83/// * `input` - Input array packed as words (8 bytes per word)
84/// * `offset` - Byte offset where slice starts
85/// * `max_n_words` - Number of output wires; the maximum slice length in bytes is `max_n_words * 8`
86///
87/// # Returns
88/// A `Vec<Wire>` of length `max_n_words` containing the extracted slice bytes packed in
89/// little-endian order. Bytes past `len_slice` are unconstrained garbage; use [`assert_slice_eq`]
90/// for comparisons that should ignore them.
91///
92/// # Panics
93/// * If `input.len() * 8 > u32::MAX`
94/// * If `max_n_words * 8 > u32::MAX`
95pub fn slice(
96	b: &CircuitBuilder,
97	len_input: Wire,
98	len_slice: Wire,
99	input: &[Wire],
100	offset: Wire,
101	max_n_words: usize,
102) -> Vec<Wire> {
103	// Static assertions to ensure maximum sizes fit within 32 bits
104	let max_len_input = input.len() << 3;
105	let max_len_slice = max_n_words << 3;
106
107	assert!(max_len_input <= u32::MAX as usize, "max_n_input must be < 2^32");
108	assert!(max_len_slice <= u32::MAX as usize, "max_n_slice must be < 2^32");
109
110	// Ensure all values fit in 32 bits to prevent overflow in iadd
111	b.assert_zero("offset_32bit", b.shr(offset, 32));
112	b.assert_zero("len_slice_32bit", b.shr(len_slice, 32));
113	b.assert_zero("len_input_32bit", b.shr(len_input, 32));
114
115	// Verify bounds: offset + len_slice <= len_input
116	let (offset_plus_len_slice, _) = b.iadd(offset, len_slice);
117	let in_bounds = b.icmp_ule(offset_plus_len_slice, len_input);
118	b.assert_true("bounds_check", in_bounds);
119
120	let sufficient_capacity = b.icmp_ule(len_slice, b.add_constant(Word(max_len_slice as u64)));
121	b.assert_true("max_n_words is sufficient", sufficient_capacity);
122
123	// For each output word, compute the corresponding bytes of the slice. Trailing positions past
124	// `len_slice` are zeroed via the byte mask and the `word_partially_valid` guard.
125	if max_n_words == 0 {
126		Vec::new()
127	} else {
128		let zero = b.add_constant(Word::ZERO);
129
130		// Decompose offset = word_offset * 8 + byte_offset
131		let word_offset = b.shr(offset, 3); // offset / 8
132		let byte_offset = b.band(offset, b.add_constant(Word(7))); // offset % 8
133		let (neg_byte_offset, _) = b.isub_bin_bout(b.add_constant(Word(8)), byte_offset, zero);
134		let is_aligned = b.icmp_eq(byte_offset, zero);
135
136		// Every output word reads `input[word_offset + i]` and its successor, so one rotate of the
137		// input serves all of them. A multiplexer per output word would instead cost
138		// `input.len() - 1` selects each.
139		//
140		// The rotate wraps, so a position past `len_slice` reads the front of the input rather than
141		// the multiplexer's out-of-range value. Both are garbage that this function leaves
142		// unconstrained.
143		let window = (max_n_words + 1).min(input.len());
144		let rotated = rotate_left_dynamic(b, input, word_offset, window);
145
146		(0..max_n_words)
147			.map(|slice_idx| {
148				let b = b.subcircuit(format!("slice_word[{slice_idx}]"));
149
150				let in_word = rotated[slice_idx % window];
151				let next_word = rotated[(slice_idx + 1) % window];
152
153				let aligned_out_word = in_word;
154				let unaligned_out_word = b.bxor(
155					var_srl_bytes(&b, in_word, byte_offset),
156					var_sll_bytes(&b, next_word, neg_byte_offset),
157				);
158				b.select(is_aligned, aligned_out_word, unaligned_out_word)
159			})
160			.collect()
161	}
162}
163
164#[cfg(test)]
165mod tests {
166
167	use super::{CircuitBuilder, Wire, Word, assert_slice_eq, slice};
168
169	/// Build a test circuit that takes input + offset wires, calls `slice`, and asserts the
170	/// returned bytes equal a separately allocated `expected` byte vector. Returns the wires the
171	/// caller needs to populate.
172	struct SliceTestSetup {
173		builder: CircuitBuilder,
174		len_input: Wire,
175		len_slice: Wire,
176		offset: Wire,
177		input: Vec<Wire>,
178		expected: Vec<Wire>,
179	}
180
181	fn build_slice_check(n_input_words: usize, n_slice_words: usize) -> SliceTestSetup {
182		let builder = CircuitBuilder::new();
183		let len_input = builder.add_inout();
184		let len_slice = builder.add_inout();
185		let offset = builder.add_inout();
186		let input: Vec<Wire> = (0..n_input_words).map(|_| builder.add_inout()).collect();
187		let expected: Vec<Wire> = (0..n_slice_words).map(|_| builder.add_inout()).collect();
188		let actual = slice(&builder, len_input, len_slice, &input, offset, n_slice_words);
189		assert_slice_eq(&builder, "slice_eq", len_slice, &actual, &expected);
190		SliceTestSetup {
191			builder,
192			len_input,
193			len_slice,
194			offset,
195			input,
196			expected,
197		}
198	}
199
200	/// Run a success-case test: pack `input_data` and `expected_slice_data` into the test setup,
201	/// run the circuit, and verify constraints.
202	fn run_slice_success(
203		setup: SliceTestSetup,
204		len_input_val: u64,
205		len_slice_val: u64,
206		offset_val: u64,
207		input_data: &[u8],
208		expected_slice_data: &[u8],
209	) {
210		let circuit = setup.builder.build();
211		let mut filler = circuit.new_witness_filler();
212		filler[setup.len_input] = Word(len_input_val);
213		filler[setup.len_slice] = Word(len_slice_val);
214		filler[setup.offset] = Word(offset_val);
215		filler.pack_bytes_le(&setup.input, input_data);
216		filler.pack_bytes_le(&setup.expected, expected_slice_data);
217
218		circuit.populate_wire_witness(&mut filler).unwrap();
219		let cs = circuit.constraint_system();
220		cs.verify(&filler.into_value_vec()).unwrap();
221	}
222
223	/// Run a failure-case test: expect `populate_wire_witness` to error.
224	fn run_slice_failure(
225		setup: SliceTestSetup,
226		len_input_val: u64,
227		len_slice_val: u64,
228		offset_val: u64,
229		input_data: &[u8],
230		expected_slice_data: &[u8],
231	) {
232		let circuit = setup.builder.build();
233		let mut filler = circuit.new_witness_filler();
234		filler[setup.len_input] = Word(len_input_val);
235		filler[setup.len_slice] = Word(len_slice_val);
236		filler[setup.offset] = Word(offset_val);
237		filler.pack_bytes_le(&setup.input, input_data);
238		filler.pack_bytes_le(&setup.expected, expected_slice_data);
239		assert!(circuit.populate_wire_witness(&mut filler).is_err());
240	}
241
242	#[test]
243	fn test_aligned_slice() {
244		// 16-byte input, 8-byte slice at offset 0
245		let setup = build_slice_check(2, 1);
246		let input_data = [
247			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
248			0x0e, 0x0f,
249		];
250		let slice_data = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
251		run_slice_success(setup, 16, 8, 0, &input_data, &slice_data);
252	}
253
254	#[test]
255	fn test_unaligned_slice() {
256		// 16-byte input, 8-byte slice at offset 3
257		let setup = build_slice_check(2, 1);
258		let input_data = [
259			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
260			0x0e, 0x0f,
261		];
262		let slice_data = [0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a];
263		run_slice_success(setup, 16, 8, 3, &input_data, &slice_data);
264	}
265
266	#[test]
267	fn test_bounds_check() {
268		// offset(5) + len_slice(8) > len_input(10) → bounds check fails.
269		let setup = build_slice_check(2, 1);
270		let dummy_input = vec![0u8; 10];
271		let dummy_slice = vec![0u8; 8];
272		run_slice_failure(setup, 10, 8, 5, &dummy_input, &dummy_slice);
273	}
274
275	#[test]
276	fn test_bounds_check_edge_case() {
277		// Exact boundary: offset(5) + len_slice(5) == len_input(10).
278		let setup = build_slice_check(2, 1);
279		let input_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
280		let slice_data = vec![5, 6, 7, 8, 9];
281		run_slice_success(setup, 10, 5, 5, &input_data, &slice_data);
282	}
283
284	#[test]
285	fn test_empty_slice() {
286		// len_slice = 0 — output is all zeros.
287		let setup = build_slice_check(2, 1);
288		let input_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
289		run_slice_success(setup, 10, 0, 5, &input_data, &[]);
290	}
291
292	#[test]
293	fn test_mismatched_slice_content() {
294		// Caller's expected slice differs from the actual extracted bytes — the external
295		// `assert_eq` in `build_slice_check` should fail.
296		let setup = build_slice_check(2, 1);
297		let input_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
298		// Actual extracted slice at offset 2 with len 5 is [2,3,4,5,6]; we claim [0,1,2,3,4].
299		let wrong_slice_data = vec![0, 1, 2, 3, 4];
300		run_slice_failure(setup, 10, 5, 2, &input_data, &wrong_slice_data);
301	}
302
303	#[test]
304	fn test_offset_at_end() {
305		// Empty slice at offset 10, where len_input = 10.
306		let setup = build_slice_check(2, 1);
307		let input_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
308		run_slice_success(setup, 10, 0, 10, &input_data, &[]);
309	}
310
311	#[test]
312	fn test_multiple_byte_extraction_paths() {
313		// Verify byte extraction works for a range of offsets in a fresh circuit each time.
314		// (The expected slice differs per case so we can't reuse one circuit.)
315		for word_idx in 0..3 {
316			for byte_offset in 0..8 {
317				let offset_val = word_idx * 8 + byte_offset;
318				if offset_val + 8 > 24 {
319					continue;
320				}
321				let setup = build_slice_check(3, 1);
322				let input_data: Vec<u8> = (0..24).map(|i| i as u8).collect();
323				let slice_data: Vec<u8> = input_data[offset_val..offset_val + 8].to_vec();
324				run_slice_success(setup, 24, 8, offset_val as u64, &input_data, &slice_data);
325			}
326		}
327	}
328
329	#[test]
330	fn test_partial_word_zero_padding() {
331		// `slice` returns words zero-padded past `len_slice`. For len_slice=12 (1.5 words), the
332		// upper 4 bytes of word 1 must be zero — and the assert_eq will catch any drift.
333		let setup = build_slice_check(3, 2);
334		let input_data = vec![
335			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // word 0
336			0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, // word 1
337			0x10, 0x11, 0x12, 0x13, // partial word 2
338		];
339		// Expected slice: 12 valid bytes, word-1 high half zero-padded.
340		let correct_slice = [
341			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // word 0
342			0x08, 0x09, 0x0a, 0x0b, 0x00, 0x00, 0x00, 0x00, // word 1 padded to 16 bytes
343		];
344		run_slice_success(setup, 20, 12, 0, &input_data, &correct_slice);
345	}
346
347	#[test]
348	fn test_partial_word_tolerates_garbage_padding() {
349		// `build_slice_check` masks the comparison to the first `len_slice` bytes, so the test
350		// helper accepts arbitrary garbage in trailing bytes past `len_slice`.
351		let setup = build_slice_check(3, 2);
352		let input_data = vec![
353			0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
354			0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13,
355		];
356		let circuit = setup.builder.build();
357		let mut filler = circuit.new_witness_filler();
358		filler[setup.len_input] = Word(20);
359		filler[setup.len_slice] = Word(12);
360		filler[setup.offset] = Word(0);
361		filler.pack_bytes_le(&setup.input, &input_data);
362		// Word 0 correct, word 1 has garbage 0xffffffff in the upper half (past len_slice).
363		filler[setup.expected[0]] = Word(0x0706050403020100);
364		filler[setup.expected[1]] = Word(0xffffffff0b0a0908);
365		circuit.populate_wire_witness(&mut filler).unwrap();
366	}
367
368	#[test]
369	fn test_large_offset_overflow() {
370		// offset has bit 32 set → 32-bit assertion fails.
371		let setup = build_slice_check(2, 1);
372		let circuit = setup.builder.build();
373		let mut filler = circuit.new_witness_filler();
374		filler[setup.len_input] = Word(10);
375		filler[setup.len_slice] = Word(5);
376		filler[setup.offset] = Word(1u64 << 32);
377		filler.pack_bytes_le(&setup.input, &[0u8; 10]);
378		filler.pack_bytes_le(&setup.expected, &[0u8; 5]);
379		assert!(circuit.populate_wire_witness(&mut filler).is_err());
380	}
381
382	#[test]
383	fn test_32bit_validation() {
384		// offset with bit 33 set
385		let setup = build_slice_check(2, 1);
386		let circuit = setup.builder.build();
387		let mut filler = circuit.new_witness_filler();
388		filler[setup.len_input] = Word(10);
389		filler[setup.len_slice] = Word(5);
390		filler[setup.offset] = Word(1u64 << 33);
391		filler.pack_bytes_le(&setup.input, &[0u8; 10]);
392		filler.pack_bytes_le(&setup.expected, &[0u8; 5]);
393		assert!(circuit.populate_wire_witness(&mut filler).is_err());
394
395		// len_input with upper 32 bits set
396		let setup = build_slice_check(2, 1);
397		let circuit = setup.builder.build();
398		let mut filler = circuit.new_witness_filler();
399		filler[setup.len_input] = Word(0xffffffff00000010);
400		filler[setup.len_slice] = Word(5);
401		filler[setup.offset] = Word(0);
402		filler.pack_bytes_le(&setup.input, &[0u8; 10]);
403		filler.pack_bytes_le(&setup.expected, &[0u8; 5]);
404		assert!(circuit.populate_wire_witness(&mut filler).is_err());
405
406		// len_slice with bit 32 set
407		let setup = build_slice_check(2, 1);
408		let circuit = setup.builder.build();
409		let mut filler = circuit.new_witness_filler();
410		filler[setup.len_input] = Word(10);
411		filler[setup.len_slice] = Word(0x100000005);
412		filler[setup.offset] = Word(0);
413		filler.pack_bytes_le(&setup.input, &[0u8; 10]);
414		filler.pack_bytes_le(&setup.expected, &[0u8; 5]);
415		assert!(circuit.populate_wire_witness(&mut filler).is_err());
416	}
417
418	#[test]
419	fn test_edge_case_len_input_zero() {
420		// Empty input + empty slice at offset 0.
421		let setup = build_slice_check(2, 1);
422		run_slice_success(setup, 0, 0, 0, &[], &[]);
423	}
424
425	#[test]
426	fn test_edge_case_len_input_zero_with_nonzero_slice() {
427		// Empty input + non-empty slice → bounds check fails.
428		let setup = build_slice_check(2, 1);
429		run_slice_failure(setup, 0, 5, 0, &[], &[1, 2, 3, 4, 5]);
430	}
431
432	#[test]
433	fn test_padding_beyond_actual_data() {
434		// 12 bytes of input data padded into 3 input words; 8-byte slice at offset 2.
435		let setup = build_slice_check(3, 2);
436		let input_data = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
437		// Slice is 8 bytes, so word 1 is fully zero-padded.
438		let slice_data = vec![2, 3, 4, 5, 6, 7, 8, 9];
439		run_slice_success(setup, 12, 8, 2, &input_data, &slice_data);
440	}
441
442	#[test]
443	fn test_direct_masking_logic() {
444		// Test the masking logic directly (Rust-side, no circuit).
445		let slice_word = Word(0xffffffff_0b0a0908);
446		let extracted_word = Word(0x00000000_0b0a0908);
447		let mask = Word(0x00000000_ffffffff);
448
449		let masked_slice = slice_word & mask;
450		let masked_extracted = extracted_word & mask;
451
452		assert_eq!(masked_slice, masked_extracted);
453		assert_eq!(masked_slice ^ masked_extracted, Word::ZERO);
454	}
455}