Skip to content

Commit

Permalink
Merge branch 'master' into feat/ir_compiler
Browse files Browse the repository at this point in the history
  • Loading branch information
charles-cooper committed Sep 8, 2023
2 parents b93f6c5 + 1ae1783 commit d7b6209
Show file tree
Hide file tree
Showing 16 changed files with 326 additions and 61 deletions.
2 changes: 2 additions & 0 deletions boa/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import contextlib
import sys

import eth.exceptions

from boa.debugger import BoaDebug
from boa.environment import Env, enable_pyevm_verbose_logging, patch_opcode
from boa.interpret import BoaError, load, load_abi, load_partial, loads, loads_abi, loads_partial
Expand Down
21 changes: 0 additions & 21 deletions boa/coverage.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,3 @@
"""
The titanoboa coverage plugin.
Usage: add the following to `.coveragerc`
```
[run]
plugins = boa.coverage
```
(for more information see https://coverage.readthedocs.io/en/latest/config.html)
Then, run with `coverage run ...`
With `pytest-cov`, it can be invoked in either of two ways,
`coverage run -m pytest ...`
or,
`pytest --cov= ...`
Coverage is experimental and there may be odd corner cases! If so,
please report them on github or in the discord.
"""


from functools import cached_property

import coverage.plugin
Expand Down
27 changes: 25 additions & 2 deletions boa/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ def get_gas_price(self):

def _init_vm(self, reset_traces=True):
self.vm = self.chain.get_vm()
self.vm.patch = VMPatcher(self.vm)

