def _generate_coverage_data(source_map_str: str, opcodes_str: str, contract_name: str, ast_json: List) -> Tuple: if not opcodes_str: return {}, {}, {} source_map = deque(expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) fn_nodes = [i for i in ast_json if i["ast_type"] == "FunctionDef"] fn_offsets = dict((i["name"], _convert_src(i["src"])) for i in fn_nodes) stmt_nodes = set( _convert_src(i["src"]) for i in _get_statement_nodes(fn_nodes)) statement_map: Dict = {} branch_map: Dict = {} pc_list: List = [] count, pc = 0, 0 while opcodes: # format of source is [start, stop, contract_id, jump code] source = source_map.popleft() pc_list.append({"op": opcodes.popleft(), "pc": pc}) if source[3] != "-": pc_list[-1]["jump"] = source[3] pc += 1 if opcodes and opcodes[0][:2] == "0x": pc_list[-1]["value"] = opcodes.popleft() pc += int(pc_list[-1]["op"][4:]) # set source offset (-1 means none) if source[0] == -1: if (len(pc_list) > 6 and pc_list[-7]["op"] == "CALLVALUE" and pc_list[-1]["op"] == "REVERT"): # special case - initial nonpayable check on vyper >=0.2.5 pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" # hackiness to prevent the source highlight from showing the entire contract pc_list[-2].update(path="0", offset=[0, 0]) continue offset = (source[0], source[0] + source[1]) pc_list[-1]["path"] = "0" pc_list[-1]["offset"] = offset try: if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = pc_list[-2]["fn"] else: # statement coverage fn = next(k for k, v in fn_offsets.items() if is_inside_offset(offset, v)) pc_list[-1]["fn"] = f"{contract_name}.{fn}" stmt_offset = next(i for i in stmt_nodes if is_inside_offset(offset, i)) stmt_nodes.remove(stmt_offset) statement_map.setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset pc_list[-1]["statement"] = count count += 1 except (KeyError, IndexError, StopIteration): pass if pc_list[-1]["op"] not in ("JUMPI", "REVERT"): continue node = _find_node_by_offset(ast_json, offset) if pc_list[-1]["op"] == "REVERT": # custom revert error strings if node["ast_type"] == "FunctionDef" and pc_list[-7][ "op"] == "CALLVALUE": pc_list[-1]["dev"] = "Cannot send ether to nonpayable function" elif node["ast_type"] == "Subscript": pc_list[-1]["dev"] = "Index out of range" elif node["ast_type"] in ("AugAssign", "BinOp"): if node["op"]["ast_type"] == "Sub": pc_list[-1]["dev"] = "Integer underflow" elif node["op"]["ast_type"] == "Div": pc_list[-1]["dev"] = "Division by zero" elif node["op"]["ast_type"] == "Mod": pc_list[-1]["dev"] = "Modulo by zero" else: pc_list[-1]["dev"] = "Integer overflow" continue if node["ast_type"] in ("Assert", "If") or ( node["ast_type"] == "Expr" and node["value"]["func"].get( "id", None) == "assert_modifiable"): # branch coverage pc_list[-1]["branch"] = count branch_map.setdefault(pc_list[-1]["fn"], {}) if node["ast_type"] == "If": branch_map[pc_list[-1]["fn"]][count] = _convert_src( node["test"]["src"]) + (False, ) else: branch_map[pc_list[-1]["fn"]][count] = offset + (True, ) count += 1 pc_list[0]["path"] = "0" pc_list[0]["offset"] = [0, _convert_src(ast_json[-1]["src"])[1]] pc_map = dict((i.pop("pc"), i) for i in pc_list) return pc_map, {"0": statement_map}, {"0": branch_map}
def _generate_coverage_data( source_map_str: str, opcodes_str: str, contract_node: Any, stmt_nodes: Dict, branch_nodes: Dict, has_fallback: bool, instruction_count: int, ) -> Tuple: # Generates data used by Brownie for debugging and coverage evaluation if not opcodes_str: return {}, {}, {} source_map = deque(expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) contract_nodes = [contract_node] + contract_node.dependencies source_nodes = {str(i.contract_id): i.parent() for i in contract_nodes} stmt_nodes = {i: stmt_nodes[i].copy() for i in source_nodes} statement_map: Dict = {i: {} for i in source_nodes} # possible branch offsets branch_original = {i: branch_nodes[i].copy() for i in source_nodes} branch_nodes = {i: set(i.offset for i in branch_nodes[i]) for i in source_nodes} # currently active branches, awaiting a jumpi branch_active: Dict = {i: {} for i in source_nodes} # branches that have been set branch_set: Dict = {i: {} for i in source_nodes} count, pc = 0, 0 pc_list: List = [] revert_map: Dict = {} fallback_hexstr: str = "unassigned" active_source_node: Optional[NodeBase] = None active_fn_node: Optional[NodeBase] = None active_fn_name: Optional[str] = None first_source = source_map[0] while source_map: # format of source_map is [start, stop, contract_id, jump code] source = source_map.popleft() pc_list.append({"op": opcodes.popleft(), "pc": pc}) if ( has_fallback is False and fallback_hexstr == "unassigned" and pc_list[-1]["op"] == "REVERT" and [i["op"] for i in pc_list[-4:-1]] == ["JUMPDEST", "PUSH1", "DUP1"] ): # flag the REVERT op at the end of the function selector, # later reverts may jump to it instead of having their own REVERT op fallback_hexstr = f"0x{hex(pc - 4).upper()[2:]}" pc_list[-1]["first_revert"] = True if source[3] != "-": pc_list[-1]["jump"] = source[3] pc += 1 if opcodes[0][:2] == "0x": pc_list[-1]["value"] = opcodes.popleft() pc += int(pc_list[-1]["op"][4:]) # for REVERT opcodes without an source offset, try to infer one if source[2] == -1 or source == first_source: if pc_list[-1]["op"] == "REVERT": _find_revert_offset( pc_list, source_map, active_source_node, active_fn_node, active_fn_name ) if source[2] == -1: continue # set contract path (-1 means none) contract_id = str(source[2]) active_source_node = source_nodes[contract_id] pc_list[-1]["path"] = contract_id # set source offset (-1 means none) if source[0] == -1: continue offset = (source[0], source[0] + source[1]) pc_list[-1]["offset"] = offset # add error messages for INVALID opcodes if pc_list[-1]["op"] == "INVALID": _set_invalid_error_string(active_source_node, pc_list[-1]) # for JUMPI instructions, set active branch markers if branch_active[contract_id] and pc_list[-1]["op"] == "JUMPI": for offset in branch_active[contract_id]: # ( program counter index, JUMPI index) branch_set[contract_id][offset] = ( branch_active[contract_id][offset], len(pc_list) - 1, ) branch_active[contract_id].clear() # if op relates to previously set branch marker, clear it elif offset in branch_nodes[contract_id]: if offset in branch_set[contract_id]: del branch_set[contract_id][offset] branch_active[contract_id][offset] = len(pc_list) - 1 try: # set fn name and statement coverage marker if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = active_fn_name else: active_fn_node, active_fn_name = _get_active_fn(active_source_node, offset) pc_list[-1]["fn"] = active_fn_name stmt_offset = next( i for i in stmt_nodes[contract_id] if sources.is_inside_offset(offset, i) ) stmt_nodes[contract_id].discard(stmt_offset) statement_map[contract_id].setdefault(active_fn_name, {})[count] = stmt_offset pc_list[-1]["statement"] = count count += 1 except (KeyError, IndexError, StopIteration): pass if pc_list[-1].get("value", None) == fallback_hexstr and opcodes[0] in ("JUMP", "JUMPI"): # track all jumps to the initial revert key = (pc_list[-1]["path"], pc_list[-1]["offset"]) revert_map.setdefault(key, []).append(len(pc_list)) while opcodes[0] not in ("INVALID", "STOP") and pc < instruction_count: # necessary because sometimes solidity returns an incomplete source map pc_list.append({"op": opcodes.popleft(), "pc": pc}) pc += 1 if opcodes and opcodes[0][:2] == "0x": pc_list[-1]["value"] = opcodes.popleft() pc += int(pc_list[-1]["op"][4:]) # compare revert and require statements against the map of revert jumps for (contract_id, fn_offset), values in revert_map.items(): fn_node = source_nodes[contract_id].children( depth=2, include_children=False, required_offset=fn_offset, filters={"nodeType": "FunctionDefinition"}, )[0] revert_nodes = fn_node.children( filters=( {"nodeType": "FunctionCall", "expression.name": "revert"}, {"nodeType": "FunctionCall", "expression.name": "require"}, ) ) for node in revert_nodes: offset = node.offset # if the node offset is not in the source map, apply it's offset to the JUMPI op if not next((x for x in pc_list if "offset" in x and x["offset"] == offset), False): pc_list[values[0]].update(offset=offset, jump_revert=True) del values[0] # set branch index markers and build final branch map branch_map: Dict = {i: {} for i in source_nodes} for path, offset, idx in [(k, x, y) for k, v in branch_set.items() for x, y in v.items()]: # for branch to be hit, need an op relating to the source and the next JUMPI # this is because of how the compiler optimizes nested BinaryOperations if "fn" in pc_list[idx[0]]: fn = pc_list[idx[0]]["fn"] pc_list[idx[0]]["branch"] = count pc_list[idx[1]]["branch"] = count node = next(i for i in branch_original[path] if i.offset == offset) branch_map[path].setdefault(fn, {})[count] = offset + (node.jump,) count += 1 pc_map = {i.pop("pc"): i for i in pc_list} return pc_map, statement_map, branch_map
def _generate_coverage_data( source_map_str: str, opcodes_str: str, contract_node: Any, stmt_nodes: Dict, branch_nodes: Dict, has_fallback: bool, ) -> Tuple: # Generates data used by Brownie for debugging and coverage evaluation if not opcodes_str: return {}, {}, {} source_map = deque(expand_source_map(source_map_str)) opcodes = deque(opcodes_str.split(" ")) contract_nodes = [contract_node] + contract_node.dependencies source_nodes = dict((i.contract_id, i.parent()) for i in contract_nodes) paths = set(v.absolutePath for v in source_nodes.values()) stmt_nodes = dict((i, stmt_nodes[i].copy()) for i in paths) statement_map: Dict = dict((i, {}) for i in paths) # possible branch offsets branch_original = dict((i, branch_nodes[i].copy()) for i in paths) branch_nodes = dict( (i, set(i.offset for i in branch_nodes[i])) for i in paths) # currently active branches, awaiting a jumpi branch_active: Dict = dict((i, {}) for i in paths) # branches that have been set branch_set: Dict = dict((i, {}) for i in paths) count, pc = 0, 0 pc_list: List = [] revert_map: Dict = {} fallback_hexstr: str = "unassigned" while source_map: # format of source is [start, stop, contract_id, jump code] source = source_map.popleft() pc_list.append({"op": opcodes.popleft(), "pc": pc}) if (has_fallback is False and fallback_hexstr == "unassigned" and pc_list[-1]["op"] == "REVERT" and [i["op"] for i in pc_list[-4:-1] ] == ["JUMPDEST", "PUSH1", "DUP1"]): # flag the REVERT op at the end of the function selector, # later reverts may jump to it instead of having their own REVERT op fallback_hexstr = "0x" + hex(pc - 4).upper()[2:] pc_list[-1]["first_revert"] = True if source[3] != "-": pc_list[-1]["jump"] = source[3] pc += 1 if opcodes[0][:2] == "0x": pc_list[-1]["value"] = opcodes.popleft() pc += int(pc_list[-1]["op"][4:]) # set contract path (-1 means none) if source[2] == -1: if pc_list[-1]["op"] == "REVERT" and pc_list[-8][ "op"] == "CALLVALUE": pc_list[-1].update({ "dev": "Cannot send ether to nonpayable function", "fn": pc_list[-8].get("fn", "<unknown>"), "offset": pc_list[-8]["offset"], "path": pc_list[-8]["path"], }) continue path = source_nodes[source[2]].absolutePath pc_list[-1]["path"] = path # set source offset (-1 means none) if source[0] == -1: continue offset = (source[0], source[0] + source[1]) pc_list[-1]["offset"] = offset # add error messages for INVALID opcodes if pc_list[-1]["op"] == "INVALID": node = source_nodes[source[2]].children(include_children=False, offset_limits=offset)[0] if node.nodeType == "IndexAccess": pc_list[-1]["dev"] = "Index out of range" elif node.nodeType == "BinaryOperation": if node.operator == "/": pc_list[-1]["dev"] = "Division by zero" elif node.operator == "%": pc_list[-1]["dev"] = "Modulus by zero" # if op is jumpi, set active branch markers if branch_active[path] and pc_list[-1]["op"] == "JUMPI": for offset in branch_active[path]: # ( program counter index, JUMPI index) branch_set[path][offset] = (branch_active[path][offset], len(pc_list) - 1) branch_active[path].clear() # if op relates to previously set branch marker, clear it elif offset in branch_nodes[path]: if offset in branch_set[path]: del branch_set[path][offset] branch_active[path][offset] = len(pc_list) - 1 try: # set fn name and statement coverage marker if "offset" in pc_list[-2] and offset == pc_list[-2]["offset"]: pc_list[-1]["fn"] = pc_list[-2]["fn"] else: pc_list[-1]["fn"] = _get_fn_full_name(source_nodes[source[2]], offset) stmt_offset = next(i for i in stmt_nodes[path] if sources.is_inside_offset(offset, i)) stmt_nodes[path].discard(stmt_offset) statement_map[path].setdefault(pc_list[-1]["fn"], {})[count] = stmt_offset pc_list[-1]["statement"] = count count += 1 except (KeyError, IndexError, StopIteration): pass if "value" not in pc_list[-1]: continue if pc_list[-1]["value"] == fallback_hexstr and opcodes[0] in { "JUMP", "JUMPI" }: # track all jumps to the initial revert revert_map.setdefault((pc_list[-1]["path"], pc_list[-1]["offset"]), []).append(len(pc_list)) # compare revert() statements against the map of revert jumps to find for (path, fn_offset), values in revert_map.items(): fn_node = next(i for i in source_nodes.values() if i.absolutePath == path).children( depth=2, include_children=False, required_offset=fn_offset, filters={"nodeType": "FunctionDefinition"}, )[0] revert_nodes = fn_node.children(filters={ "nodeType": "FunctionCall", "expression.name": "revert" }) # if the node has arguments it will always be included in the source map for node in (i for i in revert_nodes if not i.arguments): offset = node.offset # if the node offset is not in the source map, apply it's offset to the JUMPI op if not next( (x for x in pc_list if "offset" in x and x["offset"] == offset), False): pc_list[values[0]].update({ "offset": offset, "jump_revert": True }) del values[0] # set branch index markers and build final branch map branch_map: Dict = dict((i, {}) for i in paths) for path, offset, idx in [(k, x, y) for k, v in branch_set.items() for x, y in v.items()]: # for branch to be hit, need an op relating to the source and the next JUMPI # this is because of how the compiler optimizes nested BinaryOperations if "fn" not in pc_list[idx[0]]: continue fn = pc_list[idx[0]]["fn"] pc_list[idx[0]]["branch"] = count pc_list[idx[1]]["branch"] = count node = next(i for i in branch_original[path] if i.offset == offset) branch_map[path].setdefault(fn, {})[count] = offset + (node.jump, ) count += 1 pc_map = dict((i.pop("pc"), i) for i in pc_list) return pc_map, statement_map, branch_map