From fe9beebad857c30db13040360d9cd094fa219c5f Mon Sep 17 00:00:00 2001 From: Ray Gao Date: Tue, 12 Dec 2023 01:39:22 -0800 Subject: [PATCH] Identity (dataCopy) precompile (#1628) ### Description This is a follow-up PR for [PR#1396](https://github.com/privacy-scaling-explorations/zkevm-circuits/pull/1396) from @roynalnaruto. After merging in 1396's changes to a fresh fork, the branch is compared with Scroll's develop head to incorporate latest patterns on precompile-related development. ### Issue Link [Aggregating precompile issue 924](https://github.com/privacy-scaling-explorations/zkevm-circuits/issues/924) ### Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Design choices See [documentation](https://www.notion.so/scrollzkp/PR-1396-Follow-up-Precompile-Identity-for-Upstream-3624af055d9b4820a0354e9af4fa7918) --------- Co-authored-by: Rohit Narurkar Co-authored-by: Zhang Zhuo --- .gitignore | 3 +- .../src/circuit_input_builder/execution.rs | 22 +- .../circuit_input_builder/input_state_ref.rs | 246 +++++- bus-mapping/src/error.rs | 7 + bus-mapping/src/evm.rs | 2 + bus-mapping/src/evm/opcodes.rs | 11 +- bus-mapping/src/evm/opcodes/begin_end_tx.rs | 17 +- bus-mapping/src/evm/opcodes/callop.rs | 639 ++++++--------- bus-mapping/src/evm/opcodes/create.rs | 4 +- .../src/evm/opcodes/error_code_store.rs | 2 +- .../opcodes/error_invalid_creation_code.rs | 4 +- .../src/evm/opcodes/error_invalid_jump.rs | 2 +- .../evm/opcodes/error_oog_account_access.rs | 2 +- bus-mapping/src/evm/opcodes/error_oog_call.rs | 2 +- bus-mapping/src/evm/opcodes/error_oog_exp.rs | 2 +- bus-mapping/src/evm/opcodes/error_oog_log.rs | 2 +- .../src/evm/opcodes/error_oog_memory_copy.rs | 2 +- .../src/evm/opcodes/error_oog_precompile.rs | 37 + .../src/evm/opcodes/error_oog_sload_sstore.rs | 2 +- .../evm/opcodes/error_precompile_failed.rs | 66 ++ .../opcodes/error_return_data_outofbound.rs | 2 +- bus-mapping/src/evm/opcodes/error_simple.rs | 2 +- .../src/evm/opcodes/error_write_protection.rs | 2 +- bus-mapping/src/evm/opcodes/logs.rs | 2 +- bus-mapping/src/evm/opcodes/mload.rs | 4 +- .../src/evm/opcodes/precompiles/mod.rs | 61 ++ bus-mapping/src/evm/opcodes/return_revert.rs | 4 +- bus-mapping/src/evm/opcodes/sha3.rs | 2 +- bus-mapping/src/evm/opcodes/stackonlyop.rs | 2 +- bus-mapping/src/evm/opcodes/stop.rs | 2 +- bus-mapping/src/precompile.rs | 192 ++++- eth-types/src/evm_types.rs | 28 + eth-types/src/evm_types/memory.rs | 31 +- zkevm-circuits/src/evm_circuit/execution.rs | 24 + .../src/evm_circuit/execution/begin_tx.rs | 1 - .../src/evm_circuit/execution/callop.rs | 750 ++++++++++++++++-- .../execution/error_oog_precompile.rs | 278 +++++++ .../execution/error_precompile_failed.rs | 178 +++++ .../execution/precompiles/identity.rs | 266 +++++++ .../evm_circuit/execution/precompiles/mod.rs | 2 + zkevm-circuits/src/evm_circuit/step.rs | 77 +- zkevm-circuits/src/evm_circuit/table.rs | 29 +- zkevm-circuits/src/evm_circuit/util.rs | 1 + .../src/evm_circuit/util/common_gadget.rs | 52 +- .../evm_circuit/util/constraint_builder.rs | 85 +- .../src/evm_circuit/util/math_gadget.rs | 2 + .../util/math_gadget/binary_number.rs | 68 ++ .../src/evm_circuit/util/precompile_gadget.rs | 78 ++ zkevm-circuits/src/table/copy_table.rs | 1 + 49 files changed, 2773 insertions(+), 527 deletions(-) create mode 100644 bus-mapping/src/evm/opcodes/error_oog_precompile.rs create mode 100644 bus-mapping/src/evm/opcodes/error_precompile_failed.rs create mode 100644 bus-mapping/src/evm/opcodes/precompiles/mod.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/error_precompile_failed.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/precompiles/identity.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs create mode 100644 zkevm-circuits/src/evm_circuit/util/math_gadget/binary_number.rs create mode 100644 zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs diff --git a/.gitignore b/.gitignore index 3a4d0c5b8b..96227f4dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.png .DS_Store .vscode -.idea \ No newline at end of file +.idea +*.log \ No newline at end of file diff --git a/bus-mapping/src/circuit_input_builder/execution.rs b/bus-mapping/src/circuit_input_builder/execution.rs index befa358587..f23e80f7f2 100644 --- a/bus-mapping/src/circuit_input_builder/execution.rs +++ b/bus-mapping/src/circuit_input_builder/execution.rs @@ -1,8 +1,11 @@ //! Execution step related module. use crate::{ - circuit_input_builder::CallContext, error::ExecError, exec_trace::OperationRef, + circuit_input_builder::CallContext, + error::{ExecError, OogError}, + exec_trace::OperationRef, operation::RWCounter, + precompile::PrecompileCalls, }; use eth_types::{evm_types::OpcodeId, GethExecStep, Word, H256}; use gadgets::impl_expr; @@ -60,6 +63,7 @@ impl ExecStep { ExecStep { exec_state: ExecState::Op(step.op), pc: step.pc, + stack_size: step.stack.0.len(), memory_size: call_ctx.memory.len(), gas_left: step.gas, @@ -117,6 +121,16 @@ impl ExecStep { assert_eq!(memory_size % n_bytes_word, 0); memory_size / n_bytes_word } + + /// Returns `true` if this is an execution step of Precompile. + pub fn is_precompiled(&self) -> bool { + matches!(self.exec_state, ExecState::Precompile(_)) + } + + /// Returns `true` if `error` is oog in precompile calls + pub fn is_precompile_oog_err(&self) -> bool { + matches!(self.error, Some(ExecError::OutOfGas(OogError::Precompile))) + } } /// Execution state @@ -124,6 +138,8 @@ impl ExecStep { pub enum ExecState { /// EVM Opcode ID Op(OpcodeId), + /// Precompile call + Precompile(PrecompileCalls), /// Virtual step Begin Tx BeginTx, /// Virtual step End Tx @@ -279,14 +295,14 @@ impl CopyEvent { // increase in rw counter from the start of the copy event to step index fn rw_counter_increase(&self, step_index: usize) -> u64 { let source_rw_increase = match self.src_type { - CopyDataType::Bytecode | CopyDataType::TxCalldata => 0, + CopyDataType::Bytecode | CopyDataType::TxCalldata | CopyDataType::RlcAcc => 0, CopyDataType::Memory => std::cmp::min( u64::try_from(step_index + 1).unwrap() / 2, self.src_addr_end .checked_sub(self.src_addr) .unwrap_or_default(), ), - CopyDataType::RlcAcc | CopyDataType::TxLog | CopyDataType::Padding => unreachable!(), + CopyDataType::TxLog | CopyDataType::Padding => unreachable!(), }; let destination_rw_increase = match self.dst_type { CopyDataType::RlcAcc | CopyDataType::Bytecode => 0, diff --git a/bus-mapping/src/circuit_input_builder/input_state_ref.rs b/bus-mapping/src/circuit_input_builder/input_state_ref.rs index 179050c1dc..38c0db5e30 100644 --- a/bus-mapping/src/circuit_input_builder/input_state_ref.rs +++ b/bus-mapping/src/circuit_input_builder/input_state_ref.rs @@ -13,6 +13,7 @@ use crate::{ StackOp, Target, TxAccessListAccountOp, TxLogField, TxLogOp, TxReceiptField, TxReceiptOp, RW, }, + precompile::{is_precompiled, PrecompileCalls}, state_db::{CodeDB, StateDB}, Error, }; @@ -242,11 +243,23 @@ impl<'a> CircuitInputStateRef<'a> { &mut self, step: &mut ExecStep, address: MemoryAddress, - value: u8, - ) -> Result<(), Error> { + ) -> Result { + let byte = &self.call_ctx()?.memory.read_chunk(address, 1.into())[0]; let call_id = self.call()?.call_id; - self.push_op(step, RW::READ, MemoryOp::new(call_id, address, value))?; - Ok(()) + self.push_op(step, RW::READ, MemoryOp::new(call_id, address, *byte))?; + Ok(*byte) + } + + /// Almost the same as above memory_read, but read from the caller's context instead. + pub fn memory_read_caller( + &mut self, + step: &mut ExecStep, + address: MemoryAddress, // Caution: make sure this address = slot passing + ) -> Result { + let byte = &self.caller_ctx()?.memory.read_chunk(address, 1.into())[0]; + let call_id = self.call()?.caller_id; + self.push_op(step, RW::READ, MemoryOp::new(call_id, address, *byte))?; + Ok(*byte) } /// Push a write type [`MemoryOp`] into the @@ -260,10 +273,32 @@ impl<'a> CircuitInputStateRef<'a> { step: &mut ExecStep, address: MemoryAddress, value: u8, - ) -> Result<(), Error> { + ) -> Result { let call_id = self.call()?.call_id; + + let mem = &mut self.call_ctx_mut()?.memory; + let value_prev = mem.read_chunk(address, 1.into())[0]; + mem.write_chunk(address, &[value]); + self.push_op(step, RW::WRITE, MemoryOp::new(call_id, address, value))?; - Ok(()) + Ok(value_prev) + } + + /// Almost the same as above memory_write, but write to the caller's context instead. + pub fn memory_write_caller( + &mut self, + step: &mut ExecStep, + address: MemoryAddress, // Caution: make sure this address = slot passing + value: u8, + ) -> Result { + let call_id = self.call()?.caller_id; + + let mem = &mut self.caller_ctx_mut()?.memory; + let value_prev = mem.read_chunk(address, 1.into())[0]; + mem.write_chunk(address, &[value_prev]); + + self.push_op(step, RW::WRITE, MemoryOp::new(call_id, address, value))?; + Ok(value_prev) } /// Push a write type [`StackOp`] into the @@ -1005,7 +1040,11 @@ impl<'a> CircuitInputStateRef<'a> { } /// Handle a reversion group - fn handle_reversion(&mut self) { + pub fn handle_reversion(&mut self, current_exec_steps: &mut [&mut ExecStep]) { + // We already know that the call has reverted. Only the precompile failure case must be + // handled differently as the ExecSteps associated with those calls haven't yet been pushed + // to the tx's steps. + let reversion_group = self .tx_ctx .reversion_groups @@ -1022,9 +1061,15 @@ impl<'a> CircuitInputStateRef<'a> { false, op, ); - self.tx.steps_mut()[step_index] - .bus_mapping_instance - .push(rev_op_ref); + + let step: &mut ExecStep = if step_index >= self.tx.steps_mut().len() { + // the `current_exec_steps` will be appended after self.tx.steps + // So here we do an index-mapping + current_exec_steps[step_index - self.tx.steps_mut().len()] + } else { + &mut self.tx.steps_mut()[step_index] + }; + step.bus_mapping_instance.push(rev_op_ref); } } @@ -1040,20 +1085,53 @@ impl<'a> CircuitInputStateRef<'a> { /// previous call context. pub fn handle_return( &mut self, - exec_step: &mut ExecStep, + current_exec_steps: &mut [&mut ExecStep], geth_steps: &[GethExecStep], need_restore: bool, ) -> Result<(), Error> { + let step = &geth_steps[0]; + + // For these 6 opcodes, the return data should be handled in opcodes respectively. + // For other opcodes/states, return data must be empty. + if !matches!( + step.op, + OpcodeId::RETURN + | OpcodeId::REVERT + | OpcodeId::CALL + | OpcodeId::CALLCODE + | OpcodeId::DELEGATECALL + | OpcodeId::STATICCALL + ) || current_exec_steps[0].error.is_some() + { + if let Ok(caller) = self.caller_ctx_mut() { + caller.return_data.clear(); + } + } if need_restore { - self.handle_restore_context(exec_step, geth_steps)?; + // The only case where `current_exec_steps` are more than 1 is for precompiled contract + // calls. In this case, we have: [..., CALLOP, PRECOMPILE_EXEC_STEP] + // as the current execution steps. And in case of return, we + // restore context from the internal precompile execution + // step to the `CALLOP` step. + if current_exec_steps.len() > 1 { + debug_assert!( + current_exec_steps[1].is_precompiled() + || current_exec_steps[1].is_precompile_oog_err() + ); + } + + self.handle_restore_context( + current_exec_steps.last_mut().expect("last exists"), + geth_steps, + )?; } - let step = &geth_steps[0]; // handle return_data let (return_data_offset, return_data_length) = { if !self.call()?.is_root { let (offset, length) = match step.op { OpcodeId::RETURN | OpcodeId::REVERT => { + let exec_step = current_exec_steps.last_mut().expect("last exists").clone(); let (offset, length) = if exec_step.error.is_some() || (self.call()?.is_create() && self.call()?.is_success) { @@ -1110,7 +1188,7 @@ impl<'a> CircuitInputStateRef<'a> { // Handle reversion if this call doesn't end successfully if !call.is_success { - self.handle_reversion(); + self.handle_reversion(current_exec_steps); } // If current call has caller. @@ -1153,7 +1231,11 @@ impl<'a> CircuitInputStateRef<'a> { || geth_step.op == OpcodeId::RETURN) && exec_step.error.is_none(); - if !is_revert_or_return_call_success && !call.is_success { + if !is_revert_or_return_call_success + && !call.is_success + && !exec_step.is_precompiled() + && !exec_step.is_precompile_oog_err() + { // add call failure ops for exception cases self.call_context_read( exec_step, @@ -1195,6 +1277,19 @@ impl<'a> CircuitInputStateRef<'a> { } else { match geth_step.op { OpcodeId::STOP => [Word::zero(); 2], + OpcodeId::CALL + | OpcodeId::CALLCODE + | OpcodeId::STATICCALL + | OpcodeId::DELEGATECALL => { + let return_data_length = match exec_step.exec_state { + ExecState::Precompile(_) => { + // successful precompile call + self.caller_ctx()?.return_data.len().into() + } + _ => Word::zero(), + }; + [Word::zero(), return_data_length] + } OpcodeId::REVERT | OpcodeId::RETURN => { let offset = geth_step.stack.nth_last(0)?; let length = geth_step.stack.nth_last(1)?; @@ -1211,6 +1306,8 @@ impl<'a> CircuitInputStateRef<'a> { }; let gas_refund = if exec_step.error.is_some() { 0 + } else if exec_step.is_precompiled() { + exec_step.gas_left - exec_step.gas_cost } else { let curr_memory_word_size = (exec_step.memory_size as u64) / 32; let next_memory_word_size = if !last_callee_return_data_length.is_zero() { @@ -1237,11 +1334,11 @@ impl<'a> CircuitInputStateRef<'a> { geth_step.gas - memory_expansion_gas_cost - code_deposit_cost - constant_step_gas }; - let caller_gas_left = if is_revert_or_return_call_success || call.is_success { - geth_step_next.gas - gas_refund - } else { - geth_step_next.gas - }; + let caller_gas_left = geth_step_next.gas.checked_sub(gas_refund).unwrap_or_else ( + || { + panic!("caller_gas_left underflow geth_step_next {geth_step_next:?}, gas_refund {gas_refund:?}, exec_step {exec_step:?}, geth_step {geth_step:?}"); + } + ); for (field, value) in [ (CallContextField::IsRoot, (caller.is_root as u64).into()), @@ -1298,7 +1395,7 @@ impl<'a> CircuitInputStateRef<'a> { /// Push a copy event to the state. pub fn push_copy(&mut self, step: &mut ExecStep, event: CopyEvent) { - step.copy_rw_counter_delta = event.rw_counter_delta(); + step.copy_rw_counter_delta += event.rw_counter_delta(); self.block.add_copy_event(event); } @@ -1501,6 +1598,49 @@ impl<'a> CircuitInputStateRef<'a> { } } + // Precompile call failures. + if matches!( + step.op, + OpcodeId::CALL | OpcodeId::CALLCODE | OpcodeId::DELEGATECALL | OpcodeId::STATICCALL + ) { + let code_address = step.stack.nth_last(1)?.to_address(); + // NOTE: we do not know the amount of gas that precompile got here + // because the callGasTemp might probably be smaller than the gas + // on top of the stack (step.stack.last()) + // Therefore we postpone the oog handling to the implementor of callop. + if is_precompiled(&code_address) { + let precompile_call: PrecompileCalls = code_address[19].into(); + match precompile_call { + PrecompileCalls::Sha256 + | PrecompileCalls::Ripemd160 + | PrecompileCalls::Blake2F + | PrecompileCalls::ECRecover + | PrecompileCalls::Bn128Add + | PrecompileCalls::Bn128Mul + | PrecompileCalls::Bn128Pairing + | PrecompileCalls::Modexp => { + // Log the precompile address and gas left. + // Failure due to precompile being unsupported. + // Failure cases are routed to `PrecompileFailed` dummy gadget. + log::trace!( + "Precompile failed: code_address = {}, step.gas = {}", + code_address, + step.gas, + ); + return Ok(Some(ExecError::UnimplementedPrecompiles)); + } + pre_call => { + log::trace!( + "Precompile call failed: addr={:?}, step.gas={:?}", + pre_call, + step.gas + ); + return Ok(None); + } + } + } + } + return Err(Error::UnexpectedExecStepError( "*CALL*/CREATE* code not executed", Box::new(step.clone()), @@ -1559,6 +1699,70 @@ impl<'a> CircuitInputStateRef<'a> { Ok(copy_steps) } + pub(crate) fn gen_copy_steps_for_precompile_calldata( + &mut self, + exec_step: &mut ExecStep, + src_addr: u64, + copy_length: u64, + ) -> Result, Error> { + let mut input_bytes: Vec = vec![]; + + if copy_length != 0 { + let start_byte_index = src_addr; + for i in 0..copy_length { + let b = self.memory_read_caller(exec_step, (start_byte_index + i).into())?; + input_bytes.push(b); + } + } + + Ok(input_bytes) + } + + pub(crate) fn gen_copy_steps_for_precompile_callee_memory( + &mut self, + exec_step: &mut ExecStep, + result: &[u8], + ) -> Result<(Vec, Vec), Error> { + if result.is_empty() { + Ok((vec![], vec![])) + } else { + let mut prev_bytes = vec![]; + for (byte_index, byte) in result.iter().enumerate() { + let prev_byte = self.memory_write(exec_step, byte_index.into(), *byte)?; + prev_bytes.push(prev_byte); + } + + Ok((result.into(), prev_bytes)) + } + } + + pub(crate) fn gen_copy_steps_for_precompile_returndata( + &mut self, + exec_step: &mut ExecStep, + dst_addr: impl Into, + copy_length: impl Into, + result: &[u8], + ) -> Result, Error> { + let copy_length = copy_length.into().0; + let mut return_bytes: Vec = vec![]; + + if copy_length != 0 { + assert!(copy_length <= result.len()); + + let mut dst_byte_index: usize = dst_addr.into().0; + + for (src_byte_index, b) in result.iter().take(copy_length).enumerate() { + self.memory_read(exec_step, src_byte_index.into())?; + self.memory_write_caller(exec_step, dst_byte_index.into(), *b)?; + dst_byte_index += 1; + + return_bytes.push(*b); + } + } + + Ok(return_bytes) + } + /// Generate copy steps for call data. pub(crate) fn gen_copy_steps_for_call_data( &mut self, diff --git a/bus-mapping/src/error.rs b/bus-mapping/src/error.rs index 86e2d47149..313ed43652 100644 --- a/bus-mapping/src/error.rs +++ b/bus-mapping/src/error.rs @@ -94,6 +94,11 @@ pub enum OogError { SloadSstore, /// Out of Gas for CALL, CALLCODE, DELEGATECALL and STATICCALL Call, + /// Out of Gas for Precompile. + /// ecrecover/ecadd/ecmul/ecpairing/identity oog should be handled by this. + /// modexp oog is handled inside modexp gadget. + /// disabled precompiles are handled by PrecompileFailedGadget. + Precompile, /// Out of Gas for CREATE and CREATE2 Create, /// Out of Gas for SELFDESTRUCT @@ -193,6 +198,8 @@ pub enum ExecError { CodeStoreOutOfGas, /// For RETURN in a CREATE, CREATE2 MaxCodeSizeExceeded, + /// For CALL, CALLCODE, DELEGATECALL, STATICCALL + UnimplementedPrecompiles, /// For CREATE, CREATE2 NonceUintOverflow(NonceUintOverflowError), } diff --git a/bus-mapping/src/evm.rs b/bus-mapping/src/evm.rs index 2441287852..d7062eed9c 100644 --- a/bus-mapping/src/evm.rs +++ b/bus-mapping/src/evm.rs @@ -4,3 +4,5 @@ pub(crate) mod opcodes; pub use eth_types::evm_types::opcode_ids::OpcodeId; pub use opcodes::Opcode; +#[cfg(any(feature = "test", test))] +pub use opcodes::PrecompileCallArgs; diff --git a/bus-mapping/src/evm/opcodes.rs b/bus-mapping/src/evm/opcodes.rs index dfed377a36..7e2812351a 100644 --- a/bus-mapping/src/evm/opcodes.rs +++ b/bus-mapping/src/evm/opcodes.rs @@ -53,11 +53,15 @@ mod error_oog_call; mod error_oog_exp; mod error_oog_log; mod error_oog_memory_copy; +mod error_oog_precompile; mod error_oog_sload_sstore; +mod error_precompile_failed; mod error_return_data_outofbound; mod error_simple; mod error_write_protection; +mod precompiles; + #[cfg(test)] mod memory_expansion_test; @@ -106,6 +110,9 @@ use stackonlyop::StackOnlyOpcode; use stop::Stop; use swap::Swap; +#[cfg(feature = "test")] +pub use crate::precompile::PrecompileCallArgs; + /// Generic opcode trait which defines the logic of the /// [`Operation`](crate::operation::Operation) that should be generated for one /// or multiple [`ExecStep`](crate::circuit_input_builder::ExecStep) depending @@ -394,7 +401,7 @@ pub fn gen_associated_ops( need_restore = false; } - state.handle_return(&mut exec_step, geth_steps, need_restore)?; + state.handle_return(&mut [&mut exec_step], geth_steps, need_restore)?; return Ok(vec![exec_step]); } } @@ -503,6 +510,6 @@ fn dummy_gen_selfdestruct_ops( state.sdb.destruct_account(sender); } - state.handle_return(&mut exec_step, geth_steps, !state.call()?.is_root)?; + state.handle_return(&mut [&mut exec_step], geth_steps, !state.call()?.is_root)?; Ok(vec![exec_step]) } diff --git a/bus-mapping/src/evm/opcodes/begin_end_tx.rs b/bus-mapping/src/evm/opcodes/begin_end_tx.rs index 04decb43e1..52e6b2eb0c 100644 --- a/bus-mapping/src/evm/opcodes/begin_end_tx.rs +++ b/bus-mapping/src/evm/opcodes/begin_end_tx.rs @@ -7,7 +7,7 @@ use crate::{ }; use eth_types::{ evm_types::{GasCost, MAX_REFUND_QUOTIENT_OF_GAS_USED, PRECOMPILE_COUNT}, - evm_unimplemented, ToWord, Word, + ToWord, Word, }; use ethers_core::utils::get_contract_address; @@ -196,13 +196,9 @@ fn gen_begin_tx_steps(state: &mut CircuitInputStateRef) -> Result { - evm_unimplemented!("Call to precompiled is left unimplemented"); - Ok(exec_step) - } + (_, true, _) => (), (_, _, is_empty_code_hash) => { // 3. Call to account with empty code. if is_empty_code_hash { @@ -236,10 +232,15 @@ fn gen_begin_tx_steps(state: &mut CircuitInputStateRef) -> Result Result { diff --git a/bus-mapping/src/evm/opcodes/callop.rs b/bus-mapping/src/evm/opcodes/callop.rs index c2112e8ab3..316bfe454d 100644 --- a/bus-mapping/src/evm/opcodes/callop.rs +++ b/bus-mapping/src/evm/opcodes/callop.rs @@ -1,15 +1,21 @@ use super::Opcode; use crate::{ - circuit_input_builder::{CallKind, CircuitInputStateRef, CodeSource, ExecStep}, + circuit_input_builder::{ + CallKind, CircuitInputStateRef, CodeSource, CopyDataType, CopyEvent, ExecStep, NumberOrHash, + }, + evm::opcodes::{ + error_oog_precompile::ErrorOOGPrecompile, + precompiles::gen_associated_ops as precompile_associated_ops, + }, operation::{AccountField, CallContextField, TxAccessListAccountOp}, - precompile::{execute_precompiled, is_precompiled}, + precompile::{execute_precompiled, is_precompiled, PrecompileCalls}, state_db::CodeDB, Error, }; use eth_types::{ evm_types::{ gas_utils::{eip150_gas, memory_expansion_gas_cost}, - GasCost, + GasCost, OpcodeId, GAS_STIPEND_CALL_WITH_VALUE, }, GethExecStep, ToWord, Word, }; @@ -208,6 +214,7 @@ impl Opcode for CallOpcode { let has_value = !call.value.is_zero() && !call.is_delegatecall(); let memory_expansion_gas_cost = memory_expansion_gas_cost(curr_memory_word_size, next_memory_word_size); + let gas_cost = if is_warm { GasCost::WARM_ACCESS } else { @@ -223,37 +230,126 @@ impl Opcode for CallOpcode { 0 } + memory_expansion_gas_cost; let gas_specified = geth_step.stack.last()?; + + let stipend = if has_value { + GAS_STIPEND_CALL_WITH_VALUE + } else { + 0 + }; let callee_gas_left = eip150_gas(geth_step.gas - gas_cost, gas_specified); + let callee_gas_left_with_stipend = callee_gas_left + stipend; // There are 4 branches from here. // add failure case for insufficient balance or error depth in the future. + if geth_steps[0].op == OpcodeId::CALL + && geth_steps[1].depth == geth_steps[0].depth + 1 + && geth_steps[1].gas != callee_gas_left_with_stipend + { + // panic with full info + let info1 = format!("callee_gas_left {callee_gas_left} gas_specified {gas_specified} gas_cost {gas_cost} is_warm {is_warm} has_value {has_value} current_memory_word_size {curr_memory_word_size} next_memory_word_size {next_memory_word_size}, memory_expansion_gas_cost {memory_expansion_gas_cost}"); + let info2 = format!("args gas:{:?} addr:{:?} value:{:?} cd_pos:{:?} cd_len:{:?} rd_pos:{:?} rd_len:{:?}", + geth_step.stack.nth_last(0), + geth_step.stack.nth_last(1), + geth_step.stack.nth_last(2), + geth_step.stack.nth_last(3), + geth_step.stack.nth_last(4), + geth_step.stack.nth_last(5), + geth_step.stack.nth_last(6) + ); + let full_ctx = format!( + "step0 {:?} step1 {:?} call {:?}, {} {}", + geth_steps[0], geth_steps[1], call, info1, info2 + ); + debug_assert_eq!( + geth_steps[1].gas, callee_gas_left_with_stipend, + "{full_ctx}" + ); + } + match (is_precheck_ok, is_precompile, is_empty_code_hash) { // 1. Call to precompiled. (true, true, _) => { - let caller_ctx = state.caller_ctx_mut()?; let code_address = code_address.unwrap(); - let (result, contract_gas_cost) = execute_precompiled( + let precompile_call: PrecompileCalls = code_address.0[19].into(); + + // get the result of the precompile call. + // For failed call, it will cost all gas provided + let (result, precompile_call_gas_cost, has_oog_err) = execute_precompiled( &code_address, if args_length != 0 { - &caller_ctx.memory.0[args_offset..args_offset + args_length] + let caller_memory = &state.caller_ctx()?.memory; + &caller_memory.0[args_offset..args_offset + args_length] } else { &[] }, - callee_gas_left, + callee_gas_left_with_stipend, ); - log::trace!( - "precompile return data len {} gas {}", - result.len(), - contract_gas_cost - ); - caller_ctx.return_data = result.clone(); + + // mutate the callee memory by at least the precompile call's result that will be + // written from memory addr 0 to memory addr result.len() + state.call_ctx_mut()?.memory.extend_at_least(result.len()); + + state.caller_ctx_mut()?.return_data = result.clone(); + let length = min(result.len(), ret_length); - if length != 0 { - caller_ctx.memory.extend_at_least(ret_offset + length); + + if length > 0 { + state + .caller_ctx_mut()? + .memory + .extend_at_least(ret_offset + length); + } + + for (field, value) in [ + ( + CallContextField::IsSuccess, + Word::from(call.is_success as u64), + ), + ( + CallContextField::CalleeAddress, + call.code_address().unwrap().to_word(), + ), + (CallContextField::CallerId, call.caller_id.into()), + ( + CallContextField::CallDataOffset, + call.call_data_offset.into(), + ), + ( + CallContextField::CallDataLength, + call.call_data_length.into(), + ), + ( + CallContextField::ReturnDataOffset, + call.return_data_offset.into(), + ), + ( + CallContextField::ReturnDataLength, + call.return_data_length.into(), + ), + ] { + state.call_context_write(&mut exec_step, call.call_id, field, value)?; } + + let caller_ctx = state.caller_ctx_mut()?; caller_ctx.memory.0[ret_offset..ret_offset + length] .copy_from_slice(&result[..length]); + + // return while restoring some of caller's context. for (field, value) in [ + (CallContextField::ProgramCounter, (geth_step.pc + 1).into()), + ( + CallContextField::StackPointer, + (geth_step.stack.stack_pointer().0 + N_ARGS - 1).into(), + ), + ( + CallContextField::GasLeft, + (geth_step.gas - gas_cost - callee_gas_left).into(), + ), + (CallContextField::MemorySize, next_memory_word_size.into()), + ( + CallContextField::ReversibleWriteCounter, + (exec_step.reversible_write_counter + 1).into(), + ), (CallContextField::LastCalleeId, call.call_id.into()), (CallContextField::LastCalleeReturnDataOffset, 0.into()), ( @@ -264,20 +360,130 @@ impl Opcode for CallOpcode { state.call_context_write(&mut exec_step, current_call.call_id, field, value)?; } - log::warn!("missing circuit part of precompile"); - state.handle_return(&mut exec_step, geth_steps, false)?; + // insert a copy event (input) for this step and generate memory op + let rw_counter_start = state.block_ctx.rwc; + if call.call_data_length > 0 { + let n_input_bytes = if let Some(input_len) = precompile_call.input_len() { + min(input_len, call.call_data_length as usize) + } else { + call.call_data_length as usize + }; + + let input_bytes = state.gen_copy_steps_for_precompile_calldata( + &mut exec_step, + call.call_data_offset, + n_input_bytes as u64, + )?; + + state.push_copy( + &mut exec_step, + CopyEvent { + src_id: NumberOrHash::Number(call.caller_id), + src_type: CopyDataType::Memory, + src_addr: call.call_data_offset, + src_addr_end: call.call_data_offset + n_input_bytes as u64, + dst_id: NumberOrHash::Number(call.call_id), + dst_type: CopyDataType::RlcAcc, + dst_addr: 0, + log_id: None, + rw_counter_start, + bytes: input_bytes.iter().map(|s| (*s, false)).collect(), + }, + ); + } + + // write the result in the callee's memory + let rw_counter_start = state.block_ctx.rwc; + if call.is_success && !result.is_empty() { + let (output_bytes, _prev_bytes) = state + .gen_copy_steps_for_precompile_callee_memory(&mut exec_step, &result)?; + + state.push_copy( + &mut exec_step, + CopyEvent { + src_id: NumberOrHash::Number(call.call_id), + src_type: CopyDataType::RlcAcc, + src_addr: 0, + src_addr_end: output_bytes.len() as u64, + dst_id: NumberOrHash::Number(call.call_id), + dst_type: CopyDataType::Memory, + dst_addr: 0, + log_id: None, + rw_counter_start, + bytes: output_bytes.iter().map(|s| (*s, false)).collect(), + }, + ); + } - let real_cost = geth_steps[0].gas - geth_steps[1].gas; - if real_cost != exec_step.gas_cost { - log::warn!( - "precompile gas fixed from {} to {}, step {:?}", - exec_step.gas_cost, - real_cost, - geth_steps[0] + // insert another copy event (output) for this step. + let rw_counter_start = state.block_ctx.rwc; + if call.is_success && length > 0 { + let return_bytes = state.gen_copy_steps_for_precompile_returndata( + &mut exec_step, + call.return_data_offset, + length, + &result, + )?; + state.push_copy( + &mut exec_step, + CopyEvent { + src_id: NumberOrHash::Number(call.call_id), + src_type: CopyDataType::Memory, + src_addr: 0, + src_addr_end: length as u64, + dst_id: NumberOrHash::Number(call.caller_id), + dst_type: CopyDataType::Memory, + dst_addr: call.return_data_offset, + log_id: None, + rw_counter_start, + bytes: return_bytes.iter().map(|s| (*s, false)).collect(), + }, ); } - exec_step.gas_cost = real_cost; - Ok(vec![exec_step]) + + if has_oog_err { + let mut oog_step = ErrorOOGPrecompile::gen_associated_ops( + state, + &geth_steps[1], + call.clone(), + )?; + oog_step.gas_left = callee_gas_left_with_stipend; + oog_step.gas_cost = precompile_call_gas_cost; + // Make the Precompile execution step to handle return logic and restore to + // caller context (similar as STOP and RETURN). + state.handle_return(&mut [&mut exec_step, &mut oog_step], geth_steps, true)?; + + Ok(vec![exec_step, oog_step]) + } else { + let precompile_call: PrecompileCalls = code_address.0[19].into(); + + let mut precompile_step = precompile_associated_ops( + state, + geth_steps[1].clone(), + call.clone(), + precompile_call, + )?; + + // Set gas left and gas cost for precompile step. + precompile_step.gas_left = callee_gas_left_with_stipend; + precompile_step.gas_cost = precompile_call_gas_cost; + // Make the Precompile execution step to handle return logic and restore to + // caller context (similar as STOP and RETURN). + state.handle_return( + &mut [&mut exec_step, &mut precompile_step], + geth_steps, + true, + )?; + + debug_assert_eq!( + geth_steps[0].gas - gas_cost - precompile_call_gas_cost + stipend, + geth_steps[1].gas, + "precompile_call_gas_cost wrong {:?}", + precompile_step.exec_state + ); + + Ok(vec![exec_step, precompile_step]) + } } // 2. Call to account with empty code. (true, _, true) => { @@ -288,7 +494,8 @@ impl Opcode for CallOpcode { ] { state.call_context_write(&mut exec_step, current_call.call_id, field, value)?; } - state.handle_return(&mut exec_step, geth_steps, false)?; + state.caller_ctx_mut()?.return_data.clear(); + state.handle_return(&mut [&mut exec_step], geth_steps, false)?; Ok(vec![exec_step]) } // 3. Call to account with non-empty code. @@ -352,7 +559,6 @@ impl Opcode for CallOpcode { Ok(vec![exec_step]) } - // 4. insufficient balance or error depth cases. (false, _, _) => { for (field, value) in [ @@ -362,382 +568,9 @@ impl Opcode for CallOpcode { ] { state.call_context_write(&mut exec_step, current_call.call_id, field, value)?; } - state.handle_return(&mut exec_step, geth_steps, false)?; + state.caller_ctx_mut()?.return_data.clear(); + state.handle_return(&mut [&mut exec_step], geth_steps, false)?; Ok(vec![exec_step]) - } // - } - } -} - -#[cfg(test)] -mod tests { - use crate::mock::BlockData; - use eth_types::{bytecode, evm_types::OpcodeId, geth_types::GethData, word, Bytecode, Word}; - use mock::{ - test_ctx::{ - helpers::{account_0_code_account_1_no_code, tx_from_1_to_0}, - LoggerConfig, - }, - TestContext, - }; - - // move this to circuit after circuit part is complete - #[test] - fn test_precompiled_call() { - struct PrecompileCall { - name: &'static str, - setup_code: Bytecode, - ret_size: Word, - ret_offset: Word, - call_data_offset: Word, - call_data_length: Word, - address: Word, - value: Word, - gas: Word, - stack_value: Vec<(Word, Word)>, - } - - impl Default for PrecompileCall { - fn default() -> Self { - PrecompileCall { - name: "precompiled call", - setup_code: Bytecode::default(), - ret_size: Word::from(0), - ret_offset: Word::from(0), - call_data_offset: Word::from(0), - call_data_length: Word::from(0), - address: Word::from(0), - value: Word::from(0), - gas: Word::from(0xFFFFFFF), - stack_value: vec![], - } - } - } - - impl PrecompileCall { - fn with_call_op(&self, call_op: OpcodeId) -> Bytecode { - assert!( - call_op.is_call(), - "invalid setup, {:?} is not a call op", - call_op - ); - let mut code = self.setup_code.clone(); - code.push(32, self.ret_size) - .push(32, self.ret_offset) - .push(32, self.call_data_length) - .push(32, self.call_data_offset); - if call_op == OpcodeId::CALL || call_op == OpcodeId::CALLCODE { - code.push(32, self.value); - } - code.push(32, self.address) - .push(32, self.gas) - .write_op(call_op) - .write_op(OpcodeId::POP); - for (offset, _) in self.stack_value.iter().rev() { - code.push(32, *offset).write_op(OpcodeId::MLOAD); - } - - code - } - } - - let test_vector = [ - PrecompileCall { - name: "ecRecover", - setup_code: bytecode! { - PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) // hash - PUSH1(0x0) - MSTORE - PUSH1(28) // v - PUSH1(0x20) - MSTORE - PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) // r - PUSH1(0x40) - MSTORE - PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) // s - PUSH1(0x60) - MSTORE - }, - ret_size: Word::from(0x20), - ret_offset: Word::from(0x80), - call_data_length: Word::from(0x80), - address: Word::from(0x1), - stack_value: vec![( - Word::from(0x80), - word!("7156526fbd7a3c72969b54f64e42c10fbb768c8a"), - )], - ..Default::default() - }, - PrecompileCall { - name: "SHA2-256", - setup_code: bytecode! { - PUSH1(0xFF) // data - PUSH1(0) - MSTORE - }, - ret_size: Word::from(0x20), - ret_offset: Word::from(0x20), - call_data_length: Word::from(0x1), - call_data_offset: Word::from(0x1F), - address: Word::from(0x2), - stack_value: vec![( - Word::from(0x20), - word!("a8100ae6aa1940d0b663bb31cd466142ebbdbd5187131b92d93818987832eb89"), - )], - ..Default::default() - }, - PrecompileCall { - name: "RIPEMD-160", - setup_code: bytecode! { - PUSH1(0xFF) // data - PUSH1(0) - MSTORE - }, - ret_size: Word::from(0x20), - ret_offset: Word::from(0x20), - call_data_length: Word::from(0x1), - call_data_offset: Word::from(0x1F), - address: Word::from(0x3), - stack_value: vec![( - Word::from(0x20), - word!("2c0c45d3ecab80fe060e5f1d7057cd2f8de5e557"), - )], - ..Default::default() - }, - PrecompileCall { - name: "identity", - setup_code: bytecode! { - PUSH16(word!("0123456789ABCDEF0123456789ABCDEF")) - PUSH1(0x00) - MSTORE - }, - ret_size: Word::from(0x20), - ret_offset: Word::from(0x20), - call_data_length: Word::from(0x20), - address: Word::from(0x4), - stack_value: vec![(Word::from(0x20), word!("0123456789ABCDEF0123456789ABCDEF"))], - ..Default::default() - }, - PrecompileCall { - name: "modexp", - setup_code: bytecode! { - PUSH1(1) // Bsize - PUSH1(0) - MSTORE - PUSH1(1) // Esize - PUSH1(0x20) - MSTORE - PUSH1(1) // Msize - PUSH1(0x40) - MSTORE - PUSH32(word!("0x08090A0000000000000000000000000000000000000000000000000000000000")) // B, E and M - PUSH1(0x60) - MSTORE - }, - ret_size: Word::from(0x01), - ret_offset: Word::from(0x9F), - call_data_length: Word::from(0x63), - address: Word::from(0x5), - stack_value: vec![(Word::from(0x80), Word::from(8))], - ..Default::default() - }, - PrecompileCall { - name: "ecAdd", - setup_code: bytecode! { - PUSH1(1) // x1 - PUSH1(0) - MSTORE - PUSH1(2) // y1 - PUSH1(0x20) - MSTORE - PUSH1(1) // x2 - PUSH1(0x40) - MSTORE - PUSH1(2) // y2 - PUSH1(0x60) - MSTORE - }, - ret_size: Word::from(0x40), - ret_offset: Word::from(0x80), - call_data_length: Word::from(0x80), - address: Word::from(0x6), - stack_value: vec![ - ( - Word::from(0x80), - word!("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"), - ), - ( - Word::from(0xA0), - word!("15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"), - ), - ], - ..Default::default() - }, - PrecompileCall { - name: "ecMul", - setup_code: bytecode! { - PUSH1(1) // x1 - PUSH1(0) - MSTORE - PUSH1(2) // y1 - PUSH1(0x20) - MSTORE - PUSH1(2) // s - PUSH1(0x40) - MSTORE - }, - ret_size: Word::from(0x40), - ret_offset: Word::from(0x60), - call_data_length: Word::from(0x60), - address: Word::from(0x7), - stack_value: vec![ - ( - Word::from(0x60), - word!("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"), - ), - ( - Word::from(0x80), - word!("15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"), - ), - ], - ..Default::default() - }, - PrecompileCall { - name: "ecPairing", - setup_code: bytecode! { - PUSH32(word!("0x23a8eb0b0996252cb548a4487da97b02422ebc0e834613f954de6c7e0afdc1fc")) - PUSH32(word!("0x2a23af9a5ce2ba2796c1f4e453a370eb0af8c212d9dc9acd8fc02c2e907baea2")) - PUSH32(word!("0x091058a3141822985733cbdddfed0fd8d6c104e9e9eff40bf5abfef9ab163bc7")) - PUSH32(word!("0x1971ff0471b09fa93caaf13cbf443c1aede09cc4328f5a62aad45f40ec133eb4")) - PUSH32(word!("0x30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd45")) - PUSH32(word!("0x0000000000000000000000000000000000000000000000000000000000000001")) - PUSH32(word!("0x2fe02e47887507adf0ff1743cbac6ba291e66f59be6bd763950bb16041a0a85e")) - PUSH32(word!("0x2bd368e28381e8eccb5fa81fc26cf3f048eea9abfdd85d7ed3ab3698d63e4f90")) - PUSH32(word!("0x22606845ff186793914e03e21df544c34ffe2f2f3504de8a79d9159eca2d98d9")) - PUSH32(word!("0x1fb19bb476f6b9e44e2a32234da8212f61cd63919354bc06aef31e3cfaff3ebc")) - PUSH32(word!("0x2c0f001f52110ccfe69108924926e45f0b0c868df0e7bde1fe16d3242dc715f6")) - PUSH32(word!("0x2cf44499d5d27bb186308b7af7af02ac5bc9eeb6a3d147c186b21fb1b76e18da")) - - PUSH1(12) - PUSH2(0x200) - MSTORE - - JUMPDEST - - PUSH2(0x200) - MLOAD - PUSH1(12) - SUB - PUSH1(0x20) - MUL - MSTORE - PUSH1(1) - PUSH2(0x200) - MLOAD - SUB - DUP1 - PUSH2(0x200) - MSTORE - PUSH2(0x192) - JUMPI - }, - ret_size: Word::from(0x20), - call_data_length: Word::from(0x180), - address: Word::from(0x8), - stack_value: vec![(Word::from(0x0), Word::from(1))], - ..Default::default() - }, - PrecompileCall { - name: "blake2f", - setup_code: bytecode! { - PUSH32(word!("0000000003000000000000000000000000000000010000000000000000000000")) - PUSH32(word!("0000000000000000000000000000000000000000000000000000000000000000")) - PUSH32(word!("0000000000000000000000000000000000000000000000000000000000000000")) - PUSH32(word!("0000000000000000000000000000000000000000000000000000000000000000")) - PUSH32(word!("19cde05b61626300000000000000000000000000000000000000000000000000")) - PUSH32(word!("3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbabd9831f79217e13")) - PUSH32(word!("0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f")) - - PUSH1(7) - PUSH2(0x160) - MSTORE - - JUMPDEST - - PUSH2(0x160) - MLOAD - PUSH1(7) - SUB - PUSH1(0x20) - MUL - MSTORE - PUSH1(1) - PUSH2(0x160) - MLOAD - SUB - DUP1 - PUSH2(0x160) - MSTORE - PUSH2(0xed) - JUMPI - }, - ret_size: Word::from(0x40), - ret_offset: Word::from(0x0), - call_data_length: Word::from(0xd5), - address: Word::from(0x9), - stack_value: vec![ - ( - Word::from(0x20), - word!("d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079217e1319cde05b"), - ), - ( - Word::from(0x0), - word!("8c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5"), - ), - ], - ..Default::default() - }, - ]; - - let call_ops = [ - OpcodeId::CALL, - OpcodeId::CALLCODE, - OpcodeId::DELEGATECALL, - OpcodeId::STATICCALL, - ]; - - for (test_call, call_op) in itertools::iproduct!(test_vector.iter(), call_ops.iter()) { - let code = test_call.with_call_op(*call_op); - let block: GethData = TestContext::<2, 1>::new_with_logger_config( - None, - account_0_code_account_1_no_code(code), - tx_from_1_to_0, - |block, _tx| block.number(0xcafeu64), - LoggerConfig { - enable_memory: true, - ..Default::default() - }, - ) - .unwrap() - .into(); - - let builder = BlockData::new_from_geth_data(block.clone()).new_circuit_input_builder(); - builder - .handle_block(&block.eth_block, &block.geth_traces) - .unwrap(); - - let step = block.geth_traces[0] - .struct_logs - .last() - .expect("at least one step"); - log::debug!("{:?}", step.stack); - for (offset, (_, stack_value)) in test_call.stack_value.iter().enumerate() { - assert_eq!( - *stack_value, - step.stack.nth_last(offset).expect("stack value not found"), - "stack output mismatch {}", - test_call.name - ); } } } diff --git a/bus-mapping/src/evm/opcodes/create.rs b/bus-mapping/src/evm/opcodes/create.rs index 7fc24afb7f..51b399b099 100644 --- a/bus-mapping/src/evm/opcodes/create.rs +++ b/bus-mapping/src/evm/opcodes/create.rs @@ -269,7 +269,7 @@ impl Opcode for Create { ] { state.call_context_write(&mut exec_step, caller.call_id, field, value)?; } - state.handle_return(&mut exec_step, geth_steps, false)?; + state.handle_return(&mut [&mut exec_step], geth_steps, false)?; }; } // failed case: is_precheck_ok is false or is_address_collision is true @@ -281,7 +281,7 @@ impl Opcode for Create { ] { state.call_context_write(&mut exec_step, caller.call_id, field, value)?; } - state.handle_return(&mut exec_step, geth_steps, false)?; + state.handle_return(&mut [&mut exec_step], geth_steps, false)?; } Ok(vec![exec_step]) diff --git a/bus-mapping/src/evm/opcodes/error_code_store.rs b/bus-mapping/src/evm/opcodes/error_code_store.rs index eff7d37311..f407331c7c 100644 --- a/bus-mapping/src/evm/opcodes/error_code_store.rs +++ b/bus-mapping/src/evm/opcodes/error_code_store.rs @@ -42,7 +42,7 @@ impl Opcode for ErrorCodeStore { // create context check assert!(state.call()?.is_create()); - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_invalid_creation_code.rs b/bus-mapping/src/evm/opcodes/error_invalid_creation_code.rs index 0bcb5f474d..54d6314c66 100644 --- a/bus-mapping/src/evm/opcodes/error_invalid_creation_code.rs +++ b/bus-mapping/src/evm/opcodes/error_invalid_creation_code.rs @@ -28,10 +28,10 @@ impl Opcode for ErrorCreationCode { // Read the first byte of init code and check it must be 0xef for this error. let init_code_first_byte = state.call_ctx()?.memory.0[offset.as_usize()]; - state.memory_read(&mut exec_step, offset.try_into()?, init_code_first_byte)?; + state.memory_read(&mut exec_step, offset.try_into()?)?; assert_eq!(init_code_first_byte, INVALID_INIT_CODE_FIRST_BYTE); - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_invalid_jump.rs b/bus-mapping/src/evm/opcodes/error_invalid_jump.rs index 0e72a9b949..178b54f618 100644 --- a/bus-mapping/src/evm/opcodes/error_invalid_jump.rs +++ b/bus-mapping/src/evm/opcodes/error_invalid_jump.rs @@ -42,7 +42,7 @@ impl Opcode for InvalidJump { } // `IsSuccess` call context operation is added in handle_return - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_account_access.rs b/bus-mapping/src/evm/opcodes/error_oog_account_access.rs index f97f2a3d06..726a0cb7f6 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_account_access.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_account_access.rs @@ -54,7 +54,7 @@ impl Opcode for ErrorOOGAccountAccess { )?; // common error handling - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_call.rs b/bus-mapping/src/evm/opcodes/error_oog_call.rs index 8b69d070e7..8210dd6329 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_call.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_call.rs @@ -90,7 +90,7 @@ impl Opcode for OOGCall { }, )?; - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_exp.rs b/bus-mapping/src/evm/opcodes/error_oog_exp.rs index a0d7f5bdc2..23d7d7c76d 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_exp.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_exp.rs @@ -30,7 +30,7 @@ impl Opcode for OOGExp { )?; } - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_log.rs b/bus-mapping/src/evm/opcodes/error_oog_log.rs index 1e6223238c..01c6992b14 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_log.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_log.rs @@ -44,7 +44,7 @@ impl Opcode for ErrorOOGLog { Word::from(state.call()?.is_static as u8), )?; - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_memory_copy.rs b/bus-mapping/src/evm/opcodes/error_oog_memory_copy.rs index 05554104ce..50327115ba 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_memory_copy.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_memory_copy.rs @@ -67,7 +67,7 @@ impl Opcode for OOGMemoryCopy { )?; } - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_oog_precompile.rs b/bus-mapping/src/evm/opcodes/error_oog_precompile.rs new file mode 100644 index 0000000000..8c81106bca --- /dev/null +++ b/bus-mapping/src/evm/opcodes/error_oog_precompile.rs @@ -0,0 +1,37 @@ +use crate::{ + circuit_input_builder::{Call, CircuitInputStateRef, ExecStep}, + error::{ExecError, OogError}, + operation::CallContextField, + Error, +}; +use eth_types::{GethExecStep, ToWord}; + +#[derive(Copy, Clone, Debug)] +pub struct ErrorOOGPrecompile; + +impl ErrorOOGPrecompile { + pub fn gen_associated_ops( + state: &mut CircuitInputStateRef, + geth_step: &GethExecStep, + call: Call, + ) -> Result { + let mut exec_step = state.new_step(geth_step)?; + exec_step.error = Some(ExecError::OutOfGas(OogError::Precompile)); + + // callee_address + state.call_context_read( + &mut exec_step, + call.call_id, + CallContextField::CalleeAddress, + call.code_address().unwrap().to_word(), + )?; + state.call_context_read( + &mut exec_step, + call.call_id, + CallContextField::CallDataLength, + call.call_data_length.into(), + )?; + + Ok(exec_step) + } +} diff --git a/bus-mapping/src/evm/opcodes/error_oog_sload_sstore.rs b/bus-mapping/src/evm/opcodes/error_oog_sload_sstore.rs index bb873e8aa6..285ba59607 100644 --- a/bus-mapping/src/evm/opcodes/error_oog_sload_sstore.rs +++ b/bus-mapping/src/evm/opcodes/error_oog_sload_sstore.rs @@ -89,7 +89,7 @@ impl Opcode for OOGSloadSstore { )?; } - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_precompile_failed.rs b/bus-mapping/src/evm/opcodes/error_precompile_failed.rs new file mode 100644 index 0000000000..3e4783fd6a --- /dev/null +++ b/bus-mapping/src/evm/opcodes/error_precompile_failed.rs @@ -0,0 +1,66 @@ +use crate::{ + circuit_input_builder::{CircuitInputStateRef, ExecStep}, + error::ExecError, + evm::Opcode, + operation::CallContextField, + Error, +}; +use eth_types::{evm_types::OpcodeId, GethExecStep}; + +#[derive(Debug, Copy, Clone)] +pub(crate) struct PrecompileFailed; + +impl Opcode for PrecompileFailed { + fn gen_associated_ops( + state: &mut CircuitInputStateRef, + geth_steps: &[GethExecStep], + ) -> Result, Error> { + let geth_step = &geth_steps[0]; + let stack_input_num = match geth_step.op { + OpcodeId::CALL | OpcodeId::CALLCODE => 7, + OpcodeId::DELEGATECALL | OpcodeId::STATICCALL => 6, + op => unreachable!("{op} should not happen in PrecompileFailed"), + }; + + let mut exec_step = state.new_step(geth_step)?; + exec_step.error = Some(ExecError::UnimplementedPrecompiles); + + let args_offset = geth_step.stack.nth_last(stack_input_num - 4)?.as_usize(); + let args_length = geth_step.stack.nth_last(stack_input_num - 3)?.as_usize(); + let ret_offset = geth_step.stack.nth_last(stack_input_num - 2)?.as_usize(); + let ret_length = geth_step.stack.nth_last(stack_input_num - 1)?.as_usize(); + + // we need to keep the memory until parse_call complete + state.call_expand_memory(args_offset, args_length, ret_offset, ret_length)?; + + let call = state.parse_call(geth_step)?; + state.push_call(call.clone()); + state.caller_ctx_mut()?.return_data.clear(); + state.handle_return(&mut [&mut exec_step], geth_steps, false)?; + + for i in 0..stack_input_num { + state.stack_read( + &mut exec_step, + geth_step.stack.nth_last_filled(i), + geth_step.stack.nth_last(i)?, + )?; + } + + state.stack_write( + &mut exec_step, + geth_step.stack.nth_last_filled(stack_input_num - 1), + // Must fail. + (0_u64).into(), + )?; + + for (field, value) in [ + (CallContextField::LastCalleeId, call.call_id.into()), + (CallContextField::LastCalleeReturnDataOffset, 0.into()), + (CallContextField::LastCalleeReturnDataLength, 0.into()), + ] { + state.call_context_write(&mut exec_step, call.caller_id, field, value)?; + } + + Ok(vec![exec_step]) + } +} diff --git a/bus-mapping/src/evm/opcodes/error_return_data_outofbound.rs b/bus-mapping/src/evm/opcodes/error_return_data_outofbound.rs index b03ed28b19..1460cba3f7 100644 --- a/bus-mapping/src/evm/opcodes/error_return_data_outofbound.rs +++ b/bus-mapping/src/evm/opcodes/error_return_data_outofbound.rs @@ -69,7 +69,7 @@ impl Opcode for ErrorReturnDataOutOfBound { )?; // `IsSuccess` call context operation is added in handle_return - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_simple.rs b/bus-mapping/src/evm/opcodes/error_simple.rs index c27dd06e33..8964896897 100644 --- a/bus-mapping/src/evm/opcodes/error_simple.rs +++ b/bus-mapping/src/evm/opcodes/error_simple.rs @@ -22,7 +22,7 @@ impl Opcode for ErrorSimple { let next_step = geth_steps.get(1); exec_step.error = state.get_step_err(geth_step, next_step).unwrap(); - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/error_write_protection.rs b/bus-mapping/src/evm/opcodes/error_write_protection.rs index 86a02385b1..2c9bd0b87e 100644 --- a/bus-mapping/src/evm/opcodes/error_write_protection.rs +++ b/bus-mapping/src/evm/opcodes/error_write_protection.rs @@ -63,7 +63,7 @@ impl Opcode for ErrorWriteProtection { )?; // `IsSuccess` call context operation is added in handle_return - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/logs.rs b/bus-mapping/src/evm/opcodes/logs.rs index 6fd12ed736..abe43b46ad 100644 --- a/bus-mapping/src/evm/opcodes/logs.rs +++ b/bus-mapping/src/evm/opcodes/logs.rs @@ -146,7 +146,7 @@ fn gen_copy_steps( let addr = src_addr + idx as u64; // Read memory - state.memory_read(exec_step, (addr as usize).into(), *byte)?; + state.memory_read(exec_step, (addr as usize).into())?; copy_steps.push((*byte, false)); diff --git a/bus-mapping/src/evm/opcodes/mload.rs b/bus-mapping/src/evm/opcodes/mload.rs index 72a805ba77..621428f19d 100644 --- a/bus-mapping/src/evm/opcodes/mload.rs +++ b/bus-mapping/src/evm/opcodes/mload.rs @@ -40,8 +40,8 @@ impl Opcode for Mload { // First mem read -> 32 MemoryOp generated. // - for byte in mem_read_value.to_be_bytes() { - state.memory_read(&mut exec_step, mem_read_addr, byte)?; + for _ in mem_read_value.to_be_bytes() { + state.memory_read(&mut exec_step, mem_read_addr)?; // Update mem_read_addr to next byte's one mem_read_addr += MemoryAddress::from(1); diff --git a/bus-mapping/src/evm/opcodes/precompiles/mod.rs b/bus-mapping/src/evm/opcodes/precompiles/mod.rs new file mode 100644 index 0000000000..70e62ce54c --- /dev/null +++ b/bus-mapping/src/evm/opcodes/precompiles/mod.rs @@ -0,0 +1,61 @@ +use eth_types::{GethExecStep, ToWord, Word}; + +use crate::{ + circuit_input_builder::{Call, CircuitInputStateRef, ExecState, ExecStep}, + operation::CallContextField, + precompile::PrecompileCalls, + Error, +}; + +pub fn gen_associated_ops( + state: &mut CircuitInputStateRef, + geth_step: GethExecStep, + call: Call, + precompile: PrecompileCalls, +) -> Result { + assert_eq!(call.code_address(), Some(precompile.into())); + let mut exec_step = state.new_step(&geth_step)?; + exec_step.exec_state = ExecState::Precompile(precompile); + + common_call_ctx_reads(state, &mut exec_step, &call)?; + + Ok(exec_step) +} + +fn common_call_ctx_reads( + state: &mut CircuitInputStateRef, + exec_step: &mut ExecStep, + call: &Call, +) -> Result<(), Error> { + for (field, value) in [ + ( + CallContextField::IsSuccess, + Word::from(call.is_success as u64), + ), + ( + CallContextField::CalleeAddress, + call.code_address().unwrap().to_word(), + ), + (CallContextField::CallerId, call.caller_id.into()), + ( + CallContextField::CallDataOffset, + call.call_data_offset.into(), + ), + ( + CallContextField::CallDataLength, + call.call_data_length.into(), + ), + ( + CallContextField::ReturnDataOffset, + call.return_data_offset.into(), + ), + ( + CallContextField::ReturnDataLength, + call.return_data_length.into(), + ), + ] { + state.call_context_read(exec_step, call.call_id, field, value)?; + } + + Ok(()) +} diff --git a/bus-mapping/src/evm/opcodes/return_revert.rs b/bus-mapping/src/evm/opcodes/return_revert.rs index be28f609eb..414a312280 100644 --- a/bus-mapping/src/evm/opcodes/return_revert.rs +++ b/bus-mapping/src/evm/opcodes/return_revert.rs @@ -48,7 +48,7 @@ impl Opcode for ReturnRevert { if call.is_create() && call.is_success && length > 0 { // Read the first byte of init code and check it must not be 0xef (EIP-3541). let init_code_first_byte = state.call_ctx()?.memory.0[offset]; - state.memory_read(&mut exec_step, offset.into(), init_code_first_byte)?; + state.memory_read(&mut exec_step, offset.into())?; assert_ne!(init_code_first_byte, INVALID_INIT_CODE_FIRST_BYTE); // Note: handle_return updates state.code_db. All we need to do here is push the @@ -138,7 +138,7 @@ impl Opcode for ReturnRevert { } } - state.handle_return(&mut exec_step, steps, false)?; + state.handle_return(&mut [&mut exec_step], steps, false)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/evm/opcodes/sha3.rs b/bus-mapping/src/evm/opcodes/sha3.rs index 1561825804..0bc3b435dc 100644 --- a/bus-mapping/src/evm/opcodes/sha3.rs +++ b/bus-mapping/src/evm/opcodes/sha3.rs @@ -57,7 +57,7 @@ impl Opcode for Sha3 { let mut steps = Vec::with_capacity(size.as_usize()); for (i, byte) in memory.iter().enumerate() { // Read step - state.memory_read(&mut exec_step, (offset.as_usize() + i).into(), *byte)?; + state.memory_read(&mut exec_step, (offset.as_usize() + i).into())?; steps.push((*byte, false)); } state.block.sha3_inputs.push(memory); diff --git a/bus-mapping/src/evm/opcodes/stackonlyop.rs b/bus-mapping/src/evm/opcodes/stackonlyop.rs index 6f27e032ed..314d086626 100644 --- a/bus-mapping/src/evm/opcodes/stackonlyop.rs +++ b/bus-mapping/src/evm/opcodes/stackonlyop.rs @@ -49,7 +49,7 @@ impl Opcode let next_step = geth_steps.get(1); exec_step.error = state.get_step_err(geth_step, next_step).unwrap(); - state.handle_return(&mut exec_step, geth_steps, true)?; + state.handle_return(&mut [&mut exec_step], geth_steps, true)?; } Ok(vec![exec_step]) diff --git a/bus-mapping/src/evm/opcodes/stop.rs b/bus-mapping/src/evm/opcodes/stop.rs index 3e81aba67c..70d59a252c 100644 --- a/bus-mapping/src/evm/opcodes/stop.rs +++ b/bus-mapping/src/evm/opcodes/stop.rs @@ -32,7 +32,7 @@ impl Opcode for Stop { 1.into(), )?; - state.handle_return(&mut exec_step, geth_steps, !call.is_root)?; + state.handle_return(&mut [&mut exec_step], geth_steps, !call.is_root)?; Ok(vec![exec_step]) } } diff --git a/bus-mapping/src/precompile.rs b/bus-mapping/src/precompile.rs index f082224e5d..3d3f5f75e4 100644 --- a/bus-mapping/src/precompile.rs +++ b/bus-mapping/src/precompile.rs @@ -1,7 +1,10 @@ //! precompile helpers -use eth_types::Address; -use revm_precompile::{Precompile, Precompiles}; +use eth_types::{ + evm_types::{GasCost, OpcodeId}, + Address, Bytecode, Word, +}; +use revm_precompile::{Precompile, PrecompileError, Precompiles}; /// Check if address is a precompiled or not. pub fn is_precompiled(address: &Address) -> bool { @@ -10,14 +13,191 @@ pub fn is_precompiled(address: &Address) -> bool { .is_some() } -pub(crate) fn execute_precompiled(address: &Address, input: &[u8], gas: u64) -> (Vec, u64) { +pub(crate) fn execute_precompiled( + address: &Address, + input: &[u8], + gas: u64, +) -> (Vec, u64, bool) { let Some(Precompile::Standard(precompile_fn)) = Precompiles::berlin() .get(address.as_fixed_bytes()) else { panic!("calling non-exist precompiled contract address") }; + let (return_data, gas_cost, is_oog, is_ok) = match precompile_fn(input, gas) { + Ok((gas_cost, return_value)) => { + // Some Revm behavior for invalid inputs might be overridden. + (return_value, gas_cost, false, true) + } + Err(err) => match err { + PrecompileError::OutOfGas => (vec![], gas, true, false), + _ => { + log::warn!("unknown precompile err {err:?}"); + (vec![], gas, false, false) + } + }, + }; + log::trace!("called precompile with is_ok {is_ok} is_oog {is_oog}, gas_cost {gas_cost}, return_data len {}", return_data.len()); + (return_data, gas_cost, is_oog) +} + +/// Addresses of the precompiled contracts. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum PrecompileCalls { + /// Elliptic Curve Recovery + ECRecover = 0x01, + /// SHA2-256 hash function + Sha256 = 0x02, + /// Ripemd-160 hash function + Ripemd160 = 0x03, + /// Identity function + Identity = 0x04, + /// Modular exponentiation + Modexp = 0x05, + /// Point addition + Bn128Add = 0x06, + /// Scalar multiplication + Bn128Mul = 0x07, + /// Bilinear function + Bn128Pairing = 0x08, + /// Compression function + Blake2F = 0x09, +} + +impl From for Address { + fn from(value: PrecompileCalls) -> Self { + let mut addr = [0u8; 20]; + addr[19] = value as u8; + Self(addr) + } +} + +impl From for u64 { + fn from(value: PrecompileCalls) -> Self { + value as u64 + } +} + +impl From for usize { + fn from(value: PrecompileCalls) -> Self { + value as usize + } +} + +impl From for PrecompileCalls { + fn from(value: u8) -> Self { + match value { + 0x01 => Self::ECRecover, + 0x02 => Self::Sha256, + 0x03 => Self::Ripemd160, + 0x04 => Self::Identity, + 0x05 => Self::Modexp, + 0x06 => Self::Bn128Add, + 0x07 => Self::Bn128Mul, + 0x08 => Self::Bn128Pairing, + 0x09 => Self::Blake2F, + _ => unreachable!("precompile contracts only from 0x01 to 0x09"), + } + } +} + +impl PrecompileCalls { + /// Get the base gas cost for the precompile call. + pub fn base_gas_cost(&self) -> u64 { + match self { + Self::ECRecover => GasCost::PRECOMPILE_ECRECOVER_BASE, + Self::Sha256 => GasCost::PRECOMPILE_SHA256_BASE, + Self::Ripemd160 => GasCost::PRECOMPILE_RIPEMD160_BASE, + Self::Identity => GasCost::PRECOMPILE_IDENTITY_BASE, + Self::Modexp => GasCost::PRECOMPILE_MODEXP, + Self::Bn128Add => GasCost::PRECOMPILE_BN256ADD, + Self::Bn128Mul => GasCost::PRECOMPILE_BN256MUL, + Self::Bn128Pairing => GasCost::PRECOMPILE_BN256PAIRING, + Self::Blake2F => GasCost::PRECOMPILE_BLAKE2F, + } + } + + /// Get the EVM address for this precompile call. + pub fn address(&self) -> u64 { + (*self).into() + } + + /// Maximum length of input bytes considered for the precompile call. + pub fn input_len(&self) -> Option { + match self { + Self::ECRecover | Self::Bn128Add => Some(128), + Self::Bn128Mul => Some(96), + _ => None, + } + } +} + +/// Precompile call args +pub struct PrecompileCallArgs { + /// description for the instance of a precompile call. + pub name: &'static str, + /// the bytecode that when call can produce the desired precompile call. + pub setup_code: Bytecode, + /// the call's return data size. + pub ret_size: Word, + /// the call's return data offset. + pub ret_offset: Word, + /// the call's calldata offset. + pub call_data_offset: Word, + /// the call's calldata length. + pub call_data_length: Word, + /// the address to which the call is made, i.e. callee address. + pub address: Word, + /// the optional value sent along with the call. + pub value: Word, + /// the gas limit for the call. + pub gas: Word, + /// stack values during the call. + pub stack_value: Vec<(Word, Word)>, + /// maximum number of RW entries for the call. + pub max_rws: usize, +} + +impl Default for PrecompileCallArgs { + fn default() -> Self { + PrecompileCallArgs { + name: "precompiled call", + setup_code: Bytecode::default(), + ret_size: Word::zero(), + ret_offset: Word::zero(), + call_data_offset: Word::zero(), + call_data_length: Word::zero(), + address: Word::zero(), + value: Word::zero(), + gas: Word::from(0xFFFFFFF), + stack_value: vec![], + max_rws: 1000, + } + } +} + +impl PrecompileCallArgs { + /// Get the setup bytecode for call to a precompiled contract. + pub fn with_call_op(&self, call_op: OpcodeId) -> Bytecode { + assert!( + call_op.is_call(), + "invalid setup, {:?} is not a call op", + call_op + ); + let mut code = self.setup_code.clone(); + code.push(32, self.ret_size) + .push(32, self.ret_offset) + .push(32, self.call_data_length) + .push(32, self.call_data_offset); + if call_op == OpcodeId::CALL || call_op == OpcodeId::CALLCODE { + code.push(32, self.value); + } + code.push(32, self.address) + .push(32, self.gas) + .write_op(call_op) + .write_op(OpcodeId::POP); + for (offset, _) in self.stack_value.iter().rev() { + code.push(32, *offset).write_op(OpcodeId::MLOAD); + } - match precompile_fn(input, gas) { - Ok((gas_cost, return_value)) => (return_value, gas_cost), - Err(_) => (vec![], gas), + code } } diff --git a/eth-types/src/evm_types.rs b/eth-types/src/evm_types.rs index 3ca6cced55..d9fa5ec40b 100644 --- a/eth-types/src/evm_types.rs +++ b/eth-types/src/evm_types.rs @@ -126,6 +126,34 @@ impl GasCost { /// Times ceil exponent byte size for the EXP instruction, EIP-158 changed /// it from 10 to 50. pub const EXP_BYTE_TIMES: u64 = 50; + /// Base gas cost for precompile call: Elliptic curve recover + pub const PRECOMPILE_ECRECOVER_BASE: u64 = 3000; + /// Base gas cost for precompile call: SHA256 + pub const PRECOMPILE_SHA256_BASE: u64 = 60; + /// Per-word gas cost for SHA256 + pub const PRECOMPILE_SHA256_PER_WORD: u64 = 12; + /// Base gas cost for precompile call: RIPEMD160 + pub const PRECOMPILE_RIPEMD160_BASE: u64 = 600; + /// Per-word gas cost for RIPEMD160 + pub const PRECOMPILE_RIPEMD160_PER_WORD: u64 = 120; + /// Base gas cost for precompile call: Identity + pub const PRECOMPILE_IDENTITY_BASE: u64 = 15; + /// Per-word gas cost for Identity + pub const PRECOMPILE_IDENTITY_PER_WORD: u64 = 3; + /// Base gas cost for precompile call: BN256 point addition + pub const PRECOMPILE_BN256ADD: u64 = 150; + /// Base gas cost for precompile call: BN256 scalar multiplication + pub const PRECOMPILE_BN256MUL: u64 = 6000; + /// Base gas cost for precompile call: BN256 pairing op base cost + pub const PRECOMPILE_BN256PAIRING: u64 = 45000; + /// Per-pair gas cost for BN256 pairing + pub const PRECOMPILE_BN256PAIRING_PER_PAIR: u64 = 34000; + /// Base gas cost for precompile call: MODEXP + pub const PRECOMPILE_MODEXP: u64 = 200; // eip255 + /// Minimum gas cost for precompile calls: MODEXP + pub const PRECOMPILE_MODEXP_MIN: u64 = 200; + /// Base gas cost for precompile call: BLAKE2F + pub const PRECOMPILE_BLAKE2F: u64 = 0; } /// This constant is used to iterate through precompile contract addresses 0x01 to 0x09 diff --git a/eth-types/src/evm_types/memory.rs b/eth-types/src/evm_types/memory.rs index ab0e7e5eef..8b8d53a3a2 100644 --- a/eth-types/src/evm_types/memory.rs +++ b/eth-types/src/evm_types/memory.rs @@ -6,7 +6,7 @@ use core::{ }; use itertools::Itertools; use serde::{Serialize, Serializer}; -use std::fmt; +use std::{cmp, fmt}; /// Represents a `MemoryAddress` of the EVM. #[derive(Clone, Copy, Eq, PartialEq, PartialOrd, Ord)] @@ -315,6 +315,23 @@ impl Memory { chunk } + /// Write a chunk of memory[offset..offset+length]. If any data is written out-of-bound, it must + /// be zeros. This does not resize the memory. + pub fn write_chunk(&mut self, offset: MemoryAddress, data: &[u8]) { + let len = if self.0.len() > offset.0 { + let len = cmp::min(data.len(), self.0.len() - offset.0); + // Copy the data to the in-bound memory. + self.0[offset.0..offset.0 + len].copy_from_slice(&data[..len]); + len + } else { + 0 + }; + // Check that the out-of-bound data is all zeros. + for _b in &data[len..] { + // assert_eq!(*b, 0); + } + } + /// Returns the size of memory in word. pub fn word_size(&self) -> usize { self.0.len() / 32 @@ -328,6 +345,18 @@ impl Memory { } } + /// Resize the memory for at least `offset+length` and align to 32 bytes, except if `length=0` + /// then do nothing. + pub fn extend_for_range(&mut self, offset: Word, length: Word) { + // `length` should be checked for overflow during gas cost calculation. + let length = length.as_usize(); + if length != 0 { + // `dst_offset` should be within range if length is non-zero. + let offset = offset.as_usize(); + self.extend_at_least(offset + length); + } + } + /// Copy source data to memory. Used in (ext)codecopy/calldatacopy. pub fn copy_from(&mut self, dst_offset: Word, src_offset: Word, length: Word, data: &[u8]) { // Reference go-ethereum `opCallDataCopy` function for defails. diff --git a/zkevm-circuits/src/evm_circuit/execution.rs b/zkevm-circuits/src/evm_circuit/execution.rs index ec597c491d..be1d8a13b2 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -79,9 +79,11 @@ mod error_oog_dynamic_memory; mod error_oog_exp; mod error_oog_log; mod error_oog_memory_copy; +mod error_oog_precompile; mod error_oog_sha3; mod error_oog_sload_sstore; mod error_oog_static_memory; +mod error_precompile_failed; mod error_return_data_oo_bound; mod error_stack; mod error_write_protection; @@ -106,6 +108,7 @@ mod opcode_not; mod origin; mod pc; mod pop; +mod precompiles; mod push; mod return_revert; mod returndatacopy; @@ -161,6 +164,7 @@ use error_oog_memory_copy::ErrorOOGMemoryCopyGadget; use error_oog_sha3::ErrorOOGSha3Gadget; use error_oog_sload_sstore::ErrorOOGSloadSstoreGadget; use error_oog_static_memory::ErrorOOGStaticMemoryGadget; +use error_precompile_failed::ErrorPrecompileFailedGadget; use error_return_data_oo_bound::ErrorReturnDataOutOfBoundGadget; use error_stack::ErrorStackGadget; use error_write_protection::ErrorWriteProtectionGadget; @@ -176,6 +180,8 @@ use jump::JumpGadget; use jumpdest::JumpdestGadget; use jumpi::JumpiGadget; use logs::LogGadget; + +use crate::evm_circuit::execution::error_oog_precompile::ErrorOOGPrecompileGadget; use memory::MemoryGadget; use msize::MsizeGadget; use mul_div_mod::MulDivModGadget; @@ -184,6 +190,7 @@ use opcode_not::NotGadget; use origin::OriginGadget; use pc::PcGadget; use pop::PopGadget; +use precompiles::IdentityGadget; use push::PushGadget; use return_revert::ReturnRevertGadget; use returndatacopy::ReturnDataCopyGadget; @@ -303,6 +310,7 @@ pub struct ExecutionConfig { block_ctx_gadget: Box>, // error gadgets error_oog_call: Box>, + error_oog_precompile: Box>, error_oog_constant: Box>, error_oog_exp: Box>, error_oog_memory_copy: Box>, @@ -327,7 +335,9 @@ pub struct ExecutionConfig { error_contract_address_collision: Box>, error_invalid_creation_code: Box>, + error_precompile_failed: Box>, error_return_data_out_of_bound: Box>, + precompile_identity_gadget: Box>, invalid_tx: Box>, } @@ -580,6 +590,7 @@ impl ExecutionConfig { error_oog_log: configure_gadget!(), error_oog_sload_sstore: configure_gadget!(), error_oog_call: configure_gadget!(), + error_oog_precompile: configure_gadget!(), error_oog_memory_copy: configure_gadget!(), error_oog_account_access: configure_gadget!(), error_oog_sha3: configure_gadget!(), @@ -594,7 +605,10 @@ impl ExecutionConfig { error_depth: configure_gadget!(), error_contract_address_collision: configure_gadget!(), error_invalid_creation_code: configure_gadget!(), + error_precompile_failed: configure_gadget!(), error_return_data_out_of_bound: configure_gadget!(), + // precompile calls + precompile_identity_gadget: configure_gadget!(), // step and presets step: step_curr, height_map, @@ -1315,6 +1329,9 @@ impl ExecutionConfig { ExecutionState::ErrorOutOfGasCall => { assign_exec_step!(self.error_oog_call) } + ExecutionState::ErrorOutOfGasPrecompile => { + assign_exec_step!(self.error_oog_precompile) + } ExecutionState::ErrorOutOfGasDynamicMemoryExpansion => { assign_exec_step!(self.error_oog_dynamic_memory_gadget) } @@ -1368,6 +1385,13 @@ impl ExecutionConfig { ExecutionState::ErrorReturnDataOutOfBound => { assign_exec_step!(self.error_return_data_out_of_bound) } + ExecutionState::ErrorPrecompileFailed => { + assign_exec_step!(self.error_precompile_failed) + } + // precompile calls + ExecutionState::PrecompileIdentity => { + assign_exec_step!(self.precompile_identity_gadget) + } unimpl_state => evm_unimplemented!("unimplemented ExecutionState: {:?}", unimpl_state), } diff --git a/zkevm-circuits/src/evm_circuit/execution/begin_tx.rs b/zkevm-circuits/src/evm_circuit/execution/begin_tx.rs index 1fa1bdfab2..9b3c099570 100644 --- a/zkevm-circuits/src/evm_circuit/execution/begin_tx.rs +++ b/zkevm-circuits/src/evm_circuit/execution/begin_tx.rs @@ -76,7 +76,6 @@ impl ExecutionGadget for BeginTxGadget { CallContextFieldTag::IsSuccess, Word::from_lo_unchecked(reversion_info.is_persistent()), ); // rwc_delta += 1 - cb.debug_expression(format!("call_id {}", 3), call_id.expr()); // Check gas_left is sufficient let gas_left = tx.gas.expr() - tx.intrinsic_gas(); diff --git a/zkevm-circuits/src/evm_circuit/execution/callop.rs b/zkevm-circuits/src/evm_circuit/execution/callop.rs index e8127202a7..0c0e6ae666 100644 --- a/zkevm-circuits/src/evm_circuit/execution/callop.rs +++ b/zkevm-circuits/src/evm_circuit/execution/callop.rs @@ -1,7 +1,7 @@ use crate::{ evm_circuit::{ execution::ExecutionGadget, - param::{N_BYTES_GAS, N_BYTES_U64}, + param::{N_BYTES_ACCOUNT_ADDRESS, N_BYTES_GAS, N_BYTES_MEMORY_ADDRESS, N_BYTES_U64}, step::ExecutionState, util::{ and, @@ -14,20 +14,26 @@ use crate::{ ConstantDivisionGadget, IsZeroGadget, LtGadget, LtWordGadget, MinMaxGadget, }, memory_gadget::{CommonMemoryAddressGadget, MemoryAddressGadget}, - not, or, select, CachedRegion, Cell, StepRws, + not, or, + precompile_gadget::PrecompileGadget, + rlc, select, CachedRegion, Cell, StepRws, Word, }, + witness::{Block, Call, ExecStep, Transaction}, }, - util::word::{Word, WordCell, WordExpr}, -}; - -use crate::{ - evm_circuit::witness::{Block, Call, ExecStep, Transaction}, table::{AccountFieldTag, CallContextFieldTag}, - util::Expr, + util::{ + word::{WordCell, WordExpr}, + Expr, + }, }; -use bus_mapping::evm::OpcodeId; -use eth_types::{evm_types::GAS_STIPEND_CALL_WITH_VALUE, Field, ToAddress, U256}; +use bus_mapping::{ + circuit_input_builder::CopyDataType, + evm::OpcodeId, + precompile::{is_precompiled, PrecompileCalls}, +}; +use eth_types::{evm_types::GAS_STIPEND_CALL_WITH_VALUE, Field, ToAddress, ToScalar, U256}; use halo2_proofs::{circuit::Value, plonk::Error}; +use std::cmp::min; /// Gadget for call related opcodes. It supports `OpcodeId::CALL`, /// `OpcodeId::CALLCODE`, `OpcodeId::DELEGATECALL` and `OpcodeId::STATICCALL`. @@ -47,18 +53,35 @@ pub(crate) struct CallOpGadget { is_static: Cell, depth: Cell, call: CommonCallGadget, true>, - current_value: WordCell, + current_value: Word>, is_warm: Cell, is_warm_prev: Cell, callee_reversion_info: ReversionInfo, transfer: TransferGadget, // current handling Call* opcode's caller balance - caller_balance: WordCell, + caller_balance: Word>, // check if insufficient balance case is_insufficient_balance: LtWordGadget, is_depth_ok: LtGadget, one_64th_gas: ConstantDivisionGadget, capped_callee_gas_left: MinMaxGadget, + // check if the call is a precompile call. + is_code_address_zero: IsZeroGadget, + is_precompile_lt: LtGadget, + precompile_gadget: PrecompileGadget, + precompile_return_length: Cell, + precompile_return_length_zero: IsZeroGadget, + precompile_return_data_copy_size: MinMaxGadget, + precompile_input_len: Cell, // the number of input bytes taken for the precompile call. + precompile_input_bytes_rlc: Cell, + precompile_output_bytes_rlc: Cell, + precompile_return_bytes_rlc: Cell, + precompile_input_rws: Cell, + precompile_output_rws: Cell, + precompile_return_rws: Cell, + // rws are in bytes, obtain word size for memory size transition + precompile_output_word_size_div: ConstantDivisionGadget, + precompile_output_word_size_div_remainder_zero: IsZeroGadget, } impl ExecutionGadget for CallOpGadget { @@ -79,20 +102,24 @@ impl ExecutionGadget for CallOpGadget { // Use rw_counter of the step which triggers next call as its call_id. let callee_call_id = cb.curr.state.rw_counter.clone(); + // rwc_delta = 0 let tx_id = cb.call_context(None, CallContextFieldTag::TxId); + // rwc_delta = 1 let mut reversion_info = cb.reversion_info_read(None); + // rwc_delta = 3 let [is_static, depth] = [CallContextFieldTag::IsStatic, CallContextFieldTag::Depth] .map(|field_tag| cb.call_context(None, field_tag)); - + // rwc_delta = 5 let current_callee_address = cb.call_context_read_as_word(None, CallContextFieldTag::CalleeAddress); + // rwc_delta = 6 let (current_caller_address, current_value) = cb.condition(is_delegatecall.expr(), |cb| { ( cb.call_context_read_as_word(None, CallContextFieldTag::CallerAddress), cb.call_context_read_as_word(None, CallContextFieldTag::Value), ) }); - + // rwc_delta = 6 + is_delegatecall * 2 let call_gadget: CommonCallGadget, true> = CommonCallGadget::construct( cb, @@ -101,6 +128,7 @@ impl ExecutionGadget for CallOpGadget { is_delegatecall.expr(), is_staticcall.expr(), ); + // rwc_delta = 6 + is_delegatecall * 2 + call_gadget.rw_delta() cb.condition(not::expr(is_call.expr() + is_callcode.expr()), |cb| { cb.require_zero_word( "for non call/call code, value is zero", @@ -116,12 +144,13 @@ impl ExecutionGadget for CallOpGadget { let callee_address = Word::select( is_callcode.expr() + is_delegatecall.expr(), current_callee_address.to_word(), - call_gadget.callee_address(), + call_gadget.callee_address.to_word(), ); // Add callee to access list let is_warm = cb.query_bool(); let is_warm_prev = cb.query_bool(); + cb.require_true("callee add should be updated to warm", is_warm.expr()); cb.account_access_list_write_unchecked( tx_id.expr(), call_gadget.callee_address(), @@ -129,10 +158,13 @@ impl ExecutionGadget for CallOpGadget { is_warm_prev.expr(), Some(&mut reversion_info), ); + // rwc_delta = 7 + is_delegatecall * 2 + call_gadget.rw_delta() // Propagate rw_counter_end_of_reversion and is_persistent let mut callee_reversion_info = cb.reversion_info_write_unchecked(Some(callee_call_id.expr())); + // rwc_delta = 7 + is_delegatecall * 2 + call_gadget.rw_delta() + + // callee_reversion_info.rw_delta() cb.require_equal( "callee_is_persistent == is_persistent â‹… is_success", callee_reversion_info.is_persistent(), @@ -159,6 +191,8 @@ impl ExecutionGadget for CallOpGadget { AccountFieldTag::Balance, caller_balance.to_word(), ); + // rwc_delta = 8 + is_delegatecall * 2 + call_gadget.rw_delta() + + // callee_reversion_info.rw_delta() let is_insufficient_balance = LtWordGadget::construct(cb, &caller_balance.to_word(), &call_gadget.value.to_word()); // depth < 1025 @@ -177,6 +211,29 @@ impl ExecutionGadget for CallOpGadget { ); }); + // whether the call is to a precompiled contract. + // precompile contracts are stored from address 0x01 to 0x09. + let is_code_address_zero = IsZeroGadget::construct(cb, call_gadget.callee_address.expr()); + let is_precompile_lt = + LtGadget::construct(cb, call_gadget.callee_address.expr(), 0x0A.expr()); + let is_precompile = and::expr([ + not::expr(is_code_address_zero.expr()), + is_precompile_lt.expr(), + ]); + let precompile_return_length = cb.query_cell(); + let precompile_return_length_zero = + IsZeroGadget::construct(cb, precompile_return_length.expr()); + let precompile_return_data_copy_size = MinMaxGadget::construct( + cb, + precompile_return_length.expr(), + call_gadget.rd_address.length(), + ); + + let precompile_input_rws = cb.query_cell(); + let precompile_output_rws = cb.query_cell(); + let precompile_return_rws = cb.query_cell(); + let precompile_input_len = cb.query_cell(); + // Verify transfer only for CALL opcode in the successful case. If value == 0, // skip the transfer (this is necessary for non-existing accounts, which // will not be crated when value is 0 and so the callee balance lookup @@ -192,6 +249,8 @@ impl ExecutionGadget for CallOpGadget { &mut callee_reversion_info, ) }); + // rwc_delta = 8 + is_delegatecall * 2 + call_gadget.rw_delta() + + // callee_reversion_info.rw_delta() + transfer.rw_delta() // For CALLCODE opcode, verify caller balance is greater than or equal to stack // `value` in successful case. that is `is_insufficient_balance` is false. @@ -224,15 +283,252 @@ impl ExecutionGadget for CallOpGadget { all_but_one_64th_gas, ); - // TODO: Handle precompiled - let stack_pointer_delta = select::expr(is_call.expr() + is_callcode.expr(), 6.expr(), 5.expr()); let memory_expansion = call_gadget.memory_expansion.clone(); - // handle calls to accounts with no code. + let transfer_rwc_delta = is_call.expr() * is_precheck_ok.expr() * transfer.rw_delta(); + let rw_counter_delta = 8.expr() + + is_delegatecall.expr() * 2.expr() + + call_gadget.rw_delta() + + callee_reversion_info.rw_delta() + + transfer_rwc_delta.expr(); + + let callee_reversible_rwc_delta = is_call.expr() * transfer.reversible_w_delta(); + + // 1. handle precompile calls. + let ( + precompile_gadget, + precompile_input_bytes_rlc, + precompile_output_bytes_rlc, + precompile_return_bytes_rlc, + precompile_output_word_size_div, + precompile_output_word_size_div_remainder_zero, + ) = cb.condition( + and::expr([is_precompile.expr(), is_precheck_ok.expr()]), + |cb| { + cb.require_equal( + "Callee has no code for precompile", + no_callee_code.expr(), + true.expr(), + ); + cb.require_true( + "Precompile addresses are always warm", + and::expr([is_warm.expr(), is_warm_prev.expr()]), + ); + + // Write to callee's context. + cb.call_context_lookup_write( + Some(callee_call_id.expr()), + CallContextFieldTag::IsSuccess, + Word::from_lo_unchecked(call_gadget.is_success.expr()), + ); + cb.call_context_lookup_write( + Some(callee_call_id.expr()), + CallContextFieldTag::CalleeAddress, + call_gadget.callee_address.to_word(), + ); + for (field_tag, value) in [ + (CallContextFieldTag::CallerId, cb.curr.state.call_id.expr()), + ( + CallContextFieldTag::CallDataOffset, + call_gadget.cd_address.offset(), + ), + ( + CallContextFieldTag::CallDataLength, + call_gadget.cd_address.length(), + ), + ( + CallContextFieldTag::ReturnDataOffset, + call_gadget.rd_address.offset(), + ), + ( + CallContextFieldTag::ReturnDataLength, + call_gadget.rd_address.length(), + ), + ] { + cb.call_context_lookup_write( + Some(callee_call_id.expr()), + field_tag, + Word::from_lo_unchecked(value), + ); + } + + // Save caller's call state + for (field_tag, value) in [ + ( + CallContextFieldTag::ProgramCounter, + cb.curr.state.program_counter.expr() + 1.expr(), + ), + ( + CallContextFieldTag::StackPointer, + cb.curr.state.stack_pointer.expr() + + select::expr(is_call.expr() + is_callcode.expr(), 6.expr(), 5.expr()), + ), + ( + CallContextFieldTag::GasLeft, + cb.curr.state.gas_left.expr() - gas_cost.expr() - callee_gas_left.clone(), + ), + ( + CallContextFieldTag::MemorySize, + memory_expansion.next_memory_word_size(), + ), + ( + CallContextFieldTag::ReversibleWriteCounter, + cb.curr.state.reversible_write_counter.expr() + 1.expr(), + ), + (CallContextFieldTag::LastCalleeId, callee_call_id.expr()), + (CallContextFieldTag::LastCalleeReturnDataOffset, 0.expr()), + ( + CallContextFieldTag::LastCalleeReturnDataLength, + precompile_return_length.expr(), + ), + ] { + cb.call_context_lookup_write(None, field_tag, Word::from_lo_unchecked(value)); + } + + // copy table lookup to verify the copying of bytes: + // - from caller's memory (`call_data_length` bytes starting at `call_data_offset`) + // - to the current call's memory (`call_data_length` bytes starting at `0`). + let precompile_input_bytes_rlc = + cb.condition(call_gadget.cd_address.has_length(), |cb| { + let precompile_input_bytes_rlc = cb.query_cell_phase2(); + cb.copy_table_lookup( + Word::from_lo_unchecked(cb.curr.state.call_id.expr()), + CopyDataType::Memory.expr(), + Word::from_lo_unchecked(callee_call_id.expr()), + CopyDataType::RlcAcc.expr(), + call_gadget.cd_address.offset(), + call_gadget.cd_address.offset() + precompile_input_len.expr(), + 0.expr(), + precompile_input_len.expr(), + precompile_input_bytes_rlc.expr(), + precompile_input_rws.expr(), // reads + writes + ); + precompile_input_bytes_rlc + }); + + // copy table lookup to verify the precompile result. + // - from precompiled contract. + // - to the current call's memory (starting at '0'). + let precompile_output_bytes_rlc = cb.condition( + and::expr([ + call_gadget.is_success.expr(), + not::expr(precompile_return_length_zero.expr()), + ]), + |cb| { + let precompile_output_bytes_rlc = cb.query_cell_phase2(); + cb.copy_table_lookup( + Word::from_lo_unchecked(callee_call_id.expr()), + CopyDataType::RlcAcc.expr(), + Word::from_lo_unchecked(callee_call_id.expr()), + CopyDataType::Memory.expr(), + 0.expr(), + precompile_return_length.expr(), + 0.expr(), + precompile_return_length.expr(), + precompile_output_bytes_rlc.expr(), + precompile_output_rws.expr(), + ); + precompile_output_bytes_rlc + }, + ); + + // copy table lookup to verify the copying of bytes if the precompile call was + // successful. + // - from precompile (min(rd_length, precompile_return_length) bytes) + // - to caller's memory (min(rd_length, precompile_return_length) bytes starting at + // `return_data_offset`). + let precompile_return_bytes_rlc = cb.condition( + and::expr([ + call_gadget.is_success.expr(), + call_gadget.rd_address.has_length(), + not::expr(precompile_return_length_zero.expr()), + ]), + |cb| { + let precompile_return_bytes_rlc = cb.query_cell_phase2(); + cb.copy_table_lookup( + Word::from_lo_unchecked(callee_call_id.expr()), + CopyDataType::Memory.expr(), // refer u64::from(CopyDataType) + Word::from_lo_unchecked(cb.curr.state.call_id.expr()), + CopyDataType::Memory.expr(), + 0.expr(), + precompile_return_data_copy_size.min(), + call_gadget.rd_address.offset(), + precompile_return_data_copy_size.min(), + 0.expr(), + precompile_return_rws.expr(), // writes + ); // rwc_delta += `return_data_copy_size.min()` for precompile + precompile_return_bytes_rlc + }, + ); + + // +15 call context lookups for precompile. + let rw_counter_delta = 15.expr() + + rw_counter_delta.expr() + + precompile_input_rws.expr() + + precompile_output_rws.expr() + + precompile_return_rws.expr(); + + // Give gas stipend if value is not zero + let callee_gas_left = callee_gas_left.expr() + + call_gadget.has_value.clone() * GAS_STIPEND_CALL_WITH_VALUE.expr(); + + let precompile_output_word_size_div: ConstantDivisionGadget = + ConstantDivisionGadget::construct(cb, precompile_output_rws.expr(), 32); + let precompile_output_word_size_div_remainder_zero = + IsZeroGadget::construct(cb, precompile_output_word_size_div.remainder()); + let precompile_output_word_size = precompile_output_word_size_div.quotient() + + 1.expr() + - precompile_output_word_size_div_remainder_zero.expr(); + + cb.require_step_state_transition(StepStateTransition { + rw_counter: Delta(rw_counter_delta), + call_id: To(callee_call_id.expr()), + is_root: To(false.expr()), + is_create: To(false.expr()), + code_hash: To(cb.empty_code_hash()), + program_counter: Delta(1.expr()), + stack_pointer: Delta(stack_pointer_delta.expr()), + gas_left: To(callee_gas_left.expr()), + memory_word_size: To(precompile_output_word_size), + reversible_write_counter: To(callee_reversible_rwc_delta.expr()), + ..StepStateTransition::default() + }); + + let precompile_gadget = PrecompileGadget::construct( + cb, + call_gadget.is_success.expr(), + call_gadget.callee_address.expr(), + cb.curr.state.call_id.expr(), + call_gadget.cd_address.offset(), + call_gadget.cd_address.length(), + call_gadget.rd_address.offset(), + call_gadget.rd_address.length(), + precompile_return_length.expr(), + precompile_input_bytes_rlc.expr(), + precompile_output_bytes_rlc.expr(), + precompile_return_bytes_rlc.expr(), + ); + + ( + precompile_gadget, + precompile_input_bytes_rlc, + precompile_output_bytes_rlc, + precompile_return_bytes_rlc, + precompile_output_word_size_div, + precompile_output_word_size_div_remainder_zero, + ) + }, + ); + + // 2. handle calls to accounts with no code cb.condition( - and::expr(&[no_callee_code.expr(), is_precheck_ok.expr()]), + and::expr([ + not::expr(is_precompile.expr()), + no_callee_code.expr(), + is_precheck_ok.expr(), + ]), |cb| { // Save caller's call state cb.call_context_lookup_write( @@ -309,8 +605,13 @@ impl ExecutionGadget for CallOpGadget { }); }); + // handle all other calls. cb.condition( - and::expr(&[not::expr(no_callee_code), is_precheck_ok.expr()]), + and::expr([ + not::expr(is_precompile.expr()), + not::expr(no_callee_code), + is_precheck_ok.expr(), + ]), |cb| { // Save caller's call state for (field_tag, value) in [ @@ -466,6 +767,22 @@ impl ExecutionGadget for CallOpGadget { is_depth_ok, one_64th_gas, capped_callee_gas_left, + // precompile related fields. + is_code_address_zero, + is_precompile_lt, + precompile_gadget, + precompile_return_length, + precompile_return_length_zero, + precompile_return_data_copy_size, + precompile_input_len, + precompile_input_bytes_rlc, + precompile_output_bytes_rlc, + precompile_return_bytes_rlc, + precompile_input_rws, + precompile_output_rws, + precompile_return_rws, + precompile_output_word_size_div, + precompile_output_word_size_div_remainder_zero, } } @@ -483,6 +800,7 @@ impl ExecutionGadget for CallOpGadget { let is_callcode = opcode == OpcodeId::CALLCODE; let is_delegatecall = opcode == OpcodeId::DELEGATECALL; let mut rws = StepRws::new(block, step); + let tx_id = rws.next().call_context_value(); rws.next(); // RwCounterEndOfReversion rws.next(); // IsPersistent @@ -491,9 +809,9 @@ impl ExecutionGadget for CallOpGadget { let depth = rws.next().call_context_value(); let current_callee_address = rws.next().call_context_value(); - let is_valid_depth = depth.low_u64() < 1025; self.is_depth_ok .assign(region, offset, F::from(depth.low_u64()), F::from(1025))?; + // This offset is used to change the index offset of `step.rw_indices`. // Since both CALL and CALLCODE have an extra stack pop `value`, and // opcode DELEGATECALL has two extra call context lookups - current @@ -535,6 +853,25 @@ impl ExecutionGadget for CallOpGadget { .assign_u256(region, offset, caller_balance)?; self.is_insufficient_balance .assign(region, offset, caller_balance, value)?; + let is_precheck_ok = + depth.low_u64() < 1025 && (!(is_call || is_callcode) || caller_balance >= value); + + // conditionally assign + if is_call && is_precheck_ok && !value.is_zero() { + if !callee_exists { + rws.next().account_codehash_pair(); // callee hash + } + + let caller_balance_pair = rws.next().account_balance_pair(); + let callee_balance_pair = rws.next().account_balance_pair(); + self.transfer.assign( + region, + offset, + caller_balance_pair, + callee_balance_pair, + value, + )?; + } self.opcode .assign(region, offset, Value::known(F::from(opcode.as_u64())))?; @@ -566,16 +903,10 @@ impl ExecutionGadget for CallOpGadget { call.rw_counter_end_of_reversion, call.is_persistent, )?; - self.current_callee_address.assign_h160( - region, - offset, - current_callee_address.to_address(), - )?; - self.current_caller_address.assign_h160( - region, - offset, - current_caller_address.to_address(), - )?; + self.current_callee_address + .assign_u256(region, offset, current_callee_address)?; + self.current_caller_address + .assign_u256(region, offset, current_caller_address)?; self.current_value .assign_u256(region, offset, current_value)?; self.is_static @@ -608,27 +939,6 @@ impl ExecutionGadget for CallOpGadget { callee_is_persistent.low_u64() != 0, )?; - let is_call_or_callcode = is_call || is_callcode; - let is_sufficient = caller_balance >= value; - let is_precheck_ok = is_valid_depth && (is_sufficient || !is_call_or_callcode); - - // conditionally assign - if is_call && is_precheck_ok && !value.is_zero() { - if !callee_exists { - rws.next().account_codehash_pair(); // callee hash - } - - let caller_balance_pair = rws.next().account_balance_pair(); - let callee_balance_pair = rws.next().account_balance_pair(); - self.transfer.assign( - region, - offset, - caller_balance_pair, - callee_balance_pair, - value, - )?; - } - let has_value = !value.is_zero() && !is_delegatecall; let gas_cost = self.call.cal_gas_cost_for_assignment( memory_expansion_gas_cost, @@ -638,7 +948,6 @@ impl ExecutionGadget for CallOpGadget { !callee_exists, )?; let gas_available: u64 = step.gas_left - gas_cost; - self.one_64th_gas .assign(region, offset, gas_available.into())?; self.capped_callee_gas_left.assign( @@ -648,6 +957,146 @@ impl ExecutionGadget for CallOpGadget { F::from(gas_available - gas_available / 64), )?; + let (_is_precompile_call, precompile_addr) = { + let precompile_addr = callee_address.to_address(); + let is_precompiled_call = is_precompiled(&precompile_addr); + (is_precompiled_call, precompile_addr) + }; + let code_address: F = callee_address.to_address().to_scalar().unwrap(); + self.is_code_address_zero + .assign(region, offset, code_address)?; + self.is_precompile_lt + .assign(region, offset, code_address, 0x0Au64.into())?; + let precompile_return_length = if is_precompiled(&callee_address.to_address()) { + rws.offset_add(14); // skip + let value_rw = rws.next(); + assert_eq!( + value_rw.field_tag(), + Some(CallContextFieldTag::LastCalleeReturnDataLength as u64), + "expect LastCalleeReturnDataLength" + ); + value_rw.call_context_value() + } else { + 0.into() + }; + self.precompile_return_length.assign( + region, + offset, + Value::known(precompile_return_length.to_scalar().unwrap()), + )?; + self.precompile_return_length_zero.assign( + region, + offset, + precompile_return_length.to_scalar().unwrap(), + )?; + self.precompile_return_data_copy_size.assign( + region, + offset, + precompile_return_length.to_scalar().unwrap(), + rd_length.to_scalar().unwrap(), + )?; + + let ( + precompile_input_len, + precompile_input_bytes_rlc, + precompile_output_bytes_rlc, + precompile_return_bytes_rlc, + input_rws, + output_rws, + return_rws, + ) = if is_precheck_ok && is_precompiled(&callee_address.to_address()) { + let precompile_call: PrecompileCalls = precompile_addr.0[19].into(); + let input_len = if let Some(input_len) = precompile_call.input_len() { + min(input_len, cd_length.as_usize()) + } else { + cd_length.as_usize() + }; + + let input_bytes = (0..input_len) + .map(|_| rws.next().memory_value()) + .collect::>(); + let output_bytes = (0..precompile_return_length.as_u64()) + .map(|_| rws.next().memory_value()) + .collect::>(); + let return_length = min(precompile_return_length, rd_length); + let return_bytes = (0..(return_length.as_u64() * 2)) + .map(|_| rws.next().memory_value()) + .step_by(2) + .collect::>(); + + let input_bytes_rlc = region + .challenges() + .keccak_input() + .map(|randomness| rlc::value(input_bytes.iter().rev(), randomness)); + let output_bytes_rlc = region + .challenges() + .keccak_input() + .map(|randomness| rlc::value(output_bytes.iter().rev(), randomness)); + let return_bytes_rlc = region + .challenges() + .keccak_input() + .map(|randomness| rlc::value(return_bytes.iter().rev(), randomness)); + + let input_rws = input_bytes.len() as u64; + let output_rws = output_bytes.len() as u64; + let return_rws = (return_bytes.len() * 2) as u64; + + ( + input_len as u64, + input_bytes_rlc, + output_bytes_rlc, + return_bytes_rlc, + input_rws, + output_rws, + return_rws, + ) + } else { + ( + 0, + Value::known(F::ZERO), + Value::known(F::ZERO), + Value::known(F::ZERO), + 0u64, + 0u64, + 0u64, + ) + }; + + self.precompile_input_len.assign( + region, + offset, + Value::known(F::from(precompile_input_len)), + )?; + self.precompile_input_bytes_rlc + .assign(region, offset, precompile_input_bytes_rlc)?; + self.precompile_output_bytes_rlc + .assign(region, offset, precompile_output_bytes_rlc)?; + self.precompile_return_bytes_rlc + .assign(region, offset, precompile_return_bytes_rlc)?; + self.precompile_input_rws + .assign(region, offset, Value::known(F::from(input_rws)))?; + self.precompile_output_rws + .assign(region, offset, Value::known(F::from(output_rws)))?; + self.precompile_return_rws + .assign(region, offset, Value::known(F::from(return_rws)))?; + + let (_, remainder) = + self.precompile_output_word_size_div + .assign(region, offset, output_rws.into())?; + self.precompile_output_word_size_div_remainder_zero.assign( + region, + offset, + F::from_u128(remainder), + )?; + + if is_precompiled(&callee_address.to_address()) { + self.precompile_gadget.assign( + region, + offset, + callee_address.to_address().0[19].into(), + )?; + } + Ok(()) } } @@ -1040,4 +1489,199 @@ mod test { }) .run(); } + + #[test] + fn test_precompiled_call() { + use bus_mapping::{mock::BlockData, precompile::PrecompileCallArgs}; + use eth_types::{bytecode, evm_types::OpcodeId, geth_types::GethData, word, Word}; + use mock::{ + test_ctx::{ + helpers::{account_0_code_account_1_no_code, tx_from_1_to_0}, + LoggerConfig, + }, + TestContext, + }; + + let test_vector = [ + PrecompileCallArgs { + name: "ecRecover", + setup_code: bytecode! { + PUSH32(word!("0x456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3")) // hash + PUSH1(0x0) + MSTORE + PUSH1(28) // v + PUSH1(0x20) + MSTORE + PUSH32(word!("0x9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac8038825608")) // r + PUSH1(0x40) + MSTORE + PUSH32(word!("0x4f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada")) // s + PUSH1(0x60) + MSTORE + }, + ret_size: Word::from(0x20), + ret_offset: Word::from(0x80), + call_data_length: Word::from(0x80), + address: Word::from(0x1), + stack_value: vec![( + Word::from(0x80), + word!("7156526fbd7a3c72969b54f64e42c10fbb768c8a"), + )], + ..Default::default() + }, + PrecompileCallArgs { + name: "SHA2-256", + setup_code: bytecode! { + PUSH1(0xFF) // data + PUSH1(0) + MSTORE + }, + ret_size: Word::from(0x20), + ret_offset: Word::from(0x20), + call_data_length: Word::from(0x1), + call_data_offset: Word::from(0x1F), + address: Word::from(0x2), + stack_value: vec![( + Word::from(0x20), + word!("a8100ae6aa1940d0b663bb31cd466142ebbdbd5187131b92d93818987832eb89"), + )], + ..Default::default() + }, + PrecompileCallArgs { + name: "RIPEMD-160", + setup_code: bytecode! { + PUSH1(0xFF) // data + PUSH1(0) + MSTORE + }, + ret_size: Word::from(0x20), + ret_offset: Word::from(0x20), + call_data_length: Word::from(0x1), + call_data_offset: Word::from(0x1F), + address: Word::from(0x3), + stack_value: vec![( + Word::from(0x20), + word!("2c0c45d3ecab80fe060e5f1d7057cd2f8de5e557"), + )], + ..Default::default() + }, + PrecompileCallArgs { + name: "identity", + setup_code: bytecode! { + PUSH16(word!("0123456789ABCDEF0123456789ABCDEF")) + PUSH1(0x00) + MSTORE + }, + ret_size: Word::from(0x20), + ret_offset: Word::from(0x20), + call_data_length: Word::from(0x20), + address: Word::from(0x4), + stack_value: vec![(Word::from(0x20), word!("0123456789ABCDEF0123456789ABCDEF"))], + ..Default::default() + }, + PrecompileCallArgs { + name: "ecAdd", + setup_code: bytecode! { + PUSH1(1) // x1 + PUSH1(0) + MSTORE + PUSH1(2) // y1 + PUSH1(0x20) + MSTORE + PUSH1(1) // x2 + PUSH1(0x40) + MSTORE + PUSH1(2) // y2 + PUSH1(0x60) + MSTORE + }, + ret_size: Word::from(0x40), + ret_offset: Word::from(0x80), + call_data_length: Word::from(0x80), + address: Word::from(0x6), + stack_value: vec![ + ( + Word::from(0x80), + word!("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"), + ), + ( + Word::from(0xA0), + word!("15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"), + ), + ], + ..Default::default() + }, + PrecompileCallArgs { + name: "ecMul", + setup_code: bytecode! { + PUSH1(1) // x1 + PUSH1(0) + MSTORE + PUSH1(2) // y1 + PUSH1(0x20) + MSTORE + PUSH1(2) // s + PUSH1(0x40) + MSTORE + }, + ret_size: Word::from(0x40), + ret_offset: Word::from(0x60), + call_data_length: Word::from(0x60), + address: Word::from(0x7), + stack_value: vec![ + ( + Word::from(0x60), + word!("30644e72e131a029b85045b68181585d97816a916871ca8d3c208c16d87cfd3"), + ), + ( + Word::from(0x80), + word!("15ed738c0e0a7c92e7845f96b2ae9c0a68a6a449e3538fc7ff3ebf7a5a18a2c4"), + ), + ], + ..Default::default() + }, + ]; + + let call_ops = [ + OpcodeId::CALL, + OpcodeId::CALLCODE, + OpcodeId::DELEGATECALL, + OpcodeId::STATICCALL, + ]; + + for (test_call, call_op) in itertools::iproduct!(test_vector.iter(), call_ops.iter()) { + let code = test_call.with_call_op(*call_op); + let block: GethData = TestContext::<2, 1>::new_with_logger_config( + None, + account_0_code_account_1_no_code(code), + tx_from_1_to_0, + |block, _tx| block.number(0xcafeu64), + LoggerConfig { + enable_memory: true, + ..Default::default() + }, + ) + .unwrap() + .into(); + + let builder = BlockData::new_from_geth_data(block.clone()).new_circuit_input_builder(); + builder + .handle_block(&block.eth_block, &block.geth_traces) + .unwrap(); + + let step = block.geth_traces[0] + .struct_logs + .last() + .expect("at least one step"); + log::debug!("{:?}", step.stack); + for (offset, (_, stack_value)) in test_call.stack_value.iter().enumerate() { + assert_eq!( + *stack_value, + step.stack.nth_last(offset).expect("stack value not found"), + "stack output mismatch {}", + test_call.name + ); + } + } + } } diff --git a/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs b/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs new file mode 100644 index 0000000000..0da7007236 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/error_oog_precompile.rs @@ -0,0 +1,278 @@ +use crate::{ + evm_circuit::{ + execution::ExecutionGadget, + param::{N_BYTES_GAS, N_BYTES_MEMORY_WORD_SIZE, N_BYTES_WORD}, + step::ExecutionState, + util::{ + common_gadget::RestoreContextGadget, + constraint_builder::{ConstrainBuilderCommon, EVMConstraintBuilder}, + math_gadget::{BinaryNumberGadget, ConstantDivisionGadget, LtGadget}, + CachedRegion, Cell, + }, + }, + table::CallContextFieldTag, + witness::{Block, Call, ExecStep, Transaction}, +}; +use bus_mapping::precompile::PrecompileCalls; +use eth_types::{evm_types::GasCost, Field, ToScalar}; +use gadgets::util::{sum, Expr}; +use halo2_proofs::{circuit::Value, plonk::Error}; + +#[derive(Clone, Debug)] +pub(crate) struct ErrorOOGPrecompileGadget { + precompile_addr: Cell, + addr_bits: BinaryNumberGadget, + call_data_length: Cell, + // n_pairs: ConstantDivisionGadget, + n_words: ConstantDivisionGadget, + required_gas: Cell, + insufficient_gas: LtGadget, + restore_context: RestoreContextGadget, +} + +impl ExecutionGadget for ErrorOOGPrecompileGadget { + const NAME: &'static str = "ErrorOutOfGasPrecompile"; + + const EXECUTION_STATE: ExecutionState = ExecutionState::ErrorOutOfGasPrecompile; + + fn configure(cb: &mut EVMConstraintBuilder) -> Self { + let required_gas = cb.query_cell(); + + // read callee_address + let precompile_addr = cb.call_context(None, CallContextFieldTag::CalleeAddress); + let addr_bits = BinaryNumberGadget::construct(cb, precompile_addr.expr()); + + // read call data length + let call_data_length = cb.call_context(None, CallContextFieldTag::CallDataLength); + // let n_pairs = cb.condition( + // addr_bits.value_equals(PrecompileCalls::Bn128Pairing), + // |cb| { + // ConstantDivisionGadget::construct( + // cb, + // call_data_length.expr(), + // N_BYTES_EC_PAIR as u64, + // ) + // }, + // ); + let n_words = cb.condition(addr_bits.value_equals(PrecompileCalls::Identity), |cb| { + ConstantDivisionGadget::construct( + cb, + call_data_length.expr() + (N_BYTES_WORD - 1).expr(), + N_BYTES_WORD as u64, + ) + }); + + // calculate required gas for precompile + let precompiles_required_gas = vec![ + // ( + // addr_bits.value_equals(PrecompileCalls::ECRecover), + // GasCost::PRECOMPILE_ECRECOVER_BASE.expr(), + // ), + // addr_bits.value_equals(PrecompileCalls::Sha256), + // addr_bits.value_equals(PrecompileCalls::Ripemd160), + // addr_bits.value_equals(PrecompileCalls::Blake2F), + ( + addr_bits.value_equals(PrecompileCalls::Identity), + GasCost::PRECOMPILE_IDENTITY_BASE.expr() + + n_words.quotient() * GasCost::PRECOMPILE_IDENTITY_PER_WORD.expr(), + ), + // modexp is handled in ModExpGadget + // ( + // addr_bits.value_equals(PrecompileCalls::Bn128Add), + // GasCost::PRECOMPILE_BN256ADD.expr(), + // ), + // ( + // addr_bits.value_equals(PrecompileCalls::Bn128Mul), + // GasCost::PRECOMPILE_BN256MUL.expr(), + // ), + // ( + // addr_bits.value_equals(PrecompileCalls::Bn128Pairing), + // GasCost::PRECOMPILE_BN256PAIRING.expr() + // + n_pairs.quotient() * GasCost::PRECOMPILE_BN256PAIRING_PER_PAIR.expr(), + // ), + ]; + + cb.require_equal( + "precompile_addr must belong to precompile calls' set", + sum::expr( + precompiles_required_gas + .iter() + .map(|(cond, _)| cond) + .cloned(), + ), + 1.expr(), + ); + + cb.require_equal( + "require_gas == sum(is_precompile[addr] * required_gas[addr])", + required_gas.expr(), + precompiles_required_gas + .iter() + .fold(0.expr(), |acc, (condition, required_gas)| { + acc + condition.expr() * required_gas.expr() + }), + ); + + // gas_left < required_gas + let insufficient_gas = + LtGadget::construct(cb, cb.curr.state.gas_left.expr(), required_gas.expr()); + cb.require_equal("gas_left < required_gas", insufficient_gas.expr(), 1.expr()); + + let restore_context = RestoreContextGadget::construct2( + cb, + false.expr(), + cb.curr.state.gas_left.expr(), + 0.expr(), + 0.expr(), // ReturnDataOffset + 0.expr(), // ReturnDataLength + 0.expr(), + 0.expr(), + ); + + Self { + precompile_addr, + required_gas, + insufficient_gas, + // n_pairs, + n_words, + addr_bits, + call_data_length, + restore_context, + } + } + + fn assign_exec_step( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + block: &Block, + _transaction: &Transaction, + call: &Call, + step: &ExecStep, + ) -> Result<(), Error> { + // addr_bits + let precompile_addr = call.code_address().unwrap(); + self.precompile_addr.assign( + region, + offset, + Value::known(precompile_addr.to_scalar().unwrap()), + )?; + self.addr_bits + .assign(region, offset, precompile_addr.to_fixed_bytes()[19])?; + + // call_data_length + self.call_data_length.assign( + region, + offset, + Value::known(F::from(call.call_data_length)), + )?; + + // n_pairs + // let n_pairs = call.call_data_length / 192; + // self.n_pairs + // .assign(region, offset, call.call_data_length as u128)?; + + // n_words + self.n_words.assign( + region, + offset, + (call.call_data_length + (N_BYTES_WORD as u64) - 1) as u128, + )?; + + // required_gas + let precompile_call: PrecompileCalls = precompile_addr.to_fixed_bytes()[19].into(); + let required_gas = match precompile_call { + // PrecompileCalls::Bn128Pairing => { + // precompile_call.base_gas_cost() + // + n_pairs * GasCost::PRECOMPILE_BN256PAIRING_PER_PAIR + // } + PrecompileCalls::Identity => { + let n_words = (call.call_data_length + 31) / 32; + precompile_call.base_gas_cost() + n_words * GasCost::PRECOMPILE_IDENTITY_PER_WORD + } + // PrecompileCalls::Bn128Add | PrecompileCalls::Bn128Mul | PrecompileCalls::ECRecover => + // { precompile_call.base_gas_cost() + // } + _ => unreachable!(), + }; + + self.required_gas + .assign(region, offset, Value::known(F::from(required_gas)))?; + + // insufficient_gas + self.insufficient_gas.assign( + region, + offset, + F::from(step.gas_left), + F::from(required_gas), + )?; + + // restore context + self.restore_context + .assign(region, offset, block, call, step, 2)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::test_util::CircuitTestBuilder; + use bus_mapping::{evm::PrecompileCallArgs, precompile::PrecompileCalls}; + use eth_types::{ + bytecode, + evm_types::{GasCost, OpcodeId}, + word, ToWord, + }; + use itertools::Itertools; + use mock::TestContext; + + lazy_static::lazy_static! { + static ref TEST_VECTOR: Vec = { + vec![ + PrecompileCallArgs { + name: "multi-bytes success (more than 32 bytes)", + setup_code: bytecode! { + // place params in memory + PUSH30(word!("0x0123456789abcdef0f1e2d3c4b5a6978")) + PUSH1(0x00) // place from 0x00 in memory + MSTORE + PUSH30(word!("0xaabbccdd001122331039abcdefefef84")) + PUSH1(0x20) // place from 0x20 in memory + MSTORE + }, + // copy 63 bytes from memory addr 0 + call_data_offset: 0x00.into(), + call_data_length: 0x3f.into(), + // return only 35 bytes and write from memory addr 72 + ret_offset: 0x48.into(), + ret_size: 0x23.into(), + address: PrecompileCalls::Identity.address().to_word(), + gas: (PrecompileCalls::Identity.base_gas_cost() + + 2 * GasCost::PRECOMPILE_IDENTITY_PER_WORD + - 1).to_word(), + ..Default::default() + }, + ] + }; + } + + #[test] + fn precompile_oog_test() { + let call_kinds = vec![ + OpcodeId::CALL, + OpcodeId::STATICCALL, + OpcodeId::DELEGATECALL, + OpcodeId::CALLCODE, + ]; + + for (test_vector, &call_kind) in TEST_VECTOR.iter().cartesian_product(&call_kinds) { + let bytecode = test_vector.with_call_op(call_kind); + + CircuitTestBuilder::new_from_test_ctx( + TestContext::<2, 1>::simple_ctx_with_bytecode(bytecode).unwrap(), + ) + .run(); + } + } +} diff --git a/zkevm-circuits/src/evm_circuit/execution/error_precompile_failed.rs b/zkevm-circuits/src/evm_circuit/execution/error_precompile_failed.rs new file mode 100644 index 0000000000..4945564fb1 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/error_precompile_failed.rs @@ -0,0 +1,178 @@ +use crate::{ + evm_circuit::{ + execution::ExecutionGadget, + step::ExecutionState, + util::{ + constraint_builder::{ConstrainBuilderCommon, EVMConstraintBuilder}, + math_gadget::IsZeroGadget, + memory_gadget::{CommonMemoryAddressGadget, MemoryAddressGadget}, + sum, CachedRegion, Cell, Word, + }, + }, + table::CallContextFieldTag, + util::{ + word::{Word32Cell, WordExpr}, + Expr, + }, + witness::{Block, Call, ExecStep, Transaction}, +}; +use bus_mapping::evm::OpcodeId; +use eth_types::{Field, U256}; +use halo2_proofs::{circuit::Value, plonk::Error}; + +#[derive(Clone, Debug)] +pub(crate) struct ErrorPrecompileFailedGadget { + opcode: Cell, + is_call: IsZeroGadget, + is_callcode: IsZeroGadget, + is_delegatecall: IsZeroGadget, + is_staticcall: IsZeroGadget, + gas: Word32Cell, + callee_address: Word32Cell, + value: Word32Cell, + cd_address: MemoryAddressGadget, + rd_address: MemoryAddressGadget, +} + +impl ExecutionGadget for ErrorPrecompileFailedGadget { + const NAME: &'static str = "ErrorPrecompileFailed"; + + const EXECUTION_STATE: ExecutionState = ExecutionState::ErrorPrecompileFailed; + + fn configure(cb: &mut EVMConstraintBuilder) -> Self { + let opcode = cb.query_cell(); + cb.opcode_lookup(opcode.expr(), 1.expr()); + + let is_call = IsZeroGadget::construct(cb, opcode.expr() - OpcodeId::CALL.expr()); + let is_callcode = IsZeroGadget::construct(cb, opcode.expr() - OpcodeId::CALLCODE.expr()); + let is_delegatecall = + IsZeroGadget::construct(cb, opcode.expr() - OpcodeId::DELEGATECALL.expr()); + let is_staticcall = + IsZeroGadget::construct(cb, opcode.expr() - OpcodeId::STATICCALL.expr()); + + // constrain op code + // NOTE: this precompile gadget is for dummy use at the moment, the real error handling for + // precompile will be done in each precompile gadget in the future. won't add step + // state transition constraint here as well. + cb.require_equal( + "opcode is one of [call, callcode, staticcall, delegatecall]", + sum::expr(vec![ + is_call.expr(), + is_callcode.expr(), + is_delegatecall.expr(), + is_staticcall.expr(), + ]), + 1.expr(), + ); + + // Use rw_counter of the step which triggers next call as its call_id. + let callee_call_id = cb.curr.state.rw_counter.clone(); + + let gas = cb.query_word32(); + let callee_address = cb.query_word32(); + let value = cb.query_word32(); + let cd_offset = cb.query_word_unchecked(); + let cd_length = cb.query_memory_address(); + let rd_offset = cb.query_word_unchecked(); + let rd_length = cb.query_memory_address(); + + cb.stack_pop(gas.to_word()); + cb.stack_pop(callee_address.to_word()); + + // `CALL` and `CALLCODE` opcodes have an additional stack pop `value`. + cb.condition(is_call.expr() + is_callcode.expr(), |cb| { + cb.stack_pop(value.to_word()) + }); + cb.stack_pop(cd_offset.to_word()); + cb.stack_pop(cd_length.to_word()); + cb.stack_pop(rd_offset.to_word()); + cb.stack_pop(rd_length.to_word()); + cb.stack_push(Word::zero()); + + for (field_tag, value) in [ + (CallContextFieldTag::LastCalleeId, callee_call_id.expr()), + (CallContextFieldTag::LastCalleeReturnDataOffset, 0.expr()), + (CallContextFieldTag::LastCalleeReturnDataLength, 0.expr()), + ] { + cb.call_context_lookup_write(None, field_tag, Word::from_lo_unchecked(value)); + } + + let cd_address = MemoryAddressGadget::construct(cb, cd_offset, cd_length); + let rd_address = MemoryAddressGadget::construct(cb, rd_offset, rd_length); + + Self { + opcode, + is_call, + is_callcode, + is_delegatecall, + is_staticcall, + gas, + callee_address, + value, + cd_address, + rd_address, + } + } + + fn assign_exec_step( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + block: &Block, + _tx: &Transaction, + _call: &Call, + step: &ExecStep, + ) -> Result<(), Error> { + let opcode = step.opcode().unwrap(); + let is_call_or_callcode = + usize::from([OpcodeId::CALL, OpcodeId::CALLCODE].contains(&opcode)); + + let gas = block.get_rws(step, 0).stack_value(); + let callee_address = block.get_rws(step, 1).stack_value(); + let value = if is_call_or_callcode == 1 { + block.get_rws(step, 2).stack_value() + } else { + U256::zero() + }; + + let [cd_offset, cd_length, rd_offset, rd_length] = [ + block.get_rws(step, is_call_or_callcode + 2).stack_value(), + block.get_rws(step, is_call_or_callcode + 3).stack_value(), + block.get_rws(step, is_call_or_callcode + 4).stack_value(), + block.get_rws(step, is_call_or_callcode + 5).stack_value(), + ]; + + self.opcode + .assign(region, offset, Value::known(F::from(opcode.as_u64())))?; + self.is_call.assign( + region, + offset, + F::from(opcode.as_u64()) - F::from(OpcodeId::CALL.as_u64()), + )?; + self.is_callcode.assign( + region, + offset, + F::from(opcode.as_u64()) - F::from(OpcodeId::CALLCODE.as_u64()), + )?; + self.is_delegatecall.assign( + region, + offset, + F::from(opcode.as_u64()) - F::from(OpcodeId::DELEGATECALL.as_u64()), + )?; + self.is_staticcall.assign( + region, + offset, + F::from(opcode.as_u64()) - F::from(OpcodeId::STATICCALL.as_u64()), + )?; + self.gas.assign_u256(region, offset, gas)?; + self.callee_address + .assign_u256(region, offset, callee_address)?; + self.value.assign_u256(region, offset, value)?; + self.cd_address + .assign(region, offset, cd_offset, cd_length)?; + self.rd_address + .assign(region, offset, rd_offset, rd_length)?; + + Ok(()) + } +} diff --git a/zkevm-circuits/src/evm_circuit/execution/precompiles/identity.rs b/zkevm-circuits/src/evm_circuit/execution/precompiles/identity.rs new file mode 100644 index 0000000000..f39a7f40b5 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/precompiles/identity.rs @@ -0,0 +1,266 @@ +use bus_mapping::circuit_input_builder::Call; +use eth_types::{evm_types::GasCost, Field, ToScalar}; +use gadgets::util::{select, Expr}; +use halo2_proofs::{circuit::Value, plonk::Error}; + +use crate::{ + evm_circuit::{ + execution::ExecutionGadget, + param::{N_BYTES_MEMORY_WORD_SIZE, N_BYTES_WORD}, + step::ExecutionState, + util::{ + common_gadget::RestoreContextGadget, constraint_builder::EVMConstraintBuilder, + math_gadget::ConstantDivisionGadget, CachedRegion, Cell, + }, + }, + table::CallContextFieldTag, + witness::{Block, ExecStep, Transaction}, +}; + +#[derive(Clone, Debug)] +pub struct IdentityGadget { + input_word_size: ConstantDivisionGadget, + is_success: Cell, + callee_address: Cell, + caller_id: Cell, + call_data_offset: Cell, + call_data_length: Cell, + return_data_offset: Cell, + return_data_length: Cell, + restore_context: RestoreContextGadget, +} + +impl ExecutionGadget for IdentityGadget { + const EXECUTION_STATE: ExecutionState = ExecutionState::PrecompileIdentity; + + const NAME: &'static str = "IDENTITY"; + + fn configure(cb: &mut EVMConstraintBuilder) -> Self { + let [is_success, callee_address, caller_id, call_data_offset, call_data_length, return_data_offset, return_data_length] = + [ + CallContextFieldTag::IsSuccess, + CallContextFieldTag::CalleeAddress, + CallContextFieldTag::CallerId, + CallContextFieldTag::CallDataOffset, + CallContextFieldTag::CallDataLength, + CallContextFieldTag::ReturnDataOffset, + CallContextFieldTag::ReturnDataLength, + ] + .map(|tag| cb.call_context(None, tag)); + + let input_word_size = ConstantDivisionGadget::construct( + cb, + call_data_length.expr() + (N_BYTES_WORD - 1).expr(), + N_BYTES_WORD as u64, + ); + + let gas_cost = select::expr( + is_success.expr(), + GasCost::PRECOMPILE_IDENTITY_BASE.expr() + + input_word_size.quotient() * GasCost::PRECOMPILE_IDENTITY_PER_WORD.expr(), + cb.curr.state.gas_left.expr(), + ); + + cb.precompile_info_lookup( + cb.execution_state().as_u64().expr(), + callee_address.expr(), + cb.execution_state().precompile_base_gas_cost().expr(), + ); + + // In the case of Identity precompile, the only failure is in the case of insufficient gas + // for the call, which is diverted and handled in the ErrorOogPrecompile gadget. + + // A separate select statement is not added here, as we expect execution that's verified + // under this specific gadget to always succeed. + let restore_context = RestoreContextGadget::construct2( + cb, + is_success.expr(), + gas_cost.expr(), + 0.expr(), + 0x00.expr(), // ReturnDataOffset + // note: In the case of `Identity` precompile, the only failure is in the case of + // insufficient gas for the call, which is diverted to `ErrorOogPrecompile` + // gadget. Therefore, `call_data_length` can be safely put here without + // conditionals. + call_data_length.expr(), // ReturnDataLength + 0.expr(), + 0.expr(), + ); + + Self { + input_word_size, + is_success, + callee_address, + caller_id, + call_data_offset, + call_data_length, + return_data_offset, + return_data_length, + restore_context, + } + } + + fn assign_exec_step( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + block: &Block, + _tx: &Transaction, + call: &Call, + step: &ExecStep, + ) -> Result<(), Error> { + self.input_word_size.assign( + region, + offset, + (call.call_data_length + (N_BYTES_WORD as u64) - 1).into(), + )?; + self.is_success.assign( + region, + offset, + Value::known(F::from(u64::from(call.is_success))), + )?; + self.callee_address.assign( + region, + offset, + Value::known(call.code_address().unwrap().to_scalar().unwrap()), + )?; + self.caller_id.assign( + region, + offset, + Value::known(F::from(call.caller_id.try_into().unwrap())), + )?; + self.call_data_offset.assign( + region, + offset, + Value::known(F::from(call.call_data_offset)), + )?; + self.call_data_length.assign( + region, + offset, + Value::known(F::from(call.call_data_length)), + )?; + self.return_data_offset.assign( + region, + offset, + Value::known(F::from(call.return_data_offset)), + )?; + self.return_data_length.assign( + region, + offset, + Value::known(F::from(call.return_data_length)), + )?; + self.restore_context + .assign(region, offset, block, call, step, 7)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use bus_mapping::{ + evm::{OpcodeId, PrecompileCallArgs}, + precompile::PrecompileCalls, + }; + use eth_types::{bytecode, word, ToWord}; + use itertools::Itertools; + use mock::TestContext; + + use crate::test_util::CircuitTestBuilder; + + lazy_static::lazy_static! { + static ref TEST_VECTOR: Vec = { + vec![ + PrecompileCallArgs { + name: "single-byte success", + setup_code: bytecode! { + // place params in memory + PUSH1(0xff) + PUSH1(0x00) + MSTORE + }, + call_data_offset: 0x1f.into(), + call_data_length: 0x01.into(), + ret_offset: 0x3f.into(), + ret_size: 0x01.into(), + gas: 0xFFF.into(), + address: PrecompileCalls::Identity.address().to_word(), + ..Default::default() + }, + PrecompileCallArgs { + name: "multi-bytes success (less than 32 bytes)", + setup_code: bytecode! { + // place params in memory + PUSH16(word!("0x0123456789abcdef0f1e2d3c4b5a6978")) + PUSH1(0x00) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x10.into(), + ret_offset: 0x20.into(), + ret_size: 0x10.into(), + gas: 0xFFF.into(), + address: PrecompileCalls::Identity.address().to_word(), + ..Default::default() + }, + PrecompileCallArgs { + name: "multi-bytes success (more than 32 bytes)", + setup_code: bytecode! { + // place params in memory + PUSH30(word!("0x0123456789abcdef0f1e2d3c4b5a6978")) + PUSH1(0x00) // place from 0x00 in memory + MSTORE + PUSH30(word!("0xaabbccdd001122331039abcdefefef84")) + PUSH1(0x20) // place from 0x20 in memory + MSTORE + }, + // copy 63 bytes from memory addr 0 + call_data_offset: 0x00.into(), + call_data_length: 0x3f.into(), + // return only 35 bytes and write from memory addr 72 + ret_offset: 0x48.into(), + ret_size: 0x23.into(), + gas: 0xFFF.into(), + address: PrecompileCalls::Identity.address().to_word(), + ..Default::default() + }, + PrecompileCallArgs { + name: "insufficient gas (precompile call should fail)", + setup_code: bytecode! { + // place params in memory + PUSH16(word!("0x0123456789abcdef0f1e2d3c4b5a6978")) + PUSH1(0x00) + MSTORE + }, + call_data_offset: 0x00.into(), + call_data_length: 0x10.into(), + ret_offset: 0x20.into(), + ret_size: 0x10.into(), + address: PrecompileCalls::Identity.address().to_word(), + // set gas to be insufficient + gas: 2.into(), + ..Default::default() + }, + ] + }; + } + + #[test] + fn precompile_identity_test() { + let call_kinds = vec![ + OpcodeId::CALL, + OpcodeId::STATICCALL, + OpcodeId::DELEGATECALL, + OpcodeId::CALLCODE, + ]; + + for (test_vector, &call_kind) in TEST_VECTOR.iter().cartesian_product(&call_kinds) { + let bytecode = test_vector.with_call_op(call_kind); + + CircuitTestBuilder::new_from_test_ctx( + TestContext::<2, 1>::simple_ctx_with_bytecode(bytecode).unwrap(), + ) + .run(); + } + } +} diff --git a/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs b/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs new file mode 100644 index 0000000000..942ed62ef4 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/precompiles/mod.rs @@ -0,0 +1,2 @@ +mod identity; +pub use identity::IdentityGadget; diff --git a/zkevm-circuits/src/evm_circuit/step.rs b/zkevm-circuits/src/evm_circuit/step.rs index 8cc143b252..ec77d9e996 100644 --- a/zkevm-circuits/src/evm_circuit/step.rs +++ b/zkevm-circuits/src/evm_circuit/step.rs @@ -22,6 +22,7 @@ use bus_mapping::{ circuit_input_builder::ExecState, error::{DepthError, ExecError, InsufficientBalanceError, NonceUintOverflowError, OogError}, evm::OpcodeId, + precompile::PrecompileCalls, }; use eth_types::{evm_unimplemented, Field, ToWord}; use halo2_proofs::{ @@ -32,8 +33,23 @@ use std::{fmt::Display, iter}; use strum::IntoEnumIterator; use strum_macros::EnumIter; -#[allow(missing_docs, reason = "some docs here are tedious and not helpful")] -#[allow(non_camel_case_types)] +impl From for ExecutionState { + fn from(value: PrecompileCalls) -> Self { + match value { + PrecompileCalls::ECRecover => ExecutionState::PrecompileEcRecover, + PrecompileCalls::Sha256 => ExecutionState::PrecompileSha256, + PrecompileCalls::Ripemd160 => ExecutionState::PrecompileRipemd160, + PrecompileCalls::Identity => ExecutionState::PrecompileIdentity, + PrecompileCalls::Modexp => ExecutionState::PrecompileBigModExp, + PrecompileCalls::Bn128Add => ExecutionState::PrecompileBn256Add, + PrecompileCalls::Bn128Mul => ExecutionState::PrecompileBn256ScalarMul, + PrecompileCalls::Bn128Pairing => ExecutionState::PrecompileBn256Pairing, + PrecompileCalls::Blake2F => ExecutionState::PrecompileBlake2f, + } + } +} + +#[allow(non_camel_case_types, missing_docs)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, EnumIter)] /// All the possible execution states that the computation of EVM can arrive. /// Some states are shared by multiple opcodes. @@ -125,6 +141,7 @@ pub enum ExecutionState { ErrorCodeStore, // combine ErrorMaxCodeSizeExceeded and ErrorOutOfGasCodeStore ErrorInvalidJump, ErrorReturnDataOutOfBound, + ErrorPrecompileFailed, ErrorOutOfGasConstant, ErrorOutOfGasStaticMemoryExpansion, ErrorOutOfGasDynamicMemoryExpansion, @@ -136,9 +153,20 @@ pub enum ExecutionState { ErrorOutOfGasSHA3, ErrorOutOfGasEXTCODECOPY, ErrorOutOfGasCall, + ErrorOutOfGasPrecompile, ErrorOutOfGasSloadSstore, ErrorOutOfGasCREATE, ErrorOutOfGasSELFDESTRUCT, + // Precompiles + PrecompileEcRecover, + PrecompileSha256, + PrecompileRipemd160, + PrecompileIdentity, + PrecompileBigModExp, + PrecompileBn256Add, + PrecompileBn256ScalarMul, + PrecompileBn256Pairing, + PrecompileBlake2f, } impl Default for ExecutionState { @@ -181,6 +209,7 @@ impl From<&ExecError> for ExecutionState { ExecError::CodeStoreOutOfGas | ExecError::MaxCodeSizeExceeded => { ExecutionState::ErrorCodeStore } + ExecError::UnimplementedPrecompiles => ExecutionState::ErrorPrecompileFailed, ExecError::OutOfGas(oog_error) => match oog_error { OogError::Constant => ExecutionState::ErrorOutOfGasConstant, OogError::StaticMemoryExpansion => { @@ -196,6 +225,7 @@ impl From<&ExecError> for ExecutionState { OogError::Exp => ExecutionState::ErrorOutOfGasEXP, OogError::Sha3 => ExecutionState::ErrorOutOfGasSHA3, OogError::Call => ExecutionState::ErrorOutOfGasCall, + OogError::Precompile => ExecutionState::ErrorOutOfGasPrecompile, OogError::SloadSstore => ExecutionState::ErrorOutOfGasSloadSstore, OogError::Create => ExecutionState::ErrorOutOfGasCREATE, OogError::SelfDestruct => ExecutionState::ErrorOutOfGasSELFDESTRUCT, @@ -299,6 +329,17 @@ impl From<&ExecStep> for ExecutionState { _ => unimplemented!("unimplemented opcode {:?}", op), } } + ExecState::Precompile(precompile) => match precompile { + PrecompileCalls::ECRecover => ExecutionState::PrecompileEcRecover, + PrecompileCalls::Sha256 => ExecutionState::PrecompileSha256, + PrecompileCalls::Ripemd160 => ExecutionState::PrecompileRipemd160, + PrecompileCalls::Identity => ExecutionState::PrecompileIdentity, + PrecompileCalls::Modexp => ExecutionState::PrecompileBigModExp, + PrecompileCalls::Bn128Add => ExecutionState::PrecompileBn256Add, + PrecompileCalls::Bn128Mul => ExecutionState::PrecompileBn256ScalarMul, + PrecompileCalls::Bn128Pairing => ExecutionState::PrecompileBn256Pairing, + PrecompileCalls::Blake2F => ExecutionState::PrecompileBlake2f, + }, ExecState::BeginTx => ExecutionState::BeginTx, ExecState::EndTx => ExecutionState::EndTx, ExecState::EndBlock => ExecutionState::EndBlock, @@ -326,6 +367,37 @@ impl ExecutionState { Self::iter().count() } + pub(crate) fn is_precompiled(&self) -> bool { + matches!( + self, + Self::PrecompileEcRecover + | Self::PrecompileSha256 + | Self::PrecompileRipemd160 + | Self::PrecompileIdentity + | Self::PrecompileBigModExp + | Self::PrecompileBn256Add + | Self::PrecompileBn256ScalarMul + | Self::PrecompileBn256Pairing + | Self::PrecompileBlake2f + ) + } + + pub(crate) fn precompile_base_gas_cost(&self) -> u64 { + (match self { + Self::PrecompileEcRecover => PrecompileCalls::ECRecover, + Self::PrecompileSha256 => PrecompileCalls::Sha256, + Self::PrecompileRipemd160 => PrecompileCalls::Ripemd160, + Self::PrecompileIdentity => PrecompileCalls::Identity, + Self::PrecompileBigModExp => PrecompileCalls::Modexp, + Self::PrecompileBn256Add => PrecompileCalls::Bn128Add, + Self::PrecompileBn256ScalarMul => PrecompileCalls::Bn128Mul, + Self::PrecompileBn256Pairing => PrecompileCalls::Bn128Pairing, + Self::PrecompileBlake2f => PrecompileCalls::Blake2F, + _ => return 0, + }) + .base_gas_cost() + } + pub(crate) fn halts_in_exception(&self) -> bool { matches!( self, @@ -346,6 +418,7 @@ impl ExecutionState { | Self::ErrorOutOfGasSHA3 | Self::ErrorOutOfGasEXTCODECOPY | Self::ErrorOutOfGasCall + | Self::ErrorOutOfGasPrecompile | Self::ErrorOutOfGasSloadSstore | Self::ErrorOutOfGasCREATE | Self::ErrorOutOfGasSELFDESTRUCT diff --git a/zkevm-circuits/src/evm_circuit/table.rs b/zkevm-circuits/src/evm_circuit/table.rs index 38557ca6fe..b256964342 100644 --- a/zkevm-circuits/src/evm_circuit/table.rs +++ b/zkevm-circuits/src/evm_circuit/table.rs @@ -5,7 +5,7 @@ use crate::{ impl_expr, util::word::Word, }; -use bus_mapping::evm::OpcodeId; +use bus_mapping::{evm::OpcodeId, precompile::PrecompileCalls}; use eth_types::Field; use gadgets::util::Expr; use halo2_proofs::plonk::Expression; @@ -47,6 +47,8 @@ pub enum FixedTableTag { Pow2, /// Lookup constant gas cost for opcodes ConstantGasCost, + /// Preocmpile information + PrecompileInfo, } impl_expr!(FixedTableTag); @@ -137,6 +139,31 @@ impl FixedTableTag { ] }), ), + Self::PrecompileInfo => Box::new( + vec![ + PrecompileCalls::ECRecover, + PrecompileCalls::Sha256, + PrecompileCalls::Ripemd160, + PrecompileCalls::Identity, + PrecompileCalls::Modexp, + PrecompileCalls::Bn128Add, + PrecompileCalls::Bn128Mul, + PrecompileCalls::Bn128Pairing, + PrecompileCalls::Blake2F, + ] + .into_iter() + .map(move |precompile| { + [ + tag, + F::from({ + let state: ExecutionState = precompile.into(); + state.as_u64() + }), + F::from(u64::from(precompile)), + F::from(precompile.base_gas_cost()), + ] + }), + ), } } } diff --git a/zkevm-circuits/src/evm_circuit/util.rs b/zkevm-circuits/src/evm_circuit/util.rs index a857aa4e4d..1ae78336af 100644 --- a/zkevm-circuits/src/evm_circuit/util.rs +++ b/zkevm-circuits/src/evm_circuit/util.rs @@ -27,6 +27,7 @@ pub(crate) mod constraint_builder; pub(crate) mod instrumentation; pub(crate) mod math_gadget; pub(crate) mod memory_gadget; +pub(crate) mod precompile_gadget; pub(crate) mod tx; pub use gadgets::util::{and, not, or, select, sum}; diff --git a/zkevm-circuits/src/evm_circuit/util/common_gadget.rs b/zkevm-circuits/src/evm_circuit/util/common_gadget.rs index f2ba6e1e23..18fb9450da 100644 --- a/zkevm-circuits/src/evm_circuit/util/common_gadget.rs +++ b/zkevm-circuits/src/evm_circuit/util/common_gadget.rs @@ -116,6 +116,30 @@ impl RestoreContextGadget { return_data_length: Expression, memory_expansion_cost: Expression, reversible_write_counter_increase: Expression, + ) -> Self { + Self::construct2( + cb, + is_success, + 0.expr(), + subsequent_rw_lookups, + return_data_offset, + return_data_length, + memory_expansion_cost, + reversible_write_counter_increase, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn construct2( + cb: &mut EVMConstraintBuilder, + is_success: Expression, + gas_cost: Expression, + // Expression for the number of rw lookups that occur after this gadget is constructed. + subsequent_rw_lookups: Expression, + return_data_offset: Expression, + return_data_length: Expression, + memory_expansion_cost: Expression, + reversible_write_counter_increase: Expression, ) -> Self { // Read caller's context for restore let caller_id = cb.call_context(None, CallContextFieldTag::CallerId); @@ -176,6 +200,8 @@ impl RestoreContextGadget { let gas_refund = if cb.execution_state().halts_in_exception() { 0.expr() // no gas refund if call halts in exception + } else if cb.execution_state().is_precompiled() { + cb.curr.state.gas_left.expr() - gas_cost.expr() } else { cb.curr.state.gas_left.expr() - memory_expansion_cost - code_deposit_cost }; @@ -586,6 +612,7 @@ impl TransferGadget { receiver_address: Word>, receiver_exists: Expression, must_create: Expression, + // _prev_code_hash: Word>, value: Word32Cell, reversion_info: &mut ReversionInfo, ) -> Self { @@ -661,6 +688,11 @@ impl TransferGadget { .assign_value(region, offset, Value::known(Word::from(value)))?; Ok(()) } + + pub(crate) fn rw_delta(&self) -> Expression { + // +1 Write Account (sender) Balance + not::expr(self.value_is_zero.expr()) + self.receiver.rw_delta() + } } #[derive(Clone, Debug)] @@ -681,6 +713,12 @@ pub(crate) struct CommonCallGadget, Word>>, pub callee_not_exists: IsZeroWordGadget>, + + // save information + is_call: Expression, + is_callcode: Expression, + _is_delegatecall: Expression, + _is_staticcall: Expression, } impl, const IS_SUCCESS_CALL: bool> @@ -719,7 +757,9 @@ impl, const IS_SUCCESS_CAL cb.stack_pop(callee_address.to_word()); // `CALL` and `CALLCODE` opcodes have an additional stack pop `value`. - cb.condition(is_call + is_callcode, |cb| cb.stack_pop(value.to_word())); + cb.condition(is_call.clone() + is_callcode.clone(), |cb| { + cb.stack_pop(value.to_word()) + }); cb.stack_pop(cd_address.offset_word()); cb.stack_pop(cd_address.length_word()); cb.stack_pop(rd_address.offset_word()); @@ -767,6 +807,10 @@ impl, const IS_SUCCESS_CAL callee_code_hash, is_empty_code_hash, callee_not_exists, + is_call, + is_callcode, + _is_delegatecall: is_delegatecall, + _is_staticcall: is_staticcall, } } @@ -880,6 +924,12 @@ impl, const IS_SUCCESS_CAL Ok(gas_cost) } + + pub(crate) fn rw_delta(&self) -> Expression { + 6.expr() + self.is_call.expr() + self.is_callcode.expr() + // 6 + (is_call + is_callcode) stack pop + 1.expr() + // 1 stack push + 1.expr() // 1 Read Account (callee) CodeHash + } } #[derive(Clone, Debug)] diff --git a/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs b/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs index 7413e890e7..ae15682b3f 100644 --- a/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs +++ b/zkevm-circuits/src/evm_circuit/util/constraint_builder.rs @@ -17,7 +17,7 @@ use crate::{ }; use bus_mapping::{operation::Target, state_db::EMPTY_CODE_HASH_LE}; use eth_types::Field; -use gadgets::util::not; +use gadgets::util::{not, sum}; use halo2_proofs::{ circuit::Value, plonk::{ @@ -147,6 +147,13 @@ impl ReversionInfo { .assign(region, offset, Value::known(F::from(is_persistent as u64)))?; Ok(()) } + + pub(crate) fn rw_delta(&self) -> Expression { + // From definition, rws include: + // Field [`CallContextFieldTag::RwCounterEndOfReversion`] read from call context. + // Field [`CallContextFieldTag::IsPersistent`] read from call context. + 2.expr() + } } pub(crate) trait ConstrainBuilderCommon { @@ -321,6 +328,8 @@ impl<'a, F: Field> ConstrainBuilderCommon for EVMConstraintBuilder<'a, F> { } } +pub(crate) type BoxedClosure<'a, F> = Box) + 'a>; + impl<'a, F: Field> EVMConstraintBuilder<'a, F> { pub(crate) fn new( meta: &'a mut ConstraintSystem, @@ -591,6 +600,22 @@ impl<'a, F: Field> EVMConstraintBuilder<'a, F> { ); } + // precompiled contract information + pub(crate) fn precompile_info_lookup( + &mut self, + execution_state: Expression, + address: Expression, + base_gas_cost: Expression, + ) { + self.add_lookup( + "precompiles info", + Lookup::Fixed { + tag: FixedTableTag::PrecompileInfo.expr(), + values: [execution_state, address, base_gas_cost], + }, + ) + } + // constant gas pub(crate) fn constant_gas_lookup(&mut self, opcode: Expression, gas: Expression) { self.add_lookup( @@ -1414,6 +1439,64 @@ impl<'a, F: Field> EVMConstraintBuilder<'a, F> { ret } + /// Constrain the next step, given mutually exclusive conditions to determine the next state + /// and constrain it using the provided respective constraint. This mechanism is specifically + /// used for constraining the internal states for precompile calls. Each precompile call + /// expects a different cell layout, but since the next state can be at the most one precompile + /// state, we can re-use cells assgined across all thos conditions. + pub(crate) fn constrain_mutually_exclusive_next_step( + &mut self, + conditions: Vec>, + next_states: Vec, + constraints: Vec>, + ) { + assert_eq!(conditions.len(), constraints.len()); + assert_eq!(conditions.len(), next_states.len()); + + self.require_boolean( + "at the most one condition is true from mutually exclusive conditions", + sum::expr(&conditions), + ); + + // TODO: constraining the same cells repeatedly requires a height-resetting mechanism + // on the cell manager. In this case, since only identity precompile is added + // this height management maneuver is temporarily left out. + for ((&next_state, condition), constraint) in next_states + .iter() + .zip(conditions.into_iter()) + .zip(constraints.into_iter()) + { + // constrain the next step. + self.constrain_next_step(next_state, Some(condition), constraint); + } + } + + /// This function needs to be used with extra precaution. You need to make + /// sure the layout is the same as the gadget for `next_step_state`. + /// `query_cell` will return cells in the next step in the `constraint` + /// function. + pub(crate) fn constrain_next_step( + &mut self, + next_step_state: ExecutionState, + condition: Option>, + constraint: impl FnOnce(&mut Self) -> R, + ) -> R { + assert!(!self.in_next_step, "Already in the next step"); + self.in_next_step = true; + let ret = match condition { + None => { + self.require_next_state(next_step_state); + constraint(self) + } + Some(cond) => self.condition(cond, |cb| { + cb.require_next_state(next_step_state); + constraint(cb) + }), + }; + self.in_next_step = false; + ret + } + /// TODO: Doc fn constraint_at_location( &mut self, diff --git a/zkevm-circuits/src/evm_circuit/util/math_gadget.rs b/zkevm-circuits/src/evm_circuit/util/math_gadget.rs index c72c3bf632..060ba59f4b 100644 --- a/zkevm-circuits/src/evm_circuit/util/math_gadget.rs +++ b/zkevm-circuits/src/evm_circuit/util/math_gadget.rs @@ -4,6 +4,7 @@ use halo2_proofs::plonk::Expression; mod abs_word; mod add_words; +mod binary_number; mod byte_size; mod cmp_words; mod comparison; @@ -27,6 +28,7 @@ mod test_util; pub(crate) use abs_word::AbsWordGadget; pub(crate) use add_words::AddWordsGadget; +pub(crate) use binary_number::BinaryNumberGadget; pub(crate) use byte_size::ByteSizeGadget; pub(crate) use cmp_words::CmpWordsGadget; pub(crate) use comparison::ComparisonGadget; diff --git a/zkevm-circuits/src/evm_circuit/util/math_gadget/binary_number.rs b/zkevm-circuits/src/evm_circuit/util/math_gadget/binary_number.rs new file mode 100644 index 0000000000..2fa544104c --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/util/math_gadget/binary_number.rs @@ -0,0 +1,68 @@ +use eth_types::Field; +use gadgets::{binary_number::AsBits, util::Expr}; +use halo2_proofs::{ + circuit::Value, + plonk::{Error, Expression}, +}; + +use crate::evm_circuit::util::{ + constraint_builder::{ConstrainBuilderCommon, EVMConstraintBuilder}, + CachedRegion, Cell, +}; + +#[derive(Clone, Debug)] +pub struct BinaryNumberGadget { + pub(crate) bits: [Cell; N], +} + +impl BinaryNumberGadget { + pub(crate) fn construct(cb: &mut EVMConstraintBuilder, value: Expression) -> Self { + let bits = array_init::array_init(|_| cb.query_bool()); + + // the binary representation of value must be correct. + cb.require_equal( + "binary representation of value should be correct", + value, + bits.iter() + .fold(0.expr(), |res, bit| bit.expr() + res * 2.expr()), + ); + + Self { bits } + } + + pub(crate) fn assign( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + value: T, + ) -> Result<(), Error> + where + T: AsBits, + { + for (c, v) in self.bits.iter().zip(value.as_bits().iter()) { + c.assign(region, offset, Value::known(F::from(*v as u64)))?; + } + Ok(()) + } + + pub(crate) fn _value(&self) -> Expression { + self.bits + .iter() + .fold(0.expr(), |res, bit| bit.expr() + res * 2.expr()) + } + + pub(crate) fn value_equals(&self, other: T) -> Expression + where + T: AsBits, + { + gadgets::util::and::expr(other.as_bits().iter().zip(self.bits.iter()).map( + |(other_bit, self_bit)| { + if *other_bit { + self_bit.expr() + } else { + gadgets::util::not::expr(self_bit.expr()) + } + }, + )) + } +} diff --git a/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs b/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs new file mode 100644 index 0000000000..da0ce68e32 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/util/precompile_gadget.rs @@ -0,0 +1,78 @@ +use bus_mapping::precompile::PrecompileCalls; +use eth_types::Field; +use gadgets::util::{not, Expr}; +use halo2_proofs::plonk::Expression; + +use crate::evm_circuit::step::{ExecutionState, ExecutionState::ErrorOutOfGasPrecompile}; + +use super::{ + constraint_builder::{BoxedClosure, ConstrainBuilderCommon, EVMConstraintBuilder}, + math_gadget::BinaryNumberGadget, + CachedRegion, +}; + +#[derive(Clone, Debug)] +pub struct PrecompileGadget { + address: BinaryNumberGadget, +} + +impl PrecompileGadget { + #[allow(clippy::too_many_arguments)] + pub(crate) fn construct( + cb: &mut EVMConstraintBuilder, + _is_success: Expression, + callee_address: Expression, + _caller_id: Expression, + _cd_offset: Expression, + cd_length: Expression, + _rd_offset: Expression, + _rd_length: Expression, + precompile_return_length: Expression, + // input bytes to precompile call. + _input_bytes_rlc: Expression, + // output result from precompile call. + _output_bytes_rlc: Expression, + // returned bytes back to caller. + _return_bytes_rlc: Expression, + ) -> Self { + let address = BinaryNumberGadget::construct(cb, callee_address.expr()); + + let conditions = vec![ + address.value_equals(PrecompileCalls::Identity), + // match more precompiles + ] + .into_iter() + .map(|cond| { + cond.expr() * not::expr(cb.next.execution_state_selector([ErrorOutOfGasPrecompile])) + }) + .collect::>(); + + let next_states = vec![ + ExecutionState::PrecompileIdentity, // add more precompile execution states + ]; + + let constraints: Vec> = vec![ + Box::new(|cb| { + // Identity + cb.require_equal( + "input length and precompile return length are the same", + cd_length, + precompile_return_length, + ); + }), // add more precompile constraint closures + ]; + + cb.constrain_mutually_exclusive_next_step(conditions, next_states, constraints); + + Self { address } + } + + pub(crate) fn assign( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + address: PrecompileCalls, + ) -> Result<(), halo2_proofs::plonk::Error> { + self.address.assign(region, offset, address) + } +} diff --git a/zkevm-circuits/src/table/copy_table.rs b/zkevm-circuits/src/table/copy_table.rs index 392b916502..9fc4dab9a8 100644 --- a/zkevm-circuits/src/table/copy_table.rs +++ b/zkevm-circuits/src/table/copy_table.rs @@ -176,6 +176,7 @@ impl CopyTable { match (copy_event.src_type, copy_event.dst_type) { (CopyDataType::Memory, CopyDataType::Bytecode) => rlc_acc, (_, CopyDataType::RlcAcc) => rlc_acc, + (CopyDataType::RlcAcc, _) => rlc_acc, _ => Value::known(F::ZERO), }, "rlc_acc",