openvm_deferral_circuit/output/
air.rs

1use std::{array::from_fn, borrow::Borrow};
2
3use itertools::{izip, Itertools};
4use openvm_circuit::{
5    arch::{ExecutionBridge, ExecutionState, DEFAULT_BLOCK_SIZE},
6    system::memory::{
7        offline_checker::{MemoryBridge, MemoryReadAuxCols, MemoryWriteAuxCols},
8        MemoryAddress,
9    },
10};
11use openvm_circuit_primitives::{
12    bitwise_op_lookup::BitwiseOperationLookupBus,
13    utils::{assert_array_eq, not},
14    ColumnsAir, StructReflection, StructReflectionHelper,
15};
16use openvm_circuit_primitives_derive::AlignedBorrow;
17use openvm_deferral_transpiler::DeferralOpcode;
18use openvm_instructions::{
19    program::DEFAULT_PC_STEP,
20    riscv::{RV32_CELL_BITS, RV32_MEMORY_AS, RV32_REGISTER_AS, RV32_REGISTER_NUM_LIMBS},
21    LocalOpcode,
22};
23use openvm_stark_backend::{
24    interaction::InteractionBuilder,
25    p3_air::{Air, AirBuilder, BaseAir},
26    p3_field::PrimeCharacteristicRing,
27    p3_matrix::Matrix,
28    BaseAirWithPublicValues, PartitionedBaseAir,
29};
30use openvm_stark_sdk::config::baby_bear_poseidon2::DIGEST_SIZE;
31use p3_field::PrimeField32;
32
33use crate::{
34    canonicity::{CanonicityAuxCols, CanonicitySubAir},
35    count::DeferralCircuitCountBus,
36    poseidon2::DeferralPoseidon2Bus,
37    utils::{
38        byte_commit_to_f, bytes_to_f, combine_output, split_memory_ops, COMMIT_NUM_BYTES,
39        DIGEST_MEMORY_OPS, F_NUM_BYTES, OUTPUT_TOTAL_BYTES, OUTPUT_TOTAL_MEMORY_OPS,
40    },
41};
42
43#[repr(C)]
44#[derive(AlignedBorrow, StructReflection)]
45pub struct DeferralOutputCols<T> {
46    // Indicates the status of this row, i.e. if it is valid and where it is in a
47    // section of rows that correspond to a single opcode invocation
48    pub is_valid: T,
49    pub is_first: T,
50    pub is_last: T,
51    pub section_idx: T,
52
53    // Initial execution state + instruction operands
54    pub from_state: ExecutionState<T>,
55    pub rd_ptr: T,
56    pub rs_ptr: T,
57    pub deferral_idx: T,
58
59    // Heap pointers + auxiliary read columns
60    pub rd_val: [T; RV32_REGISTER_NUM_LIMBS],
61    pub rs_val: [T; RV32_REGISTER_NUM_LIMBS],
62    pub rd_aux: MemoryReadAuxCols<T>,
63    pub rs_aux: MemoryReadAuxCols<T>,
64
65    // Read data and auxiliary columns. output_commit and output_len are read
66    // contiguously from heap with layout [output_commit || output_len].
67    // The onion hash of all bytes written by this opcode invocation is
68    // constrained to output_commit.
69    pub output_commit: [T; COMMIT_NUM_BYTES],
70    pub output_len: [T; F_NUM_BYTES],
71    pub output_commit_and_len_aux: [MemoryReadAuxCols<T>; OUTPUT_TOTAL_MEMORY_OPS],
72
73    // Auxiliary columns to ensure the canonicity of each F byte decomposition in
74    // output_commit.
75    pub output_commit_lt_aux: [CanonicityAuxCols<T>; DIGEST_SIZE],
76
77    // Initial [def_idx, output_len, 0, ...] digest on the first row; on non-first
78    // rows bytes raw_output[local_idx * DIGEST_SIZE..(local_idx + 1) * DIGEST_SIZE]
79    // written to memory and auxiliary columns.
80    pub sponge_inputs: [T; DIGEST_SIZE],
81    pub write_bytes_aux: [MemoryWriteAuxCols<T, DEFAULT_BLOCK_SIZE>; DIGEST_MEMORY_OPS],
82
83    // Capacity of the permutation of write_bytes and the previous row's capacity on
84    // non-last rows, compression on the last row.
85    pub poseidon2_res: [T; DIGEST_SIZE],
86}
87
88#[derive(Clone, Copy, Debug, derive_new::new, ColumnsAir)]
89#[columns_via(DeferralOutputCols<u8>)]
90pub struct DeferralOutputAir {
91    pub execution_bridge: ExecutionBridge,
92    pub memory_bridge: MemoryBridge,
93    pub count_bus: DeferralCircuitCountBus,
94    pub poseidon2_bus: DeferralPoseidon2Bus,
95    pub bitwise_bus: BitwiseOperationLookupBus,
96    pub address_bits: usize,
97}
98
99impl<F> BaseAir<F> for DeferralOutputAir {
100    fn width(&self) -> usize {
101        DeferralOutputCols::<F>::width()
102    }
103}
104impl<F> BaseAirWithPublicValues<F> for DeferralOutputAir {}
105impl<F> PartitionedBaseAir<F> for DeferralOutputAir {}
106
107impl<AB> Air<AB> for DeferralOutputAir
108where
109    AB: InteractionBuilder,
110    AB::F: PrimeField32,
111{
112    fn eval(&self, builder: &mut AB) {
113        let main = builder.main();
114        let local = main.row_slice(0).expect("window should have two elements");
115        let next = main.row_slice(1).expect("window should have two elements");
116        let local: &DeferralOutputCols<AB::Var> = (*local).borrow();
117        let next: &DeferralOutputCols<AB::Var> = (*next).borrow();
118
119        let is_transition = next.is_valid - next.is_first;
120        let is_last = local.is_valid - is_transition.clone();
121
122        // Constrain the status flags. Particularly, section_idx must (a) always
123        // reset to 0 upon reaching a new section, and (b) otherwise increment by
124        // one each valid row. Additionally, for convenience we constrain that
125        // all valid rows must be at the top of the trace.
126        builder.assert_bool(local.is_valid);
127        builder.assert_bool(local.is_first);
128
129        builder
130            .when_transition()
131            .assert_bool(local.is_valid - next.is_valid);
132        builder
133            .when_first_row()
134            .when(local.is_valid)
135            .assert_one(local.is_first);
136
137        builder.assert_eq(local.is_last, is_last);
138
139        builder
140            .when(not(local.is_valid))
141            .assert_zero(local.is_first);
142        builder
143            .when(not(local.is_valid))
144            .assert_zero(local.section_idx);
145
146        builder.when(local.is_first).assert_zero(local.section_idx);
147        builder
148            .when(is_transition.clone())
149            .assert_one(next.section_idx - local.section_idx);
150
151        // Constrain that the read columns and other operands stay the same within a
152        // section, i.e. when section_idx is non-zero. Note that the read auxiliary
153        // columns are only used on the first row - thus we leave their consistency
154        // unconstrained.
155        let mut when_section_transition = builder.when(next.section_idx);
156
157        when_section_transition.assert_eq(local.from_state.pc, next.from_state.pc);
158        when_section_transition.assert_eq(local.from_state.timestamp, next.from_state.timestamp);
159        when_section_transition.assert_eq(local.rd_ptr, next.rd_ptr);
160        when_section_transition.assert_eq(local.rs_ptr, next.rs_ptr);
161        when_section_transition.assert_eq(local.deferral_idx, next.deferral_idx);
162
163        assert_array_eq(&mut when_section_transition, local.rd_val, next.rd_val);
164        assert_array_eq(&mut when_section_transition, local.rs_val, next.rs_val);
165
166        assert_array_eq(
167            &mut when_section_transition,
168            local.output_commit,
169            next.output_commit,
170        );
171        assert_array_eq(
172            &mut when_section_transition,
173            local.output_len,
174            next.output_len,
175        );
176
177        // Constrain the canonicity of output_commit and output_len, i.e. that every
178        // F_NUM_BYTES bytes uniquely represents an element of F.
179        let output_commit_rcs = izip!(
180            local.output_commit.chunks_exact(F_NUM_BYTES),
181            local.output_commit_lt_aux
182        )
183        .map(|(bytes, aux)| {
184            CanonicitySubAir.assert_canonicity(builder, bytes, &aux, local.is_first.into())
185        })
186        .collect_vec();
187
188        for rc_pair in output_commit_rcs.chunks_exact(2) {
189            self.bitwise_bus
190                .send_range(rc_pair[0].clone(), rc_pair[1].clone())
191                .eval(builder, local.is_first);
192        }
193
194        // Constrain the consistency of current_commit_state at each point in this
195        // section's rows. The initial state should be [deferral_idx, output_len,
196        // ..., 0].
197        let output_len = bytes_to_f(&local.output_len);
198        let mut initial_state = [AB::Expr::ZERO; DIGEST_SIZE];
199        initial_state[0] = local.deferral_idx.into();
200        initial_state[1] = output_len.clone();
201
202        assert_array_eq(
203            &mut builder.when(local.is_first),
204            initial_state,
205            local.sponge_inputs,
206        );
207
208        self.count_bus
209            .send(local.deferral_idx)
210            .eval(builder, local.is_first);
211
212        // The final state should be output_commit, and output_len must be the final
213        // section_idx * DIGEST_SIZE.
214        let mut when_last = builder.when(local.is_last);
215
216        when_last.assert_eq(
217            output_len,
218            local.section_idx * AB::Expr::from_usize(DIGEST_SIZE),
219        );
220        assert_array_eq(
221            &mut when_last,
222            byte_commit_to_f(&local.output_commit),
223            local.poseidon2_res,
224        );
225
226        // Constrain poseidon2_res is the running permute capacity on all non-last rows,
227        // and the compression on the last row.
228        let rhs = from_fn(|i| is_transition.clone() * local.poseidon2_res[i]);
229        self.poseidon2_bus
230            .lookup(next.sponge_inputs, rhs, next.poseidon2_res, next.is_last)
231            .eval(builder, next.is_valid);
232
233        // We range check the top byte of both heap pointers to ensure that each access
234        // is in [0, 2^address_bits). The memory merkle argument ensures each pointer
235        // is less than 2^addr_bits, and this range check ensures the decomposition is
236        // canonical. Note that constraining the starting output pointer is sufficient
237        // to constrain the entire write is in range - even if output_ptr + output_len
238        // wraps, there will be several written values in the middle that do not.
239        debug_assert!(RV32_CELL_BITS * RV32_REGISTER_NUM_LIMBS >= self.address_bits);
240        let limb_shift =
241            AB::F::from_usize(1 << (RV32_CELL_BITS * RV32_REGISTER_NUM_LIMBS - self.address_bits));
242
243        self.bitwise_bus
244            .send_range(
245                local.rd_val[RV32_REGISTER_NUM_LIMBS - 1] * limb_shift,
246                local.rs_val[RV32_REGISTER_NUM_LIMBS - 1] * limb_shift,
247            )
248            .eval(builder, local.is_first);
249
250        // We also constrain output_len to be under 2^address_bits.
251        self.bitwise_bus
252            .send_range(
253                local.output_len[RV32_REGISTER_NUM_LIMBS - 1] * limb_shift,
254                AB::Expr::ZERO,
255            )
256            .eval(builder, local.is_first);
257
258        // Constrain the heap pointer memory reads.
259        let d = AB::Expr::from_u32(RV32_REGISTER_AS);
260        let e = AB::Expr::from_u32(RV32_MEMORY_AS);
261
262        self.memory_bridge
263            .read(
264                MemoryAddress::new(d.clone(), local.rd_ptr),
265                local.rd_val,
266                local.from_state.timestamp,
267                &local.rd_aux,
268            )
269            .eval(builder, local.is_first);
270
271        self.memory_bridge
272            .read(
273                MemoryAddress::new(d.clone(), local.rs_ptr),
274                local.rs_val,
275                local.from_state.timestamp + AB::Expr::ONE,
276                &local.rs_aux,
277            )
278            .eval(builder, local.is_first);
279
280        // Constrain memory reads and writes using the MemoryBridge. a and b are
281        // register pointers whose values are read first, then used as heap
282        // pointers. c carries deferral_idx.
283        let input_ptr = bytes_to_f(&local.rs_val);
284        let output_ptr = bytes_to_f(&local.rd_val);
285        let output_len_full = from_fn(|i| {
286            if i < F_NUM_BYTES {
287                local.output_len[i].into()
288            } else {
289                AB::Expr::ZERO
290            }
291        });
292        let output_commit_and_len =
293            combine_output(local.output_commit.map(Into::into), output_len_full);
294        let output_commit_and_len_chunks =
295            split_memory_ops::<_, OUTPUT_TOTAL_BYTES, OUTPUT_TOTAL_MEMORY_OPS>(
296                output_commit_and_len,
297            );
298
299        for (chunk_idx, (data, aux)) in output_commit_and_len_chunks
300            .into_iter()
301            .zip(&local.output_commit_and_len_aux)
302            .enumerate()
303        {
304            self.memory_bridge
305                .read(
306                    MemoryAddress::new(
307                        e.clone(),
308                        input_ptr.clone() + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
309                    ),
310                    data,
311                    local.from_state.timestamp + AB::Expr::from_usize(2 + chunk_idx),
312                    aux,
313                )
314                .eval(builder, local.is_first);
315        }
316
317        let write_bytes_chunks =
318            split_memory_ops::<_, DIGEST_SIZE, DIGEST_MEMORY_OPS>(local.sponge_inputs);
319        let section_idx_minus_one = local.section_idx - AB::Expr::ONE;
320
321        for (chunk_idx, (data, aux)) in write_bytes_chunks
322            .into_iter()
323            .zip(&local.write_bytes_aux)
324            .enumerate()
325        {
326            for bytes in data.chunks(2) {
327                self.bitwise_bus
328                    .send_range(bytes[0], bytes[1])
329                    .eval(builder, local.is_valid - local.is_first);
330            }
331
332            self.memory_bridge
333                .write(
334                    MemoryAddress::new(
335                        e.clone(),
336                        output_ptr.clone()
337                            + (section_idx_minus_one.clone() * AB::Expr::from_usize(DIGEST_SIZE))
338                            + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
339                    ),
340                    data,
341                    local.from_state.timestamp
342                        + AB::Expr::from_usize(2 + OUTPUT_TOTAL_MEMORY_OPS + chunk_idx)
343                        + (section_idx_minus_one.clone() * AB::Expr::from_usize(DIGEST_MEMORY_OPS)),
344                    aux,
345                )
346                .eval(builder, local.is_valid - local.is_first);
347        }
348
349        // Evaluate the execution interaction. Because a single opcode spans many
350        // rows, we only execute this on the last one.
351        self.execution_bridge
352            .execute_and_increment_or_set_pc(
353                AB::Expr::from_usize(DeferralOpcode::OUTPUT.global_opcode_usize()),
354                [
355                    local.rd_ptr.into(),
356                    local.rs_ptr.into(),
357                    local.deferral_idx.into(),
358                    d,
359                    e,
360                ],
361                local.from_state,
362                (local.section_idx * AB::Expr::from_usize(DIGEST_MEMORY_OPS))
363                    + AB::Expr::from_usize(OUTPUT_TOTAL_MEMORY_OPS + 2),
364                (DEFAULT_PC_STEP, None),
365            )
366            .eval(builder, local.is_last);
367    }
368}