Skip to content
Open
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
16 changes: 15 additions & 1 deletion lib/eevm/executor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ defmodule EEVM.Executor do
alias EEVM.{HardforkConfig, MachineState}
alias EEVM.Database
alias EEVM.Gas.Static
alias EEVM.Transaction.IntrinsicGas
alias EEVM.Database
alias EEVM.Gas.Static

Expand Down Expand Up @@ -150,7 +151,20 @@ defmodule EEVM.Executor do
if HardforkConfig.enabled?(state.config.hardfork, :eip_3529), do: 5, else: 2

effective_refund = min(state.refund, div(gas_used, refund_cap_divisor))
%{state | gas: state.gas + effective_refund, refund: 0}

refunded_gas = state.gas + effective_refund

calldata_floor_gas =
IntrinsicGas.calldata_floor_gas_cost(state.tx, state.config.hardfork)

gas_after_floor =
if calldata_floor_gas > 0 and state.tx.gas_limit > 0 do
min(refunded_gas, max(state.tx.gas_limit - calldata_floor_gas, 0))
else
refunded_gas
end

%{state | gas: gas_after_floor, refund: 0}
end

defp cleanup_touched_empty_accounts(%MachineState{status: :stopped} = state) do
Expand Down
21 changes: 21 additions & 0 deletions lib/eevm/gas/intrinsic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ defmodule EEVM.Gas.Intrinsic do
- Module attributes (`@tx_data_zero_gas`) make constants auditable in one place.
"""

@tx_base_gas 21_000
# EIP-2028 (Istanbul): non-zero calldata reduced from 68 → 16 gas.
@tx_data_zero_gas 4
@tx_data_non_zero_gas 16
@tx_calldata_floor_token_cost 10

@doc """
Returns the intrinsic gas cost for zero-byte calldata.
Expand All @@ -46,6 +48,10 @@ defmodule EEVM.Gas.Intrinsic do
@spec tx_data_non_zero_gas() :: non_neg_integer()
def tx_data_non_zero_gas, do: @tx_data_non_zero_gas

@doc "Returns the Prague calldata floor gas charged per token."
@spec tx_calldata_floor_token_cost() :: non_neg_integer()
def tx_calldata_floor_token_cost, do: @tx_calldata_floor_token_cost

