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

Add intrinsic gas cost diff testing #74

Merged
merged 5 commits into from
Oct 27, 2024
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
54 changes: 54 additions & 0 deletions cairo/programs/fork.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ from starkware.cairo.common.math_cmp import is_nn
from starkware.cairo.common.bool import FALSE

from src.model import model
from src.utils.array import count_not_zero
from src.gas import Gas
from src.utils.transaction import (
TX_DATA_COST_PER_ZERO,
TX_DATA_COST_PER_NON_ZERO,
TX_CREATE_COST,
TX_BASE_COST,
TX_ACCESS_LIST_ADDRESS_COST,
TX_ACCESS_LIST_STORAGE_KEY_COST,
)

using Uint128 = felt;
using Uint64 = felt;
Expand Down Expand Up @@ -217,3 +227,47 @@ func validate_header{range_check_ptr}(header: model.BlockHeader, parent_header:
// raise InvalidBlock
return ();
}

// @notice See https://github.com/ethereum/execution-specs/blob/master/src/ethereum/cancun/fork.py#L818-L862
func calculate_intrinsic_cost{range_check_ptr}(tx: model.Transaction*) -> felt {
alloc_locals;

let count = count_not_zero(tx.payload_len, tx.payload);
let zeroes = tx.payload_len - count;
local data_cost = zeroes * TX_DATA_COST_PER_ZERO + count * TX_DATA_COST_PER_NON_ZERO;

if (tx.destination.is_some == FALSE) {
let init_code_cost = Gas.init_code_cost(tx.payload_len);
tempvar range_check_ptr = range_check_ptr;
tempvar create_cost = TX_CREATE_COST + init_code_cost;
static_assert range_check_ptr == [ap - 2];
} else {
tempvar range_check_ptr = range_check_ptr;
tempvar create_cost = 0;
static_assert range_check_ptr == [ap - 2];
}
local range_check_ptr = [ap - 2];
let create_cost = [ap - 1];

let access_list_cost = Internals.access_list_cost(tx.access_list_len, tx.access_list);
return TX_BASE_COST + data_cost + create_cost + access_list_cost;
}

// A namespace for functions not part of the execution specs file but required to ease the cairo code
namespace Internals {
func access_list_cost(access_list_len: felt, access_list: felt*) -> felt {
alloc_locals;

if (access_list_len == 0) {
return 0;
}

let address = [access_list];
let storage_keys_len = [access_list + 1];
let item_len = 2 + storage_keys_len * Uint256.SIZE;
let cum_gas_cost = access_list_cost(access_list_len - item_len, access_list + item_len);
let current_cost = TX_ACCESS_LIST_ADDRESS_COST + storage_keys_len *
TX_ACCESS_LIST_STORAGE_KEY_COST;
return cum_gas_cost + current_cost;
}
}
12 changes: 10 additions & 2 deletions cairo/src/gas.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ from starkware.cairo.common.uint256 import Uint256, uint256_lt
from src.model import model
from src.utils.uint256 import uint256_eq
from src.utils.utils import Helpers
from src.utils.maths import unsigned_div_rem
from src.utils.maths import unsigned_div_rem, ceil32

const GAS_INIT_CODE_WORD_COST = 2;

