diff --git a/bus-mapping/src/evm/opcodes/balance.rs b/bus-mapping/src/evm/opcodes/balance.rs index ea16d1882b..e563a1bb13 100644 --- a/bus-mapping/src/evm/opcodes/balance.rs +++ b/bus-mapping/src/evm/opcodes/balance.rs @@ -1,8 +1,9 @@ use crate::circuit_input_builder::{CircuitInputStateRef, ExecStep}; use crate::evm::Opcode; -use crate::operation::{TxAccessListAccountOp, RW}; +use crate::operation::{AccountField, CallContextField, TxAccessListAccountOp, RW}; +use crate::state_db::Account; use crate::Error; -use eth_types::{GethExecStep, ToAddress, ToWord}; +use eth_types::{GethExecStep, ToAddress, ToWord, Word, U256}; #[derive(Debug, Copy, Clone)] pub(crate) struct Balance; @@ -12,16 +13,39 @@ impl Opcode for Balance { state: &mut CircuitInputStateRef, geth_steps: &[GethExecStep], ) -> Result, Error> { - // TODO: finish this, only access list part is done let geth_step = &geth_steps[0]; let mut exec_step = state.new_step(geth_step)?; - let address = geth_steps[0].stack.last()?.to_address(); + // Read account address from stack. + let address = geth_step.stack.last()?.to_address(); state.stack_read( &mut exec_step, geth_step.stack.last_filled(), address.to_word(), )?; + + // Read transaction ID, rw_counter_end_of_reversion, and is_persistent + // from call context. + state.call_context_read( + &mut exec_step, + state.call()?.call_id, + CallContextField::TxId, + U256::from(state.tx_ctx.id()), + ); + state.call_context_read( + &mut exec_step, + state.call()?.call_id, + CallContextField::RwCounterEndOfReversion, + U256::from(state.call()?.rw_counter_end_of_reversion as u64), + ); + state.call_context_read( + &mut exec_step, + state.call()?.call_id, + CallContextField::IsPersistent, + U256::from(state.call()?.is_persistent as u64), + ); + + // Update transaction access list for account address. let is_warm = state.sdb.check_account_in_access_list(&address); state.push_op_reversible( &mut exec_step, @@ -34,6 +58,27 @@ impl Opcode for Balance { }, )?; + // Read account balance. + let (exists, &Account { balance, .. }) = state.sdb.get_account(&address); + if exists { + state.account_read( + &mut exec_step, + address, + AccountField::Balance, + balance, + balance, + )?; + } else { + state.account_read( + &mut exec_step, + address, + AccountField::NonExisting, + Word::zero(), + Word::zero(), + )?; + }; + + // Write the BALANCE result to stack. state.stack_write( &mut exec_step, geth_steps[1].stack.nth_last_filled(0), @@ -47,86 +92,77 @@ impl Opcode for Balance { #[cfg(test)] mod balance_tests { use super::*; - use crate::{ - circuit_input_builder::ExecState, - mock::BlockData, - operation::{StackOp, RW}, - }; - use eth_types::{ - address, bytecode, - evm_types::{OpcodeId, StackAddress}, - geth_types::GethData, - Word, - }; - use mock::eth; - use mock::test_ctx::{helpers::*, TestContext}; + use crate::circuit_input_builder::ExecState; + use crate::mock::BlockData; + use crate::operation::{AccountOp, CallContextOp, StackOp}; + use eth_types::evm_types::{OpcodeId, StackAddress}; + use eth_types::geth_types::GethData; + use eth_types::{address, bytecode, Bytecode, Word, U256}; + use mock::TestContext; use pretty_assertions::assert_eq; - // If the given account doesn't exist, it will push 0 onto the stack instead. #[test] - fn test_balance_of_non_exists_address() { - let address = address!("0x0000000000001111111111111111111111111111"); - - let code = bytecode! { - PUSH32(address.to_word()) - BALANCE - STOP - }; - - // Get the execution steps from the external tracer - let block: GethData = TestContext::<2, 1>::new( - None, - account_0_code_account_1_no_code(code), - tx_from_1_to_0, - |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(); - let call_id = builder.block.txs()[0].calls()[0].call_id; - - let step = builder.block.txs()[0] - .steps() - .iter() - .find(|step| step.exec_state == ExecState::Op(OpcodeId::BALANCE)) - .unwrap(); - - let operation = &builder.block.container.stack[step.bus_mapping_instance[1].as_usize()]; + fn test_balance_of_non_existing_address() { + test_ok(false, false); + } - assert_eq!( - { (operation.rw(), operation.op()) }, - ( - RW::WRITE, - &StackOp::new(call_id, StackAddress::from(1023), Word::zero()) - ) - ); + #[test] + fn test_balance_of_cold_address() { + test_ok(true, false); } #[test] - fn test_balance_of_exists_address() { - let (addr_a, addr_b) = (mock::MOCK_ACCOUNTS[0], mock::MOCK_ACCOUNTS[1]); + fn test_balance_of_warm_address() { + test_ok(true, true); + } + + fn test_ok(exists: bool, is_warm: bool) { + let address = address!("0xaabbccddee000000000000000000000000000000"); - let code = bytecode! { - PUSH32(addr_a.to_word()) + // 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::<2, 1>::new( + // Get the execution steps from the external tracer. + let block: GethData = TestContext::<3, 1>::new( None, |accs| { - accs[0].address(addr_a).balance(eth(10)).code(code); - accs[1].address(addr_b).balance(eth(10)); + 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].from(accs[1].address).to(accs[0].address); + txs[0].to(accs[0].address).from(accs[2].address); }, - |block, _tx| block, + |block, _tx| block.number(0xcafeu64), ) .unwrap() .into(); @@ -136,37 +172,105 @@ mod balance_tests { .handle_block(&block.eth_block, &block.geth_traces) .unwrap(); - let step = builder.block.txs()[0] + // 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 indices = transaction .steps() .iter() - .find(|step| step.exec_state == ExecState::Op(OpcodeId::BALANCE)) - .unwrap(); + .filter(|step| step.exec_state == ExecState::Op(OpcodeId::BALANCE)) + .last() + .unwrap() + .bus_mapping_instance + .clone(); - let call_id = builder.block.txs()[0].calls()[0].call_id; - let balance_a = builder.sdb.get_account(&addr_a).1.balance; + let container = builder.block.container; - assert_eq!(addr_a, block.eth_block.transactions[0].to.unwrap()); + let operation = &container.stack[indices[0].as_usize()]; + assert_eq!(operation.rw(), RW::READ); assert_eq!( - { - let operation = - &builder.block.container.stack[step.bus_mapping_instance[0].as_usize()]; - (operation.rw(), operation.op()) - }, - ( - RW::READ, - &StackOp::new(call_id, StackAddress::from(1023), addr_a.to_word()) - ) + operation.op(), + &StackOp { + call_id, + address: StackAddress::from(1023u32), + value: address.to_word() + } ); + + let operation = &container.call_context[indices[1].as_usize()]; + assert_eq!(operation.rw(), RW::READ); assert_eq!( - { - let operation = - &builder.block.container.stack[step.bus_mapping_instance[1].as_usize()]; - (operation.rw(), operation.op()) - }, - ( - RW::WRITE, - &StackOp::new(call_id, StackAddress::from(1023), balance_a) - ) + operation.op(), + &CallContextOp { + call_id, + field: CallContextField::TxId, + value: tx_id.into() + } + ); + + let operation = &container.call_context[indices[2].as_usize()]; + assert_eq!(operation.rw(), RW::READ); + assert_eq!( + operation.op(), + &CallContextOp { + call_id, + field: CallContextField::RwCounterEndOfReversion, + value: U256::zero() + } + ); + + let operation = &container.call_context[indices[3].as_usize()]; + assert_eq!(operation.rw(), RW::READ); + assert_eq!( + operation.op(), + &CallContextOp { + call_id, + field: CallContextField::IsPersistent, + value: U256::one() + } + ); + + let operation = &container.tx_access_list_account[indices[4].as_usize()]; + assert_eq!(operation.rw(), RW::WRITE); + assert_eq!( + operation.op(), + &TxAccessListAccountOp { + tx_id, + address, + is_warm: true, + is_warm_prev: is_warm + } + ); + + let operation = &container.account[indices[5].as_usize()]; + assert_eq!(operation.rw(), RW::READ); + assert_eq!( + operation.op(), + &AccountOp { + address, + field: if exists { + AccountField::Balance + } else { + AccountField::NonExisting + }, + value: balance, + value_prev: balance, + } + ); + + let operation = &container.stack[indices[6].as_usize()]; + assert_eq!(operation.rw(), RW::WRITE); + assert_eq!( + operation.op(), + &StackOp { + call_id, + address: 1023u32.into(), + value: balance, + } ); } } diff --git a/zkevm-circuits/src/evm_circuit/execution.rs b/zkevm-circuits/src/evm_circuit/execution.rs index ea3ad3ebe6..577bb5e0dc 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -30,6 +30,7 @@ use strum::IntoEnumIterator; mod add_sub; mod addmod; mod address; +mod balance; mod begin_tx; mod bitwise; mod block_ctx; @@ -91,6 +92,7 @@ use self::sha3::Sha3Gadget; use add_sub::AddSubGadget; use addmod::AddModGadget; use address::AddressGadget; +use balance::BalanceGadget; use begin_tx::BeginTxGadget; use bitwise::BitwiseGadget; use block_ctx::{BlockCtxU160Gadget, BlockCtxU256Gadget, BlockCtxU64Gadget}; @@ -192,6 +194,7 @@ pub(crate) struct ExecutionConfig { add_sub_gadget: AddSubGadget, addmod_gadget: AddModGadget, address_gadget: AddressGadget, + balance_gadget: BalanceGadget, bitwise_gadget: BitwiseGadget, byte_gadget: ByteGadget, call_op_gadget: CallOpGadget, @@ -228,7 +231,6 @@ pub(crate) struct ExecutionConfig { selfbalance_gadget: SelfbalanceGadget, sha3_gadget: Sha3Gadget, shl_shr_gadget: ShlShrGadget, - balance_gadget: DummyGadget, sar_gadget: DummyGadget, extcodesize_gadget: DummyGadget, extcodecopy_gadget: DummyGadget, @@ -972,6 +974,7 @@ impl ExecutionConfig { ExecutionState::ADD_SUB => assign_exec_step!(self.add_sub_gadget), ExecutionState::ADDMOD => assign_exec_step!(self.addmod_gadget), ExecutionState::ADDRESS => assign_exec_step!(self.address_gadget), + ExecutionState::BALANCE => assign_exec_step!(self.balance_gadget), ExecutionState::BITWISE => assign_exec_step!(self.bitwise_gadget), ExecutionState::BYTE => assign_exec_step!(self.byte_gadget), ExecutionState::CALL_OP => assign_exec_step!(self.call_op_gadget), @@ -1014,7 +1017,6 @@ impl ExecutionConfig { ExecutionState::BLOCKHASH => assign_exec_step!(self.blockhash_gadget), ExecutionState::SELFBALANCE => assign_exec_step!(self.selfbalance_gadget), // dummy gadgets - ExecutionState::BALANCE => assign_exec_step!(self.balance_gadget), ExecutionState::SAR => assign_exec_step!(self.sar_gadget), ExecutionState::EXTCODESIZE => assign_exec_step!(self.extcodesize_gadget), ExecutionState::EXTCODECOPY => assign_exec_step!(self.extcodecopy_gadget), diff --git a/zkevm-circuits/src/evm_circuit/execution/balance.rs b/zkevm-circuits/src/evm_circuit/execution/balance.rs new file mode 100644 index 0000000000..8dc3054f89 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/balance.rs @@ -0,0 +1,315 @@ +use crate::evm_circuit::execution::ExecutionGadget; +use crate::evm_circuit::param::N_BYTES_ACCOUNT_ADDRESS; +use crate::evm_circuit::step::ExecutionState; +use crate::evm_circuit::util::common_gadget::SameContextGadget; +use crate::evm_circuit::util::constraint_builder::Transition::Delta; +use crate::evm_circuit::util::constraint_builder::{ + ConstraintBuilder, ReversionInfo, StepStateTransition, +}; +use crate::evm_circuit::util::{from_bytes, select, CachedRegion, Cell, RandomLinearCombination}; +use crate::evm_circuit::witness::{Block, Call, ExecStep, Rw, Transaction}; +use crate::table::{AccountFieldTag, CallContextFieldTag}; +use crate::util::Expr; +use eth_types::evm_types::GasCost; +use eth_types::{Field, ToAddress, ToLittleEndian}; +use halo2_proofs::circuit::Value; +use halo2_proofs::plonk::Error; + +#[derive(Clone, Debug)] +pub(crate) struct BalanceGadget { + same_context: SameContextGadget, + address: RandomLinearCombination, + reversion_info: ReversionInfo, + tx_id: Cell, + is_warm: Cell, + balance: Cell, + exists: Cell, +} + +impl ExecutionGadget for BalanceGadget { + const NAME: &'static str = "BALANCE"; + + const EXECUTION_STATE: ExecutionState = ExecutionState::BALANCE; + + fn configure(cb: &mut ConstraintBuilder) -> Self { + let address = cb.query_rlc(); + cb.stack_pop(address.expr()); + + let tx_id = cb.call_context(None, CallContextFieldTag::TxId); + let mut reversion_info = cb.reversion_info_read(None); + let is_warm = cb.query_bool(); + cb.account_access_list_write( + tx_id.expr(), + from_bytes::expr(&address.cells), + 1.expr(), + is_warm.expr(), + Some(&mut reversion_info), + ); + + let balance = cb.query_cell(); + let exists = cb.query_bool(); + cb.condition(exists.expr(), |cb| { + cb.account_read( + from_bytes::expr(&address.cells), + AccountFieldTag::Balance, + balance.expr(), + ); + }); + cb.condition(1.expr() - exists.expr(), |cb| { + cb.account_read( + from_bytes::expr(&address.cells), + AccountFieldTag::NonExisting, + 0.expr(), + ); + }); + + cb.stack_push(select::expr(exists.expr(), balance.expr(), 0.expr())); + + let gas_cost = select::expr( + is_warm.expr(), + GasCost::WARM_ACCESS.expr(), + GasCost::COLD_ACCOUNT_ACCESS.expr(), + ); + + let step_state_transition = StepStateTransition { + rw_counter: Delta(7.expr()), + program_counter: Delta(1.expr()), + stack_pointer: Delta(0.expr()), + gas_left: Delta(-gas_cost), + reversible_write_counter: Delta(1.expr()), + ..Default::default() + }; + + let opcode = cb.query_cell(); + let same_context = SameContextGadget::construct(cb, opcode, step_state_transition); + + Self { + same_context, + address, + reversion_info, + tx_id, + is_warm, + balance, + exists, + } + } + + fn assign_exec_step( + &self, + region: &mut CachedRegion<'_, '_, F>, + offset: usize, + block: &Block, + tx: &Transaction, + call: &Call, + step: &ExecStep, + ) -> Result<(), Error> { + self.same_context.assign_exec_step(region, offset, step)?; + + let address = block.rws[step.rw_indices[0]].stack_value().to_address(); + let mut address_bytes = address.0; + address_bytes.reverse(); + self.address.assign(region, offset, Some(address_bytes))?; + + self.tx_id + .assign(region, offset, Value::known(F::from(tx.id as u64)))?; + + self.reversion_info.assign( + region, + offset, + call.rw_counter_end_of_reversion, + call.is_persistent, + )?; + + let (_, is_warm) = block.rws[step.rw_indices[4]].tx_access_list_value_pair(); + self.is_warm + .assign(region, offset, Value::known(F::from(is_warm)))?; + + let (balance, exists) = match block.rws[step.rw_indices[5]] { + Rw::Account { + field_tag: AccountFieldTag::Balance, + value, + .. + } => ( + RandomLinearCombination::random_linear_combine( + value.to_le_bytes(), + block.randomness, + ), + true, + ), + Rw::Account { + field_tag: AccountFieldTag::NonExisting, + .. + } => (F::zero(), false), + _ => unreachable!(), + }; + + self.balance.assign(region, offset, Value::known(balance))?; + self.exists + .assign(region, offset, Value::known(F::from(exists)))?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::evm_circuit::test::rand_bytes; + use crate::test_util::run_test_circuits; + use eth_types::geth_types::Account; + use eth_types::{address, bytecode, Address, Bytecode, ToWord, Word, U256}; + use lazy_static::lazy_static; + use mock::TestContext; + + lazy_static! { + static ref TEST_ADDRESS: Address = address!("0xaabbccddee000000000000000000000000000000"); + } + + #[test] + fn balance_gadget_non_existing_account() { + test_root_ok(&None, false); + test_internal_ok(0x20, 0x00, &None, false); + test_internal_ok(0x1010, 0xff, &None, false); + } + + #[test] + fn balance_gadget_cold_account() { + let account = Some(Account { + address: *TEST_ADDRESS, + balance: U256::from(900), + ..Default::default() + }); + + test_root_ok(&account, false); + test_internal_ok(0x20, 0x00, &account, false); + test_internal_ok(0x1010, 0xff, &account, false); + } + + #[test] + fn balance_gadget_warm_account() { + let account = Some(Account { + address: *TEST_ADDRESS, + balance: U256::from(900), + ..Default::default() + }); + + test_root_ok(&account, true); + test_internal_ok(0x20, 0x00, &account, true); + test_internal_ok(0x1010, 0xff, &account, true); + } + + fn test_root_ok(account: &Option, 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.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + POP + }); + } + code.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + STOP + }); + + 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); + }, + |block, _tx| block, + ) + .unwrap(); + + assert_eq!(run_test_circuits(ctx, None), Ok(())); + } + + fn test_internal_ok( + call_data_offset: usize, + call_data_length: usize, + account: &Option, + 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.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + POP + }); + } + code_b.append(&bytecode! { + PUSH20(address.to_word()) + BALANCE + 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(0x1_0000) // gas + 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(); + + assert_eq!(run_test_circuits(ctx, None), Ok(())); + } +} diff --git a/zkevm-circuits/src/witness/step.rs b/zkevm-circuits/src/witness/step.rs index 848944d6e8..f342ed0ecf 100644 --- a/zkevm-circuits/src/witness/step.rs +++ b/zkevm-circuits/src/witness/step.rs @@ -126,6 +126,7 @@ impl From<&circuit_input_builder::ExecStep> for ExecutionState { OpcodeId::ADD | OpcodeId::SUB => ExecutionState::ADD_SUB, OpcodeId::ADDMOD => ExecutionState::ADDMOD, OpcodeId::ADDRESS => ExecutionState::ADDRESS, + OpcodeId::BALANCE => ExecutionState::BALANCE, OpcodeId::MUL | OpcodeId::DIV | OpcodeId::MOD => ExecutionState::MUL_DIV_MOD, OpcodeId::MULMOD => ExecutionState::MULMOD, OpcodeId::SDIV | OpcodeId::SMOD => ExecutionState::SDIV_SMOD, @@ -181,7 +182,6 @@ impl From<&circuit_input_builder::ExecStep> for ExecutionState { OpcodeId::RETURNDATASIZE => ExecutionState::RETURNDATASIZE, OpcodeId::RETURNDATACOPY => ExecutionState::RETURNDATACOPY, // dummy ops - OpcodeId::BALANCE => dummy!(ExecutionState::BALANCE), OpcodeId::SAR => dummy!(ExecutionState::SAR), OpcodeId::EXTCODESIZE => dummy!(ExecutionState::EXTCODESIZE), OpcodeId::EXTCODECOPY => dummy!(ExecutionState::EXTCODECOPY),