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

Evm daemon #891

Merged
merged 6 commits into from
Mar 11, 2024
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
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ install_requires =
pycryptodome>=3,<4
coincurve>=18,<19
typing_extensions>=4
eth2spec @ git+https://github.com/ethereum/consensus-specs.git@fe344b79d420a3d1020ebc9f254b77b7ed931591
eth2spec @ git+https://github.com/ethereum/consensus-specs.git@d302b35d40c72842d444ef2ea64344e3cb889804

[options.package_data]
ethereum =
Expand Down Expand Up @@ -151,6 +151,7 @@ test =
pytest-xdist>=3.3.1,<4
GitPython>=3.1.0,<3.2
filelock>=3.12.3,<3.13
platformdirs>=4.2,<5
requests

lint =
Expand Down
76 changes: 50 additions & 26 deletions src/ethereum_spec_tools/evm_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

import argparse
import subprocess
import sys
from typing import Optional, Sequence, Text, TextIO

from ethereum import __version__

from .b11r import B11R, b11r_arguments
from .daemon import Daemon, daemon_arguments
from .t8n import T8N, t8n_arguments
from .utils import get_supported_forks

Expand All @@ -30,10 +33,35 @@
+ get_supported_forks()
)

parser = argparse.ArgumentParser(
description=DESCRIPTION,
formatter_class=argparse.RawDescriptionHelpFormatter,
)

def create_parser() -> argparse.ArgumentParser:
"""
Create a command-line argument parser for the evm tool.
"""
new_parser = argparse.ArgumentParser(
description=DESCRIPTION,
formatter_class=argparse.RawDescriptionHelpFormatter,
)

commit_hash = get_git_commit_hash()

# Add -v option to parser to show the version of the tool
new_parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s {__version__} (Git commit: {commit_hash})",
help="Show the version of the tool.",
)

# Add options to the t8n tool
subparsers = new_parser.add_subparsers(dest="evm_tool")

daemon_arguments(subparsers)
t8n_arguments(subparsers)
b11r_arguments(subparsers)

return new_parser


def get_git_commit_hash() -> str:
Expand All @@ -59,35 +87,31 @@ def get_git_commit_hash() -> str:
return "Error: " + str(e)


commit_hash = get_git_commit_hash()

# Add -v option to parser to show the version of the tool
parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s {__version__} (Git commit: {commit_hash})",
help="Show the version of the tool.",
)


# Add options to the t8n tool
subparsers = parser.add_subparsers(dest="evm_tool")
def main(
args: Optional[Sequence[Text]] = None,
out_file: Optional[TextIO] = None,
in_file: Optional[TextIO] = None,
) -> int:
"""Run the tools based on the given options."""
parser = create_parser()

options, _ = parser.parse_known_args(args)

def main() -> int:
"""Run the tools based on the given options."""
t8n_arguments(subparsers)
b11r_arguments(subparsers)
if out_file is None:
out_file = sys.stdout

options, _ = parser.parse_known_args()
if in_file is None:
in_file = sys.stdin

if options.evm_tool == "t8n":
t8n_tool = T8N(options)
t8n_tool = T8N(options, out_file, in_file)
return t8n_tool.run()
elif options.evm_tool == "b11r":
b11r_tool = B11R(options)
b11r_tool = B11R(options, out_file, in_file)
return b11r_tool.run()
elif options.evm_tool == "daemon":
daemon = Daemon(options)
return daemon.run()
else:
parser.print_help()
parser.print_help(file=out_file)
return 0
14 changes: 9 additions & 5 deletions src/ethereum_spec_tools/evm_tools/b11r/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@

import argparse
import json
import sys
from typing import Optional
from typing import Optional, TextIO

