Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[lang]: add blobhash() builtin #3962

Merged
merged 22 commits into from
May 3, 2024
Merged
Show file tree
Hide file tree
Changes from 19 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
24 changes: 24 additions & 0 deletions docs/built-in-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -949,6 +949,30 @@ Utilities
>>> ExampleContract.foo()
0xf3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

.. py:function:: blobhash(index: uint256) -> bytes32

Return the versioned hash of the ``index``-th BLOB associated with the current transaction.

.. note::

A versioned hash consists of a single byte representing the version (currently ``0x01``), followed by the last 31 bytes of the ``SHA256`` hash of the KZG commitment (`EIP-4844 <https://eips.ethereum.org/EIPS/eip-4844>`_). For the case ``index >= len(tx.blob_versioned_hashes)``, ``blobhash(index: uint256)`` returns ``empty(bytes32)``.

.. code-block:: vyper

@external
@view
def foo(index: uint256) -> bytes32:
return blobhash(index)

.. code-block:: vyper

>>> ExampleContract.foo(0)
0xfd28610fb309939bfec12b6db7c4525446f596a5a5a66b8e2cb510b45b2bbeb5

>>> ExampleContract.foo(6)
0x0000000000000000000000000000000000000000000000000000000000000000


.. py:function:: empty(typename) -> Any

Return a value which is the default (zero-ed) value of its type. Useful for initializing new memory variables.
Expand Down
9 changes: 8 additions & 1 deletion tests/evm_backends/base_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ def timestamp(self, value: int):
def last_result(self) -> ExecutionResult:
raise NotImplementedError # must be implemented by subclasses

@property
def blob_hashes(self) -> list[bytes]:
raise NotImplementedError # must be implemented by subclasses

@blob_hashes.setter
def blob_hashes(self, value: list[bytes]):
raise NotImplementedError # must be implemented by subclasses

def message_call(
self,
to: str,
Expand All @@ -159,7 +167,6 @@ def message_call(
gas: int | None = None,
gas_price: int = 0,
is_modifying: bool = True,
blob_hashes: Optional[list[bytes]] = None, # for blobbasefee >= Cancun
) -> bytes:
raise NotImplementedError # must be implemented by subclasses

Expand Down
28 changes: 22 additions & 6 deletions tests/evm_backends/pyevm_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

import rlp
from cached_property import cached_property
from eth.abc import ChainAPI, ComputationAPI
from eth.abc import ChainAPI, ComputationAPI, VirtualMachineAPI
Copy link
Member

Choose a reason for hiding this comment

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

this is a nit but i'm not a huge fan of importing things that are only used in type signatures, i am starting to prefer guarding these with if TYPE_CHECKING

from eth.chains.mainnet import MainnetChain
from eth.constants import CREATE_CONTRACT_ADDRESS, GENESIS_DIFFICULTY
from eth.db.atomic import AtomicDB
from eth.exceptions import Revert, VMError
from eth.tools.builder import chain as chain_builder
from eth.vm.base import StateAPI
from eth.vm.execution_context import ExecutionContext
from eth.vm.forks.cancun.transaction_context import CancunTransactionContext
from eth.vm.message import Message
from eth.vm.transaction_context import BaseTransactionContext
from eth_keys.datatypes import PrivateKey
from eth_typing import Address
from eth_utils import setup_DEBUG2_logging, to_canonical_address, to_checksum_address
Expand Down Expand Up @@ -54,13 +54,14 @@ def __init__(
)

self._last_computation: ComputationAPI = None
self._blob_hashes: list[bytes] = []

@cached_property
def _state(self) -> StateAPI:
return self._vm.state

@cached_property
def _vm(self):
def _vm(self) -> VirtualMachineAPI:
return self._chain.get_vm()

@cached_property
Expand Down Expand Up @@ -109,6 +110,14 @@ def last_result(self) -> ExecutionResult:
gas_used=result.get_gas_used(),
)

@property
def blob_hashes(self) -> list[bytes]:
return self._blob_hashes

