Exemplo n.º 1
0
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}
Exemplo n.º 2
0
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
Exemplo n.º 3
0
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