Skip to content

Commit

Permalink
feat[lang]: add blobhash() builtin (#3962)
Browse files Browse the repository at this point in the history
This commit adds access to the `BLOBHASH` opcode via the new builtin
function `blobhash()`. The opcode was introduced in EIP-4844.

References:
https://eips.ethereum.org/EIPS/eip-4844#opcode-to-get-versioned-hashes

---------

Signed-off-by: Pascal Marco Caversaccio <pascal.caversaccio@hotmail.ch>
Co-authored-by: tserg <8017125+tserg@users.noreply.github.com>
Co-authored-by: Daniel Schiavini <daniel.schiavini@gmail.com>
Co-authored-by: Charles Cooper <cooper.charles.m@gmail.com>
  • Loading branch information
4 people authored May 3, 2024
1 parent 097aecf commit 533b271
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 13 deletions.
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
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
65 changes: 65 additions & 0 deletions tests/functional/builtins/codegen/test_blobhash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import random

import pytest

from vyper.compiler import compile_code

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 compile_code(good_code) is not None
out = compile_code(good_code, output_formats=["opcodes_runtime"])
assembly = out["opcodes_runtime"].split(" ")
assert "BLOBHASH" in assembly


@pytest.mark.requires_evm_version("cancun")
def test_get_blobhashes(env, get_contract, tx_failed):
code = """
@external
def get_blobhash(i: uint256) -> bytes32:
return blobhash(i)
"""
c = get_contract(code)

# mock the evm blobhash attribute
env.blob_hashes = [random.randbytes(32) for _ in range(6)]

for i in range(6):
assert c.get_blobhash(i) == env.blob_hashes[i]

assert c.get_blobhash(len(env.blob_hashes)) == b"\0" * 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"):
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

0 comments on commit 533b271

Please sign in to comment.