From b3f8e1864c410fb35d689cdcad8fc271694e4c86 Mon Sep 17 00:00:00 2001 From: Oba Date: Tue, 27 Aug 2024 11:37:33 +0200 Subject: [PATCH] feat: eth_send_raw_transaction (#1357) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time spent on this PR: ## 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 #1277 ## What is the new behavior? Add `eth_send_raw_transaction` entrypoint - - - This change is [Reviewable](https://reviewable.io/reviews/kkrt-labs/kakarot/1357) --------- Co-authored-by: Clément Walter --- src/kakarot/accounts/library.cairo | 91 +----- src/kakarot/eth_rpc.cairo | 106 ++++++- src/kakarot/interfaces/interfaces.cairo | 5 + src/kakarot/interpreter.cairo | 2 +- src/kakarot/kakarot.cairo | 1 + src/kakarot/library.cairo | 2 +- src/kakarot/model.cairo | 2 +- src/utils/eth_transaction.cairo | 11 +- .../kakarot/accounts/test_account_contract.py | 299 +----------------- tests/src/kakarot/test_kakarot.cairo | 22 +- tests/src/kakarot/test_kakarot.py | 181 ++++++++++- tests/src/utils/test_eth_transaction.py | 2 +- tests/utils/serde.py | 4 +- 13 files changed, 339 insertions(+), 389 deletions(-) diff --git a/src/kakarot/accounts/library.cairo b/src/kakarot/accounts/library.cairo index 53955c5f4..a58cd19d1 100644 --- a/src/kakarot/accounts/library.cairo +++ b/src/kakarot/accounts/library.cairo @@ -5,11 +5,10 @@ from starkware.cairo.common.alloc import alloc from starkware.cairo.common.bool import FALSE, TRUE from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin -from starkware.cairo.common.math import split_int, split_felt +from starkware.cairo.common.math import split_int from starkware.cairo.common.memcpy import memcpy -from starkware.cairo.common.uint256 import Uint256, uint256_not, uint256_le +from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.math_cmp import is_nn, is_le_felt -from starkware.cairo.common.math import assert_le_felt from starkware.starknet.common.syscalls import ( StorageRead, StorageWrite, @@ -29,7 +28,6 @@ from kakarot.accounts.model import CallArray from kakarot.errors import Errors from kakarot.constants import Constants from utils.eth_transaction import EthTransaction -from utils.uint256 import uint256_add from utils.bytes import bytes_to_bytes8_little_endian from utils.signature import Signature from utils.utils import Helpers @@ -205,94 +203,17 @@ namespace AccountContract { helpers_class=helpers_class, ); - let tx = EthTransaction.decode(tx_data_len, tx_data); - - // Whitelisting pre-eip155 or validate chain_id for post eip155 + // Whitelisting pre-eip155 + let (is_authorized) = Account_authorized_message_hashes.read(msg_hash); if (pre_eip155_tx != FALSE) { - let (is_authorized) = Account_authorized_message_hashes.read(msg_hash); with_attr error_message("Unauthorized pre-eip155 transaction") { assert is_authorized = TRUE; } - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } else { - with_attr error_message("Invalid chain id") { - assert tx.chain_id = chain_id; - } - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } - let syscall_ptr = cast([ap - 3], felt*); - let pedersen_ptr = cast([ap - 2], HashBuiltin*); - let range_check_ptr = [ap - 1]; - - // Validate nonce - let (account_nonce) = Account_nonce.read(); - with_attr error_message("Invalid nonce") { - assert tx.signer_nonce = account_nonce; - } - - // Validate gas and value - let (kakarot_address) = Ownable_owner.read(); - let (native_token_address) = IKakarot.get_native_token(kakarot_address); - let (contract_address) = get_contract_address(); - let (balance) = IERC20.balanceOf(native_token_address, contract_address); - - with_attr error_message("Gas limit too high") { - assert_le_felt(tx.gas_limit, 2 ** 64 - 1); - } - - with_attr error_message("Max fee per gas too high") { - assert [range_check_ptr] = tx.max_fee_per_gas; - let range_check_ptr = range_check_ptr + 1; - } - - let max_gas_fee = tx.gas_limit * tx.max_fee_per_gas; - let (max_fee_high, max_fee_low) = split_felt(max_gas_fee); - let (tx_cost, carry) = uint256_add(tx.amount, Uint256(low=max_fee_low, high=max_fee_high)); - assert carry = 0; - let (is_balance_enough) = uint256_le(tx_cost, balance); - with_attr error_message("Not enough ETH to pay msg.value + max gas fees") { - assert is_balance_enough = TRUE; - } - - let (block_gas_limit) = IKakarot.get_block_gas_limit(kakarot_address); - let tx_gas_fits_in_block = is_nn(block_gas_limit - tx.gas_limit); - with_attr error_message("Transaction gas_limit > Block gas_limit") { - assert tx_gas_fits_in_block = TRUE; } - let (block_base_fee) = IKakarot.get_base_fee(kakarot_address); - let enough_fee = is_nn(tx.max_fee_per_gas - block_base_fee); - with_attr error_message("Max fee per gas too low") { - assert enough_fee = TRUE; - } - - with_attr error_message("Max priority fee greater than max fee per gas") { - assert_le_felt(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); - } - - let possible_priority_fee = tx.max_fee_per_gas - block_base_fee; - let priority_fee_is_max_priority_fee = is_nn( - possible_priority_fee - tx.max_priority_fee_per_gas - ); - let priority_fee_per_gas = priority_fee_is_max_priority_fee * tx.max_priority_fee_per_gas + - (1 - priority_fee_is_max_priority_fee) * possible_priority_fee; - let effective_gas_price = priority_fee_per_gas + block_base_fee; - // Send tx to Kakarot - let (return_data_len, return_data, success, gas_used) = IKakarot.eth_send_transaction( - contract_address=kakarot_address, - to=tx.destination, - gas_limit=tx.gas_limit, - gas_price=effective_gas_price, - value=tx.amount, - data_len=tx.payload_len, - data=tx.payload, - access_list_len=tx.access_list_len, - access_list=tx.access_list, + let (return_data_len, return_data, success, gas_used) = IKakarot.eth_send_raw_unsigned_tx( + contract_address=kakarot_address, tx_data_len=tx_data_len, tx_data=tx_data ); // See Argent account diff --git a/src/kakarot/eth_rpc.cairo b/src/kakarot/eth_rpc.cairo index b982e194a..9f54a1b01 100644 --- a/src/kakarot/eth_rpc.cairo +++ b/src/kakarot/eth_rpc.cairo @@ -1,9 +1,12 @@ %lang starknet +from openzeppelin.access.ownable.library import Ownable_owner +from starkware.cairo.common.bool import FALSE, TRUE from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin -from starkware.cairo.common.math_cmp import is_not_zero +from starkware.cairo.common.math import assert_le, assert_nn, split_felt +from starkware.cairo.common.math_cmp import is_not_zero, is_nn from starkware.cairo.common.registers import get_fp_and_pc -from starkware.cairo.common.uint256 import Uint256 +from starkware.cairo.common.uint256 import Uint256, uint256_add, uint256_le from starkware.starknet.common.syscalls import get_caller_address, get_tx_info from backend.starknet import Starknet @@ -12,6 +15,7 @@ from kakarot.interfaces.interfaces import IAccount, IERC20 from kakarot.library import Kakarot from kakarot.model import model from kakarot.storages import Kakarot_native_token_address +from utils.eth_transaction import EthTransaction from utils.maths import unsigned_div_rem from utils.utils import Helpers @@ -180,7 +184,6 @@ func eth_estimate_gas{ // @return return_data An array of returned felts // @return success An boolean, TRUE if the transaction succeeded, FALSE otherwise // @return gas_used The amount of gas used by the transaction -@external func eth_send_transaction{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* }( @@ -221,3 +224,100 @@ func eth_send_transaction{ return result; } + +// @notice The eth_send_raw_unsigned_tx. Modified version of eth_sendRawTransaction function described in the spec. +// See https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendrawtransaction +// @dev This function takes the transaction data unsigned. Signature validation should be done before calling this function. +// @param tx_data_len The length of the unsigned transaction data +// @param tx_data The unsigned transaction data +// @return return_data_len The length of the return_data +// @return return_data An array of returned felts +// @return success An boolean, TRUE if the transaction succeeded, FALSE otherwise +// @return gas_used The amount of gas used by the transaction +@external +func eth_send_raw_unsigned_tx{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}(tx_data_len: felt, tx_data: felt*) -> ( + return_data_len: felt, return_data: felt*, success: felt, gas_used: felt +) { + alloc_locals; + let tx = EthTransaction.decode(tx_data_len, tx_data); + + // Validate chain_id for post eip155 + let (chain_id) = Kakarot.eth_chain_id(); + if (tx.chain_id.is_some != FALSE) { + with_attr error_message("Invalid chain id") { + assert tx.chain_id.value = chain_id; + } + } + + // Get the caller address + let (caller_address) = get_caller_address(); + + // Validate nonce + let (account_nonce) = IAccount.get_nonce(contract_address=caller_address); + with_attr error_message("Invalid nonce") { + assert tx.signer_nonce = account_nonce; + } + + // Validate gas + with_attr error_message("Gas limit too high") { + assert [range_check_ptr] = tx.gas_limit; + let range_check_ptr = range_check_ptr + 1; + assert_le(tx.gas_limit, 2 ** 64 - 1); + } + + with_attr error_message("Max fee per gas too high") { + assert [range_check_ptr] = tx.max_fee_per_gas; + let range_check_ptr = range_check_ptr + 1; + } + + let (block_gas_limit) = Kakarot.get_block_gas_limit(); + with_attr error_message("Transaction gas_limit > Block gas_limit") { + assert_nn(block_gas_limit - tx.gas_limit); + } + + let (block_base_fee) = Kakarot.get_base_fee(); + with_attr error_message("Max fee per gas too low") { + assert_nn(tx.max_fee_per_gas - block_base_fee); + } + + with_attr error_message("Max priority fee greater than max fee per gas") { + assert [range_check_ptr] = tx.max_priority_fee_per_gas; + let range_check_ptr = range_check_ptr + 1; + assert_le(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); + } + + let (evm_address) = IAccount.get_evm_address(caller_address); + let (balance) = eth_get_balance(evm_address); + let max_gas_fee = tx.gas_limit * tx.max_fee_per_gas; + let (max_fee_high, max_fee_low) = split_felt(max_gas_fee); + let (tx_cost, carry) = uint256_add(tx.amount, Uint256(low=max_fee_low, high=max_fee_high)); + assert carry = 0; + let (is_balance_enough) = uint256_le(tx_cost, balance); + with_attr error_message("Not enough ETH to pay msg.value + max gas fees") { + assert is_balance_enough = TRUE; + } + + let possible_priority_fee = tx.max_fee_per_gas - block_base_fee; + let priority_fee_is_max_priority_fee = is_nn( + possible_priority_fee - tx.max_priority_fee_per_gas + ); + let priority_fee_per_gas = priority_fee_is_max_priority_fee * tx.max_priority_fee_per_gas + ( + 1 - priority_fee_is_max_priority_fee + ) * possible_priority_fee; + let effective_gas_price = priority_fee_per_gas + block_base_fee; + + let (return_data_len, return_data, success, gas_used) = eth_send_transaction( + to=tx.destination, + gas_limit=tx.gas_limit, + gas_price=effective_gas_price, + value=tx.amount, + data_len=tx.payload_len, + data=tx.payload, + access_list_len=tx.access_list_len, + access_list=tx.access_list, + ); + + return (return_data_len, return_data, success, gas_used); +} diff --git a/src/kakarot/interfaces/interfaces.cairo b/src/kakarot/interfaces/interfaces.cairo index 3ba5e2f6f..e0c19abf0 100644 --- a/src/kakarot/interfaces/interfaces.cairo +++ b/src/kakarot/interfaces/interfaces.cairo @@ -189,6 +189,11 @@ namespace IKakarot { func eth_chain_id() -> (chain_id: felt) { } + + func eth_send_raw_unsigned_tx(tx_data_len: felt, tx_data: felt*) -> ( + return_data_len: felt, return_data: felt*, success: felt, gas_used: felt + ) { + } } @contract_interface diff --git a/src/kakarot/interpreter.cairo b/src/kakarot/interpreter.cairo index fa77b6c93..a51dd805c 100644 --- a/src/kakarot/interpreter.cairo +++ b/src/kakarot/interpreter.cairo @@ -897,7 +897,7 @@ namespace Interpreter { // Charge the gas fee to the user without setting up a transfer. // Transfers with the exact amounts will be performed post-execution. - // Note: balance > effective_fee was verified in AccountContract.execute_from_outside() + // Note: balance > effective_fee was verified in eth_send_raw_unsigned_tx() let max_fee = gas_limit * env.gas_price; let (fee_high, fee_low) = split_felt(max_fee); let max_fee_u256 = Uint256(low=fee_low, high=fee_high); diff --git a/src/kakarot/kakarot.cairo b/src/kakarot/kakarot.cairo index 1496e7350..6d73141fd 100644 --- a/src/kakarot/kakarot.cairo +++ b/src/kakarot/kakarot.cairo @@ -25,6 +25,7 @@ from kakarot.eth_rpc import ( eth_call, eth_estimate_gas, eth_send_transaction, + eth_send_raw_unsigned_tx, ) // Constructor diff --git a/src/kakarot/library.cairo b/src/kakarot/library.cairo index 833f077a0..07d8c68fd 100644 --- a/src/kakarot/library.cairo +++ b/src/kakarot/library.cairo @@ -218,7 +218,7 @@ namespace Kakarot { block_gas_limit: felt ) { let (block_gas_limit) = Kakarot_block_gas_limit.read(); - return (block_gas_limit,); + return (block_gas_limit=block_gas_limit); } // @notice Deploy a new externally owned account. diff --git a/src/kakarot/model.cairo b/src/kakarot/model.cairo index 308b5f730..cb4ed97b1 100644 --- a/src/kakarot/model.cairo +++ b/src/kakarot/model.cairo @@ -217,6 +217,6 @@ namespace model { payload: felt*, access_list_len: felt, access_list: felt*, - chain_id: felt, + chain_id: Option, } } diff --git a/src/utils/eth_transaction.cairo b/src/utils/eth_transaction.cairo index e2989d4c3..a272b1688 100644 --- a/src/utils/eth_transaction.cairo +++ b/src/utils/eth_transaction.cairo @@ -57,6 +57,7 @@ namespace EthTransaction { // pre eip-155 txs have 6 fields, post eip-155 txs have 9 fields if (items_len == 6) { + tempvar is_some = 0; tempvar chain_id = 0; } else { assert items_len = 9; @@ -64,7 +65,11 @@ namespace EthTransaction { assert items[7].is_list = FALSE; assert items[8].is_list = FALSE; let chain_id = Helpers.bytes_to_felt(items[6].data_len, items[6].data); + + tempvar is_some = 1; + tempvar chain_id = chain_id; } + let is_some = [ap - 2]; let chain_id = [ap - 1]; tempvar tx = new model.EthTransaction( @@ -78,7 +83,7 @@ namespace EthTransaction { payload=payload, access_list_len=0, access_list=cast(0, felt*), - chain_id=chain_id, + chain_id=model.Option(is_some=is_some, value=chain_id), ); return tx; } @@ -135,7 +140,7 @@ namespace EthTransaction { payload=payload, access_list_len=access_list_len, access_list=access_list, - chain_id=chain_id, + chain_id=model.Option(is_some=1, value=chain_id), ); return tx; } @@ -193,7 +198,7 @@ namespace EthTransaction { payload=payload, access_list_len=access_list_len, access_list=access_list, - chain_id=chain_id, + chain_id=model.Option(is_some=1, value=chain_id), ); return tx; } diff --git a/tests/src/kakarot/accounts/test_account_contract.py b/tests/src/kakarot/accounts/test_account_contract.py index 3208f5355..d6e776596 100644 --- a/tests/src/kakarot/accounts/test_account_contract.py +++ b/tests/src/kakarot/accounts/test_account_contract.py @@ -6,7 +6,7 @@ import rlp from eth_account.account import Account from eth_utils import keccak -from hypothesis import assume, given, settings +from hypothesis import given, settings from hypothesis.strategies import binary, composite, integers, lists, permutations from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME from starkware.starknet.public.abi import ( @@ -15,7 +15,7 @@ ) from kakarot_scripts.constants import ARACHNID_PROXY_DEPLOYER, ARACHNID_PROXY_SIGNED_TX -from tests.utils.constants import CHAIN_ID, TRANSACTION_GAS_LIMIT, TRANSACTIONS +from tests.utils.constants import CHAIN_ID, TRANSACTIONS from tests.utils.errors import cairo_error from tests.utils.helpers import generate_random_private_key, rlp_encode_signed_data from tests.utils.hints import patch_hint @@ -382,272 +382,11 @@ def test_should_raise_invalid_signature_for_invalid_chain_id_when_tx_type0_not_p chain_id=CHAIN_ID + 1, ) - def test_should_raise_invalid_chain_id_tx_type_different_from_0( - self, cairo_run - ): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": 2_000_000_000, - "maxPriorityFeePerGas": 2_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "", - "value": 0x00, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - cairo_error(message="Invalid chain id"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=CHAIN_ID + 1, - ) - - @SyscallHandler.patch("Account_nonce", 1) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_should_raise_invalid_nonce(self, cairo_run, transaction): - # explicitly set the nonce in transaction to be different from the patch - transaction = {**transaction, "nonce": 0} - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - cairo_error(message="Invalid nonce"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch("IERC20.balanceOf", lambda addr, data: [0, 0]) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_not_enough_ETH_balance(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Not enough ETH to pay msg.value + max gas fees"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch("IKakarot.get_block_gas_limit", lambda addr, data: [0]) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_transaction_gas_limit_too_high(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Transaction gas_limit > Block gas_limit"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch( - "IKakarot.get_base_fee", lambda addr, data: [TRANSACTION_GAS_LIMIT * 10**10] - ) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - def test_raise_max_fee_per_gas_too_low(self, cairo_run, transaction): - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - tx_data = list(rlp_encode_signed_data(transaction)) - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)), - cairo_error(message="Max fee per gas too low"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction.get("chainId") or CHAIN_ID, - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @given(gas_limit=integers(min_value=2**64, max_value=DEFAULT_PRIME - 1)) - def test_raise_gas_limit_too_high(self, cairo_run, gas_limit): - transaction = { - "type": 2, - "gas": gas_limit, - "maxFeePerGas": 2_000_000_000, - "maxPriorityFeePerGas": 3_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Gas limit too high"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @given(maxFeePerGas=integers(min_value=2**128, max_value=DEFAULT_PRIME - 1)) - def test_raise_max_fee_per_gas_too_high(self, cairo_run, maxFeePerGas): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": maxFeePerGas, - "maxPriorityFeePerGas": 3_000_000_000, - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Max fee per gas too high"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @composite - def max_priority_fee_too_high(draw): - maxFeePerGas = draw(integers(min_value=0, max_value=2**128 - 1)) - maxPriorityFeePerGas = draw(integers(min_value=0, max_value=2**128 - 1)) - assume(maxFeePerGas < maxPriorityFeePerGas) - return (maxFeePerGas, maxPriorityFeePerGas) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) + @SyscallHandler.patch("IKakarot.get_native_token", lambda _, __: [0xDEAD]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @given(max_priority_fee_too_high()) - def test_raise_max_priority_fee_too_high( - self, cairo_run, max_priority_fee_too_high - ): - transaction = { - "type": 2, - "gas": 100_000, - "maxFeePerGas": max_priority_fee_too_high[0], - "maxPriorityFeePerGas": max_priority_fee_too_high[1], - "data": "0x616263646566", - "nonce": 34, - "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", - "value": 0x5AF3107A4000, - "accessList": [], - "chainId": CHAIN_ID, - } - tx_data = list(rlp_encode_signed_data(transaction)) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) - signature = [*int_to_uint256(signed.r), *int_to_uint256(signed.s), signed.v] - - with ( - SyscallHandler.patch("Account_evm_address", address), - SyscallHandler.patch("Account_nonce", transaction["nonce"]), - cairo_error(message="Max priority fee greater than max fee per gas"), - ): - cairo_run( - "test__execute_from_outside", - tx_data=tx_data, - signature=signature, - chain_id=transaction["chainId"], - ) - - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) - @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) def test_pass_authorized_pre_eip155_transaction(self, cairo_run): rlp_decoded = rlp.decode(ARACHNID_PROXY_SIGNED_TX) @@ -688,17 +427,10 @@ def test_pass_authorized_pre_eip155_transaction(self, cairo_run): assert output_len == 1 assert output[0] == 0x68656C6C6F - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) @pytest.mark.parametrize("transaction", TRANSACTIONS) def test_pass_all_transactions_types(self, cairo_run, seed, transaction): @@ -732,17 +464,10 @@ def test_pass_all_transactions_types(self, cairo_run, seed, transaction): assert output_len == 1 assert output[0] == 0x68656C6C6F - @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) - @SyscallHandler.patch( - "IERC20.balanceOf", lambda addr, data: int_to_uint256(10**128) - ) - @SyscallHandler.patch( - "IKakarot.get_block_gas_limit", lambda addr, data: [TRANSACTION_GAS_LIMIT] - ) - @SyscallHandler.patch("IKakarot.get_base_fee", lambda addr, data: [0]) + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: int_to_uint256(10**128)) @SyscallHandler.patch( - "IKakarot.eth_send_transaction", - lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + "IKakarot.eth_send_raw_unsigned_tx", + lambda _, __: [1, 0x68656C6C6F, 1, 1], # hello ) def test_should_pass_all_data_len(self, cairo_run, bytecode): transaction = { diff --git a/tests/src/kakarot/test_kakarot.cairo b/tests/src/kakarot/test_kakarot.cairo index 9a5fad07d..a97d08208 100644 --- a/tests/src/kakarot/test_kakarot.cairo +++ b/tests/src/kakarot/test_kakarot.cairo @@ -6,6 +6,7 @@ from starkware.cairo.common.uint256 import Uint256 from kakarot.library import Kakarot from kakarot.kakarot import ( + eth_send_raw_unsigned_tx, register_account, set_native_token, set_base_fee, @@ -24,8 +25,6 @@ from kakarot.account import Account func eth_call{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* }() -> (model.EVM*, model.State*, felt, felt) { - // Given - tempvar origin; tempvar to: model.Option; tempvar gas_limit; @@ -40,7 +39,6 @@ func eth_call{ %{ from tests.utils.uint256 import int_to_uint256 - ids.origin = program_input.get("origin", 0) ids.to.is_some = int(bool(program_input.get("to") is not None)) ids.to.value = program_input.get("to") or 0 @@ -70,6 +68,24 @@ func eth_call{ return (evm, state, gas_used, required_gas); } +func test__eth_send_raw_unsigned_tx{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr, bitwise_ptr: BitwiseBuiltin* +}() -> (felt, felt*, felt, felt) { + tempvar tx_data_len: felt; + let (tx_data) = alloc(); + + %{ + segments.write_arg(ids.tx_data, program_input["tx_data"]) + ids.tx_data_len = len(program_input["tx_data"]) + %} + + let (return_data_len, return_data, success, gas_used) = eth_send_raw_unsigned_tx( + tx_data_len=tx_data_len, tx_data=tx_data + ); + + return (return_data_len, return_data, success, gas_used); +} + func compute_starknet_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( ) -> felt { tempvar evm_address; diff --git a/tests/src/kakarot/test_kakarot.py b/tests/src/kakarot/test_kakarot.py index 4c47d22c4..fe01e6aec 100644 --- a/tests/src/kakarot/test_kakarot.py +++ b/tests/src/kakarot/test_kakarot.py @@ -7,16 +7,17 @@ from eth_utils import keccak from eth_utils.address import to_checksum_address from hypothesis import given -from hypothesis.strategies import integers +from hypothesis.strategies import composite, integers +from starkware.cairo.lang.cairo_constants import DEFAULT_PRIME from starkware.starknet.public.abi import get_storage_var_address from web3._utils.abi import map_abi_data from web3._utils.normalizers import BASE_RETURN_NORMALIZERS from web3.exceptions import NoABIFunctionsFound from kakarot_scripts.ef_tests.fetch import EF_TESTS_PARSED_DIR -from tests.utils.constants import TRANSACTION_GAS_LIMIT +from tests.utils.constants import CHAIN_ID, TRANSACTION_GAS_LIMIT, TRANSACTIONS from tests.utils.errors import cairo_error -from tests.utils.helpers import felt_to_signed_int +from tests.utils.helpers import felt_to_signed_int, rlp_encode_signed_data from tests.utils.syscall_handler import SyscallHandler, parse_state CONTRACT_ADDRESS = 1234 @@ -414,6 +415,180 @@ def test_should_return_chain_id_modulo_53(self, cairo_run, chain_id): res = cairo_run("test__eth_chain_id") assert res == chain_id % 2**53 + class TestEthSendRawTransactionEntrypoint: + def test_should_raise_invalid_chain_id_tx_type_different_from_0( + self, cairo_run + ): + transaction = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": 2_000_000_000, + "maxPriorityFeePerGas": 2_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "", + "value": 0x00, + "accessList": [], + "chainId": 9999, + } + tx_data = list(rlp_encode_signed_data(transaction)) + + with cairo_error(message="Invalid chain id"): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("IAccount.get_nonce", lambda addr, data: [1]) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_should_raise_invalid_nonce(self, cairo_run, tx): + # explicitly set the nonce in transaction to be different from the patch + tx = {**tx, "nonce": 0} + tx_data = list(rlp_encode_signed_data(tx)) + with cairo_error(message="Invalid nonce"): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @given(gas_limit=integers(min_value=2**64, max_value=DEFAULT_PRIME - 1)) + def test_raise_gas_limit_too_high(self, cairo_run, gas_limit): + tx = { + "type": 2, + "gas": gas_limit, + "maxFeePerGas": 2_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Gas limit too high"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @given(maxFeePerGas=integers(min_value=2**128, max_value=DEFAULT_PRIME - 1)) + def test_raise_max_fee_per_gas_too_high(self, cairo_run, maxFeePerGas): + tx = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": maxFeePerGas, + "maxPriorityFeePerGas": 3_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max fee per gas too high"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_transaction_gas_limit_too_high(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Transaction gas_limit > Block gas_limit"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @SyscallHandler.patch("Kakarot_base_fee", TRANSACTION_GAS_LIMIT * 10**10) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_max_fee_per_gas_too_low(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max fee per gas too low"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @composite + def max_priority_fee_too_high(draw): + max_fee_per_gas = draw(integers(min_value=0, max_value=2**128 - 2)) + max_priority_fee_per_gas = draw( + integers(min_value=max_fee_per_gas + 1, max_value=DEFAULT_PRIME - 1) + ) + return (max_fee_per_gas, max_priority_fee_per_gas) + + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @given(max_priority_fee_too_high()) + def test_raise_max_priority_fee_too_high( + self, cairo_run, max_priority_fee_too_high + ): + tx = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": max_priority_fee_too_high[0], + "maxPriorityFeePerGas": max_priority_fee_too_high[1], + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Max priority fee greater than max fee per gas"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + + @SyscallHandler.patch("IERC20.balanceOf", lambda _, __: [0, 0]) + @SyscallHandler.patch("Kakarot_block_gas_limit", TRANSACTION_GAS_LIMIT) + @SyscallHandler.patch("IAccount.get_evm_address", lambda _, __: [0xABDE1]) + @pytest.mark.parametrize("tx", TRANSACTIONS) + def test_raise_not_enough_ETH_balance(self, cairo_run, tx): + tx_data = list(rlp_encode_signed_data(tx)) + + with ( + SyscallHandler.patch("IAccount.get_nonce", lambda _, __: [tx["nonce"]]), + cairo_error(message="Not enough ETH to pay msg.value + max gas fees"), + ): + cairo_run( + "test__eth_send_raw_unsigned_tx", + tx_data_len=len(tx_data), + tx_data=tx_data, + ) + class TestLoopProfiling: @pytest.mark.slow @pytest.mark.NoCI diff --git a/tests/src/utils/test_eth_transaction.py b/tests/src/utils/test_eth_transaction.py index fb80a8901..1e6569bf7 100644 --- a/tests/src/utils/test_eth_transaction.py +++ b/tests/src/utils/test_eth_transaction.py @@ -49,7 +49,7 @@ async def test_should_decode_all_transactions_types( assert expected_to == decoded_tx["destination"] assert transaction["value"] == int(decoded_tx["amount"], 16) # pre-eip155 txs have an internal chain_id set to 0 in the decoded tx - assert transaction.get("chainId", 0) == decoded_tx["chain_id"] + assert transaction.get("chainId") == decoded_tx["chain_id"] assert expected_data == decoded_tx["payload"] assert expected_access_list == decoded_tx["access_list"] diff --git a/tests/utils/serde.py b/tests/utils/serde.py index b8460c8e7..4d6208fef 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -164,7 +164,9 @@ def serialize_eth_transaction(self, ptr): if raw["access_list"] is not None else [] ), - "chain_id": raw["chain_id"], + "chain_id": ( + raw["chain_id"]["value"] if raw["chain_id"]["is_some"] == 1 else None + ), } def serialize_message(self, ptr):