Add basic vyper-run with vdb support.

This commit is contained in:
Jacques Wagener 2018-07-12 18:57:04 +02:00
parent 258e5df121
commit ab9a8bf82c
No known key found for this signature in database
GPG Key ID: C294D1025DA0E923
4 changed files with 426 additions and 5 deletions

1
.gitignore vendored
View File

@ -11,7 +11,6 @@ build
eggs
.eggs
parts
bin
var
sdist
develop-eggs

187
bin/vyper-run Executable file
View File

@ -0,0 +1,187 @@
#!/usr/bin/env python3.6
import argparse
import vyper
from pprint import pprint
from vyper import compiler
from vyper.parser import (
parser,
)
from vyper.vdb import (
set_evm_opcode_debugger,
set_evm_opcode_pass
)
from eth_tester import (
EthereumTester,
)
from web3.providers.eth_tester import (
EthereumTesterProvider,
)
from web3 import (
Web3,
)
aparser = argparse.ArgumentParser(description='Vyper {0} quick CLI runner'.format(vyper.__version__))
aparser.add_argument('input_file', help='Vyper sourcecode to run')
aparser.add_argument('call_list', help='call list, without parameters: func, with parameters func(1, 2, 3). Semicolon separated')
aparser.add_argument('-i', help='init args, comma separated', default='', dest='init_args')
args = aparser.parse_args()
set_evm_opcode_pass() # by default just pass over the debug opcode.
def cast_types(args, abi_signature):
newargs = args.copy()
for idx, abi_arg in enumerate(abi_signature['inputs']):
if abi_arg['type'] in ('int128', 'uint256'):
newargs[idx] = int(args[idx])
elif abi_arg['type'].startswith('bytes'):
newargs[idx] = args[idx].encode()
return newargs
def get_tester():
tester = EthereumTester()
def zero_gas_price_strategy(web3, transaction_params=None):
return 0 # zero gas price makes testing simpler.
w3 = Web3(EthereumTesterProvider(tester))
w3.eth.setGasPriceStrategy(zero_gas_price_strategy)
return tester, w3
def get_contract(w3, source_code, *args, **kwargs):
abi = compiler.mk_full_signature(source_code)
bytecode = '0x' + compiler.compile(source_code).hex()
contract = w3.eth.contract(abi=abi, bytecode=bytecode)
value = kwargs.pop('value', 0)
value_in_eth = kwargs.pop('value_in_eth', 0)
value = value_in_eth * 10**18 if value_in_eth else value # Handle deploying with an eth value.
gasPrice = kwargs.pop('gasPrice', 0)
deploy_transaction = {
'from': w3.eth.accounts[0],
'data': contract._encode_constructor_data(args, kwargs),
'value': value,
'gasPrice': gasPrice
}
tx = w3.eth.sendTransaction(deploy_transaction)
address = w3.eth.getTransactionReceipt(tx)['contractAddress']
contract = w3.eth.contract(address, abi=abi, bytecode=bytecode)
# Filter logs.
contract._logfilter = w3.eth.filter({
'fromBlock': w3.eth.blockNumber - 1,
'address': contract.address
})
return contract
if __name__ == '__main__':
with open(args.input_file) as fh:
code = fh.read()
# Patch in vdb.
init_args = args.init_args.split(',') if args.init_args else []
tester, w3 = get_tester()
# Built list of calls to make.
calls = []
for signature in args.call_list.split(';'):
name = signature.strip()
args = []
if '(' in signature:
start_pos = signature.find('(')
name = signature[:start_pos]
args = signature[start_pos+1:-1].split(',')
args = [arg.strip() for arg in args]
args = [arg for arg in args if len(arg) > 0]
calls.append((name, args))
abi = compiler.mk_full_signature(code)
def serialise_var_rec(var_rec):
return {
'type': var_rec.typ.typ,
'size': var_rec.size * 32,
'position': var_rec.pos
}
_contracts, _events, _defs, _globals, _custom_units = parser.get_contracts_and_defs_and_globals(parser.parse(code))
source_map = {
'globals': {},
'locals': {}
}
source_map['globals'] = {
name: serialise_var_rec(var_record)
for name, var_record in _globals.items()
}
# Fetch context for each function.
lll = parser.parse_tree_to_lll(parser.parse(code), code, runtime_only=True)
contexts = {
f.func_name: f.context
for f in lll.args[1:] if hasattr(f, 'context')
}
prev_func_name = None
for _def in _defs:
func_info = {
'from_lineno': _def.lineno,
'variables': {}
}
# set local variables for specific function.
context = contexts[_def.name]
func_info['variables'] = {
var_name: serialise_var_rec(var_rec)
for var_name, var_rec in context.vars.items()
}
source_map['locals'][_def.name] = func_info
# set to_lineno
if prev_func_name:
source_map['locals'][prev_func_name]['to_lineno'] = _def.lineno
prev_func_name = _def.name
source_map['locals'][_def.name]['to_lineno'] = len(code.splitlines())
# Format init args.
if init_args:
init_abi = next(filter(lambda func: func["name"] == '__init__', abi))
init_args = cast_types(init_args, init_abi)
# Compile contract to chain.
contract = get_contract(w3, code, *init_args, language='vyper')
# Execute calls
for func_name, args in calls:
if not hasattr(contract.functions, func_name):
print('\n No method {} found, skipping.'.format(func_name))
continue
print('\n* Calling {}({})'.format(func_name, ','.join(args)))
func_abi = next(filter(lambda func: func["name"] == func_name, abi))
if len(args) != len(func_abi['inputs']):
print('Argument mismatch, please provide correct arguments.')
break
cast_args = cast_types(args, func_abi)
res = getattr(contract.functions, func_name)(*cast_args).call({'gas': func_abi['gas'] + 22000})
set_evm_opcode_debugger(source_code=code, source_map=source_map)
tx_hash = getattr(contract.functions, func_name)(*cast_args).transact({'gas': func_abi['gas'] + 22000})
set_evm_opcode_pass()
print('- Returns:')
pprint('{}'.format(res))
# Detect any new log events, and print them.
print('- Logs:')
event_names = [x['name'] for x in abi if x['type'] == 'event']
tx_receipt = w3.eth.getTransactionReceipt(tx_hash)
for event_name in event_names:
logs = getattr(contract.events, event_name)().processReceipt(tx_receipt)
for log in logs:
print(log.event + ":")
pprint(dict(log.args))
else:
print(' No events found.')

