From ce77cf72346cb275f528d8364c506635c18c7ff8 Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Fri, 19 Jan 2024 17:37:17 +0100 Subject: [PATCH] feat: gas accounting call opcodes (#894) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time spent on this PR: 0.5d ## Pull request type Please check the type of change your PR introduces: - [ ] Bugfix - [x] Feature - [ ] Code style update (formatting, renaming) - [ ] Refactoring (no functional changes, no api changes) - [ ] Build related changes - [ ] Documentation content changes - [ ] Other (please describe): ## What is the current behavior? Resolves #887 ## What is the new behavior? - - - - - - This change is [Reviewable](https://reviewable.io/reviews/kkrt-labs/kakarot/894) --- blockchain-tests-skip.yml | 1 + .../instructions/system_operations.cairo | 329 +++++++++++------- 2 files changed, 199 insertions(+), 131 deletions(-) diff --git a/blockchain-tests-skip.yml b/blockchain-tests-skip.yml index ca582c320..ed292bf47 100644 --- a/blockchain-tests-skip.yml +++ b/blockchain-tests-skip.yml @@ -2462,6 +2462,7 @@ testname: - static_CheckOpcodes3_d3g0v1_Shanghai - static_CheckOpcodes3_d4g0v1_Shanghai - static_CheckOpcodes4_d0g1v1_Shanghai + - static_CheckOpcodes4_d0g0v1_Shanghai - static_CheckOpcodes5_d0g0v1_Shanghai - static_CheckOpcodes5_d1g0v0_Shanghai - static_CheckOpcodes5_d1g0v1_Shanghai diff --git a/src/kakarot/instructions/system_operations.cairo b/src/kakarot/instructions/system_operations.cairo index 460d36f05..6d11266c1 100644 --- a/src/kakarot/instructions/system_operations.cairo +++ b/src/kakarot/instructions/system_operations.cairo @@ -373,19 +373,18 @@ namespace SystemOperations { return evm; } - // TODO taking the lower part of the value, as anything higher than 2^128 will fail anyway + // TODO: fix value let child_evm = CallHelper.generic_call( evm, - gas_stipend, - value.low, - to, - to, - TRUE, - FALSE, - args_offset, - args_size, - ret_offset, - ret_size, + gas=gas_stipend, + value=value.low, + to=to, + code_address=to, + is_staticcall=FALSE, + args_offset=args_offset, + args_size=args_size, + ret_offset=ret_offset, + ret_size=ret_size, ); let transfer = model.Transfer(evm.message.address, child_evm.message.address, value); @@ -417,9 +416,51 @@ namespace SystemOperations { memory: model.Memory*, state: model.State*, }(evm: model.EVM*) -> model.EVM* { - let child_evm = CallHelper.init_sub_context( - evm=evm, with_value=FALSE, read_only=TRUE, self_call=FALSE + alloc_locals; + // Stack + let (popped) = Stack.pop_n(4); + let gas_param = popped[0]; + let to = uint256_to_uint160(popped[1]); + let args_offset = popped + 2 * Uint256.SIZE; + let args_size = popped + 3 * Uint256.SIZE; + let (ret_offset) = Stack.peek(0); + let (ret_size) = Stack.peek(1); + + // Gas + // Memory expansion cost + let memory_expansion_cost = Gas.max_memory_expansion_cost( + memory.words_len, args_offset, args_size, ret_offset, ret_size + ); + + // TODO: Access gas cost + let access_gas_cost = 0; + + // Charge the fixed cost of the extra_gas + memory expansion + let evm = EVM.charge_gas(evm, access_gas_cost + memory_expansion_cost); + if (evm.reverted != FALSE) { + return evm; + } + + let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left); + let evm = EVM.charge_gas(evm, gas); + if (evm.reverted != FALSE) { + return evm; + } + + // Operation + let child_evm = CallHelper.generic_call( + evm, + gas, + value=0, + to=to, + code_address=to, + is_staticcall=TRUE, + args_offset=args_offset, + args_size=args_size, + ret_offset=ret_offset, + ret_size=ret_size, ); + return child_evm; } @@ -440,8 +481,80 @@ namespace SystemOperations { memory: model.Memory*, state: model.State*, }(evm: model.EVM*) -> model.EVM* { - let child_evm = CallHelper.init_sub_context( - evm=evm, with_value=TRUE, read_only=evm.message.read_only, self_call=TRUE + alloc_locals; + // Stack + let (popped) = Stack.pop_n(5); + let gas_param = popped[0]; + let code_address = uint256_to_uint160(popped[1]); + let value = popped[2]; + let args_offset = popped + 3 * Uint256.SIZE; + let args_size = popped + 4 * Uint256.SIZE; + let (ret_offset) = Stack.peek(0); + let (ret_size) = Stack.peek(1); + + tempvar is_value_non_zero = is_not_zero(value.low) + is_not_zero(value.high); + tempvar is_value_non_zero = is_not_zero(is_value_non_zero); + + // Gas + let memory_expansion_cost = Gas.max_memory_expansion_cost( + memory.words_len, args_offset, args_size, ret_offset, ret_size + ); + + // Access gas cost + // TODO + let access_gas_cost = 0; + + // TODO: fix value when refactoring CALLs for proper gas accounting + let transfer_gas_cost = is_value_non_zero * Gas.CALL_VALUE; + + let extra_gas = access_gas_cost + transfer_gas_cost; + let evm = EVM.charge_gas(evm, extra_gas + memory_expansion_cost); + if (evm.reverted != FALSE) { + return evm; + } + + let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left); + let evm = EVM.charge_gas(evm, gas); + if (evm.reverted != FALSE) { + return evm; + } + + tempvar gas_stipend = gas + is_value_non_zero * Gas.CALL_STIPEND; + + let sender = State.get_account(evm.message.address.evm); + let (sender_balance_lt_value) = uint256_lt([sender.balance], value); + tempvar is_max_depth_reached = 1 - is_not_zero( + (Constants.STACK_MAX_DEPTH + 1) - evm.message.depth + ); + tempvar is_call_invalid = sender_balance_lt_value + is_max_depth_reached; + if (is_call_invalid != FALSE) { + // Requires popping the returndata offset and size before pushing 0 + Stack.pop_n(2); + Stack.push_uint128(0); + let (return_data) = alloc(); + tempvar evm = new model.EVM( + message=evm.message, + return_data_len=0, + return_data=return_data, + program_counter=evm.program_counter, + stopped=FALSE, + gas_left=evm.gas_left + gas_stipend, + reverted=FALSE, + ); + return evm; + } + + let child_evm = CallHelper.generic_call( + evm, + gas=gas_stipend, + value=value.low, + to=evm.message.address.evm, + code_address=code_address, + is_staticcall=FALSE, + args_offset=args_offset, + args_size=args_size, + ret_offset=ret_offset, + ret_size=ret_size, ); return child_evm; @@ -464,8 +577,71 @@ namespace SystemOperations { memory: model.Memory*, state: model.State*, }(evm: model.EVM*) -> model.EVM* { - let child_evm = CallHelper.init_sub_context( - evm=evm, with_value=FALSE, read_only=evm.message.read_only, self_call=TRUE + alloc_locals; + // Stack + let (popped) = Stack.pop_n(4); + let gas_param = popped[0]; + let code_address = uint256_to_uint160(popped[1]); + let args_offset = popped + 2 * Uint256.SIZE; + let args_size = popped + 3 * Uint256.SIZE; + let (ret_offset) = Stack.peek(0); + let (ret_size) = Stack.peek(1); + let to = evm.message.address.evm; + + // Gas + // Memory expansion cost + let memory_expansion_cost = Gas.max_memory_expansion_cost( + memory.words_len, args_offset, args_size, ret_offset, ret_size + ); + + // TODO: Access gas cost + let access_gas_cost = 0; + + // Charge the fixed cost of the extra_gas + memory expansion + let extra_gas = access_gas_cost; + let evm = EVM.charge_gas(evm, extra_gas + memory_expansion_cost); + if (evm.reverted != FALSE) { + return evm; + } + + let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left); + let evm = EVM.charge_gas(evm, gas); + if (evm.reverted != FALSE) { + return evm; + } + + tempvar is_max_depth_reached = 1 - is_not_zero( + (Constants.STACK_MAX_DEPTH + 1) - evm.message.depth + ); + if (is_max_depth_reached != FALSE) { + // Requires popping the returndata offset and size before pushing 0 + Stack.pop_n(2); + Stack.push_uint128(0); + let (return_data) = alloc(); + tempvar evm = new model.EVM( + message=evm.message, + return_data_len=0, + return_data=return_data, + program_counter=evm.program_counter, + stopped=FALSE, + gas_left=evm.gas_left + gas, + reverted=FALSE, + ); + return evm; + } + + // Operation + let child_evm = CallHelper.generic_call( + evm, + gas, + value=evm.message.value, + to=to, + code_address=code_address, + is_staticcall=FALSE, + args_offset=args_offset, + args_size=args_size, + ret_offset=ret_offset, + ret_size=ret_size, ); return child_evm; @@ -539,8 +715,7 @@ namespace CallHelper { // @param gas The gas to be used by the new EVM. // @param value The value to be transferred in the call // @param to The address of the target account. - // @param code_address The address of the code account. - // @param should_transfer_value A boolean indicating whether the value should be transferred to the target account. + // @param code_address The address of the account whose code will be executed. // @param is_staticcall A boolean indicating whether the call is a static call. // @param args_offset The offset of the calldata in memory. // @param args_size The size of the calldata in memory. @@ -561,7 +736,6 @@ namespace CallHelper { value: felt, to: felt, code_address: felt, - should_transfer_value: bool, is_staticcall: bool, args_offset: Uint256*, args_size: Uint256*, @@ -570,11 +744,11 @@ namespace CallHelper { ) -> model.EVM* { alloc_locals; - // 3. Calldata + // 1. Calldata let (calldata: felt*) = alloc(); Memory.load_n(args_size.low, calldata, args_offset.low); - // 4. Build child_evm + // 2. Build child_evm // Check if the called address is a precompiled contract let is_precompile = Precompiles.is_precompile(address=code_address); if (is_precompile != FALSE) { @@ -632,114 +806,6 @@ namespace CallHelper { return child_evm; } - // @notice The shared logic of the CALL ops, allowing CALL, CALLCODE, STATICCALL, and DELEGATECALL to - // share structure and parameterize whether the call requires a value (CALL, CALLCODE) and - // whether the returned sub context's is read only (STATICCODE) - // @param calling_evm The pointer to the calling execution context. - // @param with_value The boolean that determines whether the sub-context's calling context has a value read - // from the calling context's stack or the calling context's calling context. - // @param read_only The boolean that determines whether state modifications can be executed from the sub-execution context. - // @param self_call A boolean to indicate whether the account to message-call into is self (address of the current executing account) - // or the call argument's address (address of the call's target account) - // @return EVM The pointer to the sub context. - func init_sub_context{ - syscall_ptr: felt*, - pedersen_ptr: HashBuiltin*, - range_check_ptr, - bitwise_ptr: BitwiseBuiltin*, - stack: model.Stack*, - memory: model.Memory*, - state: model.State*, - }(evm: model.EVM*, with_value: felt, read_only: felt, self_call: felt) -> model.EVM* { - alloc_locals; - // 1. Parse args from Stack - // Note: We don't pop ret_offset and ret_size here but at the end of the sub context - // See finalize_parent - // Pop ret_offset and ret_size - let (popped) = Stack.pop_n(4 + with_value); - let gas_param = popped[0]; - let to = uint256_to_uint160(popped[1]); - let stack_value = popped[2]; - let args_offset = popped + (2 + with_value) * Uint256.SIZE; - let args_size = popped + (3 + with_value) * Uint256.SIZE; - let (ret_offset) = Stack.peek(0); - let (ret_size) = Stack.peek(1); - - // TODO: fix value when refactoring CALLs for proper gas accounting - let value = with_value * stack_value.low + (1 - with_value) * evm.message.value; - - // 2. Gas - // Memory expansion cost - let memory_expansion_cost = Gas.max_memory_expansion_cost( - memory.words_len, args_offset, args_size, ret_offset, ret_size - ); - - // Access list - // TODO - let evm = EVM.charge_gas(evm, memory_expansion_cost); - let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left); - - // All the gas is charged upfront and remaining gas is refunded at the end - let evm = EVM.charge_gas(evm, gas); - if (evm.reverted != FALSE) { - return evm; - } - - // 3. Calldata - let (calldata: felt*) = alloc(); - Memory.load_n(args_size.low, calldata, args_offset.low); - - // 4. Build child_evm - // Check if the called address is a precompiled contract - let is_precompile = Precompiles.is_precompile(address=to); - if (is_precompile != FALSE) { - tempvar parent = new model.Parent(evm, stack, memory, state); - let child_evm = Precompiles.run( - evm_address=to, - calldata_len=args_size.low, - calldata=calldata, - value=value, - parent=parent, - gas_left=gas, - ); - - return child_evm; - } - - let to_account = State.get_account(to); - - tempvar parent = new model.Parent(evm, stack, memory, state); - let stack = Stack.init(); - let memory = Memory.init(); - let (valid_jumpdests_start, valid_jumpdests) = Account.get_jumpdests( - bytecode_len=to_account.code_len, bytecode=to_account.code - ); - if (self_call == FALSE) { - tempvar message_address = to_account.address; - } else { - tempvar message_address = evm.message.address; - } - - tempvar message = new model.Message( - bytecode=to_account.code, - bytecode_len=to_account.code_len, - valid_jumpdests_start=valid_jumpdests_start, - valid_jumpdests=valid_jumpdests, - calldata=calldata, - calldata_len=args_size.low, - value=value, - parent=parent, - address=message_address, - read_only=read_only, - is_create=FALSE, - depth=evm.message.depth + 1, - env=evm.message.env, - ); - let child_evm = EVM.init(message, gas); - let state = State.copy(); - return child_evm; - } - // @return EVM The pointer to the updated calling context. func finalize_parent{ syscall_ptr: felt*, @@ -753,7 +819,8 @@ namespace CallHelper { alloc_locals; // Pop ret_offset and ret_size - // See init_sub_context, the Stack here is guaranteed to have enough items + // See call family opcodes who don't pop these + // two values, the Stack here is guaranteed to have enough items // values are checked there as Memory expansion cost is computed there. let (popped) = Stack.pop_n(n=2); let ret_offset = popped[0];