From 3d406bf990d35f069a4594b6070ea064462c2747 Mon Sep 17 00:00:00 2001 From: DreamWuGit Date: Tue, 4 Apr 2023 16:47:59 +0800 Subject: [PATCH] Circuit for oog account access error (#426) * add buss mapping * implement circuit gadget * fix oog account root test * test EXTCODEHASH * refactor root tests * misc update * buss mapping test --- bus-mapping/src/evm/opcodes.rs | 5 + .../evm/opcodes/error_oog_account_access.rs | 176 ++++++++++ zkevm-circuits/src/evm_circuit/execution.rs | 5 +- .../execution/error_oog_account_access.rs | 312 ++++++++++++++++++ 4 files changed, 496 insertions(+), 2 deletions(-) create mode 100644 bus-mapping/src/evm/opcodes/error_oog_account_access.rs create mode 100644 zkevm-circuits/src/evm_circuit/execution/error_oog_account_access.rs diff --git a/bus-mapping/src/evm/opcodes.rs b/bus-mapping/src/evm/opcodes.rs index 46f4b9c5bd..a5f8029def 100644 --- a/bus-mapping/src/evm/opcodes.rs +++ b/bus-mapping/src/evm/opcodes.rs @@ -60,6 +60,7 @@ mod error_codestore; mod error_contract_address_collision; mod error_invalid_creation_code; mod error_invalid_jump; +mod error_oog_account_access; mod error_oog_call; mod error_oog_dynamic_memory; mod error_oog_log; @@ -89,6 +90,7 @@ use dup::Dup; use error_codestore::ErrorCodeStore; use error_invalid_creation_code::ErrorCreationCode; use error_invalid_jump::InvalidJump; +use error_oog_account_access::ErrorOOGAccountAccess; use error_oog_call::OOGCall; use error_oog_dynamic_memory::OOGDynamicMemory; use error_oog_log::ErrorOOGLog; @@ -306,6 +308,9 @@ fn fn_gen_error_state_associated_ops( Some(StackOnlyOpcode::<2, 0, true>::gen_associated_ops) } ExecError::OutOfGas(OogError::SloadSstore) => Some(OOGSloadSstore::gen_associated_ops), + ExecError::OutOfGas(OogError::AccountAccess) => { + Some(ErrorOOGAccountAccess::gen_associated_ops) + } // ExecError:: ExecError::StackOverflow => Some(StackOnlyOpcode::<0, 0, true>::gen_associated_ops), ExecError::StackUnderflow => Some(StackOnlyOpcode::<0, 0, true>::gen_associated_ops), diff --git a/bus-mapping/src/evm/opcodes/error_oog_account_access.rs b/bus-mapping/src/evm/opcodes/error_oog_account_access.rs new file mode 100644 index 0000000000..2506d28bfb --- /dev/null +++ b/bus-mapping/src/evm/opcodes/error_oog_account_access.rs @@ -0,0 +1,176 @@ +use crate::{ + circuit_input_builder::{CircuitInputStateRef, ExecStep}, + error::{ExecError, OogError}, + evm::{Opcode, OpcodeId}, + operation::{CallContextField, TxAccessListAccountOp, RW}, + Error, +}; +use eth_types::{GethExecStep, ToAddress, U256}; + +#[derive(Debug, Copy, Clone)] +pub struct ErrorOOGAccountAccess; + +impl Opcode for ErrorOOGAccountAccess { + fn gen_associated_ops( + state: &mut CircuitInputStateRef, + geth_steps: &[GethExecStep], + ) -> Result, Error> { + let geth_step = &geth_steps[0]; + let mut exec_step = state.new_step(geth_step)?; + exec_step.error = Some(ExecError::OutOfGas(OogError::AccountAccess)); + + // assert op code is BALANCE | EXTCODESIZE | EXTCODEHASH + assert!([ + OpcodeId::BALANCE, + OpcodeId::EXTCODESIZE, + OpcodeId::EXTCODEHASH + ] + .contains(&geth_step.op)); + // Read account address from stack. + let address_word = geth_step.stack.last()?; + let address = address_word.to_address(); + state.stack_read(&mut exec_step, geth_step.stack.last_filled(), address_word)?; + + // Read transaction ID from call context. + state.call_context_read( + &mut exec_step, + state.call()?.call_id, + CallContextField::TxId, + U256::from(state.tx_ctx.id()), + ); + + // transaction access list for account address. + let is_warm = state.sdb.check_account_in_access_list(&address); + // read `is_warm` state + state.push_op( + &mut exec_step, + RW::READ, + TxAccessListAccountOp { + tx_id: state.tx_ctx.id(), + address, + is_warm, + is_warm_prev: is_warm, + }, + ); + + // common error handling + state.gen_restore_context_ops(&mut exec_step, geth_steps)?; + state.handle_return(geth_step)?; + Ok(vec![exec_step]) + } +} + +#[cfg(test)] +mod oog_account_access_tests { + use crate::{ + circuit_input_builder::ExecState, + error::{ExecError, OogError}, + mock::BlockData, + operation::{StackOp, RW}, + }; + use eth_types::{ + address, bytecode, evm_types::OpcodeId, geth_types::GethData, Bytecode, ToWord, Word, + }; + use mock::TestContext; + use pretty_assertions::assert_eq; + + #[test] + fn test_balance_of_warm_address() { + test_ok(true, false); + test_ok(false, false); + test_ok(true, true); + } + + // test balance opcode as an example + fn test_ok(exists: bool, is_warm: bool) { + let address = address!("0xaabbccddee000000000000000000000000000000"); + + // Pop balance first for warm account. + let mut code = Bytecode::default(); + if is_warm { + code.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + POP + }); + } + code.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + STOP + }); + + let balance = if exists { + Word::from(800u64) + } else { + Word::zero() + }; + + // Get the execution steps from the external tracer. + let block: GethData = TestContext::<3, 1>::new( + None, + |accs| { + accs[0] + .address(address!("0x0000000000000000000000000000000000000010")) + .balance(Word::from(1u64 << 20)) + .code(code.clone()); + if exists { + accs[1].address(address).balance(balance); + } else { + accs[1] + .address(address!("0x0000000000000000000000000000000000000020")) + .balance(Word::from(1u64 << 20)); + } + accs[2] + .address(address!("0x0000000000000000000000000000000000cafe01")) + .balance(Word::from(1u64 << 20)); + }, + |mut txs, accs| { + txs[0] + .to(accs[0].address) + .from(accs[2].address) + .gas(21005.into()); + }, + |block, _tx| block.number(0xcafeu64), + ) + .unwrap() + .into(); + + let mut builder = BlockData::new_from_geth_data(block.clone()).new_circuit_input_builder(); + builder + .handle_block(&block.eth_block, &block.geth_traces) + .unwrap(); + + // Check if account address is in access list as a result of bus mapping. + assert!(builder.sdb.add_account_to_access_list(address)); + + let tx_id = 1; + let transaction = &builder.block.txs()[tx_id - 1]; + let call_id = transaction.calls()[0].call_id; + + let step = transaction + .steps() + .iter() + .filter(|step| step.exec_state == ExecState::Op(OpcodeId::BALANCE)) + .last() + .unwrap(); + + // check expected error occurs + assert_eq!( + step.error, + Some(ExecError::OutOfGas(OogError::AccountAccess)) + ); + + let container = builder.block.container.clone(); + let operation = &container.stack[step.bus_mapping_instance[0].as_usize()]; + assert_eq!(operation.rw(), RW::READ); + assert_eq!( + operation.op(), + &StackOp { + call_id, + address: 1023.into(), + value: address.to_word(), + } + ); + } +} diff --git a/zkevm-circuits/src/evm_circuit/execution.rs b/zkevm-circuits/src/evm_circuit/execution.rs index c580eb7ba3..db282a3294 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -81,6 +81,7 @@ mod error_code_store; mod error_invalid_creation_code; mod error_invalid_jump; mod error_invalid_opcode; +mod error_oog_account_access; mod error_oog_call; mod error_oog_constant; mod error_oog_create2; @@ -161,6 +162,7 @@ use error_code_store::ErrorCodeStoreGadget; use error_invalid_creation_code::ErrorInvalidCreationCodeGadget; use error_invalid_jump::ErrorInvalidJumpGadget; use error_invalid_opcode::ErrorInvalidOpcodeGadget; +use error_oog_account_access::ErrorOOGAccountAccessGadget; use error_oog_call::ErrorOOGCallGadget; use error_oog_constant::ErrorOOGConstantGadget; use error_oog_create2::ErrorOOGCreate2Gadget; @@ -324,8 +326,7 @@ pub(crate) struct ExecutionConfig { error_write_protection: Box>, error_oog_dynamic_memory_gadget: Box>, error_oog_log: Box>, - error_oog_account_access: - Box>, + error_oog_account_access: Box>, error_oog_sha3: Box>, error_oog_create2: Box>, error_code_store: Box>, diff --git a/zkevm-circuits/src/evm_circuit/execution/error_oog_account_access.rs b/zkevm-circuits/src/evm_circuit/execution/error_oog_account_access.rs new file mode 100644 index 0000000000..5fff18a561 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/error_oog_account_access.rs @@ -0,0 +1,312 @@ +use crate::{ + evm_circuit::{ + execution::ExecutionGadget, + param::{N_BYTES_ACCOUNT_ADDRESS, N_BYTES_GAS}, + step::ExecutionState, + util::{ + common_gadget::CommonErrorGadget, constraint_builder::ConstraintBuilder, from_bytes, + math_gadget::LtGadget, select, CachedRegion, Cell, Word, + }, + witness::{Block, Call, ExecStep, Transaction}, + }, + table::CallContextFieldTag, + util::Expr, +}; +use eth_types::{ + evm_types::{GasCost, OpcodeId}, + Field, ToLittleEndian, +}; +use halo2_proofs::{circuit::Value, plonk::Error}; + +/// Gadget to implement the corresponding out of gas errors for +/// [`OpcodeId::EXP`]. +#[derive(Clone, Debug)] +pub(crate) struct ErrorOOGAccountAccessGadget { + opcode: Cell, + address_word: Word, + tx_id: Cell, + is_warm: Cell, + insufficient_gas_cost: LtGadget, + common_error_gadget: CommonErrorGadget, +} + +impl ExecutionGadget for ErrorOOGAccountAccessGadget { + const NAME: &'static str = "ErrorOutOfGasAccountAccess"; + + const EXECUTION_STATE: ExecutionState = ExecutionState::ErrorOutOfGasAccountAccess; + + fn configure(cb: &mut ConstraintBuilder) -> Self { + let opcode = cb.query_cell(); + cb.require_in_set( + "ErrorOutOfGasAccountAccess happens for BALANCE | EXTCODESIZE | EXTCODEHASH ", + opcode.expr(), + vec![ + OpcodeId::BALANCE.expr(), + OpcodeId::EXTCODESIZE.expr(), + OpcodeId::EXTCODEHASH.expr(), + ], + ); + + let address_word = cb.query_word_rlc(); + let address = from_bytes::expr(&address_word.cells[..N_BYTES_ACCOUNT_ADDRESS]); + cb.stack_pop(address_word.expr()); + + let tx_id = cb.call_context(None, CallContextFieldTag::TxId); + let is_warm = cb.query_bool(); + // read is_warm + cb.account_access_list_read(tx_id.expr(), address.expr(), is_warm.expr()); + + let gas_cost = select::expr( + is_warm.expr(), + GasCost::WARM_ACCESS.expr(), + GasCost::COLD_ACCOUNT_ACCESS.expr(), + ); + + let insufficient_gas_cost = + LtGadget::construct(cb, cb.curr.state.gas_left.expr(), gas_cost); + + cb.require_equal( + "Gas left is less than gas cost", + insufficient_gas_cost.expr(), + 1.expr(), + ); + + let common_error_gadget = CommonErrorGadget::construct(cb, opcode.expr(), 5.expr()); + Self { + opcode, + address_word, + tx_id, + is_warm, + insufficient_gas_cost, + common_error_gadget, + } + } + + 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(); + self.opcode + .assign(region, offset, Value::known(F::from(opcode.as_u64())))?; + + let address = block.rws[step.rw_indices[0]].stack_value(); + self.address_word + .assign(region, offset, Some(address.to_le_bytes()))?; + + self.tx_id + .assign(region, offset, Value::known(F::from(tx.id as u64)))?; + + let (_, is_warm) = block.rws[step.rw_indices[2]].tx_access_list_value_pair(); + self.is_warm + .assign(region, offset, Value::known(F::from(is_warm)))?; + + // BALANCE EXTCODESIZE EXTCODEHASH shares same gas cost model + let gas_cost = if is_warm { + GasCost::WARM_ACCESS + } else { + GasCost::COLD_ACCOUNT_ACCESS + }; + + self.insufficient_gas_cost.assign_value( + region, + offset, + Value::known(F::from(step.gas_left)), + Value::known(F::from(gas_cost.as_u64())), + )?; + self.common_error_gadget + .assign(region, offset, block, call, step, 5)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::{evm_circuit::test::rand_bytes, test_util::CircuitTestBuilder}; + use eth_types::{ + address, bytecode, + evm_types::{GasCost, OpcodeId}, + geth_types::Account, + Address, Bytecode, ToWord, Word, U256, + }; + use itertools::Itertools; + use lazy_static::lazy_static; + use mock::TestContext; + + lazy_static! { + static ref TEST_ADDRESS: Address = address!("0xaabbccddee000000000000000000000000000000"); + } + + #[test] + fn oog_account_access_root() { + let account = Some(Account { + address: *TEST_ADDRESS, + balance: U256::from(900), + ..Default::default() + }); + + for (opcode, is_warm) in [ + OpcodeId::BALANCE, + OpcodeId::EXTCODESIZE, + OpcodeId::EXTCODEHASH, + ] + .iter() + .cartesian_product([true, false]) + { + test_root_ok(&account, *opcode, is_warm); + } + } + + #[test] + fn oog_account_internal() { + let account = Some(Account { + address: *TEST_ADDRESS, + balance: U256::from(900), + ..Default::default() + }); + + for (opcode, is_warm) in [ + OpcodeId::BALANCE, + OpcodeId::EXTCODESIZE, + OpcodeId::EXTCODEHASH, + ] + .iter() + .cartesian_product([false, true]) + { + test_internal_ok(0x20, 0x20, &account, *opcode, is_warm); + test_internal_ok(0x1010, 0xff, &account, *opcode, is_warm); + } + } + + fn test_root_ok(account: &Option, opcode: OpcodeId, is_warm: bool) { + let address = account.as_ref().map(|a| a.address).unwrap_or(*TEST_ADDRESS); + + let mut code = Bytecode::default(); + if is_warm { + code.push(20, address.to_word()); + code.write_op(opcode); + code.write_op(OpcodeId::POP); + } + + code.push(20, address.to_word()); + code.write_op(opcode); + code.write_op(OpcodeId::STOP); + + let gas = GasCost::TX.0 + + if is_warm { + GasCost::WARM_ACCESS.as_u64() + + OpcodeId::PUSH32.constant_gas_cost().0 + + OpcodeId::POP.constant_gas_cost().0 + + GasCost::COLD_ACCOUNT_ACCESS.as_u64() + } else { + GasCost::COLD_ACCOUNT_ACCESS.as_u64() + } + + OpcodeId::PUSH32.constant_gas_cost().0 + - 1; + let ctx = TestContext::<3, 1>::new( + None, + |accs| { + accs[0] + .address(address!("0x000000000000000000000000000000000000cafe")) + .balance(Word::from(1_u64 << 20)) + .code(code); + // Set balance if account exists. + if let Some(account) = account { + accs[1].address(address).balance(account.balance); + } else { + accs[1] + .address(address!("0x0000000000000000000000000000000000000010")) + .balance(Word::from(1_u64 << 20)); + } + accs[2] + .address(address!("0x0000000000000000000000000000000000000020")) + .balance(Word::from(1_u64 << 20)); + }, + |mut txs, accs| { + txs[0] + .to(accs[0].address) + .from(accs[2].address) + .gas(gas.into()); + }, + |block, _tx| block, + ) + .unwrap(); + + CircuitTestBuilder::new_from_test_ctx(ctx).run(); + } + + fn test_internal_ok( + call_data_offset: usize, + call_data_length: usize, + account: &Option, + opcode: OpcodeId, + is_warm: bool, + ) { + let address = account.as_ref().map(|a| a.address).unwrap_or(*TEST_ADDRESS); + let (addr_a, addr_b) = (mock::MOCK_ACCOUNTS[0], mock::MOCK_ACCOUNTS[1]); + + // code B gets called by code A, so the call is an internal call. + let mut code_b = Bytecode::default(); + if is_warm { + code_b.push(20, address.to_word()); + + code_b.write_op(opcode); + code_b.write_op(OpcodeId::POP); + } + + code_b.push(20, address.to_word()); + code_b.write_op(opcode); + code_b.write_op(OpcodeId::STOP); + + // code A calls code B. + let pushdata = rand_bytes(8); + let code_a = bytecode! { + // populate memory in A's context. + PUSH8(Word::from_big_endian(&pushdata)) + PUSH1(0x00) // offset + MSTORE + // call ADDR_B. + PUSH1(0x00) // retLength + PUSH1(0x00) // retOffset + PUSH32(call_data_length) // argsLength + PUSH32(call_data_offset) // argsOffset + PUSH1(0x00) // value + PUSH32(addr_b.to_word()) // addr + PUSH32(Word::from(2599u64)) // gas insufficient + CALL + STOP + }; + + let ctx = TestContext::<4, 1>::new( + None, + |accs| { + accs[0].address(addr_b).code(code_b); + accs[1].address(addr_a).code(code_a); + // Set balance if account exists. + if let Some(account) = account { + accs[2].address(address).balance(account.balance); + } else { + accs[2] + .address(mock::MOCK_ACCOUNTS[2]) + .balance(Word::from(1_u64 << 20)); + } + accs[3] + .address(mock::MOCK_ACCOUNTS[3]) + .balance(Word::from(1_u64 << 20)); + }, + |mut txs, accs| { + txs[0].to(accs[1].address).from(accs[3].address); + }, + |block, _tx| block, + ) + .unwrap(); + + CircuitTestBuilder::new_from_test_ctx(ctx).run(); + } +}