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
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ Test fixtures for use by clients are available for each release on the [Github r

### 🧪 Test Cases

- ✨ Add test cases for eip7778 ([#2026](https://github.com/ethereum/execution-specs/pull/2026)).
- ✨ Add missing fuzzy-compute benchmark configurations for `KECCAK256`, `CODECOPY`, `CALLDATACOPY`, `RETURNDATACOPY`, `MLOAD`, `MSTORE`, `MSTORE8`, `MCOPY`, `LOG*`, `CALLDATASIZE`, `CALLDATALOAD`, and `RETURNDATASIZE` opcodes ([#1956](https://github.com/ethereum/execution-specs/pull/1956)).
- ✨ Add precompile benchmark configurations for `ecPairing`, `blake2f`, `BLS12_G1_MSM`, `BLS12_G2_MSM` and `BLS12_PAIRING` to unblock repricing analysis ([#2003](https://github.com/ethereum/execution-specs/pull/2003)).
- 🔀 Relabel `@pytest.mark.repricing` markers in benchmark tests to reflect configurations requested for gas repricing analysis ([#1971](https://github.com/ethereum/execution-specs/pull/1971)).
Expand Down
9 changes: 9 additions & 0 deletions packages/testing/src/execution_testing/specs/blockchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,7 @@ class BlockchainTest(BaseTest):
genesis_environment: Environment = Field(default_factory=Environment)
chain_id: int = 1
exclude_full_post_state_in_output: bool = False
expected_gas_used: int | None = None
"""
Exclude the post state from the fixture output. In this case, the state
verification is only performed based on the state root.
Expand Down Expand Up @@ -656,6 +657,14 @@ def generate_block_data(
f"Verification of block {int(env.number)} failed"
) from e

if last_block and self.expected_gas_used is not None:
gas_used = int(transition_tool_output.result.gas_used)
assert gas_used == self.expected_gas_used, (
f"gas_used ({gas_used}) does not match expected_gas_used "
f"({self.expected_gas_used})"
f", difference: {gas_used - self.expected_gas_used}"
)

if last_block and self._operation_mode == OpMode.BENCHMARKING:
expected_benchmark_gas_used = self.expected_benchmark_gas_used
assert expected_benchmark_gas_used is not None, (
Expand Down
10 changes: 10 additions & 0 deletions packages/testing/src/execution_testing/specs/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,16 @@ def verify_transaction_receipt(
expected_value=expected_receipt.gas_used,
actual_value=actual_receipt.gas_used,
)
if (
expected_receipt.gas_spent is not None
and actual_receipt.gas_spent != expected_receipt.gas_spent
):
raise TransactionReceiptMismatchError(
index=transaction_index,
field_name="gas_spent",
expected_value=expected_receipt.gas_spent,
actual_value=actual_receipt.gas_spent,
)
# TODO: Add more fields as needed


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ def strip_extra_fields(cls, data: Any) -> Any:
blob_gas_used: HexNumber | None = None
blob_gas_price: HexNumber | None = None
delegations: List[ReceiptDelegation] | None = None
gas_spent: HexNumber | None = None
7 changes: 7 additions & 0 deletions src/ethereum/forks/amsterdam/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ class Receipt:
cumulative_gas_used: Uint
"""
Total gas used in the block up to and including this transaction.
This is the gas used before refunds, used for block gas accounting.
"""

bloom: Bloom
Expand All @@ -371,6 +372,12 @@ class Receipt:
payload.
"""

gas_spent: Uint
"""
Gas actually spent by the transaction after refunds. This is the amount
the user pays for and is exposed via eth_getTransactionReceipt.
"""


def encode_receipt(tx: Transaction, receipt: Receipt) -> Bytes | Receipt:
r"""
Expand Down
23 changes: 15 additions & 8 deletions src/ethereum/forks/amsterdam/fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def make_receipt(
tx: Transaction,
error: Optional[EthereumException],
cumulative_gas_used: Uint,
gas_spent: Uint,
logs: Tuple[Log, ...],
) -> Bytes | Receipt:
"""
Expand All @@ -584,7 +585,9 @@ def make_receipt(
Error in the top level frame of the transaction, if any.
cumulative_gas_used :
The total gas used so far in the block after the transaction was
executed.
executed. This is the gas used before refunds.
gas_spent :
The gas actually spent by this transaction after refunds.
logs :
The logs produced by the transaction.

Expand All @@ -599,6 +602,7 @@ def make_receipt(
cumulative_gas_used=cumulative_gas_used,
bloom=logs_bloom(logs),
logs=logs,
gas_spent=gas_spent,
)

return encode_receipt(tx, receipt)
Expand Down Expand Up @@ -1031,16 +1035,15 @@ def process_transaction(

# Transactions with less execution_gas_used than the floor pay at the
# floor cost.
tx_gas_used_after_refund = max(
tx_gas_used_after_refund, calldata_floor_gas_cost
)
tx_gas_used = max(tx_gas_used_after_refund, calldata_floor_gas_cost)
block_gas_used_in_tx = max(tx_gas_used_before_refund, calldata_floor_gas_cost)

tx_gas_left = tx.gas - tx_gas_used_after_refund
tx_gas_left = tx.gas - tx_gas_used
gas_refund_amount = tx_gas_left * effective_gas_price

# For non-1559 transactions effective_gas_price == tx.gas_price
priority_fee_per_gas = effective_gas_price - block_env.base_fee_per_gas
transaction_fee = tx_gas_used_after_refund * priority_fee_per_gas
transaction_fee = tx_gas_used * priority_fee_per_gas

# refund gas
sender_balance_after_refund = get_account(
Expand Down Expand Up @@ -1071,11 +1074,15 @@ def process_transaction(
):
destroy_account(block_env.state, block_env.coinbase)

block_output.block_gas_used += tx_gas_used_after_refund
block_output.block_gas_used += block_gas_used_in_tx
block_output.blob_gas_used += tx_blob_gas_used

receipt = make_receipt(
tx, tx_output.error, block_output.block_gas_used, tx_output.logs
tx,
tx_output.error,
block_output.block_gas_used,
tx_gas_used,
tx_output.logs,
)

receipt_key = rlp.encode(Uint(index))
Expand Down
3 changes: 3 additions & 0 deletions src/ethereum_spec_tools/evm_tools/t8n/t8n_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,9 @@ def json_encode_receipts(self) -> Any:
receipt_dict["gasUsed"] = hex(receipt.cumulative_gas_used)
receipt_dict["bloom"] = "0x" + receipt.bloom.hex()

if hasattr(receipt, "gas_spent"):
receipt_dict["gasSpent"] = hex(receipt.gas_spent)

receipts_json.append(receipt_dict)

return receipts_json
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
"""
Test cases for
[EIP-7778 Block Gas Accounting without Refunds](https://eips.ethereum.org/EIPS/eip-7778).
"""

import pytest
from execution_testing import (
Account,
Alloc,
Block,
BlockchainTestFiller,
Bytecode,
Environment,
Fork,
Hash,
Transaction,
)
from execution_testing.vm import Op

REFERENCE_SPEC_GIT_PATH = "EIPS/eip-7928.md"
REFERENCE_SPEC_VERSION = "DUMMY_VERSION"


@pytest.mark.valid_from("Amsterdam")
def test_simple_gas_accounting(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
) -> None:
"""Test gas accounting when SSTORE refunds."""
gsc = fork.gas_costs()
intrinsic_cost_calc = fork.transaction_intrinsic_cost_calculator()
max_refund_quotient = fork.max_refund_quotient()

num_slot = 10

# gas cost for SSTORE(slot, 0), resetting storage to zero
per_slot_cost = (
gsc.G_BASE + gsc.G_VERY_LOW + gsc.G_COLD_SLOAD + gsc.G_STORAGE_RESET
)
gas_used_pre_refund = intrinsic_cost_calc() + per_slot_cost * num_slot

# Calculate refund (still applied to user's balance)
refund_counter = gsc.R_STORAGE_CLEAR * num_slot
effective_refund = min(
refund_counter, gas_used_pre_refund // max_refund_quotient
)
gas_used_post_refund = gas_used_pre_refund - effective_refund

initial_fund = 10**18
sender = pre.fund_eoa(initial_fund)

storage_slots = list(range(num_slot))

code = Bytecode()
for slot in storage_slots:
code += Op.SSTORE(slot, Op.PUSH0)

contract_address = pre.deploy_contract(
code=code,
storage=dict.fromkeys(storage_slots, 1),
)

tx = Transaction(
to=contract_address,
gas_limit=fork.transaction_gas_limit_cap(),
sender=sender,
expected_receipt={
"gas_used": gas_used_pre_refund,
"gas_spent": gas_used_post_refund,
},
)

gas_price = tx.gas_price or 10
# User still receives refund - they pay POST-refund amount
expected_balance = initial_fund - gas_used_post_refund * gas_price

post = {
contract_address: Account(
storage=dict.fromkeys(storage_slots, 0),
),
sender: Account(balance=expected_balance),
}

blockchain_test(
pre=pre,
blocks=[Block(txs=[tx])],
post=post,
expected_gas_used=gas_used_pre_refund,
)


@pytest.mark.valid_from("Amsterdam")
def test_multi_block_gas_accounting(
blockchain_test: BlockchainTestFiller,
pre: Alloc,
fork: Fork,
env: Environment,
) -> None:
"""Test the maximum gas refund behavior according to EIP-3529."""
gas_costs = fork.gas_costs()
tx_gas_limit_cap = fork.transaction_gas_limit_cap()
intrinsic_cost = fork.transaction_intrinsic_cost_calculator()

# Base Operation: SSTORE(slot, 0)
iteration_cost = (
gas_costs.G_COLD_SLOAD
+ gas_costs.G_STORAGE_RESET
+ gas_costs.G_BASE
+ gas_costs.G_VERY_LOW
)

txs = []
post = {}
total_gas_used = 0

assert env.gas_limit is not None, "env.gas_limit must be set"
assert tx_gas_limit_cap is not None, "tx_gas_limit_cap must be set"
gas_remaining: int = env.gas_limit

while gas_remaining > 0:
tx_intrinsic = intrinsic_cost()
if gas_remaining < tx_intrinsic:
break

max_tx_gas = min(gas_remaining, tx_gas_limit_cap)
execution_gas = max_tx_gas - tx_intrinsic

iteration_count = min(
execution_gas // iteration_cost,
fork.max_code_size() // len(Op.SSTORE(0xFFFF, Op.PUSH0)),
)

if iteration_count == 0:
break

opcode = sum(
(Op.SSTORE(i, Op.PUSH0) for i in range(iteration_count)),
Bytecode(),
)

contract = pre.deploy_contract(
code=opcode,
storage={i: Hash(1) for i in range(iteration_count)},
)

actual_execution_gas = iteration_cost * iteration_count
tx_cost = tx_intrinsic + actual_execution_gas

total_gas_used += tx_cost
gas_remaining -= tx_cost

tx = Transaction(
to=contract,
sender=pre.fund_eoa(),
gas_limit=tx_cost,
# Transaction receipt being the cumulative gas used
expected_receipt={"gas_used": total_gas_used},
)

txs.append(tx)
post[contract] = Account(
storage={i: Hash(0) for i in range(iteration_count)}
)

blockchain_test(
pre=pre,
blocks=[Block(txs=txs)],
post=post,
expected_gas_used=total_gas_used,
)
Loading