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

cov: fork #721

Merged
merged 14 commits into from
Feb 10, 2025
Merged
34 changes: 23 additions & 11 deletions cairo/ethereum/cancun/fork.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@
return base_fee_per_gas;
}

func validate_header{range_check_ptr}(header: Header, parent_header: Header) {
func validate_header{range_check_ptr, bitwise_ptr: BitwiseBuiltin*, keccak_ptr: KeccakBuiltin*}(
header: Header, parent_header: Header
) {
with_attr error_message("InvalidBlock") {
assert [range_check_ptr] = header.value.gas_limit.value - header.value.gas_used.value;
let range_check_ptr = range_check_ptr + 1;
Expand All @@ -180,21 +182,28 @@
);

assert expected_base_fee_per_gas = header.value.base_fee_per_gas;
assert [range_check_ptr] = header.value.timestamp.value -
parent_header.value.timestamp.value - 1;
assert [range_check_ptr + 1] = header.value.number.value -
parent_header.value.number.value - 1;
assert [range_check_ptr + 2] = 32 - header.value.extra_data.value.len;
let range_check_ptr = range_check_ptr + 3;

let timestamp_invalid = U256_le(header.value.timestamp, parent_header.value.timestamp);
assert timestamp_invalid.value = 0;

let number_is_valid = is_zero(
header.value.number.value - parent_header.value.number.value - 1
);
assert number_is_valid = 1;

let extra_data_is_valid = is_zero(32 - header.value.extra_data.value.len);
assert extra_data_is_valid = 1;

assert header.value.difficulty.value = 0;

assert header.value.nonce.value = 0;

assert header.value.ommers_hash.value.low = EMPTY_OMMER_HASH_LOW;
assert header.value.ommers_hash.value.high = EMPTY_OMMER_HASH_HIGH;
}

// TODO: Implement block header hash check
// let block_parent_hash = keccak256(rlp.encode(parent_header));
// assert header.value.parent_hash = block_parent_hash;
let parent_block_hash = keccak256_header(parent_header);
assert header.value.parent_hash = parent_block_hash;

Check warning on line 205 in cairo/ethereum/cancun/fork.cairo

View check run for this annotation

Codecov / codecov/patch

cairo/ethereum/cancun/fork.cairo#L205

Added line #L205 was not covered by tests
}
return ();
}

Expand Down Expand Up @@ -454,6 +463,9 @@

// Calculate priority fee
let priority_fee_per_gas = env.value.gas_price.value - env.value.base_fee_per_gas.value;
// INVARIANT: tx_gas.value - output.value.gas_left.value - gas_refund does not wrap around the prime field
assert [range_check_ptr] = tx_gas.value - output.value.gas_left.value - gas_refund;
let range_check_ptr = range_check_ptr + 1;
let transaction_fee = (tx_gas.value - output.value.gas_left.value - gas_refund) *
priority_fee_per_gas;

Expand Down
153 changes: 115 additions & 38 deletions cairo/tests/ethereum/cancun/test_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
process_transaction,
validate_header,
)
from ethereum.cancun.fork_types import Address
from ethereum.cancun.fork_types import Address, VersionedHash
from ethereum.cancun.state import State, set_account
from ethereum.cancun.transactions import (
AccessListTransaction,
Expand All @@ -29,28 +29,29 @@
)
from ethereum.cancun.vm import Environment
from ethereum.cancun.vm.gas import TARGET_BLOB_GAS_PER_BLOCK
from ethereum.crypto.hash import Hash32, keccak256
from ethereum.exceptions import EthereumException
from ethereum_types.bytes import Bytes, Bytes0, Bytes20
from ethereum_rlp import rlp
from ethereum_types.bytes import Bytes, Bytes0, Bytes8, 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 cairo_addons.testing.errors import strict_raises
from tests.ethereum.cancun.vm.test_interpreter import unimplemented_precompiles
from tests.utils.strategies import account_strategy, address, bytes32, state, uint
from tests.utils.constants import OMMER_HASH
from tests.utils.strategies import account_strategy, address, bytes32, small_bytes, uint

MIN_BASE_FEE = 1_000


