Skip to content
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
97 changes: 15 additions & 82 deletions test/support/blockchain_test_runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,13 @@ defmodule EEVM.TestSupport.BlockchainTestRunner do
alias EEVM.Context.{Block, Transaction}
alias EEVM.Database
alias EEVM.Database.InMemory
alias EEVM.Handler.Execution
alias EEVM.HardforkConfig
alias EEVM.StateRoot
alias EEVM.SystemContracts.{BeaconRoots, BlockHashes}
alias EEVM.TestSupport.BlockchainHeaderValidator, as: HeaderValidator
alias EEVM.TestSupport.BlockchainTestFixture.{Account, Case, TransactionFields, Withdrawal}
alias EEVM.TestSupport.BlockchainTestFixture.Block, as: FixtureBlock
alias EEVM.Transaction.{IntrinsicGas, Validator}
alias EEVM.TestSupport.TxExecutor

@min_blob_base_fee 1

Expand Down Expand Up @@ -222,95 +221,29 @@ defmodule EEVM.TestSupport.BlockchainTestRunner do
defp tx_executor(%Config{} = config, header) do
fn %Transaction{} = tx, %Block{} = block_ctx, db ->
block_ctx_with_blob = put_blob_base_fee(block_ctx, header)
execute_transaction(tx, db, block_ctx_with_blob, config)
end
end

# Until full EIP-4844 fake-exponential blob-fee math lands, set blob_base_fee
# to its post-Cancun floor of 1 wei whenever the header carries blob fields,
# and leave it at 0 for older blocks. Non-blob fixtures don't read this.
defp put_blob_base_fee(%Block{} = block_ctx, header) do
blob_base_fee = if header.excess_blob_gas == nil, do: 0, else: @min_blob_base_fee
%{block_ctx | blob_base_fee: blob_base_fee}
end

defp execute_transaction(%Transaction{} = tx, db, %Block{} = block_ctx, %Config{} = config) do
case Validator.validate(tx, db, block_ctx, config.hardfork) do
:ok ->
db_after_nonce = Database.increment_nonce(db, tx.origin)

with {:ok, machine} <- run_interpreter(tx, db_after_nonce, block_ctx, config),
{:ok, finalized_db} <- finalize_fees(db_after_nonce, machine, tx, block_ctx) do
failed = failed_status?(machine.status)
gas_used = tx.gas_limit - machine.frame.gas

case TxExecutor.execute(tx, db, block_ctx_with_blob, config) do
{:ok, %{db: db, machine: machine, gas_used: gas_used, failed?: failed?}} ->
{:ok,
%{
status: if(failed, do: 0, else: 1),
status: if(failed?, do: 0, else: 1),
gas_used: gas_used,
logs: if(failed, do: [], else: machine.substate.logs),
db: finalized_db
logs: if(failed?, do: [], else: machine.substate.logs),
db: db
}}
end

{:error, reason} ->
{:error, {:validation, reason}}
end
end

defp run_interpreter(%Transaction{} = tx, db, %Block{} = block_ctx, %Config{} = config) do
intrinsic_gas = IntrinsicGas.calculate(tx)
execution_gas = max(tx.gas_limit - intrinsic_gas, 0)
{machine, _new_address} = Execution.run_top_level(tx, db, block_ctx, config, execution_gas)
{:ok, machine}
end

defp finalize_fees(db_after_nonce, machine, tx, %Block{} = block_ctx) do
failed = failed_status?(machine.status)
settled = if failed, do: db_after_nonce, else: machine.db
gas_used = tx.gas_limit - machine.frame.gas
apply_fees(settled, tx, block_ctx, gas_used)
end

defp apply_fees(db, tx, block_ctx, gas_used) do
sender_fee = gas_used * effective_gas_price(tx, block_ctx)
miner_reward = gas_used * miner_reward_per_gas(tx, block_ctx)

with {:ok, debited} <- debit_balance(db, tx.origin, sender_fee) do
{:ok, credit_balance(debited, block_ctx.coinbase, miner_reward)}
{:error, _} = err ->
err
end
end
end

