diff --git a/zkevm-circuits/src/evm_circuit/execution.rs b/zkevm-circuits/src/evm_circuit/execution.rs index 9801ef2fa6..c5ad8242b5 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -36,6 +36,7 @@ use halo2_proofs::{ }, poly::Rotation, }; +use itertools::Itertools; use std::{ collections::{BTreeSet, HashMap}, iter, @@ -246,7 +247,7 @@ pub(crate) struct ExecutionConfig { // EVM Circuit selector, which enables all usable rows. The rows where this selector is // disabled won't verify any constraint (they can be unused rows or rows with blinding // factors). - q_usable: Selector, + q_usable: Column, // Dynamic selector that is enabled at the rows where each assigned execution step starts (a // step has dynamic height). q_step: Column, @@ -385,7 +386,7 @@ impl ExecutionConfig { pow_of_rand_table: &dyn LookupTable, ) -> Self { let mut instrument = Instrument::default(); - let q_usable = meta.complex_selector(); + let q_usable = meta.fixed_column(); let q_step = meta.advice_column(); let constants = meta.fixed_column(); meta.enable_constant(constants); @@ -414,7 +415,7 @@ impl ExecutionConfig { let mut height_map = HashMap::new(); meta.create_gate("Constrain execution state", |meta| { - let q_usable = meta.query_selector(q_usable); + let q_usable = meta.query_fixed(q_usable, Rotation::cur()); let q_step = meta.query_advice(q_step, Rotation::cur()); let q_step_first = meta.query_selector(q_step_first); let q_step_last = meta.query_selector(q_step_last); @@ -448,7 +449,7 @@ impl ExecutionConfig { }); meta.create_gate("q_step", |meta| { - let q_usable = meta.query_selector(q_usable); + let q_usable = meta.query_fixed(q_usable, Rotation::cur()); let q_step_first = meta.query_selector(q_step_first); let q_step_last = meta.query_selector(q_step_last); let q_step = meta.query_advice(q_step, Rotation::cur()); @@ -675,7 +676,7 @@ impl ExecutionConfig { fn configure_gadget>( meta: &mut ConstraintSystem, advices: [Column; STEP_WIDTH], - q_usable: Selector, + q_usable: Column, q_step: Column, num_rows_until_next_step: Column, q_step_first: Selector, @@ -736,7 +737,7 @@ impl ExecutionConfig { #[allow(clippy::too_many_arguments)] fn configure_gadget_impl( meta: &mut ConstraintSystem, - q_usable: Selector, + q_usable: Column, q_step: Column, num_rows_until_next_step: Column, q_step_first: Selector, @@ -796,7 +797,7 @@ impl ExecutionConfig { ] { if !constraints.is_empty() { meta.create_gate(name, |meta| { - let q_usable = meta.query_selector(q_usable); + let q_usable = meta.query_fixed(q_usable, Rotation::cur()); let selector = selector(meta); constraints.into_iter().map(move |(name, constraint)| { ( @@ -813,7 +814,7 @@ impl ExecutionConfig { // Enforce the state transitions for this opcode meta.create_gate("Constrain state machine transitions", |meta| { - let q_usable = meta.query_selector(q_usable); + let q_usable = meta.query_fixed(q_usable, Rotation::cur()); let q_step = meta.query_advice(q_step, Rotation::cur()); let q_step_last = meta.query_selector(q_step_last); @@ -966,6 +967,15 @@ impl ExecutionConfig { } } + pub fn get_num_rows_required_no_padding(&self, block: &Block) -> usize { + let mut num_rows = 0; + for transaction in &block.txs { + for step in &transaction.steps { + num_rows += step.execution_state.get_step_height(); + } + } + num_rows + } pub fn get_num_rows_required(&self, block: &Block) -> usize { // Start at 1 so we can be sure there is an unused `next` row available let mut num_rows = 1; @@ -994,7 +1004,12 @@ impl ExecutionConfig { // Name Advice columns for idx in 0..height { let offset = offset + idx; - self.q_usable.enable(region, offset)?; + region.assign_fixed( + || "q_usable selector", + self.q_usable, + offset, + || Value::known(F::one()), + )?; region.assign_advice( || "step selector", self.q_step, @@ -1027,155 +1042,284 @@ impl ExecutionConfig { block: &Block, challenges: &Challenges>, ) -> Result>, Error> { - let mut is_first_time = true; + // If the height is not 1, padding to fixed height will be impossible + debug_assert_eq!(ExecutionState::EndBlock.get_step_height(), 1); - layouter.assign_region( - || "Execution step", - |mut region| { - if is_first_time { - is_first_time = false; - region.assign_advice( - || "step selector", - self.q_step, - self.get_num_rows_required(block) - 1, - || Value::known(F::zero()), - )?; - return Ok(()); - } - let mut offset = 0; - - let inverter = Inverter::new(MAX_STEP_HEIGHT as u64); - - // Annotate the EVMCircuit columns within it's single region. - self.annotate_circuit(&mut region); - - self.q_step_first.enable(&mut region, offset)?; - - let dummy_tx = Transaction::default(); - let last_call = block - .txs - .last() - .map(|tx| tx.calls[0].clone()) - .unwrap_or_else(Call::default); - let end_block_not_last = &block.end_block_not_last; - let end_block_last = &block.end_block_last; - // Collect all steps - let mut steps = block - .txs - .iter() - .flat_map(|tx| { - tx.steps - .iter() - .map(move |step| (tx, &tx.calls[step.call_index], step)) + let inverter = Inverter::new(MAX_STEP_HEIGHT as u64); + let evm_rows = block.circuits_params.max_evm_rows; + // 0 means "dynamic height". If fixed height is used in unittests, CI will be quite slow. + let no_padding = evm_rows == 0; + + // There should be 3 group of regions + // 1. real steps + // 2. padding EndBlocks. + // For the ease of implementation, even for `no_padding` case, + // we will still pad 1 end_block_not_last. + // 3. final EndBlock + let region1_height = self.get_num_rows_required_no_padding(block); + let region3_height = 2; // EndBlock, plus a dummy "next" row used for Rotation + let region2_height = if no_padding { + 1 + } else { + if region1_height + region3_height >= evm_rows { + log::error!( + "evm circuit row not enough, region1_height:{}, region3_height:{}, max_evm_rows:{}", + region1_height, + region3_height, + evm_rows + ); + return Err(Error::Synthesis); + } + evm_rows - region3_height - region1_height + }; + + // A quick path for "reporting" height for the halo2 first pass layouter. + let assign_shape_fn = |region: &mut Region<'_, F>, height| { + region.assign_advice( + || "step selector", + self.q_step, + height - 1, + || Value::known(F::zero()), + )?; + Ok(height) + }; + + let dummy_tx = Transaction::default(); + let last_call = block + .txs + .last() + .map(|tx| tx.calls[0].clone()) + .unwrap_or_else(Call::default); + let end_block_not_last = &block.end_block_not_last; + let end_block_last = &block.end_block_last; + + // A helper struct used for parallel assignment + struct StepAssignment { + tx_idx: usize, + step_idx_in_tx: usize, + height: usize, + offset: usize, + } + let total_step_num = block.txs.iter().map(|t| t.steps.len()).sum::(); + let mut step_assignments: Vec = Vec::new(); + step_assignments.reserve(total_step_num); + + // the "global offset" + let mut offset = 0; + for (tx_idx, tx) in block.txs.iter().enumerate() { + for (step_idx, step) in tx.steps.iter().enumerate() { + let height = step.execution_state.get_step_height(); + step_assignments.push(StepAssignment { + tx_idx, + step_idx_in_tx: step_idx, + offset, + height, + }); + offset += height; + } + } + assert_eq!(offset, region1_height); + offset = 0; + + // Print some logs after each tx, for debugging + let log_step_fn = |transaction: &Transaction, step: &ExecStep, offset| { + if step.execution_state == ExecutionState::EndTx { + let mut tx = transaction.clone(); + tx.call_data.clear(); + tx.calls.clear(); + tx.steps.clear(); + tx.rlp_signed.clear(); + tx.rlp_unsigned.clear(); + let total_gas = { + let gas_used = tx.gas - step.gas_left; + let current_cumulative_gas_used: u64 = if tx.id == 1 { + 0 + } else { + // first transaction needs TxReceiptFieldTag::COUNT(3) lookups + // to tx receipt, + // while later transactions need 4 (with one extra cumulative + // gas read) lookups + let rw = &block.rws[( + RwTableTag::TxReceipt, + (tx.id - 2) * (TxReceiptFieldTag::COUNT + 1) + 2, + )]; + rw.receipt_value() + }; + current_cumulative_gas_used + gas_used + }; + log::info!( + "offset {} tx_num {} total_gas {} assign last step {:?} of tx {:?}", + offset, + tx.id, + total_gas, + step, + tx + ); + } + }; + + // Calculate chunk_size and chunk_num + // Here a min_chunk_size is provided to reduce threading overhead + let chunking_fn = |name: &str, task_len: usize, min_chunk_size: usize| -> (usize, usize) { + if task_len == 0 { + return (0, 0); + } + let num_threads = std::thread::available_parallelism() + .map(|e| e.get()) + .unwrap_or(1); + //let num_threads = 1; + let chunk_size = ((task_len + num_threads - 1) / num_threads).max(min_chunk_size); + let chunk_num = (task_len + chunk_size - 1) / chunk_size; + log::debug!( + "{} chunking: len = {}, num_threads = {}, chunk_size = {}, chunk_num = {}", + name, + task_len, + num_threads, + chunk_size, + chunk_num + ); + (chunk_size, chunk_num) + }; + + // Step1: assign real steps + let (region1_chunk_size, region1_chunk_num) = + chunking_fn("region1", step_assignments.len(), 50); + let mut region1_is_first_time: Vec<(usize, bool)> = (0..region1_chunk_num) + .map(|chunk_idx| (chunk_idx, true)) + .collect(); + let region1_height_sum = layouter + .assign_regions( + || "Execution step region1", + region1_is_first_time + .iter_mut() + .map(|(chunk_idx, is_first_time)| { + |mut region: Region<'_, F>| { + let chunk_idx = *chunk_idx; + let begin = chunk_idx * region1_chunk_size; + let end = + ((chunk_idx + 1) * region1_chunk_size).min(step_assignments.len()); + let step_idxs: Vec = (begin..end).collect(); + log::trace!("region1 range {} {} {}", chunk_idx, begin, end); + let total_height = step_idxs + .iter() + .map(|idx| step_assignments[*idx].height) + .sum::(); + if *is_first_time { + *is_first_time = false; + return assign_shape_fn(&mut region, total_height); + } + let mut offset = 0; + + // Annotate the EVMCircuit columns within it's single region. + self.annotate_circuit(&mut region); + + if chunk_idx == 0 { + self.q_step_first.enable(&mut region, offset)?; + } + for step_idx in step_idxs { + let step_assignment = &step_assignments[step_idx]; + let transaction = &block.txs[step_assignment.tx_idx]; + let step = &transaction.steps[step_assignment.step_idx_in_tx]; + let call = &transaction.calls[step.call_index]; + + let height = step.execution_state.get_step_height(); + + log_step_fn(transaction, step, offset); + + let next = match step_assignments.get(step_idx + 1) { + None => (&dummy_tx, &last_call, end_block_not_last), + Some(step_assignment) => { + let transaction = &block.txs[step_assignment.tx_idx]; + let step = + &transaction.steps[step_assignment.step_idx_in_tx]; + let call = &transaction.calls[step.call_index]; + (transaction, call, step) + } + }; + + self.assign_exec_step( + &mut region, + offset, + block, + transaction, + call, + step, + height, + Some(next), + challenges, + )?; + + self.assign_q_step(&mut region, &inverter, offset, height)?; + + offset += height; + } + debug_assert_eq!(offset, total_height); + Ok(total_height) + } }) - .chain(std::iter::once((&dummy_tx, &last_call, end_block_not_last))) - .peekable(); - - let evm_rows = block.circuits_params.max_evm_rows; - let no_padding = evm_rows == 0; - - // part1: assign real steps - loop { - let (transaction, call, step) = steps.next().expect("should not be empty"); - let next = steps.peek(); - if next.is_none() { - break; - } - let height = step.execution_state.get_step_height(); - - // Assign the step witness - if step.execution_state == ExecutionState::EndTx { - let mut tx = transaction.clone(); - tx.call_data.clear(); - tx.calls.clear(); - tx.steps.clear(); - tx.rlp_signed.clear(); - tx.rlp_unsigned.clear(); - let total_gas = { - let gas_used = tx.gas - step.gas_left; - let current_cumulative_gas_used: u64 = if tx.id == 1 { - 0 - } else { - // first transaction needs TxReceiptFieldTag::COUNT(3) lookups - // to tx receipt, - // while later transactions need 4 (with one extra cumulative - // gas read) lookups - let rw = &block.rws[( - RwTableTag::TxReceipt, - (tx.id - 2) * (TxReceiptFieldTag::COUNT + 1) + 2, - )]; - rw.receipt_value() - }; - current_cumulative_gas_used + gas_used - }; - log::info!( - "offset {} tx_num {} total_gas {} assign last step {:?} of tx {:?}", - offset, - tx.id, - total_gas, - step, - tx - ); - } - self.assign_exec_step( - &mut region, - offset, - block, - transaction, - call, - step, - height, - next.copied(), - challenges, - )?; - - // q_step logic - self.assign_q_step(&mut region, &inverter, offset, height)?; - - offset += height; - } + .collect_vec(), + )? + .into_iter() + .sum::(); - // part2: assign non-last EndBlock steps when padding needed - if !no_padding { - let height = ExecutionState::EndBlock.get_step_height(); - debug_assert_eq!(height, 1); - // 1 for EndBlock(last), 1 for "part 4" cells - let last_row = evm_rows - 2; - log::trace!( - "assign non-last EndBlock in range [{},{})", - offset, - last_row - ); - if offset > last_row { - log::error!( - "evm circuit row not enough, offset: {}, max_evm_rows: {}", - offset, - evm_rows - ); - return Err(Error::Synthesis); - } - self.assign_same_exec_step_in_range( - &mut region, - offset, - last_row, - block, - &dummy_tx, - &last_call, - end_block_not_last, - height, - challenges, - )?; - - for row_idx in offset..last_row { - self.assign_q_step(&mut region, &inverter, row_idx, height)?; + debug_assert_eq!(region1_height, region1_height_sum); + + // part2: assign non-last EndBlock steps when padding needed + + let (region2_chunk_size, region2_chunk_num) = chunking_fn("region2", region2_height, 300); + let idxs: Vec = (0..region2_height).collect(); + let mut region2_is_first_time = vec![true; region2_chunk_num]; + + log::trace!( + "assign non-last EndBlock in range [{},{})", + region1_height, + region1_height + region2_height + ); + layouter.assign_regions( + || "Execution step region2", + idxs.chunks(region2_chunk_size) + .zip_eq(region2_is_first_time.iter_mut()) + .map(|(rows, is_first_time)| { + |mut region: Region<'_, F>| { + if *is_first_time { + *is_first_time = false; + return assign_shape_fn(&mut region, rows.len()); + } + self.assign_same_exec_step_in_range( + &mut region, + 0, + rows.len(), + block, + &dummy_tx, + &last_call, + end_block_not_last, + 1, + challenges, + )?; + for row_idx in 0..rows.len() { + self.assign_q_step(&mut region, &inverter, row_idx, 1)?; + } + Ok(rows.len()) } - offset = last_row; - } + }) + .collect_vec(), + )?; - // part3: assign the last EndBlock at offset `evm_rows - 1` - let height = ExecutionState::EndBlock.get_step_height(); - debug_assert_eq!(height, 1); - log::trace!("assign last EndBlock at offset {}", offset); + // part3: assign the last EndBlock at offset `evm_rows - 1` + // This region don't need to be parallelized + log::trace!( + "assign last EndBlock at offset {}", + region1_height + region2_height + ); + + let mut region3_is_first_time = true; + layouter.assign_region( + || "Execution step region3", + |mut region| { + if region3_is_first_time { + region3_is_first_time = false; + return assign_shape_fn(&mut region, region3_height); + } self.assign_exec_step( &mut region, offset, @@ -1183,32 +1327,26 @@ impl ExecutionConfig { &dummy_tx, &last_call, end_block_last, - height, + 1, None, challenges, )?; - self.assign_q_step(&mut region, &inverter, offset, height)?; - // enable q_step_last + self.assign_q_step(&mut region, &inverter, offset, 1)?; self.q_step_last.enable(&mut region, offset)?; - offset += height; - - // part4: // These are still referenced (but not used) in next rows region.assign_advice( || "step height", self.num_rows_until_next_step, - offset, + 1, || Value::known(F::zero()), )?; region.assign_advice( || "step height inv", self.q_step, - offset, + 1, || Value::known(F::zero()), )?; - - log::debug!("assign for region done at offset {}", offset); - Ok(()) + Ok(2) // region height }, )?; @@ -1217,15 +1355,10 @@ impl ExecutionConfig { let final_withdraw_root_cell = self .end_block_gadget .withdraw_root_assigned - .borrow() + .lock() + .unwrap() .expect("withdraw_root cell should has been assigned"); - // sanity check - let evm_rows = block.circuits_params.max_evm_rows; - if evm_rows >= 2 { - assert_eq!(final_withdraw_root_cell.row_offset, evm_rows - 2); - } - let withdraw_root_rlc = challenges .evm_word() .map(|r| rlc::value(&block.withdraw_root.to_le_bytes(), r)); @@ -1298,6 +1431,7 @@ impl ExecutionConfig { challenges, self.advices.to_vec(), 1, + 1, offset_begin, ); self.assign_exec_step_int(region, offset_begin, block, transaction, call, step, false)?; @@ -1332,6 +1466,7 @@ impl ExecutionConfig { challenges, self.advices.to_vec(), MAX_STEP_HEIGHT * 3, + height, offset, ); diff --git a/zkevm-circuits/src/evm_circuit/execution/end_block.rs b/zkevm-circuits/src/evm_circuit/execution/end_block.rs index a1426d2ddc..dc32fd1333 100644 --- a/zkevm-circuits/src/evm_circuit/execution/end_block.rs +++ b/zkevm-circuits/src/evm_circuit/execution/end_block.rs @@ -1,3 +1,5 @@ +use std::sync::Mutex; + use crate::{ evm_circuit::{ execution::ExecutionGadget, @@ -23,7 +25,7 @@ use halo2_proofs::{ plonk::{Error, Expression}, }; -#[derive(Clone, Debug)] +#[derive(Debug)] pub(crate) struct EndBlockGadget { total_txs: Cell, total_txs_is_max_txs: IsEqualGadget, @@ -32,7 +34,24 @@ pub(crate) struct EndBlockGadget { max_txs: Cell, phase2_withdraw_root: Cell, phase2_withdraw_root_prev: Cell, - pub withdraw_root_assigned: std::cell::RefCell>, + pub withdraw_root_assigned: Mutex>, +} + +impl Clone for EndBlockGadget { + fn clone(&self) -> Self { + let withdraw_root_assigned: Option = + *self.withdraw_root_assigned.lock().unwrap(); + Self { + withdraw_root_assigned: Mutex::new(withdraw_root_assigned), + total_txs: self.total_txs.clone(), + total_txs_is_max_txs: self.total_txs_is_max_txs.clone(), + is_empty_block: self.is_empty_block.clone(), + max_rws: self.max_rws.clone(), + max_txs: self.max_txs.clone(), + phase2_withdraw_root: self.phase2_withdraw_root.clone(), + phase2_withdraw_root_prev: self.phase2_withdraw_root_prev.clone(), + } + } } const EMPTY_BLOCK_N_RWS: u64 = 0; @@ -172,10 +191,9 @@ impl ExecutionGadget for EndBlockGadget { offset, region.word_rlc(block.prev_withdraw_root), )?; - - self.withdraw_root_assigned - .borrow_mut() - .replace(withdraw_root.cell()); + if let Some(cell) = withdraw_root { + *self.withdraw_root_assigned.lock().unwrap() = Some(cell.cell()); + } // TODO: now we do not export withdraw_root_prev for we have only one // phase2 cell which is enabled for copy constraint // self.withdraw_root_prev_assigned @@ -185,8 +203,8 @@ impl ExecutionGadget for EndBlockGadget { // When rw_indices is not empty, we're at the last row (at a fixed offset), // where we need to access the max_rws and max_txs constant. if !step.rw_indices.is_empty() { - region.constrain_constant(max_rws_assigned, max_rws)?; - region.constrain_constant(max_txs_assigned, max_txs)?; + region.constrain_constant(max_rws_assigned.unwrap(), max_rws)?; + region.constrain_constant(max_txs_assigned.unwrap(), max_txs)?; } Ok(()) } diff --git a/zkevm-circuits/src/evm_circuit/util.rs b/zkevm-circuits/src/evm_circuit/util.rs index 3df8b4e04c..44a4119bcc 100644 --- a/zkevm-circuits/src/evm_circuit/util.rs +++ b/zkevm-circuits/src/evm_circuit/util.rs @@ -65,7 +65,7 @@ impl Cell { region: &mut CachedRegion<'_, '_, F>, offset: usize, value: Value, - ) -> Result, Error> { + ) -> Result>, Error> { region.assign_advice( || { format!( @@ -98,6 +98,12 @@ pub struct CachedRegion<'r, 'b, F: FieldExt> { advice_columns: Vec>, width_start: usize, height_start: usize, + // the `CachedRegion` can be seen as a written buffer for real halo2 regions. + // All writes beyond `height_limit` will not be written through to halo2 columns. + // This is used for the evm step "assign next then assign current" pattern. + // When we remove this pattern later, this field can also be removed. + // More: + height_limit: usize, } impl<'r, 'b, F: FieldExt> CachedRegion<'r, 'b, F> { @@ -107,6 +113,7 @@ impl<'r, 'b, F: FieldExt> CachedRegion<'r, 'b, F> { challenges: &'r Challenges>, advice_columns: Vec>, height: usize, + height_limit: usize, height_start: usize, ) -> Self { Self { @@ -115,6 +122,7 @@ impl<'r, 'b, F: FieldExt> CachedRegion<'r, 'b, F> { challenges, width_start: advice_columns[0].index(), height_start, + height_limit, advice_columns, } } @@ -153,13 +161,15 @@ impl<'r, 'b, F: FieldExt> CachedRegion<'r, 'b, F> { } /// Assign an advice column value (witness). + /// If return value is None, it means the assignment will only happen + /// inside the CachedRegion, and is not written into real halo2 columns. pub fn assign_advice<'v, V, VR, A, AR>( &'v mut self, annotation: A, column: Column, offset: usize, to: V, - ) -> Result, Error> + ) -> Result>, Error> where V: Fn() -> Value + 'v, for<'vr> Assigned: From<&'vr VR>, @@ -167,18 +177,26 @@ impl<'r, 'b, F: FieldExt> CachedRegion<'r, 'b, F> { AR: Into, { // Actually set the value - let res = self.region.assign_advice(annotation, column, offset, &to); - // Cache the value - // Note that the `value_field` in `AssignedCell` might be `Value::unkonwn` if - // the column has different phase than current one, so we call to `to` - // again here to cache the value. - if res.is_ok() { + if offset - self.height_start < self.height_limit { + let res = self.region.assign_advice(annotation, column, offset, &to); + // Cache the value + // Note that the `value_field` in `AssignedCell` might be `Value::unkonwn` if + // the column has different phase than current one, so we call to `to` + // again here to cache the value. + if res.is_ok() { + to().map(|f| { + self.advice[column.index() - self.width_start][offset - self.height_start] = + Assigned::from(&f).evaluate(); + }); + } + Ok(Some(res?)) + } else { to().map(|f| { self.advice[column.index() - self.width_start][offset - self.height_start] = Assigned::from(&f).evaluate(); }); + Ok(None) } - res } pub fn get_fixed(&self, _row_index: usize, _column_index: usize, _rotation: Rotation) -> F { @@ -524,7 +542,7 @@ impl RandomLinearCombination { region: &mut CachedRegion<'_, '_, F>, offset: usize, bytes: Option<[u8; N]>, - ) -> Result>, Error> { + ) -> Result>>, Error> { bytes.map_or(Err(Error::Synthesis), |bytes| { self.cells .iter() diff --git a/zkevm-circuits/src/evm_circuit/util/math_gadget/test_util.rs b/zkevm-circuits/src/evm_circuit/util/math_gadget/test_util.rs index 3c160480d4..cda313a482 100644 --- a/zkevm-circuits/src/evm_circuit/util/math_gadget/test_util.rs +++ b/zkevm-circuits/src/evm_circuit/util/math_gadget/test_util.rs @@ -203,6 +203,7 @@ impl> Circuit for UnitTestMathGadgetBaseC &challenge_values, config.advices.to_vec(), MAX_STEP_HEIGHT * 3, + MAX_STEP_HEIGHT * 3, offset, ); config.step.state.execution_state.assign(