Skip to main content

midnight_circuits/parsing/
data_types.rs

1// This file is part of MIDNIGHT-ZK.
2// Copyright (C) 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
14use midnight_proofs::{circuit::Layouter, plonk::Error};
15use num_bigint::BigUint;
16
17use super::ParserGadget;
18use crate::{
19    field::{native::AssignedBit, AssignedNative},
20    instructions::NativeInstructions,
21    types::{AssignedByte, InnerValue},
22    CircuitField,
23};
24
25/// Order of day, month and year in the date string.
26#[allow(clippy::upper_case_acronyms)]
27#[derive(Clone, Copy, Debug)]
28pub enum DateFormat {
29    /// Four-digit year, month, day.
30    YYYYMMDD,
31    /// Day, month, four-digit year.
32    DDMMYYYY,
33    /// Two-digit year, month, day. Requires a `century_base` parameter
34    /// in [`ParserGadget::date_to_int`] to resolve the century.
35    YYMMDD,
36}
37
38/// Date strings may have 1 character separating year, month and day
39/// or no separator.
40#[derive(Clone, Copy, Debug)]
41pub enum Separator {
42    /// No separator between day, month and year.
43    NoSep,
44    /// Day, month and year separated by the specified character.
45    Sep(char),
46}
47
48impl<F, N> ParserGadget<F, N>
49where
50    F: CircuitField,
51    N: NativeInstructions<F>,
52{
53    /// Given an assigned byte as a character represented in ASCII,
54    /// returns its numeric value as an assigned native.
55    ///
56    /// # Unsatisfiable Circuit
57    ///
58    /// If the input byte is not an ASCII digit, i.e. in the range \[48, 57\].
59    fn ascii_to_digit(
60        &self,
61        layouter: &mut impl Layouter<F>,
62        byte: &AssignedByte<F>,
63    ) -> Result<AssignedNative<F>, Error> {
64        // Digits in ascii are represented by the values in [48, 57].
65        // So substracting 48 gives the represented value.
66        let val = self.native_gadget.add_constant(layouter, &byte.into(), -F::from(48u64))?;
67        self.native_gadget
68            .assert_lower_than_fixed(layouter, &val, &BigUint::from(10u64))?;
69        Ok(val)
70    }
71
72    /// Given a string of ASCII digits as assigned bytes, returns the
73    /// represented integer as an assigned native.
74    /// Leading 0s are allowed.
75    ///
76    /// # Unsatisfiable Circuit
77    ///
78    /// If the input bytes are not valid ASCII digits, i.e. in the range
79    /// \[48, 57\].
80    ///
81    /// # Panics
82    ///
83    /// If |input| exceeds the maximum number of digits that can be reliably
84    /// stored in an `F` element.
85    pub fn ascii_to_int(
86        &self,
87        layouter: &mut impl Layouter<F>,
88        input: &[AssignedByte<F>],
89    ) -> Result<AssignedNative<F>, Error> {
90        let native = &self.native_gadget;
91        let digit_capacity = (F::CAPACITY as f64 / 10f64.log2()) as usize;
92        let n = input.len();
93        assert!(
94            n < digit_capacity,
95            "Cannot parse intgers with more than {digit_capacity} digits"
96        );
97
98        let mut terms = Vec::with_capacity(n);
99        let mut base = F::ONE;
100        for byte in input.iter().rev() {
101            let val = self.ascii_to_digit(layouter, byte)?;
102            terms.push((base, val));
103            base *= F::from(10u64);
104        }
105
106        let res = native.linear_combination(layouter, terms.as_slice(), F::ZERO)?;
107
108        Ok(res)
109    }
110
111    /// Given an ASCII string of assigned bytes, representing a date in the
112    /// specified format, returns `DD + 100 * MM + 10_000 * YYYY` as an
113    /// assigned native value. This function does not check the validity of
114    /// the date (e.g. `"32011990"` in DDMMYYYY is accepted as 32 January
115    /// 1990).
116    ///
117    /// For two-digit year formats ([`DateFormat::YYMMDD`]), `century_base`
118    /// must be `Some(N)` where N is an assigned value in [0, 99]. The year
119    /// is then resolved as `1900 + YY + (if YY < N { 100 } else { 0 })`,
120    /// i.e. the 100-year window is [1900+N, 2000+N). The caller is
121    /// responsible for constraining N < 100.
122    ///
123    /// For four-digit year formats, `century_base` must be `None`.
124    pub fn date_to_int(
125        &self,
126        layouter: &mut impl Layouter<F>,
127        input: &[AssignedByte<F>],
128        format: (DateFormat, Separator),
129        century_base: Option<&AssignedNative<F>>,
130    ) -> Result<AssignedNative<F>, Error> {
131        let native = &self.native_gadget;
132        let n = input.len();
133
134        match format {
135            (DateFormat::YYMMDD, Separator::NoSep) => {
136                assert_eq!(n, 6, "Date format must be 6 characters long: YYMMDD");
137                let century_base =
138                    century_base.expect("YYMMDD format requires a century_base parameter");
139                self.date_to_int_short_year(layouter, input, century_base)
140            }
141            (DateFormat::YYMMDD, Separator::Sep(_)) => {
142                panic!("YYMMDD with separator is not supported")
143            }
144            _ => {
145                assert!(
146                    century_base.is_none(),
147                    "century_base is only used with YYMMDD format"
148                );
149                // Indices for day, month, year bytes in the input.
150                let indices: (_, _, _) = match format {
151                    (DateFormat::DDMMYYYY, Separator::NoSep) => {
152                        assert_eq!(n, 8, "Date format must be 8 characters long: DDMMYYYY");
153                        ((0..2), (2..4), (4..8))
154                    }
155                    (DateFormat::DDMMYYYY, Separator::Sep(sep)) => {
156                        assert_eq!(
157                            n, 10,
158                            "Date format must be 10 characters long: DD{sep}MM{sep}YYYY"
159                        );
160                        native.assert_equal_to_fixed(layouter, &input[2], sep as u8)?;
161                        native.assert_equal_to_fixed(layouter, &input[5], sep as u8)?;
162                        ((0..2), (3..5), (6..10))
163                    }
164                    (DateFormat::YYYYMMDD, Separator::NoSep) => {
165                        assert_eq!(n, 8, "Date format must be 8 characters long: YYYYMMDD");
166                        ((6..8), (4..6), (0..4))
167                    }
168                    (DateFormat::YYYYMMDD, Separator::Sep(sep)) => {
169                        assert_eq!(
170                            n, 10,
171                            "Date format must be 10 characters long: YYYY{sep}MM{sep}DD"
172                        );
173                        native.assert_equal_to_fixed(layouter, &input[4], sep as u8)?;
174                        native.assert_equal_to_fixed(layouter, &input[7], sep as u8)?;
175                        ((8..10), (5..7), (0..4))
176                    }
177                    (DateFormat::YYMMDD, _) => unreachable!(),
178                };
179                let bytes = [&input[indices.2], &input[indices.1], &input[indices.0]].concat();
180                self.ascii_to_int(layouter, &bytes)
181            }
182        }
183    }
184
185    /// Resolves a 6-byte YYMMDD date into a YYYYMMDD integer using the
186    /// century base N. The year is `1900 + YY + (if YY < N { 100 } else { 0
187    /// })`.
188    fn date_to_int_short_year(
189        &self,
190        layouter: &mut impl Layouter<F>,
191        input: &[AssignedByte<F>],
192        century_base: &AssignedNative<F>,
193    ) -> Result<AssignedNative<F>, Error> {
194        let native = &self.native_gadget;
195
196        let yy = self.ascii_to_int(layouter, &input[0..2])?;
197        let mmdd = self.ascii_to_int(layouter, &input[2..6])?;
198
199        // Off-circuit: compute is_20xx = (YY < N).
200        let yy_val = input[0]
201            .value()
202            .zip(input[1].value())
203            .map(|(d0, d1)| (d0 - 48) as u64 * 10 + (d1 - 48) as u64);
204        let n_val =
205            InnerValue::value(century_base).map(|be| u64::try_from(be.to_biguint()).unwrap());
206        let is_20xx_val = yy_val.zip(n_val).map(|(yy, n)| yy < n);
207
208        // Assign is_20xx as a bit (constrains it to {0, 1}).
209        let is_20xx: AssignedBit<F> = native.assign(layouter, is_20xx_val)?;
210        let is_20xx_native: AssignedNative<F> = is_20xx.into();
211
212        // Constrain: yy - century_base + is_20xx * 100 ∈ [0, 128).
213        // This enforces the correct relationship between is_20xx, yy, and N.
214        let check = native.linear_combination(
215            layouter,
216            &[
217                (F::ONE, yy.clone()),
218                (-F::ONE, century_base.clone()),
219                (F::from(100u64), is_20xx_native.clone()),
220            ],
221            F::ZERO,
222        )?;
223        native.assert_lower_than_fixed(layouter, &check, &BigUint::from(128u64))?;
224
225        // Result: (1900 + YY + is_20xx * 100) * 10_000 + MMDD
226        native.linear_combination(
227            layouter,
228            &[
229                (F::from(10_000u64), yy),
230                (F::from(1_000_000u64), is_20xx_native),
231                (F::ONE, mmdd),
232            ],
233            F::from(19_000_000u64),
234        )
235    }
236}
237
238/// A calendar date with a full 4-digit year.
239#[derive(Clone, Copy, Debug)]
240pub struct Date {
241    /// Day of the month (1-31).
242    pub day: u8,
243    /// Month (1-12).
244    pub month: u8,
245    /// Four-digit year.
246    pub year: u16,
247}
248
249impl Date {
250    /// Encodes as YYYYMMDD integer: `year * 10_000 + month * 100 + day`.
251    pub fn as_yyyymmdd(&self) -> u64 {
252        self.year as u64 * 10_000 + self.month as u64 * 100 + self.day as u64
253    }
254}
255
256impl From<Date> for BigUint {
257    fn from(value: Date) -> Self {
258        value.as_yyyymmdd().into()
259    }
260}
261
262impl<F, N> ParserGadget<F, N>
263where
264    F: CircuitField,
265    N: NativeInstructions<F>,
266{
267    /// Asserts that a date (as assigned bytes in the given format) is
268    /// strictly before `limit_date`. Convenience wrapper around
269    /// [`date_to_int`](Self::date_to_int) + a range check.
270    pub fn assert_date_before_fixed(
271        &self,
272        layouter: &mut impl Layouter<F>,
273        date_bytes: &[AssignedByte<F>],
274        format: (DateFormat, Separator),
275        limit_date: Date,
276    ) -> Result<(), Error> {
277        let date = self.date_to_int(layouter, date_bytes, format, None)?;
278        self.native_gadget.assert_lower_than_fixed(layouter, &date, &limit_date.into())
279    }
280}
281
282#[cfg(test)]
283mod tests {
284    use std::marker::PhantomData;
285
286    use ff::FromUniformBytes;
287    use midnight_proofs::{
288        circuit::{SimpleFloorPlanner, Value},
289        dev::MockProver,
290        plonk::{Circuit, ConstraintSystem},
291    };
292
293    use super::*;
294    use crate::{
295        field::{decomposition::chip::P2RDecompositionChip, NativeChip, NativeGadget},
296        testing_utils::FromScratch,
297    };
298
299    #[derive(Clone, Copy, Debug)]
300    enum ParseTarget {
301        Int,
302        Date((DateFormat, Separator)),
303        /// Parse a short-year date with `century_base`.
304        DateShortYear((DateFormat, Separator), u64),
305    }
306
307    #[derive(Clone, Debug)]
308    struct TestCircuit<F, N> {
309        string: Vec<Value<F>>,
310        expected: F,
311        operation: ParseTarget,
312        _marker: PhantomData<N>,
313    }
314
315    impl<F, N> Circuit<F> for TestCircuit<F, N>
316    where
317        F: CircuitField,
318        N: NativeInstructions<F> + FromScratch<F>,
319    {
320        type Config = <N as FromScratch<F>>::Config;
321        type FloorPlanner = SimpleFloorPlanner;
322        type Params = ();
323
324        fn without_witnesses(&self) -> Self {
325            unreachable!()
326        }
327
328        fn configure(meta: &mut ConstraintSystem<F>) -> Self::Config {
329            let committed_instance_column = meta.instance_column();
330            let instance_column = meta.instance_column();
331            <N as FromScratch<F>>::configure_from_scratch(
332                meta,
333                &mut vec![],
334                &mut vec![],
335                &[committed_instance_column, instance_column],
336            )
337        }
338
339        fn synthesize(
340            &self,
341            config: Self::Config,
342            mut layouter: impl Layouter<F>,
343        ) -> Result<(), Error> {
344            let native_gadget = <N as FromScratch<F>>::new_from_scratch(&config);
345            let parser_gadget = ParserGadget::<F, N>::new(&native_gadget);
346
347            let string = native_gadget.assign_many(&mut layouter, &self.string)?;
348            let bytes = string
349                .iter()
350                .map(|x| native_gadget.convert(&mut layouter, x))
351                .collect::<Result<Vec<AssignedByte<F>>, Error>>()?;
352
353            let res = match self.operation {
354                ParseTarget::Int => parser_gadget.ascii_to_int(&mut layouter, &bytes),
355                ParseTarget::Date(format) => {
356                    parser_gadget.date_to_int(&mut layouter, &bytes, format, None)
357                }
358                ParseTarget::DateShortYear(format, n) => {
359                    let century_base =
360                        native_gadget.assign(&mut layouter, Value::known(F::from(n)))?;
361                    parser_gadget.date_to_int(&mut layouter, &bytes, format, Some(&century_base))
362                }
363            }?;
364
365            native_gadget.assert_equal_to_fixed(&mut layouter, &res, self.expected)?;
366
367            native_gadget.load_from_scratch(&mut layouter)
368        }
369    }
370
371    fn run<F>(string: &[u8], expected: u64, operation: ParseTarget, must_pass: bool)
372    where
373        F: CircuitField + FromUniformBytes<64> + Ord,
374    {
375        let circuit = TestCircuit::<F, NativeGadget<F, P2RDecompositionChip<F>, NativeChip<F>>> {
376            string: string.iter().map(|x| F::from(*x as u64)).map(Value::known).collect(),
377            expected: F::from(expected),
378            operation,
379            _marker: PhantomData,
380        };
381        let public_inputs = vec![vec![], vec![]];
382        match MockProver::run(&circuit, public_inputs) {
383            Ok(prover) => match prover.verify() {
384                Ok(()) => assert!(must_pass),
385                Err(e) => assert!(!must_pass, "Failed verifier with error {e:?}"),
386            },
387            Err(e) => assert!(!must_pass, "Failed prover with error {e:?}"),
388        }
389    }
390
391    #[test]
392    fn test_parse_int() {
393        type F = midnight_curves::Fq;
394        let test_vecs: Vec<(&[u8], u64, bool)> = vec![
395            (b"987654321", 987654321, true),
396            (b"123456", 123456, true),
397            (b"123", 123, true),
398            (b"0123", 123, true),
399            (b"00123", 123, true),
400            (b"0", 0, true),
401            (b"54321", 54320, false),
402            (b"54321", 54322, false),
403            (b"54321", 0, false),
404        ];
405        test_vecs.iter().for_each(|(input, expected, must_pass)| {
406            run::<F>(input, *expected, ParseTarget::Int, *must_pass)
407        });
408    }
409
410    #[test]
411    fn test_parse_date() {
412        type F = midnight_curves::Fq;
413        let format1 = (DateFormat::DDMMYYYY, Separator::NoSep);
414        let format2 = (DateFormat::DDMMYYYY, Separator::Sep('-'));
415        let format3 = (DateFormat::YYYYMMDD, Separator::Sep('-'));
416        let format4 = (DateFormat::YYYYMMDD, Separator::NoSep);
417
418        let test_vecs: Vec<(&[u8], _, _, _)> = vec![
419            (b"40052025", format1, 20250540, true),
420            (b"01011970", format1, 19700101, true),
421            (b"12121970", format1, 19701212, true),
422            (b"40121970", format1, 19701240, true),
423            (b"01011970", format1, 19700102, false),
424            (b"40-05-2025", format2, 20250540, true),
425            (b"01-01-1970", format2, 19700101, true),
426            (b"12-12-1970", format2, 19701212, true),
427            (b"40-12-1970", format2, 19701240, true),
428            (b"01-01-1970", format2, 19700102, false),
429            (b"02-01-1970", format2, 19700201, false),
430            (b"01/01/1970", format2, 19700102, false),
431            (b"2025-05-40", format3, 20250540, true),
432            (b"1970-01-01", format3, 19700101, true),
433            (b"1970-12-12", format3, 19701212, true),
434            (b"20250540", format4, 20250540, true),
435            (b"19700101", format4, 19700101, true),
436            (b"19701225", format4, 19701225, true),
437        ];
438        test_vecs.iter().for_each(|(input, format, expected, must_pass)| {
439            run::<F>(input, *expected, ParseTarget::Date(*format), *must_pass)
440        });
441    }
442
443    #[test]
444    fn test_parse_date_short_year() {
445        type F = midnight_curves::Fq;
446        let yymmdd = (DateFormat::YYMMDD, Separator::NoSep);
447
448        // N = 26: window [1926, 2026). YY >= 26 → 19xx, YY < 26 → 20xx.
449        let test_vecs: Vec<(&[u8], _, u64, _, _)> = vec![
450            (b"000101", yymmdd, 26, 20000101, true),  // YY=0 < 26 → 2000
451            (b"251231", yymmdd, 26, 20251231, true),  // YY=25 < 26 → 2025
452            (b"260101", yymmdd, 26, 19260101, true),  // YY=26 >= 26 → 1926
453            (b"911214", yymmdd, 26, 19911214, true),  // YY=91 >= 26 → 1991
454            (b"991231", yymmdd, 26, 19991231, true),  // YY=99 >= 26 → 1999
455            (b"100812", yymmdd, 26, 20100812, true),  // YY=10 < 26 → 2010
456            (b"911214", yymmdd, 26, 20171214, false), // wrong century
457            // N = 0: window [1900, 2000). All YY → 19xx.
458            (b"000101", yymmdd, 0, 19000101, true),
459            (b"991231", yymmdd, 0, 19991231, true),
460            // N = 99: window [1999, 2099). YY=99 → 1999, YY=0..98 → 20xx.
461            (b"990101", yymmdd, 99, 19990101, true),
462            (b"000101", yymmdd, 99, 20000101, true),
463            (b"980101", yymmdd, 99, 20980101, true),
464        ];
465        test_vecs.iter().for_each(|(input, format, n, expected, must_pass)| {
466            run::<F>(
467                input,
468                *expected,
469                ParseTarget::DateShortYear(*format, *n),
470                *must_pass,
471            )
472        });
473    }
474}