├── .gitignore ├── README.md ├── components ├── __init__.py ├── breakpoint.py ├── contract.py ├── instruction.py ├── jump.py ├── memory.py ├── step.py ├── transaction.py ├── utils.py └── vm.py ├── config.py ├── edb.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | *.pyc 4 | .vscode -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EDB 以太坊单合约交易调试工具 2 | 3 | ### Idea 4 | 5 | 在刷题的时候遇到一类```JOP```(Jump-Oriented-Programming)的题目,fuzz或者调试这类题目缺少简单易用的工具,由此开发了一个简单的调试工具```EDB```(The Ethereum Debugger),利用```debug_traceTransaction```的rpc调用实现调用复现,目前仅支持单合约调试,由于关闭了Memory下载,因此大部分情况下不会出现geth崩溃的问题(参考QWB2020线下赛EthGaMe),Memory采用手动模拟实现,缺点是跨合约指令较为繁琐,目前没有精力全部模拟,只支持单合约交易的调试 6 | 7 | ### 环境要求 8 | 9 | 1. python >= 3.6 10 | 2. 开启```debug_traceTransaction```RPC调用的以太坊网络,推荐[ganache](https://github.com/trufflesuite/ganache-cli) 11 | 3. pip install -r requirements.txt 12 | 4. Windows平台没有经过测试,建议在Linux和macOS系统使用 13 | 14 | 15 | ### 说明 16 | 17 | 1. 修改```config.py```中的```ENDPOINT_URI``` 18 | 2. 执行```python edb.py TXHASH``` 19 | 20 | ### 调试命令 21 | 22 | 1. ```n [delta]```: 执行下一条或下delta条指令,可以为负数 23 | 2. ```r```: 顺序执行直到下一个断点或结束 24 | 3. ```rb```: 逆序执行直到上一个断点或起始 25 | 4. ```b op=sha3;sta[-2]=0x100```: 下条件断点,可用条件如下: 26 | a. op: 操作码 27 | b. sta[xx]: 栈元素 28 | c. sto[xx]: storage元素 29 | d. pc: pc指针 30 | 5. ```p mem|sto|sta```: 打印全部Memory或Storage或Stack,限制大小可以在```config.py```中修改 31 | 6. ```x mem[a:b]|sto[k]|sta[k]```: 打印Memory或Storage或Stack上特定位置的数值 32 | 7. ```db i```: 删除第i个断点 33 | 8. ```g step```: 跳转到step位置 34 | 9. ```j```: 打印当前跳转栈 35 | 10. ```ws k```: 向后执行直到stack[k]的元素发生变化,用于追踪栈上元素来源 36 | 11. ```q```: 退出 -------------------------------------------------------------------------------- /components/__init__.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | from components.vm import DebugVM 3 | from components.memory import Memory 4 | from components.breakpoint import Breakpoint 5 | from components.contract import BytecodeContract 6 | -------------------------------------------------------------------------------- /components/breakpoint.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | 3 | 4 | class Breakpoint(): 5 | def __init__(self, conditions: str) -> None: 6 | self.condition_str = conditions 7 | conditions = conditions.split(';') 8 | self.conditions = [] 9 | for c in conditions: 10 | c = c.strip().replace(' ', '') 11 | c = c.replace('=', '==').replace('====', '==') # 防止意外赋值 12 | if c.startswith('op'): 13 | # e.g. op==sha3 14 | op = c.split('=')[-1].upper() 15 | self.conditions.append(f"self.op == '{op}'") 16 | elif c.startswith("sta["): 17 | # e.g. sta[-1] == 0x123 18 | left, right = c.split(']') 19 | offset = left.split('[')[-1] 20 | self.conditions.append(f"int(self.stack[{offset}],16){right}") 21 | elif c.startswith("sto["): 22 | # e.g. sto[0x1] == 0x123 23 | left, right = c.split(']') 24 | offset = left.split('[')[-1] 25 | offset = hex(eval(offset))[2:].zfill(64) 26 | self.conditions.append( 27 | f"int(self.storage['{offset}'],16){right}") 28 | elif c.startswith("pc"): 29 | self.conditions.append(f"self.{c}") 30 | else: 31 | print(f"[w] Cannot parse condition: {c}") 32 | 33 | def inspect(self): 34 | for c in self.conditions: 35 | print(f" {c}") 36 | -------------------------------------------------------------------------------- /components/contract.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | from components.instruction import Ins 3 | 4 | 5 | class BytecodeContract(): 6 | def __init__(self, addr, bytecode) -> None: 7 | print("[*] Disassembling bytecode via pyevmasm") 8 | code = os.popen(f'echo {bytecode} | evmasm -d').read() 9 | code = code.split('\n') 10 | self.pc2ins = {} 11 | 12 | for c in code: 13 | if c: 14 | ins = Ins(c.strip()) 15 | assert ins.addr not in self.pc2ins, "PC duplicated!" 16 | self.pc2ins[ins.addr] = ins 17 | print(f"[i] Loaded {len(self.pc2ins)} instructions of {addr}") 18 | 19 | self.addr = addr 20 | self.bytecode = bytecode 21 | self.codehash = w3.sha3(hexstr=bytecode) # 用codehash映射到contract 22 | 23 | def ins_at(self, pc) -> Ins: 24 | return self.pc2ins[pc] 25 | -------------------------------------------------------------------------------- /components/instruction.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | 3 | 4 | class Ins(): 5 | def __init__(self, addr_op: str) -> None: 6 | addr, op_s = addr_op.split(':') 7 | self.addr = int(addr.strip(), 16) 8 | splited = op_s.strip().split(' ') 9 | self.opcode = splited[0] 10 | self.oprand = splited[1:] 11 | 12 | def short_str(self): 13 | return f"{self.opcode} {' '.join(self.oprand)}".strip() 14 | 15 | def __str__(self) -> str: 16 | return f"{self.addr}: {self.opcode} {' '.join(self.oprand)}".strip() 17 | -------------------------------------------------------------------------------- /components/jump.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | 3 | 4 | class Jump(): 5 | def __init__(self, cur, src_ins, dst_ins, stack: list) -> None: 6 | self.src_cur = cur 7 | self.dst_cur = cur + 1 8 | self.src_ins = src_ins 9 | self.dst_ins = dst_ins 10 | self.stack = stack 11 | 12 | def __str__(self) -> str: 13 | pass 14 | -------------------------------------------------------------------------------- /components/memory.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | 3 | 4 | class Memory(): 5 | def __init__(self) -> None: 6 | self._mem = {} # 每0x20存一段 7 | 8 | def copy(self): 9 | new = Memory() 10 | new._mem = copy.copy(self._mem) 11 | return new 12 | 13 | def set(self, offset, length, value): 14 | assert len(value) == length * \ 15 | 2, f"memory.set len mismatch {length}: {value}" 16 | start = offset // 0x20 * 0x20 17 | end = math.ceil((offset + length) / 0x20 - 1) * 0x20 18 | assert end >= start 19 | 20 | prefix = self._mem.get(start, ZERODATA) 21 | prefix = prefix[:(offset-start)*2] 22 | suffix = self._mem.get(end, ZERODATA) 23 | suffix = suffix[(offset+length-end)*2:] 24 | value = prefix + value + suffix 25 | assert len(value) % 0x40 == 0, f"Wrong length {len(value)}" 26 | 27 | vi = 0 28 | for i in range(start, end+0x20, 0x20): 29 | self._mem[i] = value[vi:vi+0x40] 30 | vi += 0x40 31 | 32 | def get(self, offset, length): 33 | start = offset // 0x20 * 0x20 34 | end = math.ceil((offset + length) / 0x20) * 0x20 35 | res = "" 36 | for i in range(start, end, 0x20): 37 | data = self._mem.get(i, ZERODATA) 38 | res += data 39 | if offset+length-end == 0: 40 | res = res[(offset-start)*2:] 41 | else: 42 | res = res[(offset-start)*2:(offset+length-end)*2] 43 | assert len(res) == length*2, "[x] Wrong ret length" 44 | return res 45 | 46 | def show(self, max_size): 47 | print("Memory:") 48 | l = min(max_size, len(self._mem)) 49 | keys = sorted(self._mem.keys()) 50 | padding = len(hex(keys[-1])[2:]) if keys else 4 51 | for i in range(l): 52 | k = keys[i] 53 | print(f" 0x{hex(k)[2:].zfill(padding)}: ", end='') 54 | print(f" {C.HEADER}{self._mem[k]}{C.ENDC}") 55 | if len(self._mem) > max_size: 56 | print(f" ...") 57 | if not self._mem: 58 | print(f" (empty)") 59 | print() 60 | -------------------------------------------------------------------------------- /components/step.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | from components.contract import BytecodeContract 3 | from components.memory import Memory 4 | from components.instruction import Ins 5 | from components.breakpoint import Breakpoint 6 | from components.jump import Jump 7 | 8 | 9 | class Step(): 10 | def __init__(self, cur: int, log, contract: BytecodeContract, calldata: str, last_step=None) -> None: 11 | self.pc = log["pc"] 12 | self.depth = log['depth'] 13 | self.gas = log['gas'] 14 | self.gasCost = log['gasCost'] 15 | self.storage = log['storage'] 16 | self.stack = log['stack'] 17 | self.op = log['op'] 18 | self.log = log 19 | self.cur = cur 20 | self.ins: Ins = contract.ins_at(self.pc) 21 | 22 | if last_step: 23 | self.memory = last_step.memory.copy() 24 | self.update_memory(contract, calldata, last_step) 25 | self.jumps = copy.copy(last_step.jumps) 26 | self.update_jump(last_step) 27 | else: 28 | self.memory = Memory() 29 | self.jumps = [] 30 | assert self.op in self.ins.opcode, f"{self.ins} {self.op} {self.pc}" 31 | 32 | def print_jump(self): 33 | print("Jump:\n", end='') 34 | for j in self.jumps: 35 | print( 36 | f" from: {j.src_cur} pc = {j.src_ins.addr} = {hex(j.src_ins.addr)} ->", end='') 37 | print( 38 | f" to: {j.dst_cur} pc = {j.dst_ins.addr} = {hex(j.dst_ins.addr)}", end='') 39 | print(f" {C.OKCYAN}{j.src_ins.short_str()}{C.ENDC}", end='') 40 | print(f" stack: [{', '.join(short_stack(j.stack))}]") 41 | print() 42 | 43 | def update_jump(self, last): 44 | if last.op == 'JUMP': 45 | pass 46 | elif last.op == 'JUMPI': 47 | dst = int(last.stack[-1], 16) 48 | if dst != self.pc: 49 | return 50 | else: 51 | return 52 | jump = Jump(last.cur, last.ins, self.ins, last.stack) 53 | self.jumps.append(jump) 54 | 55 | def update_memory(self, contract: BytecodeContract, calldata: str, last): 56 | if last.op == 'MSTORE': 57 | assert len( 58 | last.stack) >= 2, f"{last.op} Stack under flow: {last.stack}" 59 | offset = int(last.stack[-1], 16) 60 | value = last.stack[-2] 61 | self.memory.set(offset, 0x20, value) 62 | elif last.op == 'MSTORE8': 63 | assert len( 64 | last.stack) >= 2, f"{last.op} Stack under flow: {last.stack}" 65 | offset = int(last.stack[-1], 16) 66 | value = last.stack[-2][-2:] 67 | self.memory.set(offset, 0x1, value) 68 | elif last.op == 'CALLDATACOPY': 69 | assert len( 70 | last.stack) >= 3, f"{last.op} Stack under flow: {last.stack}" 71 | destoffset = int(last.stack[-1], 16) 72 | offset = int(last.stack[-2], 16) 73 | length = int(last.stack[-3], 16) 74 | # calldata末尾补0... 75 | data = calldata[(offset)*2:(offset+length)*2].ljust(length*2, '0') 76 | self.memory.set(destoffset, length, data) 77 | elif last.op == 'CODECOPY': 78 | assert len( 79 | last.stack) >= 3, f"{last.op} Stack under flow: {last.stack}" 80 | destoffset = int(last.stack[-1], 16) 81 | offset = int(last.stack[-2], 16) 82 | length = int(last.stack[-3], 16) 83 | data = contract.bytecode[( 84 | offset)*2:(offset+length)*2].ljust(length*2, '0') 85 | self.memory.set(destoffset, length, data) 86 | elif last.op == 'EXTCODECOPY': 87 | raise Exception("EXTCODECOPY Not Implemented") 88 | elif last.op == 'RETURNDATACOPY': 89 | raise Exception("RETURNDATACOPY Not Implemented") 90 | elif last.op.endswith("CALL"): 91 | raise Exception(f"{last.op} Not Implemented") 92 | elif last.op == 'MLOAD': 93 | # Test My Memory 94 | offset = int(last.stack[-1], 16) 95 | real = self.stack[-1] 96 | if last.memory.get(offset, 0x20) != real: 97 | print(f"Memory bad {self.cur}") 98 | print(real) 99 | print(last.memory.get(offset, 0x20)) 100 | print(offset) 101 | last.memory.show(Z_MAX) 102 | exit(-1) 103 | 104 | def match(self, bp: Breakpoint) -> bool: 105 | for c in bp.conditions: 106 | try: 107 | flag = eval(c) 108 | except: 109 | # 有可能offset不在其中 110 | flag = False 111 | if not flag: 112 | return False 113 | return True 114 | -------------------------------------------------------------------------------- /components/transaction.py: -------------------------------------------------------------------------------- 1 | from components.utils import * 2 | 3 | 4 | class FakeTransaction(): 5 | # 方便解析internal transaction 6 | def __init__(self, caller, to, data, value) -> None: 7 | self.caller = caller 8 | self.to = to 9 | -------------------------------------------------------------------------------- /components/utils.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import sys 3 | import math 4 | import os 5 | import copy 6 | import rlp 7 | from sha3 import keccak_256 8 | from web3 import Web3 9 | from eth_utils import remove_0x_prefix 10 | from pyevmasm import instruction_tables 11 | from config import * 12 | instruction_table = {} 13 | ZERODATA = '0' * 64 14 | 15 | 16 | class C: 17 | HEADER = '\033[95m' 18 | OKBLUE = '\033[94m' 19 | OKCYAN = '\033[96m' 20 | OKGREEN = '\033[92m' 21 | WARNING = '\033[93m' 22 | FAIL = '\033[91m' 23 | ENDC = '\033[0m' 24 | BOLD = '\033[1m' 25 | UNDERLINE = '\033[4m' 26 | 27 | 28 | def init_instruction_table(): 29 | global instruction_table 30 | instruction_table = {} 31 | for ik in instruction_tables["serenity"].keys(): 32 | ins = instruction_tables["serenity"][ik] 33 | name = ins.name 34 | if name in ["DUP", "PUSH"]: 35 | name = name + str(ins.operand) 36 | assert name not in instruction_table 37 | instruction_table[name] = ins 38 | 39 | 40 | def short_stack(stack): 41 | ret = [] 42 | for v in stack: 43 | ret.append(hex(int(v, 16))) 44 | return ret 45 | 46 | 47 | def parse(arg): 48 | 'Convert a series of zero or more numbers to an argument tuple' 49 | try: 50 | return tuple(map(int, arg.split())) 51 | except: 52 | return [] 53 | 54 | 55 | def create_addr(creator: str, nonce: int): 56 | return keccak_256(rlp.encode( 57 | [bytes.fromhex(remove_0x_prefix(creator)), nonce]) 58 | ).hexdigest()[-40:] 59 | 60 | 61 | w3 = Web3(Web3.HTTPProvider(ENDPOINT_URI)) 62 | init_instruction_table() 63 | -------------------------------------------------------------------------------- /components/vm.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | import cmd 3 | from components.utils import * 4 | from components.contract import BytecodeContract 5 | from components.step import * 6 | 7 | 8 | class DebugVM(cmd.Cmd): 9 | intro = None 10 | real_intro = 'Welcome to the EDB shell. Type help or ? to list commands.\n' 11 | file = None 12 | 13 | def __init__(self, txhash: str) -> None: 14 | self.txhash = txhash 15 | info = w3.eth.getTransaction(txhash) 16 | contract_addr = info["to"] 17 | self.caller = info["from"] 18 | self.calldata = info['input'] # 是不是不应该放这里 19 | self.block_number = info['blockNumber'] - 1 20 | if self.calldata: 21 | self.calldata = remove_0x_prefix(self.calldata) 22 | print(f"[*] Calldata: {self.calldata}") 23 | code = w3.eth.getCode( 24 | contract_addr, block_identifier=self.block_number).hex() 25 | code = remove_0x_prefix(code) 26 | self.contract = BytecodeContract(contract_addr, code) 27 | 28 | self.steps = [] 29 | self.load_trace() 30 | 31 | self.total = len(self.steps) 32 | self.cur = 0 33 | self.breakpoints = [] 34 | super(DebugVM, self).__init__() 35 | 36 | def load_trace(self): 37 | trace = w3.provider.make_request( 38 | 'debug_traceTransaction', [self.txhash, {"disableMemory": True}] 39 | ) 40 | trace = trace['result']['structLogs'] 41 | print(f"[i] Loaded {len(trace)} steps") 42 | last_step = None 43 | cur = 0 44 | for s in trace: 45 | step = Step(cur, s, self.contract, self.calldata, last_step) 46 | self.steps.append(step) 47 | last_step = step 48 | cur += 1 49 | 50 | def start(self): 51 | print(self.real_intro) 52 | self.info() 53 | while True: 54 | try: 55 | self.cmdloop() 56 | break 57 | except KeyboardInterrupt: 58 | break 59 | 60 | def check_cur(self): 61 | self.cur = min(self.total-1, self.cur) 62 | self.cur = max(0, self.cur) 63 | 64 | def do_n(self, args): 65 | "n [delta]: Next delta steps" 66 | args = parse(args) 67 | delta = 1 68 | if args: 69 | delta = args[0] 70 | self.cur += delta 71 | self.check_cur() 72 | self.info() 73 | 74 | def _run(self, delta): 75 | broken = False 76 | while True: 77 | self.cur += delta 78 | if not 0 <= self.cur < self.total: 79 | break 80 | bcnt = 0 81 | for b in self.breakpoints: 82 | if self.steps[self.cur].match(b): 83 | print( 84 | f"Breakpoint #{bcnt} {C.OKGREEN}{b.condition_str}{C.ENDC} matched") 85 | broken = True 86 | break 87 | bcnt += 1 88 | if broken: 89 | break 90 | self.check_cur() 91 | self.info() 92 | if not broken: 93 | print("[*] Transaction finished without match any breakpoints") 94 | 95 | def do_rb(self, args): 96 | "rb: Run backward until match breakpoints" 97 | self._run(-1) 98 | 99 | def do_r(self, args): 100 | "r: Run until match breakpoints" 101 | self._run(1) 102 | 103 | def do_b(self, args): 104 | """ 105 | b [exp]: Add breakpoint with exp or show breakpoints 106 | e.g b op==sha3;sta[-2]==0x100 107 | 可用条件如下: 108 | op: 操作码 109 | sta[xx]: 栈元素 110 | sto[xx]: storage元素 111 | pc: pc 112 | """ 113 | print() 114 | if not args.strip(): 115 | self.show_breakpoints() 116 | return 117 | self.breakpoints.append(Breakpoint(args)) 118 | print(f"Breakpoint #{len(self.breakpoints)-1} added:") 119 | self.breakpoints[-1].inspect() 120 | 121 | def show_breakpoints(self): 122 | print("Breakpoints:") 123 | bcnt = 0 124 | for b in self.breakpoints: 125 | print(f" #{bcnt}: {C.OKGREEN}{b.condition_str}{C.ENDC}") 126 | bcnt += 1 127 | if not self.breakpoints: 128 | print(f" (empty)") 129 | print() 130 | return 131 | 132 | def do_p(self, args): 133 | "p mem|sto|sta: Print memory or storage or stack (full print)" 134 | args = args.strip() 135 | if args == 'mem': 136 | self.print_memory(Z_MAX) 137 | elif args == 'sto': 138 | self.print_storage(Z_MAX) 139 | elif args == 'sta': 140 | self.print_stack() 141 | else: 142 | print(f"[x] Unkown {args} to print") 143 | 144 | def do_x(self, args): 145 | "x mem[a:b]|sto[k]|sta[k]: Print memory or storage or stack value at specified position" 146 | args = args.strip() 147 | if args.startswith('mem'): 148 | l, r = map(eval, args.split('[')[-1].strip(']').split(':')) 149 | print(self.steps[self.cur].memory.get(l, r-l)) 150 | elif args.startswith('sto'): 151 | k = eval(args.split('[')[-1].strip(']')) 152 | k = hex(k)[2:].zfill(64) 153 | record = self.steps[self.cur].storage.get(k, None) 154 | if not record: 155 | print("No record, loading origin data from chain") 156 | record = self.get_old_storage(self.contract.addr, k) 157 | print(record) 158 | elif args.startswith('sta'): 159 | k = eval(args.split('[')[-1].strip(']')) 160 | print(self.steps[self.cur].stack[k]) 161 | else: 162 | print(f"[x] Unkown {args} to exp") 163 | 164 | def do_db(self, args): 165 | "db [k]: Delete breakpoint #k" 166 | print() 167 | args = parse(args) 168 | if not args or args[0] >= len(self.breakpoints) or args[0] < 0: 169 | print("Invalid breakpoint id to delete") 170 | print( 171 | f"Deleted breakpoint #{args[0]}: {C.OKGREEN}{self.breakpoints[args[0]].condition_str}{C.ENDC}") 172 | del self.breakpoints[args[0]] 173 | self.show_breakpoints() 174 | 175 | def do_g(self, args): 176 | "goto step: Goto step" 177 | args = parse(args) 178 | if not args: 179 | print("Wrong destination") 180 | return 181 | self.cur = args[0] - 1 182 | self.check_cur() 183 | self.info() 184 | 185 | def do_q(self, args): 186 | "Quit" 187 | return True 188 | 189 | def do_j(self, args): 190 | "j: Print jump info" 191 | self.print_jump() 192 | 193 | def do_ws(self, args): 194 | "ws stack_k: Run back to watch who changed stack[k]" 195 | args = parse(args) 196 | k = args[0] 197 | if k < 0: 198 | k += len(self.steps[self.cur].stack) 199 | check_stack = self.steps[self.cur].stack[:k+1] 200 | found = False 201 | while True: 202 | self.cur -= 1 203 | if not 0 <= self.cur < self.total: 204 | break 205 | if self.steps[self.cur].stack[:k+1] != check_stack: 206 | found = True 207 | break 208 | self.check_cur() 209 | self.info() 210 | if not found: 211 | print("[*] Cannot found who changed the stack") 212 | 213 | def print_stack(self): 214 | stack = self.steps[self.cur].stack 215 | print("Stack:") 216 | l = len(stack) 217 | for i in range(l): 218 | print( 219 | f" {C.WARNING}{stack[i]}{C.ENDC} ({str(l-i-1) + ' from ' if i max_size: 244 | print(f" ...") 245 | if not storage: 246 | print(" (empty)") 247 | print() 248 | 249 | def print_memory(self, max_size=MAX_SIZE): 250 | memory: Memory = self.steps[self.cur].memory 251 | memory.show(max_size) 252 | 253 | def print_jump(self): 254 | self.steps[self.cur].print_jump() 255 | 256 | @property 257 | def prompt(self): 258 | return f"debug({self.contract.addr})> " 259 | 260 | def info(self): 261 | print() 262 | self.print_memory() 263 | self.print_storage() 264 | self.print_stack() 265 | # self.print_jump() 266 | self.print_pc() 267 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ENDPOINT_URI = "http://127.0.0.1:7545" 2 | MAX_SIZE = 24 # 默认打印的memory和storage数量 3 | Z_MAX = 128 # 可以手动调节更大 4 | -------------------------------------------------------------------------------- /edb.py: -------------------------------------------------------------------------------- 1 | # coding=utf8 2 | from components import * 3 | 4 | 5 | def main(): 6 | txhash = sys.argv[1] 7 | vm = DebugVM(txhash) 8 | vm.start() 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | eth_utils>=1.9.5 2 | web3>=5.19.0 3 | pyevmasm>=0.2.3 4 | rlp==2.0.1 --------------------------------------------------------------------------------