@composite
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)
)
access_list = tuple(
(addr, tuple(storage_keys)) for addr, storage_keys in access_list_entries
access_list = draw(
st.lists(
st.tuples(address, st.lists(bytes32, max_size=3).map(tuple)), max_size=3
).map(tuple)
)

addr = (
Expand Down Expand Up @@ -102,6 +103,9 @@ def tx_with_small_data(draw, gas_strategy=uint, gas_price_strategy=uint):
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_versioned_hashes=st.lists(st.from_type(VersionedHash), max_size=3).map(
tuple
),
)

# Choose one transaction type
Expand All @@ -110,6 +114,70 @@ def tx_with_small_data(draw, gas_strategy=uint, gas_price_strategy=uint):
return tx


@composite
def headers(draw):
# Gas limit is in the order of magnitude of millions today,
# 2**32 is a safe upper bound and 21_000 is the minimum amount of gas in a transaction.
gas_limit = draw(st.integers(min_value=21_000, max_value=2**32 - 1).map(Uint))
parent_header = draw(
st.builds(
Header,
difficulty=uint,
number=uint,
nonce=st.from_type(Bytes8),
ommers_hash=st.just(OMMER_HASH).map(Hash32),
gas_limit=st.just(gas_limit),
gas_used=st.one_of(uint, st.just(gas_limit // Uint(2))),
# Base fee per gas is in the order of magnitude of the GWEI today which is 10^9,
# 2**48 is a safe upper bound with good slack.
base_fee_per_gas=st.integers(min_value=0, max_value=2**48 - 1).map(Uint),
prev_randao=bytes32,
withdrawals_root=bytes32,
parent_beacon_block_root=bytes32,
transactions_root=bytes32,
receipt_root=bytes32,
parent_hash=bytes32,
)
)
correct_base_fee = calculate_base_fee_per_gas(
parent_header.gas_limit,
parent_header.gas_limit,
parent_header.gas_used,
parent_header.base_fee_per_gas,
)
header = draw(
st.builds(
Header,
parent_hash=st.one_of(
st.just(keccak256(rlp.encode(parent_header))), st.from_type(Hash32)
),
gas_limit=st.just(parent_header.gas_limit),
gas_used=st.one_of(uint, st.just(parent_header.gas_limit // Uint(2))),
base_fee_per_gas=st.one_of(
st.just(correct_base_fee),
st.integers(min_value=0, max_value=2**48 - 1).map(Uint),
),
extra_data=st.one_of(small_bytes, bytes32.map(Bytes)),
difficulty=st.one_of(uint, st.just(Uint(0))),
ommers_hash=st.just(OMMER_HASH).map(Hash32),
nonce=st.one_of(
st.from_type(Bytes8),
st.just(Bytes8(int(0).to_bytes(8, "big"))),
),
number=st.one_of(
st.just(parent_header.number + Uint(1)),
uint,
),
prev_randao=bytes32,
withdrawals_root=bytes32,
parent_beacon_block_root=bytes32,
transactions_root=bytes32,
receipt_root=bytes32,
)
)
return parent_header, header


@composite
def tx_with_sender_in_state(
draw,
Expand All @@ -120,10 +188,23 @@ def tx_with_sender_in_state(
min_value=MIN_BASE_FEE - 1, max_value=2**64
).map(Uint),
),
state_strategy=state,
account_strategy=account_strategy,
):
state = draw(state_strategy)
env = draw(st.from_type(Environment))
if env.gas_price < env.base_fee_per_gas:
env.gas_price = draw(
st.one_of(
st.integers(
min_value=int(env.base_fee_per_gas), max_value=2**64 - 1
).map(Uint),
st.just(env.base_fee_per_gas),
)
)
# Values too high would cause taylor_exponential to run indefinitely.
env.excess_blob_gas = draw(
st.integers(0, 10 * int(TARGET_BLOB_GAS_PER_BLOCK)).map(U64)
)
state = env.state
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))
Expand Down Expand Up @@ -160,23 +241,7 @@ def tx_with_sender_in_state(
if should_add_sender_to_state:
sender = Address(int(expected_address).to_bytes(20, "little"))
set_account(state, sender, account)
return tx, state, chain_id


@composite
def env_with_valid_gas_price(draw):
env = draw(st.from_type(Environment))
if env.gas_price < env.base_fee_per_gas:
env.gas_price = draw(
st.integers(min_value=int(env.base_fee_per_gas), max_value=2**64 - 1).map(
Uint
)
)
# Values too high would cause taylor_exponential to run indefinitely.
env.excess_blob_gas = draw(
st.integers(0, 10 * int(TARGET_BLOB_GAS_PER_BLOCK)).map(U64)
)
return env
return tx, env, chain_id


class TestFork:
Expand Down Expand Up @@ -219,8 +284,9 @@ def test_calculate_base_fee_per_gas(
parent_base_fee_per_gas,
)

@given(header=..., parent_header=...)
def test_validate_header(self, cairo_run, header: Header, parent_header: Header):
@given(headers=headers())
def test_validate_header(self, cairo_run, headers: Tuple[Header, Header]):
parent_header, header = headers
try:
cairo_run("validate_header", header, parent_header)
except Exception as e:
Expand Down Expand Up @@ -253,13 +319,13 @@ def test_make_receipt(
"make_receipt", tx, error, cumulative_gas_used, logs
)

@given(env=env_with_valid_gas_price(), data=tx_with_sender_in_state())
@given(data=tx_with_sender_in_state())
def test_process_transaction(
self, cairo_run, env: Environment, data: Tuple[Transaction, State, U64]
self, cairo_run, data: Tuple[Transaction, Environment, 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
tx, env, _ = data
try:
env_cairo, cairo_result = cairo_run("process_transaction", env, tx)
except Exception as cairo_e:
Expand Down Expand Up @@ -299,11 +365,11 @@ def test_check_transaction(
base_fee_per_gas: Uint,
excess_blob_gas: U64,
):
tx, state, chain_id = data
tx, env, chain_id = data
try:
cairo_state, cairo_result = cairo_run_py(
"check_transaction",
state,
env.state,
tx,
gas_available,
chain_id,
Expand All @@ -313,7 +379,7 @@ def test_check_transaction(
except Exception as e:
with strict_raises(type(e)):
check_transaction(
state,
env.state,
tx,
gas_available,
chain_id,
Expand All @@ -323,9 +389,9 @@ def test_check_transaction(
return

assert cairo_result == check_transaction(
state, tx, gas_available, chain_id, base_fee_per_gas, excess_blob_gas
env.state, tx, gas_available, chain_id, base_fee_per_gas, excess_blob_gas
)
assert cairo_state == state
assert cairo_state == env.state

@given(blocks=st.lists(st.builds(Block), max_size=300))
def test_get_last_256_block_hashes(self, cairo_run, blocks):
Expand All @@ -335,3 +401,14 @@ def test_get_last_256_block_hashes(self, cairo_run, blocks):
cairo_result = cairo_run("get_last_256_block_hashes", chain)

assert py_result == cairo_result

@given(header=...)
def test_keccak256_header(self, cairo_run, header: Header):
try:
cairo_result = cairo_run("keccak256_header", header)
except Exception as cairo_error:
with strict_raises(type(cairo_error)):
keccak256(rlp.encode(header))
return

assert cairo_result == keccak256(rlp.encode(header))
3 changes: 3 additions & 0 deletions cairo/tests/utils/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
# Source: <https://eips.ethereum.org/EIPS/eip-4844#specification>
MAX_BLOB_GAS_PER_BLOCK = 786432
COINBASE = "0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba"
OMMER_HASH = int(
0x1DCC4DE8DEC75D7AAB85B567B6CCD41AD312451B948A7413F0A142FD40D49347
).to_bytes(32, "little")
BASE_FEE_PER_GAS = 1_000
signers = {
keys.PrivateKey(
Expand Down
21 changes: 21 additions & 0 deletions cairo/tests/utils/strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,26 @@ def bounded_u256_strategy(min_value: int = 0, max_value: int = 2**256 - 1):
),
)

header = st.builds(
Header,
parent_hash=hash32,
ommers_hash=hash32,
coinbase=address,
state_root=root,
transactions_root=root,
receipt_root=root,
bloom=bloom,
difficulty=uint,
number=uint,
gas_limit=uint,
gas_used=uint,
timestamp=uint256,
extra_data=small_bytes,
prev_randao=bytes32,
nonce=bytes8,
base_fee_per_gas=uint,
)


private_key = (
st.integers(min_value=1, max_value=int(SECP256K1N) - 1)
Expand Down Expand Up @@ -555,3 +575,4 @@ def register_type_strategies():
st.register_type_strategy(TransientStorage, transient_storage)
st.register_type_strategy(MutableBloom, bloom.map(MutableBloom))
st.register_type_strategy(Environment, environment_lite)
st.register_type_strategy(Header, header)