View File

@ -41,15 +41,19 @@ setup(
version='0.1.0-alpha.0',
description="""vyper-debug: Easy to use Vyper debugger | vdb""",
long_description_markdown_filename='README.md',
author='Jason Carver',
author_email='ethcalibur+pip@gmail.com',
author='Jacques Wagener',
author_email='jacques+pip@dilectum.co.za',
url='https://github.com/ethereum/vyper-debug',
include_package_data=True,
install_requires=[
"eth-utils>=1,<2",
"eth-tester==0.1.0b28",
"myvyper==0.1.0b1"
],
dependency_links=[
"git+https://github.com/ethereum/vyper.git#egg=myvyper-0.1.0b1"
],
setup_requires=['setuptools-markdown'],
python_requires='>=3.5, <4',
python_requires='>=3.6, <4',
extras_require=extras_require,
py_modules=['vdb'],
license="MIT",
@ -64,4 +68,7 @@ setup(
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: Implementation :: PyPy',
],
scripts=[
'bin/vyper-run',
]
)

228
vdb/vdb.py Normal file
View File

@ -0,0 +1,228 @@
import cmd
import evm
# from eth_hash.auto import keccak
from eth_utils import to_hex
from evm import constants
from evm.vm.opcode import as_opcode
from evm.utils.numeric import (
int_to_big_endian,
big_endian_to_int,
# ceil32
)
from vyper.opcodes import opcodes as vyper_opcodes
commands = [
'continue',
'locals',
'globals'
]
base_types = ('int128', 'uint256', 'address', 'bytes32')
def history():
import readline
for i in range(1, readline.get_current_history_length() + 1):
print("%3d %s" % (i, readline.get_history_item(i)))
logo = """
__ __
\ \ _ / /
\ v v / Vyper Debugger
\ / 0.0.0b1
\ / "help" to get a list of commands
v
"""
def print_var(value, var_typ):
if isinstance(value, int):
v = int_to_big_endian(value)
else:
v = value
if isinstance(v, bytes):
if var_typ == 'uint256':
print(big_endian_to_int(v))
elif var_typ == 'int128':
print('TODO!')
elif var_typ == 'address':
print(to_hex(v[12:]))
else:
print(v)
class VyperDebugCmd(cmd.Cmd):
def __init__(self, computation, line_no=None, source_code=None, source_map=None):
if source_map is None:
source_map = {}
self.computation = computation
self.prompt = '\033[92mvdb\033[0m> '
self.intro = logo
self.source_code = source_code
self.line_no = line_no
self.globals = source_map.get("globals")
self.locals = source_map.get("locals")
super().__init__()
def _print_code_position(self):
if not all((self.source_code, self.line_no)):
print('No source loaded')
return
lines = self.source_code.splitlines()
begin = self.line_no - 1 if self.line_no > 1 else 0
end = self.line_no + 1 if self.line_no < len(lines) else self.line_no
for idx, line in enumerate(lines[begin - 1:end]):
line_number = begin + idx
if line_number == self.line_no:
print("--> \033[92m{}\033[0m\t{}".format(line_number, line))
else:
print(" \033[92m{}\033[0m\t{}".format(line_number, line))
def preloop(self):
super().preloop()
self._print_code_position()
def postloop(self):
print('Exiting vdb')
super().postloop()
def do_state(self, *args):
""" Show current EVM state information. """
print('Block Number => {}'.format(self.computation.state.block_number))
print('Program Counter => {}'.format(self.computation.code.pc))
print('Memory Size => {}'.format(len(self.computation._memory)))
print('Gas Remaining => {}'.format(self.computation.get_gas_remaining()))
def do_globals(self, *args):
if not self.globals:
print('No globals found.')
print('Name\t\tType')
for name, info in self.globals.items():
print('self.{}\t\t{}'.format(name, info['type']))
def _get_fn_name_locals(self):
for fn_name, info in self.locals.items():
if info['from_lineno'] < self.line_no < info['to_lineno']:
return fn_name, info['variables']
return '', {}
def do_locals(self, *args):
if not self.locals:
print('No locals found.')
fn_name, variables = self._get_fn_name_locals()
print('Function: {}'.format(fn_name))
print('Name\t\tType')
for name, info in variables.items():
print('{}\t\t{}'.format(name, info['type']))
def default(self, line):
fn_name, local_variables = self._get_fn_name_locals()
if line.startswith('self.') and len(line) > 4:
if not self.globals:
print('No globals found.')
# print global value.
name = line.split('.')[1]
if name not in self.globals:
print('Global named "{}" not found.'.format(name))
else:
global_type = self.globals[name]['type']
slot = None
if global_type in base_types:
slot = self.globals[name]['position']
elif global_type == 'mapping':
# location_hash= keccak(int_to_big_endian(self.globals[name]['position']).rjust(32, b'\0'))
# slot = big_endian_to_int(location_hash)
pass
else:
print('Can not read global of type "{}".'.format(global_type))
if slot is not None:
value = self.computation.state.account_db.get_storage(
address=self.computation.msg.storage_address,
slot=slot,
)
print_var(value, global_type)
elif line in local_variables:
var_info = local_variables[line]
local_type = var_info['type']
if local_type in base_types:
start_position = var_info['position']
value = self.computation.memory_read(start_position, 32)
print_var(value, local_type)
else:
print('Can not read local of type ')
else:
self.stdout.write('*** Unknown syntax: %s\n' % line)
def do_stack(self, *args):
""" Show contents of the stack """
for idx, value in enumerate(self.computation._stack.values):
print("{}\t{}".format(idx, to_hex(value)))
else:
print("Stack is empty")
def do_pdb(self, *args):
# Break out to pdb for vdb debugging.
import pdb; pdb.set_trace() # noqa
def do_history(self, *args):
history()
def emptyline(self):
pass
def do_quit(self, *args):
return True
def do_exit(self, *args):
""" Exit vdb """
return True
def do_continue(self, *args):
""" Exit vdb """
return True
def do_EOF(self, line):
""" Exit vdb """
return True
original_opcodes = evm.vm.forks.byzantium.computation.ByzantiumComputation.opcodes
def set_evm_opcode_debugger(source_code=None, source_map=None):
def debug_opcode(computation):
line_no = computation.stack_pop(num_items=1, type_hint=constants.UINT256)
VyperDebugCmd(computation, line_no=line_no, source_code=source_code, source_map=source_map).cmdloop()
opcodes = original_opcodes.copy()
opcodes[vyper_opcodes['DEBUG'][0]] = as_opcode(
logic_fn=debug_opcode,
mnemonic="DEBUG",
gas_cost=0
)
setattr(evm.vm.forks.byzantium.computation.ByzantiumComputation, 'opcodes', opcodes)
def set_evm_opcode_pass():
def debug_opcode(computation):
computation.stack_pop(num_items=1, type_hint=constants.UINT256)
opcodes = original_opcodes.copy()
opcodes[vyper_opcodes['DEBUG'][0]] = as_opcode(
logic_fn=debug_opcode,
mnemonic="DEBUG",
gas_cost=0
)
setattr(evm.vm.forks.byzantium.computation.ByzantiumComputation, 'opcodes', opcodes)