openvm_ecc_circuit/weierstrass_chip/
preflight.rs

1//! Fast PreflightExecutor implementations for ECC operations that use native field arithmetic
2//! instead of BigUint-based computation.
3
4use openvm_algebra_circuit::fields::FieldType;
5use openvm_circuit::{
6    arch::{AdapterTraceExecutor, ExecutionError, PreflightExecutor, RecordArena, VmStateMut},
7    system::memory::online::TracingMemory,
8};
9use openvm_ecc_transpiler::Rv32WeierstrassOpcode;
10use openvm_instructions::{instruction::Instruction, program::DEFAULT_PC_STEP};
11use openvm_mod_circuit_builder::{
12    run_field_expression_precomputed, FieldExpressionCoreRecordMut, FieldExpressionRecordLayout,
13};
14use openvm_rv32_adapters::Rv32VecHeapAdapterExecutor;
15use openvm_stark_backend::p3_field::PrimeField32;
16use strum::EnumCount;
17
18use super::{
19    add_ne::EcAddNeExecutor,
20    curves::{ec_add_ne, ec_double, CurveType},
21    double::EcDoubleExecutor,
22};
23
24/// Generates a match statement dispatching an enum to const-generic function calls.
25macro_rules! dispatch_enum {
26    ($fn:ident, $val:expr, $read_data:expr,
27     [$(($variant_type:ident :: $variant:ident)),* $(,)?]) => {
28        match $val {
29            $(
30                $variant_type::$variant => $fn::<
31                    { $variant_type::$variant as u8 },
32                    BLOCKS,
33                    BLOCK_SIZE,
34                >($read_data),
35            )*
36            #[allow(unreachable_patterns)]
37            _ => unreachable!(),
38        }
39    };
40}
41
42/// Compute EC point addition using fast native field arithmetic.
43#[inline]
44fn compute_ec_add_ne_fast<const BLOCKS: usize, const BLOCK_SIZE: usize>(
45    field_type: Option<FieldType>,
46    read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2],
47) -> Option<[[u8; BLOCK_SIZE]; BLOCKS]> {
48    let field_type = field_type?;
49
50    Some(match field_type {
51        FieldType::K256Coordinate
52        | FieldType::P256Coordinate
53        | FieldType::BN254Coordinate
54        | FieldType::BLS12_381Coordinate => {
55            dispatch_enum!(
56                ec_add_ne,
57                field_type,
58                read_data,
59                [
60                    (FieldType::K256Coordinate),
61                    (FieldType::P256Coordinate),
62                    (FieldType::BN254Coordinate),
63                    (FieldType::BLS12_381Coordinate),
64                ]
65            )
66        }
67        // Scalar fields are not used for ECC point coordinates
68        _ => return None,
69    })
70}
71
72/// Compute EC point doubling using fast native field arithmetic.
73#[inline]
74fn compute_ec_double_fast<const BLOCKS: usize, const BLOCK_SIZE: usize>(
75    curve_type: Option<CurveType>,
76    read_data: [[u8; BLOCK_SIZE]; BLOCKS],
77) -> Option<[[u8; BLOCK_SIZE]; BLOCKS]> {
78    let curve_type = curve_type?;
79
80    Some(dispatch_enum!(
81        ec_double,
82        curve_type,
83        read_data,
84        [
85            (CurveType::K256),
86            (CurveType::P256),
87            (CurveType::BN254),
88            (CurveType::BLS12_381),
89        ]
90    ))
91}
92
93/// Check if this is a SETUP opcode (not a regular EC operation)
94#[inline]
95fn is_setup_opcode(local_opcode: usize) -> bool {
96    let base_opcode = local_opcode % Rv32WeierstrassOpcode::COUNT;
97    base_opcode == Rv32WeierstrassOpcode::SETUP_EC_ADD_NE as usize
98        || base_opcode == Rv32WeierstrassOpcode::SETUP_EC_DOUBLE as usize
99}
100
101/// Slow-path fallback for EC operations (used for SETUP opcodes and unknown field types).
102#[inline]
103fn compute_ec_slow<const BLOCKS: usize, const BLOCK_SIZE: usize>(
104    expr: &openvm_mod_circuit_builder::FieldExpr,
105    local_opcode: usize,
106    read_bytes: &[u8],
107) -> [[u8; BLOCK_SIZE]; BLOCKS] {
108    let flag_idx = if is_setup_opcode(local_opcode) {
109        // SETUP operations: pass num_flags so no flag is set
110        expr.num_flags()
111    } else {
112        // Single operation chip, flag_idx = 0
113        0
114    };
115    run_field_expression_precomputed::<true>(expr, flag_idx, read_bytes).into()
116}
117
118// Implementation for EcAddNeExecutor
119impl<F, RA, const BLOCKS: usize, const BLOCK_SIZE: usize> PreflightExecutor<F, RA>
120    for EcAddNeExecutor<BLOCKS, BLOCK_SIZE>
121where
122    F: PrimeField32,
123    for<'buf> RA: RecordArena<
124        'buf,
125        FieldExpressionRecordLayout<
126            F,
127            Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE>,
128        >,
129        (
130            <Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as AdapterTraceExecutor<F>>::RecordMut<'buf>,
131            FieldExpressionCoreRecordMut<'buf>,
132        ),
133    >,
134{
135    fn execute(
136        &self,
137        state: VmStateMut<F, TracingMemory, RA>,
138        instruction: &Instruction<F>,
139    ) -> Result<(), ExecutionError> {
140        let (mut adapter_record, mut core_record) =
141            state.ctx.alloc(self.inner.get_record_layout());
142
143        <Rv32VecHeapAdapterExecutor<2, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as AdapterTraceExecutor<F>>::start(
144            *state.pc,
145            state.memory,
146            &mut adapter_record,
147        );
148
149        let read_data: [[[u8; BLOCK_SIZE]; BLOCKS]; 2] = self
150            .inner
151            .adapter()
152            .read(state.memory, instruction, &mut adapter_record);
153
154        let local_opcode = instruction.opcode.local_opcode_idx(self.inner.offset);
155
156        core_record.fill_from_execution_data(
157            local_opcode as u8,
158            read_data.as_flattened().as_flattened(),
159        );
160
161        // Try fast path for non-SETUP operations with known field types
162        let output: [[u8; BLOCK_SIZE]; BLOCKS] =
163            if !is_setup_opcode(local_opcode) {
164                compute_ec_add_ne_fast::<BLOCKS, BLOCK_SIZE>(self.cached_field_type, read_data)
165                    .unwrap_or_else(|| {
166                        compute_ec_slow::<BLOCKS, BLOCK_SIZE>(
167                            &self.inner.expr,
168                            local_opcode,
169                            read_data.as_flattened().as_flattened(),
170                        )
171                    })
172            } else {
173                compute_ec_slow::<BLOCKS, BLOCK_SIZE>(
174                    &self.inner.expr,
175                    local_opcode,
176                    read_data.as_flattened().as_flattened(),
177                )
178            };
179
180        self.inner.adapter().write(
181            state.memory,
182            instruction,
183            output,
184            &mut adapter_record,
185        );
186
187        *state.pc = state.pc.wrapping_add(DEFAULT_PC_STEP);
188        Ok(())
189    }
190
191    fn get_opcode_name(&self, _opcode: usize) -> String {
192        self.inner.name.clone()
193    }
194}
195
196// Implementation for EcDoubleExecutor
197impl<F, RA, const BLOCKS: usize, const BLOCK_SIZE: usize> PreflightExecutor<F, RA>
198    for EcDoubleExecutor<BLOCKS, BLOCK_SIZE>
199where
200    F: PrimeField32,
201    for<'buf> RA: RecordArena<
202        'buf,
203        FieldExpressionRecordLayout<
204            F,
205            Rv32VecHeapAdapterExecutor<1, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE>,
206        >,
207        (
208            <Rv32VecHeapAdapterExecutor<1, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as AdapterTraceExecutor<F>>::RecordMut<'buf>,
209            FieldExpressionCoreRecordMut<'buf>,
210        ),
211    >,
212{
213    fn execute(
214        &self,
215        state: VmStateMut<F, TracingMemory, RA>,
216        instruction: &Instruction<F>,
217    ) -> Result<(), ExecutionError> {
218        let (mut adapter_record, mut core_record) =
219            state.ctx.alloc(self.inner.get_record_layout());
220
221        <Rv32VecHeapAdapterExecutor<1, BLOCKS, BLOCKS, BLOCK_SIZE, BLOCK_SIZE> as AdapterTraceExecutor<F>>::start(
222            *state.pc,
223            state.memory,
224            &mut adapter_record,
225        );
226
227        let read_data_arr: [[[u8; BLOCK_SIZE]; BLOCKS]; 1] = self
228            .inner
229            .adapter()
230            .read(state.memory, instruction, &mut adapter_record);
231        let read_data: [[u8; BLOCK_SIZE]; BLOCKS] = read_data_arr[0];
232
233        let local_opcode = instruction.opcode.local_opcode_idx(self.inner.offset);
234
235        core_record.fill_from_execution_data(local_opcode as u8, read_data.as_flattened());
236
237        // Try fast path for non-SETUP operations with known curve types
238        let output: [[u8; BLOCK_SIZE]; BLOCKS] =
239            if !is_setup_opcode(local_opcode) {
240                compute_ec_double_fast::<BLOCKS, BLOCK_SIZE>(self.cached_curve_type, read_data)
241                    .unwrap_or_else(|| {
242                        compute_ec_slow::<BLOCKS, BLOCK_SIZE>(
243                            &self.inner.expr,
244                            local_opcode,
245                            read_data.as_flattened(),
246                        )
247                    })
248            } else {
249                compute_ec_slow::<BLOCKS, BLOCK_SIZE>(
250                    &self.inner.expr,
251                    local_opcode,
252                    read_data.as_flattened(),
253                )
254            };
255
256        self.inner.adapter().write(
257            state.memory,
258            instruction,
259            output,
260            &mut adapter_record,
261        );
262
263        *state.pc = state.pc.wrapping_add(DEFAULT_PC_STEP);
264        Ok(())
265    }
266
267    fn get_opcode_name(&self, _opcode: usize) -> String {
268        self.inner.name.clone()
269    }
270}