diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa7c0bde1..497877be7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,8 +175,8 @@ jobs: - name: Checkout ef-tests uses: actions/checkout@v4 with: - repository: kkrt-labs/ef-tests - ref: v0.2.2 + repository: obatirou/ef-tests + ref: oba/execute-from-outside-ef-tests - name: Checkout local skip file uses: actions/checkout@v4 with: diff --git a/.github/workflows/trunk-check.yaml b/.github/workflows/trunk-check.yaml index 31d062b6b..2e0ec335f 100644 --- a/.github/workflows/trunk-check.yaml +++ b/.github/workflows/trunk-check.yaml @@ -20,12 +20,12 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up Python 3.10 + - name: Set up Python 3.10.14 uses: actions/setup-python@v4 with: python-version: 3.10.14 cache: pip - - run: pip install sympy==1.11.1 cairo-lang==0.13.1 + - run: pip install cairo-lang==0.13.1 sympy==1.11.1 - name: Install Foundry uses: foundry-rs/foundry-toolchain@v1 diff --git a/deployments/kakarot-staging/declarations.json b/deployments/kakarot-staging/declarations.json index 255a4afaa..5cd4db417 100644 --- a/deployments/kakarot-staging/declarations.json +++ b/deployments/kakarot-staging/declarations.json @@ -10,4 +10,4 @@ "replace_class": "0xa187318c5e79b010cf45975f589f0a8d441fadde5b1e7ccad46501568437b5", "Counter": "0x2abf5b9916d3c6ae6000ab239bf5aba8b40d9a1750ffc54b6d281ac83137382", "MockPragmaOracle": "0x675f00328ff84f127d71b179b3f3a3a06ce8432054770cddd5729c8d62866da" -} \ No newline at end of file +} diff --git a/deployments/kakarot-staging/deployments.json b/deployments/kakarot-staging/deployments.json index 7a553636f..13f652d2b 100644 --- a/deployments/kakarot-staging/deployments.json +++ b/deployments/kakarot-staging/deployments.json @@ -19,4 +19,4 @@ "tx": "0x27f878dbcff30f3d0a031bd5af2af6feb269f5e41df545b92f2ef875bcd385f", "artifact": "build/ssj/contracts_MockPragmaOracle" } -} \ No newline at end of file +} diff --git a/kakarot_scripts/ef_tests/fetch.py b/kakarot_scripts/ef_tests/fetch.py index d4c124b46..87c6eb9d2 100644 --- a/kakarot_scripts/ef_tests/fetch.py +++ b/kakarot_scripts/ef_tests/fetch.py @@ -8,9 +8,9 @@ import requests -EF_TESTS_TAG = "v13.2" +EF_TESTS_TAG = "v13.3-kkrt-1" EF_TESTS_URL = ( - f"https://github.com/ethereum/tests/archive/refs/tags/{EF_TESTS_TAG}.tar.gz" + f"https://github.com/kkrt-labs/tests/archive/refs/tags/{EF_TESTS_TAG}.tar.gz" ) EF_TESTS_DIR = Path("tests") / "ef_tests" / "test_data" / EF_TESTS_TAG EF_TESTS_PARSED_DIR = Path("tests") / "ef_tests" / "test_data" / "parsed" diff --git a/kakarot_scripts/utils/kakarot.py b/kakarot_scripts/utils/kakarot.py index 30f2e7e1e..6dea3956e 100644 --- a/kakarot_scripts/utils/kakarot.py +++ b/kakarot_scripts/utils/kakarot.py @@ -16,8 +16,6 @@ from hexbytes import HexBytes from starknet_py.net.account.account import Account from starknet_py.net.client_errors import ClientError -from starknet_py.net.client_models import Call -from starknet_py.net.models.transaction import InvokeV1 from starknet_py.net.signer.stark_curve_signer import KeyPair from starkware.starknet.public.abi import starknet_keccak from web3 import Web3 @@ -44,6 +42,7 @@ from kakarot_scripts.utils.starknet import get_balance from kakarot_scripts.utils.starknet import get_contract as _get_starknet_contract from kakarot_scripts.utils.starknet import get_deployments as _get_starknet_deployments +from kakarot_scripts.utils.starknet import get_starknet_account from kakarot_scripts.utils.starknet import invoke as _invoke_starknet from kakarot_scripts.utils.starknet import wait_for_transaction from tests.utils.constants import TRANSACTION_GAS_LIMIT @@ -411,7 +410,13 @@ async def eth_send_transaction( evm_account.signer.public_key.to_checksum_address() ) else: - nonce = await evm_account.get_nonce() + nonce = ( + await ( + _get_starknet_contract("account_contract", address=evm_account.address) + .functions["get_nonce"] + .call() + ) + ).nonce payload = { "type": 0x2, @@ -447,7 +452,6 @@ async def eth_send_transaction( evm_tx.s, evm_tx.v, packed_encoded_unsigned_tx, - evm_account, max_fee, ) @@ -458,38 +462,44 @@ async def send_starknet_transaction( signature_s: int, signature_v: int, packed_encoded_unsigned_tx: List[int], - caller_eoa: Optional[Account] = None, max_fee: Optional[int] = None, ): - prepared_invoke = await evm_account._prepare_invoke( - calls=[ - Call( - to_addr=0xDEAD, # unused in current EOA implementation - selector=0xDEAD, # unused in current EOA implementation - calldata=packed_encoded_unsigned_tx, - ) - ], - max_fee=int(5e17) if max_fee is None else max_fee, - ) - # We need to reconstruct the prepared_invoke with the new signature - # And Invoke.signature is Frozen - prepared_invoke = InvokeV1( - version=prepared_invoke.version, - max_fee=prepared_invoke.max_fee, - signature=[ - *int_to_uint256(signature_r), - *int_to_uint256(signature_s), - signature_v, - ], - nonce=prepared_invoke.nonce, - sender_address=prepared_invoke.sender_address, - calldata=prepared_invoke.calldata, + relayer = await get_starknet_account() + current_timestamp = (await RPC_CLIENT.get_block("latest")).timestamp + outside_execution = { + "caller": int.from_bytes(b"ANY_CALLER", "big"), + "nonce": 0, # not used in Kakarot + "execute_after": current_timestamp - 60 * 60, + "execute_before": current_timestamp + 60 * 60, + } + max_fee = int(5e17) if max_fee in [None, 0] else max_fee + response = ( + await _get_starknet_contract( + "account_contract", address=evm_account.address, provider=relayer + ) + .functions["execute_from_outside"] + .invoke_v1( + outside_execution=outside_execution, + call_array=[ + { + "to": 0xDEAD, + "selector": 0xDEAD, + "data_offset": 0, + "data_len": len(packed_encoded_unsigned_tx), + } + ], + calldata=list(packed_encoded_unsigned_tx), + signature=[ + *int_to_uint256(signature_r), + *int_to_uint256(signature_s), + signature_v, + ], + max_fee=max_fee, + ) ) - response = await evm_account.client.send_transaction(prepared_invoke) - - await wait_for_transaction(tx_hash=response.transaction_hash) - receipt = await RPC_CLIENT.get_transaction_receipt(response.transaction_hash) + await wait_for_transaction(tx_hash=response.hash) + receipt = await RPC_CLIENT.get_transaction_receipt(response.hash) transaction_events = [ event for event in receipt.events diff --git a/kakarot_scripts/utils/l1.py b/kakarot_scripts/utils/l1.py index c95455a02..14c6e77cb 100644 --- a/kakarot_scripts/utils/l1.py +++ b/kakarot_scripts/utils/l1.py @@ -65,14 +65,14 @@ def l1_contract_exists(address: HexBytes) -> bool: return False -async def deploy_on_l1( +def deploy_on_l1( contract_app: str, contract_name: str, *args, **kwargs ) -> Web3Contract: logger.info(f"⏳ Deploying {contract_name}") caller_eoa = kwargs.pop("caller_eoa", None) contract = get_l1_contract(contract_app, contract_name) value = kwargs.pop("value", 0) - receipt, response, success, gas_used = await send_l1_transaction( + receipt, response, success, gas_used = send_l1_transaction( to=0, gas=int(TRANSACTION_GAS_LIMIT), data=contract.constructor(*args, **kwargs).data_in_transaction, @@ -117,7 +117,7 @@ def get_l1_contract( return contract -async def send_l1_transaction( +def send_l1_transaction( to: Union[int, str], data: Union[str, bytes], gas: int = 21_000, @@ -161,7 +161,7 @@ async def send_l1_transaction( def _wrap_web3(fun: str, caller_eoa_: Optional[EvmAccount] = None): """Wrap a contract function call with the WEB3 provider.""" - async def _wrapper(self, *args, **kwargs): + def _wrapper(self, *args, **kwargs): abi = self.get_function_by_name(fun).abi gas_price = kwargs.pop("gas_price", DEFAULT_GAS_PRICE) gas_limit = kwargs.pop("gas_limit", TRANSACTION_GAS_LIMIT) @@ -196,7 +196,7 @@ async def _wrapper(self, *args, **kwargs): return normalized[0] if len(normalized) == 1 else normalized logger.info(f"⏳ Executing {fun} at address {self.address}") - receipt, response, success, gas_used = await send_l1_transaction( + receipt, response, success, gas_used = send_l1_transaction( to=self.address, value=value, gas=gas_limit, diff --git a/src/backend/starknet.cairo b/src/backend/starknet.cairo index 787db152a..c54cd47bd 100644 --- a/src/backend/starknet.cairo +++ b/src/backend/starknet.cairo @@ -8,6 +8,7 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin from starkware.cairo.common.dict_access import DictAccess from starkware.cairo.common.uint256 import Uint256 from starkware.cairo.common.math import unsigned_div_rem +from starkware.cairo.common.math_cmp import is_le from starkware.cairo.common.memset import memset from starkware.starknet.common.syscalls import ( emit_event, @@ -241,8 +242,13 @@ namespace Internals { } let event: model.Event = [events]; + // See 300 max data_len for events + // https://github.com/starkware-libs/blockifier/blob/9bfb3d4c8bf1b68a0c744d1249b32747c75a4d87/crates/blockifier/resources/versioned_constants.json + // The whole data_len should be less than 300 + tempvar data_len = is_le(event.data_len, 300) * (event.data_len - 300) + 300; + emit_event( - keys_len=event.topics_len, keys=event.topics, data_len=event.data_len, data=event.data + keys_len=event.topics_len, keys=event.topics, data_len=data_len, data=event.data ); _emit_events(events_len - 1, events + model.Event.SIZE); diff --git a/src/kakarot/accounts/account_contract.cairo b/src/kakarot/accounts/account_contract.cairo index 38527e8d7..aabc9ed22 100644 --- a/src/kakarot/accounts/account_contract.cairo +++ b/src/kakarot/accounts/account_contract.cairo @@ -2,31 +2,30 @@ %lang starknet -// Starkware dependencies from openzeppelin.access.ownable.library import Ownable, Ownable_owner +from starkware.cairo.common.alloc import alloc from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin, SignatureBuiltin +from starkware.cairo.common.math import assert_le, unsigned_div_rem +from starkware.cairo.common.math_cmp import is_le from starkware.cairo.common.uint256 import Uint256 +from starkware.starknet.common.syscalls import ( + get_tx_info, + get_caller_address, + get_block_timestamp, + call_contract, +) from starkware.cairo.common.bool import FALSE, TRUE -// Local dependencies from kakarot.accounts.library import ( AccountContract, Account_implementation, - Account_cairo1_helpers_class_hash, Account_authorized_message_hashes, + Account_bytecode_len, ) -from kakarot.accounts.model import CallArray -from utils.utils import Helpers -from kakarot.errors import Errors +from kakarot.accounts.model import CallArray, OutsideExecution from kakarot.interfaces.interfaces import IKakarot, IAccount -from starkware.starknet.common.syscalls import ( - get_tx_info, - get_caller_address, - replace_class, - call_contract, -) -from starkware.cairo.common.math import assert_le, unsigned_div_rem -from starkware.cairo.common.alloc import alloc +from kakarot.errors import Errors +from utils.utils import Helpers const COMPUTE_STARKNET_ADDRESS_SELECTOR = 0x0ad7772990f7f5a506d84e5723efd1242e989c23f45653870d49d6d107f6e7; @@ -97,6 +96,65 @@ func is_initialized{ } // EOA specific entrypoints +@external +func execute_from_outside{ + syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr +}( + outside_execution: OutsideExecution, + call_array_len: felt, + call_array: CallArray*, + calldata_len: felt, + calldata: felt*, + signature_len: felt, + signature: felt*, +) -> (response_len: felt, response: felt*) { + alloc_locals; + let (caller) = get_caller_address(); + + // Starknet validation + if (outside_execution.caller != 'ANY_CALLER') { + assert caller = outside_execution.caller; + } + let (block_timestamp) = get_block_timestamp(); + let too_early = is_le(block_timestamp, outside_execution.execute_after); + with_attr error_message("Execute from outside: too early") { + assert too_early = FALSE; + } + let too_late = is_le(outside_execution.execute_before, block_timestamp); + with_attr error_message("Execute from outside: too late") { + assert too_late = FALSE; + } + with_attr error_message("Execute from outside: multicall not supported") { + assert call_array_len = 1; + } + let (tx_info) = get_tx_info(); + let version = tx_info.version; + with_attr error_message("Deprecated tx version: {version}") { + assert_le(1, version); + } + + // EOA validation + let (bytecode_len) = Account_bytecode_len.read(); + with_attr error_message("EOAs cannot have code") { + assert bytecode_len = 0; + } + + // Unpack the tx data + let packed_tx_data_len = [call_array].data_len; + let packed_tx_data = calldata + [call_array].data_offset; + let tx_data_len = [packed_tx_data]; + let (tx_data) = Helpers.load_packed_bytes( + packed_tx_data_len - 1, packed_tx_data + 1, tx_data_len + ); + + // Cast Starknet chain id to u32 + let (_, chain_id) = unsigned_div_rem(tx_info.chain_id, 2 ** 32); + + let (response_len, response) = AccountContract.execute_from_outside( + tx_data_len, tx_data, signature_len, signature, chain_id + ); + return (response_len, response); +} // @notice Validate a transaction // @dev The transaction is considered as valid if it is signed with the correct address and is a valid kakarot transaction @@ -108,12 +166,9 @@ func is_initialized{ func __validate__{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr }(call_array_len: felt, call_array: CallArray*, calldata_len: felt, calldata: felt*) { - AccountContract.validate( - call_array_len=call_array_len, - call_array=call_array, - calldata_len=calldata_len, - calldata=calldata, - ); + with_attr error_message("EOA: __validate__ not supported") { + assert 1 = 0; + } return (); } @@ -148,65 +203,11 @@ func __execute__{ }(call_array_len: felt, call_array: CallArray*, calldata_len: felt, calldata: felt*) -> ( response_len: felt, response: felt* ) { - alloc_locals; - - // Upgrade flow - let (latest_account_class, latest_helpers_class) = AccountContract.get_latest_classes(); - let (this_helpers_class) = Account_cairo1_helpers_class_hash.read(); - if (latest_helpers_class != this_helpers_class) { - Account_cairo1_helpers_class_hash.write(latest_helpers_class); - tempvar syscall_ptr = syscall_ptr; - tempvar range_check_ptr = range_check_ptr; - tempvar pedersen_ptr = pedersen_ptr; - } else { - tempvar syscall_ptr = syscall_ptr; - tempvar range_check_ptr = range_check_ptr; - tempvar pedersen_ptr = pedersen_ptr; - } - let syscall_ptr = cast([ap - 3], felt*); - let range_check_ptr = [ap - 2]; - let pedersen_ptr = cast([ap - 1], HashBuiltin*); - - let (this_class) = Account_implementation.read(); - if (latest_account_class != this_class) { - Account_implementation.write(latest_account_class); - // Library call to new class - let (response_len, response) = IAccount.library_call___execute__( - class_hash=latest_account_class, - call_array_len=call_array_len, - call_array=call_array, - calldata_len=calldata_len, - calldata=calldata, - ); - replace_class(latest_account_class); - return (response_len, response); - } - - // Execution flow - let (tx_info) = get_tx_info(); - let version = tx_info.version; - with_attr error_message("EOA: deprecated tx version: {version}") { - assert_le(1, version); - } - - let (caller) = get_caller_address(); - with_attr error_message("EOA: reentrant call") { - assert caller = 0; - } - - with_attr error_message("EOA: multicall not supported") { - assert call_array_len = 1; + with_attr error_message("EOA: __execute__ not supported") { + assert 1 = 0; } - - let (local response: felt*) = alloc(); - let (response_len) = AccountContract.execute( - call_array_len=call_array_len, - call_array=call_array, - calldata_len=calldata_len, - calldata=calldata, - response=response, - ); - return (response_len, response); + let (response) = alloc(); + return (0, response); } // @notice Store the bytecode of the contract. diff --git a/src/kakarot/accounts/library.cairo b/src/kakarot/accounts/library.cairo index 42788db1a..b398d00ef 100644 --- a/src/kakarot/accounts/library.cairo +++ b/src/kakarot/accounts/library.cairo @@ -2,14 +2,13 @@ from openzeppelin.access.ownable.library import Ownable, Ownable_owner from starkware.cairo.common.alloc import alloc -from starkware.cairo.common.bool import FALSE +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 unsigned_div_rem, split_int, split_felt from starkware.cairo.common.memcpy import memcpy from starkware.cairo.common.uint256 import Uint256, uint256_not, uint256_le from starkware.cairo.common.math_cmp import is_le -from starkware.cairo.common.math import assert_not_zero from starkware.starknet.common.syscalls import ( StorageRead, StorageWrite, @@ -19,7 +18,6 @@ from starkware.starknet.common.syscalls import ( storage_write, StorageReadRequest, CallContract, - get_tx_info, get_contract_address, get_caller_address, replace_class, @@ -61,10 +59,6 @@ func Account_implementation() -> (address: felt) { func Account_evm_address() -> (evm_address: felt) { } -@storage_var -func Account_cairo1_helpers_class_hash() -> (res: felt) { -} - @storage_var func Account_valid_jumpdests() -> (is_valid: felt) { } @@ -114,10 +108,6 @@ namespace AccountContract { let infinite = Uint256(Constants.UINT128_MAX, Constants.UINT128_MAX); IERC20.approve(native_token_address, kakarot_address, infinite); - // Write Cairo1Helpers class to storage - let (cairo1_helpers_class_hash) = IKakarot.get_cairo1_helpers_class_hash(kakarot_address); - Account_cairo1_helpers_class_hash.write(cairo1_helpers_class_hash); - // Register the account in the Kakarot mapping IKakarot.register_account(kakarot_address, evm_address); return (); @@ -160,198 +150,166 @@ namespace AccountContract { // EOA functions - // @notice Validate the signature of every call in the call array. - // @dev Recursively validates if tx is signed and valid for each call -> see utils/eth_transaction.cairo - // @param call_array_len The length of the call array. - // @param call_array The call array. - // @param calldata_len The length of the calldata. - // @param calldata The calldata. - func validate{ + // @notice Validate an Ethereum transaction and execute it. + // @dev This function validates the transaction by checking its signature, + // chain_id, nonce and gas. It then sends it to Kakarot. + // @param tx_data_len The length of tx data + // @param tx_data The tx data. + // @param signature_len The length of tx signature. + // @param signature The tx signature. + // @param chain_id The expected chain id of the tx + func execute_from_outside{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr, - }(call_array_len: felt, call_array: CallArray*, calldata_len: felt, calldata: felt*) -> () { + }(tx_data_len: felt, tx_data: felt*, signature_len: felt, signature: felt*, chain_id: felt) -> ( + response_len: felt, response: felt* + ) { alloc_locals; - if (call_array_len == 0) { - // Validates that this account doesn't have code. Check only once at the end of the recursion. - let (bytecode_len) = Account_bytecode_len.read(); - with_attr error_message("EOAs cannot have code") { - assert bytecode_len = 0; - } - return (); + with_attr error_message("Incorrect signature length") { + assert signature_len = 5; } + let r = Uint256(signature[0], signature[1]); + let s = Uint256(signature[2], signature[3]); + let v = signature[4]; - let (address) = Account_evm_address.read(); - let (tx_info) = get_tx_info(); - - // Assert signature field is of length 5: r_low, r_high, s_low, s_high, v - assert tx_info.signature_len = 5; - let r = Uint256(tx_info.signature[0], tx_info.signature[1]); - let s = Uint256(tx_info.signature[2], tx_info.signature[3]); - let v = tx_info.signature[4]; - let (_, chain_id) = unsigned_div_rem(tx_info.chain_id, 2 ** 32); - - // Unpack the tx data - let packed_tx_data_len = [call_array].data_len; - let packed_tx_data = calldata + [call_array].data_offset; - - let tx_data_len = [packed_tx_data]; - let (tx_data) = Helpers.load_packed_bytes( - packed_tx_data_len - 1, packed_tx_data + 1, tx_data_len - ); - - Internals.validate(address, tx_info.nonce, chain_id, r, s, v, tx_data_len, tx_data); - - validate( - call_array_len=call_array_len - 1, - call_array=call_array + CallArray.SIZE, - calldata_len=calldata_len, - calldata=calldata, - ); - - return (); - } - - // @notice Execute the transaction. - // @param call_array_len The length of the call array. - // @param call_array The call array. - // @param calldata_len The length of the calldata. - // @param calldata The calldata. - // @param response The response data array to be updated. - // @return response_len The total length of the response data array. - func execute{ - syscall_ptr: felt*, - pedersen_ptr: HashBuiltin*, - bitwise_ptr: BitwiseBuiltin*, - range_check_ptr, - }( - call_array_len: felt, - call_array: CallArray*, - calldata_len: felt, - calldata: felt*, - response: felt*, - ) -> (response_len: felt) { - alloc_locals; - if (call_array_len == 0) { - return (response_len=0); + let tx_type = EthTransaction.get_tx_type(tx_data); + local y_parity: felt; + local pre_eip155_tx: felt; + if (tx_type == 0) { + let is_eip155_tx = is_le(v, 28); + assert pre_eip155_tx = is_eip155_tx; + if (is_eip155_tx != FALSE) { + assert y_parity = v - 27; + } else { + assert y_parity = (v - 2 * chain_id - 35); + } + tempvar range_check_ptr = range_check_ptr; + } else { + assert pre_eip155_tx = FALSE; + assert y_parity = v; + tempvar range_check_ptr = range_check_ptr; } + let range_check_ptr = [ap - 1]; - // Unpack the tx data - let packed_tx_data_len = [call_array].data_len; - let packed_tx_data = calldata + [call_array].data_offset; - - let tx_data_len = [packed_tx_data]; - let (tx_data) = Helpers.load_packed_bytes( - packed_tx_data_len - 1, packed_tx_data + 1, tx_data_len + let (local words: felt*) = alloc(); + let (words_len, last_word, last_word_num_bytes) = bytes_to_bytes8_little_endian( + words, tx_data_len, tx_data + ); + let (kakarot_address) = Ownable_owner.read(); + let (helpers_class) = IKakarot.get_cairo1_helpers_class_hash(kakarot_address); + let (msg_hash) = ICairo1Helpers.library_call_keccak( + class_hash=helpers_class, + words_len=words_len, + words=words, + last_input_word=last_word, + last_input_num_bytes=last_word_num_bytes, + ); + let (address) = Account_evm_address.read(); + Signature.verify_eth_signature_uint256( + msg_hash=msg_hash, + r=r, + s=s, + v=y_parity, + eth_address=address, + helpers_class=helpers_class, ); let tx = EthTransaction.decode(tx_data_len, tx_data); - // No matter the status of the execution in EVM terms (success - failure - rejected), the nonce of the - // transaction sender must be incremented, as the protocol nonce is. While we use the protocol nonce for the - // transaction validation, we don't make the distinction between CAs and EOAs in their - // Starknet contract representation. As such, the stored nonce of an EOA account must always match the - // protocol nonce, increased by one right before each transaction execution. - // - // In the official specification, this nonce increment is done right after the tx validation checks. - // Since we can only perform these checks in __execute__, which increments the protocol nonce by one, - // we need to increment the stored nonce here as well. - // - // The protocol nonce is updated once per __execute__ call, while the EVM nonce is updated once per - // transaction. If we were to execute more than one transaction in a single __execute__ call, we would - // need to change the nonce incrementation logic. - // - // TODO! If the previous execute failed with a CairoVM error, the protocol nonce was - // incremented but not the storage nonce, and there is an off-by-one count until it's - // overwritten by the next successful execute. - let (tx_info) = get_tx_info(); - Account_nonce.write(tx_info.nonce + 1); + // Whitelisting pre-eip155 or validate chain_id for post eip155 + 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]; - let (kakarot_address) = Ownable_owner.read(); - let (block_gas_limit) = IKakarot.get_block_gas_limit(kakarot_address); - let tx_gas_fits_in_block = is_le(tx.gas_limit, block_gas_limit); + // Validate nonce + let (account_nonce) = Account_nonce.read(); + with_attr error_message("Invalid nonce") { + assert tx.signer_nonce = account_nonce; + } - let (base_fee) = IKakarot.get_base_fee(kakarot_address); + // 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); - // ensure that the user was willing to at least pay the base fee - let enough_fee = is_le(base_fee, tx.max_fee_per_gas); - let max_fee_greater_priority_fee = is_le(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); 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; + } - if (enough_fee * max_fee_greater_priority_fee * is_balance_enough * tx_gas_fits_in_block == 0) { - let (return_data_len, return_data) = Errors.eth_validation_failed(); - tempvar range_check_ptr = range_check_ptr; - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar return_data_len = return_data_len; - tempvar return_data = return_data; - tempvar success = FALSE; - tempvar gas_used = 0; - } else { - // priority fee is capped because the base fee is filled first - let possible_priority_fee = tx.max_fee_per_gas - base_fee; - let priority_fee_is_max_priority_fee = is_le( - tx.max_priority_fee_per_gas, possible_priority_fee - ); - 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; - // signer pays both the priority fee and the base fee - let effective_gas_price = priority_fee_per_gas + base_fee; - - 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, - ); - tempvar range_check_ptr = range_check_ptr; - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar return_data_len = return_data_len; - tempvar return_data = return_data; - tempvar success = success; - tempvar gas_used = gas_used; + let (block_gas_limit) = IKakarot.get_block_gas_limit(kakarot_address); + let tx_gas_fits_in_block = is_le(tx.gas_limit, block_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_le(block_base_fee, tx.max_fee_per_gas); + with_attr error_message("Max fee per gas too low") { + assert enough_fee = TRUE; + } + + let max_fee_greater_priority_fee = is_le(tx.max_priority_fee_per_gas, tx.max_fee_per_gas); + with_attr error_message("Max priority fee greater than max fee per gas") { + assert max_fee_greater_priority_fee = TRUE; } - let range_check_ptr = [ap - 7]; - let syscall_ptr = cast([ap - 6], felt*); - let pedersen_ptr = cast([ap - 5], HashBuiltin*); - let return_data_len = [ap - 4]; - let return_data = cast([ap - 3], felt*); - let success = [ap - 2]; - let gas_used = [ap - 1]; - memcpy(response, return_data, return_data_len); + let possible_priority_fee = tx.max_fee_per_gas - block_base_fee; + let priority_fee_is_max_priority_fee = is_le( + tx.max_priority_fee_per_gas, possible_priority_fee + ); + 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, + ); // See Argent account // https://github.com/argentlabs/argent-contracts-starknet/blob/c6d3ee5e05f0f4b8a5c707b4094446c3bc822427/contracts/account/ArgentAccount.cairo#L132 + // See 300 max data_len for events + // https://github.com/starkware-libs/blockifier/blob/9bfb3d4c8bf1b68a0c744d1249b32747c75a4d87/crates/blockifier/resources/versioned_constants.json + // The whole data_len should be less than 300, so it's the return_data should be less than 297 (+3 for return_data_len, success, gas_used) + tempvar return_data_len = is_le(return_data_len, 297) * (return_data_len - 297) + 297; transaction_executed.emit( response_len=return_data_len, response=return_data, success=success, gas_used=gas_used ); - let (response_len) = execute( - call_array_len - 1, - call_array + CallArray.SIZE, - calldata_len, - calldata, - response + return_data_len, - ); - - return (response_len=return_data_len + response_len); + return (response_len=return_data_len, response=return_data); } // Contract Account functions @@ -733,99 +691,4 @@ namespace Internals { jmp body if count != 0; jmp read; } - - // @notice Validate an Ethereum transaction - // @dev This function validates an Ethereum transaction by checking if the transaction - // is correctly signed by the given address, and if the nonce in the transaction - // matches the nonce of the account. It decodes the transaction using the decode function, - // and then verifies the Ethereum signature on the transaction hash. - // @param address The address that is supposed to have signed the transaction - // @param account_nonce The nonce of the account - // @param tx_data_len The length of the raw transaction data - // @param tx_data The raw transaction data - func validate{ - syscall_ptr: felt*, - pedersen_ptr: HashBuiltin*, - bitwise_ptr: BitwiseBuiltin*, - range_check_ptr, - }( - address: felt, - account_nonce: felt, - chain_id: felt, - r: Uint256, - s: Uint256, - v: felt, - tx_data_len: felt, - tx_data: felt*, - ) { - alloc_locals; - let tx = EthTransaction.decode(tx_data_len, tx_data); - with_attr error_message("Invalid nonce") { - assert tx.signer_nonce = account_nonce; - } - - // Note: here, the validate process assumes an ECDSA signature, and r, s, v field - // Technically, the transaction type can determine the signature scheme. - let tx_type = EthTransaction.get_tx_type(tx_data); - local y_parity: felt; - local pre_eip155_tx; - if (tx_type == 0) { - let is_eip155_tx = is_le(v, 28); - assert pre_eip155_tx = is_eip155_tx; - if (is_eip155_tx != FALSE) { - assert y_parity = v - 27; - } else { - assert y_parity = (v - 2 * chain_id - 35); - with_attr error_message("Invalid chain id") { - assert tx.chain_id = chain_id; - } - } - tempvar range_check_ptr = range_check_ptr; - } else { - assert pre_eip155_tx = FALSE; - assert y_parity = v; - with_attr error_message("Invalid chain id") { - assert tx.chain_id = chain_id; - } - tempvar range_check_ptr = range_check_ptr; - } - - let (local words: felt*) = alloc(); - let (words_len, last_word, last_word_num_bytes) = bytes_to_bytes8_little_endian( - words, tx_data_len, tx_data - ); - - let (helpers_class) = Account_cairo1_helpers_class_hash.read(); - let (msg_hash) = ICairo1Helpers.library_call_keccak( - class_hash=helpers_class, - words_len=words_len, - words=words, - last_input_word=last_word, - last_input_num_bytes=last_word_num_bytes, - ); - - 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 = 1; - } - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } else { - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - } - let pedersen_ptr = cast([ap - 2], HashBuiltin*); - let range_check_ptr = [ap - 1]; - - Signature.verify_eth_signature_uint256( - msg_hash=msg_hash, - r=r, - s=s, - v=y_parity, - eth_address=address, - helpers_class=helpers_class, - ); - return (); - } } diff --git a/src/kakarot/accounts/model.cairo b/src/kakarot/accounts/model.cairo index 02592cb59..ba21f937b 100644 --- a/src/kakarot/accounts/model.cairo +++ b/src/kakarot/accounts/model.cairo @@ -12,3 +12,10 @@ struct CallArray { data_offset: felt, data_len: felt, } + +struct OutsideExecution { + caller: felt, + nonce: felt, + execute_after: felt, + execute_before: felt, +} diff --git a/src/kakarot/interpreter.cairo b/src/kakarot/interpreter.cairo index b2910f662..638648ebd 100644 --- a/src/kakarot/interpreter.cairo +++ b/src/kakarot/interpreter.cairo @@ -6,14 +6,13 @@ from starkware.cairo.common.alloc import alloc 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_le, is_not_zero, is_nn +from starkware.cairo.common.math_cmp import is_le, is_not_zero, is_nn, is_le_felt from starkware.cairo.common.math import split_felt from starkware.cairo.common.default_dict import default_dict_new from starkware.cairo.common.dict import DictAccess -from starkware.cairo.lang.compiler.lib.registers import get_fp_and_pc +from starkware.cairo.lang.compiler.lib.registers import get_fp_and_pc, get_ap from starkware.cairo.common.uint256 import Uint256, uint256_le from starkware.cairo.common.math import unsigned_div_rem -from starkware.starknet.common.syscalls import get_tx_info // Internal dependencies from kakarot.account import Account @@ -847,7 +846,6 @@ namespace Interpreter { let valid_jumpdests_start = cast([ap - 2], DictAccess*); let valid_jumpdests = cast([ap - 1], DictAccess*); - tempvar authorized = new model.Option(is_some=0, value=0); tempvar message = new model.Message( bytecode=bytecode, bytecode_len=bytecode_len, @@ -870,44 +868,45 @@ namespace Interpreter { let memory = Memory.init(); let state = State.init(); + // Cache the coinbase, precompiles, caller, and target, making them warm with state { - // Cache the coinbase, precompiles, caller, and target, making them warm let coinbase = State.get_account(env.coinbase); - local coinbase_address: model.Address* = coinbase.address; State.cache_precompiles(); State.get_account(address.evm); let access_list_cost = State.cache_access_list(access_list_len, access_list); - let sender = State.get_account(env.origin); - local sender_address: model.Address* = sender.address; + } - // TODO: missing overflow checks on gas operations and values - let intrinsic_gas = intrinsic_gas + access_list_cost; - let evm = EVM.init(message, gas_limit - intrinsic_gas); + let intrinsic_gas = intrinsic_gas + access_list_cost; + let evm = EVM.init(message, gas_limit - intrinsic_gas); - let is_gas_limit_enough = is_le(intrinsic_gas, gas_limit); - if (is_gas_limit_enough == FALSE) { - let evm = EVM.halt_validation_failed(evm); - State.finalize(); - return (evm, stack, memory, state, 0, 0); - } + let is_gas_limit_enough = is_le_felt(intrinsic_gas, gas_limit); + if (is_gas_limit_enough == FALSE) { + let evm = EVM.halt_validation_failed(evm); + State.finalize{state=state}(); + return (evm, stack, memory, state, 0, 0); + } - tempvar is_initcode_invalid = is_deploy_tx * is_le( - 2 * Constants.MAX_CODE_SIZE + 1, bytecode_len - ); - if (is_initcode_invalid != FALSE) { - let evm = EVM.halt_validation_failed(evm); - State.finalize(); - return (evm, stack, memory, state, 0, 0); - } + tempvar is_initcode_invalid = is_deploy_tx * is_le( + 2 * Constants.MAX_CODE_SIZE + 1, bytecode_len + ); + if (is_initcode_invalid != FALSE) { + let evm = EVM.halt_validation_failed(evm); + State.finalize{state=state}(); + return (evm, stack, memory, state, 0, 0); + } + + // 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() + 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); - // 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() - let effective_gas_fee = gas_limit * env.gas_price; - let (fee_high, fee_low) = split_felt(effective_gas_fee); - let max_fee_u256 = Uint256(low=fee_low, high=fee_high); + with state { + let sender = State.get_account(env.origin); let (local new_balance) = uint256_sub([sender.balance], max_fee_u256); let sender = Account.set_balance(sender, &new_balance); + let sender = Account.set_nonce(sender, sender.nonce + 1); State.update_account(sender); let transfer = model.Transfer(sender.address, address, [value]); @@ -919,7 +918,7 @@ namespace Interpreter { let is_collision = code_or_nonce * is_deploy_tx; // Nonce is set to 1 in case of deploy_tx and account is marked as created let nonce = account.nonce * (1 - is_deploy_tx) + is_deploy_tx; - let account = Account.set_nonce(account, nonce); // Increase the nonce of the sender of the tx + let account = Account.set_nonce(account, nonce); let account = Account.set_created(account, is_deploy_tx); State.update_account(account); } @@ -951,44 +950,35 @@ namespace Interpreter { let total_gas_used = required_gas - gas_refund; - with state { - // Reset the state if the execution has failed. - // Only the gas fee paid will be committed. - if (evm.reverted != 0) { - let state = State.init(); - - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - tempvar state = state; - } else { - // Because we only "cached" for the local execution the sender's balance minus the maximum - // fee paid for the transaction, we need to restore the sender's balance to its original - // value to charge the actual fee proportional to the gas used. - let sender = State.get_account(env.origin); - let (local balance_pre_fee, _) = uint256_add([sender.balance], max_fee_u256); - let sender = Account.set_balance(sender, &balance_pre_fee); - State.update_account(sender); - tempvar syscall_ptr = syscall_ptr; - tempvar pedersen_ptr = pedersen_ptr; - tempvar range_check_ptr = range_check_ptr; - tempvar state = state; - } - let syscall_ptr = cast([ap - 4], felt*); - let pedersen_ptr = cast([ap - 3], HashBuiltin*); - let range_check_ptr = [ap - 2]; - let state = cast([ap - 1], model.State*); + // Reset the state if the execution has failed. + // Only the gas fee paid will be committed. + State.finalize{state=state}(); + if (evm.reverted != 0) { + tempvar state = State.init(); + } else { + tempvar state = state; + } + let is_reverted = is_not_zero(evm.reverted); + let success = 1 - is_reverted; + let paid_fee_u256 = Uint256(max_fee_u256.low * success, max_fee_u256.high * success); - // So as to not burn the base_fee_per gas, we send it to the coinbase. - let total_fee_charged = total_gas_used * env.gas_price; - let (fee_high, fee_low) = split_felt(total_fee_charged); - let fee_u256 = Uint256(low=fee_low, high=fee_high); + with state { + let sender = State.get_account(env.origin); + uint256_add([sender.balance], paid_fee_u256); + let (ap_val) = get_ap(); + let sender = Account.set_balance(sender, cast(ap_val - 3, Uint256*)); + let sender = Account.set_nonce(sender, sender.nonce + is_reverted); + State.update_account(sender); + } - let transfer = model.Transfer(sender_address, coinbase.address, fee_u256); - // This should always succeed as we ensured the user has enough balance for value + gas_price * gas_limit - let success = State.add_transfer(transfer); + // So as to not burn the base_fee_per gas, we send it to the coinbase. + let actual_fee = total_gas_used * env.gas_price; + let (fee_high, fee_low) = split_felt(actual_fee); + let actual_fee_u256 = Uint256(low=fee_low, high=fee_high); + let transfer = model.Transfer(sender.address, coinbase.address, actual_fee_u256); - // State must be finalized as we added entries with the latest transfer + with state { + State.add_transfer(transfer); State.finalize(); } diff --git a/src/kakarot/kakarot.cairo b/src/kakarot/kakarot.cairo index 0514ccca0..9822cd394 100644 --- a/src/kakarot/kakarot.cairo +++ b/src/kakarot/kakarot.cairo @@ -8,7 +8,7 @@ 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.uint256 import Uint256 -from starkware.starknet.common.syscalls import get_caller_address, replace_class, get_tx_info +from starkware.starknet.common.syscalls import get_caller_address, replace_class from starkware.cairo.common.registers import get_fp_and_pc from openzeppelin.access.ownable.library import Ownable, Ownable_owner @@ -439,11 +439,10 @@ func eth_send_transaction{ local __fp__: felt* = fp_and_pc.fp_val; let (local starknet_caller_address) = get_caller_address(); let (local origin) = Kakarot.safe_get_evm_address(starknet_caller_address); - - let (tx_info) = get_tx_info(); + let (local nonce) = IAccount.get_nonce(starknet_caller_address); let (evm, state, gas_used, _) = Kakarot.eth_call( - tx_info.nonce, + nonce, origin, to, gas_limit, @@ -461,10 +460,6 @@ func eth_send_transaction{ let is_reverted = is_not_zero(evm.reverted); let result = (evm.return_data_len, evm.return_data, 1 - is_reverted, gas_used); - if (evm.reverted != FALSE) { - return result; - } - return result; } diff --git a/tests/end_to_end/L1L2Messaging/test_messaging.py b/tests/end_to_end/L1L2Messaging/test_messaging.py index 038135ab1..2ee8772fd 100644 --- a/tests/end_to_end/L1L2Messaging/test_messaging.py +++ b/tests/end_to_end/L1L2Messaging/test_messaging.py @@ -2,6 +2,7 @@ import time import pytest +import pytest_asyncio from kakarot_scripts.utils.l1 import ( deploy_on_l1, @@ -14,7 +15,7 @@ @pytest.fixture(scope="session") -async def sn_messaging_local(): +def sn_messaging_local(): # If the contract is already deployed on the l1, we can get the address from the deployments file # Otherwise, we deploy it l1_addresses = get_l1_addresses() @@ -23,7 +24,7 @@ async def sn_messaging_local(): if l1_contract_exists(address): return get_l1_contract("starknet", "StarknetMessagingLocal", address) - contract = await deploy_on_l1( + contract = deploy_on_l1( "starknet", "StarknetMessagingLocal", ) @@ -32,13 +33,13 @@ async def sn_messaging_local(): return contract -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def l1_kakarot_messaging(sn_messaging_local, invoke): # If the contract is already deployed on the l1, we can get the address from the deployments file # Otherwise, we deploy it l1_addresses = get_l1_addresses() kakarot_address = get_deployments()["kakarot"]["address"] - contract = await deploy_on_l1( + contract = deploy_on_l1( "L1L2Messaging", "L1KakarotMessaging", starknetMessaging_=sn_messaging_local.address, @@ -56,7 +57,7 @@ async def l1_kakarot_messaging(sn_messaging_local, invoke): return contract -@pytest.fixture(scope="session") +@pytest_asyncio.fixture(scope="session") async def message_app_l2(deploy_contract, owner): message_sender = await deploy_contract( "L1L2Messaging", @@ -67,9 +68,9 @@ async def message_app_l2(deploy_contract, owner): @pytest.fixture(scope="session") -async def message_app_l1(sn_messaging_local, l1_kakarot_messaging): +def message_app_l1(sn_messaging_local, l1_kakarot_messaging): kakarot_address = get_deployments()["kakarot"]["address"] - return await deploy_on_l1( + return deploy_on_l1( "L1L2Messaging", "MessageAppL1", starknetMessaging=sn_messaging_local.address, @@ -98,29 +99,27 @@ async def _factory(): class TestL2ToL1Messages: @pytest.mark.slow async def test_should_increment_counter_on_l1( - self, sn_messaging_local, message_app_l1, message_app_l2, wait_for_message + self, message_app_l1, message_app_l2, wait_for_message ): - msg_counter_before = await message_app_l1.receivedMessagesCounter() + msg_counter_before = message_app_l1.receivedMessagesCounter() increment_value = 8 await message_app_l2.increaseL1AppCounter( message_app_l1.address, increment_value ) await wait_for_message() message_payload = increment_value.to_bytes(32, "big") - await message_app_l1.consumeCounterIncrease(message_payload) - msg_counter_after = await message_app_l1.receivedMessagesCounter() + message_app_l1.consumeCounterIncrease(message_payload) + msg_counter_after = message_app_l1.receivedMessagesCounter() assert msg_counter_after == msg_counter_before + increment_value @pytest.mark.asyncio(scope="module") class TestL1ToL2Messages: @pytest.mark.slow - async def test_should_increment_counter_on_l2( - self, l1_kakarot_messaging, message_app_l1, message_app_l2 - ): + async def test_should_increment_counter_on_l2(self, message_app_l1, message_app_l2): msg_counter_before = await message_app_l2.receivedMessagesCounter() increment_value = 1 - await message_app_l1.increaseL2AppCounter( + message_app_l1.increaseL2AppCounter( message_app_l2.address, value=increment_value ) time.sleep(4) @@ -138,7 +137,7 @@ async def test_should_fail_unauthorized_message_sender( int(l1_kakarot_messaging.address, 16), False, ) - await message_app_l1.increaseL2AppCounter(message_app_l2.address, value=1) + message_app_l1.increaseL2AppCounter(message_app_l2.address, value=1) time.sleep(4) msg_counter_after = await message_app_l2.receivedMessagesCounter() assert msg_counter_after == msg_counter_before diff --git a/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py b/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py index 6cf19fd02..b4c9e17ad 100644 --- a/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py +++ b/tests/end_to_end/PlainOpcodes/test_plain_opcodes.py @@ -493,7 +493,7 @@ class TestKeccak: ], ) async def test_should_emit_keccak_hash(self, plain_opcodes, input_length): - input = os.urandom(input_length) - receipt = (await plain_opcodes.computeHash(input))["receipt"] + input_bytes = os.urandom(input_length) + receipt = (await plain_opcodes.computeHash(input_bytes))["receipt"] events = plain_opcodes.events.parse_events(receipt) - assert events["HashComputed"][0]["hash"] == keccak(input) + assert events["HashComputed"][0]["hash"] == keccak(input_bytes) diff --git a/tests/end_to_end/PlainOpcodes/test_safe.py b/tests/end_to_end/PlainOpcodes/test_safe.py index 7a5842ffa..520b8a60c 100644 --- a/tests/end_to_end/PlainOpcodes/test_safe.py +++ b/tests/end_to_end/PlainOpcodes/test_safe.py @@ -5,6 +5,11 @@ from tests.utils.constants import ACCOUNT_BALANCE +@pytest_asyncio.fixture(scope="package") +async def owner(new_eoa): + return await new_eoa() + + @pytest_asyncio.fixture(scope="package") async def safe(deploy_contract, owner): return await deploy_contract( diff --git a/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py b/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py index 049cc65e9..23419c097 100644 --- a/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py +++ b/tests/end_to_end/UniswapV2/test_uniswap_v2_factory.py @@ -69,7 +69,7 @@ async def test_should_use_correct_gas(self, factory, owner): await factory.createPair( *TEST_ADDRESSES, caller_eoa=owner.starknet_contract, max_fee=0 ) - assert factory.tx.result.gas_used == 2512920 + assert factory.tx.result.gas_used == 2_512_920 class TestSetFeeTo: async def test_should_revert_when_caller_is_not_owner(self, factory, other): diff --git a/tests/end_to_end/conftest.py b/tests/end_to_end/conftest.py index 0458d140e..9080246e5 100644 --- a/tests/end_to_end/conftest.py +++ b/tests/end_to_end/conftest.py @@ -61,6 +61,27 @@ def starknet(): return RPC_CLIENT +@pytest.fixture(scope="session") +def new_eoa(max_fee) -> Wallet: + from kakarot_scripts.utils.kakarot import get_eoa + + seed = 0 + + async def _factory(): + + private_key = generate_random_private_key(seed) + return Wallet( + address=private_key.public_key.to_checksum_address(), + private_key=private_key, + # deploying an account with enough ETH to pass ~10 tx + starknet_contract=await get_eoa(private_key, amount=100 * max_fee / 1e18), + ) + + yield _factory + + seed += 1 + + @pytest_asyncio.fixture(scope="session") async def addresses(max_fee) -> List[Wallet]: """ @@ -97,7 +118,7 @@ async def owner(addresses, eth_balance_of): """ account = addresses[0] current_balance = await eth_balance_of(account.address) - if current_balance / 1e18 < 10: + if current_balance < 10e18: from kakarot_scripts.utils.starknet import fund_address await fund_address(account.starknet_contract.address, 10) diff --git a/tests/end_to_end/test_account.py b/tests/end_to_end/test_account.py deleted file mode 100644 index 0f1da920b..000000000 --- a/tests/end_to_end/test_account.py +++ /dev/null @@ -1,209 +0,0 @@ -from collections import namedtuple - -import pytest -import pytest_asyncio -from eth_utils import keccak -from starknet_py.net.full_node_client import FullNodeClient -from starkware.starknet.public.abi import get_storage_var_address - -Wallet = namedtuple("Wallet", ["address", "private_key", "starknet_contract"]) - -TOTAL_SUPPLY = 10000 * 10**18 -TEST_AMOUNT = 10 * 10**18 - - -@pytest.fixture(scope="session") -async def class_hashes(): - """ - All declared class hashes. - """ - from kakarot_scripts.utils.starknet import get_declarations - - return get_declarations() - - -@pytest_asyncio.fixture(scope="function") -async def counter(deploy_contract, new_account): - return await deploy_contract( - "PlainOpcodes", - "Counter", - caller_eoa=new_account.starknet_contract, - ) - - -@pytest_asyncio.fixture(scope="module") -async def caller(deploy_contract, owner): - return await deploy_contract( - "PlainOpcodes", - "Caller", - caller_eoa=owner.starknet_contract, - ) - - -@pytest.fixture(autouse=True) -async def cleanup(invoke, class_hashes): - yield - - await invoke( - "kakarot", - "set_account_contract_class_hash", - class_hashes["account_contract"], - ) - await invoke( - "kakarot", "set_cairo1_helpers_class_hash", class_hashes["Cairo1Helpers"] - ) - - -async def assert_counter_transaction_success(counter, new_account): - """ - Assert that the transaction sent, other than upgrading the account contract, is successful. - """ - prev_count = await counter.count() - await counter.inc(caller_eoa=new_account.starknet_contract) - assert await counter.count() == prev_count + 1 - - -async def assert_caller_contract_increases_counter(caller, counter, new_account): - """ - Assert that the transaction sent, other than upgrading the account contract, is successful. - """ - prev_count = await counter.count() - inc_selector = keccak(b"inc()")[0:4] - await caller.call( - counter.address, inc_selector, caller_eoa=new_account.starknet_contract - ) - assert await counter.count() == prev_count + 1 - - -@pytest.mark.asyncio(scope="session") -@pytest.mark.AccountContract -class TestAccount: - class TestAutoUpgradeOnTransaction: - async def test_should_upgrade_outdated_account_on_transfer( - self, - starknet: FullNodeClient, - invoke, - counter, - new_account, - class_hashes, - cleanup, - ): - prev_class = await starknet.get_class_hash_at( - new_account.starknet_contract.address - ) - target_class = class_hashes["account_contract_fixture"] - assert prev_class != target_class - assert prev_class == class_hashes["account_contract"] - - await invoke( - "kakarot", - "set_account_contract_class_hash", - target_class, - ) - - await assert_counter_transaction_success(counter, new_account) - - new_class = await starknet.get_class_hash_at( - new_account.starknet_contract.address - ) - assert new_class == target_class - - async def test_should_update_cairo1_helpers_class( - self, - starknet: FullNodeClient, - invoke, - counter, - new_account, - class_hashes, - ): - prev_cairo1_helpers_class = await starknet.get_storage_at( - new_account.starknet_contract.address, - get_storage_var_address("Account_cairo1_helpers_class_hash"), - ) - target_class = class_hashes["Cairo1HelpersFixture"] - assert prev_cairo1_helpers_class != target_class - - await invoke( - "kakarot", - "set_account_contract_class_hash", - class_hashes["account_contract_fixture"], - ) - await invoke("kakarot", "set_cairo1_helpers_class_hash", target_class) - - await assert_counter_transaction_success(counter, new_account) - - assert ( - await starknet.get_storage_at( - new_account.starknet_contract.address, - get_storage_var_address("Account_cairo1_helpers_class_hash"), - ) - == target_class - ) - - @pytest.mark.xfail(reason="Disabled") - class TestAutoUpgradeContracts: - async def test_should_upgrade_outdated_contract_transaction_target( - self, - starknet: FullNodeClient, - invoke, - call, - counter, - new_account, - class_hashes, - ): - counter_starknet_address = ( - await call( - "kakarot", - "get_starknet_address", - int(counter.address, 16), - ) - ).starknet_address - prev_class = await starknet.get_class_hash_at(counter_starknet_address) - target_class = class_hashes["account_contract_fixture"] - assert prev_class != target_class - assert prev_class == class_hashes["account_contract"] - - await invoke( - "kakarot", - "set_account_contract_class_hash", - target_class, - ) - - await assert_counter_transaction_success(counter, new_account) - - new_class = await starknet.get_class_hash_at(counter_starknet_address) - assert new_class == target_class - - async def test_should_upgrade_outdated_contract_called_contract( - self, - starknet: FullNodeClient, - invoke, - counter, - call, - caller, - new_account, - class_hashes, - cleanup, - ): - counter_starknet_address = ( - await call( - "kakarot", - "get_starknet_address", - int(counter.address, 16), - ) - ).starknet_address - prev_class = await starknet.get_class_hash_at(counter_starknet_address) - target_class = class_hashes["account_contract_fixture"] - assert prev_class != target_class - assert prev_class == class_hashes["account_contract"] - - await invoke( - "kakarot", - "set_account_contract_class_hash", - target_class, - ) - - await assert_caller_contract_increases_counter(caller, counter, new_account) - - new_class = await starknet.get_class_hash_at(counter_starknet_address) - assert new_class == target_class diff --git a/tests/end_to_end/test_kakarot.py b/tests/end_to_end/test_kakarot.py index 6ee882b22..a9130ef77 100644 --- a/tests/end_to_end/test_kakarot.py +++ b/tests/end_to_end/test_kakarot.py @@ -200,6 +200,7 @@ async def test_should_fail_when_account_is_already_registered( starknet: FullNodeClient, deploy_externally_owned_account, register_account, + random_seed, ): evm_address = generate_random_evm_address(random_seed) await deploy_externally_owned_account(evm_address) diff --git a/tests/fixtures/starknet.py b/tests/fixtures/starknet.py index 385741613..86e5fa29d 100644 --- a/tests/fixtures/starknet.py +++ b/tests/fixtures/starknet.py @@ -23,6 +23,7 @@ from starkware.cairo.lang.vm.utils import RunResources from starkware.starknet.compiler.starknet_pass_manager import starknet_pass_manager +from tests.utils.constants import Opcodes from tests.utils.coverage import VmWithCoverage, report_runs from tests.utils.hints import debug_info from tests.utils.reporting import dump_coverage, profile_from_tracer_data @@ -176,7 +177,11 @@ def _factory(entrypoint, **kwargs) -> list: "program_input": kwargs, "syscall_handler": SyscallHandler(), }, - static_locals={"debug_info": debug_info(program)}, + static_locals={ + "debug_info": debug_info(program), + "serde": serde, + "Opcodes": Opcodes, + }, vm_class=VmWithCoverage, ) run_resources = RunResources(n_steps=10_000_000) diff --git a/tests/src/kakarot/accounts/test_account_contract.cairo b/tests/src/kakarot/accounts/test_account_contract.cairo index 6a12def6f..3223b4be3 100644 --- a/tests/src/kakarot/accounts/test_account_contract.cairo +++ b/tests/src/kakarot/accounts/test_account_contract.cairo @@ -129,35 +129,30 @@ func test__execute_starknet_call{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, return (retdata, success); } -func test__validate{ +func test__execute_from_outside{ syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, bitwise_ptr: BitwiseBuiltin*, range_check_ptr -}() { +}() -> (felt, felt*) { // Given - tempvar address: felt; - tempvar nonce: felt; - tempvar chain_id: felt; - tempvar r: Uint256; - tempvar s: Uint256; - tempvar v: felt; tempvar tx_data_len: felt; let (tx_data) = alloc(); + tempvar signature_len: felt; + let (signature) = alloc(); + tempvar chain_id: felt; + %{ - ids.address = program_input["address"] - ids.nonce = program_input["nonce"] - ids.chain_id = program_input["chain_id"] - ids.r.low = program_input["r"][0] - ids.r.high = program_input["r"][1] - ids.s.low = program_input["s"][0] - ids.s.high = program_input["s"][1] - ids.v = program_input["v"] ids.tx_data_len = len(program_input["tx_data"]) segments.write_arg(ids.tx_data, program_input["tx_data"]) + ids.signature_len = len(program_input["signature"]) + segments.write_arg(ids.signature, program_input["signature"]) + ids.chain_id = program_input["chain_id"] %} // When - AccountInternals.validate(address, nonce, chain_id, r, s, v, tx_data_len, tx_data); + let (return_data_len, return_data) = AccountContract.execute_from_outside( + tx_data_len, tx_data, signature_len, signature, chain_id + ); - return (); + return (return_data_len, return_data); } func test__write_jumpdests{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}() { diff --git a/tests/src/kakarot/accounts/test_account_contract.py b/tests/src/kakarot/accounts/test_account_contract.py index fbff17465..33f618d0a 100644 --- a/tests/src/kakarot/accounts/test_account_contract.py +++ b/tests/src/kakarot/accounts/test_account_contract.py @@ -12,13 +12,9 @@ ) from kakarot_scripts.constants import ARACHNID_PROXY_DEPLOYER, ARACHNID_PROXY_SIGNED_TX -from tests.utils.constants import CAIRO1_HELPERS_CLASS_HASH, CHAIN_ID, TRANSACTIONS +from tests.utils.constants import CHAIN_ID, TRANSACTION_GAS_LIMIT, TRANSACTIONS from tests.utils.errors import cairo_error -from tests.utils.helpers import ( - generate_random_evm_address, - generate_random_private_key, - rlp_encode_signed_data, -) +from tests.utils.helpers import generate_random_private_key, rlp_encode_signed_data from tests.utils.syscall_handler import SyscallHandler from tests.utils.uint256 import int_to_uint256 @@ -45,10 +41,6 @@ class TestInitialize: @SyscallHandler.patch("IKakarot.register_account", lambda addr, data: []) @SyscallHandler.patch("IKakarot.get_native_token", lambda addr, data: [0xDEAD]) @SyscallHandler.patch("IERC20.approve", lambda addr, data: [1]) - @SyscallHandler.patch( - "IKakarot.get_cairo1_helpers_class_hash", - lambda addr, data: [CAIRO1_HELPERS_CLASS_HASH], - ) def test_should_set_storage_variables(self, cairo_run): cairo_run( "test__initialize", @@ -69,10 +61,6 @@ def test_should_set_storage_variables(self, cairo_run): SyscallHandler.mock_storage.assert_any_call( address=get_storage_var_address("Account_implementation"), value=0xC1A55 ) - SyscallHandler.mock_storage.assert_any_call( - address=get_storage_var_address("Account_cairo1_helpers_class_hash"), - value=CAIRO1_HELPERS_CLASS_HASH, - ) SyscallHandler.mock_event.assert_any_call( keys=[get_selector_from_name("OwnershipTransferred")], data=[0, 0x1234] ) @@ -351,176 +339,431 @@ def test_should_execute_starknet_call(self, cairo_run): calldata=calldata, ) - class TestValidate: - @pytest.mark.parametrize("seed", (41, 42)) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - @SyscallHandler.patch( - "Account_cairo1_helpers_class_hash", CAIRO1_HELPERS_CLASS_HASH - ) - def test_should_pass_all_transactions_types(self, cairo_run, seed, transaction): - """ - Note: the seeds 41 and 42 have been manually selected after observing that some private keys - were making the Counter deploy transaction failing because their signature parameters length (s and v) - were not 32 bytes. - """ - random.seed(seed) - private_key = generate_random_private_key() - address = int(private_key.public_key.to_checksum_address(), 16) - signed = Account.sign_transaction(transaction, private_key) + class TestExecuteFromOutside: + def test_should_raise_with_incorrect_signature_length(self, cairo_run): + with cairo_error(message="Incorrect signature length"): + cairo_run( + "test__execute_from_outside", + tx_data=[], + signature=list(range(4)), + chain_id=CHAIN_ID, + ) - encoded_unsigned_tx = rlp_encode_signed_data(transaction) + def test_should_raise_with_wrong_signature(self, cairo_run): + with cairo_error(message="Invalid signature."): + cairo_run( + "test__execute_from_outside", + tx_data=[1], + signature=list(range(5)), + chain_id=CHAIN_ID, + ) + + @SyscallHandler.patch("Account_evm_address", int(ARACHNID_PROXY_DEPLOYER, 16)) + def test_should_raise_unauthorized_pre_eip155_tx(self, cairo_run): + rlp_decoded = rlp.decode(ARACHNID_PROXY_SIGNED_TX) + v, r, s = rlp_decoded[-3:] + signature = [ + *int_to_uint256(int.from_bytes(r, "big")), + *int_to_uint256(int.from_bytes(s, "big")), + int.from_bytes(v, "big"), + ] + unsigned_tx_data = rlp_decoded[:-3] + encoded_unsigned_tx = rlp.encode(unsigned_tx_data) tx_data = list(encoded_unsigned_tx) - cairo_run( - "test__validate", - address=address, - nonce=transaction["nonce"], - chain_id=transaction.get("chainId") or CHAIN_ID, - r=int_to_uint256(signed.r), - s=int_to_uint256(signed.s), - v=signed["v"], - tx_data=tx_data, - ) + with cairo_error(message="Unauthorized pre-eip155 transaction"): + cairo_run( + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, + chain_id=CHAIN_ID, + ) - @SyscallHandler.patch( - "Account_cairo1_helpers_class_hash", CAIRO1_HELPERS_CLASS_HASH - ) - def test_should_pass_all_data_len(self, cairo_run, bytecode): + def test_should_raise_invalid_signature_for_invalid_chain_id_when_tx_type0_not_pre_eip155( + self, cairo_run + ): transaction = { "to": "0xF0109fC8DF283027b6285cc889F5aA624EaC1F55", - "value": 0, + "value": 1_000_000_000, "gas": 2_000_000, "gasPrice": 234567897654321, "nonce": 0, "chainId": CHAIN_ID, - "data": bytecode, + "data": b"", } encoded_unsigned_tx = rlp_encode_signed_data(transaction) tx_data = list(encoded_unsigned_tx) 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, + ] - cairo_run( - "test__validate", - address=address, - nonce=transaction["nonce"], - chain_id=transaction.get("chainId") or CHAIN_ID, - r=int_to_uint256(signed.r), - s=int_to_uint256(signed.s), - v=signed["v"], - tx_data=tx_data, - ) + with cairo_error(message="Invalid signature."), SyscallHandler.patch( + "Account_evm_address", address + ): + cairo_run( + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, + chain_id=CHAIN_ID + 1, + ) - @pytest.mark.parametrize("transaction", TRANSACTIONS) - async def test_should_raise_with_wrong_signed_chain_id( - self, cairo_run, transaction + 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, + } + encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) 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 cairo_error(message="Invalid chain id"), SyscallHandler.patch( + "Account_evm_address", address + ): + 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, + ] encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) - with cairo_error(message="Invalid chain id"): + with cairo_error(message="Invalid nonce"), SyscallHandler.patch( + "Account_evm_address", address + ): cairo_run( - "test__validate", - address=address, - nonce=transaction["nonce"], - chain_id=transaction["chainId"] + 1, - r=int_to_uint256(signed.r), - s=int_to_uint256(signed.s), - v=signed["v"], - tx_data=list(encoded_unsigned_tx), + "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) - async def test_should_raise_with_wrong_address(self, cairo_run, transaction): + def test_raise_not_enough_ETH_balance(self, cairo_run, transaction): private_key = generate_random_private_key() - address = int(generate_random_evm_address(), 16) + 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, + ] encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) - assert address != int(private_key.public_key.to_address(), 16) - with cairo_error("Invalid signature."): + with cairo_error( + message="Not enough ETH to pay msg.value + max gas fees" + ), SyscallHandler.patch( + "Account_evm_address", address + ), SyscallHandler.patch( + "Account_nonce", transaction.get("nonce", 0) + ): cairo_run( - "test__validate", - address=address, - nonce=transaction["nonce"], + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, chain_id=transaction.get("chainId") or CHAIN_ID, - r=int_to_uint256(signed.r), - s=int_to_uint256(signed.s), - v=signed["v"], - tx_data=list(encoded_unsigned_tx), ) + @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) - async def test_should_raise_with_wrong_nonce(self, cairo_run, transaction): + 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, + ] + encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) + with cairo_error( + message="Transaction gas_limit > Block gas_limit" + ), SyscallHandler.patch( + "Account_evm_address", address + ), SyscallHandler.patch( + "Account_nonce", transaction.get("nonce", 0) + ): + 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, + ] encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) - with cairo_error(message="Invalid nonce"): + with cairo_error(message="Max fee per gas too low"), SyscallHandler.patch( + "Account_evm_address", address + ), SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)): cairo_run( - "test__validate", - address=address, - nonce=transaction["nonce"] + 1, + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, chain_id=transaction.get("chainId") or CHAIN_ID, - r=int_to_uint256(signed.r), - s=int_to_uint256(signed.s), - v=signed["v"], - tx_data=list(encoded_unsigned_tx), ) - async def test_should_fail_unauthorized_pre_eip155_tx(self, cairo_run): - rlp_decoded = rlp.decode(ARACHNID_PROXY_SIGNED_TX) - v, r, s = rlp_decoded[-3:] - unsigned_tx_data = rlp_decoded[:-3] - unsigned_encoded_tx = rlp.encode(unsigned_tx_data) + @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]) + def test_raise_max_priority_fee_too_high(self, cairo_run): + transaction = { + "type": 2, + "gas": 100_000, + "maxFeePerGas": 2_000_000_000, + "maxPriorityFeePerGas": 3_000_000_000, + "data": "0x616263646566", + "nonce": 34, + "to": "0x09616C3d61b3331fc4109a9E41a8BDB7d9776609", + "value": 0x5AF3107A4000, + "accessList": [], + "chainId": CHAIN_ID, + } + encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) + 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 cairo_error(message="Unauthorized pre-eip155 transaction"): + with cairo_error( + message="Max priority fee greater than max fee per gas" + ), SyscallHandler.patch( + "Account_evm_address", address + ), SyscallHandler.patch( + "Account_nonce", transaction["nonce"] + ): cairo_run( - "test__validate", - address=int(ARACHNID_PROXY_DEPLOYER, 16), - nonce=0, - chain_id=CHAIN_ID, - r=int_to_uint256(int.from_bytes(r, "big")), - s=int_to_uint256(int.from_bytes(s, "big")), - v=int.from_bytes(v, "big"), - tx_data=list(unsigned_encoded_tx), + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, + chain_id=transaction["chainId"], ) - async def test_should_validate_authorized_pre_eip155_tx(self, cairo_run): + @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 + ) + def test_pass_authorized_pre_eip155_transaction(self, cairo_run): rlp_decoded = rlp.decode(ARACHNID_PROXY_SIGNED_TX) v, r, s = rlp_decoded[-3:] + signature = [ + *int_to_uint256(int.from_bytes(r, "big")), + *int_to_uint256(int.from_bytes(s, "big")), + int.from_bytes(v, "big"), + ] unsigned_tx_data = rlp_decoded[:-3] - unsigned_encoded_tx = rlp.encode(unsigned_tx_data) + encoded_unsigned_tx = rlp.encode(unsigned_tx_data) + tx_data = list(encoded_unsigned_tx) tx_hash_low, tx_hash_high = int_to_uint256( - int.from_bytes(keccak(unsigned_encoded_tx), "big") + int.from_bytes(keccak(encoded_unsigned_tx), "big") ) with SyscallHandler.patch( + "Account_evm_address", int(ARACHNID_PROXY_DEPLOYER, 16) + ), SyscallHandler.patch( "Account_authorized_message_hashes", tx_hash_low, tx_hash_high, 0x1, + ), SyscallHandler.patch( + "Account_nonce", 0 ): - cairo_run( - "test__validate", - address=int(ARACHNID_PROXY_DEPLOYER, 16), - nonce=0, + output_len, output = cairo_run( + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, chain_id=CHAIN_ID, - r=int_to_uint256(int.from_bytes(r, "big")), - s=int_to_uint256(int.from_bytes(s, "big")), - v=int.from_bytes(v, "big"), - tx_data=list(unsigned_encoded_tx), ) - SyscallHandler.mock_storage.assert_any_call( - address=get_storage_var_address( - "Account_authorized_message_hashes", tx_hash_low, tx_hash_high + SyscallHandler.mock_event.assert_any_call( + keys=[get_selector_from_name("transaction_executed")], + data=[1, 0x68656C6C6F, 1, 1], + ) + + 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( + "IKakarot.eth_send_transaction", + lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + ) + @pytest.mark.parametrize("seed", (41, 42)) + @pytest.mark.parametrize("transaction", TRANSACTIONS) + def test_pass_all_transactions_types(self, cairo_run, seed, transaction): + """ + Note: the seeds 41 and 42 have been manually selected after observing that some private keys + were making the Counter deploy transaction failing because their signature parameters length (s and v) + were not 32 bytes. + """ + random.seed(seed) + 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, + ] + encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) + + with SyscallHandler.patch( + "Account_evm_address", address + ), SyscallHandler.patch("Account_nonce", transaction.get("nonce", 0)): + output_len, output = cairo_run( + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, + chain_id=transaction.get("chainId") or CHAIN_ID, + ) + + SyscallHandler.mock_event.assert_any_call( + keys=[get_selector_from_name("transaction_executed")], + data=[1, 0x68656C6C6F, 1, 1], + ) + + 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( + "IKakarot.eth_send_transaction", + lambda addr, data: [1, 0x68656C6C6F, 1, 1], # hello + ) + def test_should_pass_all_data_len(self, cairo_run, bytecode): + transaction = { + "to": "0xF0109fC8DF283027b6285cc889F5aA624EaC1F55", + "value": 0, + "gas": 2_000_000, + "gasPrice": 234567897654321, + "nonce": 0, + "chainId": CHAIN_ID, + "data": bytecode, + } + encoded_unsigned_tx = rlp_encode_signed_data(transaction) + tx_data = list(encoded_unsigned_tx) + + 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.get("nonce", 0)): + output_len, output = cairo_run( + "test__execute_from_outside", + tx_data=tx_data, + signature=signature, + chain_id=transaction.get("chainId") or CHAIN_ID, ) + + SyscallHandler.mock_event.assert_any_call( + keys=[get_selector_from_name("transaction_executed")], + data=[1, 0x68656C6C6F, 1, 1], ) + + assert output_len == 1 + assert output[0] == 0x68656C6C6F diff --git a/tests/src/kakarot/test_kakarot.cairo b/tests/src/kakarot/test_kakarot.cairo index 0b7d38bae..6d5b455ca 100644 --- a/tests/src/kakarot/test_kakarot.cairo +++ b/tests/src/kakarot/test_kakarot.cairo @@ -22,7 +22,7 @@ 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) { +}() -> (model.EVM*, model.State*, felt, felt) { // Given tempvar origin; @@ -42,7 +42,7 @@ func eth_call{ 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", 0) + ids.to.value = program_input.get("to") or 0 ids.gas_limit = program_input.get("gas_limit", int(2**63 - 1)) ids.gas_price = program_input.get("gas_price", 0) ids.nonce = program_input.get("nonce", 0) @@ -53,7 +53,7 @@ func eth_call{ ids.access_list_len = 0 %} - let (evm, state, gas_used, _) = Kakarot.eth_call( + let (evm, state, gas_used, required_gas) = Kakarot.eth_call( nonce=nonce, origin=origin, to=to, @@ -66,7 +66,7 @@ func eth_call{ access_list=access_list, ); - return (evm, state, gas_used); + return (evm, state, gas_used, required_gas); } func compute_starknet_address{syscall_ptr: felt*, pedersen_ptr: HashBuiltin*, range_check_ptr}( diff --git a/tests/src/kakarot/test_kakarot.py b/tests/src/kakarot/test_kakarot.py index 1348eb69e..8b02d5f4b 100644 --- a/tests/src/kakarot/test_kakarot.py +++ b/tests/src/kakarot/test_kakarot.py @@ -38,7 +38,7 @@ def _wrapper(self, *args, **kwargs): data = self.get_function_by_name(fun)( *args, **kwargs )._encode_transaction_data() - evm, state, gas = cairo_run( + evm, state, gas, _ = cairo_run( "eth_call", origin=origin, to=CONTRACT_ADDRESS, @@ -321,23 +321,23 @@ def test_erc721_transfer(self, get_contract): ) assert not evm["reverted"] + @pytest.mark.slow @pytest.mark.NoCI @pytest.mark.EFTests @pytest.mark.parametrize( - "ef_blockchain_test", EF_TESTS_PARSED_DIR.glob("*.json") + "ef_blockchain_test", + EF_TESTS_PARSED_DIR.glob("*walletConstruction_d0g1v0_Cancun*.json"), ) async def test_case( self, cairo_run, ef_blockchain_test, ): - test_case = json.loads( - (EF_TESTS_PARSED_DIR / ef_blockchain_test).read_text() - ) + test_case = json.loads(ef_blockchain_test.read_text()) block = test_case["blocks"][0] + tx = block["transactions"][0] with SyscallHandler.patch_state(parse_state(test_case["pre"])): - tx = block["transactions"][0] - evm, state, gas_used = cairo_run( + evm, state, gas_used, required_gas = cairo_run( "eth_call", origin=int(tx["sender"], 16), to=int(tx.get("to"), 16) if tx.get("to") else None, @@ -345,6 +345,7 @@ async def test_case( gas_price=int(tx["gasPrice"], 16), value=int(tx["value"], 16), data=tx["data"], + nonce=int(tx["nonce"], 16), ) parsed_state = { diff --git a/tests/src/kakarot/test_memory.cairo b/tests/src/kakarot/test_memory.cairo index 9e13371e1..79d7b6084 100644 --- a/tests/src/kakarot/test_memory.cairo +++ b/tests/src/kakarot/test_memory.cairo @@ -14,7 +14,7 @@ func test__init__should_return_an_empty_memory() { return (); } -func test__store__should_add_an_element_to_the_memory{range_check_ptr}() -> model.Memory* { +func test__store__should_add_an_element_to_the_memory{range_check_ptr}() { alloc_locals; // Given let memory = Memory.init(); @@ -23,10 +23,13 @@ func test__store__should_add_an_element_to_the_memory{range_check_ptr}() -> mode // When with memory { Memory.store(value, 0); + let stored = Memory.load(0); } + assert_uint256_eq(value, stored); + // Then - return memory; + return (); } func test__load__should_load_an_element_from_the_memory_with_offset{range_check_ptr}() { diff --git a/tests/src/kakarot/test_memory.py b/tests/src/kakarot/test_memory.py index 31c968134..ff87fe86f 100644 --- a/tests/src/kakarot/test_memory.py +++ b/tests/src/kakarot/test_memory.py @@ -8,8 +8,7 @@ def test_should_return_an_empty_memory(self, cairo_run): class TestStore: def test_should_add_an_element_to_the_memory(self, cairo_run): - memory = cairo_run("test__store__should_add_an_element_to_the_memory") - assert memory == f"{1:064x}" + cairo_run("test__store__should_add_an_element_to_the_memory") class TestLoad: @pytest.mark.parametrize( diff --git a/tests/utils/serde.py b/tests/utils/serde.py index a0eee967d..3556a813a 100644 --- a/tests/utils/serde.py +++ b/tests/utils/serde.py @@ -56,8 +56,9 @@ def serialize_list(self, segment_ptr, item_scope=None, list_len=None): break return output - def serialize_dict(self, dict_ptr, value_scope=None): - dict_size = self.runner.segments.get_segment_size(dict_ptr.segment_index) + def serialize_dict(self, dict_ptr, value_scope=None, dict_size=None): + if dict_size is None: + dict_size = self.runner.segments.get_segment_size(dict_ptr.segment_index) output = {} value_scope = ( self.get_identifier(value_scope, StructDefinition).full_name @@ -194,14 +195,19 @@ def serialize_evm(self, ptr): def serialize_stack(self, ptr): raw = self.serialize_pointers("model.Stack", ptr) - stack_dict = self.serialize_dict(raw["dict_ptr_start"], "Uint256") + stack_dict = self.serialize_dict( + raw["dict_ptr_start"], "Uint256", raw["dict_ptr"] - raw["dict_ptr_start"] + ) return [stack_dict[i] for i in range(raw["size"])] def serialize_memory(self, ptr): raw = self.serialize_pointers("model.Memory", ptr) - memory_dict = self.serialize_dict(raw["word_dict_start"]) - items_count = len(memory_dict.items()) - return "".join([f"{memory_dict.get(i, 0):032x}" for i in range(items_count)]) + memory_dict = self.serialize_dict( + raw["word_dict_start"], dict_size=raw["word_dict"] - raw["word_dict_start"] + ) + return "".join( + [f"{memory_dict.get(i, 0):032x}" for i in range(raw["words_len"] * 2)] + ) def serialize_scope(self, scope, scope_ptr): if scope.path[-1] == "State": diff --git a/tests/utils/syscall_handler.py b/tests/utils/syscall_handler.py index 8190206b3..9cd505be7 100644 --- a/tests/utils/syscall_handler.py +++ b/tests/utils/syscall_handler.py @@ -16,7 +16,11 @@ get_storage_var_address, ) -from tests.utils.constants import ACCOUNT_CLASS_IMPLEMENTATION, CHAIN_ID +from tests.utils.constants import ( + ACCOUNT_CLASS_IMPLEMENTATION, + CAIRO1_HELPERS_CLASS_HASH, + CHAIN_ID, +) from tests.utils.uint256 import int_to_uint256, uint256_to_int @@ -179,6 +183,9 @@ class SyscallHandler: ACCOUNT_CLASS_IMPLEMENTATION ], get_selector_from_name("set_implementation"): lambda addr, data: [], + get_selector_from_name("get_cairo1_helpers_class_hash"): lambda addr, data: [ + CAIRO1_HELPERS_CLASS_HASH + ], } def __post_init__(self):