From 64d78409dd8afdb68577a9ab38eddac3ea1e7d01 Mon Sep 17 00:00:00 2001 From: Oba Date: Wed, 5 Feb 2025 13:23:54 +0100 Subject: [PATCH 1/2] fix: test_process_transaction --- cairo/ethereum/cancun/fork.cairo | 32 +++++---- cairo/tests/ethereum/cancun/test_fork.py | 89 +++++++++++++++++------- 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/cairo/ethereum/cancun/fork.cairo b/cairo/ethereum/cancun/fork.cairo index 62950a8d4..9cf5b822a 100644 --- a/cairo/ethereum/cancun/fork.cairo +++ b/cairo/ethereum/cancun/fork.cairo @@ -64,6 +64,7 @@ from ethereum.cancun.transactions_types import ( TransactionImpl, TransactionType, TupleAccessList, + TupleAccessListStruct, To, ToStruct, ) @@ -84,6 +85,7 @@ from ethereum.utils.numeric import ( divmod, min, U256_add, + U256_sub, U256__eq__, U256_from_felt, U256_le, @@ -303,6 +305,7 @@ func process_transaction{ local tx_to: To; local tx_value: U256; local blob_gas_fee: Uint; + local access_lists: TupleAccessList; if (tx.value.blob_transaction.value != 0) { assert tx_gas = tx.value.blob_transaction.value.gas; assert tx_data = tx.value.blob_transaction.value.data; @@ -312,6 +315,7 @@ func process_transaction{ assert tx_value = tx.value.blob_transaction.value.value; let blob_gas_fee_res = calculate_data_fee(env.value.excess_blob_gas, tx); assert blob_gas_fee = blob_gas_fee_res; + assert access_lists = TupleAccessList(cast(0, TupleAccessListStruct*)); tempvar range_check_ptr = range_check_ptr; } else { tempvar range_check_ptr = range_check_ptr; @@ -325,6 +329,7 @@ func process_transaction{ assert tx_to = tx.value.fee_market_transaction.value.to; assert tx_value = tx.value.fee_market_transaction.value.value; assert blob_gas_fee = Uint(0); + assert access_lists = tx.value.fee_market_transaction.value.access_list; } if (tx.value.legacy_transaction.value != 0) { @@ -333,6 +338,7 @@ func process_transaction{ assert tx_to = tx.value.legacy_transaction.value.to; assert tx_value = tx.value.legacy_transaction.value.value; assert blob_gas_fee = Uint(0); + assert access_lists = TupleAccessList(cast(0, TupleAccessListStruct*)); } if (tx.value.access_list_transaction.value != 0) { @@ -341,6 +347,7 @@ func process_transaction{ assert tx_to = tx.value.access_list_transaction.value.to; assert tx_value = tx.value.access_list_transaction.value.value; assert blob_gas_fee = Uint(0); + assert access_lists = tx.value.access_list_transaction.value.access_list; } let effective_gas_fee = tx_gas.value * env.value.gas_price.value; @@ -353,18 +360,13 @@ func process_transaction{ increment_nonce{state=state}(sender); // Deduct gas fee from sender - with_attr error_message("OverflowError") { - assert sender_account.value.balance.value.high = 0; // emulate the cast to Uint - assert_le_felt( - sender_account.value.balance.value.low, effective_gas_fee + blob_gas_fee.value - ); - let sender_balance_after_gas_fee = sender_account.value.balance.value.low - - effective_gas_fee - blob_gas_fee.value; - assert [range_check_ptr] = sender_balance_after_gas_fee; - let range_check_ptr = range_check_ptr + 1; - } - tempvar sender_balance_after_gas_fee_u256 = U256( - new U256Struct(sender_balance_after_gas_fee, 0) + tempvar effective_gas_fee_u256 = U256(new U256Struct(effective_gas_fee, 0)); + tempvar blob_gas_fee_u256 = U256(new U256Struct(blob_gas_fee.value, 0)); + let sender_balance_after_gas_fee = U256_sub( + sender_account.value.balance, effective_gas_fee_u256 + ); + let sender_balance_after_gas_fee_u256 = U256_sub( + sender_balance_after_gas_fee, blob_gas_fee_u256 ); set_account_balance{state=state}(sender, sender_balance_after_gas_fee_u256); EnvImpl.set_state{env=env}(state); @@ -390,11 +392,10 @@ func process_transaction{ ); if (tx.value.legacy_transaction.value == 0) { - let access_list = tx.value.access_list_transaction.value.access_list; process_access_list{ preaccessed_addresses=preaccessed_addresses, preaccessed_storage_keys=preaccessed_storage_keys, - }(access_list, access_list.value.len, 0); + }(access_lists, access_lists.value.len, 0); tempvar keccak_ptr = keccak_ptr; tempvar bitwise_ptr = bitwise_ptr; tempvar poseidon_ptr = poseidon_ptr; @@ -436,6 +437,7 @@ func process_transaction{ // Calculate gas refund with_attr error_message("OverflowError") { assert output.value.refund_counter.value.high = 0; + assert_le_felt(env.value.base_fee_per_gas.value, env.value.gas_price.value); } let gas_used = tx_gas.value - output.value.gas_left.value; let (gas_refund_div_5, _) = divmod(gas_used, 5); @@ -665,7 +667,7 @@ func check_transaction{ let blob_gas_price = calculate_blob_gas_price(excess_blob_gas); let max_fee_per_blob_gas_u256 = tx.value.blob_transaction.value.max_fee_per_blob_gas; let max_fee_per_blob_gas_uint = U256_to_Uint(max_fee_per_blob_gas_u256); - let blob_gas_price_valid = is_le(blob_gas_price.value, max_fee_per_blob_gas_uint.value); + let blob_gas_price_valid = is_le(max_fee_per_blob_gas_uint.value, blob_gas_price.value - 1); with_attr error_message("InvalidBlock") { assert blob_gas_price_valid = 1; } diff --git a/cairo/tests/ethereum/cancun/test_fork.py b/cairo/tests/ethereum/cancun/test_fork.py index 74914e332..b28dec030 100644 --- a/cairo/tests/ethereum/cancun/test_fork.py +++ b/cairo/tests/ethereum/cancun/test_fork.py @@ -30,7 +30,7 @@ from ethereum.cancun.vm import Environment from ethereum.cancun.vm.gas import TARGET_BLOB_GAS_PER_BLOCK from ethereum.exceptions import EthereumException -from ethereum_types.bytes import Bytes, Bytes20 +from ethereum_types.bytes import Bytes, Bytes0, Bytes20 from ethereum_types.numeric import U64, U256, Uint from hypothesis import assume, given from hypothesis import strategies as st @@ -38,11 +38,13 @@ from tests.ethereum.cancun.vm.test_interpreter import unimplemented_precompiles from tests.utils.errors import strict_raises -from tests.utils.strategies import account_strategy, address, bytes32, state +from tests.utils.strategies import account_strategy, address, bytes32, state, uint + +MIN_BASE_FEE = 1_000 @composite -def tx_without_code(draw): +def tx_with_small_data(draw, gas_strategy=uint, gas_price_strategy=uint): # Generate access list access_list_entries = draw( st.lists(st.tuples(address, st.lists(bytes32, max_size=2)), max_size=3) @@ -51,16 +53,22 @@ def tx_without_code(draw): (addr, tuple(storage_keys)) for addr, storage_keys in access_list_entries ) - to = ( + addr = ( st.integers(min_value=0, max_value=2**160 - 1) .filter(lambda x: x not in unimplemented_precompiles) .map(lambda x: Bytes20(x.to_bytes(20, "little"))) .map(Address) ) + to = st.one_of(addr, st.just(Bytes0())) + # Define strategies for each transaction type legacy_tx = st.builds( - LegacyTransaction, data=st.just(Bytes(bytes.fromhex("6060"))), to=to + LegacyTransaction, + data=st.just(Bytes(bytes.fromhex("6060"))), + to=to, + gas=gas_strategy, + gas_price=gas_price_strategy, ) access_list_tx = st.builds( @@ -68,20 +76,32 @@ def tx_without_code(draw): data=st.just(Bytes(bytes.fromhex("6060"))), access_list=st.just(access_list), to=to, + gas=gas_strategy, + gas_price=gas_price_strategy, ) + base_fee_per_gas = draw(gas_price_strategy) + max_priority_fee_per_gas = draw(gas_price_strategy) + max_fee_per_gas = max_priority_fee_per_gas + base_fee_per_gas + fee_market_tx = st.builds( FeeMarketTransaction, data=st.just(Bytes(bytes.fromhex("6060"))), access_list=st.just(access_list), to=to, + gas=gas_strategy, + max_priority_fee_per_gas=st.just(max_priority_fee_per_gas), + max_fee_per_gas=st.just(max_fee_per_gas), ) blob_tx = st.builds( BlobTransaction, data=st.just(Bytes(bytes.fromhex("6060"))), access_list=st.just(access_list), - to=to, + to=addr, + gas=gas_strategy, + max_priority_fee_per_gas=st.just(max_priority_fee_per_gas), + max_fee_per_gas=st.just(max_fee_per_gas), ) # Choose one transaction type @@ -92,11 +112,29 @@ def tx_without_code(draw): @composite def tx_with_sender_in_state( - draw, state_strategy=state, account_strategy=account_strategy + draw, + # We need to set a high tx gas so that tx.gas > intrinsic cost and base_fee < gas_price + tx_strategy=tx_with_small_data( + gas_strategy=st.integers(min_value=2**16, max_value=2**26 - 1).map(Uint), + gas_price_strategy=st.integers( + min_value=MIN_BASE_FEE - 1, max_value=2**64 + ).map(Uint), + ), + state_strategy=state, + account_strategy=account_strategy, ): state = draw(state_strategy) - chain_id = draw(st.from_type(U64)) - tx = draw(st.from_type(Transaction)) + account = draw(account_strategy) + # 2 * chain_id + 35 + v must be less than 2^64 for the signature of a legacy transaction to be valid + chain_id = draw(st.integers(min_value=1, max_value=(2**64 - 37) // 2).map(U64)) + tx = draw(tx_strategy) + nonce = U256(account.nonce) + # To avoid useless failures, set nonce of the transaction to the nonce of the sender account + tx = ( + replace(tx, chain_id=chain_id, nonce=nonce) + if not isinstance(tx, LegacyTransaction) + else replace(tx, nonce=nonce) + ) private_key = draw(st.from_type(PrivateKey)) expected_address = int(private_key.public_key.to_address(), 16) if isinstance(tx, LegacyTransaction): @@ -113,7 +151,7 @@ def tx_with_sender_in_state( # Overwrite r and s with valid values v_or_y_parity = {} if isinstance(tx, LegacyTransaction): - v_or_y_parity["v"] = U256(signature.v) + v_or_y_parity["v"] = U256(U64(2) * chain_id + U64(35) + U64(signature.v)) else: v_or_y_parity["y_parity"] = U256(signature.v) tx = replace(tx, r=U256(signature.r), s=U256(signature.s), **v_or_y_parity) @@ -121,11 +159,8 @@ def tx_with_sender_in_state( should_add_sender_to_state = draw(integers(0, 99)) < 80 if should_add_sender_to_state: sender = Address(int(expected_address).to_bytes(20, "little")) - account = draw(account_strategy) set_account(state, sender, account) - return tx, state - else: - return tx, state + return tx, state, chain_id @composite @@ -218,12 +253,15 @@ def test_make_receipt( "make_receipt", tx, error, cumulative_gas_used, logs ) - @given(env=env_with_valid_gas_price(), tx=tx_without_code()) - def test_process_transaction(self, cairo_run, env: Environment, tx: Transaction): + @given(env=env_with_valid_gas_price(), data=tx_with_sender_in_state()) + def test_process_transaction( + self, cairo_run, env: Environment, data: Tuple[Transaction, State, U64] + ): # The Cairo Runner will raise if an exception is in the return values OR if # an assert expression fails (e.g. InvalidBlock) + tx, __, _ = data try: - env_cairo, gas_cairo, logs_cairo = cairo_run("process_transaction", env, tx) + env_cairo, cairo_result = cairo_run("process_transaction", env, tx) except Exception as cairo_e: # 1. Handle exceptions thrown try: @@ -233,23 +271,23 @@ def test_process_transaction(self, cairo_run, env: Environment, tx: Transaction) return # 2. Handle exceptions in return values - assert env_cairo == env - assert gas_cairo == output_py[0] - assert logs_cairo == output_py[1] + # Never reached with the current strategy and 300 examples + # For that, it would be necessary to send a tx with correct data + # that would raise inside execute_code with strict_raises(type(cairo_e)): if len(output_py) == 3: raise output_py[2] return - gas_used, logs = process_transaction(env, tx) + gas_used, logs, error = process_transaction(env, tx) assert env_cairo == env - assert gas_used == gas_cairo - assert logs == logs_cairo + assert gas_used == cairo_result[0] + assert logs == cairo_result[1] + assert error == cairo_result[2] @given( data=tx_with_sender_in_state(), gas_available=..., - chain_id=..., base_fee_per_gas=..., excess_blob_gas=..., ) @@ -258,11 +296,10 @@ def test_check_transaction( cairo_run_py, data, gas_available: Uint, - chain_id: U64, base_fee_per_gas: Uint, excess_blob_gas: U64, ): - tx, state = data + tx, state, chain_id = data try: cairo_state, cairo_result = cairo_run_py( "check_transaction", From c5de612beb6c5b9d32b91315ae9038dee4ef67a4 Mon Sep 17 00:00:00 2001 From: Oba Date: Thu, 6 Feb 2025 11:56:12 +0100 Subject: [PATCH 2/2] fix: blob transaction access_list in process_transaction --- cairo/ethereum/cancun/fork.cairo | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cairo/ethereum/cancun/fork.cairo b/cairo/ethereum/cancun/fork.cairo index 9cf5b822a..cde448027 100644 --- a/cairo/ethereum/cancun/fork.cairo +++ b/cairo/ethereum/cancun/fork.cairo @@ -315,7 +315,7 @@ func process_transaction{ assert tx_value = tx.value.blob_transaction.value.value; let blob_gas_fee_res = calculate_data_fee(env.value.excess_blob_gas, tx); assert blob_gas_fee = blob_gas_fee_res; - assert access_lists = TupleAccessList(cast(0, TupleAccessListStruct*)); + assert access_lists = tx.value.blob_transaction.value.access_list; tempvar range_check_ptr = range_check_ptr; } else { tempvar range_check_ptr = range_check_ptr;