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
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""
EIP-8272: Recent Roots for Frame Transactions.

Add the recent root contract, an ordinary contract deployed by a
pre-signed creation transaction, that stores application roots by source
and slot and lets a frame transaction verify recent roots through a
`VERIFY` frame targeting it. The transaction payload, gas rules and
opcodes are unchanged.

https://eips.ethereum.org/EIPS/eip-8272
"""

from typing import Mapping

from ....base_fork import BaseFork

RECENT_ROOT_ADDRESS = 0x8272D9679689EA2F307140CDF9002D27DC00FFFF
RECENT_ROOT_BYTECODE = bytes.fromhex(
"346100ba57366040146100c05736604836066100ba5780156100ba57610480"
"81116100ba574b60005b602081013560c01c828110156100ba578083036120"
"0011156100ba577f8f42481679c8e6fefa040974b3c905e0ce3f2e464ba93a"
"cdb074a41181617efc60005260488260203760686000207fbdc897da2177d2"
"60ff5f4be5d4b2aad43f89c3347a305b584fa5a2546d053daa60005290611f"
"ff1660c01b60405260486000205414156100ba5760480182811061002857005b"
"60006000fd5b33600052602060006020376034600c20807f8f42481679c8e6"
"fefa040974b3c905e0ce3f2e464ba93acdb074a41181617efc6040524b6068"
"52606052602060206088376068604020817fbdc897da2177d260ff5f4be5d4"
"b2aad43f89c3347a305b584fa5a2546d053daa60a852611fff4b1660d05260"
"c852604860a8205500"
)
RECENT_ROOT_NONCE = 1


class EIP8272(BaseFork):
"""EIP-8272 class."""

@classmethod
def pre_allocation(cls) -> Mapping:
"""Pre-allocate the recent root contract."""
return {
RECENT_ROOT_ADDRESS: {
"nonce": RECENT_ROOT_NONCE,
"code": RECENT_ROOT_BYTECODE,
}
} | super(EIP8272, cls).pre_allocation() # type: ignore

@classmethod
def pre_allocation_blockchain(cls) -> Mapping:
"""Pre-allocate the recent root contract."""
return {
RECENT_ROOT_ADDRESS: {
"nonce": RECENT_ROOT_NONCE,
"code": RECENT_ROOT_BYTECODE,
}
} | super(EIP8272, cls).pre_allocation_blockchain() # type: ignore
9 changes: 9 additions & 0 deletions src/ethereum/forks/bogota/transactions/frame_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,15 @@
big-endian expiry timestamp.
"""

RECENT_ROOT_ADDRESS: Final[Address] = Address(
bytes.fromhex("8272d9679689ea2f307140cdf9002d27dc00ffff")
)
"""
Address of the recent root contract of [EIP-8272].

[EIP-8272]: https://eips.ethereum.org/EIPS/eip-8272
"""