from ethereum import rlp
from ethereum.base_types import Bytes32
Expand Down Expand Up @@ -65,17 +64,22 @@ class B11R:
Creates the b11r tool.
"""

def __init__(self, options: argparse.Namespace) -> None:
def __init__(
self, options: argparse.Namespace, out_file: TextIO, in_file: TextIO
) -> None:
"""
Initializes the b11r tool.
"""
self.options = options
self.out_file = out_file
self.in_file = in_file

if "stdin" in (
options.input_header,
options.input_ommers,
options.input_txs,
):
stdin = json.load(sys.stdin)
stdin = json.load(in_file)
else:
stdin = None

Expand Down Expand Up @@ -126,7 +130,7 @@ def run(self) -> int:

self.logger.info("Writing the result...")
if self.options.output_block == "stdout":
json.dump(result, sys.stdout, indent=4)
json.dump(result, self.out_file, indent=4)
else:
with open(self.options.output_block, "w") as f:
json.dump(result, f, indent=4)
Expand Down
122 changes: 122 additions & 0 deletions src/ethereum_spec_tools/evm_tools/daemon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
"""
Run ethereum-spec-evm as a daemon.
"""

import argparse
import json
import os.path
import socketserver
import time
from http.server import BaseHTTPRequestHandler
from io import StringIO, TextIOWrapper
from socket import socket
from threading import Thread
from typing import Any, Optional, Tuple, Union

from platformdirs import user_runtime_dir


def daemon_arguments(subparsers: argparse._SubParsersAction) -> None:
"""
Adds the arguments for the daemon tool subparser.
"""
parser = subparsers.add_parser("daemon", help="Spawn t8n as a daemon")
parser.add_argument("--uds", help="Unix domain socket path")


class _EvmToolHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
from . import main

content_length = int(self.headers["Content-Length"])
content_bytes = self.rfile.read(content_length)
content = json.loads(content_bytes)

input_string = json.dumps(content["input"])
input = StringIO(input_string)

args = [
"t8n",
"--input.env=stdin",
"--input.alloc=stdin",
"--input.txs=stdin",
"--output.result=stdout",
"--output.body=stdout",
"--output.alloc=stdout",
f"--state.fork={content['state']['fork']}",
f"--state.chainid={content['state']['chainid']}",
f"--state.reward={content['state']['reward']}",
]

self.send_response(200)
self.send_header("Content-type", "application/octet-stream")
self.end_headers()

out_wrapper = TextIOWrapper(self.wfile, encoding="utf-8")
main(args=args, out_file=out_wrapper, in_file=input)
out_wrapper.flush()


class _UnixSocketHttpServer(socketserver.UnixStreamServer):
last_response: Optional[float] = None

def get_request(self) -> Tuple[Any, Any]:
request, client_address = super().get_request()
return (request, ["local", 0])

def finish_request(
self, request: Union[socket, Tuple[bytes, socket]], client_address: Any
) -> None:
try:
super().finish_request(request, client_address)
finally:
self.last_response = time.monotonic()

def check_timeout(self) -> None:
while True:
time.sleep(11.0)
now = time.monotonic()
last_response = self.last_response
if last_response is None:
self.last_response = now
elif now - last_response > 60.0:
self.shutdown()
break


class Daemon:
"""
Converts HTTP requests into ethereum-spec-evm calls.
"""

def __init__(self, options: argparse.Namespace) -> None:
if options.uds is None:
runtime_dir = user_runtime_dir(
appname="ethereum-spec-evm",
appauthor="org.ethereum",
ensure_exists=True,
)
self.uds = os.path.join(runtime_dir, "daemon.sock")
else:
self.uds = options.uds

def _run(self) -> int:
try:
os.remove(self.uds)
except IOError:
pass

with _UnixSocketHttpServer((self.uds), _EvmToolHandler) as server:
server.timeout = 7.0
timer = Thread(target=server.check_timeout, daemon=True)
timer.start()

server.serve_forever()

return 0

def run(self) -> int:
"""
Execute the tool.
"""
return self._run()
29 changes: 24 additions & 5 deletions src/ethereum_spec_tools/evm_tools/t8n/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
import argparse
import json
import os
import sys
from functools import partial
from typing import Any
from typing import Any, TextIO

from ethereum import rlp, trace
from ethereum.base_types import U64, U256, Uint
from ethereum.crypto.hash import keccak256
from ethereum.exceptions import InvalidBlock
from ethereum.utils.ensure import ensure
from ethereum_spec_tools.forks import Hardfork

from ..fixture_loader import Load
Expand Down Expand Up @@ -74,7 +75,11 @@ def t8n_arguments(subparsers: argparse._SubParsersAction) -> None:
class T8N(Load):
"""The class that carries out the transition"""

def __init__(self, options: Any) -> None:
def __init__(
self, options: Any, out_file: TextIO, in_file: TextIO
) -> None:
self.out_file = out_file
self.in_file = in_file
self.options = options
self.forks = Hardfork.discover()

Expand All @@ -83,7 +88,7 @@ def __init__(self, options: Any) -> None:
options.input_alloc,
options.input_txs,
):
stdin = json.load(sys.stdin)
stdin = json.load(in_file)
else:
stdin = None

Expand Down Expand Up @@ -336,6 +341,7 @@ def apply_body(self) -> None:
transactions_trie = self.trie.Trie(secured=False, default=None)
receipts_trie = self.trie.Trie(secured=False, default=None)
block_logs = ()
blob_gas_used = Uint(0)

if self.is_after_fork("ethereum.cancun"):
beacon_block_roots_contract_code = self.get_account(
Expand Down Expand Up @@ -391,6 +397,15 @@ def apply_body(self) -> None:
env = self.environment(tx, gas_available)

process_transaction_return = self.process_transaction(env, tx)

if self.is_after_fork("ethereum.cancun"):
blob_gas_used += self._module(
"vm.gas"
).calculate_total_blob_gas(tx)
ensure(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By @gurukamath in #889:

Based on the EIP, this check has to happen once outside the for loop, after all the transactions have been processed. But tbh, this makes more sense imo unless I am missing something.

blob_gas_used <= self.fork.MAX_BLOB_GAS_PER_BLOCK,
InvalidBlock,
)
except Exception as e:
# The tf tools expects some non-blank error message
# even in case e is blank.
Expand Down Expand Up @@ -451,6 +466,10 @@ def apply_body(self) -> None:

self.result.withdrawals_root = self.trie.root(withdrawals_trie)

if self.is_after_fork("ethereum.cancun"):
self.result.blob_gas_used = blob_gas_used
self.result.excess_blob_gas = self.env.excess_blob_gas

self.result.state_root = self.state.state_root(self.alloc.state)
self.result.tx_root = self.trie.root(transactions_trie)
self.result.receipt_root = self.trie.root(receipts_trie)
Expand Down Expand Up @@ -509,6 +528,6 @@ def run(self) -> int:
self.logger.info(f"Wrote result to {result_output_path}")

if json_output:
json.dump(json_output, sys.stdout, indent=4)
json.dump(json_output, self.out_file, indent=4)

return 0
Loading
Loading