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 + 2^max_bits` is non-negative.
79    ///     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), deg(lower))
139        builder.when(condition).assert_eq(intermed_val, check_val);
140        builder.assert_bool(out);
141    }
142
143    #[inline(always)]
144    pub(crate) fn eval_range_checks<AB: InteractionBuilder>(
145        &self,
146        builder: &mut AB,
147        lower_decomp: &[AB::Var],
148        count: impl Into<AB::Expr>,
149    ) {
150        let count = count.into();
151        let mut bits_remaining = self.max_bits;
152        // we range check the limbs of the lower_decomp so that we know each element
153        // of lower_decomp has the correct number of bits
154        for limb in lower_decomp {
155            // the last limb might have fewer than `bus.range_max_bits` bits
156            let range_bits = bits_remaining.min(self.bus.range_max_bits);
157            self.bus
158                .range_check(*limb, range_bits)
159                .eval(builder, count.clone());
160            bits_remaining = bits_remaining.saturating_sub(self.bus.range_max_bits);
161        }
162    }
163}
164
165impl<AB: InteractionBuilder> SubAir<AB> for IsLtSubAir {
166    type AirContext<'a>
167        = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
168    where
169        AB::Expr: 'a,
170        AB::Var: 'a,
171        AB: 'a;
172
173    // constrain that out == (x < y) when count != 0
174    // warning: send for range check must be included for the constraints to be sound
175    fn eval<'a>(
176        &'a self,
177        builder: &'a mut AB,
178        (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
179    ) where
180        AB::Var: 'a,
181        AB::Expr: 'a,
182    {
183        // Note: every AIR that uses this sub-AIR must include the range checks for soundness
184        self.eval_range_checks(builder, lower_decomp, io.count.clone());
185        self.eval_without_range_checks(builder, io.y - io.x, io.out, io.count, lower_decomp);
186    }
187}
188
189/// The same subair as [IsLtSubAir] except that non-range check
190/// constraints are not imposed on the last row.
191/// Intended use case is for asserting less than between entries in
192/// adjacent rows.
193#[derive(Clone, Copy, Debug)]
194pub struct IsLtWhenTransitionAir(pub IsLtSubAir);
195
196impl<AB: InteractionBuilder> SubAir<AB> for IsLtWhenTransitionAir {
197    type AirContext<'a>
198        = (IsLessThanIo<AB::Expr>, &'a [AB::Var])
199    where
200        AB::Expr: 'a,
201        AB::Var: 'a,
202        AB: 'a;
203
204    /// Imposes the non-interaction constraints on all except the last row. This is
205    /// intended for use when the comparators `x, y` are on adjacent rows.
206    ///
207    /// This function does also enable the interaction constraints _on every row_.
208    /// The `eval_interactions` performs range checks on `lower_decomp` on every row, even
209    /// though in this AIR the lower_decomp is not used on the last row.
210    /// This simply means the trace generation must fill in the last row with numbers in
211    /// range (e.g., with zeros)
212    fn eval<'a>(
213        &'a self,
214        builder: &'a mut AB,
215        (io, lower_decomp): (IsLessThanIo<AB::Expr>, &'a [AB::Var]),
216    ) where
217        AB::Var: 'a,
218        AB::Expr: 'a,
219    {
220        self.0
221            .eval_range_checks(builder, lower_decomp, io.count.clone());
222        self.0.eval_without_range_checks(
223            &mut builder.when_transition(),
224            io.y - io.x,
225            io.out,
226            io.count,
227            lower_decomp,
228        );
229    }
230}
231
232impl<F: Field> TraceSubRowGenerator<F> for IsLtSubAir {
233    /// `(range_checker, x, y)`
234    type TraceContext<'a> = (&'a VariableRangeCheckerChip, u32, u32);
235    /// `(lower_decomp, out)`
236    type ColsMut<'a> = (&'a mut [F], &'a mut F);
237
238    /// Only use this when `count != 0`.
239    #[inline(always)]
240    fn generate_subrow<'a>(
241        &'a self,
242        (range_checker, x, y): (&'a VariableRangeCheckerChip, u32, u32),
243        (lower_decomp, out): (&'a mut [F], &'a mut F),
244    ) {
245        debug_assert_eq!(lower_decomp.len(), self.decomp_limbs);
246        debug_assert!(
247            x < (1 << self.max_bits),
248            "{x} has more than {} bits",
249            self.max_bits
250        );
251        debug_assert!(
252            y < (1 << self.max_bits),
253            "{y} has more than {} bits",
254            self.max_bits
255        );
256        *out = F::from_bool(x < y);
257
258        // obtain the lower_bits
259        let check_less_than = (1 << self.max_bits) + y - x - 1;
260        let lower_u32 = check_less_than & ((1 << self.max_bits) - 1);
261
262        // decompose lower_u32 into limbs and range check
263        range_checker.decompose(lower_u32, self.max_bits, lower_decomp);
264    }
265}