namespace Gas {
const JUMPDEST = 1;
Expand Down Expand Up @@ -49,13 +51,19 @@ namespace Gas {
const COLD_SLOAD = 2100;
const COLD_ACCOUNT_ACCESS = 2600;
const WARM_ACCESS = 100;
const INIT_CODE_WORD_COST = 2;
const TX_BASE_COST = 21000;
const TX_ACCESS_LIST_ADDRESS_COST = 2400;
const TX_ACCESS_LIST_STORAGE_KEY_COST = 1900;
const BLOBHASH = 3;
const MEMORY_COST_U32 = 0x200018000000;

// @notive See https://github.com/ethereum/execution-specs/blob/master/src/ethereum/cancun/vm/gas.py#L253-L269
func init_code_cost{range_check_ptr}(init_code_length: felt) -> felt {
let init_code_bytes = ceil32(init_code_length);
let (init_code_words, _) = unsigned_div_rem(init_code_bytes, 32);
return GAS_INIT_CODE_WORD_COST * init_code_words;
}

// @notice Compute the cost of the memory for a given words length.
// @dev To avoid range_check overflow, we compute words_len / 512
// instead of words_len * words_len / 512. Then we recompute the
Expand Down
5 changes: 2 additions & 3 deletions cairo/src/instructions/system_operations.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin, BitwiseBuiltin
from starkware.cairo.common.math import split_felt
from starkware.cairo.common.math_cmp import is_nn, is_not_zero
from starkware.cairo.common.uint256 import Uint256, uint256_lt, uint256_le
from starkware.cairo.common.default_dict import default_dict_new
from starkware.cairo.common.dict_access import DictAccess

from src.account import Account
from src.interfaces.interfaces import ICairo1Helpers
from src.constants import Constants
from src.errors import Errors
from src.evm import EVM
from src.gas import Gas
from src.gas import Gas, GAS_INIT_CODE_WORD_COST
from src.memory import Memory
from src.model import model
from src.stack import Stack
Expand Down Expand Up @@ -57,7 +56,7 @@ namespace SystemOperations {
return evm;
}
let (calldata_words, _) = unsigned_div_rem(size.low + 31, 32);
let init_code_gas_low = Gas.INIT_CODE_WORD_COST * calldata_words;
let init_code_gas_low = GAS_INIT_CODE_WORD_COST * calldata_words;
tempvar init_code_gas_high = is_not_zero(size.high) * 2 ** 128;
let calldata_word_gas = is_create2 * Gas.KECCAK256_WORD * calldata_words;
let evm = EVM.charge_gas(
Expand Down
4 changes: 2 additions & 2 deletions cairo/src/interpreter.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ from src.precompiles.precompiles import Precompiles
from src.precompiles.precompiles_helpers import PrecompilesHelpers
from src.stack import Stack
from src.state import State
from src.gas import Gas
from src.gas import Gas, GAS_INIT_CODE_WORD_COST
from src.utils.utils import Helpers
from src.utils.array import count_not_zero
from src.utils.uint256 import uint256_sub, uint256_add
Expand Down Expand Up @@ -851,7 +851,7 @@ namespace Interpreter {
if (is_deploy_tx != FALSE) {
let (empty: felt*) = alloc();
let (init_code_words, _) = unsigned_div_rem(bytecode_len + 31, 32);
let init_code_gas = Gas.INIT_CODE_WORD_COST * init_code_words;
let init_code_gas = GAS_INIT_CODE_WORD_COST * init_code_words;
assert bytecode = tmp_calldata;
assert calldata = empty;
assert intrinsic_gas = tmp_intrinsic_gas + Gas.CREATE + init_code_gas;
Expand Down
2 changes: 1 addition & 1 deletion cairo/src/state.cairo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from starkware.cairo.common.alloc import alloc
from starkware.cairo.common.cairo_builtins import HashBuiltin
from starkware.cairo.common.default_dict import default_dict_new, default_dict_finalize
from starkware.cairo.common.default_dict import default_dict_new
from starkware.cairo.common.dict import dict_read, dict_write
from starkware.cairo.common.dict_access import DictAccess
from starkware.cairo.common.memcpy import memcpy
Expand Down
8 changes: 8 additions & 0 deletions cairo/src/utils/maths.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,11 @@ func unsigned_div_rem{range_check_ptr}(value, div) -> (q: felt, r: felt) {
assert value = q * div + r;
return (q, r);
}

func ceil32{range_check_ptr}(value: felt) -> felt {
if (value == 0) {
return 0;
}
let (q, r) = unsigned_div_rem(value + 31, 32);
return q * 32;
}
7 changes: 7 additions & 0 deletions cairo/src/utils/transaction.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ from src.utils.utils import Helpers
from src.utils.bytes import keccak
from src.utils.signature import Signature

const TX_BASE_COST = 21000;
const TX_DATA_COST_PER_NON_ZERO = 16;
const TX_DATA_COST_PER_ZERO = 4;
const TX_CREATE_COST = 32000;
const TX_ACCESS_LIST_ADDRESS_COST = 2400;
const TX_ACCESS_LIST_STORAGE_KEY_COST = 1900;

// @title Transaction utils
// @notice This file contains utils for decoding eth transactions
// @custom:namespace Transaction
Expand Down
30 changes: 0 additions & 30 deletions cairo/tests/fixtures/data.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,7 @@
import pytest
from hypothesis import strategies as st

from tests.utils.models import Account, Block, State

block_header_strategy = st.fixed_dictionaries(
{
"parent_hash": st.binary(min_size=32, max_size=32),
"ommers_hash": st.just(
bytes.fromhex(
"1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347"
)
),
"coinbase": st.binary(min_size=20, max_size=20),
"state_root": st.binary(min_size=32, max_size=32),
"transactions_root": st.binary(min_size=32, max_size=32),
"receipt_root": st.binary(min_size=32, max_size=32),
"bloom": st.binary(min_size=256, max_size=256),
"difficulty": st.just(0x00),
"number": st.integers(min_value=0, max_value=2**64 - 1),
"gas_limit": st.integers(min_value=0, max_value=2**64 - 1),
"gas_used": st.integers(min_value=0, max_value=2**64 - 1),
"timestamp": st.integers(min_value=0, max_value=2**64 - 1),
"extra_data": st.binary(max_size=32),
"prev_randao": st.binary(min_size=32, max_size=32),
"nonce": st.just("0x0000000000000000"),
"base_fee_per_gas": st.integers(min_value=0, max_value=2**64 - 1),
"withdrawals_root": st.binary(min_size=32, max_size=32),
"blob_gas_used": st.integers(min_value=0, max_value=2**64 - 1),
"excess_blob_gas": st.integers(min_value=0, max_value=2**64 - 1),
"parent_beacon_block_root": st.binary(min_size=32, max_size=32),
}
)


@pytest.fixture
def block():
Expand Down
23 changes: 22 additions & 1 deletion cairo/tests/programs/test_fork.cairo
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from programs.fork import check_gas_limit, calculate_base_fee_per_gas, validate_header, Uint128
from programs.fork import (
check_gas_limit,
calculate_base_fee_per_gas,
validate_header,
Uint128,
calculate_intrinsic_cost,
)
from src.model import model

func test_check_gas_limit{range_check_ptr}() {
Expand Down Expand Up @@ -46,3 +52,18 @@ func test_validate_header{range_check_ptr}() {
validate_header([header], [parent_header]);
return ();
}

func test_calculate_intrinsic_cost{range_check_ptr}() -> felt {
tempvar tx: model.Transaction*;
%{
if '__dict_manager' not in globals():
from starkware.cairo.common.dict import DictManager
__dict_manager = DictManager()

from tests.utils.hints import gen_arg

ids.tx = gen_arg(__dict_manager, segments, program_input["tx"])
%}

return calculate_intrinsic_cost(tx);
}
32 changes: 18 additions & 14 deletions cairo/tests/programs/test_fork.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
from ethereum.cancun.blocks import Header
from ethereum.cancun.fork import (
calculate_base_fee_per_gas,
calculate_intrinsic_cost,
check_gas_limit,
validate_header,
)
from ethereum.cancun.transactions import AccessListTransaction
from ethereum.exceptions import InvalidBlock
from hypothesis import given
from hypothesis.strategies import integers

from tests.fixtures.data import block_header_strategy
from tests.utils.errors import cairo_error
from tests.utils.models import BlockHeader
from tests.utils.models import BlockHeader, Transaction
from tests.utils.strategies import (
access_list_transaction,
block_header,
uint64,
uint128,
)


class TestFork:
@given(
integers(min_value=0, max_value=2**128 - 1),
integers(min_value=0, max_value=2**128 - 1),
)
@given(uint128, uint128)
def test_check_gas_limit(self, cairo_run, gas_limit, parent_gas_limit):
error = check_gas_limit(gas_limit, parent_gas_limit)
if not error:
Expand All @@ -34,12 +37,7 @@ def test_check_gas_limit(self, cairo_run, gas_limit, parent_gas_limit):
parent_gas_limit=parent_gas_limit,
)

@given(
integers(min_value=0, max_value=2**64 - 1),
integers(min_value=0, max_value=2**64 - 1),
integers(min_value=0, max_value=2**64 - 1),
integers(min_value=0, max_value=2**64 - 1),
)
@given(uint64, uint64, uint64, uint64)
def test_calculate_base_fee_per_gas(
self,
cairo_run,
Expand Down Expand Up @@ -76,7 +74,7 @@ def test_calculate_base_fee_per_gas(
parent_base_fee_per_gas=parent_base_fee_per_gas,
)

@given(header=block_header_strategy, parent_header=block_header_strategy)
@given(header=block_header, parent_header=block_header)
def test_validate_header(self, cairo_run, header, parent_header):
error = None
try:
Expand All @@ -97,3 +95,9 @@ def test_validate_header(self, cairo_run, header, parent_header):
header=BlockHeader.model_validate(header),
parent_header=BlockHeader.model_validate(parent_header),
)

@given(tx=access_list_transaction)
def test_calculate_intrinsic_cost(self, cairo_run, tx):
assert calculate_intrinsic_cost(AccessListTransaction(**tx)) == cairo_run(
"test_calculate_intrinsic_cost", tx=Transaction.model_validate(tx)
)
6 changes: 6 additions & 0 deletions cairo/tests/src/test_gas.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ func test__compute_message_call_gas{range_check_ptr}() -> felt {

return gas;
}

func test__init_code_cost{range_check_ptr}() -> felt {
tempvar init_code_len: felt;
%{ ids.init_code_len = program_input["init_code_len"]; %}
return Gas.init_code_cost(init_code_len);
}
39 changes: 15 additions & 24 deletions cairo/tests/src/test_gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,22 @@
from ethereum.shanghai.vm.gas import (
calculate_gas_extend_memory,
calculate_memory_gas_cost,
init_code_cost,
)
from hypothesis import given
from hypothesis.strategies import integers

from src.utils.uint256 import int_to_uint256

int_to_uint256(0) # (0, 0)
from tests.utils.strategies import uint20, uint24, uint64, uint128, uint256


class TestGas:
class TestCost:
@given(max_offset=integers(min_value=0, max_value=0xFFFFFF))
def test_should_return_same_as_execution_specs(self, cairo_run, max_offset):
@given(max_offset=uint24)
def test_memory_cost(self, cairo_run, max_offset):
output = cairo_run("test__memory_cost", words_len=(max_offset + 31) // 32)
assert calculate_memory_gas_cost(max_offset) == output

@given(
bytes_len=integers(min_value=0, max_value=2**128 - 1),
added_offset=integers(min_value=0, max_value=2**128 - 1),
)
def test_should_return_correct_expansion_cost(
self, cairo_run, bytes_len, added_offset
):
@given(bytes_len=uint128, added_offset=uint128)
def test_memory_expansion_cost(self, cairo_run, bytes_len, added_offset):
max_offset = bytes_len + added_offset
output = cairo_run(
"test__memory_expansion_cost",
Expand All @@ -36,13 +29,8 @@ def test_should_return_correct_expansion_cost(
diff = cost_after - cost_before
assert diff == output

@given(
offset_1=integers(min_value=0, max_value=0xFFFFF),
size_1=integers(min_value=0, max_value=0xFFFFF),
offset_2=integers(min_value=0, max_value=0xFFFFF),
size_2=integers(min_value=0, max_value=0xFFFFF),
)
def test_should_return_max_expansion_cost(
@given(offset_1=uint20, size_1=uint20, offset_2=uint20, size_2=uint20)
def test_max_memory_expansion_cost(
self, cairo_run, offset_1, size_1, offset_2, size_2
):
output = cairo_run(
Expand All @@ -64,10 +52,7 @@ def test_should_return_max_expansion_cost(
).cost
)

@given(
offset=integers(min_value=0, max_value=2**256 - 1),
size=integers(min_value=0, max_value=2**256 - 1),
)
@given(offset=uint256, size=uint256)
def test_memory_expansion_cost_saturated(self, cairo_run, offset, size):
output = cairo_run(
"test__memory_expansion_cost_saturated",
Expand All @@ -84,6 +69,12 @@ def test_memory_expansion_cost_saturated(self, cairo_run, offset, size):

assert cost == output

@given(init_code_len=uint64)
def test_init_code_cost(self, cairo_run, init_code_len):
assert init_code_cost(init_code_len) == cairo_run(
"test__init_code_cost", init_code_len=init_code_len
)

class TestMessageGas:
@pytest.mark.parametrize(
"gas_param, gas_left, expected",
Expand Down
Loading
Loading