openvm_circuit_primitives/is_less_than/
mod.rs

1use openvm_stark_backend::{
2    interaction::InteractionBuilder,
3    p3_air::AirBuilder,
4    p3_field::{Field, FieldAlgebra},
5};
6
7use crate::{
8    var_range::{VariableRangeCheckerBus, VariableRangeCheckerChip},
9    SubAir, TraceSubRowGenerator,
10};
11
12#[cfg(test)]
13pub mod tests;
14
15// Aux cols are the same for assert_less_than and is_less_than
16pub use super::assert_less_than::LessThanAuxCols;
17
18/// The IO is typically provided with `T = AB::Expr` as external context.
19// This does not derive AlignedBorrow because it is usually **not** going to be
20// direct columns in an AIR.
21#[repr(C)]
22#[derive(Clone, Copy, Debug, Default)]
23pub struct IsLessThanIo<T> {
24    pub x: T,
25    pub y: T,
26    /// The boolean output, constrained to equal (x < y) when `count != 0`.
27    pub out: T,
28    /// Range checks are done with multiplicity `count`.
29    /// If `count == 0` then no range checks are done.
30    /// `count` is **assumed** to be boolean and must be constrained as such by
31    /// the caller.
32    ///
33    /// N.B.: in fact range checks could always be done, if the aux
34    /// subrow values are set to 0 when `count == 0`. This would slightly
35    /// simplify the range check interactions, although usually doesn't change
36    /// the overall constraint degree. It however leads to the annoyance that
37    /// you must update the RangeChecker's multiplicities even on dummy padding
38    /// rows. To improve quality of life,
39    /// we currently use this more complex constraint.
40    pub count: T,
41}
42impl<T> IsLessThanIo<T> {
43    pub fn new(x: impl Into<T>, y: impl Into<T>, out: impl Into<T>, count: impl Into<T>) -> Self {
44        Self {
45            x: x.into(),
46            y: y.into(),
47            out: out.into(),
48            count: count.into(),
49        }
50    }
51}
52
53/// This is intended for use as a **SubAir**, not as a standalone Air.
54///
55/// This SubAir constrains the boolean equal to 1 iff `x < y`, assuming
56/// the two numbers both have a max number of bits, given by `max_bits`.
57/// The SubAir compares the numbers by decomposing `y - x - 1 + 2^max_bits`
58/// into limbs of size `bus.range_max_bits`, and interacts with a
59/// `VariableRangeCheckerBus` to range check the decompositions.
60///
61/// The SubAir will own auxiliary columns to store the decomposed limbs.
62/// The number of limbs is `max_bits.div_ceil(bus.range_max_bits)`.
63///
64/// The expected max constraint degree of `eval` is
65///     deg(count) + max(1, deg(x), deg(y))
66///
67/// N.B.: AssertLtSubAir could be implemented by directly passing through
68/// to IsLtSubAir with `out = AB::Expr::ONE`. The only additional
69/// constraint in this air is `assert_bool(io.out)`. However since both Airs
70/// are fundamental and the constraints are simple, we opt to keep the two
71/// versions separate.
72#[derive(Copy, Clone, Debug)]
73pub struct IsLtSubAir {
74    /// The bus for sends to range chip
75    pub bus: VariableRangeCheckerBus,
76    /// The maximum number of bits for the numbers to compare
77    /// Soundness requirement: max_bits <= 29
78    ///     max_bits > 29 doesn't work: the approach is to decompose and range check `y - x - 1 +
79    /// 2^max_bits` is non-negative.     This requires 2^{max_bits+1} < |F|.
80    ///     When F::bits() = 31, this implies max_bits <= 29.
81    pub max_bits: usize,
82    /// `decomp_limbs = max_bits.div_ceil(bus.range_max_bits)`.
83    pub decomp_limbs: usize,
84}
85
86impl IsLtSubAir {
87    pub fn new(bus: VariableRangeCheckerBus, max_bits: usize) -> Self {
88        assert!(max_bits <= 29); // see soundness requirement above
89        let decomp_limbs = max_bits.div_ceil(bus.range_max_bits);
90        Self {
91            bus,
92            max_bits,
93            decomp_limbs,
94        }
95    }
96
97    pub fn range_max_bits(&self) -> usize {
98        self.bus.range_max_bits
99    }
100
101    pub fn when_transition(self) -> IsLtWhenTransitionAir {
102        IsLtWhenTransitionAir(self)
103    }
104
105    /// FOR INTERNAL USE ONLY.
106    /// This AIR is only sound if interactions are enabled
107    ///
108    /// Constraints between `io` and `aux` are only enforced when `condition != 0`.
109    /// This means `aux` can be all zero independent on what `io` is by setting `condition = 0`.
110    #[inline(always)]
111    pub(crate) fn eval_without_range_checks<AB: AirBuilder>(
112        &self,
113        builder: &mut AB,
114        y_minus_x: impl Into<AB::Expr>,
115        out: impl Into<AB::Expr>,
116        condition: impl Into<AB::Expr>,
117        lower_decomp: &[AB::Var],
118    ) {
119        assert_eq!(lower_decomp.len(), self.decomp_limbs);
120        // this is the desired intermediate value (i.e. y - x - 1)
121        // deg(intermed_val) = deg(io)
122        let intermed_val =
123            y_minus_x.into() + AB::Expr::from_canonical_usize((1 << self.max_bits) - 1);
124
125        // Construct lower from lower_decomp:
126        // - each limb of lower_decomp will be range checked
127        // deg(lower) = 1
128        let lower = lower_decomp
129            .iter()
130            .enumerate()
131            .fold(AB::Expr::ZERO, |acc, (i, &val)| {
132                acc + val * AB::Expr::from_canonical_usize(1 << (i * self.range_max_bits()))
133            });
134
135        let out = out.into();
136        // constrain that the lower + out * 2^max_bits is the correct intermediate sum
137        let check_val = lower + out.clone() * AB::Expr::from_canonical_usize(1 << self.max_bits);
138        // the degree of this constraint is expected to be deg(count) + max(deg(intermed_val),
139        // deg(lower))
140        builder.when(condition).assert_eq(intermed_val, check_val);
141        builder.assert_bool(out);
142    }
143
144    #[inline(always)]
145    pub(crate) fn eval_range_checks<AB: InteractionBuilder>(
146        &self,
147        builder: &mut AB,
148        lower_decomp: &[AB::Var],
149        count: impl Into<AB::Expr>,
150    ) {
151        let count = count.into();
152        let mut bits_remaining = self.max_bits;
153        // we range check the limbs of the lower_decomp so that we know each element
154        // of lower_decomp has the correct number of bits
155        for limb in lower_decomp {
156            // the last limb might have fewer than `bus.range_max_bits` bits
157            let range_bits = bits_remaining.min(self.bus.range_max_bits);
158            self.bus
159                .range_check(*limb, range_bits)
160                .eval(builder, count.clone());
161            bits_remaining = bits_remaining.saturating_sub(self.bus.range_max_bits);
162        }
163    }
164}
165
166impl<AB: InteractionBuilder> SubAir<AB> for IsLtSubAir {
167    type AirContext<'a>
168        = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
169    where
170        AB::Expr: 'a,
171        AB::Var: 'a,
172        AB: 'a;
173
174    // constrain that out == (x < y) when count != 0
175    // warning: send for range check must be included for the constraints to be sound
176    fn eval<'a>(
177        &'a self,
178        builder: &'a mut AB,
179        (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
180    ) where
181        AB::Var: 'a,
182        AB::Expr: 'a,
183    {
184        // Note: every AIR that uses this sub-AIR must include the range checks for soundness
185        self.eval_range_checks(builder, lower_decomp, io.count.clone());
186        self.eval_without_range_checks(builder, io.y - io.x, io.out, io.count, lower_decomp);
187    }
188}
189
190/// The same subair as [IsLtSubAir] except that non-range check
191/// constraints are not imposed on the last row.
192/// Intended use case is for asserting less than between entries in
193/// adjacent rows.
194#[derive(Clone, Copy, Debug)]
195pub struct IsLtWhenTransitionAir(pub IsLtSubAir);
196
197impl<AB: InteractionBuilder> SubAir<AB> for IsLtWhenTransitionAir {
198    type AirContext<'a>
199        = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
200    where
201        AB::Expr: 'a,
202        AB::Var: 'a,
203        AB: 'a;
204
205    /// Imposes the non-interaction constraints on all except the last row. This is
206    /// intended for use when the comparators `x, y` are on adjacent rows.
207    ///
208    /// This function does also enable the interaction constraints _on every row_.
209    /// The `eval_interactions` performs range checks on `lower_decomp` on every row, even
210    /// though in this AIR the lower_decomp is not used on the last row.
211    /// This simply means the trace generation must fill in the last row with numbers in
212    /// range (e.g., with zeros)
213    fn eval<'a>(
214        &'a self,
215        builder: &'a mut AB,
216        (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
217    ) where
218        AB::Var: 'a,
219        AB::Expr: 'a,
220    {
221        self.0
222            .eval_range_checks(builder, lower_decomp, io.count.clone());
223        self.0.eval_without_range_checks(
224            &mut builder.when_transition(),
225            io.y - io.x,
226            io.out,
227            io.count,
228            lower_decomp,
229        );
230    }
231}
232
233impl<F: Field> TraceSubRowGenerator<F> for IsLtSubAir {
234    /// `(range_checker, x, y)`
235    type TraceContext<'a> = (&'a VariableRangeCheckerChip, u32, u32);
236    /// `(lower_decomp, out)`
237    type ColsMut<'a> = (&'a mut [F], &'a mut F);
238
239    /// Only use this when `count != 0`.
240    #[inline(always)]
241    fn generate_subrow<'a>(
242        &'a self,
243        (range_checker, x, y): (&'a VariableRangeCheckerChip, u32, u32),
244        (lower_decomp, out): (&'a mut [F], &'a mut F),
245    ) {
246        debug_assert_eq!(lower_decomp.len(), self.decomp_limbs);
247        debug_assert!(
248            x < (1 << self.max_bits),
249            "{x} has more than {} bits",
250            self.max_bits
251        );
252        debug_assert!(
253            y < (1 << self.max_bits),
254            "{y} has more than {} bits",
255            self.max_bits
256        );
257        *out = F::from_bool(x < y);
258
259        // obtain the lower_bits
260        let check_less_than = (1 << self.max_bits) + y - x - 1;
261        let lower_u32 = check_less_than & ((1 << self.max_bits) - 1);
262
263        // decompose lower_u32 into limbs and range check
264        range_checker.decompose(lower_u32, self.max_bits, lower_decomp);
265    }
266}