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}