@final
class FrameMode(UintEnum, boundary=STRICT):
Expand Down
6 changes: 6 additions & 0 deletions tests/bogota/eip8272_recent_roots/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
Tests for [EIP-8272: Recent Roots for Frame Transactions](https://eips.ethereum.org/EIPS/eip-8272).

EIP-8272 is a delta against EIP-8141, so its tests build on the frame
transaction helpers of `tests/bogota/eip8141_frame_transactions`.
""" # noqa: E501
106 changes: 106 additions & 0 deletions tests/bogota/eip8272_recent_roots/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""Helpers for EIP-8272 recent root tests."""

from typing import Any, Dict, Sequence

from execution_testing import Fork, Frame, Op

from ..eip8141_frame_transactions.spec import Spec as FrameSpec
from .spec import Spec

VALIDATION_FIXED_GAS = 119
"""
Execution gas of the validation operation outside the tuple loop: the
value check, the calldata length dispatch and the three length checks,
reading the current slot and the loop offset, and the first tuple's
memory expansion to the four words its entry preimage occupies.
"""

VALIDATION_TUPLE_GAS = 296
"""
Execution gas of one tuple's successful path through the validation
loop, excluding its storage read: decoding the slot, the two slot
checks, hashing the entry preimage and the storage key preimage, the
comparison and the loop step. Measured against `RECENT_ROOT_CODE` and
confirmed by counting its assembly; re-derive both constants if the
canonical code changes.
"""


def validation_gas(
fork: Fork, *, tuples: int, cold_keys: int, target_warm: bool = False
) -> int:
"""
Return the execution gas a recent root verifier frame reports for
`tuples` references of which `cold_keys` read distinct storage keys
for the first time in the transaction.

The frame charges its target's access at entry, then the contract's
fixed cost, its per-tuple cost, and one warm or cold storage read
per tuple.
"""
warm_keys = tuples - cold_keys
return (
fork.frame_entry_gas_calculator()(target_warm=target_warm)
+ VALIDATION_FIXED_GAS
+ tuples * VALIDATION_TUPLE_GAS
+ cold_keys
* Op.SLOAD.with_metadata(key_warm=False).execution_cost(fork)
+ warm_keys
* Op.SLOAD.with_metadata(key_warm=True).execution_cost(fork)
)


def recent_root_frame(
references: Sequence[bytes] | bytes, **overrides: Any
) -> Frame:
"""
Return a recent root verifier frame: a `VERIFY` frame targeting the
recent root contract with no flags, no value, no state gas and the
concatenated validation tuples as data.

Keyword arguments override the corresponding frame fields, for
variants that differ from the canonical frame in a single field.
"""
data = (
bytes(references)
if isinstance(references, (bytes, bytearray))
else b"".join(references)
)
kwargs: Dict[str, Any] = dict(
mode=FrameSpec.MODE_VERIFY,
flags=FrameSpec.APPROVE_NONE,
target=Spec.RECENT_ROOT_ADDRESS,
state_gas_limit=0,
data=data,
)
kwargs.update(overrides)
return Frame(**kwargs)


def write_state_gas(fork: Fork, *, writes: int = 1) -> int:
"""
Return the state gas budget for `writes` fresh storage sets: a
published entry is one, and a relay that also records its own
witness slot needs a second.
"""
return writes * Op.SSTORE(0, 1).state_cost(fork)


def write_frame(
fork: Fork, salt: bytes, root: bytes, **overrides: Any
) -> Frame:
"""
Return a `SENDER` frame publishing `root` under `salt` for the
transaction sender: it calls the recent root contract with the 64-byte
write encoding and a state gas budget covering the entry's creation.

Keyword arguments override the corresponding frame fields.
"""
kwargs: Dict[str, Any] = dict(
mode=FrameSpec.MODE_SENDER,
target=Spec.RECENT_ROOT_ADDRESS,
state_gas_limit=write_state_gas(fork),
data=bytes(salt) + bytes(root),
)
kwargs.update(overrides)
return Frame(**kwargs)
16 changes: 16 additions & 0 deletions tests/bogota/eip8272_recent_roots/recent_root_deploy_tx.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "0x0",
"nonce": "0x0",
"to": null,
"gas": "0x13d620",
"gasPrice": "0xe8d4a51000",
"maxPriorityFeePerGas": null,
"maxFeePerGas": null,
"value": "0x0",
"input": "0x61014080600a5f395ff3346100ba57366040146100c05736604836066100ba5780156100ba5761048081116100ba574b60005b602081013560c01c828110156100ba5780830361200011156100ba577f8f42481679c8e6fefa040974b3c905e0ce3f2e464ba93acdb074a41181617efc60005260488260203760686000207fbdc897da2177d260ff5f4be5d4b2aad43f89c3347a305b584fa5a2546d053daa60005290611fff1660c01b60405260486000205414156100ba5760480182811061002857005b60006000fd5b33600052602060006020376034600c20807f8f42481679c8e6fefa040974b3c905e0ce3f2e464ba93acdb074a41181617efc6040524b606852606052602060206088376068604020817fbdc897da2177d260ff5f4be5d4b2aad43f89c3347a305b584fa5a2546d053daa60a852611fff4b1660d05260c852604860a8205500",
"v": "0x1b",
"r": "0x539",
"s": "0xfadf66b1e192785c",
"hash": "0x56c2adbbfa3ba5dfe47adf48828f6c581134e39e14c3b80e06214de5e5875272",
"sender": "0x14bf16d4c9842bf1EbF396e553477C66EB0a8A82"
}
118 changes: 118 additions & 0 deletions tests/bogota/eip8272_recent_roots/spec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Defines EIP-8272 specification constants and types."""

from dataclasses import dataclass

from execution_testing import Address, keccak256


@dataclass(frozen=True)
class ReferenceSpec:
"""Defines the reference spec version and git path."""

git_path: str
version: str


ref_spec_8272 = ReferenceSpec(
"EIPS/eip-8272.md", "0acc21ad8359be83c0929c7cc70e45326be8acff"
)


@dataclass(frozen=True)
class Spec:
"""
Parameters from the EIP-8272 specification as defined at
https://eips.ethereum.org/EIPS/eip-8272.
"""

RECENT_ROOT_ADDRESS = Address(0x8272D9679689EA2F307140CDF9002D27DC00FFFF)
RECENT_ROOT_DEPLOYER = Address(0x14BF16D4C9842BF1EBF396E553477C66EB0A8A82)
RECENT_ROOT_CODE = bytes.fromhex(
"346100ba57366040146100c05736604836066100ba5780156100ba57610480"
"81116100ba574b60005b602081013560c01c828110156100ba578083036120"
"0011156100ba577f8f42481679c8e6fefa040974b3c905e0ce3f2e464ba93a"
"cdb074a41181617efc60005260488260203760686000207fbdc897da2177d2"
"60ff5f4be5d4b2aad43f89c3347a305b584fa5a2546d053daa60005290611f"
"ff1660c01b60405260486000205414156100ba5760480182811061002857005b"
"60006000fd5b33600052602060006020376034600c20807f8f42481679c8e6"
"fefa040974b3c905e0ce3f2e464ba93acdb074a41181617efc6040524b6068"
"52606052602060206088376068604020817fbdc897da2177d260ff5f4be5d4"
"b2aad43f89c3347a305b584fa5a2546d053daa60a852611fff4b1660d05260"
"c852604860a8205500"
)
RECENT_ROOT_NONCE = 1
RECENT_ROOT_LENGTH = 8192
RECENT_ROOT_USABLE_WINDOW = 8191
MAX_RECENT_ROOT_REFERENCES = 16
RECENT_ROOT_TUPLE_BYTES = 72
RECENT_ROOT_WRITE_BYTES = 64
RECENT_ROOT_ENTRY_DOMAIN = keccak256(b"RECENT_ROOT_ENTRY")
RECENT_ROOT_STORAGE_DOMAIN = keccak256(b"RECENT_ROOT_STORAGE")

# Reference vector from the EIP, valid at `current_slot = 2`.
VECTOR_SOURCE_ADDRESS = Address(0x01)
VECTOR_SALT = bytes(32)
VECTOR_SLOT = 1
VECTOR_ROOT = (2).to_bytes(32, "big")
VECTOR_CURRENT_SLOT = 2
VECTOR_SOURCE_ID = bytes.fromhex(
"b9382d35273c75a50631a3e84d3c75ec9266e2b18c35a627e16cdbf26a18ca85"
)
VECTOR_ENTRY_HASH = bytes.fromhex(
"0a0d1254c851be5a133b4c9a9e300f5602fc0f43dbe65aa6a66930d4ca0a51b8"
)
VECTOR_STORAGE_KEY = bytes.fromhex(
"5f027aa1cbe2df279bf6518edd4b44ea5409fd800189ec35224e10ab05e574c3"
)


def source_id(source_address: Address, salt: bytes) -> bytes:
"""
Return the root source identifier: the keccak256 of the 20-byte source
address followed by the 32-byte salt.
"""
return bytes(keccak256(bytes(source_address) + bytes(salt)))


def entry_hash(source: bytes, slot: int, root: bytes) -> bytes:
"""
Return the committed entry for `(source_id, slot, root)`: the keccak256
of the entry domain, the source identifier, the slot as an eight-byte
big-endian integer, and the root.
"""
return bytes(
keccak256(
bytes(Spec.RECENT_ROOT_ENTRY_DOMAIN)
+ bytes(source)
+ slot.to_bytes(8, "big")
+ bytes(root)
)
)


def storage_key(source: bytes, slot: int) -> int:
"""
Return the recent root storage key holding the entry of `slot` for a
source: the keccak256 of the storage domain, the source identifier,
and the ring buffer index `slot mod RECENT_ROOT_LENGTH` as an
eight-byte big-endian integer.
"""
index = slot % Spec.RECENT_ROOT_LENGTH
return int.from_bytes(
keccak256(
bytes(Spec.RECENT_ROOT_STORAGE_DOMAIN)
+ bytes(source)
+ index.to_bytes(8, "big")
),
"big",
)


def validation_tuple(source: bytes, slot: int, root: bytes) -> bytes:
"""Return the 72-byte validation encoding of `(source_id, slot, root)`."""
return bytes(source) + slot.to_bytes(8, "big") + bytes(root)


def write_calldata(salt: bytes, root: bytes) -> bytes:
"""Return the 64-byte write encoding of `(salt, root)`."""
return bytes(salt) + bytes(root)
Loading
Loading