openvm_deferral_circuit/call/
air.rs

1use std::{array::from_fn, borrow::Borrow};
2
3use itertools::{izip, Itertools as _};
4use openvm_circuit::{
5    arch::{
6        AdapterAirContext, ExecutionBridge, ExecutionState, ImmInstruction, VmAdapterAir,
7        VmAdapterInterface, VmCoreAir, DEFAULT_BLOCK_SIZE,
8    },
9    system::memory::{
10        offline_checker::{MemoryBridge, MemoryReadAuxCols, MemoryWriteAuxCols},
11        MemoryAddress,
12    },
13};
14use openvm_circuit_primitives::{
15    bitwise_op_lookup::BitwiseOperationLookupBus, ColumnsAir, StructReflection,
16    StructReflectionHelper,
17};
18use openvm_circuit_primitives_derive::AlignedBorrow;
19use openvm_deferral_transpiler::DeferralOpcode;
20use openvm_instructions::{
21    program::DEFAULT_PC_STEP,
22    riscv::{RV32_CELL_BITS, RV32_MEMORY_AS, RV32_REGISTER_AS, RV32_REGISTER_NUM_LIMBS},
23    LocalOpcode, DEFERRAL_AS,
24};
25use openvm_stark_backend::{
26    interaction::InteractionBuilder,
27    p3_air::BaseAir,
28    p3_field::{Field, PrimeCharacteristicRing},
29    BaseAirWithPublicValues,
30};
31use openvm_stark_sdk::config::baby_bear_poseidon2::DIGEST_SIZE;
32use p3_field::PrimeField32;
33
34use crate::{
35    canonicity::{CanonicityAuxCols, CanonicitySubAir},
36    count::DeferralCircuitCountBus,
37    poseidon2::DeferralPoseidon2Bus,
38    utils::{
39        byte_commit_to_f, bytes_to_f, combine_output, split_memory_ops, COMMIT_MEMORY_OPS,
40        COMMIT_NUM_BYTES, DIGEST_MEMORY_OPS, F_NUM_BYTES, OUTPUT_TOTAL_BYTES,
41        OUTPUT_TOTAL_MEMORY_OPS,
42    },
43};
44
45// ========================= CORE ==============================
46
47#[repr(C)]
48#[derive(AlignedBorrow, StructReflection, Clone, Copy, Debug)]
49pub struct DeferralCallReads<B, F> {
50    // Commit to a specific deferral input, passed in by the user as a pointer
51    pub input_commit: [B; COMMIT_NUM_BYTES],
52
53    // Deferral address space accumulators immediately prior to the current deferral call
54    pub old_input_acc: [F; DIGEST_SIZE],
55    pub old_output_acc: [F; DIGEST_SIZE],
56}
57
58#[repr(C)]
59#[derive(AlignedBorrow, StructReflection, Clone, Copy, Debug)]
60pub struct DeferralCallWrites<B, F> {
61    // Output key for raw output + its length in bytes. These bytes are written as one
62    // contiguous heap write, with layout [output_commit || output_len_le]. Note output_len
63    // **must** be divisible by DIGEST_SIZE.
64    pub output_commit: [B; COMMIT_NUM_BYTES],
65    pub output_len: [B; F_NUM_BYTES],
66
67    // Deferral address space accumulators after incorporating the current deferral call
68    pub new_input_acc: [F; DIGEST_SIZE],
69    pub new_output_acc: [F; DIGEST_SIZE],
70}
71
72#[repr(C)]
73#[derive(AlignedBorrow, StructReflection)]
74pub struct DeferralCallCoreCols<T> {
75    pub is_valid: T,
76    pub deferral_idx: T,
77    pub reads: DeferralCallReads<T, T>,
78    pub writes: DeferralCallWrites<T, T>,
79
80    pub input_commit_lt_aux: [CanonicityAuxCols<T>; DIGEST_SIZE],
81    pub output_commit_lt_aux: [CanonicityAuxCols<T>; DIGEST_SIZE],
82}
83
84#[derive(Copy, Clone, Debug, derive_new::new, ColumnsAir)]
85#[columns_via(DeferralCallCoreCols<u8>)]
86pub struct DeferralCallCoreAir {
87    pub count_bus: DeferralCircuitCountBus,
88    pub poseidon2_bus: DeferralPoseidon2Bus,
89    pub bitwise_bus: BitwiseOperationLookupBus,
90}
91
92impl<F: Field> BaseAir<F> for DeferralCallCoreAir {
93    fn width(&self) -> usize {
94        DeferralCallCoreCols::<F>::width()
95    }
96}
97impl<F: Field> BaseAirWithPublicValues<F> for DeferralCallCoreAir {}
98
99impl<AB, I> VmCoreAir<AB, I> for DeferralCallCoreAir
100where
101    AB: InteractionBuilder,
102    AB::F: PrimeField32,
103    I: VmAdapterInterface<AB::Expr>,
104    I::Reads: From<DeferralCallReads<AB::Expr, AB::Expr>>,
105    I::Writes: From<DeferralCallWrites<AB::Expr, AB::Expr>>,
106    I::ProcessedInstruction: From<ImmInstruction<AB::Expr>>,
107{
108    fn eval(
109        &self,
110        builder: &mut AB,
111        local_core: &[AB::Var],
112        _from_pc: AB::Var,
113    ) -> AdapterAirContext<AB::Expr, I> {
114        let cols: &DeferralCallCoreCols<_> = local_core.borrow();
115        builder.assert_bool(cols.is_valid);
116
117        // Constrain the canonicity of both commits and output_len, i.e. that every
118        // F_NUM_BYTES bytes uniquely represents an element of F.
119        let input_commit_rcs = izip!(
120            cols.reads.input_commit.chunks_exact(F_NUM_BYTES),
121            cols.input_commit_lt_aux
122        )
123        .map(|(bytes, aux)| {
124            CanonicitySubAir.assert_canonicity(builder, bytes, &aux, cols.is_valid.into())
125        })
126        .collect_vec();
127
128        let output_commit_rcs = izip!(
129            cols.writes.output_commit.chunks_exact(F_NUM_BYTES),
130            cols.output_commit_lt_aux
131        )
132        .map(|(bytes, aux)| {
133            CanonicitySubAir.assert_canonicity(builder, bytes, &aux, cols.is_valid.into())
134        })
135        .collect_vec();
136
137        for rc_pair in input_commit_rcs.chunks_exact(2) {
138            self.bitwise_bus
139                .send_range(rc_pair[0].clone(), rc_pair[1].clone())
140                .eval(builder, cols.is_valid);
141        }
142        for rc_pair in output_commit_rcs.chunks_exact(2) {
143            self.bitwise_bus
144                .send_range(rc_pair[0].clone(), rc_pair[1].clone())
145                .eval(builder, cols.is_valid);
146        }
147
148        // Range check the bytes that we write to RV32 heap memory.
149        for bytes in cols.writes.output_commit.chunks_exact(2) {
150            self.bitwise_bus
151                .send_range(bytes[0], bytes[1])
152                .eval(builder, cols.is_valid);
153        }
154
155        for bytes in cols.writes.output_len.chunks_exact(2) {
156            self.bitwise_bus
157                .send_range(bytes[0], bytes[1])
158                .eval(builder, cols.is_valid);
159        }
160
161        // Constrain the updated accumulators.
162        let input_f_commit = byte_commit_to_f(&cols.reads.input_commit);
163        let output_f_commit = byte_commit_to_f(&cols.writes.output_commit);
164
165        self.poseidon2_bus
166            .lookup(
167                cols.reads.old_input_acc,
168                input_f_commit,
169                cols.writes.new_input_acc,
170                AB::Expr::ONE,
171            )
172            .eval(builder, cols.is_valid);
173
174        self.poseidon2_bus
175            .lookup(
176                cols.reads.old_output_acc,
177                output_f_commit,
178                cols.writes.new_output_acc,
179                AB::Expr::ONE,
180            )
181            .eval(builder, cols.is_valid);
182
183        self.count_bus
184            .send(cols.deferral_idx)
185            .eval(builder, cols.is_valid);
186
187        AdapterAirContext {
188            to_pc: None,
189            reads: DeferralCallReads {
190                input_commit: cols.reads.input_commit.map(Into::into),
191                old_input_acc: cols.reads.old_input_acc.map(Into::into),
192                old_output_acc: cols.reads.old_output_acc.map(Into::into),
193            }
194            .into(),
195            writes: DeferralCallWrites {
196                output_commit: cols.writes.output_commit.map(Into::into),
197                output_len: cols.writes.output_len.map(Into::into),
198                new_input_acc: cols.writes.new_input_acc.map(Into::into),
199                new_output_acc: cols.writes.new_output_acc.map(Into::into),
200            }
201            .into(),
202            instruction: ImmInstruction {
203                is_valid: cols.is_valid.into(),
204                opcode: AB::Expr::from_usize(DeferralOpcode::CALL.global_opcode_usize()),
205                immediate: cols.deferral_idx.into(),
206            }
207            .into(),
208        }
209    }
210
211    fn start_offset(&self) -> usize {
212        DeferralOpcode::CLASS_OFFSET
213    }
214}
215
216// ========================= ADAPTER ==============================
217
218pub struct DeferralCallAdapterInterface;
219
220impl<T> VmAdapterInterface<T> for DeferralCallAdapterInterface {
221    type Reads = DeferralCallReads<T, T>;
222    type Writes = DeferralCallWrites<T, T>;
223    type ProcessedInstruction = ImmInstruction<T>;
224}
225
226#[repr(C)]
227#[derive(AlignedBorrow, StructReflection)]
228pub struct DeferralCallAdapterCols<T> {
229    pub from_state: ExecutionState<T>,
230    pub rd_ptr: T,
231    pub rs_ptr: T,
232
233    // Heap pointers and aux columns
234    pub rd_val: [T; RV32_REGISTER_NUM_LIMBS],
235    pub rs_val: [T; RV32_REGISTER_NUM_LIMBS],
236    pub rd_aux: MemoryReadAuxCols<T>,
237    pub rs_aux: MemoryReadAuxCols<T>,
238
239    // Read auxiliary columns
240    pub input_commit_aux: [MemoryReadAuxCols<T>; COMMIT_MEMORY_OPS],
241    pub old_input_acc_aux: [MemoryReadAuxCols<T>; DIGEST_MEMORY_OPS],
242    pub old_output_acc_aux: [MemoryReadAuxCols<T>; DIGEST_MEMORY_OPS],
243
244    // Write auxiliary columns
245    pub output_commit_and_len_aux:
246        [MemoryWriteAuxCols<T, DEFAULT_BLOCK_SIZE>; OUTPUT_TOTAL_MEMORY_OPS],
247    pub new_input_acc_aux: [MemoryWriteAuxCols<T, DEFAULT_BLOCK_SIZE>; DIGEST_MEMORY_OPS],
248    pub new_output_acc_aux: [MemoryWriteAuxCols<T, DEFAULT_BLOCK_SIZE>; DIGEST_MEMORY_OPS],
249}
250
251#[derive(Clone, Copy, Debug, derive_new::new, ColumnsAir)]
252#[columns_via(DeferralCallAdapterCols<u8>)]
253pub struct DeferralCallAdapterAir {
254    pub execution_bridge: ExecutionBridge,
255    pub memory_bridge: MemoryBridge,
256    pub bitwise_bus: BitwiseOperationLookupBus,
257    pub address_bits: usize,
258}
259
260impl<F: Field> BaseAir<F> for DeferralCallAdapterAir {
261    fn width(&self) -> usize {
262        DeferralCallAdapterCols::<F>::width()
263    }
264}
265
266impl<AB: InteractionBuilder> VmAdapterAir<AB> for DeferralCallAdapterAir {
267    type Interface = DeferralCallAdapterInterface;
268
269    fn eval(
270        &self,
271        builder: &mut AB,
272        local: &[AB::Var],
273        ctx: AdapterAirContext<AB::Expr, Self::Interface>,
274    ) {
275        let cols: &DeferralCallAdapterCols<_> = local.borrow();
276        let timestamp = cols.from_state.timestamp;
277        let mut timestamp_delta = 0usize;
278        let mut timestamp_pp = || {
279            timestamp_delta += 1;
280            timestamp + AB::F::from_usize(timestamp_delta - 1)
281        };
282
283        // Operands a and b are RV32 register pointers. Their values are read first
284        // to get heap pointers for output write and input commit read respectively.
285        let d = AB::Expr::from_u32(RV32_REGISTER_AS);
286        let e = AB::Expr::from_u32(RV32_MEMORY_AS);
287
288        // Heap pointers are first read from their respective registers.
289        self.memory_bridge
290            .read(
291                MemoryAddress::new(d.clone(), cols.rd_ptr),
292                cols.rd_val,
293                timestamp_pp(),
294                &cols.rd_aux,
295            )
296            .eval(builder, ctx.instruction.is_valid.clone());
297
298        self.memory_bridge
299            .read(
300                MemoryAddress::new(d.clone(), cols.rs_ptr),
301                cols.rs_val,
302                timestamp_pp(),
303                &cols.rs_aux,
304            )
305            .eval(builder, ctx.instruction.is_valid.clone());
306
307        // We range check the top byte of both heap pointers to ensure that each
308        // access is in [0, 2^address_bits). The memory merkle argument ensures
309        // that each read/write pointer is less than 2^addr_bits, and this range
310        // check ensures the accesses don't wrap around P.
311        debug_assert!(RV32_CELL_BITS * RV32_REGISTER_NUM_LIMBS >= self.address_bits);
312        let limb_shift =
313            AB::F::from_usize(1 << (RV32_CELL_BITS * RV32_REGISTER_NUM_LIMBS - self.address_bits));
314
315        self.bitwise_bus
316            .send_range(
317                cols.rd_val[RV32_REGISTER_NUM_LIMBS - 1] * limb_shift,
318                cols.rs_val[RV32_REGISTER_NUM_LIMBS - 1] * limb_shift,
319            )
320            .eval(builder, ctx.instruction.is_valid.clone());
321
322        // Accumulators are read then updated in the deferral address space,
323        // using deferral_idx (instruction immediate / operand c) to determine
324        // the accumulator memory address.
325        let input_ptr = bytes_to_f(&cols.rs_val);
326        let output_ptr = bytes_to_f(&cols.rd_val);
327
328        let deferral_idx = ctx.instruction.immediate;
329        let deferral_as = AB::Expr::from_u32(DEFERRAL_AS);
330
331        let digest_size = AB::F::from_usize(DIGEST_SIZE);
332        let input_acc_ptr = deferral_idx.clone() * AB::Expr::TWO * digest_size;
333        let output_acc_ptr = input_acc_ptr.clone() + digest_size;
334
335        let DeferralCallReads {
336            input_commit,
337            old_input_acc,
338            old_output_acc,
339        } = ctx.reads;
340        let DeferralCallWrites {
341            output_commit,
342            output_len,
343            new_input_acc,
344            new_output_acc,
345        } = ctx.writes;
346
347        // Constrain output_len to be under 2^address_bits also.
348        self.bitwise_bus
349            .send_range(
350                output_len[RV32_REGISTER_NUM_LIMBS - 1].clone() * limb_shift,
351                AB::Expr::ZERO,
352            )
353            .eval(builder, ctx.instruction.is_valid.clone());
354
355        let output_len_full = from_fn(|i| {
356            if i < F_NUM_BYTES {
357                output_len[i].clone()
358            } else {
359                AB::Expr::ZERO
360            }
361        });
362
363        let input_commit_chunks =
364            split_memory_ops::<_, COMMIT_NUM_BYTES, COMMIT_MEMORY_OPS>(input_commit);
365        for (chunk_idx, (data, aux)) in input_commit_chunks
366            .into_iter()
367            .zip(&cols.input_commit_aux)
368            .enumerate()
369        {
370            self.memory_bridge
371                .read(
372                    MemoryAddress::new(
373                        e.clone(),
374                        input_ptr.clone() + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
375                    ),
376                    data,
377                    timestamp_pp(),
378                    aux,
379                )
380                .eval(builder, ctx.instruction.is_valid.clone());
381        }
382
383        let old_input_acc_chunks =
384            split_memory_ops::<_, DIGEST_SIZE, DIGEST_MEMORY_OPS>(old_input_acc);
385        for (chunk_idx, (data, aux)) in old_input_acc_chunks
386            .into_iter()
387            .zip(&cols.old_input_acc_aux)
388            .enumerate()
389        {
390            self.memory_bridge
391                .read(
392                    MemoryAddress::new(
393                        deferral_as.clone(),
394                        input_acc_ptr.clone()
395                            + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
396                    ),
397                    data,
398                    timestamp_pp(),
399                    aux,
400                )
401                .eval(builder, ctx.instruction.is_valid.clone());
402        }
403
404        let old_output_acc_chunks =
405            split_memory_ops::<_, DIGEST_SIZE, DIGEST_MEMORY_OPS>(old_output_acc);
406        for (chunk_idx, (data, aux)) in old_output_acc_chunks
407            .into_iter()
408            .zip(&cols.old_output_acc_aux)
409            .enumerate()
410        {
411            self.memory_bridge
412                .read(
413                    MemoryAddress::new(
414                        deferral_as.clone(),
415                        output_acc_ptr.clone()
416                            + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
417                    ),
418                    data,
419                    timestamp_pp(),
420                    aux,
421                )
422                .eval(builder, ctx.instruction.is_valid.clone());
423        }
424
425        let output_commit_and_len = combine_output(output_commit, output_len_full);
426        let output_commit_and_len_chunks =
427            split_memory_ops::<_, OUTPUT_TOTAL_BYTES, OUTPUT_TOTAL_MEMORY_OPS>(
428                output_commit_and_len,
429            );
430        for (chunk_idx, (data, aux)) in output_commit_and_len_chunks
431            .into_iter()
432            .zip(&cols.output_commit_and_len_aux)
433            .enumerate()
434        {
435            self.memory_bridge
436                .write(
437                    MemoryAddress::new(
438                        e.clone(),
439                        output_ptr.clone() + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
440                    ),
441                    data,
442                    timestamp_pp(),
443                    aux,
444                )
445                .eval(builder, ctx.instruction.is_valid.clone());
446        }
447
448        let new_input_acc_chunks =
449            split_memory_ops::<_, DIGEST_SIZE, DIGEST_MEMORY_OPS>(new_input_acc);
450        for (chunk_idx, (data, aux)) in new_input_acc_chunks
451            .into_iter()
452            .zip(&cols.new_input_acc_aux)
453            .enumerate()
454        {
455            self.memory_bridge
456                .write(
457                    MemoryAddress::new(
458                        deferral_as.clone(),
459                        input_acc_ptr.clone()
460                            + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
461                    ),
462                    data,
463                    timestamp_pp(),
464                    aux,
465                )
466                .eval(builder, ctx.instruction.is_valid.clone());
467        }
468
469        let new_output_acc_chunks =
470            split_memory_ops::<_, DIGEST_SIZE, DIGEST_MEMORY_OPS>(new_output_acc);
471        for (chunk_idx, (data, aux)) in new_output_acc_chunks
472            .into_iter()
473            .zip(&cols.new_output_acc_aux)
474            .enumerate()
475        {
476            self.memory_bridge
477                .write(
478                    MemoryAddress::new(
479                        deferral_as.clone(),
480                        output_acc_ptr.clone()
481                            + AB::Expr::from_usize(chunk_idx * DEFAULT_BLOCK_SIZE),
482                    ),
483                    data,
484                    timestamp_pp(),
485                    aux,
486                )
487                .eval(builder, ctx.instruction.is_valid.clone());
488        }
489
490        self.execution_bridge
491            .execute_and_increment_or_set_pc(
492                ctx.instruction.opcode,
493                [
494                    cols.rd_ptr.into(),
495                    cols.rs_ptr.into(),
496                    deferral_idx,
497                    d.clone(),
498                    e.clone(),
499                ],
500                cols.from_state,
501                AB::Expr::from_usize(timestamp_delta),
502                (DEFAULT_PC_STEP, ctx.to_pc),
503            )
504            .eval(builder, ctx.instruction.is_valid);
505    }
506
507    fn get_from_pc(&self, local: &[AB::Var]) -> AB::Var {
508        let cols: &DeferralCallAdapterCols<_> = local.borrow();
509        cols.from_state.pc
510    }
511}