Bug: JSONDecodeError crash when running `hybrid-echidna`
Describe the bug
It runs echidna for about 2 minutes and then crashes.
To Reproduce Sadly I cannot share my environment. I could narrowing things down with some guidance.
Additional context:
- Manjaro Linux, Python 3.10.8
- Echidna 2.0.4
- Slither 0.9.1
❯ hybrid-echidna . --contract E2E --config echidna-config.yaml
Traceback (most recent call last):
File "/home/rappie/.local/bin/hybrid-echidna", line 8, in <module>
sys.exit(main())
File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 554, in main
func(sys.argv[1:])
File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 344, in run_hybrid_echidna_with_display
raise exc
File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 321, in run_hybrid_echidna_with_display
run_hybrid_echidna(args)
File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/__main__.py", line 235, in run_hybrid_echidna
nb_cov_insts = count_unique_pc(p.stdout)
File "/home/rappie/.local/lib/python3.10/site-packages/optik/echidna/interface.py", line 523, in count_unique_pc
data = json.loads(output)
File "/usr/lib/python3.10/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File "/usr/lib/python3.10/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python3.10/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
I had the same issue. I managed to fix it by completely uninstalling nix (https://nix-tutorial.gitlabpages.inria.fr/nix-tutorial/installation.html#uninstalling-nix) and downloading the echidna-test v2.0.4 binary from https://github.com/crytic/echidna/releases
I see what's going on. In https://github.com/crytic/optik/blob/master/optik/echidna/interface.py#L522 optik assumes that echidna-test --format json will have a json in the second line of the output. But starting from commit 1f59ae189258ab52342331a92208f5b6b8930ea7 the json is in the third line of echidna's output.
The following fix works:
--- a/optik/echidna/interface.py
+++ b/optik/echidna/interface.py
@@ -469,7 +469,7 @@ def extract_cases_from_json_output(output: str) -> List[List[str]]:
"""
# Sometimes the JSON output starts with a line such as
# "Loaded total of 500 transactions from /tmp/c4/coverage"
- if output.startswith("Loaded total of"):
+ while output.startswith("Loaded total of"):
output = output.split("\n", 1)[1]
data = json.loads(output)
if "tests" not in data:
@@ -518,7 +518,7 @@ def count_unique_pc(output: str) -> int:
:param output: echidna's JSON output as a simple string
"""
- if output.startswith("Loaded total of"):
+ while output.startswith("Loaded total of"):
output = output.split("\n", 1)[1]
data = json.loads(output)
res = 0
But that's not sufficient to make echidna v2.0.5 work. The json format has also changed so the following dirty fix is needed:
--- a/optik/corpus/generator.py
+++ b/optik/corpus/generator.py
@@ -108,10 +108,10 @@ class EchidnaCorpusGenerator(CorpusGenerator):
with open(os.path.join(corpus_dir, filename), "rb") as f:
data = json.loads(f.read())
for tx in data:
- if tx["_call"]["tag"] == "NoCall":
+ if tx["call"]["tag"] == "NoCall":
continue
func_name, args_spec, _ = extract_func_from_call(
- tx["_call"]
+ tx["call"]
)
func_prototype = func_signature(func_name, args_spec)
# Only store one tx for each function
diff --git a/optik/echidna/interface.py b/optik/echidna/interface.py
index ca23622..3f580b8 100644
--- a/optik/echidna/interface.py
+++ b/optik/echidna/interface.py
@@ -168,13 +168,13 @@ def load_tx(tx: Dict, tx_name: str = "") -> AbstractTx:
# Translate block number/timestamp increments
block_num_inc = Var(256, f"{tx_name}_block_num_inc")
block_timestamp_inc = Var(256, f"{tx_name}_block_timestamp_inc")
- ctx.set(block_num_inc.name, int(tx["_delay"][1], 16), block_num_inc.size)
+ ctx.set(block_num_inc.name, int(tx["delay"][1], 16), block_num_inc.size)
ctx.set(
- block_timestamp_inc.name, int(tx["_delay"][0], 16), block_num_inc.size
+ block_timestamp_inc.name, int(tx["delay"][0], 16), block_num_inc.size
)
# Check if it's a "NoCall" echidna transaction
- if tx["_call"]["tag"] == "NoCall":
+ if tx["call"]["tag"] == "NoCall":
return AbstractTx(
None,
block_num_inc,
@@ -183,27 +183,27 @@ def load_tx(tx: Dict, tx_name: str = "") -> AbstractTx:
)
# Translate function call
- func_name, args_spec, arg_values = extract_func_from_call(tx["_call"])
+ func_name, args_spec, arg_values = extract_func_from_call(tx["call"])
call_data = function_call(func_name, args_spec, ctx, tx_name, *arg_values)
# Translate message sender
sender = Var(160, f"{tx_name}_sender")
- ctx.set(sender.name, int(tx["_src"], 16), sender.size)
+ ctx.set(sender.name, int(tx["src"], 16), sender.size)
# Translate message value
# Echidna will only send non-zero msg.value to payable funcs
# so we only make an abstract value in that case
- if int(tx["_value"], 16) != 0:
+ if int(tx["value"], 16) != 0:
value = Var(256, f"{tx_name}_value")
- ctx.set(value.name, int(tx["_value"], 16), value.size)
+ ctx.set(value.name, int(tx["value"], 16), value.size)
else:
value = Cst(256, 0)
# Build transaction
# TODO: make EVMTransaction accept integers as arguments
- gas_limit = Cst(256, int(tx["_gas'"], 16))
- gas_price = Cst(256, int(tx["_gasprice'"], 16))
- recipient = int(tx["_dst"], 16)
+ gas_limit = Cst(256, tx["gas"], 16)
+ gas_price = Cst(256, int(tx["gasprice"], 16))
+ recipient = int(tx["dst"], 16)
return AbstractTx(
EVMTransaction(
sender, # origin
@@ -335,7 +335,7 @@ def update_tx(tx: Dict, new_model: VarContext, tx_name: str = "") -> Dict:
tx = tx.copy() # Copy transaction to avoid in-place modifications
# Update call arguments
- call = tx["_call"]
+ call = tx["call"]
args = call["contents"][1]
for i, arg in enumerate(args):
update_argument(arg, f"{tx_name}_arg{i}", new_model)
@@ -344,20 +344,20 @@ def update_tx(tx: Dict, new_model: VarContext, tx_name: str = "") -> Dict:
block_num_inc = f"{tx_name}_block_num_inc"
block_timestamp_inc = f"{tx_name}_block_timestamp_inc"
if new_model.contains(block_num_inc):
- tx["_delay"][1] = hex(new_model.get(block_num_inc))
+ tx["delay"][1] = hex(new_model.get(block_num_inc))
if new_model.contains(block_timestamp_inc):
- tx["_delay"][0] = hex(new_model.get(block_timestamp_inc))
+ tx["delay"][0] = hex(new_model.get(block_timestamp_inc))
# Update sender
sender = f"{tx_name}_sender"
if new_model.contains(sender):
# Address so we need to pad it to 40 chars (20bytes)
- tx["_src"] = f"0x{new_model.get(sender):0{40}x}"
+ tx["src"] = f"0x{new_model.get(sender):0{40}x}"
# Update transaction value
value = f"{tx_name}_value"
if new_model.contains(value):
- tx["_value"] = hex(new_model.get(value))
+ tx["value"] = hex(new_model.get(value))
return tx
I will let someone more knowledgeable identify and implement a proper solution.