Add basic vyper-run with vdb support.
This commit is contained in:
parent
258e5df121
commit
ab9a8bf82c
|
@ -11,7 +11,6 @@ build
|
|||
eggs
|
||||
.eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
|
|
|
@ -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.')
|
15
setup.py
15
setup.py
|
@ -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',
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue