openvm_sha256_circuit/sha256_chip/
air.rs

1use std::{array, borrow::Borrow, cmp::min};
2
3use openvm_circuit::{
4    arch::ExecutionBridge,
5    system::memory::{offline_checker::MemoryBridge, MemoryAddress},
6};
7use openvm_circuit_primitives::{
8    bitwise_op_lookup::BitwiseOperationLookupBus, encoder::Encoder, utils::not, SubAir,
9};
10use openvm_instructions::{
11    riscv::{RV32_CELL_BITS, RV32_MEMORY_AS, RV32_REGISTER_AS, RV32_REGISTER_NUM_LIMBS},
12    LocalOpcode,
13};
14use openvm_sha256_air::{
15    compose, Sha256Air, SHA256_BLOCK_U8S, SHA256_HASH_WORDS, SHA256_ROUNDS_PER_ROW,
16    SHA256_WORD_U16S, SHA256_WORD_U8S,
17};
18use openvm_sha256_transpiler::Rv32Sha256Opcode;
19use openvm_stark_backend::{
20    interaction::InteractionBuilder,
21    p3_air::{Air, AirBuilder, BaseAir},
22    p3_field::{Field, FieldAlgebra},
23    p3_matrix::Matrix,
24    rap::{BaseAirWithPublicValues, PartitionedBaseAir},
25};
26
27use super::{
28    Sha256VmDigestCols, Sha256VmRoundCols, SHA256VM_CONTROL_WIDTH, SHA256VM_DIGEST_WIDTH,
29    SHA256VM_ROUND_WIDTH, SHA256VM_WIDTH, SHA256_READ_SIZE,
30};
31
32/// Sha256VmAir does all constraints related to message padding and
33/// the Sha256Air subair constrains the actual hash
34#[derive(Clone, Debug, derive_new::new)]
35pub struct Sha256VmAir {
36    pub execution_bridge: ExecutionBridge,
37    pub memory_bridge: MemoryBridge,
38    /// Bus to send byte checks to
39    pub bitwise_lookup_bus: BitwiseOperationLookupBus,
40    /// Maximum number of bits allowed for an address pointer
41    /// Must be at least 24
42    pub ptr_max_bits: usize,
43    pub(super) sha256_subair: Sha256Air,
44    pub(super) padding_encoder: Encoder,
45}
46
47impl<F: Field> BaseAirWithPublicValues<F> for Sha256VmAir {}
48impl<F: Field> PartitionedBaseAir<F> for Sha256VmAir {}
49impl<F: Field> BaseAir<F> for Sha256VmAir {
50    fn width(&self) -> usize {
51        SHA256VM_WIDTH
52    }
53}
54
55impl<AB: InteractionBuilder> Air<AB> for Sha256VmAir {
56    fn eval(&self, builder: &mut AB) {
57        self.eval_padding(builder);
58        self.eval_transitions(builder);
59        self.eval_reads(builder);
60        self.eval_last_row(builder);
61
62        self.sha256_subair.eval(builder, SHA256VM_CONTROL_WIDTH);
63    }
64}
65
66#[allow(dead_code, non_camel_case_types)]
67pub(super) enum PaddingFlags {
68    /// Not considered for padding - W's are not constrained
69    NotConsidered,
70    /// Not padding - W's should be equal to the message
71    NotPadding,
72    /// FIRST_PADDING_i: it is the first row with padding and there are i cells of non-padding
73    FirstPadding0,
74    FirstPadding1,
75    FirstPadding2,
76    FirstPadding3,
77    FirstPadding4,
78    FirstPadding5,
79    FirstPadding6,
80    FirstPadding7,
81    FirstPadding8,
82    FirstPadding9,
83    FirstPadding10,
84    FirstPadding11,
85    FirstPadding12,
86    FirstPadding13,
87    FirstPadding14,
88    FirstPadding15,
89    /// FIRST_PADDING_i_LastRow: it is the first row with padding and there are i cells of
90    /// non-padding                          AND it is the last reading row of the message
91    /// NOTE: if the Last row has padding it has to be at least 9 cells since the last 8 cells are
92    /// padded with the message length
93    FirstPadding0_LastRow,
94    FirstPadding1_LastRow,
95    FirstPadding2_LastRow,
96    FirstPadding3_LastRow,
97    FirstPadding4_LastRow,
98    FirstPadding5_LastRow,
99    FirstPadding6_LastRow,
100    FirstPadding7_LastRow,
101    /// The entire row is padding AND it is not the first row with padding
102    /// AND it is the 4th row of the last block of the message
103    EntirePaddingLastRow,
104    /// The entire row is padding AND it is not the first row with padding
105    EntirePadding,
106}
107
108impl PaddingFlags {
109    /// The number of padding flags (including NotConsidered)
110    pub const COUNT: usize = EntirePadding as usize + 1;
111}
112
113use PaddingFlags::*;
114impl Sha256VmAir {
115    /// Implement all necessary constraints for the padding
116    fn eval_padding<AB: InteractionBuilder>(&self, builder: &mut AB) {
117        let main = builder.main();
118        let (local, next) = (main.row_slice(0), main.row_slice(1));
119        let local_cols: &Sha256VmRoundCols<AB::Var> = local[..SHA256VM_ROUND_WIDTH].borrow();
120        let next_cols: &Sha256VmRoundCols<AB::Var> = next[..SHA256VM_ROUND_WIDTH].borrow();
121
122        // Constrain the sanity of the padding flags
123        self.padding_encoder
124            .eval(builder, &local_cols.control.pad_flags);
125
126        builder.assert_one(self.padding_encoder.contains_flag_range::<AB>(
127            &local_cols.control.pad_flags,
128            NotConsidered as usize..=EntirePadding as usize,
129        ));
130
131        Self::eval_padding_transitions(self, builder, local_cols, next_cols);
132        Self::eval_padding_row(self, builder, local_cols);
133    }
134
135    fn eval_padding_transitions<AB: InteractionBuilder>(
136        &self,
137        builder: &mut AB,
138        local: &Sha256VmRoundCols<AB::Var>,
139        next: &Sha256VmRoundCols<AB::Var>,
140    ) {
141        let next_is_last_row = next.inner.flags.is_digest_row * next.inner.flags.is_last_block;
142
143        // Constrain that `padding_occured` is 1 on a suffix of rows in each message, excluding the
144        // last digest row, and 0 everywhere else. Furthermore, the suffix starts in the
145        // first 4 rows of some block.
146
147        builder.assert_bool(local.control.padding_occurred);
148        // Last round row in the last block has padding_occurred = 1
149        // This is the end of the suffix
150        builder
151            .when(next_is_last_row.clone())
152            .assert_one(local.control.padding_occurred);
153
154        // Digest row in the last block has padding_occurred = 0
155        builder
156            .when(next_is_last_row.clone())
157            .assert_zero(next.control.padding_occurred);
158
159        // If padding_occurred = 1 in the current row, then padding_occurred = 1 in the next row,
160        // unless next is the last digest row
161        builder
162            .when(local.control.padding_occurred - next_is_last_row.clone())
163            .assert_one(next.control.padding_occurred);
164
165        // If next row is not first 4 rows of a block, then next.padding_occurred =
166        // local.padding_occurred. So padding_occurred only changes in the first 4 rows of a
167        // block.
168        builder
169            .when_transition()
170            .when(not(next.inner.flags.is_first_4_rows) - next_is_last_row)
171            .assert_eq(
172                next.control.padding_occurred,
173                local.control.padding_occurred,
174            );
175
176        // Constrain the that the start of the padding is correct
177        let next_is_first_padding_row =
178            next.control.padding_occurred - local.control.padding_occurred;
179        // Row index if its between 0..4, else 0
180        let next_row_idx = self.sha256_subair.row_idx_encoder.flag_with_val::<AB>(
181            &next.inner.flags.row_idx,
182            &(0..4).map(|x| (x, x)).collect::<Vec<_>>(),
183        );
184        // How many non-padding cells there are in the next row.
185        // Will be 0 on non-padding rows.
186        let next_padding_offset = self.padding_encoder.flag_with_val::<AB>(
187            &next.control.pad_flags,
188            &(0..16)
189                .map(|i| (FirstPadding0 as usize + i, i))
190                .collect::<Vec<_>>(),
191        ) + self.padding_encoder.flag_with_val::<AB>(
192            &next.control.pad_flags,
193            &(0..8)
194                .map(|i| (FirstPadding0_LastRow as usize + i, i))
195                .collect::<Vec<_>>(),
196        );
197
198        // Will be 0 on last digest row since:
199        //   - padding_occurred = 0 is constrained above
200        //   - next_row_idx = 0 since row_idx is not in 0..4
201        //   - and next_padding_offset = 0 since `pad_flags = NotConsidered`
202        let expected_len = next.inner.flags.local_block_idx
203            * next.control.padding_occurred
204            * AB::Expr::from_canonical_usize(SHA256_BLOCK_U8S)
205            + next_row_idx * AB::Expr::from_canonical_usize(SHA256_READ_SIZE)
206            + next_padding_offset;
207
208        // Note: `next_is_first_padding_row` is either -1,0,1
209        // If 1, then this constrains the length of message
210        // If -1, then `next` must be the last digest row and so this constraint will be 0 == 0
211        builder.when(next_is_first_padding_row).assert_eq(
212            expected_len,
213            next.control.len * next.control.padding_occurred,
214        );
215
216        // Constrain the padding flags are of correct type (eg is not padding or first padding)
217        let is_next_first_padding = self.padding_encoder.contains_flag_range::<AB>(
218            &next.control.pad_flags,
219            FirstPadding0 as usize..=FirstPadding7_LastRow as usize,
220        );
221
222        let is_next_last_padding = self.padding_encoder.contains_flag_range::<AB>(
223            &next.control.pad_flags,
224            FirstPadding0_LastRow as usize..=EntirePaddingLastRow as usize,
225        );
226
227        let is_next_entire_padding = self.padding_encoder.contains_flag_range::<AB>(
228            &next.control.pad_flags,
229            EntirePaddingLastRow as usize..=EntirePadding as usize,
230        );
231
232        let is_next_not_considered = self
233            .padding_encoder
234            .contains_flag::<AB>(&next.control.pad_flags, &[NotConsidered as usize]);
235
236        let is_next_not_padding = self
237            .padding_encoder
238            .contains_flag::<AB>(&next.control.pad_flags, &[NotPadding as usize]);
239
240        let is_next_4th_row = self
241            .sha256_subair
242            .row_idx_encoder
243            .contains_flag::<AB>(&next.inner.flags.row_idx, &[3]);
244
245        // `pad_flags` is `NotConsidered` on all rows except the first 4 rows of a block
246        builder.assert_eq(
247            not(next.inner.flags.is_first_4_rows),
248            is_next_not_considered,
249        );
250
251        // `pad_flags` is `EntirePadding` if the previous row is padding
252        builder.when(next.inner.flags.is_first_4_rows).assert_eq(
253            local.control.padding_occurred * next.control.padding_occurred,
254            is_next_entire_padding,
255        );
256
257        // `pad_flags` is `FirstPadding*` if current row is padding and the previous row is not
258        // padding
259        builder.when(next.inner.flags.is_first_4_rows).assert_eq(
260            not(local.control.padding_occurred) * next.control.padding_occurred,
261            is_next_first_padding,
262        );
263
264        // `pad_flags` is `NotPadding` if current row is not padding
265        builder
266            .when(next.inner.flags.is_first_4_rows)
267            .assert_eq(not(next.control.padding_occurred), is_next_not_padding);
268
269        // `pad_flags` is `*LastRow` on the row that contsrains the last four words of the message
270        builder
271            .when(next.inner.flags.is_last_block)
272            .assert_eq(is_next_4th_row, is_next_last_padding);
273    }
274
275    fn eval_padding_row<AB: InteractionBuilder>(
276        &self,
277        builder: &mut AB,
278        local: &Sha256VmRoundCols<AB::Var>,
279    ) {
280        let message: [AB::Var; SHA256_READ_SIZE] = array::from_fn(|i| {
281            local.inner.message_schedule.carry_or_buffer[i / (SHA256_WORD_U8S)]
282                [i % (SHA256_WORD_U8S)]
283        });
284
285        let get_ith_byte = |i: usize| {
286            let word_idx = i / SHA256_ROUNDS_PER_ROW;
287            let word = local.inner.message_schedule.w[word_idx].map(|x| x.into());
288            // Need to reverse the byte order to match the endianness of the memory
289            let byte_idx = 4 - i % 4 - 1;
290            compose::<AB::Expr>(&word[byte_idx * 8..(byte_idx + 1) * 8], 1)
291        };
292
293        let is_not_padding = self
294            .padding_encoder
295            .contains_flag::<AB>(&local.control.pad_flags, &[NotPadding as usize]);
296
297        // Check the `w`s on case by case basis
298        for (i, message_byte) in message.iter().enumerate() {
299            let w = get_ith_byte(i);
300            let should_be_message = is_not_padding.clone()
301                + if i < 15 {
302                    self.padding_encoder.contains_flag_range::<AB>(
303                        &local.control.pad_flags,
304                        FirstPadding0 as usize + i + 1..=FirstPadding15 as usize,
305                    )
306                } else {
307                    AB::Expr::ZERO
308                }
309                + if i < 7 {
310                    self.padding_encoder.contains_flag_range::<AB>(
311                        &local.control.pad_flags,
312                        FirstPadding0_LastRow as usize + i + 1..=FirstPadding7_LastRow as usize,
313                    )
314                } else {
315                    AB::Expr::ZERO
316                };
317            builder
318                .when(should_be_message)
319                .assert_eq(w.clone(), *message_byte);
320
321            let should_be_zero = self
322                .padding_encoder
323                .contains_flag::<AB>(&local.control.pad_flags, &[EntirePadding as usize])
324                + if i < 12 {
325                    self.padding_encoder.contains_flag::<AB>(
326                        &local.control.pad_flags,
327                        &[EntirePaddingLastRow as usize],
328                    ) + if i > 0 {
329                        self.padding_encoder.contains_flag_range::<AB>(
330                            &local.control.pad_flags,
331                            FirstPadding0_LastRow as usize
332                                ..=min(
333                                    FirstPadding0_LastRow as usize + i - 1,
334                                    FirstPadding7_LastRow as usize,
335                                ),
336                        )
337                    } else {
338                        AB::Expr::ZERO
339                    }
340                } else {
341                    AB::Expr::ZERO
342                }
343                + if i > 0 {
344                    self.padding_encoder.contains_flag_range::<AB>(
345                        &local.control.pad_flags,
346                        FirstPadding0 as usize..=FirstPadding0 as usize + i - 1,
347                    )
348                } else {
349                    AB::Expr::ZERO
350                };
351            builder.when(should_be_zero).assert_zero(w.clone());
352
353            // Assumes bit-length of message is a multiple of 8 (message is bytes)
354            // This is true because the message is given as &[u8]
355            let should_be_128 = self
356                .padding_encoder
357                .contains_flag::<AB>(&local.control.pad_flags, &[FirstPadding0 as usize + i])
358                + if i < 8 {
359                    self.padding_encoder.contains_flag::<AB>(
360                        &local.control.pad_flags,
361                        &[FirstPadding0_LastRow as usize + i],
362                    )
363                } else {
364                    AB::Expr::ZERO
365                };
366
367            builder
368                .when(should_be_128)
369                .assert_eq(AB::Expr::from_canonical_u32(1 << 7), w);
370
371            // should be len is handled outside of the loop
372        }
373        let appended_len = compose::<AB::Expr>(
374            &[
375                get_ith_byte(15),
376                get_ith_byte(14),
377                get_ith_byte(13),
378                get_ith_byte(12),
379            ],
380            RV32_CELL_BITS,
381        );
382
383        let actual_len = local.control.len;
384
385        let is_last_padding_row = self.padding_encoder.contains_flag_range::<AB>(
386            &local.control.pad_flags,
387            FirstPadding0_LastRow as usize..=EntirePaddingLastRow as usize,
388        );
389
390        builder.when(is_last_padding_row.clone()).assert_eq(
391            appended_len * AB::F::from_canonical_usize(RV32_CELL_BITS).inverse(), // bit to byte conversion
392            actual_len,
393        );
394
395        // We constrain that the appended length is in bytes
396        builder.when(is_last_padding_row.clone()).assert_zero(
397            local.inner.message_schedule.w[3][0]
398                + local.inner.message_schedule.w[3][1]
399                + local.inner.message_schedule.w[3][2],
400        );
401
402        // We can't support messages longer than 2^30 bytes because the length has to fit in a field
403        // element. So, constrain that the first 4 bytes of the length are 0.
404        // Thus, the bit-length is < 2^32 so the message is < 2^29 bytes.
405        for i in 8..12 {
406            builder
407                .when(is_last_padding_row.clone())
408                .assert_zero(get_ith_byte(i));
409        }
410    }
411    /// Implement constraints on `len`, `read_ptr` and `cur_timestamp`
412    fn eval_transitions<AB: InteractionBuilder>(&self, builder: &mut AB) {
413        let main = builder.main();
414        let (local, next) = (main.row_slice(0), main.row_slice(1));
415        let local_cols: &Sha256VmRoundCols<AB::Var> = local[..SHA256VM_ROUND_WIDTH].borrow();
416        let next_cols: &Sha256VmRoundCols<AB::Var> = next[..SHA256VM_ROUND_WIDTH].borrow();
417
418        let is_last_row =
419            local_cols.inner.flags.is_last_block * local_cols.inner.flags.is_digest_row;
420
421        // Len should be the same for the entire message
422        builder
423            .when_transition()
424            .when(not::<AB::Expr>(is_last_row.clone()))
425            .assert_eq(next_cols.control.len, local_cols.control.len);
426
427        // Read ptr should increment by [SHA256_READ_SIZE] for the first 4 rows and stay the same
428        // otherwise
429        let read_ptr_delta = local_cols.inner.flags.is_first_4_rows
430            * AB::Expr::from_canonical_usize(SHA256_READ_SIZE);
431        builder
432            .when_transition()
433            .when(not::<AB::Expr>(is_last_row.clone()))
434            .assert_eq(
435                next_cols.control.read_ptr,
436                local_cols.control.read_ptr + read_ptr_delta,
437            );
438
439        // Timestamp should increment by 1 for the first 4 rows and stay the same otherwise
440        let timestamp_delta = local_cols.inner.flags.is_first_4_rows * AB::Expr::ONE;
441        builder
442            .when_transition()
443            .when(not::<AB::Expr>(is_last_row.clone()))
444            .assert_eq(
445                next_cols.control.cur_timestamp,
446                local_cols.control.cur_timestamp + timestamp_delta,
447            );
448    }
449
450    /// Implement the reads for the first 4 rows of a block
451    fn eval_reads<AB: InteractionBuilder>(&self, builder: &mut AB) {
452        let main = builder.main();
453        let local = main.row_slice(0);
454        let local_cols: &Sha256VmRoundCols<AB::Var> = local[..SHA256VM_ROUND_WIDTH].borrow();
455
456        let message: [AB::Var; SHA256_READ_SIZE] = array::from_fn(|i| {
457            local_cols.inner.message_schedule.carry_or_buffer[i / (SHA256_WORD_U16S * 2)]
458                [i % (SHA256_WORD_U16S * 2)]
459        });
460
461        self.memory_bridge
462            .read(
463                MemoryAddress::new(
464                    AB::Expr::from_canonical_u32(RV32_MEMORY_AS),
465                    local_cols.control.read_ptr,
466                ),
467                message,
468                local_cols.control.cur_timestamp,
469                &local_cols.read_aux,
470            )
471            .eval(builder, local_cols.inner.flags.is_first_4_rows);
472    }
473    /// Implement the constraints for the last row of a message
474    fn eval_last_row<AB: InteractionBuilder>(&self, builder: &mut AB) {
475        let main = builder.main();
476        let local = main.row_slice(0);
477        let local_cols: &Sha256VmDigestCols<AB::Var> = local[..SHA256VM_DIGEST_WIDTH].borrow();
478
479        let timestamp: AB::Var = local_cols.from_state.timestamp;
480        let mut timestamp_delta: usize = 0;
481        let mut timestamp_pp = || {
482            timestamp_delta += 1;
483            timestamp + AB::Expr::from_canonical_usize(timestamp_delta - 1)
484        };
485
486        let is_last_row =
487            local_cols.inner.flags.is_last_block * local_cols.inner.flags.is_digest_row;
488
489        self.memory_bridge
490            .read(
491                MemoryAddress::new(
492                    AB::Expr::from_canonical_u32(RV32_REGISTER_AS),
493                    local_cols.rd_ptr,
494                ),
495                local_cols.dst_ptr,
496                timestamp_pp(),
497                &local_cols.register_reads_aux[0],
498            )
499            .eval(builder, is_last_row.clone());
500
501        self.memory_bridge
502            .read(
503                MemoryAddress::new(
504                    AB::Expr::from_canonical_u32(RV32_REGISTER_AS),
505                    local_cols.rs1_ptr,
506                ),
507                local_cols.src_ptr,
508                timestamp_pp(),
509                &local_cols.register_reads_aux[1],
510            )
511            .eval(builder, is_last_row.clone());
512
513        self.memory_bridge
514            .read(
515                MemoryAddress::new(
516                    AB::Expr::from_canonical_u32(RV32_REGISTER_AS),
517                    local_cols.rs2_ptr,
518                ),
519                local_cols.len_data,
520                timestamp_pp(),
521                &local_cols.register_reads_aux[2],
522            )
523            .eval(builder, is_last_row.clone());
524
525        // range check that the memory pointers don't overflow
526        // Note: no need to range check the length since we read from memory step by step and
527        //       the memory bus will catch any memory accesses beyond ptr_max_bits
528        let shift = AB::Expr::from_canonical_usize(
529            1 << (RV32_REGISTER_NUM_LIMBS * RV32_CELL_BITS - self.ptr_max_bits),
530        );
531        // This only works if self.ptr_max_bits >= 24 which is typically the case
532        self.bitwise_lookup_bus
533            .send_range(
534                // It is fine to shift like this since we already know that dst_ptr and src_ptr
535                // have [RV32_CELL_BITS] bits
536                local_cols.dst_ptr[RV32_REGISTER_NUM_LIMBS - 1] * shift.clone(),
537                local_cols.src_ptr[RV32_REGISTER_NUM_LIMBS - 1] * shift.clone(),
538            )
539            .eval(builder, is_last_row.clone());
540
541        // the number of reads that happened to read the entire message: we do 4 reads per block
542        let time_delta = (local_cols.inner.flags.local_block_idx + AB::Expr::ONE)
543            * AB::Expr::from_canonical_usize(4);
544        // Every time we read the message we increment the read pointer by SHA256_READ_SIZE
545        let read_ptr_delta = time_delta.clone() * AB::Expr::from_canonical_usize(SHA256_READ_SIZE);
546
547        let result: [AB::Var; SHA256_WORD_U8S * SHA256_HASH_WORDS] = array::from_fn(|i| {
548            // The limbs are written in big endian order to the memory so need to be reversed
549            local_cols.inner.final_hash[i / SHA256_WORD_U8S]
550                [SHA256_WORD_U8S - i % SHA256_WORD_U8S - 1]
551        });
552
553        let dst_ptr_val =
554            compose::<AB::Expr>(&local_cols.dst_ptr.map(|x| x.into()), RV32_CELL_BITS);
555
556        // Note: revisit in the future to do 2 block writes of 16 cells instead of 1 block write of
557        // 32 cells       This could be beneficial as the output is often an input for
558        // another hash
559        self.memory_bridge
560            .write(
561                MemoryAddress::new(AB::Expr::from_canonical_u32(RV32_MEMORY_AS), dst_ptr_val),
562                result,
563                timestamp_pp() + time_delta.clone(),
564                &local_cols.writes_aux,
565            )
566            .eval(builder, is_last_row.clone());
567
568        self.execution_bridge
569            .execute_and_increment_pc(
570                AB::Expr::from_canonical_usize(Rv32Sha256Opcode::SHA256.global_opcode().as_usize()),
571                [
572                    local_cols.rd_ptr.into(),
573                    local_cols.rs1_ptr.into(),
574                    local_cols.rs2_ptr.into(),
575                    AB::Expr::from_canonical_u32(RV32_REGISTER_AS),
576                    AB::Expr::from_canonical_u32(RV32_MEMORY_AS),
577                ],
578                local_cols.from_state,
579                AB::Expr::from_canonical_usize(timestamp_delta) + time_delta.clone(),
580            )
581            .eval(builder, is_last_row.clone());
582
583        // Assert that we read the correct length of the message
584        let len_val = compose::<AB::Expr>(&local_cols.len_data.map(|x| x.into()), RV32_CELL_BITS);
585        builder
586            .when(is_last_row.clone())
587            .assert_eq(local_cols.control.len, len_val);
588        // Assert that we started reading from the correct pointer initially
589        let src_val = compose::<AB::Expr>(&local_cols.src_ptr.map(|x| x.into()), RV32_CELL_BITS);
590        builder
591            .when(is_last_row.clone())
592            .assert_eq(local_cols.control.read_ptr, src_val + read_ptr_delta);
593        // Assert that we started reading from the correct timestamp
594        builder.when(is_last_row.clone()).assert_eq(
595            local_cols.control.cur_timestamp,
596            local_cols.from_state.timestamp + AB::Expr::from_canonical_u32(3) + time_delta,
597        );
598    }
599}