Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: test_process_transaction #664

Merged
merged 3 commits into from
Feb 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions cairo/ethereum/cancun/fork.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ from ethereum.cancun.transactions_types import (
TransactionImpl,
TransactionType,
TupleAccessList,
TupleAccessListStruct,
To,
ToStruct,
)
Expand All @@ -84,6 +85,7 @@ from ethereum.utils.numeric import (
divmod,
min,
U256_add,
U256_sub,
U256__eq__,
U256_from_felt,
U256_le,
Expand Down Expand Up @@ -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;
Expand All @@ -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 = tx.value.blob_transaction.value.access_list;
tempvar range_check_ptr = range_check_ptr;
obatirou marked this conversation as resolved.
Show resolved Hide resolved
} else {
tempvar range_check_ptr = range_check_ptr;
Expand All @@ -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) {
Expand All @@ -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) {
Expand All @@ -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;
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down
89 changes: 63 additions & 26 deletions cairo/tests/ethereum/cancun/test_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,21 @@
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
from hypothesis.strategies import composite, integers

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)
Expand All @@ -51,37 +53,55 @@ 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(
AccessListTransaction,
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
Expand All @@ -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):
Expand All @@ -113,19 +151,16 @@ 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)

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
Expand Down Expand Up @@ -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:
Expand All @@ -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=...,
)
Expand All @@ -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",
Expand Down