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

feat: calculate_gas_extend_memory #342

Merged
merged 2 commits into from
Jan 3, 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
1 change: 1 addition & 0 deletions cairo/ethereum/cancun/vm.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ namespace EvmImpl {
code=evm.value.code,
gas_left=evm.value.gas_left,
env=evm.value.env,
valid_jump_destinations=evm.value.valid_jump_destinations,
logs=evm.value.logs,
refund_counter=evm.value.refund_counter,
running=evm.value.running,
Expand Down
196 changes: 132 additions & 64 deletions cairo/ethereum/cancun/vm/gas.cairo
Original file line number Diff line number Diff line change
@@ -1,20 +1,79 @@
from ethereum_types.numeric import U256, Uint, U64
from ethereum_types.numeric import U256, Uint, U64, U256Struct
from ethereum.utils.numeric import is_zero, divmod, taylor_exponential, min, ceil32
from ethereum_types.bytes import BytesStruct
from ethereum.cancun.blocks import Header
from ethereum.cancun.transactions import Transaction
from ethereum_types.others import ListTupleU256U256, TupleU256U256
from ethereum.cancun.vm import Evm, EvmStruct, EvmImpl
from ethereum.cancun.vm.exceptions import ExceptionalHalt, OutOfGasError
from ethereum.cancun.vm.memory import Memory

from starkware.cairo.common.math_cmp import is_le, is_not_zero, RC_BOUND
from starkware.cairo.common.math import assert_le_felt
from starkware.cairo.common.uint256 import ALL_ONES, uint256_eq, uint256_le

const TARGET_BLOB_GAS_PER_BLOCK = 393216;
const GAS_INIT_CODE_WORD_COST = 2;
const GAS_MEMORY = 3;
const GAS_PER_BLOB = 2 ** 17;
const MIN_BLOB_GASPRICE = 1;
const BLOB_GASPRICE_UPDATE_FRACTION = 3338477;
from src.utils.uint256 import uint256_add
from src.constants import Constants

namespace GasConstants {
const GAS_JUMPDEST = 1;
const GAS_BASE = 2;
const GAS_VERY_LOW = 3;
const GAS_STORAGE_SET = 20000;
const GAS_STORAGE_UPDATE = 5000;
const GAS_STORAGE_CLEAR_REFUND = 4800;
const GAS_LOW = 5;
const GAS_MID = 8;
const GAS_HIGH = 10;
const GAS_EXPONENTIATION = 10;
const GAS_EXPONENTIATION_PER_BYTE = 50;
const GAS_MEMORY = 3;
const GAS_KECCAK256 = 30;
const GAS_KECCAK256_WORD = 6;
const GAS_COPY = 3;
const GAS_BLOCK_HASH = 20;
const GAS_LOG = 375;
const GAS_LOG_DATA = 8;
const GAS_LOG_TOPIC = 375;
const GAS_CREATE = 32000;
const GAS_CODE_DEPOSIT = 200;
const GAS_ZERO = 0;
const GAS_NEW_ACCOUNT = 25000;
const GAS_CALL_VALUE = 9000;
const GAS_CALL_STIPEND = 2300;
const GAS_SELF_DESTRUCT = 5000;
const GAS_SELF_DESTRUCT_NEW_ACCOUNT = 25000;
const GAS_ECRECOVER = 3000;
const GAS_SHA256 = 60;
const GAS_SHA256_WORD = 12;
const GAS_RIPEMD160 = 600;
const GAS_RIPEMD160_WORD = 120;
const GAS_IDENTITY = 15;
const GAS_IDENTITY_WORD = 3;
const GAS_RETURN_DATA_COPY = 3;
const GAS_FAST_STEP = 5;
const GAS_BLAKE2_PER_ROUND = 1;
const GAS_COLD_SLOAD = 2100;
const GAS_COLD_ACCOUNT_ACCESS = 2600;
const GAS_WARM_ACCESS = 100;
const GAS_INIT_CODE_WORD_COST = 2;
const GAS_BLOBHASH_OPCODE = 3;
const GAS_POINT_EVALUATION = 50000;

const TARGET_BLOB_GAS_PER_BLOCK = 393216;
const GAS_PER_BLOB = 2 ** 17;
const MIN_BLOB_GASPRICE = 1;
const BLOB_GASPRICE_UPDATE_FRACTION = 3338477;
}

struct ExtendMemory {
value: ExtendMemoryStruct*,
}

struct ExtendMemoryStruct {
cost: Uint,
expand_by: Uint,
}

struct MessageCallGasStruct {
cost: Uint,
Expand Down Expand Up @@ -75,16 +134,73 @@ func charge_gas{range_check_ptr, evm: Evm}(amount: Uint) -> ExceptionalHalt* {
return err;
}

const MAX_MEMORY_COST = 0x20000000000017f7fffffffffffd;
const MAX_MEMORY_SIZE = 2 ** 64 - 32;

// @dev: assumption: not called with size_in_bytes >= 2**64
// only used by calculate_gas_extend_memory which saturates at 2**64-32
// @dev: max output value given this saturation is MAX_MEMORY_COST
func calculate_memory_gas_cost{range_check_ptr}(size_in_bytes: Uint) -> Uint {
let size = ceil32(size_in_bytes);
let (size_in_words, _) = divmod(size.value, 32);
let linear_cost = size_in_words * GAS_MEMORY;
let linear_cost = size_in_words * GasConstants.GAS_MEMORY;
let quadratic_cost = size_in_words * size_in_words;
let (quadratic_cost, _) = divmod(quadratic_cost, 512);
let total_gas_cost = Uint(linear_cost + quadratic_cost);
return total_gas_cost;
}

// @dev: saturates extensions at (MAX_MEMORY_SIZE, MAX_MEMORY_COST)
func calculate_gas_extend_memory{range_check_ptr}(
memory: Memory, extensions: ListTupleU256U256
) -> ExtendMemory {
alloc_locals;
let max_memory_offset = _max_offset(Uint(memory.value.len), extensions, 0);
let size_to_extend = Uint(max_memory_offset.value - memory.value.len);
let already_paid = calculate_memory_gas_cost(Uint(memory.value.len));
let total_cost = calculate_memory_gas_cost(Uint(max_memory_offset.value));
let to_be_paid = Uint(total_cost.value - already_paid.value);
tempvar res = ExtendMemory(new ExtendMemoryStruct(to_be_paid, size_to_extend));
return res;
}

// @dev saturates at 2**64 (Uint size)
func _max_offset{range_check_ptr}(
before_size: Uint, extensions: ListTupleU256U256, idx: felt
) -> Uint {
alloc_locals;
let extensions_len = extensions.value.len - idx;
if (extensions_len == 0) {
return before_size;
}

let offset = extensions.value.data[idx].value.val_1;
let size = extensions.value.data[idx].value.val_2;
let (is_zero) = uint256_eq([size.value], U256Struct(0, 0));
if (is_zero != 0) {
return _max_offset(before_size, extensions, idx + 1);
}

let (max_offset, carry) = uint256_add([offset.value], [size.value]);
if (carry != 0) {
tempvar res = Uint(MAX_MEMORY_SIZE);
return _max_offset(res, extensions, idx + 1);
}
let (is_saturated) = uint256_le(U256Struct(MAX_MEMORY_SIZE + 1, 0), max_offset);
if (is_saturated != 0) {
tempvar res = Uint(MAX_MEMORY_SIZE);
return _max_offset(res, extensions, idx + 1);
}

let after_size = ceil32(Uint(max_offset.low));
let is_smaller = is_le(after_size.value, before_size.value);
if (is_smaller == 1) {
return _max_offset(before_size, extensions, idx + 1);
}

return _max_offset(after_size, extensions, idx + 1);
}

func calculate_message_call_gas{range_check_ptr}(
value: U256, gas: Uint, gas_left: Uint, memory_cost: Uint, extra_gas: Uint, call_stipend: Uint
) -> MessageCallGas {
Expand Down Expand Up @@ -123,26 +239,27 @@ func max_message_call_gas{range_check_ptr}(gas: Uint) -> Uint {
func init_code_cost{range_check_ptr}(init_code_length: Uint) -> Uint {
let length = ceil32(init_code_length);
let (words, _) = divmod(length.value, 32);
let cost = Uint(GAS_INIT_CODE_WORD_COST * words);
let cost = Uint(GasConstants.GAS_INIT_CODE_WORD_COST * words);
return cost;
}

func calculate_excess_blob_gas{range_check_ptr}(parent_header: Header) -> U64 {
let parent_blob_gas = parent_header.value.excess_blob_gas.value +
parent_header.value.blob_gas_used.value;
let cond = is_le(parent_blob_gas, TARGET_BLOB_GAS_PER_BLOCK - 1);
let cond = is_le(parent_blob_gas, GasConstants.TARGET_BLOB_GAS_PER_BLOCK - 1);
if (cond == 1) {
let excess_blob_gas = U64(0);
return excess_blob_gas;
}
let excess_blob_gas = U64(parent_blob_gas - TARGET_BLOB_GAS_PER_BLOCK);
let excess_blob_gas = U64(parent_blob_gas - GasConstants.TARGET_BLOB_GAS_PER_BLOCK);
return excess_blob_gas;
}

func calculate_total_blob_gas{range_check_ptr}(tx: Transaction) -> Uint {
if (tx.value.blob_transaction.value != 0) {
let total_blob_gas = Uint(
GAS_PER_BLOB * tx.value.blob_transaction.value.blob_versioned_hashes.value.len
GasConstants.GAS_PER_BLOB *
tx.value.blob_transaction.value.blob_versioned_hashes.value.len,
);
return total_blob_gas;
}
Expand All @@ -152,7 +269,9 @@ func calculate_total_blob_gas{range_check_ptr}(tx: Transaction) -> Uint {

func calculate_blob_gas_price{range_check_ptr}(excess_blob_gas: U64) -> Uint {
let blob_gas_price = taylor_exponential(
Uint(MIN_BLOB_GASPRICE), Uint(excess_blob_gas.value), Uint(BLOB_GASPRICE_UPDATE_FRACTION)
Uint(GasConstants.MIN_BLOB_GASPRICE),
Uint(excess_blob_gas.value),
Uint(GasConstants.BLOB_GASPRICE_UPDATE_FRACTION),
);
return blob_gas_price;
}
Expand All @@ -164,54 +283,3 @@ func calculate_data_fee{range_check_ptr}(excess_blob_gas: U64, tx: Transaction)
let data_fee = Uint(total_blob_gas.value * blob_gas_price.value);
return data_fee;
}

namespace GasConstants {
const GAS_JUMPDEST = 1;
const GAS_BASE = 2;
const GAS_VERY_LOW = 3;
const GAS_STORAGE_SET = 20000;
const GAS_STORAGE_UPDATE = 5000;
const GAS_STORAGE_CLEAR_REFUND = 4800;
const GAS_LOW = 5;
const GAS_MID = 8;
const GAS_HIGH = 10;
const GAS_EXPONENTIATION = 10;
const GAS_EXPONENTIATION_PER_BYTE = 50;
const GAS_MEMORY = 3;
const GAS_KECCAK256 = 30;
const GAS_KECCAK256_WORD = 6;
const GAS_COPY = 3;
const GAS_BLOCK_HASH = 20;
const GAS_LOG = 375;
const GAS_LOG_DATA = 8;
const GAS_LOG_TOPIC = 375;
const GAS_CREATE = 32000;
const GAS_CODE_DEPOSIT = 200;
const GAS_ZERO = 0;
const GAS_NEW_ACCOUNT = 25000;
const GAS_CALL_VALUE = 9000;
const GAS_CALL_STIPEND = 2300;
const GAS_SELF_DESTRUCT = 5000;
const GAS_SELF_DESTRUCT_NEW_ACCOUNT = 25000;
const GAS_ECRECOVER = 3000;
const GAS_SHA256 = 60;
const GAS_SHA256_WORD = 12;
const GAS_RIPEMD160 = 600;
const GAS_RIPEMD160_WORD = 120;
const GAS_IDENTITY = 15;
const GAS_IDENTITY_WORD = 3;
const GAS_RETURN_DATA_COPY = 3;
const GAS_FAST_STEP = 5;
const GAS_BLAKE2_PER_ROUND = 1;
const GAS_COLD_SLOAD = 2100;
const GAS_COLD_ACCOUNT_ACCESS = 2600;
const GAS_WARM_ACCESS = 100;
const GAS_INIT_CODE_WORD_COST = 2;
const GAS_BLOBHASH_OPCODE = 3;
const GAS_POINT_EVALUATION = 50000;

const TARGET_BLOB_GAS_PER_BLOCK = 393216;
const GAS_PER_BLOB = 2 ** 17;
const MIN_BLOB_GASPRICE = 1;
const BLOB_GASPRICE_UPDATE_FRACTION = 3338477;
}
20 changes: 20 additions & 0 deletions cairo/ethereum_types/others.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,26 @@

// None values are just null pointers generally speaking (i.e. cast(my_var, felt) == 0)
// but we need to explicitly define None to be able to serialize/deserialize None
from ethereum_types.numeric import U256

struct None {
value: felt*,
}

struct TupleU256U256Struct {
val_1: U256,
val_2: U256,
}

struct TupleU256U256 {
value: TupleU256U256Struct*,
}

struct ListTupleU256U256Struct {
data: TupleU256U256*,
len: felt,
}

struct ListTupleU256U256 {
value: ListTupleU256U256Struct*,
}
1 change: 1 addition & 0 deletions cairo/src/constants.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ from src.gas import Gas
// @notice This file contains global constants.
namespace Constants {
const UINT128_MAX = 0xffffffffffffffffffffffffffffffff;
const UINT64_MAX = 2 ** 64 - 1;

// STACK
const STACK_MAX_DEPTH = 1024;
Expand Down
1 change: 1 addition & 0 deletions cairo/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ def pytest_addoption(parser):
max_examples=30,
phases=[Phase.explicit, Phase.reuse, Phase.generate, Phase.target],
derandomize=True,
print_blob=True,
)
settings.register_profile(
"debug",
Expand Down
29 changes: 28 additions & 1 deletion cairo/tests/ethereum/cancun/vm/test_gas.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,36 @@
from typing import List, Tuple

import pytest
from ethereum_types.numeric import U256, Uint
from hypothesis import assume, given
from hypothesis import strategies as st
from hypothesis.strategies import composite

from ethereum.cancun.blocks import Header
from ethereum.cancun.transactions import BlobTransaction
from ethereum.cancun.vm.exceptions import ExceptionalHalt
from ethereum.cancun.vm.gas import (
GAS_CALL_STIPEND,
calculate_blob_gas_price,
calculate_data_fee,
calculate_excess_blob_gas,
calculate_gas_extend_memory,
calculate_memory_gas_cost,
calculate_message_call_gas,
calculate_total_blob_gas,
charge_gas,
init_code_cost,
max_message_call_gas,
)
from tests.utils.args_gen import Evm
from tests.utils.args_gen import Evm, Memory


@composite
def extensions_strategy(draw):
offset = draw(st.integers(min_value=0, max_value=2**64 - 32))
max_size = (2**64 - 32) - offset
size = draw(st.integers(min_value=0, max_value=max_size))
return (U256(offset), U256(size))


class TestGas:
Expand All @@ -39,6 +52,20 @@ def test_calculate_memory_gas_cost(self, cairo_run, size_in_bytes: Uint):
"calculate_memory_gas_cost", size_in_bytes
)

# We saturate the memory (offsets + size) at 2**64-32
@given(memory=..., extensions=st.lists(extensions_strategy()))
def test_calculate_gas_extend_memory(
self, cairo_run, memory: Memory, extensions: List[Tuple[U256, U256]]
):
try:
cairo_result = cairo_run("calculate_gas_extend_memory", memory, extensions)
except ExceptionalHalt as cairo_error:
with pytest.raises(type(cairo_error)):
calculate_gas_extend_memory(memory, extensions)
return

assert calculate_gas_extend_memory(memory, extensions) == cairo_result

@given(
value=...,
gas=...,
Expand Down
Loading
Loading