Skip to main content

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}