@blob_hashes.setter
def blob_hashes(self, value: list[bytes]):
self._blob_hashes = value

def message_call(
self,
to: str,
Expand All @@ -118,7 +127,6 @@ def message_call(
gas: int | None = None,
gas_price: int = 0,
is_modifying: bool = True,
blob_hashes: Optional[list[bytes]] = None, # for blobbasefee >= Cancun
):
if isinstance(data, str):
data = bytes.fromhex(data.removeprefix("0x"))
Expand All @@ -135,7 +143,7 @@ def message_call(
gas=self.gas_limit if gas is None else gas,
is_static=not is_modifying,
),
transaction_context=BaseTransactionContext(origin=sender, gas_price=gas_price),
transaction_context=self._make_tx_context(sender, gas_price),
)
except VMError as e:
# py-evm raises when user is out-of-funds instead of returning a failed computation
Expand All @@ -144,6 +152,14 @@ def message_call(
self._check_computation(computation)
return computation.output

def _make_tx_context(self, sender, gas_price):
context_class = self._state.transaction_context_class
context = context_class(origin=sender, gas_price=gas_price)
if self._blob_hashes:
assert isinstance(context, CancunTransactionContext)
context._blob_versioned_hashes = self._blob_hashes
return context

def clear_transient_storage(self) -> None:
try:
self._state.clear_transient_storage()
Expand Down Expand Up @@ -185,7 +201,7 @@ def _deploy(self, code: bytes, value: int, gas: int = None) -> str:
gas=gas or self.gas_limit,
create_address=target_address,
),
transaction_context=BaseTransactionContext(origin=sender, gas_price=0),
transaction_context=self._make_tx_context(sender, gas_price=0),
)
except VMError as e:
# py-evm raises when user is out-of-funds instead of returning a failed computation
Expand Down
14 changes: 10 additions & 4 deletions tests/evm_backends/revm_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,16 @@ def last_result(self) -> ExecutionResult:
logs=result.logs,
)

@property
def blob_hashes(self):
return self._evm.env.tx.blob_hashes

@blob_hashes.setter
def blob_hashes(self, value):
tx = self._evm.env.tx
tx.blob_hashes = value
self._evm.set_tx_env(tx)

