From fdad69f62f084cd8e42b1ed3ccc4c91b53991095 Mon Sep 17 00:00:00 2001 From: Mathieu <60658558+enitrat@users.noreply.github.com> Date: Fri, 19 Apr 2024 09:56:10 +0200 Subject: [PATCH] feat: eip-3074 (#1104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time spent on this PR: 2d ## 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 #1103 ## What is the new behavior? - Implements EIP 3074 - - - - - This change is [Reviewable](https://reviewable.io/reviews/kkrt-labs/kakarot/1104) --- blockchain-tests-skip.yml | 3 + src/kakarot/constants.cairo | 19 +- src/kakarot/evm.cairo | 2 + src/kakarot/gas.cairo | 2 + .../instructions/system_operations.cairo | 336 +++++++++++++++++- src/kakarot/interpreter.cairo | 6 +- src/kakarot/model.cairo | 2 + src/kakarot/precompiles/ec_recover.cairo | 8 +- src/utils/array.cairo | 11 + .../instructions/test_system_operations.cairo | 93 +++++ .../instructions/test_system_operations.py | 197 ++++++++++ .../kakarot/precompiles/test_ec_recover.py | 6 +- tests/src/utils/test_array.cairo | 17 +- tests/src/utils/test_array.py | 16 + tests/utils/constants.py | 3 + tests/utils/helpers.cairo | 13 + tests/utils/serde.py | 1 + 17 files changed, 700 insertions(+), 35 deletions(-) create mode 100644 tests/src/kakarot/instructions/test_system_operations.cairo create mode 100644 tests/src/kakarot/instructions/test_system_operations.py diff --git a/blockchain-tests-skip.yml b/blockchain-tests-skip.yml index 8fb69c7da..ae7185405 100644 --- a/blockchain-tests-skip.yml +++ b/blockchain-tests-skip.yml @@ -48,6 +48,9 @@ testname: - sha3_d3g0v0_Cancun #RunResources error stAttackTest: - ContractCreationSpam_d0g0v0_Cancun #RunResources error + stBadOpcode: + - badOpcodes_d111g0v0_Cancun #EIP-3074 + - badOpcodes_d112g0v0_Cancun #EIP-3074 stChainId: - chainId_d0g0v0_Cancun stCreate2: diff --git a/src/kakarot/constants.cairo b/src/kakarot/constants.cairo index 8ce8018a4..4b3ba56ad 100644 --- a/src/kakarot/constants.cairo +++ b/src/kakarot/constants.cairo @@ -18,6 +18,9 @@ namespace Constants { const MAX_CODE_SIZE = 0x6000; const BURN_ADDRESS = 0xdead; + + // AUTH MAGIC + const MAGIC = 0x04; } // See model.Opcode: @@ -1503,18 +1506,18 @@ dw Gas.CREATE; dw 4; dw 4; dw -3; -// INVALID +// AUTH dw 0xf6; dw 0; -dw 0; -dw 0; -dw 0; -// INVALID +dw 3; +dw 3; +dw -2; +// AUTHCALL dw 0xf7; dw 0; -dw 0; -dw 0; -dw 0; +dw 7; +dw 7; +dw -6; // INVALID dw 0xf8; dw 0; diff --git a/src/kakarot/evm.cairo b/src/kakarot/evm.cairo index b9acc4125..151aa8262 100644 --- a/src/kakarot/evm.cairo +++ b/src/kakarot/evm.cairo @@ -57,6 +57,7 @@ namespace EVM { code_address=evm.message.code_address, read_only=evm.message.read_only, is_create=evm.message.is_create, + authorized=evm.message.authorized, depth=evm.message.depth, env=evm.message.env, ); @@ -244,6 +245,7 @@ namespace EVM { code_address=self.message.code_address, read_only=self.message.read_only, is_create=self.message.is_create, + authorized=self.message.authorized, depth=self.message.depth, env=self.message.env, ); diff --git a/src/kakarot/gas.cairo b/src/kakarot/gas.cairo index 2495aaece..e0856ec48 100644 --- a/src/kakarot/gas.cairo +++ b/src/kakarot/gas.cairo @@ -31,6 +31,7 @@ namespace Gas { const ZERO = 0; const NEW_ACCOUNT = 25000; const CALL_VALUE = 9000; + const AUTHCALL_VALUE = 6700; const CALL_STIPEND = 2300; const SELF_DESTRUCT = 5000; const SELF_DESTRUCT_NEW_ACCOUNT = 25000; @@ -53,6 +54,7 @@ namespace Gas { const TX_ACCESS_LIST_ADDRESS_COST = 2400; const TX_ACCESS_LIST_STORAGE_KEY_COST = 1900; const BLOBHASH = 3; + const AUTH = 3100; // @notice Compute the cost of the memory for a given words length. // @dev To avoid range_check overflow, we compute words_len / 512 diff --git a/src/kakarot/instructions/system_operations.cairo b/src/kakarot/instructions/system_operations.cairo index 230cea11b..fdaa70bb6 100644 --- a/src/kakarot/instructions/system_operations.cairo +++ b/src/kakarot/instructions/system_operations.cairo @@ -7,6 +7,12 @@ from starkware.cairo.common.bool import TRUE, FALSE from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin from starkware.cairo.common.math import split_felt, unsigned_div_rem from starkware.cairo.common.math_cmp import is_le, is_nn, is_not_zero +from starkware.cairo.common.memset import memset +from starkware.cairo.common.memcpy import memcpy +from starkware.cairo.common.cairo_secp.signature import recover_public_key +from starkware.cairo.common.cairo_secp.ec_point import EcPoint +from starkware.cairo.common.cairo_secp.bigint import uint256_to_bigint +from starkware.cairo.common.cairo_secp.bigint3 import BigInt3 from starkware.cairo.common.registers import get_fp_and_pc from starkware.cairo.common.uint256 import Uint256, uint256_lt, uint256_le from starkware.cairo.common.default_dict import default_dict_new @@ -23,8 +29,9 @@ from kakarot.model import model from kakarot.stack import Stack from kakarot.storages import Kakarot_cairo1_helpers_class_hash from kakarot.state import State +from kakarot.precompiles.ec_recover import EcRecoverHelpers from utils.utils import Helpers -from utils.array import slice +from utils.array import slice, pad_end from utils.bytes import ( bytes_to_bytes8_little_endian, felt_to_bytes, @@ -159,6 +166,7 @@ namespace SystemOperations { let (valid_jumpdests_start, valid_jumpdests) = Account.get_jumpdests( bytecode_len=size.low, bytecode=bytecode ); + tempvar authorized = new model.Option(is_some=0, value=0); tempvar message = new model.Message( bytecode=bytecode, bytecode_len=size.low, @@ -173,6 +181,7 @@ namespace SystemOperations { code_address=0, read_only=FALSE, is_create=TRUE, + authorized=authorized, depth=evm.message.depth + 1, env=evm.message.env, ); @@ -343,6 +352,8 @@ namespace SystemOperations { let (ret_offset) = Stack.peek(0); let (ret_size) = Stack.peek(1); + local call_sender = evm.message.address.evm; + // 2. Gas // Memory expansion cost let memory_expansion = Gas.max_memory_expansion_cost( @@ -389,9 +400,9 @@ namespace SystemOperations { return evm; } - tempvar gas_stipend = gas + is_value_non_zero * Gas.CALL_STIPEND; + tempvar gas_with_stipend = gas + is_value_non_zero * Gas.CALL_STIPEND; - let sender = State.get_account(evm.message.address.evm); + let sender = State.get_account(call_sender); let (sender_balance_lt_value) = uint256_lt([sender.balance], [value]); tempvar is_max_depth_reached = Helpers.is_zero( Constants.STACK_MAX_DEPTH - evm.message.depth @@ -408,7 +419,7 @@ namespace SystemOperations { return_data=return_data, program_counter=evm.program_counter, stopped=FALSE, - gas_left=evm.gas_left + gas_stipend, + gas_left=evm.gas_left + gas_with_stipend, gas_refund=evm.gas_refund, reverted=FALSE, ); @@ -417,9 +428,9 @@ namespace SystemOperations { let child_evm = CallHelper.generic_call( evm, - gas=gas_stipend, + gas=gas_with_stipend, value=value, - caller=evm.message.address.evm, + caller=call_sender, to=to, code_address=to, is_staticcall=FALSE, @@ -470,6 +481,8 @@ namespace SystemOperations { let (ret_offset) = Stack.peek(0); let (ret_size) = Stack.peek(1); + local call_sender = evm.message.address.evm; + // Gas // Memory expansion cost let memory_expansion = Gas.max_memory_expansion_cost( @@ -526,7 +539,7 @@ namespace SystemOperations { evm, gas, value=zero, - caller=evm.message.address.evm, + caller=call_sender, to=to, code_address=to, is_staticcall=TRUE, @@ -567,6 +580,8 @@ namespace SystemOperations { let (ret_offset) = Stack.peek(0); let (ret_size) = Stack.peek(1); + local call_sender = evm.message.address.evm; + // Gas let memory_expansion = Gas.max_memory_expansion_cost( memory.words_len, args_offset, args_size, ret_offset, ret_size @@ -593,13 +608,13 @@ namespace SystemOperations { if (evm.reverted != FALSE) { return evm; } - tempvar gas_stipend = gas + is_value_non_zero * Gas.CALL_STIPEND; + tempvar gas_with_stipend = gas + is_value_non_zero * Gas.CALL_STIPEND; // Operation tempvar memory = new model.Memory( memory.word_dict_start, memory.word_dict, memory_expansion.new_words_len ); - let sender = State.get_account(evm.message.address.evm); + let sender = State.get_account(call_sender); let (sender_balance_lt_value) = uint256_lt([sender.balance], [value]); tempvar is_max_depth_reached = Helpers.is_zero( Constants.STACK_MAX_DEPTH - evm.message.depth @@ -616,7 +631,7 @@ namespace SystemOperations { return_data=return_data, program_counter=evm.program_counter, stopped=FALSE, - gas_left=evm.gas_left + gas_stipend, + gas_left=evm.gas_left + gas_with_stipend, gas_refund=evm.gas_refund, reverted=FALSE, ); @@ -625,10 +640,10 @@ namespace SystemOperations { let child_evm = CallHelper.generic_call( evm, - gas=gas_stipend, + gas=gas_with_stipend, value=value, - caller=evm.message.address.evm, - to=evm.message.address.evm, + caller=call_sender, + to=call_sender, code_address=code_address, is_staticcall=FALSE, args_offset=args_offset, @@ -666,6 +681,8 @@ namespace SystemOperations { let args_size = popped + 3 * Uint256.SIZE; let (ret_offset) = Stack.peek(0); let (ret_size) = Stack.peek(1); + + let call_sender = evm.message.caller; let to = evm.message.address.evm; // Gas @@ -722,7 +739,7 @@ namespace SystemOperations { evm, gas, value=evm.message.value, - caller=evm.message.caller, + caller=call_sender, to=to, code_address=code_address, is_staticcall=FALSE, @@ -811,6 +828,291 @@ namespace SystemOperations { return evm; } + + func exec_auth{ + syscall_ptr: felt*, + pedersen_ptr: HashBuiltin*, + range_check_ptr, + bitwise_ptr: BitwiseBuiltin*, + stack: model.Stack*, + memory: model.Memory*, + state: model.State*, + }(evm: model.EVM*) -> model.EVM* { + alloc_locals; + // Stack + let (popped) = Stack.pop_n(3); + let authority = uint256_to_uint160(popped[0]); + let offset = popped[1]; + let length = popped[2]; + + // Gas + // Calling `get_account` subsequently will make the account warm for the next interaction + let is_warm = State.is_account_warm(authority); + tempvar access_gas_cost = is_warm * Gas.WARM_ACCESS + (1 - is_warm) * + Gas.COLD_ACCOUNT_ACCESS; + + // Charge memory access gas - will revert if offset.high | length.high != 0 + let memory_expansion = Gas.memory_expansion_cost_saturated( + memory.words_len, offset, length + ); + let evm = EVM.charge_gas(evm, memory_expansion.cost + access_gas_cost); + if (evm.reverted != FALSE) { + return evm; + } + + // OPERATION + tempvar memory = new model.Memory( + word_dict_start=memory.word_dict_start, + word_dict=memory.word_dict, + words_len=memory_expansion.new_words_len, + ); + + // authority not EOA + let authority_account = State.get_account(authority); + if (authority_account.code_len != 0) { + Stack.push_uint128(0); + tempvar authorized = new model.Option(is_some=0, value=0); + tempvar message = new model.Message( + bytecode=evm.message.bytecode, + bytecode_len=evm.message.bytecode_len, + valid_jumpdests_start=evm.message.valid_jumpdests_start, + valid_jumpdests=evm.message.valid_jumpdests, + calldata=evm.message.calldata, + calldata_len=evm.message.calldata_len, + value=evm.message.value, + caller=evm.message.caller, + parent=evm.message.parent, + address=evm.message.address, + code_address=evm.message.code_address, + read_only=evm.message.read_only, + is_create=evm.message.is_create, + authorized=authorized, + depth=evm.message.depth, + env=evm.message.env, + ); + return new model.EVM( + message=message, + return_data_len=evm.return_data_len, + return_data=evm.return_data, + program_counter=evm.program_counter, + stopped=evm.stopped, + gas_left=evm.gas_left, + gas_refund=evm.gas_refund, + reverted=evm.reverted, + ); + } + + let (mem_slice) = alloc(); + let mem_slice_len = length.low; + Memory.load_n(mem_slice_len, mem_slice, offset.low); + + // Pad the memory slice with zeros to a size of 97 + pad_end(mem_slice_len, mem_slice, 97); + + // The first 65 bytes hold the Signature {y_parity, r, s} + let y_parity = mem_slice[0]; + let r = Helpers.bytes32_to_uint256(mem_slice + 1); + let s = Helpers.bytes32_to_uint256(mem_slice + 33); + + // Build the original message to compute the hash and verify the sig + let (msg) = alloc(); + assert msg[0] = Constants.MAGIC; + Helpers.split_word(evm.message.env.chain_id, 32, msg + 1); + Helpers.split_word(authority_account.nonce, 32, msg + 33); + Helpers.split_word(evm.message.address.evm, 32, msg + 65); + // copy the commit to msg, held in mem_slice[65, 97] + memcpy(msg + 97, mem_slice + 65, 32); + let message_len = 129; + + let (message_bytes8: felt*) = alloc(); + let (message_bytes8_len, last_word, last_word_num_bytes) = bytes_to_bytes8_little_endian( + message_bytes8, message_len, msg + ); + + let (helpers_class) = Kakarot_cairo1_helpers_class_hash.read(); + let (msg_hash) = ICairo1Helpers.library_call_keccak( + class_hash=helpers_class, + words_len=message_bytes8_len, + words=message_bytes8, + last_input_word=last_word, + last_input_num_bytes=last_word_num_bytes, + ); + + let (msg_hash_bigint: BigInt3) = uint256_to_bigint(msg_hash); + let (r_bigint: BigInt3) = uint256_to_bigint(r); + let (s_bigint: BigInt3) = uint256_to_bigint(s); + let (public_key_point: EcPoint) = recover_public_key( + msg_hash=msg_hash_bigint, r=r_bigint, s=s_bigint, v=y_parity + ); + let (calculated_eth_address) = EcRecoverHelpers.public_key_point_to_eth_address( + public_key_point=public_key_point, helpers_class=helpers_class + ); + + if (calculated_eth_address != authority) { + Stack.push_uint128(0); + return evm; + } + + Stack.push_uint128(1); + tempvar authorized = new model.Option(is_some=1, value=authority); + tempvar message = new model.Message( + bytecode=evm.message.bytecode, + bytecode_len=evm.message.bytecode_len, + valid_jumpdests_start=evm.message.valid_jumpdests_start, + valid_jumpdests=evm.message.valid_jumpdests, + calldata=evm.message.calldata, + calldata_len=evm.message.calldata_len, + value=evm.message.value, + caller=evm.message.caller, + parent=evm.message.parent, + address=evm.message.address, + code_address=evm.message.code_address, + read_only=evm.message.read_only, + is_create=evm.message.is_create, + authorized=authorized, + depth=evm.message.depth, + env=evm.message.env, + ); + + return new model.EVM( + message=message, + return_data_len=evm.return_data_len, + return_data=evm.return_data, + program_counter=evm.program_counter, + stopped=evm.stopped, + gas_left=evm.gas_left, + gas_refund=evm.gas_refund, + reverted=evm.reverted, + ); + } + + func exec_authcall{ + syscall_ptr: felt*, + pedersen_ptr: HashBuiltin*, + range_check_ptr, + bitwise_ptr: BitwiseBuiltin*, + stack: model.Stack*, + memory: model.Memory*, + state: model.State*, + }(evm: model.EVM*) -> 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 + let (popped) = Stack.pop_n(5); + let gas_param = popped[0]; + let to = uint256_to_uint160(popped[1]); + let value = popped + 2 * Uint256.SIZE; + 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); + + local call_sender = evm.message.authorized.value; + + // 2. Gas + // Memory expansion cost + let memory_expansion = Gas.max_memory_expansion_cost( + memory.words_len, args_offset, args_size, ret_offset, ret_size + ); + + // Access gas cost. The account is marked as warm in the `generic_call` function, + // which performs a `get_account`. + let is_account_warm = State.is_account_warm(to); + tempvar access_gas_cost = is_account_warm * Gas.WARM_ACCESS + (1 - is_account_warm) * + Gas.COLD_ACCOUNT_ACCESS; + + // Create gas cost + let is_account_alive = State.is_account_alive(to); + 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); + let create_gas_cost = (1 - is_account_alive) * is_value_non_zero * Gas.NEW_ACCOUNT; + + // Transfer gas cost + let transfer_gas_cost = is_value_non_zero * Gas.AUTHCALL_VALUE; + + // Charge the fixed cost of the extra_gas + memory expansion + tempvar extra_gas = access_gas_cost + create_gas_cost + transfer_gas_cost; + let evm = EVM.charge_gas(evm, extra_gas + memory_expansion.cost); + + let gas = Gas.compute_message_call_gas(gas_param, evm.gas_left); + + // Charge the fixed message call gas + let evm = EVM.charge_gas(evm, gas); + if (evm.reverted != FALSE) { + // This EVM's stack will not be used anymore, since it reverted - no need to pop the + // last remaining 2 values ret_offset and ret_size. + return evm; + } + + // Operation + tempvar memory = new model.Memory( + memory.word_dict_start, memory.word_dict, memory_expansion.new_words_len + ); + if (evm.message.read_only * is_value_non_zero != FALSE) { + // No need to pop + let (revert_reason_len, revert_reason) = Errors.stateModificationError(); + let evm = EVM.stop(evm, revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT); + return evm; + } + + let is_authorized_unset = 1 - evm.message.authorized.is_some; + let sender = State.get_account(call_sender); + let (sender_balance_lt_value) = uint256_lt([sender.balance], [value]); + tempvar is_max_depth_reached = Helpers.is_zero( + Constants.STACK_MAX_DEPTH - evm.message.depth + ); + tempvar is_call_invalid = sender_balance_lt_value + is_max_depth_reached + + is_authorized_unset; + 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, + gas_refund=evm.gas_refund, + reverted=FALSE, + ); + return evm; + } + + let child_evm = CallHelper.generic_call( + evm, + gas=gas, + value=value, + caller=call_sender, + 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 call_sender_starknet_address = Account.compute_starknet_address(call_sender); + tempvar call_sender_address = new model.Address( + starknet=call_sender_starknet_address, evm=call_sender + ); + let transfer = model.Transfer(call_sender_address, child_evm.message.address, [value]); + let success = State.add_transfer(transfer); + if (success == 0) { + let (revert_reason_len, revert_reason) = Errors.balanceError(); + tempvar child_evm = EVM.stop( + child_evm, revert_reason_len, revert_reason, Errors.EXCEPTIONAL_HALT + ); + } else { + tempvar child_evm = child_evm; + } + + return child_evm; + } } namespace CallHelper { @@ -876,6 +1178,7 @@ namespace CallHelper { tempvar read_only = evm.message.read_only; } + tempvar authorized = new model.Option(is_some=0, value=0); tempvar message = new model.Message( bytecode=code, bytecode_len=code_len, @@ -890,6 +1193,7 @@ namespace CallHelper { code_address=code_address, read_only=read_only, is_create=FALSE, + authorized=authorized, depth=evm.message.depth + 1, env=evm.message.env, ); @@ -1012,7 +1316,7 @@ namespace CreateHelper { ); let (implementation) = Kakarot_cairo1_helpers_class_hash.read(); - let (message_hash) = ICairo1Helpers.library_call_keccak( + let (msg_hash) = ICairo1Helpers.library_call_keccak( class_hash=implementation, words_len=message_bytes8_len, words=message_bytes8, @@ -1020,7 +1324,7 @@ namespace CreateHelper { last_input_num_bytes=last_word_num_bytes, ); - let address = uint256_to_uint160(message_hash); + let address = uint256_to_uint160(msg_hash); return (address,); } diff --git a/src/kakarot/interpreter.cairo b/src/kakarot/interpreter.cairo index 0272cad3a..2c6c70c8f 100644 --- a/src/kakarot/interpreter.cairo +++ b/src/kakarot/interpreter.cairo @@ -623,9 +623,9 @@ namespace Interpreter { jmp end; call SystemOperations.exec_create; // 0xf5 jmp end; - call unknown_opcode; // 0xf6 + call SystemOperations.exec_auth; // 0xf6 jmp end; - call unknown_opcode; // 0xf7 + call SystemOperations.exec_authcall; // 0xf7 jmp end; call unknown_opcode; // 0xf8 jmp end; @@ -821,6 +821,7 @@ namespace Interpreter { let (valid_jumpdests_start, valid_jumpdests) = Account.get_jumpdests( bytecode_len=bytecode_len, bytecode=bytecode ); + tempvar authorized = new model.Option(is_some=0, value=0); tempvar message = new model.Message( bytecode=bytecode, bytecode_len=bytecode_len, @@ -835,6 +836,7 @@ namespace Interpreter { code_address=code_address, read_only=FALSE, is_create=is_deploy_tx, + authorized=authorized, depth=0, env=env, ); diff --git a/src/kakarot/model.cairo b/src/kakarot/model.cairo index e6d0590fe..933ec612b 100644 --- a/src/kakarot/model.cairo +++ b/src/kakarot/model.cairo @@ -118,6 +118,7 @@ namespace model { // @param code_address The EVM address the bytecode of the message is taken from. // @param read_only if set to true, context cannot do any state modifying instructions or send ETH in the sub context. // @param is_create if set to true, the call context is a CREATEs or deploy execution + // @param authorized If set, delegates control of the externally owned account (EOA) to a smart contract. // @param depth The depth of the current execution context. struct Message { bytecode: felt*, @@ -133,6 +134,7 @@ namespace model { code_address: felt, read_only: felt, is_create: felt, + authorized: model.Option*, depth: felt, env: Environment*, } diff --git a/src/kakarot/precompiles/ec_recover.cairo b/src/kakarot/precompiles/ec_recover.cairo index 5c0f0926f..7c796bedc 100644 --- a/src/kakarot/precompiles/ec_recover.cairo +++ b/src/kakarot/precompiles/ec_recover.cairo @@ -73,7 +73,10 @@ namespace PrecompileEcRecover { return (0, output, GAS_COST_EC_RECOVER, 0); } - let (public_address) = EcRecoverHelpers.public_key_point_to_eth_address(public_key_point); + let (helpers_class) = Kakarot_cairo1_helpers_class_hash.read(); + let (public_address) = EcRecoverHelpers.public_key_point_to_eth_address( + public_key_point, helpers_class + ); let (output) = alloc(); Helpers.split_word(public_address, 32, output); @@ -100,7 +103,7 @@ namespace EcRecoverHelpers { pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin*, - }(public_key_point: EcPoint) -> (eth_address: felt) { + }(public_key_point: EcPoint, helpers_class: felt) -> (eth_address: felt) { alloc_locals; let (local elements: Uint256*) = alloc(); let (x_uint256: Uint256) = bigint_to_uint256(public_key_point.x); @@ -112,7 +115,6 @@ namespace EcRecoverHelpers { let inputs_start = inputs; keccak_add_uint256s{inputs=inputs}(n_elements=2, elements=elements, bigend=1); - let (helpers_class) = Kakarot_cairo1_helpers_class_hash.read(); let (point_hash) = ICairo1Helpers.library_call_keccak( class_hash=helpers_class, words_len=8, diff --git a/src/utils/array.cairo b/src/utils/array.cairo index 985039448..697d124f1 100644 --- a/src/utils/array.cairo +++ b/src/utils/array.cairo @@ -110,3 +110,14 @@ func contains{range_check_ptr}(arr_len: felt, arr: felt*, value: felt) -> felt { } return FALSE; } + +func pad_end{range_check_ptr}(arr_len, arr: felt*, size: felt) { + alloc_locals; + let size_to_fill = size - arr_len; + let is_within_bound = is_nn(size_to_fill); + if (is_within_bound == FALSE) { + return (); + } + memset(arr + arr_len, 0, size_to_fill); + return (); +} diff --git a/tests/src/kakarot/instructions/test_system_operations.cairo b/tests/src/kakarot/instructions/test_system_operations.cairo new file mode 100644 index 000000000..6840b0502 --- /dev/null +++ b/tests/src/kakarot/instructions/test_system_operations.cairo @@ -0,0 +1,93 @@ +%lang starknet + +from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin +from starkware.cairo.common.alloc import alloc +from starkware.cairo.common.uint256 import Uint256 + +from kakarot.stack import Stack +from kakarot.state import State +from kakarot.memory import Memory +from kakarot.model import model +from kakarot.evm import EVM +from kakarot.instructions.system_operations import SystemOperations + +from tests.utils.helpers import TestHelpers + +func test__auth{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}() -> (model.Stack*, model.EVM*) { + alloc_locals; + local auth_stack_len; + local auth_memory_len; + local invoker_address; + let (auth_stack_ptr) = alloc(); + let (auth_memory) = alloc(); + let auth_stack = cast(auth_stack_ptr, Uint256*); + %{ + from itertools import chain + ids.auth_stack_len = len(program_input["stack"]) + segments.write_arg(ids.auth_stack_ptr, list(chain.from_iterable(program_input["stack"]))) + ids.auth_memory_len = len(program_input["memory"]) + segments.write_arg(ids.auth_memory, program_input["memory"]) + ids.invoker_address = program_input["invoker_address"] + %} + let stack = TestHelpers.init_stack_with_values(auth_stack_len, auth_stack); + let memory = TestHelpers.init_memory_with_values(auth_memory_len, auth_memory); + let state = State.init(); + + let (bytecode) = alloc(); + let evm = TestHelpers.init_evm_at_address(0, bytecode, 0x1234, invoker_address); + + with stack, state, memory { + let evm = SystemOperations.exec_auth(evm); + } + + return (stack, evm); +} + +func test__auth_authcall{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}() -> model.EVM* { + alloc_locals; + local auth_stack_len; + local auth_memory_len; + local invoker_address; + let (auth_stack_ptr) = alloc(); + let (auth_memory) = alloc(); + let auth_stack = cast(auth_stack_ptr, Uint256*); + %{ + from itertools import chain + ids.auth_stack_len = len(program_input["auth_stack"]) + segments.write_arg(ids.auth_stack_ptr, list(chain.from_iterable(program_input["auth_stack"]))) + ids.auth_memory_len = len(program_input["auth_memory"]) + segments.write_arg(ids.auth_memory, program_input["auth_memory"]) + ids.invoker_address = program_input["invoker_address"] + %} + let stack = TestHelpers.init_stack_with_values(auth_stack_len, auth_stack); + let memory = TestHelpers.init_memory_with_values(auth_memory_len, auth_memory); + let state = State.init(); + + let (bytecode) = alloc(); + let evm = TestHelpers.init_evm_at_address(0, bytecode, 0x1234, invoker_address); + + with stack, state, memory { + let evm = SystemOperations.exec_auth(evm); + } + + local authcall_stack_len; + let (authcall_stack_ptr) = alloc(); + let authcall_stack = cast(authcall_stack_ptr, Uint256*); + %{ + from itertools import chain + ids.authcall_stack_len = len(program_input["authcall_stack"]) + segments.write_arg(ids.authcall_stack_ptr, list(chain.from_iterable(program_input["authcall_stack"]))) + %} + + let stack = TestHelpers.init_stack_with_values(authcall_stack_len, authcall_stack); + + with stack, state, memory { + let evm = SystemOperations.exec_authcall(evm); + } + + return evm; +} diff --git a/tests/src/kakarot/instructions/test_system_operations.py b/tests/src/kakarot/instructions/test_system_operations.py new file mode 100644 index 000000000..722181427 --- /dev/null +++ b/tests/src/kakarot/instructions/test_system_operations.py @@ -0,0 +1,197 @@ +import pytest +from eth_account.account import Account +from eth_utils import keccak + +from tests.utils.constants import CHAIN_ID +from tests.utils.helpers import generate_random_private_key +from tests.utils.syscall_handler import SyscallHandler +from tests.utils.uint256 import int_to_uint256 + +NONCE = 1 +MAGIC = 0x04 +CAIRO1_HELPERS_CLASS_HASH = 0xDEADBEEFABDE1 + + +@pytest.fixture +def private_key(): + return generate_random_private_key() + + +@pytest.fixture +def invoker_address(private_key): + return int(private_key.public_key.to_checksum_address(), 16) + + +@pytest.fixture +def message(invoker_address): + return { + "MAGIC": MAGIC.to_bytes(1, "big"), + "chainId": CHAIN_ID.to_bytes(32, "big"), + "nonce": NONCE.to_bytes(32, "big"), + "invokerAddress": invoker_address.to_bytes(32, "big"), + "commit": "commit".encode("utf-8").ljust(32, b"\x00"), + } + + +def prepare_stack_and_memory(invoker_address, message, private_key): + serialized_msg = b"".join(message.values()) + msg_hash = keccak(serialized_msg) + (_, r, s, v, signature) = Account.signHash(msg_hash, private_key) + y_parity = v - 27 + + stack = [ + int_to_uint256(value) + for value in [invoker_address, 0, 65 + len(message["commit"])] + ] # invoker_address, offset, len + + memory = [ + y_parity, + *r.to_bytes(32, "big"), + *s.to_bytes(32, "big"), + *message["commit"], + ] + + return serialized_msg, int.from_bytes(msg_hash, "big"), stack, memory + + +class TestSystemOperations: + class TestAuth: + @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [NONCE]) + @SyscallHandler.patch("IAccount.bytecode", lambda addr, data: [0]) + @SyscallHandler.patch( + "Kakarot_cairo1_helpers_class_hash", + CAIRO1_HELPERS_CLASS_HASH, + ) + def test__should_pass_valid_signature( + self, cairo_run, private_key, invoker_address, message + ): + _, _, stack, memory = prepare_stack_and_memory( + invoker_address, message, private_key + ) + + with SyscallHandler.patch( + "Kakarot_evm_to_starknet_address", invoker_address, 0x1234 + ): + stack, evm = cairo_run( + "test__auth", + stack=stack, + memory=memory, + invoker_address=invoker_address, + ) + + assert stack == ["0x1"] + assert evm["message"]["authorized"] == { + "is_some": 1, + "value": invoker_address, + } + + @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [NONCE]) + @SyscallHandler.patch("IAccount.bytecode", lambda addr, data: [0]) + def test__should_fail_invalid_signature( + self, cairo_run, private_key, invoker_address, message + ): + _, _, stack, memory = prepare_stack_and_memory( + invoker_address, message, private_key + ) + + # Modify a byte in the signature to make it invalid + memory[0] = 255 + + with SyscallHandler.patch( + "Kakarot_evm_to_starknet_address", invoker_address, 0x1234 + ): + stack, evm = cairo_run( + "test__auth", + stack=stack, + memory=memory, + invoker_address=invoker_address, + ) + + assert stack == ["0x0"] + assert evm["message"]["authorized"] == {"is_some": 0, "value": 0} + + @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [NONCE]) + @SyscallHandler.patch("IAccount.bytecode", lambda addr, data: [0]) + def test__should_fail_invalid_invoker( + self, cairo_run, private_key, invoker_address, message + ): + invoker_address += 1 + _, msg_hash, stack, memory = prepare_stack_and_memory( + invoker_address, message, private_key + ) + + with SyscallHandler.patch( + "Kakarot_evm_to_starknet_address", invoker_address, 0x1234 + ): + stack, evm = cairo_run( + "test__auth", + stack=stack, + memory=memory, + invoker_address=invoker_address, + ) + + assert stack == ["0x0"] + assert evm["message"]["authorized"] == {"is_some": 0, "value": 0} + + class TestAuthCall: + @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 1]) + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [NONCE]) + @SyscallHandler.patch("IAccount.bytecode", lambda addr, data: [0]) + @SyscallHandler.patch( + "Kakarot_cairo1_helpers_class_hash", + CAIRO1_HELPERS_CLASS_HASH, + ) + def test__should_return_correct_evm_frame( + self, cairo_run, private_key, invoker_address, message + ): + _, _, auth_stack, auth_memory = prepare_stack_and_memory( + invoker_address, message, private_key + ) + + gas = 100000 + called_address = 0xC411 + value = 100 + args_offset = 0 + args_len = 0 + ret_offset = 0 + ret_len = 0 + authcall_stack = [ + int_to_uint256(value) + for value in [ + gas, + called_address, + value, + args_offset, + args_len, + ret_offset, + ret_len, + ] + ] + + with SyscallHandler.patch( + "Kakarot_evm_to_starknet_address", invoker_address, 0x1234 + ): + evm = cairo_run( + "test__auth_authcall", + auth_stack=auth_stack, + auth_memory=auth_memory, + invoker_address=invoker_address, + authcall_stack=authcall_stack, + ) + + assert evm["message"]["authorized"] == { + "is_some": 0, + "value": 0, + } # Authorized should be None in the new frame + assert int(evm["message"]["caller"], 16) == invoker_address + assert int(evm["message"]["value"], 16) == value + assert int(evm["message"]["address"]["evm"], 16) == called_address + + # Parent frame should keep the authorized value + assert evm["message"]["parent"]["evm"]["message"]["authorized"] == { + "is_some": 1, + "value": invoker_address, + } diff --git a/tests/src/kakarot/precompiles/test_ec_recover.py b/tests/src/kakarot/precompiles/test_ec_recover.py index 1e025cfc6..bbb798667 100644 --- a/tests/src/kakarot/precompiles/test_ec_recover.py +++ b/tests/src/kakarot/precompiles/test_ec_recover.py @@ -7,7 +7,6 @@ from ethereum.utils.byte import left_pad_zero_bytes from tests.utils.helpers import ec_sign, generate_random_private_key -from tests.utils.syscall_handler import SyscallHandler from tests.utils.uint256 import int_to_uint256 @@ -62,10 +61,7 @@ def test_should_return_evm_address_in_bytes32(self, cairo_run): ) # output of cairo_keccak is in little endian, but our library reverses it back to big endian low, high = int_to_uint256(keccak_res) - with SyscallHandler.patch( - "ICairo1Helpers.library_call_keccak", lambda class_hash, data: [low, high] - ): - [output] = cairo_run("test__ec_recover", input=input_data) + [output] = cairo_run("test__ec_recover", input=input_data) assert bytes(output) == bytes(padded_address) diff --git a/tests/src/utils/test_array.cairo b/tests/src/utils/test_array.cairo index b5c56ef32..c7284f5b9 100644 --- a/tests/src/utils/test_array.cairo +++ b/tests/src/utils/test_array.cairo @@ -4,7 +4,7 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.alloc import alloc -from utils.array import reverse, count_not_zero, slice, contains +from utils.array import reverse, count_not_zero, slice, contains, pad_end func test__reverse(output_ptr: felt*) { alloc_locals; @@ -64,3 +64,18 @@ func test_contains{range_check_ptr}(output_ptr: felt*) { assert [output_ptr] = result; return (); } + +func test_pad_end{range_check_ptr}() -> (arr: felt*) { + alloc_locals; + tempvar arr_len: felt; + let (local arr) = alloc(); + tempvar size: felt; + %{ + ids.arr_len = len(program_input["arr"]) + segments.write_arg(ids.arr, program_input["arr"]) + ids.size = program_input["size"] + %} + + pad_end(arr_len, arr, size); + return (arr=arr); +} diff --git a/tests/src/utils/test_array.py b/tests/src/utils/test_array.py index a544024d5..4be7c001a 100644 --- a/tests/src/utils/test_array.py +++ b/tests/src/utils/test_array.py @@ -56,3 +56,19 @@ class TestContains: async def test_should_return_if_contains(self, cairo_run, arr, value, expected): output = cairo_run("test_contains", arr=arr, value=value) assert expected == output[0] + + class TestPadEnd: + @pytest.mark.parametrize( + "arr, size, expected", + [ + ([0, 1, 2, 3, 4], 7, [0, 1, 2, 3, 4, 0, 0]), + ([0, 1, 2, 3], 5, [0, 1, 2, 3, 0]), + ([0], 1, [0]), + ([], 1, [0]), + ([0, 1, 2, 3], 4, [0, 1, 2, 3]), + ([0, 1, 2, 3], 1, [0, 1, 2, 3]), + ], + ) + async def test_should_pad_end(self, cairo_run, arr, size, expected): + [output] = cairo_run("test_pad_end", arr=arr, size=size) + assert expected == output diff --git a/tests/utils/constants.py b/tests/utils/constants.py index 9765e8c14..5460a3de9 100644 --- a/tests/utils/constants.py +++ b/tests/utils/constants.py @@ -43,6 +43,9 @@ BLOCK_NUMBER = 0x42 BLOCK_TIMESTAMP = int(time()) +# AUTH +MAGIC = 0x04 + # Taken from eth_account.account.Account.sign_transaction docstring # https://eth-account.readthedocs.io/en/stable/eth_account.html?highlight=sign_transaction#eth_account.account.Account.sign_transaction TRANSACTIONS = [ diff --git a/tests/utils/helpers.cairo b/tests/utils/helpers.cairo index 4f49dedcf..c9fbef8ff 100644 --- a/tests/utils/helpers.cairo +++ b/tests/utils/helpers.cairo @@ -38,6 +38,7 @@ namespace TestHelpers { bytecode_len=bytecode_len, bytecode=bytecode ); tempvar zero = new Uint256(0, 0); + tempvar authorized = new model.Option(is_some=0, value=0); local message: model.Message* = new model.Message( bytecode=bytecode, bytecode_len=bytecode_len, @@ -52,6 +53,7 @@ namespace TestHelpers { code_address=evm_contract_address, read_only=FALSE, is_create=FALSE, + authorized=authorized, depth=0, env=env, ); @@ -102,6 +104,17 @@ namespace TestHelpers { return stack_; } + func init_memory_with_values{range_check_ptr}( + serialized_memory_len: felt, serialized_memory: felt* + ) -> model.Memory* { + alloc_locals; + let memory = Memory.init(); + with memory { + Memory.store_n(serialized_memory_len, serialized_memory, 0); + } + return memory; + } + func assert_array_equal(array_0_len: felt, array_0: felt*, array_1_len: felt, array_1: felt*) { assert array_0_len = array_1_len; if (array_0_len == 0) { diff --git a/tests/utils/serde.py b/tests/utils/serde.py index b536c0536..b2d40c66e 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -176,6 +176,7 @@ def serialize_message(self, ptr): "code_address": raw["code_address"], "read_only": bool(raw["read_only"]), "is_create": bool(raw["is_create"]), + "authorized": self.serialize_struct("model.Option", raw["authorized"]), "depth": raw["depth"], "env": self.serialize_struct("model.Environment", raw["env"]), }