Skip to content

Commit

Permalink
Add error messages, improve error handling
Browse files Browse the repository at this point in the history
Adds tests to fuzz run
  • Loading branch information
joaosantos15 committed Mar 30, 2021
1 parent 2a4d1e7 commit 5e06772
Show file tree
Hide file tree
Showing 12 changed files with 8,092 additions and 41 deletions.
7 changes: 7 additions & 0 deletions .mythx.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
fuzz:
deployed_contract_address: "0x7277646075fa72737e1F6114654C5d9949a67dF2"
number_of_cores: 1
campaign_name_prefix: "brownie_test"
rpc_url: "http://localhost:9898"
faas_url: "http://localhost:9899"
build_directory: /private/var/folders/w9/36q5pfw94rz6bh0062p2j8p40000gn/T/pytest-of-boss/pytest-100/test_rpc_not_running0/build/contracts
12 changes: 12 additions & 0 deletions mythx_cli/fuzz/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,15 @@ class SourceError(BrownieError):
class PayloadError(BrownieError):
"""Exception raised for errors assembling the FaaS payload"""
pass

#
# RPC client
#

class RPCCallError(FaaSError):
"""Exception raised when there is an error calling the RPC endpoint"""
pass

class SeedStateError(FaaSError):
"""Exception raised when there is an error generating the seed state"""
pass
9 changes: 5 additions & 4 deletions mythx_cli/fuzz/faas.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import logging
import random
from mythx_cli.analyze.scribble import ScribbleMixin
Expand Down Expand Up @@ -41,7 +42,7 @@ def start_faas_campaign(self, payload):
raise BadStatusCode(f"Got http status code {response.status_code} for request {req_url}")
return response_data["id"]
except Exception as e:
raise RequestError(f"Error starting FaaS campaign: \n {e}")
raise RequestError(f"Error starting FaaS campaign.")

