midnight_circuits/parsing/scanner/varlen.rs
1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) 2025 Midnight Foundation
3// SPDX-License-Identifier: Apache-2.0
4// Licensed under the Apache License, Version 2.0 (the "License");
5// You may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7// http://www.apache.org/licenses/LICENSE-2.0
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14//! Variable-length vector type for scanner operations.
15
16use midnight_proofs::{
17 circuit::{Layouter, Value},
18 plonk::Error,
19};
20
21use super::{ScannerChip, ALPHABET_MAX_SIZE};
22use crate::{
23 field::{native::AssignedBit, AssignedNative},
24 instructions::{
25 AssignmentInstructions, ControlFlowInstructions, UnsafeConversionInstructions,
26 VectorInstructions,
27 },
28 types::{AssignedByte, AssignedVector},
29 CircuitField,
30};
31
32/// A [`ScannerVec`] is built from an [`AssignedVector`] of [`AssignedByte`]s
33/// with the following guarantees enforced in-circuit:
34///
35/// - **Payload elements are range-checked** to `[0, 255]` (they originate from
36/// [`AssignedByte`]s).
37/// - **Filler elements are constrained to 256** ([`ALPHABET_MAX_SIZE`]),
38/// making them unmatchable in substring lookup arguments.
39/// - **Padding flags and limits are cached** and available at no extra cost
40/// after construction.
41///
42/// The chunk size `A` of the source [`AssignedVector`] determines filler
43/// placement and is preserved in the type.
44///
45/// These properties make [`ScannerVec`] safe for use in both automaton parsing
46/// ([`ScannerChip::parse_varlen`](super::ScannerChip::parse_varlen)) and
47/// variable-length substring checks
48/// ([`ScannerChip::check_bytes_varlen`](super::ScannerChip::check_bytes_varlen)).
49#[derive(Debug, Clone)]
50pub struct ScannerVec<F: CircuitField, const M: usize, const A: usize> {
51 /// The effective length of the payload (constrained during construction).
52 length: AssignedNative<F>,
53 /// Buffer with filler positions constrained to 256. Boxed to keep
54 /// large buffers off the stack.
55 pub(crate) buffer: Box<[AssignedNative<F>; M]>,
56 /// (start, end) positions of the payload in the buffer.
57 pub(crate) limits: (AssignedNative<F>, AssignedNative<F>),
58 /// Per-element padding flags (1 = filler, 0 = payload). Boxed to
59 /// keep large buffers off the stack.
60 pub(crate) padding_flags: Box<[AssignedBit<F>; M]>,
61}
62
63impl<F: CircuitField, const M: usize, const A: usize> ScannerVec<F, M, A> {
64 /// Returns the (start, end) positions of the payload in the buffer.
65 pub fn get_limits(&self) -> &(AssignedNative<F>, AssignedNative<F>) {
66 &self.limits
67 }
68
69 /// Returns the per-element padding flags (1 = filler, 0 = payload).
70 pub fn padding_flags(&self) -> &[AssignedBit<F>; M] {
71 &self.padding_flags
72 }
73
74 /// Returns the effective length of the payload.
75 pub fn len(&self) -> &AssignedNative<F> {
76 &self.length
77 }
78}
79
80impl<F: CircuitField, const M: usize, const A: usize> From<ScannerVec<F, M, A>>
81 for AssignedVector<F, AssignedNative<F>, M, A>
82{
83 fn from(value: ScannerVec<F, M, A>) -> Self {
84 AssignedVector {
85 buffer: value.buffer,
86 len: value.length,
87 }
88 }
89}
90
91impl<F> ScannerChip<F>
92where
93 F: CircuitField + Ord,
94{
95 /// Converts a `ScannerVec` into an `AssignedVector` of [`AssignedByte`]s.
96 ///
97 /// Filler positions (value 256) are replaced with 0 so that all buffer
98 /// elements are valid bytes. The resulting vector can be passed to
99 /// operations that expect `AssignedByte` inputs, such as variable-length
100 /// SHA-256.
101 ///
102 /// No range-check constraints are added: payload bytes were already
103 /// range-checked during `ScannerVec` construction, and fillers are
104 /// replaced by a known constant (0).
105 pub fn scanner_vec_to_byte_vector<const M: usize, const A: usize>(
106 &self,
107 layouter: &mut impl Layouter<F>,
108 sv: &ScannerVec<F, M, A>,
109 ) -> Result<AssignedVector<F, AssignedByte<F>, M, A>, Error> {
110 let zero = self.native_gadget.assign_fixed(layouter, F::ZERO)?;
111
112 // Replace fillers (256) with 0, keep payload bytes unchanged.
113 let byte_buffer: Vec<AssignedByte<F>> = (sv.buffer.iter().zip(sv.padding_flags.iter()))
114 .map(|(elem, flag)| {
115 // flag=1 (filler) -> zero; flag=0 (payload) -> elem.
116 let zeroed = self.native_gadget.select(layouter, flag, &zero, elem)?;
117 self.native_gadget.convert_unsafe(layouter, &zeroed)
118 })
119 .collect::<Result<Vec<_>, Error>>()?;
120
121 Ok(AssignedVector {
122 buffer: Box::new(byte_buffer.try_into().unwrap()),
123 len: sv.length.clone(),
124 })
125 }
126
127 /// Assigns a variable-length byte vector as a `ScannerVec`.
128 ///
129 /// The input bytes are assigned as [`AssignedByte`]s (range-checked to
130 /// `[0, 255]`), promoted to [`AssignedNative`] elements, and filler
131 /// positions are constrained to `ALPHABET_MAX_SIZE` in-circuit.
132 pub fn assign_scanner_vec<const M: usize, const A: usize>(
133 &self,
134 layouter: &mut impl Layouter<F>,
135 value: Value<Vec<u8>>,
136 ) -> Result<ScannerVec<F, M, A>, Error> {
137 let byte_vec: AssignedVector<F, AssignedByte<F>, M, A> =
138 self.vector_gadget.assign_with_filler(layouter, value, None)?;
139 self.scanner_vec_from_byte_vec(layouter, byte_vec)
140 }
141
142 /// Converts an existing [`AssignedVector`] of [`AssignedByte`]s into a
143 /// `ScannerVec`, constraining filler positions to `ALPHABET_MAX_SIZE`
144 /// and anchoring the length.
145 ///
146 /// The input elements are already range-checked (they are
147 /// [`AssignedByte`]s). This function computes padding flags and constrains
148 /// fillers via [`select`](`ControlFlowInstructions::select`).
149 pub fn scanner_vec_from_byte_vec<const M: usize, const A: usize>(
150 &self,
151 layouter: &mut impl Layouter<F>,
152 vec: AssignedVector<F, AssignedByte<F>, M, A>,
153 ) -> Result<ScannerVec<F, M, A>, Error> {
154 // Compute padding flags and limits in one call.
155 let (padding_flags, limits) = self.vector_gadget.padding_flag(layouter, &vec)?;
156
157 // Constrain filler positions to ALPHABET_MAX_SIZE.
158 let filler =
159 self.native_gadget.assign_fixed(layouter, F::from(ALPHABET_MAX_SIZE as u64))?;
160 let buffer: Box<[AssignedNative<F>; M]> = Box::new(
161 (vec.buffer.iter().zip(padding_flags.iter()))
162 .map(|(elem, is_padding)| {
163 let native_elem = AssignedNative::from(elem);
164 self.native_gadget.select(layouter, is_padding, &filler, &native_elem)
165 })
166 .collect::<Result<Vec<_>, Error>>()?
167 .try_into()
168 .expect("Length mismatch in ScannerVec buffer"),
169 );
170
171 Ok(ScannerVec {
172 length: vec.len,
173 buffer,
174 limits,
175 padding_flags,
176 })
177 }
178}