Skip to main content

sp1_core_machine/operations/
page.rs

1use slop_air::AirBuilder;
2use slop_algebra::{AbstractField, Field, PrimeField32};
3use sp1_core_executor::{
4    events::{ByteLookupEvent, ByteRecord, PageProtRecord},
5    ByteOpcode,
6};
7use sp1_derive::AlignedBorrow;
8use sp1_hypercube::air::{BaseAirBuilder, SP1AirBuilder};
9use sp1_primitives::consts::{
10    split_page_idx, PAGE_SIZE, PROT_FAILURE_READ, PROT_FAILURE_WRITE, PROT_READ, PROT_WRITE,
11};
12use struct_reflection::{StructReflection, StructReflectionHelper};
13
14use crate::{air::MemoryAirBuilder, memory::PageProtAccessCols};
15
16/// A set of columns needed to compute the page_idx from an address.
17#[derive(AlignedBorrow, Default, Debug, Clone, Copy, StructReflection)]
18#[repr(C)]
19pub struct PageOperation<T> {
20    /// Split that least significant limb into a 4 bit limb and a 12 bit limb.
21    pub addr_4_bits: T,
22    pub addr_12_bits: T,
23}
24
25impl<F: Field> PageOperation<F> {
26    pub fn populate(&mut self, record: &mut impl ByteRecord, addr: u64) {
27        let addr_12_bits: u16 = (addr & 0xFFF).try_into().unwrap();
28        let addr_4_bits: u16 = ((addr >> 12) & 0xF).try_into().unwrap();
29
30        self.addr_12_bits = F::from_canonical_u16(addr_12_bits);
31        self.addr_4_bits = F::from_canonical_u16(addr_4_bits);
32
33        record.add_bit_range_check(addr_12_bits, 12);
34        record.add_bit_range_check(addr_4_bits, 4);
35    }
36
37    /// Evaluate the calculation of the page idx from the address.
38    pub fn eval<AB: SP1AirBuilder>(
39        builder: &mut AB,
40        addr: &[AB::Expr; 3],
41        cols: PageOperation<AB::Var>,
42        is_real: AB::Expr,
43    ) -> [AB::Expr; 3] {
44        builder.assert_bool(is_real.clone());
45
46        // Check that the least significant address limb is correctly decomposed to the 4 bit limb
47        // and the 12 bit limb.
48        builder.when(is_real.clone()).assert_eq(
49            addr[0].clone(),
50            cols.addr_12_bits + cols.addr_4_bits * (AB::Expr::from_canonical_u32(1 << 12)),
51        );
52        // Range check the limbs.
53        builder.send_byte(
54            AB::Expr::from_canonical_u32(ByteOpcode::Range as u32),
55            cols.addr_4_bits.into(),
56            AB::Expr::from_canonical_u32(4),
57            AB::Expr::zero(),
58            is_real.clone(),
59        );
60        builder.send_byte(
61            AB::Expr::from_canonical_u32(ByteOpcode::Range as u32),
62            cols.addr_12_bits.into(),
63            AB::Expr::from_canonical_u32(12),
64            AB::Expr::zero(),
65            is_real.clone(),
66        );
67
68        [cols.addr_4_bits.into(), addr[1].clone(), addr[2].clone()]
69    }
70}
71
72/// A set of columns needed to retrieve the page permissions from an address.
73#[derive(AlignedBorrow, Default, Debug, Clone, Copy, StructReflection)]
74#[repr(C)]
75pub struct PageProtOperation<T> {
76    /// The page operation to calculate page idx from address.
77    pub page_op: PageOperation<T>,
78
79    /// The page prot access columns.
80    pub page_prot_access: PageProtAccessCols<T>,
81}
82
83impl<F: PrimeField32> PageProtOperation<F> {
84    pub fn populate(
85        &mut self,
86        record: &mut impl ByteRecord,
87        addr: u64,
88        clk: u64,
89        previous_page_prot_access: &PageProtRecord,
90    ) {
91        self.page_op.populate(record, addr);
92
93        assert!(previous_page_prot_access.timestamp < clk);
94        self.page_prot_access.populate(previous_page_prot_access, clk, record);
95    }
96}
97
98impl<F: Field> PageProtOperation<F> {
99    pub fn eval<AB: SP1AirBuilder>(
100        builder: &mut AB,
101        clk_high: AB::Expr,
102        clk_low: AB::Expr,
103        addr: &[AB::Expr; 3],
104        cols: PageProtOperation<AB::Var>,
105        is_real: AB::Expr,
106    ) {
107        builder.assert_bool(is_real.clone());
108
109        let page_idx = PageOperation::<AB::F>::eval(builder, addr, cols.page_op, is_real.clone());
110
111        builder.eval_page_prot_access_read(
112            clk_high,
113            clk_low,
114            &page_idx,
115            cols.page_prot_access,
116            is_real.clone(),
117        );
118    }
119}
120
121/// A set of columns needed to check if two page indices are equal or adjacent.
122#[derive(AlignedBorrow, Default, Debug, Clone, Copy, StructReflection)]
123#[repr(C)]
124pub struct PageIsEqualOrAdjacentOperation<T> {
125    pub is_overflow: T,
126
127    // Bool flag that is set to 0 if equal, 1 if adjacent.
128    pub is_adjacent: T,
129}
130
131impl<F: Field> PageIsEqualOrAdjacentOperation<F> {
132    pub fn populate(&mut self, curr_page_idx: u64, next_page_idx: u64) {
133        if curr_page_idx == next_page_idx {
134            self.is_adjacent = F::zero();
135        } else if curr_page_idx + 1 == next_page_idx {
136            self.is_adjacent = F::one();
137        } else {
138            panic!("curr_page_idx and next_page_idx are not equal or adjacent");
139        }
140
141        // Check that the bottom 20 bits of the next page are 0, if so we know there's an overflow
142        // into the third limb
143        let next_page_limbs = split_page_idx(next_page_idx);
144        let next_page_20_bits = next_page_limbs[0] as u64 + ((next_page_limbs[1] as u64) << 4);
145
146        // Check for overflow.
147        self.is_overflow = F::from_bool(next_page_20_bits == 0);
148    }
149
150    pub fn eval<AB: SP1AirBuilder>(
151        builder: &mut AB,
152        curr_page_idx: [AB::Expr; 3],
153        next_page_idx: [AB::Expr; 3],
154        cols: PageIsEqualOrAdjacentOperation<AB::Var>,
155        is_real: AB::Expr,
156    ) {
157        builder.assert_bool(is_real.clone());
158        builder.assert_bool(cols.is_adjacent);
159        builder.assert_bool(cols.is_overflow);
160
161        // Combine the 1st and 2nd limbs.  The 1st limb is 4 bits and the 2nd limb is 16 bits.
162        let curr_page_20_bits = curr_page_idx[0].clone()
163            + curr_page_idx[1].clone() * (AB::Expr::from_canonical_u32(1 << 4));
164        let next_page_20_bits = next_page_idx[0].clone()
165            + next_page_idx[1].clone() * (AB::Expr::from_canonical_u32(1 << 4));
166
167        // First check for the case when they are equal.
168        builder
169            .when(is_real.clone())
170            .when_not(cols.is_adjacent)
171            .assert_eq(curr_page_20_bits.clone(), next_page_20_bits.clone());
172        builder
173            .when(is_real.clone())
174            .when_not(cols.is_adjacent)
175            .assert_eq(curr_page_idx[2].clone(), next_page_idx[2].clone());
176
177        // Now check if they are adjacent.
178
179        // First check to see is_adjacent == 1, then is_real == 1.
180        // This is so that we don't need to check for is_real when is_adjacent == 1.
181        builder.when(cols.is_adjacent).assert_one(is_real.clone());
182
183        let mut is_adjacent_builder = builder.when(cols.is_adjacent);
184
185        // Find out what each limb's relationship should be.
186        // If !is_overflow -> (20bit limbs are adjacent, 3rd limb is equal).
187        // if is_overflow -> (20bit limbs are at boundary, 3rd limb is adjacent).
188
189        // Check that first page bottom 20 bits are adjacent to second page bottom 20 bits
190        is_adjacent_builder
191            .when_not(cols.is_overflow)
192            .assert_eq(curr_page_20_bits.clone() + AB::Expr::one(), next_page_20_bits.clone());
193
194        // Check that top limbs are equal
195        is_adjacent_builder
196            .when_not(cols.is_overflow)
197            .assert_eq(curr_page_idx[2].clone(), next_page_idx[2].clone());
198
199        // Check that first page bottom 20 bits are maxed out
200        is_adjacent_builder
201            .when(cols.is_overflow)
202            .assert_eq(curr_page_20_bits, AB::Expr::from_canonical_u32((1 << 20) - 1));
203
204        // Check that second page bottom 20 bits are 0
205        is_adjacent_builder.when(cols.is_overflow).assert_eq(next_page_20_bits, AB::Expr::zero());
206
207        // Check that top limb (top 16 bits) of second page is 1 more than top limb of first page
208        is_adjacent_builder
209            .when(cols.is_overflow)
210            .assert_eq(curr_page_idx[2].clone() + AB::Expr::one(), next_page_idx[2].clone());
211    }
212}
213
214/// A set of columns needed to check the page prot permissions and return the trap code.
215#[derive(AlignedBorrow, Default, Debug, Clone, Copy, StructReflection)]
216#[repr(C)]
217pub struct TrapPageProtOperation<T> {
218    pub page_prot_access: PageProtAccessCols<T>,
219    pub is_read_fail: T,
220    pub is_write_fail: T,
221    pub is_now_trap: T,
222}
223
224#[allow(clippy::too_many_arguments)]
225impl<F: PrimeField32> TrapPageProtOperation<F> {
226    #[allow(clippy::too_many_arguments)]
227    pub fn populate(
228        &mut self,
229        record: &mut impl ByteRecord,
230        clk: u64,
231        permissions: u8,
232        page_prot_access: PageProtRecord,
233        is_not_trap: &mut bool,
234        trap_code: &mut u8,
235    ) {
236        assert!(*is_not_trap);
237        self.page_prot_access.populate(&page_prot_access, clk, record);
238        let perm = page_prot_access.page_prot;
239        if permissions == PROT_READ {
240            self.is_read_fail = F::from_bool((perm & PROT_READ) == 0);
241            self.is_write_fail = F::zero();
242            self.is_now_trap = self.is_read_fail;
243            if self.is_now_trap == F::one() {
244                *trap_code = PROT_FAILURE_READ as u8;
245            }
246        }
247        if permissions == PROT_WRITE {
248            self.is_read_fail = F::zero();
249            self.is_write_fail = F::from_bool((perm & PROT_WRITE) == 0);
250            self.is_now_trap = self.is_write_fail;
251            if self.is_now_trap == F::one() {
252                *trap_code = PROT_FAILURE_WRITE as u8;
253            }
254        }
255        if permissions == (PROT_READ | PROT_WRITE) {
256            self.is_read_fail = F::from_bool((perm & PROT_READ) == 0);
257            self.is_write_fail = F::from_bool((perm & PROT_WRITE) == 0);
258            self.is_now_trap = F::from_bool((perm & permissions) != permissions);
259            if self.is_read_fail == F::one() {
260                *trap_code = PROT_FAILURE_READ as u8;
261            } else if self.is_write_fail == F::one() {
262                *trap_code = PROT_FAILURE_WRITE as u8;
263            }
264        }
265        record.add_byte_lookup_event(ByteLookupEvent {
266            opcode: ByteOpcode::AND,
267            a: (perm & permissions) as u16,
268            b: perm,
269            c: permissions,
270        });
271        if self.is_now_trap == F::one() {
272            *is_not_trap = false;
273        }
274    }
275}
276
277impl<F: Field> TrapPageProtOperation<F> {
278    #[allow(clippy::too_many_arguments)]
279    pub fn eval<AB: SP1AirBuilder>(
280        builder: &mut AB,
281        clk_high: AB::Expr,
282        clk_low: AB::Expr,
283        page_idx: &[AB::Expr; 3],
284        permissions: u8,
285        cols: &TrapPageProtOperation<AB::Var>,
286        is_real: AB::Expr,
287        is_not_trap: &mut AB::Expr,
288        trap_code: &mut AB::Expr,
289    ) {
290        builder.assert_bool(is_real.clone());
291        builder.when(is_real.clone()).assert_one(is_not_trap.clone());
292        builder.eval_page_prot_access_read(
293            clk_high.clone(),
294            clk_low.clone(),
295            page_idx,
296            cols.page_prot_access,
297            is_real.clone(),
298        );
299
300        let perm = cols.page_prot_access.prev_prot_bitmap;
301
302        let mut permission_and = AB::Expr::zero();
303
304        if permissions == PROT_READ {
305            permission_and =
306                (AB::Expr::one() - cols.is_read_fail) * AB::Expr::from_canonical_u8(PROT_READ);
307            builder.assert_zero(cols.is_write_fail);
308        }
309        if permissions == PROT_WRITE {
310            permission_and =
311                (AB::Expr::one() - cols.is_write_fail) * AB::Expr::from_canonical_u8(PROT_WRITE);
312            builder.assert_zero(cols.is_read_fail);
313        }
314        if permissions == (PROT_READ | PROT_WRITE) {
315            permission_and = permission_and.clone()
316                + (AB::Expr::one() - cols.is_read_fail) * AB::Expr::from_canonical_u8(PROT_READ);
317            permission_and = permission_and.clone()
318                + (AB::Expr::one() - cols.is_write_fail) * AB::Expr::from_canonical_u8(PROT_WRITE);
319        }
320
321        builder.send_byte(
322            AB::Expr::from_canonical_u8(ByteOpcode::AND as u8),
323            permission_and.clone(),
324            perm.into(),
325            AB::Expr::from_canonical_u8(permissions),
326            is_real.clone(),
327        );
328
329        builder.when_not(is_real.clone()).assert_zero(cols.is_now_trap);
330        builder.when_not(is_not_trap.clone()).assert_zero(cols.is_now_trap);
331        builder.assert_bool(cols.is_now_trap);
332        builder.assert_bool(cols.is_read_fail);
333        builder.assert_bool(cols.is_write_fail);
334        builder.assert_eq(
335            cols.is_now_trap,
336            cols.is_read_fail + cols.is_write_fail - cols.is_read_fail * cols.is_write_fail,
337        );
338
339        *is_not_trap = is_not_trap.clone() - cols.is_now_trap.into();
340        *trap_code = trap_code.clone()
341            + cols.is_read_fail * AB::Expr::from_canonical_u64(PROT_FAILURE_READ)
342            + (cols.is_now_trap - cols.is_read_fail)
343                * AB::Expr::from_canonical_u64(PROT_FAILURE_WRITE);
344    }
345}
346
347/// A set of columns needed to check the page prot permissions for a range of addrs.
348/// This operation only supports an addr range that spans at most 2 pages.
349#[derive(AlignedBorrow, Default, Debug, Clone, Copy, StructReflection)]
350#[repr(C)]
351pub struct AddressSlicePageProtOperation<T> {
352    pub page_is_equal_or_adjacent: PageIsEqualOrAdjacentOperation<T>,
353    pub page_operations: [PageOperation<T>; 2],
354    pub trap_page_operations: [TrapPageProtOperation<T>; 2],
355}
356
357#[allow(clippy::too_many_arguments)]
358impl<F: PrimeField32> AddressSlicePageProtOperation<F> {
359    #[allow(clippy::too_many_arguments)]
360    pub fn populate(
361        &mut self,
362        record: &mut impl ByteRecord,
363        start_addr: u64,
364        end_addr: u64,
365        clk: u64,
366        permissions: u8,
367        page_prot_access: &[PageProtRecord],
368        is_not_trap: &mut bool,
369        trap_code: &mut u8,
370    ) {
371        if !(*is_not_trap) {
372            assert_eq!(page_prot_access.len(), 0);
373            *self = AddressSlicePageProtOperation::<F>::default();
374            return;
375        }
376
377        let start_page_idx = start_addr / (PAGE_SIZE as u64);
378        let end_page_idx = end_addr / (PAGE_SIZE as u64);
379        assert!(start_page_idx == end_page_idx || start_page_idx + 1 == end_page_idx);
380
381        self.page_operations[0].populate(record, start_addr);
382        self.page_operations[1].populate(record, end_addr);
383
384        assert!(!page_prot_access.is_empty());
385
386        // Populate the first slice.
387        self.trap_page_operations[0].populate(
388            record,
389            clk,
390            permissions,
391            page_prot_access[0],
392            is_not_trap,
393            trap_code,
394        );
395
396        if !(*is_not_trap) {
397            self.trap_page_operations[1] = TrapPageProtOperation::<F>::default();
398            self.page_is_equal_or_adjacent = PageIsEqualOrAdjacentOperation::<F>::default();
399            return;
400        }
401
402        self.page_is_equal_or_adjacent.populate(start_page_idx, end_page_idx);
403
404        if end_page_idx == start_page_idx + 1 {
405            assert!(page_prot_access.len() == 2);
406            self.trap_page_operations[1].populate(
407                record,
408                clk,
409                permissions,
410                page_prot_access[1],
411                is_not_trap,
412                trap_code,
413            );
414        } else {
415            self.trap_page_operations[1] = TrapPageProtOperation::<F>::default();
416        }
417    }
418}
419
420impl<F: Field> AddressSlicePageProtOperation<F> {
421    #[allow(clippy::too_many_arguments)]
422    pub fn eval<AB: SP1AirBuilder>(
423        builder: &mut AB,
424        clk_high: AB::Expr,
425        clk_low: AB::Expr,
426        start_addr: &[AB::Expr; 3],
427        end_addr: &[AB::Expr; 3],
428        permissions: u8,
429        cols: &AddressSlicePageProtOperation<AB::Var>,
430        is_not_trap: &mut AB::Expr,
431        trap_code: &mut AB::Expr,
432    ) {
433        builder.assert_bool(is_not_trap.clone());
434
435        let start_page_idx = PageOperation::<AB::F>::eval(
436            builder,
437            start_addr,
438            cols.page_operations[0],
439            is_not_trap.clone(),
440        );
441
442        let end_page_idx = PageOperation::<AB::F>::eval(
443            builder,
444            &end_addr.clone(),
445            cols.page_operations[1],
446            is_not_trap.clone(),
447        );
448
449        TrapPageProtOperation::<AB::F>::eval(
450            builder,
451            clk_high.clone(),
452            clk_low.clone(),
453            &start_page_idx.clone(),
454            permissions,
455            &cols.trap_page_operations[0],
456            is_not_trap.clone(),
457            is_not_trap,
458            trap_code,
459        );
460
461        PageIsEqualOrAdjacentOperation::<AB::F>::eval(
462            builder,
463            start_page_idx.map(Into::into),
464            end_page_idx.clone().map(Into::into),
465            cols.page_is_equal_or_adjacent,
466            is_not_trap.clone(),
467        );
468
469        TrapPageProtOperation::<AB::F>::eval(
470            builder,
471            clk_high.clone(),
472            clk_low.clone(),
473            &end_page_idx.clone(),
474            permissions,
475            &cols.trap_page_operations[1],
476            cols.page_is_equal_or_adjacent.is_adjacent.into(),
477            is_not_trap,
478            trap_code,
479        );
480    }
481}