diff --git a/bus-mapping/src/evm/opcodes/balance.rs b/bus-mapping/src/evm/opcodes/balance.rs index f578e8f454..73a36f6c9b 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, 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,17 @@ impl Opcode for Balance { }, )?; + // Read account balance. + let &Account { balance, .. } = state.sdb.get_account(&address).1; + state.account_read( + &mut exec_step, + address, + AccountField::Balance, + balance, + balance, + )?; + + // Write the BALANCE result to stack. state.stack_write( &mut exec_step, geth_steps[1].stack.nth_last_filled(0), @@ -43,3 +78,179 @@ impl Opcode for Balance { Ok(vec![exec_step]) } } + +#[cfg(test)] +mod balance_tests { + use super::*; + 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; + + #[test] + fn test_balance_of_non_existing_address() { + test_ok(false, false); + } + + #[test] + fn test_balance_of_cold_address() { + test_ok(true, false); + } + + #[test] + fn test_balance_of_warm_address() { + test_ok(true, true); + } + + fn test_ok(is_existing: 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 is_existing { + 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()); + accs[1].address(address).balance(balance); + accs[2] + .address(address!("0x0000000000000000000000000000000000cafe01")) + .balance(Word::from(1u64 << 20)); + }, + |mut txs, accs| { + txs[0].to(accs[0].address).from(accs[2].address); + }, + |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 indices = transaction + .steps() + .iter() + .filter(|step| step.exec_state == ExecState::Op(OpcodeId::BALANCE)) + .last() + .unwrap() + .bus_mapping_instance + .clone(); + + let container = builder.block.container; + + let operation = &container.stack[indices[0].as_usize()]; + assert_eq!(operation.rw(), RW::READ); + assert_eq!( + 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!( + 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: AccountField::Balance, + 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 770108384c..35ebe3728f 100644 --- a/zkevm-circuits/src/evm_circuit/execution.rs +++ b/zkevm-circuits/src/evm_circuit/execution.rs @@ -26,6 +26,7 @@ use strum::IntoEnumIterator; mod add_sub; mod addmod; mod address; +mod balance; mod begin_tx; mod bitwise; mod block_ctx; @@ -79,6 +80,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}; @@ -164,6 +166,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_gadget: CallGadget, @@ -199,7 +202,6 @@ pub(crate) struct ExecutionConfig { selfbalance_gadget: SelfbalanceGadget, sha3_gadget: Sha3Gadget, shr_gadget: ShrGadget, - balance_gadget: DummyGadget, blockhash_gadget: DummyGadget, exp_gadget: DummyGadget, shl_gadget: DummyGadget, @@ -889,6 +891,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 => assign_exec_step!(self.call_gadget), @@ -927,7 +930,6 @@ impl ExecutionConfig { ExecutionState::BLOCKCTXU256 => assign_exec_step!(self.block_ctx_u256_gadget), ExecutionState::SELFBALANCE => assign_exec_step!(self.selfbalance_gadget), // dummy gadgets - ExecutionState::BALANCE => assign_exec_step!(self.balance_gadget), ExecutionState::BLOCKHASH => assign_exec_step!(self.blockhash_gadget), ExecutionState::EXP => assign_exec_step!(self.exp_gadget), ExecutionState::SHL => assign_exec_step!(self.shl_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..8b4d352b77 --- /dev/null +++ b/zkevm-circuits/src/evm_circuit/execution/balance.rs @@ -0,0 +1,271 @@ +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, CachedRegion, Cell, RandomLinearCombination}; +use crate::evm_circuit::witness::{Block, Call, ExecStep, Transaction}; +use crate::table::{AccountFieldTag, CallContextFieldTag}; +use crate::util::Expr; +use eth_types::evm_types::GasCost; +use eth_types::{Field, ToAddress, ToScalar, U256}; +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, +} + +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(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(); + cb.account_read( + from_bytes::expr(&address.cells), + AccountFieldTag::Balance, + balance.expr(), + ); + cb.stack_push(balance.expr()); + + let gas_cost = is_warm.expr() * GasCost::WARM_ACCESS.expr() + + (1.expr() - is_warm.expr()) * GasCost::COLD_ACCOUNT_ACCESS.expr(); + + let step_state_transition = StepStateTransition { + rw_counter: Delta(cb.rw_counter_offset()), + 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, + } + } + + 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 mut address_bytes = block.rws[step.rw_indices[0]].stack_value().to_address().0; + address_bytes.reverse(); + self.address.assign(region, offset, Some(address_bytes))?; + + self.tx_id + .assign(region, offset, U256::from(tx.id).to_scalar())?; + + 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, Some(F::from(is_warm)))?; + + let balance = block.rws[step.rw_indices[5]] + .table_assignment(block.randomness) + .value; + self.balance.assign(region, offset, Some(balance))?; + + 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); + accs[1].address(address); + if let Some(account) = account { + accs[1].balance(account.balance); + } + accs[2] + .address(address!("0x0000000000000000000000000000000000000010")) + .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); + accs[2].address(address); + if let Some(account) = account { + accs[2].balance(account.balance); + } + accs[3] + .address(mock::MOCK_ACCOUNTS[2]) + .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/evm_circuit/witness.rs b/zkevm-circuits/src/evm_circuit/witness.rs index 23d4133044..93fb3cdd0f 100644 --- a/zkevm-circuits/src/evm_circuit/witness.rs +++ b/zkevm-circuits/src/evm_circuit/witness.rs @@ -1238,6 +1238,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, @@ -1286,7 +1287,6 @@ impl From<&circuit_input_builder::ExecStep> for ExecutionState { OpcodeId::CODESIZE => ExecutionState::CODESIZE, OpcodeId::RETURN | OpcodeId::REVERT => ExecutionState::RETURN, // dummy ops - OpcodeId::BALANCE => dummy!(ExecutionState::BALANCE), OpcodeId::BLOCKHASH => dummy!(ExecutionState::BLOCKHASH), OpcodeId::EXP => dummy!(ExecutionState::EXP), OpcodeId::SHL => dummy!(ExecutionState::SHL),