c = type(
"TitanoboaComputation",
Expand All @@ -435,8 +436,6 @@ def _init_vm(self, reset_traces=True):
c.opcodes[0x20] = Sha3PreimageTracer(c.opcodes[0x20], self)
c.opcodes[0x55] = SstoreTracer(c.opcodes[0x55], self)

self.vm.patch = VMPatcher(self.vm)

def _trace_sha3_preimage(self, preimage, image):
self.sha3_trace[image] = preimage

Expand Down Expand Up @@ -504,6 +503,8 @@ def _lookup_contract_fast(self, address: PYEVM_Address):
return self._contracts.get(address)

def lookup_contract(self, address: _AddressType):
if address == b"":
return None
return self._contracts.get(Address(address).canonical_address)

def alias(self, address, name):
Expand Down Expand Up @@ -603,6 +604,28 @@ def deploy_code(

return target_address, c.output

def raw_call(
self,
to_address,
sender: Optional[AddressType] = None,
gas: Optional[int] = None,
value: int = 0,
data: bytes = b"",
):
# simple wrapper around `execute_code` to help simulate calling
# a contract from an EOA.
ret = self.execute_code(
to_address=to_address, sender=sender, gas=gas, value=value, data=data
)
if ret.is_error:
# differ from execute_code, consumers of execute_code want to get
# error returned "silently" (not thru exception handling mechanism)
# whereas users of call() expect the exception to be thrown, just
# like a regular contract call.
raise ret.error

return ret

def execute_code(
self,
to_address: _AddressType = constants.ZERO_ADDRESS,
Expand Down
39 changes: 28 additions & 11 deletions boa/interpret.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def set_cache_dir(cache_dir="~/.cache/titanoboa"):
_disk_cache = DiskCache(cache_dir, compiler_version)


def compiler_data(source_code: str, contract_name: str) -> CompilerData:
def compiler_data(source_code: str, contract_name: str, **kwargs) -> CompilerData:
global _disk_cache

def _ifaces():
Expand All @@ -42,16 +42,16 @@ def _ifaces():

if _disk_cache is None:
ifaces = _ifaces()
ret = CompilerData(source_code, contract_name, interface_codes=ifaces)
ret = CompilerData(source_code, contract_name, interface_codes=ifaces, **kwargs)
return ret

def func():
ifaces = _ifaces()
ret = CompilerData(source_code, contract_name, interface_codes=ifaces)
ret.bytecode_runtime # force compilation to happen
ret = CompilerData(source_code, contract_name, interface_codes=ifaces, **kwargs)
_ = ret.bytecode_runtime # force compilation to happen
return ret

return _disk_cache.caching_lookup(source_code, func)
return _disk_cache.caching_lookup(str((kwargs, source_code)), func)


def load(filename: str, *args, **kwargs) -> _Contract: # type: ignore
Expand All @@ -63,8 +63,16 @@ def load(filename: str, *args, **kwargs) -> _Contract: # type: ignore
return loads(f.read(), *args, name=name, **kwargs, filename=filename)


def loads(source_code, *args, as_blueprint=False, name=None, filename=None, **kwargs):
d = loads_partial(source_code, name, filename=filename)
def loads(
source_code,
*args,
as_blueprint=False,
name=None,
filename=None,
compiler_args=None,
**kwargs,
):
d = loads_partial(source_code, name, filename=filename, compiler_args=compiler_args)
if as_blueprint:
return d.deploy_as_blueprint(**kwargs)
else:
Expand All @@ -83,18 +91,27 @@ def loads_abi(json_str: str, *args, name: str = None, **kwargs) -> ABIContractFa


def loads_partial(
source_code: str, name: str = None, filename: str = None, dedent: bool = True
source_code: str,
name: str = None,
filename: str = None,
dedent: bool = True,
compiler_args: dict = None,
) -> VyperDeployer:
name = name or "VyperContract" # TODO handle this upstream in CompilerData
if dedent:
source_code = textwrap.dedent(source_code)
data = compiler_data(source_code, name)

compiler_args = compiler_args or {}

data = compiler_data(source_code, name, **compiler_args)
return VyperDeployer(data, filename=filename)


def load_partial(filename: str) -> VyperDeployer: # type: ignore
def load_partial(filename: str, compiler_args=None) -> VyperDeployer: # type: ignore
with open(filename) as f:
return loads_partial(f.read(), name=filename, filename=filename)
return loads_partial(
f.read(), name=filename, filename=filename, compiler_args=compiler_args
)


__all__ = ["BoaError"]
54 changes: 37 additions & 17 deletions boa/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,15 +181,17 @@ def execute_code(
assert computation.is_error
return computation

output = trace.returndata_bytes
# gas_used = to_int(receipt["gasUsed"])

# the node reverted but we didn't. consider this an unrecoverable
# error and bail out
if trace.is_error and not computation.is_error:
raise RuntimeError(
f"panic: local computation succeeded but node didnt: {trace}"
)
output = None
if trace is not None:
output = trace.returndata_bytes
# gas_used = to_int(receipt["gasUsed"])

# the node reverted but we didn't. consider this an
# unrecoverable error and bail out
if trace.is_error and not computation.is_error:
raise RuntimeError(
f"panic: local computation succeeded but node didnt: {trace}"
)

else:
args = _fixup_dict(
Expand All @@ -212,9 +214,10 @@ def execute_code(
# not the greatest, but we will just patch the returndata and
# pretend nothing happened (which is not really a problem unless
# the caller wants to inspect the trace or memory).
if computation.output != output:
if output is not None and computation.output != output:
warnings.warn(
"local fork did not match node! this likely indicates a bug in titanoboa!",
"local fork did not match node! this indicates state got out "
"of sync with the network or a bug in titanoboa!",
stacklevel=2,
)
# just return whatever the node had.
Expand All @@ -238,19 +241,25 @@ def deploy_code(self, sender=None, gas=None, value=0, bytecode=b"", **kwargs):

create_address = to_canonical_address(receipt["contractAddress"])

if local_bytecode != trace.returndata_bytes:
deployed_bytecode = local_bytecode

if trace is not None and local_bytecode != trace.returndata_bytes:
# not sure what to do about this, for now just complain
warnings.warn(
"local fork did not match node! this likely indicates a bug in titanoboa!",
stacklevel=1,
"local fork did not match node! this indicates state got out "
"of sync with the network or a bug in titanoboa!",
stacklevel=2,
)
# return what the node returned anyways.
deployed_bytecode = trace.returndata_bytes

if local_address != create_address:
raise RuntimeError(f"uh oh! {local_address} != {create_address}")

# TODO get contract info in here
print(f"contract deployed at {to_checksum_address(create_address)}")

return create_address, trace.returndata_bytes
return create_address, deployed_bytecode

def _wait_for_tx_trace(self, tx_hash, timeout=60, poll_latency=0.25):
start = time.time()
Expand All @@ -262,7 +271,9 @@ def _wait_for_tx_trace(self, tx_hash, timeout=60, poll_latency=0.25):
raise ValueError(f"Timed out waiting for ({tx_hash})")
time.sleep(poll_latency)

trace = self._rpc.fetch("debug_traceTransaction", [tx_hash, self._tracer])
trace = None
if self._tracer is not None:
trace = self._rpc.fetch("debug_traceTransaction", [tx_hash, self._tracer])
return receipt, trace

@cached_property
Expand All @@ -275,6 +286,14 @@ def _tracer(self):
self._rpc.fetch("debug_traceTransaction", [txn_hash, call_tracer])
except RPCError as e:
if e.code == -32601:
warnings.warn(
"debug_traceTransaction not available! "
"titanoboa will try hard to interact with the network, but "
"this means that titanoboa is not able to do certain "
"safety checks at runtime. it is recommended to switch "
"to a node or provider with debug_traceTransaction.",
stacklevel=2,
)
return None
# can't handle callTracer, use default (i.e. structLogs)
if e.code == -32602:
Expand Down Expand Up @@ -344,4 +363,5 @@ def _send_txn(self, from_, to=None, gas=None, value=None, data=None):
# the block was mined, reset state
self._reset_fork(block_identifier=receipt["blockNumber"])

return receipt, TraceObject(trace)
t_obj = TraceObject(trace) if trace is not None else None
return receipt, t_obj
4 changes: 2 additions & 2 deletions boa/precompile.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def decorator(func):
vy_ast = parse_to_ast(user_signature + ": view").body[0]
func_t = ContractFunctionT.from_FunctionDef(vy_ast, is_interface=True)

args_t = TupleT(tuple(func_t.arguments.values()))
args_t = TupleT(tuple(func_t.argument_types))

def wrapper(computation):
# Decode input arguments from message data
Expand All @@ -88,7 +88,7 @@ def wrapper(computation):
address = keccak256(user_signature.encode("utf-8"))[:20]
register_raw_precompile(address, wrapper, force=force)

args = list(func_t.arguments.items())
args = [(arg.name, arg.typ) for arg in func_t.arguments]
fn_name = func_t.name
builtin = PrecompileBuiltin(fn_name, args, func_t.return_type, address)

Expand Down
61 changes: 61 additions & 0 deletions boa/util/eip5202.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Optional, Any
import eth_typing
from vyper.utils import keccak256
from eth_utils import to_canonical_address, to_checksum_address


# TODO replace return type with upcoming AddressType wrapper
def get_create2_address(blueprint_bytecode: bytes, deployer_address: Any, salt: bytes) -> str:
_, _, initcode = parse_erc5202(blueprint_bytecode)

initcode_hash = keccak256(initcode)

prefix = b"\xFF"
addr = to_canonical_address(deployer_address)
if len(salt) != 32:
raise ValueError(f"bad salt (must be bytes32): {salt}")

create2_hash = keccak256(prefix + addr + salt + initcode_hash)

return to_checksum_address(create2_hash[12:])


# basically copied from ERC5202 reference implementation
def parse_erc5202(blueprint_bytecode: bytes) -> tuple[int, Optional[bytes], bytes]:
"""
Given bytecode as a sequence of bytes, parse the blueprint preamble and
deconstruct the bytecode into:
the ERC version, preamble data and initcode.
Raises an exception if the bytecode is not a valid blueprint contract
according to this ERC.
arguments:
blueprint_bytecode: a `bytes` object representing the blueprint bytecode
returns:
(version,
None if <length encoding bits> is 0, otherwise the bytes of the data section,
the bytes of the initcode,
)
"""
if blueprint_bytecode[:2] != b"\xFE\x71":
raise ValueError("Not a blueprint!")

erc_version = (blueprint_bytecode[2] & 0b11111100) >> 2

n_length_bytes = blueprint_bytecode[2] & 0b11
if n_length_bytes == 0b11:
raise ValueError("Reserved bits are set")

data_length = int.from_bytes(blueprint_bytecode[3 : 3 + n_length_bytes], byteorder="big")

if n_length_bytes == 0:
preamble_data = None
else:
data_start = 3 + n_length_bytes
preamble_data = blueprint_bytecode[data_start : data_start + data_length]

initcode = blueprint_bytecode[3 + n_length_bytes + data_length :]

if len(initcode) == 0:
raise ValueError("Empty initcode!")

return erc_version, preamble_data, initcode
Loading

0 comments on commit d7b6209

Please sign in to comment.