def message_call(
self,
to: str,
Expand All @@ -89,10 +99,6 @@ def message_call(
):
if isinstance(data, str):
data = bytes.fromhex(data.removeprefix("0x"))
if blob_hashes is not None:
tx = self._evm.env.tx
tx.blob_hashes = blob_hashes
self._evm.set_tx_env(tx)

try:
return self._evm.message_call(
Expand Down
78 changes: 78 additions & 0 deletions tests/functional/builtins/codegen/test_blobhash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import pytest

from vyper import compiler

valid_list = [
"""
@external
@view
def foo() -> bytes32:
return blobhash(0)
""",
"""
@external
@view
def foo() -> bytes32:
a: bytes32 = 0x0000000000000000000000000000000000000000000000000000000000000005
a = blobhash(2)
return a
""",
"""
@external
@view
def foo() -> bytes32:
a: bytes32 = blobhash(0)
assert a != empty(bytes32)
return a
""",
"""
@external
@view
def foo() -> bytes32:
a: bytes32 = blobhash(1337)
assert a == empty(bytes32)
return a
""",
]


@pytest.mark.requires_evm_version("cancun")
@pytest.mark.parametrize("good_code", valid_list)
def test_blobhash_success(good_code):
assert compiler.compile_code(good_code) is not None
assembly = compiler.compile_code(good_code, output_formats=["asm"])["asm"].split(" ")
assert "BLOBHASH" in assembly


@pytest.mark.requires_evm_version("cancun")
def test_get_blobhashes(env, get_contract, tx_failed):
code = """
x: public(bytes32)
@external
def set_blobhash(i: uint256):
self.x = blobhash(i)
"""
c = get_contract(code)

# to get the expected versioned hashes:
#
# from eth_account._utils.typed_transactions import BlobTransaction
# blob_transaction = BlobTransaction.from_bytes(HexBytes(signed.rawTransaction))
# print(blob_transaction.blob_data.versioned_hashes)
expected_versioned_hash = "0x0168dea5bd14ec82691edc861dcee360342a921c1664b02745465f6c42239f06"

def _send_tx_with_blobs(num_blobs, input_idx):
env.blob_hashes = [bytes.fromhex(expected_versioned_hash[2:])] * num_blobs
c.set_blobhash(input_idx)

c.set_blobhash(0)
assert c.x() == b"\x00" * 32

_send_tx_with_blobs(1, 0)
assert "0x" + c.x().hex() == expected_versioned_hash

_send_tx_with_blobs(6, 5)
assert "0x" + c.x().hex() == expected_versioned_hash

_send_tx_with_blobs(1, 1)
assert c.x() == b"\x00" * 32
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,14 @@ def get_blobbasefee() -> uint256:
env.set_excess_blob_gas(10**6)

# kzg_hash(b"Vyper is the language of the sneks")
blob_hashes = [
env.blob_hashes = [
(bytes.fromhex("015a5c97e3cc516f22a95faf7eefff00eb2fee7a65037fde07ac5446fc93f2a0"))
] * 6

env.message_call(
"0xb45BEc6eeCA2a09f4689Dd308F550Ad7855051B5", # random address
gas=21000,
gas_price=10**10,
blob_hashes=blob_hashes,
)

excess_blob_gas = env.get_excess_blob_gas()
Expand Down
15 changes: 15 additions & 0 deletions vyper/builtins/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@
from vyper.codegen.ir_node import Encoding, scope_multi
from vyper.codegen.keccak256_helper import keccak256_helper
from vyper.evm.address_space import MEMORY
from vyper.evm.opcodes import version_check
from vyper.exceptions import (
ArgumentException,
CompilerPanic,
EvmVersionException,
InvalidLiteral,
InvalidType,
StateAccessViolation,
Expand Down Expand Up @@ -1213,6 +1215,18 @@ def build_IR(self, expr, args, kwargs, contact):
)


class BlobHash(BuiltinFunctionT):
_id = "blobhash"
_inputs = [("index", UINT256_T)]
_return_type = BYTES32_T

@process_inputs
def build_IR(self, expr, args, kwargs, contact):
if not version_check(begin="cancun"):
charles-cooper marked this conversation as resolved.
Show resolved Hide resolved
raise EvmVersionException("`blobhash` is not available pre-cancun", expr)
return IRnode.from_list(["blobhash", args[0]], typ=BYTES32_T)


class RawRevert(BuiltinFunctionT):
_id = "raw_revert"
_inputs = [("data", BytesT.any())]
Expand Down Expand Up @@ -2594,6 +2608,7 @@ def _try_fold(self, node):
"as_wei_value": AsWeiValue(),
"raw_call": RawCall(),
"blockhash": BlockHash(),
"blobhash": BlobHash(),
"bitwise_and": BitwiseAnd(),
"bitwise_or": BitwiseOr(),
"bitwise_xor": BitwiseXor(),
Expand Down
1 change: 1 addition & 0 deletions vyper/evm/opcodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"CHAINID": (0x46, 0, 1, 2),
"SELFBALANCE": (0x47, 0, 1, 5),
"BASEFEE": (0x48, 0, 1, 2),
"BLOBHASH": (0x49, 1, 1, (None, None, None, 3)),
"BLOBBASEFEE": (0x4A, 0, 1, (None, None, None, 2)),
"POP": (0x50, 1, 0, 2),
"MLOAD": (0x51, 1, 1, 3),
Expand Down
1 change: 1 addition & 0 deletions vyper/venom/ir_node_to_venom.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"signextend",
"chainid",
"basefee",
"blobhash",
"blobbasefee",
"timestamp",
"blockhash",
Expand Down
1 change: 1 addition & 0 deletions vyper/venom/venom_to_assembly.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"delegatecall",
"codesize",
"basefee",
"blobhash",
"blobbasefee",
"prevrandao",
"difficulty",
Expand Down
Loading