def create_faas_campaign(self, campaign_data, seed_state):
"""Submit a campaign to the FaaS and start that campaign.
Expand Down Expand Up @@ -84,10 +85,10 @@ def create_faas_campaign(self, campaign_data, seed_state):
if instr_meta is not None:
api_payload["instrumentation_metadata"] = instr_meta
except Exception as e:
raise ScribbleMetaError(f"Error getting Scribble arming metadata:\n {e}")
raise ScribbleMetaError(f"Error getting Scribble arming metadata.") from e

campaign_id = self.start_faas_campaign(api_payload)

return campaign_id
except Exception as e:
raise CreateFaaSCampaignError(f"Error creating the FaaS campaign: \n {e}")
except (PayloadError,ScribbleMetaError ) as e:
raise CreateFaaSCampaignError(f"Error creating the FaaS campaign:")
56 changes: 32 additions & 24 deletions mythx_cli/fuzz/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
import json
import os
import logging

import click
import requests
from requests import RequestException

from .exceptions import RPCCallError
LOGGER = logging.getLogger("mythx-cli")

headers = {
Expand All @@ -13,40 +16,46 @@
time_limit_seconds = 3000
NUM_BLOCKS_UPPER_LIMIT = 9999

# TODO: separate into an RPC class and a Harvey class
class RPCClient():
def __init__(self, rpc_url: str, number_of_cores: int):
self.rpc_url = rpc_url
self.number_of_cores = number_of_cores

"""Makes an rpc call to the RPC endpoint passed to the construcotr
and returns the `result` property of the rpc response.
"""
# TODO: rename to call
def rpc_call(self, method: str, params: str):
# TODO: add try catch, use the base exceptions if available
payload = "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"params\":" + params + ",\"id\":1}"
response = (requests.request("POST", self.rpc_url, headers=headers, data=payload)).json()
return response["result"]

def call(self, method: str, params: str):
"""Make an rpc call to the RPC endpoint
:return: Result property of the RPC response
"""
try:
payload = "{\"jsonrpc\":\"2.0\",\"method\":\"" + method + "\",\"params\":" + params + ",\"id\":1}"
response = (requests.request("POST", self.rpc_url, headers=headers, data=payload)).json()
return response["result"]
except RequestException as e:
raise RPCCallError(f"HTTP error calling RPC method {method} with parameters: {params}"
f"\nAre you sure the RPC is running at {self.rpc_url}?")

def contract_exists(self, contract_address):
return self.call("eth_getCode", "[\"" + contract_address + "\",\"latest\"]")

def get_block(self, latest: bool = False, block_number: int = -1):
block_value = "latest" if latest else str(block_number)
if not latest:
block_value = hex(block_number)

block = self.rpc_call("eth_getBlockByNumber", "[\"" + block_value + "\", true]")
block = self.call("eth_getBlockByNumber", "[\"" + block_value + "\", true]")
if block is None:
return None
else:
return block

"""Returns all the blocks that exist on the target
ethereum node. Raises an exception if the number of blocks
exceeds 10000 as it is likely a user error who passed the wrong
RPC address.
"""

def get_all_blocks(self):
""" Get all blocks from the node running at rpc_url
Raises an exception if the number of blocks
exceeds 10000 as it is likely a user error who passed the wrong
RPC address.
"""
latest_block = self.get_block(latest=True)
if not latest_block:
return []
Expand All @@ -62,10 +71,9 @@ def get_all_blocks(self):
blocks.append(self.get_block(block_number=i))
return blocks

"""Returns a seed state for the target contract in a format that can be used by
the FaaS API and Harvey.
"""

def get_seed_state(self, address: str, other_addresses: [str]):
"""Get a seed state for the target contract to be used by Harvey"""
try:
blocks = self.get_all_blocks()
processed_transactions = []
Expand All @@ -89,12 +97,12 @@ def get_seed_state(self, address: str, other_addresses: [str]):
"num-cores": self.number_of_cores
}
)
except:
LOGGER.warning(f"Could generate seed state for address: {address}")
click.echo(
except Exception as e:
LOGGER.warning(f"Could not generate seed state for address: {address}")
raise click.exceptions.UsageError(
(
"Unable to generate the seed state for address"
+ str(address)
+ "Are you sure you passed the correct contract address?"
)
)
) from e
26 changes: 13 additions & 13 deletions mythx_cli/fuzz/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import requests
import click
from mythx_cli.fuzz.ide.brownie import BrownieJob
from .exceptions import RPCCallError
from .faas import FaasClient
from .rpc import RPCClient

Expand Down Expand Up @@ -64,8 +65,6 @@ def fuzz_run(ctx, address, more_addresses, target):
# print FaaS dashboard url pointing to campaign
analyze_config = ctx.get("fuzz")

contract_address = analyze_config["deployed_contract_address"]

default_config = {
"rpc_url": "http://localhost:7545",
"faas_url": "http://localhost:8080",
Expand All @@ -82,7 +81,7 @@ def fuzz_run(ctx, address, more_addresses, target):
)
if "deployed_contract_address" not in config_options:
raise click.exceptions.UsageError(
"deployed_contract_address not found on .mythx.yaml config file"
"deployed_contract_address not found on .mythx.yaml config file."
"You need to provide the address where your main contract is deployed on the .mythx.yaml"
)
if not target and "target" not in config_options:
Expand All @@ -96,21 +95,24 @@ def fuzz_run(ctx, address, more_addresses, target):
target = [analyze_config["target"]]
# Optional config parameters
# Here we parse the config parameters from the config file and use defaults for non available values
# TODO: make into ifs or use dict.update dict.get('key', default), safe way to check it exists
contract_address = analyze_config["deployed_contract_address"]
rpc_url = analyze_config["rpc_url"] if 'rpc_url' in config_options else default_config["rpc_url"]
faas_url = analyze_config["faas_url"] if 'faas_url' in config_options else default_config["faas_url"]
number_of_cores = analyze_config["number_of_cores"] if 'number_of_cores' in config_options else default_config[
"harvey_num_cores"]
campaign_name_prefix = analyze_config["campaign_name_prefix"] if "campaign_name_prefix" in config_options else \
default_config["campaign_name_prefix"]

rpc_client = RPCClient(rpc_url, number_of_cores)

contract_code_response = rpc_client.rpc_call("eth_getCode", "[\"" + contract_address + "\",\"latest\"]")
try:
rpc_client = RPCClient(rpc_url, number_of_cores)
contract_code_response = rpc_client.contract_exists( contract_address)
except RPCCallError as e:
raise click.exceptions.UsageError(f"RPC endpoint."
f"\n{e}")

# TODO: raise exception with wrong contract address
if contract_code_response is None:
print("Invalid address")
if not contract_code_response:
LOGGER.warning(f"Contract code not found")
raise click.exceptions.ClickException(f"Unable to find a contract deployed at {contract_address}.")

if more_addresses is None:
other_addresses = []
Expand All @@ -128,7 +130,5 @@ def fuzz_run(ctx, address, more_addresses, target):
click.echo("You can view campaign here: " + faas_url + "/campaigns/" + str(campaign_id))
except Exception as e:
LOGGER.warning(f"Could not submit campaign to the FaaS")
click.echo(f"Unable to submit the campaign to the faas. Are you sure the service is running on {faas_url} ?"
f"Error: {e}")
raise
raise click.exceptions.UsageError(f"Unable to submit the campaign to the faas. Are you sure the service is running on {faas_url} ?")
pass
6 changes: 6 additions & 0 deletions tests/.mythx.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
fuzz:
build_directory: build/contracts
number_of_cores: 1
campaign_name_prefix: "brownie_test"
rpc_url: "http://localhost:7545"
faas_url: "http://localhost:8080"
14 changes: 14 additions & 0 deletions tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ def get_test_case(path: str, obj=None, raw=False):

AST = get_test_case("testdata/test-ast.json")

@contextmanager
def mock_faas_context():
# with patch("mythx_cli.fuzz.rpc.RPCClient.get_all_blocks", new=) as get_all_blocks_patch, patch(
# "mythx_cli.fuzz.rpc.RPCClient.contract_exists"
# ) as contract_exists_patch:
# print("-->patching blocks")
# get_all_blocks_patch.return_value = get_test_case("testdata/ganache-all-blocks.json")
# contract_exists_patch.return_value = True
with patch("mythx_cli.fuzz.rpc.RPCClient") as RPCClient_mock:
instance = RPCClient_mock.return_value
instance.get_all_blocks.return_value = get_test_case("testdata/ganache-all-blocks.json")
instance.contract_exists.return_value = True
yield


@contextmanager
def mock_context(
Expand Down
Loading

0 comments on commit 5e06772

Please sign in to comment.