@doc """
Computes the total intrinsic calldata gas cost for the given binary.

Expand All @@ -68,4 +74,19 @@ defmodule EEVM.Gas.Intrinsic do
acc -> acc + @tx_data_non_zero_gas
end
end

@doc "Counts EIP-7623 calldata floor tokens (zero=1, non-zero=4)."
@spec calldata_floor_tokens(binary()) :: non_neg_integer()
def calldata_floor_tokens(data) when is_binary(data) do
for <<byte <- data>>, reduce: 0 do
acc when byte == 0 -> acc + 1
acc -> acc + 4
end
end

@doc "Computes the Prague calldata floor gas cost for the given calldata."
@spec calldata_floor_cost(binary()) :: non_neg_integer()
def calldata_floor_cost(data) when is_binary(data) do
@tx_base_gas + @tx_calldata_floor_token_cost * calldata_floor_tokens(data)
end
end
5 changes: 3 additions & 2 deletions lib/eevm/hardfork_config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule EEVM.HardforkConfig do
| `:paris` | The Merge (PREVRANDAO replaces DIFFICULTY) |
| `:shanghai` | EIP-3651 (COINBASE pre-warm), EIP-3855 (PUSH0), EIP-3860 (initcode limit) |
| `:cancun` | EIP-1153 (TLOAD/TSTORE), EIP-4844 (blobs), EIP-5656 (MCOPY), EIP-6780 (SELFDESTRUCT) |
| `:prague` | EIP-7702 (set-code) — in progress |
| `:prague` | EIP-7623 (calldata floor), EIP-7702 (set-code) |

## Elixir Learning Notes

Expand Down Expand Up @@ -105,7 +105,8 @@ defmodule EEVM.HardforkConfig do
# EIP-5656 (Cancun): MCOPY opcode
eip_5656: :cancun,
# EIP-6780 (Cancun): SELFDESTRUCT only deletes if created this tx
eip_6780: :cancun
eip_6780: :cancun,
eip_7623: :prague
}

@doc """
Expand Down
20 changes: 14 additions & 6 deletions lib/eevm/transaction/intrinsic_gas.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ defmodule EEVM.Transaction.IntrinsicGas do
"""

alias EEVM.Context.Transaction
alias EEVM.Gas.Intrinsic, as: Intrinsic
alias EEVM.HardforkConfig

@tx_base_cost 21_000
@tx_data_zero_cost 4
@tx_data_non_zero_cost 16
@tx_create_cost 32_000
@initcode_word_cost 2
@access_list_address_cost 2_400
Expand All @@ -29,17 +29,25 @@ defmodule EEVM.Transaction.IntrinsicGas do
@spec calculate(Transaction.t()) :: non_neg_integer()
def calculate(%Transaction{} = tx) do
@tx_base_cost +
calldata_cost(tx.data) +
Intrinsic.calldata_cost(tx.data) +
creation_cost(tx) +
access_list_cost(tx.access_list)
end

defp calldata_cost(data) when is_binary(data) do
for <<byte <- data>>, reduce: 0 do
acc -> acc + if(byte == 0, do: @tx_data_zero_cost, else: @tx_data_non_zero_cost)
@spec calldata_floor_gas_cost(Transaction.t(), HardforkConfig.t()) :: non_neg_integer()
def calldata_floor_gas_cost(%Transaction{} = tx, %HardforkConfig{} = hardfork) do
if HardforkConfig.enabled?(hardfork, :eip_7623) do
Intrinsic.calldata_floor_cost(tx.data)
else
0
end
end

@spec minimum_gas_limit(Transaction.t(), HardforkConfig.t()) :: non_neg_integer()
def minimum_gas_limit(%Transaction{} = tx, %HardforkConfig{} = hardfork) do
max(calculate(tx), calldata_floor_gas_cost(tx, hardfork))
end

defp creation_cost(%Transaction{to: nil, data: initcode}) do
@tx_create_cost + @initcode_word_cost * word_count(initcode)
end
Expand Down
18 changes: 15 additions & 3 deletions lib/eevm/transaction/validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,26 @@ defmodule EEVM.Transaction.Validator do

alias EEVM.Context.{Block, Transaction}
alias EEVM.Database
alias EEVM.HardforkConfig
alias EEVM.Transaction.IntrinsicGas

@blob_gas_per_blob 131_072
@max_initcode_size 49_152

@spec validate(Transaction.t(), Database.t(), Block.t()) :: :ok | {:error, atom()}
def validate(%Transaction{} = tx, %Database{} = db, %Block{} = block) do
with :ok <- validate_intrinsic_gas(tx),
validate(tx, db, block, HardforkConfig.default())
end

@spec validate(Transaction.t(), Database.t(), Block.t(), HardforkConfig.t()) ::
:ok | {:error, atom()}
def validate(
%Transaction{} = tx,
%Database{} = db,
%Block{} = block,
%HardforkConfig{} = hardfork
) do
with :ok <- validate_intrinsic_gas(tx, hardfork),
:ok <- validate_gas_limit_vs_block(tx, block),
:ok <- validate_nonce(tx, db),
:ok <- validate_eip1559_fees(tx, block),
Expand All @@ -35,8 +47,8 @@ defmodule EEVM.Transaction.Validator do
end
end

defp validate_intrinsic_gas(%Transaction{} = tx) do
if tx.gas_limit >= IntrinsicGas.calculate(tx),
defp validate_intrinsic_gas(%Transaction{} = tx, %HardforkConfig{} = hardfork) do
if tx.gas_limit >= IntrinsicGas.minimum_gas_limit(tx, hardfork),
do: :ok,
else: {:error, :intrinsic_gas_too_low}
end
Expand Down
8 changes: 8 additions & 0 deletions test/gas/eip_2028_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ defmodule EEVM.Gas.EIP2028Test do
test "module constants are 4 gas for zero byte and 16 gas for non-zero byte" do
assert Intrinsic.tx_data_zero_gas() == 4
assert Intrinsic.tx_data_non_zero_gas() == 16
assert Intrinsic.tx_calldata_floor_token_cost() == 10
end

test "all-zero calldata charges 4 gas per byte" do
Expand All @@ -30,5 +31,12 @@ defmodule EEVM.Gas.EIP2028Test do
test "empty calldata has zero intrinsic data cost" do
assert Intrinsic.calldata_cost(<<>>) == 0
end

test "Prague calldata floor token pricing weights zero bytes as 1 and non-zero bytes as 4" do
data = <<0x00, 0xFF, 0x00>>

assert Intrinsic.calldata_floor_tokens(data) == 6
assert Intrinsic.calldata_floor_cost(data) == 21_000 + 10 * 6
end
end
end
5 changes: 5 additions & 0 deletions test/hardfork_config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ defmodule EEVM.HardforkConfigTest do
refute HardforkConfig.enabled?(HardforkConfig.new(:shanghai), :eip_6780)
end

test "EIP-7623 (calldata floor pricing) is enabled from Prague onward" do
assert HardforkConfig.enabled?(HardforkConfig.new(:prague), :eip_7623)
refute HardforkConfig.enabled?(HardforkConfig.new(:cancun), :eip_7623)
end

test "EIP-2929 (cold/warm access) is enabled from Berlin onward" do
assert HardforkConfig.enabled?(HardforkConfig.new(:berlin), :eip_2929)
assert HardforkConfig.enabled?(HardforkConfig.new(:london), :eip_2929)
Expand Down
30 changes: 30 additions & 0 deletions test/opcodes/gas_refund_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ defmodule EEVM.Opcodes.GasRefundTest do
alias EEVM.{CallFrame, Executor, MachineState, Memory, Stack}
alias EEVM.Context.Contract
alias EEVM.Gas.Static
alias EEVM.Context.Transaction
alias EEVM.Transaction.IntrinsicGas

test "refund counter defaults to 0" do
result = EEVM.execute(<<0x00>>)
Expand Down Expand Up @@ -42,6 +44,34 @@ defmodule EEVM.Opcodes.GasRefundTest do
assert result.refund == 0
end

test "Prague floors final charged gas after refunds to calldata floor cost" do
tx = Transaction.new(gas_limit: 22_000, to: 0xCAFE, data: <<0x00>>)
initial_execution_gas = tx.gas_limit - IntrinsicGas.calculate(tx)

prague_result =
EEVM.execute(<<0x60, 0x01, 0x50, 0x00>>,
gas: initial_execution_gas,
tx: tx,
hardfork: :prague,
refund: 100
)

cancun_result =
EEVM.execute(<<0x60, 0x01, 0x50, 0x00>>,
gas: initial_execution_gas,
tx: tx,
hardfork: :cancun,
refund: 100
)

assert prague_result.status == :stopped
assert prague_result.refund == 0
assert tx.gas_limit - prague_result.gas == 21_010

assert cancun_result.status == :stopped
assert tx.gas_limit - cancun_result.gas == 21_008
end

test "top-level revert resets refund to 0" do
result = EEVM.execute(<<0x60, 0x00, 0x60, 0x00, 0xFD>>, refund: 25)

Expand Down
2 changes: 1 addition & 1 deletion test/support/state_test_runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ defmodule EEVM.TestSupport.StateTestRunner do
end

defp execute_transaction(tx, db, block, hardfork) do
case Validator.validate(tx, db, block) do
case Validator.validate(tx, db, block, Config.new(hardfork).hardfork) do
:ok ->
db_after_nonce = Database.increment_nonce(db, tx.origin)

Expand Down
15 changes: 15 additions & 0 deletions test/transaction/intrinsic_gas_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,19 @@ defmodule EEVM.Transaction.IntrinsicGasTest do
21_000 + 32_000 + 33 * 16 + 2 * 2
end
end

describe "minimum_gas_limit/2" do
test "keeps the standard intrinsic gas formula before Prague" do
tx = Transaction.new(to: 0xCAFE, data: <<0x00>>)

assert IntrinsicGas.minimum_gas_limit(tx, EEVM.HardforkConfig.new(:cancun)) == 21_004
end

test "uses calldata floor pricing from Prague onward" do
tx = Transaction.new(to: 0xCAFE, data: <<0x00, 0x01>>)

assert IntrinsicGas.calldata_floor_gas_cost(tx, EEVM.HardforkConfig.new(:prague)) == 21_050
assert IntrinsicGas.minimum_gas_limit(tx, EEVM.HardforkConfig.new(:prague)) == 21_050
end
end
end
15 changes: 15 additions & 0 deletions test/transaction/validator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule EEVM.Transaction.ValidatorTest do

alias EEVM.Context.{Block, Transaction}
alias EEVM.Database.InMemory
alias EEVM.HardforkConfig
alias EEVM.Transaction.Validator

@origin 0xA11CE
Expand All @@ -23,6 +24,20 @@ defmodule EEVM.Transaction.ValidatorTest do
Validator.validate(tx, db_for_sender(), valid_block())
end

test "Prague enforces calldata floor gas for validity" do
tx = valid_tx(data: <<0x00>>, gas_limit: 21_004)

assert {:error, :intrinsic_gas_too_low} =
Validator.validate(tx, db_for_sender(), valid_block(), HardforkConfig.new(:prague))
end

test "pre-Prague validation only checks standard intrinsic gas" do
tx = valid_tx(data: <<0x00>>, gas_limit: 21_004)

assert :ok =
Validator.validate(tx, db_for_sender(), valid_block(), HardforkConfig.new(:cancun))
end

test "returns gas limit exceeds block" do
tx = valid_tx(gas_limit: 30_000)
block = valid_block(gaslimit: 29_999)
Expand Down
Loading