defp effective_gas_price(%Transaction{type: type} = tx, %Block{} = block_ctx)
when type in [:eip1559, :eip4844] do
min(tx.max_fee_per_gas, block_ctx.basefee + tx.max_priority_fee_per_gas)
end

defp effective_gas_price(%Transaction{} = tx, _block_ctx), do: tx.gasprice

defp miner_reward_per_gas(%Transaction{type: type} = tx, %Block{} = block_ctx)
when type in [:eip1559, :eip4844] do
max(effective_gas_price(tx, block_ctx) - block_ctx.basefee, 0)
end

defp miner_reward_per_gas(%Transaction{} = tx, %Block{} = block_ctx) do
max(tx.gasprice - block_ctx.basefee, 0)
end

defp failed_status?(status) when status in [:reverted, :invalid, :out_of_gas], do: true
defp failed_status?({:error, _}), do: true
defp failed_status?(_), do: false

defp debit_balance(db, _addr, 0), do: {:ok, db}

defp debit_balance(db, addr, amount) do
current = Database.get_balance(db, addr)

if current < amount do
{:error, :insufficient_balance}
else
{:ok, Database.set_balance(db, addr, current - amount)}
end
# Until full EIP-4844 fake-exponential blob-fee math lands, set blob_base_fee
# to its post-Cancun floor of 1 wei whenever the header carries blob fields,
# and leave it at 0 for older blocks. Non-blob fixtures don't read this.
defp put_blob_base_fee(%Block{} = block_ctx, header) do
blob_base_fee = if header.excess_blob_gas == nil, do: 0, else: @min_blob_base_fee
%{block_ctx | blob_base_fee: blob_base_fee}
end

defp credit_balance(db, _addr, 0), do: db
Expand Down
131 changes: 15 additions & 116 deletions test/support/state_test_runner.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
defmodule EEVM.TestSupport.StateTestRunner do
@moduledoc "Executes a single StateTest case and validates the post-state root and logs hash."

alias EEVM.{Config, Database, StateRoot}
alias EEVM.Context.Contract
alias EEVM.{Config, StateRoot}
alias EEVM.Database.InMemory
alias EEVM.TestSupport.StateTestFixture
alias EEVM.TestSupport.{StateTestFixture, TxExecutor}
alias EEVM.TestSupport.StateTestFixture.{Case, PostExpectation}
alias EEVM.Transaction.{IntrinsicGas, Validator}

