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
11 changes: 11 additions & 0 deletions packages/testing/src/execution_testing/forks/base_fork.py
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,17 @@ def _frame_transaction_nonce_bytes(
del nonce_keys, nonce_seq
return []

@classmethod
def _frame_transaction_nonce_access_cost(
cls, nonce_keys: Sequence[int]
) -> int:
"""
Return the intrinsic cost of accessing a frame transaction's
nonce key set.
"""
del nonce_keys
return 0

# Engine API information abstract methods
@classmethod
@abstractmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,14 @@ def _frame_transaction_base_cost(
frames: Sequence[FrameGasInfo],
signatures: Sequence[FrameSignatureGasInfo],
sender: BytesConvertible | None,
nonce_keys: Sequence[int],
) -> int:
"""
Return the costs a frame transaction always pays regardless of
execution: the base cost, the per-frame cost, the verification
cost of each signature entry, and the value transfer cost of
each value-bearing frame whose explicit target differs from the
sender.
cost of each signature entry, the value transfer cost of each
value-bearing frame whose explicit target differs from the
sender, and any nonce key access cost priced by a later EIP.
"""
gas_costs = cls.gas_costs()
scheme_gas = {
Expand All @@ -166,6 +167,7 @@ def _frame_transaction_base_cost(
+ len(frames) * gas_costs.TX_PER_FRAME
+ sum(scheme_gas[int(sig.scheme)] for sig in signatures)
+ value_transfer_cost
+ cls._frame_transaction_nonce_access_cost(nonce_keys)
)

@classmethod
Expand Down Expand Up @@ -197,7 +199,7 @@ def fn(
)
)
return cls._frame_transaction_base_cost(
frame_list, signatures, sender
frame_list, signatures, sender, nonce_keys
) + (
data_length
* gas_costs.TX_DATA_TOKEN_STANDARD
Expand Down Expand Up @@ -273,7 +275,7 @@ def fn(
) -> int:
frame_list = cls._frame_list(frames)
intrinsic_cost = cls._frame_transaction_base_cost(
frame_list, signatures, sender
frame_list, signatures, sender, nonce_keys
) + sum(
calldata_gas_calculator(data=data)
for data in cls._frame_transaction_charged_bytes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
https://eips.ethereum.org/EIPS/eip-8250
"""

from dataclasses import replace
from typing import List, Mapping, Sequence

from ethereum_rlp import rlp
Expand All @@ -16,6 +17,7 @@
from execution_testing.base_types import Bytes

from ....base_fork import BaseFork
from ....gas_costs import GasCosts

NONCE_MANAGER_ADDRESS = 0x8250968C12E01A19D6F667B9B2F3B3A4D0E51CB7
NONCE_MANAGER_BYTECODE = bytes.fromhex("60006000fd")
Expand All @@ -24,6 +26,28 @@
class EIP8250(BaseFork):
"""EIP-8250 class."""

@classmethod
def gas_costs(cls) -> GasCosts:
"""
Add the keyed nonce access cost: EIP-8038's cold storage access
plus storage write, which the framework models together as
`COLD_STORAGE_WRITE`.
"""
parent = super(EIP8250, cls).gas_costs()
return replace(parent, KEYED_NONCE_ACCESS=parent.COLD_STORAGE_WRITE)

@classmethod
def _frame_transaction_nonce_access_cost(
cls, nonce_keys: Sequence[int]
) -> int:
"""
Charge the read and write of each non-zero key's nonce manager
slot. The legacy key set uses the account nonce instead.
"""
if list(nonce_keys) == [0]:
return 0
return len(nonce_keys) * cls.gas_costs().KEYED_NONCE_ACCESS

@classmethod
def _frame_transaction_nonce_bytes(
cls, nonce_keys: Sequence[int], nonce_seq: int
Expand Down
3 changes: 3 additions & 0 deletions packages/testing/src/execution_testing/forks/gas_costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ class GasCosts:
FRAME_SIGNATURE_SCHEME_ARBITRARY: int = 0
FRAME_SIGNATURE_SCHEME_SECP256K1: int = 0
FRAME_SIGNATURE_SCHEME_P256: int = 0
# Intrinsic cost of each non-zero EIP-8250 nonce key; 0 before keyed
# nonces are introduced.
KEYED_NONCE_ACCESS: int = 0

# Refunds
REFUND_STORAGE_CLEAR: int
Expand Down
6 changes: 5 additions & 1 deletion src/ethereum/forks/bogota/keyed_nonces.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@

Keyed nonce reads and writes are protocol bookkeeping: they bypass the
access lists and `SSTORE` pricing of ordinary storage access, and warm
nothing for later user-level access.
nothing for later user-level access. Each non-zero key pays for its
slot's read and write in the transaction's intrinsic cost instead (see
[`KEYED_NONCE_ACCESS`][kna]). They are still ordinary storage of the
nonce manager, so the block access list records them as such.

[EIP-8250]: https://eips.ethereum.org/EIPS/eip-8250
[kna]: ref:ethereum.forks.bogota.vm.gas.GasCosts.KEYED_NONCE_ACCESS
"""

from ethereum_types.bytes import Bytes32
Expand Down
20 changes: 16 additions & 4 deletions src/ethereum/forks/bogota/transactions/frame_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,21 +882,25 @@ def calculate_frame_transaction_intrinsic_cost(
cost of the byte fields priced as calldata — the `data` of each
frame, the `signer`, `message`, and `signature` bytes of each
signature entry, and the encoding of the nonce fields (see
[`nonce_calldata`][nc]) — the signature verification cost, and the value transfer cost
[`nonce_calldata`][nc]) — the signature verification cost, the value transfer cost
of each value-bearing frame with an explicit target other than the
sender, covering the recipient balance write and transfer log.
sender, covering the recipient balance write and transfer log, and
the keyed nonce access cost of each non-zero nonce key (see
[`KEYED_NONCE_ACCESS`][kna]), covering the read and write of its slot.
Unlike other transaction types, there is no recipient component:
target access is paid during frame execution from each frame's own
execution gas budget.

The calldata floor of [EIP-7976] counts every charged byte uniformly
and is anchored on the costs the transaction always pays regardless
of execution — the base cost, the per-frame cost, the signature
verification cost, and the value transfer cost — so it never
undercuts the transaction's own intrinsic base.
verification cost, the value transfer cost, and the keyed nonce
access cost — so it never undercuts the transaction's own intrinsic
base.

[EIP-7976]: https://eips.ethereum.org/EIPS/eip-7976
[nc]: ref:ethereum.forks.bogota.transactions.frame_transaction.nonce_calldata
[kna]: ref:ethereum.forks.bogota.vm.gas.GasCosts.KEYED_NONCE_ACCESS
""" # noqa: E501
from ..vm.gas import GasCosts
from . import IntrinsicGasCost, count_tokens_in_data
Expand Down Expand Up @@ -929,11 +933,19 @@ def calculate_frame_transaction_intrinsic_cost(
# EIP-7976 floor tokens: all charged bytes count uniformly.
floor_tokens = data_length * GasCosts.TX_DATA_TOKEN_STANDARD

# The legacy key set reads and writes the account nonce instead.
keyed_nonce_access_gas = Uint(0)
if tx.nonce_keys != LEGACY_NONCE_KEYS:
keyed_nonce_access_gas = (
ulen(tx.nonce_keys) * GasCosts.KEYED_NONCE_ACCESS
)

base_execution_gas = (
GasCosts.TX_FRAME_INTRINSIC
+ ulen(tx.frames) * GasCosts.TX_PER_FRAME
+ signature_gas
+ value_transfer_gas
+ keyed_nonce_access_gas
)

return IntrinsicGasCost(
Expand Down
11 changes: 11 additions & 0 deletions src/ethereum/forks/bogota/vm/gas.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ class GasCosts:
[ftx]: ref:ethereum.forks.bogota.transactions.frame_transaction.FrameTransaction
""" # noqa: E501

KEYED_NONCE_ACCESS: Final[ExecutionGas] = (
COLD_STORAGE_ACCESS + STORAGE_WRITE
)
"""
Intrinsic cost of each non-zero nonce key of a [`FrameTransaction`][ftx]:
the cold read and the write of its slot in the nonce manager, which the
protocol performs outside the access lists and `SSTORE` pricing.

[ftx]: ref:ethereum.forks.bogota.transactions.frame_transaction.FrameTransaction
""" # noqa: E501

# Block
LIMIT_ADJUSTMENT_FACTOR: Final[Uint] = Uint(1024)
LIMIT_MINIMUM: Final[Uint] = Uint(5000)
Expand Down
22 changes: 22 additions & 0 deletions tests/bogota/eip8250_keyed_nonces/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import Sequence

from ethereum_rlp import rlp
from ethereum_types.numeric import U256
from execution_testing import Account, Alloc, Fork, Op, Transaction

from ..eip8141_frame_transactions.helpers import default_code_frame_gas
Expand All @@ -22,6 +24,26 @@ def keyed_nonce_first_use(fork: Fork) -> int:
return Op.SSTORE(original_value=0, new_value=1).state_cost(fork)


def nonce_field_calldata(nonce_keys: Sequence[int], nonce_seq: int) -> bytes:
"""
Return the encoding of the nonce fields the EIP prices as transaction
data: `rlp(nonce_keys) || rlp(nonce_seq)`.
"""
return rlp.encode([U256(key) for key in nonce_keys]) + rlp.encode(
U256(nonce_seq)
)


def keyed_nonce_access_cost(nonce_keys: Sequence[int]) -> int:
"""
Return the EIP's `keyed_nonce_access_cost`: `KEYED_NONCE_ACCESS_COST`
per key, and nothing for the legacy key set.
"""
if list(nonce_keys) == [0]:
return 0
return len(nonce_keys) * Spec.KEYED_NONCE_ACCESS_COST


def nonce_fields(tx: Transaction) -> tuple[Sequence[int], int]:
"""Return the transaction's nonce keys and sequence as integers."""
assert tx.nonce_keys is not None
Expand Down
6 changes: 5 additions & 1 deletion tests/bogota/eip8250_keyed_nonces/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class ReferenceSpec:


ref_spec_8250 = ReferenceSpec(
"EIPS/eip-8250.md", "8b225fbc980e98831058a393c0f35df6e878bda0"
"EIPS/eip-8250.md", "f8db63dd96df22c2ee9475ccec41fa9b8cbcb226"
)


Expand All @@ -32,6 +32,10 @@ class Spec:
MAX_NONCE_KEYS = 16
MAX_NONCE_SEQ = 2**64 - 1

# EIP-8038's `COLD_STORAGE_ACCESS + STORAGE_WRITE`, charged per
# non-zero key in the intrinsic cost and the calldata floor.
KEYED_NONCE_ACCESS_COST = 2_100 + 10_000

# `0x11` is the first selector after the four this EIP adds.
TXPARAM_NONCE_SEQ = 0x01
TXPARAM_LEGACY_NONCE = 0x0D
Expand Down
23 changes: 18 additions & 5 deletions tests/bogota/eip8250_keyed_nonces/test_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,13 @@ def test_keyed_then_legacy_from_one_sender(
[
pytest.param([NONCE_KEY], 0, {}, id="fresh_key"),
pytest.param([NONCE_KEY], 1, {NONCE_KEY: 1}, id="used_key"),
pytest.param(
[NONCE_KEY, OTHER_KEY],
1,
{NONCE_KEY: 1, OTHER_KEY: 1},
id="two_used_keys",
),
pytest.param([NONCE_KEY, OTHER_KEY], 0, {}, id="two_fresh_keys"),
pytest.param([0], 0, {}, id="legacy_key_set"),
],
)
Expand All @@ -367,9 +374,10 @@ def test_block_access_list(
used_keys: Dict[int, int],
) -> None:
"""
Record a keyed write as a nonce manager storage change with no
sender nonce change, and leave the nonce manager out of the list
for the legacy key set.
Record each keyed write as a nonce manager storage change at the
transaction's index, with the written sequence and never among the
storage reads, with no sender nonce change, and leave the nonce
manager out of the list for the legacy key set.
"""
sender = pre.fund_eoa()
if used_keys:
Expand All @@ -389,23 +397,28 @@ def test_block_access_list(
post_slots = {}
sender_nonce = 1
else:
written_slots = sorted(
keyed_nonce_slot(sender, key) for key in nonce_keys
)
nonce_manager_expectation = BalAccountExpectation(
nonce_changes=[],
balance_changes=[],
code_changes=[],
storage_changes=[
BalStorageSlot(
slot=keyed_nonce_slot(sender, NONCE_KEY),
slot=slot,
slot_changes=[
BalStorageChange(
block_access_index=1, post_value=nonce_seq + 1
)
],
)
for slot in written_slots
],
storage_reads=[],
)
sender_expectation = BalAccountExpectation(nonce_changes=[])
post_slots = {keyed_nonce_slot(sender, NONCE_KEY): nonce_seq + 1}
post_slots = dict.fromkeys(written_slots, nonce_seq + 1)
sender_nonce = 0
block = Block(
txs=[tx],
Expand Down
Loading
Loading