sp1_core_machine/syscall/instructions/
air.rs

1use std::borrow::Borrow;
2
3use p3_air::{Air, AirBuilder};
4use p3_field::AbstractField;
5use p3_matrix::Matrix;
6use sp1_core_executor::{
7    events::MemoryAccessPosition, syscalls::SyscallCode, Opcode, Register::X5,
8};
9use sp1_stark::{
10    air::{
11        BaseAirBuilder, InteractionScope, PublicValues, SP1AirBuilder, POSEIDON_NUM_WORDS,
12        PV_DIGEST_NUM_WORDS, SP1_PROOF_NUM_PV_ELTS,
13    },
14    Word,
15};
16
17use crate::{
18    air::{MemoryAirBuilder, WordAirBuilder},
19    memory::MemoryCols,
20    operations::{BabyBearWordRangeChecker, IsZeroOperation},
21};
22
23use super::{columns::SyscallInstrColumns, SyscallInstrsChip};
24
25impl<AB> Air<AB> for SyscallInstrsChip
26where
27    AB: SP1AirBuilder,
28    AB::Var: Sized,
29{
30    #[inline(never)]
31    fn eval(&self, builder: &mut AB) {
32        let main = builder.main();
33        let local = main.row_slice(0);
34        let local: &SyscallInstrColumns<AB::Var> = (*local).borrow();
35
36        let public_values_slice: [AB::PublicVar; SP1_PROOF_NUM_PV_ELTS] =
37            core::array::from_fn(|i| builder.public_values()[i]);
38        let public_values: &PublicValues<Word<AB::PublicVar>, AB::PublicVar> =
39            public_values_slice.as_slice().borrow();
40
41        // SAFETY: Only `ECALL` opcode can be received in this chip.
42        // `is_real` is checked to be boolean, and the `opcode` matches the corresponding opcode.
43        builder.assert_bool(local.is_real);
44
45        // Verify that local.is_halt is correct.
46        self.eval_is_halt_syscall(builder, local);
47
48        // SAFETY: This checks the following.
49        // - `shard`, `clk` are correctly received from the CpuChip
50        // - `op_a_0 = 0` enforced, as `op_a = X5` for all ECALL
51        // - `op_a_immutable = 0`
52        // - `is_memory = 0`
53        // - `is_syscall = 1`
54        // `next_pc`, `num_extra_cycles`, `op_a_val`, `is_halt` need to be constrained. We outline
55        // the checks below. `next_pc` is constrained for the case where `is_halt` is true
56        // to be `0` in `eval_is_halt_unimpl`. `next_pc` is constrained for the case where
57        // `is_halt` is false to be `pc + 4` in `eval`. `num_extra_cycles` is checked to be
58        // equal to the return value of `get_num_extra_ecall_cycles`, in `eval`. `op_a_val`
59        // is constrained in `eval_ecall`. `is_halt` is checked to be correct in
60        // `eval_is_halt_syscall`.
61        builder.receive_instruction(
62            local.shard,
63            local.clk,
64            local.pc,
65            local.next_pc,
66            local.num_extra_cycles,
67            Opcode::ECALL.as_field::<AB::F>(),
68            *local.op_a_access.value(),
69            local.op_b_value,
70            local.op_c_value,
71            AB::Expr::zero(), // op_a is always register 5 for ecall instructions.
72            AB::Expr::zero(),
73            AB::Expr::zero(),
74            AB::Expr::one(),
75            local.is_halt,
76            local.is_real,
77        );
78
79        // If the syscall is not halt, then next_pc should be pc + 4.
80        // `next_pc` is constrained for the case where `is_halt` is false to be `pc + 4`
81        builder
82            .when(local.is_real)
83            .when(AB::Expr::one() - local.is_halt)
84            .assert_eq(local.next_pc, local.pc + AB::Expr::from_canonical_u32(4));
85
86        // `num_extra_cycles` is checked to be equal to the return value of
87        // `get_num_extra_ecall_cycles`
88        builder.assert_eq::<AB::Var, AB::Expr>(
89            local.num_extra_cycles,
90            self.get_num_extra_ecall_cycles::<AB>(local),
91        );
92
93        // Do the memory eval for op_a. For syscall instructions, we need to eval at register X5.
94        builder.eval_memory_access(
95            local.shard,
96            local.clk + AB::F::from_canonical_u32(MemoryAccessPosition::A as u32),
97            AB::Expr::from_canonical_u32(X5 as u32),
98            &local.op_a_access,
99            local.is_real,
100        );
101
102        // ECALL instruction.
103        self.eval_ecall(builder, local);
104
105        // COMMIT/COMMIT_DEFERRED_PROOFS ecall instruction.
106        self.eval_commit(
107            builder,
108            local,
109            public_values.committed_value_digest,
110            public_values.deferred_proofs_digest,
111        );
112
113        // HALT ecall and UNIMPL instruction.
114        self.eval_halt_unimpl(builder, local, public_values);
115    }
116}
117
118impl SyscallInstrsChip {
119    /// Constraints related to the ECALL opcode.
120    ///
121    /// This method will do the following:
122    /// 1. Send the syscall to the precompile table, if needed.
123    /// 2. Check for valid op_a values.
124    pub(crate) fn eval_ecall<AB: SP1AirBuilder>(
125        &self,
126        builder: &mut AB,
127        local: &SyscallInstrColumns<AB::Var>,
128    ) {
129        // The syscall code is the read-in value of op_a at the start of the instruction.
130        let syscall_code = local.op_a_access.prev_value();
131
132        // We interpret the syscall_code as little-endian bytes and interpret each byte as a u8
133        // with different information.
134        let syscall_id = syscall_code[0];
135        let send_to_table = syscall_code[1];
136
137        // SAFETY: Assert that for non real row, the send_to_table value is 0 so that the
138        // `send_syscall` interaction is not activated.
139        builder.when(AB::Expr::one() - local.is_real).assert_zero(send_to_table);
140
141        builder.send_syscall(
142            local.shard,
143            local.clk,
144            syscall_id,
145            local.op_b_value.reduce::<AB>(),
146            local.op_c_value.reduce::<AB>(),
147            send_to_table,
148            InteractionScope::Local,
149        );
150
151        // Compute whether this ecall is ENTER_UNCONSTRAINED.
152        let is_enter_unconstrained = {
153            IsZeroOperation::<AB::F>::eval(
154                builder,
155                syscall_id -
156                    AB::Expr::from_canonical_u32(SyscallCode::ENTER_UNCONSTRAINED.syscall_id()),
157                local.is_enter_unconstrained,
158                local.is_real.into(),
159            );
160            local.is_enter_unconstrained.result
161        };
162
163        // Compute whether this ecall is HINT_LEN.
164        let is_hint_len = {
165            IsZeroOperation::<AB::F>::eval(
166                builder,
167                syscall_id - AB::Expr::from_canonical_u32(SyscallCode::HINT_LEN.syscall_id()),
168                local.is_hint_len,
169                local.is_real.into(),
170            );
171            local.is_hint_len.result
172        };
173
174        // `op_a_val` is constrained.
175
176        // When syscall_id is ENTER_UNCONSTRAINED, the new value of op_a should be 0.
177        let zero_word = Word::<AB::F>::from(0);
178        builder
179            .when(local.is_real)
180            .when(is_enter_unconstrained)
181            .assert_word_eq(*local.op_a_access.value(), zero_word);
182
183        // When the syscall is not one of ENTER_UNCONSTRAINED or HINT_LEN, op_a shouldn't change.
184        builder
185            .when(local.is_real)
186            .when_not(is_enter_unconstrained + is_hint_len)
187            .assert_word_eq(*local.op_a_access.value(), *local.op_a_access.prev_value());
188
189        // SAFETY: This leaves the case where syscall is `HINT_LEN`.
190        // In this case, `op_a`'s value can be arbitrary, but it still must be a valid word if
191        // `is_real = 1`. This is due to `op_a_val` being connected to the CpuChip.
192        // In the CpuChip, `op_a_val` is constrained to be a valid word via `eval_registers`.
193        // As this is a syscall for HINT, the value itself being arbitrary is fine, as long as it is
194        // a valid word.
195
196        // Verify value of ecall_range_check_operand column.
197        // SAFETY: If `is_real = 0`, then `ecall_range_check_operand = 0`.
198        // If `is_real = 1`, then `is_halt_check` and `is_commit_deferred_proofs` are constrained.
199        // The two results will both be boolean due to `IsZeroOperation`, and both cannot be `1` at
200        // the same time. Both of them being `1` will require `syscall_id` being `HALT` and
201        // `COMMIT_DEFERRED_PROOFS` at the same time. This implies that if `is_real = 1`,
202        // `ecall_range_check_operand` will be correct, and boolean.
203        builder.assert_eq(
204            local.ecall_range_check_operand,
205            local.is_real * (local.is_halt_check.result + local.is_commit_deferred_proofs.result),
206        );
207
208        // Babybear range check the operand_to_check word.
209        // SAFETY: `ecall_range_check_operand` is boolean, and no interactions can be made in
210        // padding rows. `operand_to_check` is already known to be a valid word, as it is
211        // either
212        // - `op_b_val` in the case of `HALT`
213        // - `op_c_val` in the case of `COMMIT_DEFERRED_PROOFS`
214        BabyBearWordRangeChecker::<AB::F>::range_check::<AB>(
215            builder,
216            local.operand_to_check,
217            local.operand_range_check_cols,
218            local.ecall_range_check_operand.into(),
219        );
220    }
221
222    /// Constraints related to the COMMIT and COMMIT_DEFERRED_PROOFS instructions.
223    pub(crate) fn eval_commit<AB: SP1AirBuilder>(
224        &self,
225        builder: &mut AB,
226        local: &SyscallInstrColumns<AB::Var>,
227        commit_digest: [Word<AB::PublicVar>; PV_DIGEST_NUM_WORDS],
228        deferred_proofs_digest: [AB::PublicVar; POSEIDON_NUM_WORDS],
229    ) {
230        let (is_commit, is_commit_deferred_proofs) =
231            self.get_is_commit_related_syscall(builder, local);
232
233        // Verify the index bitmap.
234        let mut bitmap_sum = AB::Expr::zero();
235        // They should all be bools.
236        for bit in local.index_bitmap.iter() {
237            builder.when(local.is_real).assert_bool(*bit);
238            bitmap_sum = bitmap_sum.clone() + (*bit).into();
239        }
240        // When the syscall is COMMIT or COMMIT_DEFERRED_PROOFS, there should be one set bit.
241        builder
242            .when(local.is_real)
243            .when(is_commit.clone() + is_commit_deferred_proofs.clone())
244            .assert_one(bitmap_sum.clone());
245        // When it's some other syscall, there should be no set bits.
246        builder
247            .when(local.is_real)
248            .when(AB::Expr::one() - (is_commit.clone() + is_commit_deferred_proofs.clone()))
249            .assert_zero(bitmap_sum);
250
251        // Verify that word_idx corresponds to the set bit in index bitmap.
252        for (i, bit) in local.index_bitmap.iter().enumerate() {
253            builder
254                .when(local.is_real)
255                .when(*bit)
256                .assert_eq(local.op_b_value[0], AB::Expr::from_canonical_u32(i as u32));
257        }
258        // Verify that the 3 upper bytes of the word_idx are 0.
259        for i in 0..3 {
260            builder
261                .when(local.is_real)
262                .when(is_commit.clone() + is_commit_deferred_proofs.clone())
263                .assert_zero(local.op_b_value[i + 1]);
264        }
265
266        // Retrieve the expected public values digest word to check against the one passed into the
267        // commit ecall. Note that for the interaction builder, it will not have any digest words,
268        // since it's used during AIR compilation time to parse for all send/receives. Since
269        // that interaction builder will ignore the other constraints of the air, it is safe
270        // to not include the verification check of the expected public values digest word.
271        let expected_pv_digest_word = builder.index_word_array(&commit_digest, &local.index_bitmap);
272
273        let digest_word = local.op_c_value;
274
275        // Verify the public_values_digest_word.
276        builder
277            .when(local.is_real)
278            .when(is_commit.clone())
279            .assert_word_eq(expected_pv_digest_word, digest_word);
280
281        let expected_deferred_proofs_digest_element =
282            builder.index_array(&deferred_proofs_digest, &local.index_bitmap);
283
284        // Verify that the operand that was range checked is digest_word.
285        builder
286            .when(local.is_real)
287            .when(is_commit_deferred_proofs.clone())
288            .assert_word_eq(digest_word, local.operand_to_check);
289
290        builder
291            .when(local.is_real)
292            .when(is_commit_deferred_proofs.clone())
293            .assert_eq(expected_deferred_proofs_digest_element, digest_word.reduce::<AB>());
294    }
295
296    /// Constraint related to the halt and unimpl instruction.
297    pub(crate) fn eval_halt_unimpl<AB: SP1AirBuilder>(
298        &self,
299        builder: &mut AB,
300        local: &SyscallInstrColumns<AB::Var>,
301        public_values: &PublicValues<Word<AB::PublicVar>, AB::PublicVar>,
302    ) {
303        // `next_pc` is constrained for the case where `is_halt` is true to be `0`
304        builder.when(local.is_halt).assert_zero(local.next_pc);
305
306        // Verify that the operand that was range checked is op_b.
307        builder.when(local.is_halt).assert_word_eq(local.op_b_value, local.operand_to_check);
308
309        // Check that the `op_b_value` reduced is the `public_values.exit_code`.
310        builder
311            .when(local.is_halt)
312            .assert_eq(local.op_b_value.reduce::<AB>(), public_values.exit_code);
313    }
314
315    /// Returns a boolean expression indicating whether the instruction is a HALT instruction.
316    pub(crate) fn eval_is_halt_syscall<AB: SP1AirBuilder>(
317        &self,
318        builder: &mut AB,
319        local: &SyscallInstrColumns<AB::Var>,
320    ) {
321        // `is_halt` is checked to be correct in `eval_is_halt_syscall`.
322
323        // The syscall code is the read-in value of op_a at the start of the instruction.
324        let syscall_code = local.op_a_access.prev_value();
325
326        let syscall_id = syscall_code[0];
327
328        // Compute whether this ecall is HALT.
329        let is_halt = {
330            IsZeroOperation::<AB::F>::eval(
331                builder,
332                syscall_id - AB::Expr::from_canonical_u32(SyscallCode::HALT.syscall_id()),
333                local.is_halt_check,
334                local.is_real.into(),
335            );
336            local.is_halt_check.result
337        };
338
339        // Verify that the is_halt flag is correct.
340        // If `is_real = 0`, then `local.is_halt = 0`.
341        // If `is_real = 1`, then `is_halt_check.result` will be correct, so `local.is_halt` is
342        // correct.
343        builder.assert_eq(local.is_halt, is_halt * local.is_real);
344    }
345
346    /// Returns two boolean expression indicating whether the instruction is a COMMIT or
347    /// COMMIT_DEFERRED_PROOFS instruction.
348    pub(crate) fn get_is_commit_related_syscall<AB: SP1AirBuilder>(
349        &self,
350        builder: &mut AB,
351        local: &SyscallInstrColumns<AB::Var>,
352    ) -> (AB::Expr, AB::Expr) {
353        // The syscall code is the read-in value of op_a at the start of the instruction.
354        let syscall_code = local.op_a_access.prev_value();
355
356        let syscall_id = syscall_code[0];
357
358        // Compute whether this ecall is COMMIT.
359        let is_commit = {
360            IsZeroOperation::<AB::F>::eval(
361                builder,
362                syscall_id - AB::Expr::from_canonical_u32(SyscallCode::COMMIT.syscall_id()),
363                local.is_commit,
364                local.is_real.into(),
365            );
366            local.is_commit.result
367        };
368
369        // Compute whether this ecall is COMMIT_DEFERRED_PROOFS.
370        let is_commit_deferred_proofs = {
371            IsZeroOperation::<AB::F>::eval(
372                builder,
373                syscall_id -
374                    AB::Expr::from_canonical_u32(
375                        SyscallCode::COMMIT_DEFERRED_PROOFS.syscall_id(),
376                    ),
377                local.is_commit_deferred_proofs,
378                local.is_real.into(),
379            );
380            local.is_commit_deferred_proofs.result
381        };
382
383        (is_commit.into(), is_commit_deferred_proofs.into())
384    }
385
386    /// Returns the number of extra cycles from an ECALL instruction.
387    pub(crate) fn get_num_extra_ecall_cycles<AB: SP1AirBuilder>(
388        &self,
389        local: &SyscallInstrColumns<AB::Var>,
390    ) -> AB::Expr {
391        // The syscall code is the read-in value of op_a at the start of the instruction.
392        let syscall_code = local.op_a_access.prev_value();
393
394        let num_extra_cycles = syscall_code[2];
395
396        // If `is_real = 0`, then the return value is `0` regardless of `num_extra_cycles`.
397        // If `is_real = 1`, then the `op_a_access` will be done, and `num_extra_cycles` will be
398        // correct.
399        num_extra_cycles * local.is_real
400    }
401}