@empty_logs_hash Base.decode16!(
"1DCC4DE8DEC75D7AAB85B567B6CCD41AD312451B948A7413F0A142FD40D49347",
Expand All @@ -17,99 +15,23 @@ defmodule EEVM.TestSupport.StateTestRunner do
db = InMemory.new(accounts: state_test.pre_accounts, storage: state_test.pre_storage)
tx = StateTestFixture.build_transaction(state_test, expectation)
block = StateTestFixture.block(state_test.env)

case execute_transaction(tx, db, block, expectation.hardfork) do
{:ok, result} -> validate_success(result, expectation)
{:error, reason} -> validate_failure(reason, expectation)
end
end

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

with {:ok, db_for_execution} <- apply_top_level_value(db_after_nonce, tx),
{:ok, machine} <- run_top_level(tx, db_for_execution, block, hardfork),
{:ok, finalized_db} <- finalize_fees(db_after_nonce, machine, tx, block) do
failed = failed_status?(machine.status)

{:ok,
%{
state_root: StateRoot.compute_state_root(finalized_db),
logs_hash: logs_hash(if(failed, do: [], else: machine.substate.logs)),
output: if(failed, do: <<>>, else: machine.frame.return_data),
status: machine.status,
gas_used: tx.gas_limit - machine.frame.gas,
db: finalized_db
}}
end
config = Config.new(expectation.hardfork)

case TxExecutor.execute(tx, db, block, config) do
{:ok, %{db: db, machine: machine, failed?: failed?}} ->
validate_success(
%{
state_root: StateRoot.compute_state_root(db),
logs_hash: logs_hash(if(failed?, do: [], else: machine.substate.logs))
},
expectation
)

{:error, reason} ->
{:error, {:validation, reason}}
end
end

defp run_top_level(%{to: nil}, _db, _block, _hardfork), do: {:error, :unsupported_creation}

defp run_top_level(tx, db, block, hardfork) do
intrinsic_gas = IntrinsicGas.calculate(tx)
execution_gas = tx.gas_limit - intrinsic_gas
code = Database.get_code(db, tx.to)

machine =
EEVM.execute(code,
gas: execution_gas,
db: db,
tx: tx,
block: block,
contract:
Contract.new(address: tx.to, caller: tx.origin, callvalue: tx.value, calldata: tx.data),
config: Config.new(hardfork)
)

{:ok, machine}
end

defp apply_top_level_value(db, %{to: nil}), do: {:ok, db}
defp apply_top_level_value(db, %{value: 0}), do: {:ok, db}

defp apply_top_level_value(db, %{origin: origin, to: to, value: value}),
do: Database.transfer(db, origin, to, value)

defp settle_state(db_after_nonce, _machine, true), do: db_after_nonce
defp settle_state(_db_after_nonce, machine, false), do: machine.db

defp finalize_fees(db_after_nonce, machine, tx, block) do
failed = failed_status?(machine.status)
settled_db = settle_state(db_after_nonce, machine, failed)
gas_used = tx.gas_limit - machine.frame.gas
apply_fees(settled_db, tx, block, gas_used)
end

defp apply_fees(db, tx, block, gas_used) do
sender_fee = gas_used * effective_gas_price(tx, block)
miner_reward = gas_used * miner_reward_per_gas(tx, block)

with {:ok, debited_db} <- debit_balance(db, tx.origin, sender_fee) do
{:ok, credit_balance(debited_db, block.coinbase, miner_reward)}
validate_failure(reason, expectation)
end
end

defp effective_gas_price(%{type: type} = tx, block) when type in [:eip1559, :eip4844] do
min(tx.max_fee_per_gas, block.basefee + tx.max_priority_fee_per_gas)
end

defp effective_gas_price(tx, _block), do: tx.gasprice

defp miner_reward_per_gas(%{type: type} = tx, block) when type in [:eip1559, :eip4844] do
max(effective_gas_price(tx, block) - block.basefee, 0)
end

defp miner_reward_per_gas(tx, block) do
max(tx.gasprice - block.basefee, 0)
end

defp validate_success(_result, %PostExpectation{expect_exception: exception})
when is_binary(exception) and exception != "" do
{:error, {:expected_exception, exception}}
Expand All @@ -128,18 +50,13 @@ defmodule EEVM.TestSupport.StateTestRunner do
end
end

defp validate_failure(reason, %PostExpectation{expect_exception: exception})
defp validate_failure(_reason, %PostExpectation{expect_exception: exception})
when is_binary(exception) and exception != "" do
_ = reason
:ok
end

defp validate_failure(reason, _expectation), do: {:error, reason}

defp failed_status?(status) when status in [:reverted, :invalid, :out_of_gas], do: true
defp failed_status?({:error, _}), do: true
defp failed_status?(_), do: false

defp logs_hash([]), do: @empty_logs_hash

defp logs_hash(logs) do
Expand All @@ -160,22 +77,4 @@ defmodule EEVM.TestSupport.StateTestRunner do
true -> binary_part(encoded, byte_size(encoded) - bytes, bytes)
end
end

defp debit_balance(db, _address, 0), do: {:ok, db}

defp debit_balance(db, address, amount) do
current_balance = Database.get_balance(db, address)

if current_balance < amount do
{:error, :insufficient_balance}
else
{:ok, Database.set_balance(db, address, current_balance - amount)}
end
end

defp credit_balance(db, _address, 0), do: db

defp credit_balance(db, address, amount) do
Database.set_balance(db, address, Database.get_balance(db, address) + amount)
end
end
Loading
Loading