Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions eth/abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ class MiningHeaderAPI(ABC):
gas_used: int
timestamp: int
extra_data: bytes
gas_target: int # EIP-1559
base_fee_per_gas: int # EIP-1559
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels... unfortunate, but it might be necessary. Ideally, it would be nice if these fields didn't leak into all the previous VM APIs.... but doing so might introduce even more unfortunate complexity into our type definitions which might be worse...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree and will take a deeper look -- for now I can say a similar problem applies to transactions. Base transaction.validate methods all type-check gas_price which is no longer a thing in London transactions so they need to be duplicated almost entirely. We could keep gas_price as an (internal, not exposed) alias for either max_fee_per_gas or max_priority_fee_per_gas so that we could extend and call basic validation methods via super(), but it sounds confusing :(

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After dealing with typed transactions, my general take on it was: the latest VM should have the cleanest implementation. If a kludge is called for, it should be on the old VMs (like v vs y-parity in the signature, where we started to prefer y-parity as the default/native field). At first glance, this change seems to follow that approach, so I'm 👌🏻 to keep it. I'll comment if I stumble on a better option while reading the rest.


@property
@abstractmethod
Expand Down
1 change: 1 addition & 0 deletions eth/vm/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ def validate_header(cls,
validate_length_lte(
header.extra_data, cls.extra_data_max_bytes, title="BlockHeader.extra_data")

# TODO skip for EIP-1559, or override this whole function?
validate_gas_limit(header.gas_limit, parent_header.gas_limit)

if header.block_number != parent_header.block_number + 1:
Expand Down
3 changes: 3 additions & 0 deletions eth/vm/forks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@
from .berlin import ( # noqa: F401
BerlinVM,
)
from .london import ( # noqa: F401
LondonVM
)
4 changes: 3 additions & 1 deletion eth/vm/forks/berlin/receipts.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class TypedReceipt(ReceiptAPI, ReceiptDecoderAPI):
type_id: int
rlp_type = Binary(min_length=1) # must have at least one byte for the type
_inner: ReceiptAPI
codecs = TYPED_RECEIPT_BODY_CODECS

def __init__(self, type_id: int, proxy_target: ReceiptAPI) -> None:
self.type_id = type_id
Expand Down Expand Up @@ -124,14 +125,15 @@ def __eq__(self, other: Any) -> bool:

class BerlinReceiptBuilder(ReceiptBuilderAPI):
legacy_sedes = Receipt
codecs = TYPED_RECEIPT_BODY_CODECS

@classmethod
def decode(cls, encoded: bytes) -> ReceiptAPI:
if len(encoded) == 0:
raise ValidationError("Encoded receipt was empty, which makes it invalid")

type_id = to_int(encoded[0])
if type_id in TYPED_RECEIPT_BODY_CODECS:
if type_id in cls.codecs:
return TypedReceipt.decode(encoded)
else:
return rlp.decode(encoded, sedes=cls.legacy_sedes)
Expand Down
113 changes: 113 additions & 0 deletions eth/vm/forks/london/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from eth_utils.exceptions import ValidationError
from eth.vm.forks.london.constants import (
BASE_FEE_MAX_CHANGE_DENOMINATOR,
ELASTICITY_MULTIPLIER
)
from typing import Type

from eth.abc import BlockAPI, BlockHeaderAPI
from eth.rlp.blocks import BaseBlock
from eth._utils.db import get_parent_header
from eth.vm.forks.berlin import BerlinVM
from eth.vm.state import BaseState

from .blocks import LondonBlock
from .state import LondonState


class LondonVM(BerlinVM):
# fork name
fork = 'london'

# classes
block_class: Type[BaseBlock] = LondonBlock
_state_class: Type[BaseState] = LondonState

# Methods
# create_header_from_parent = staticmethod(create_berlin_header_from_parent) # type: ignore
# compute_difficulty = staticmethod(compute_berlin_difficulty) # type: ignore
# configure_header = configure_berlin_header

# @staticmethod
# def make_receipt(
# base_header: BlockHeaderAPI,
# transaction: SignedTransactionAPI,
# computation: ComputationAPI,
# state: StateAPI) -> ReceiptAPI:

# gas_used = base_header.gas_used + finalize_gas_used(transaction, computation)

# if computation.is_error:
# status_code = EIP658_TRANSACTION_STATUS_CODE_FAILURE
# else:
# status_code = EIP658_TRANSACTION_STATUS_CODE_SUCCESS

# return transaction.make_receipt(status_code, gas_used, computation.get_log_entries())

@staticmethod
def calculate_expected_base_fee_per_gas(parent_header: BlockHeaderAPI) -> int:
parent_base_fee_per_gas = parent_header.base_fee_per_gas
parent_gas_target = parent_header.gas_target
parent_gas_used = parent_header.gas_used

if parent_gas_used == parent_gas_target:
return parent_base_fee_per_gas

elif parent_gas_used > parent_gas_target:
gas_used_delta = parent_gas_used - parent_base_fee_per_gas
base_fee_per_gas_delta = max(
(
parent_base_fee_per_gas * gas_used_delta // \
parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
),
1
)
return parent_base_fee_per_gas + base_fee_per_gas_delta

else:
gas_used_delta = parent_gas_target - parent_gas_used
base_fee_per_gas_delta = parent_base_fee_per_gas * gas_used_delta \
// parent_gas_target // BASE_FEE_MAX_CHANGE_DENOMINATOR
return max(parent_base_fee_per_gas - base_fee_per_gas_delta, 0)

@classmethod
def validate_header(cls,
header: BlockHeaderAPI,
parent_header: BlockHeaderAPI) -> None:

parent_gas_target = parent_header.gas_target

max_usable_gas = header.gas_target * ELASTICITY_MULTIPLIER
if header.gas_used > max_usable_gas:
raise ValidationError(
f"Block used too much gas: {header.gas_used} "
f"(max: {max_usable_gas})"
)

if header.gas_target > parent_gas_target + (parent_gas_target // 1024):
raise ValidationError(
f"Gas target increased too much (from {parent_gas_target} "
f"to {header.gas_target})"
)

if header.gas_target < parent_gas_target - (parent_gas_target // 1024):
raise ValidationError(
f"Gas target decreased too much (from {parent_gas_target} "
f"to {header.gas_target})"
)

expected_base_fee_per_gas = LondonVM.calculate_expected_base_fee_per_gas(parent_header)
if expected_base_fee_per_gas != header.base_fee_per_gas:
raise ValidationError(
f"Incorrect base fee per gas (got {header.base_fee_per_gas}"
f", expected {expected_base_fee_per_gas})"
)

# TODO continue validation

def validate_block(self, block: BlockAPI) -> None:
header = block.header
parent_header = get_parent_header(block.header, self.chaindb)
LondonVM.validate_header(header, parent_header)

# return super().validate_block(block)
211 changes: 211 additions & 0 deletions eth/vm/forks/london/blocks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import time

from typing import (
Dict,
Type,
)

import rlp

from rlp.sedes import (
Binary,
CountableList,
big_endian_int,
binary
)

from eth.abc import (
BlockHeaderAPI,
MiningHeaderAPI,
ReceiptBuilderAPI,
TransactionBuilderAPI,
)
from eth.constants import (
ZERO_ADDRESS,
ZERO_HASH32,
EMPTY_UNCLE_HASH,
GENESIS_NONCE,
GENESIS_PARENT_HASH,
BLANK_ROOT_HASH,
)
from eth.rlp.sedes import (
address,
hash32,
trie_root,
uint256,
)
from eth.typing import HeaderParams
from eth.vm.forks.berlin.blocks import (
BerlinBlock,
)
from eth_hash.auto import keccak

from eth_typing import (
BlockNumber,
)
from eth_typing.evm import (
Address,
Hash32
)
from eth_utils import (
encode_hex,
)

from .receipts import (
LondonReceiptBuilder,
)
from .transactions import (
LondonTransactionBuilder,
)


class LondonMiningHeader(rlp.Serializable, MiningHeaderAPI):
fields = [
('parent_hash', hash32),
('uncles_hash', hash32),
('coinbase', address),
('state_root', trie_root),
('transaction_root', trie_root),
('receipt_root', trie_root),
('bloom', uint256),
('difficulty', big_endian_int),
('block_number', big_endian_int),
('gas_target', big_endian_int),
('gas_used', big_endian_int),
('timestamp', big_endian_int),
('extra_data', binary),
('base_fee_per_gas', big_endian_int),
]


class LondonBlockHeader(rlp.Serializable, BlockHeaderAPI):
fields = [
('parent_hash', hash32),
('uncles_hash', hash32),
('coinbase', address),
('state_root', trie_root),
('transaction_root', trie_root),
('receipt_root', trie_root),
('bloom', uint256),
('difficulty', big_endian_int),
('block_number', big_endian_int),
('gas_target', big_endian_int),
('gas_used', big_endian_int),
('timestamp', big_endian_int),
('extra_data', binary),
('mix_hash', binary),
('nonce', Binary(8, allow_empty=True)),
('base_fee_per_gas', big_endian_int),
]

def __init__(self, # type: ignore # noqa: F811
difficulty: int,
block_number: BlockNumber,
gas_target: int,
timestamp: int = None,
coinbase: Address = ZERO_ADDRESS,
parent_hash: Hash32 = ZERO_HASH32,
uncles_hash: Hash32 = EMPTY_UNCLE_HASH,
state_root: Hash32 = BLANK_ROOT_HASH,
transaction_root: Hash32 = BLANK_ROOT_HASH,
receipt_root: Hash32 = BLANK_ROOT_HASH,
bloom: int = 0,
gas_used: int = 0,
extra_data: bytes = b'',
mix_hash: Hash32 = ZERO_HASH32,
nonce: bytes = GENESIS_NONCE,
base_fee_per_gas: int = 0) -> None:
if timestamp is None:
timestamp = int(time.time())
super().__init__(
parent_hash=parent_hash,
uncles_hash=uncles_hash,
coinbase=coinbase,
state_root=state_root,
transaction_root=transaction_root,
receipt_root=receipt_root,
bloom=bloom,
difficulty=difficulty,
block_number=block_number,
gas_target=gas_target,
gas_used=gas_used,
timestamp=timestamp,
extra_data=extra_data,
mix_hash=mix_hash,
nonce=nonce,
base_fee_per_gas=base_fee_per_gas,
)

def __str__(self) -> str:
return f'<BlockHeader #{self.block_number} {encode_hex(self.hash)[2:10]}>'

_hash = None

@property
def hash(self) -> Hash32:
if self._hash is None:
self._hash = keccak(rlp.encode(self))
return self._hash

@property
def mining_hash(self) -> Hash32:
return keccak(rlp.encode(self[:-2], LondonMiningHeader))

@property
def hex_hash(self) -> str:
return encode_hex(self.hash)

@classmethod
def from_parent(cls,
parent: 'LondonBlockHeader',
gas_target: int,
difficulty: int,
timestamp: int,
coinbase: Address = ZERO_ADDRESS,
base_fee_per_gas: int = 0,
nonce: bytes = None,
extra_data: bytes = None,
transaction_root: bytes = None,
receipt_root: bytes = None) -> 'LondonBlockHeader':
"""
Initialize a new block header with the `parent` header as the block's
parent hash.
"""
header_kwargs: Dict[str, HeaderParams] = {
'parent_hash': parent.hash,
'coinbase': coinbase,
'state_root': parent.state_root,
'gas_target': gas_target,
'base_fee_per_gas': base_fee_per_gas,
'difficulty': difficulty,
'block_number': parent.block_number + 1,
'timestamp': timestamp,
}
if nonce is not None:
header_kwargs['nonce'] = nonce
if extra_data is not None:
header_kwargs['extra_data'] = extra_data
if transaction_root is not None:
header_kwargs['transaction_root'] = transaction_root
if receipt_root is not None:
header_kwargs['receipt_root'] = receipt_root

header = cls(**header_kwargs)
return header

@property
def is_genesis(self) -> bool:
# if removing the block_number == 0 test, consider the validation consequences.
# validate_header stops trying to check the current header against a parent header.
# Can someone trick us into following a high difficulty header with genesis parent hash?
return self.parent_hash == GENESIS_PARENT_HASH and self.block_number == 0


class LondonBlock(BerlinBlock):
transaction_builder: Type[TransactionBuilderAPI] = LondonTransactionBuilder # type: ignore
receipt_builder: Type[ReceiptBuilderAPI] = LondonReceiptBuilder # type: ignore
fields = [
('header', LondonBlockHeader),
('transactions', CountableList(transaction_builder)),
('uncles', CountableList(LondonBlockHeader))
]
11 changes: 11 additions & 0 deletions eth/vm/forks/london/computation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from eth.vm.forks.berlin.computation import (
BerlinComputation,
)


class LondonComputation(BerlinComputation):
"""
A class for all execution computations in the ``London`` fork.
Inherits from :class:`~eth.vm.forks.berlin.BerlinComputation`
"""
pass
12 changes: 12 additions & 0 deletions eth/vm/forks/london/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from eth.vm.forks.berlin.constants import (
ACCESS_LIST_ADDRESS_COST_EIP_2930,
ACCESS_LIST_STORAGE_KEY_COST_EIP_2930,
)

# EIP 1559
BASE_GAS_FEE_TRANSACTION_TYPE = 2
BASE_GAS_FEE_ADDRESS_COST = ACCESS_LIST_ADDRESS_COST_EIP_2930
BASE_GAS_FEE_STORAGE_KEY_COST = ACCESS_LIST_STORAGE_KEY_COST_EIP_2930

BASE_FEE_MAX_CHANGE_DENOMINATOR = 8
ELASTICITY_MULTIPLIER = 2
Loading