Skip to content
This repository was archived by the owner on Feb 14, 2025. It is now read-only.

Commit 0055c8f

Browse files
authored
Merge pull request #51 from trailofbits/48-optional-manticore
Makes Manticore an optional dependency, and various logging fixes
2 parents 446918d + 40ed206 commit 0055c8f

File tree

10 files changed

+300
-149
lines changed

10 files changed

+300
-149
lines changed

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,15 @@
22

33
The format is based on [Keep a Changelog](http://keepachangelog.com/).
44

5-
## [Unreleased](https://github.com/trailofbits/manticore/compare/0.2.2...HEAD)
5+
## [Unreleased](https://github.com/trailofbits/etheno/compare/v0.2.0...HEAD)
6+
7+
### 0.2.1 — 2019-02-07
8+
9+
Bugfix release.
10+
11+
- Manticore is now an optional requirement
12+
- Improvements and bugfixes to the logger integration with Manticore
13+
- Added a workaround to the examples for a bug in Truffle
614

715
## 0.2.0 — 2018-11-02
816

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ COPY etheno/*.py /home/etheno/etheno/etheno/
5353
RUN mkdir -p /home/etheno/examples
5454
COPY examples /home/etheno/examples/
5555

56-
RUN cd etheno && pip3 install --user .
56+
RUN cd etheno && pip3 install --user '.[manticore]'
5757

5858
USER root
5959

etheno/__main__.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,24 @@
88
from .client import RpcProxyClient
99
from .differentials import DifferentialTester
1010
from .echidna import echidna_exists, EchidnaPlugin, install_echidna
11-
from .etheno import app, EthenoView, GETH_DEFAULT_RPC_PORT, ManticoreClient, ETHENO, VERSION_NAME
11+
from .etheno import app, EthenoView, GETH_DEFAULT_RPC_PORT, ETHENO, VERSION_NAME
1212
from .genesis import Account, make_accounts, make_genesis
1313
from .synchronization import AddressSynchronizingClient, RawTransactionClient
1414
from .utils import clear_directory, decode_value, find_open_port, format_hex_address, ynprompt
1515
from . import Etheno
1616
from . import ganache
1717
from . import geth
1818
from . import logger
19-
from . import manticoreutils
2019
from . import parity
2120
from . import truffle
2221

22+
try:
23+
from .manticoreclient import ManticoreClient
24+
from . import manticoreutils
25+
MANTICORE_INSTALLED = True
26+
except ModuleNotFoundError:
27+
MANTICORE_INSTALLED = False
28+
2329
def main(argv = None):
2430
parser = argparse.ArgumentParser(description='An Ethereum JSON RPC multiplexer and Manticore wrapper')
2531
parser.add_argument('--debug', action='store_true', default=False, help='Enable debugging from within the web server')
@@ -238,12 +244,19 @@ def main(argv = None):
238244

239245
manticore_client = None
240246
if args.manticore:
247+
if not MANTICORE_INSTALLED:
248+
ETHENO.logger.error('Manticore is not installed! Running Etheno with Manticore requires Manticore version 0.2.2 or newer. Reinstall Etheno with Manticore support by running `pip3 install --user \'etheno[manticore]\'`, or install Manticore separately with `pip3 install --user \'manticore\'`')
249+
sys.exit(1)
250+
new_enough = manticoreutils.manticore_is_new_enough()
251+
if new_enough is None:
252+
ETHENO.logger.warning(f"Unknown Manticore version {manticoreutils.manticore_version()}; it may not be new enough to have Etheno support!")
253+
elif not new_enough:
254+
ETHENO.logger.error(f"The version of Manticore installed is {manticoreutils.manticore_version()}, but the minimum required version with Etheno support is 0.2.2. We will try to proceed, but things might not work correctly! Please upgrade Manticore.")
241255
manticore_client = ManticoreClient()
242256
ETHENO.add_client(manticore_client)
243257
if args.manticore_max_depth is not None:
244258
manticore_client.manticore.register_detector(manticoreutils.StopAtDepth(args.manticore_max_depth))
245259
manticore_client.manticore.verbosity(getattr(logger, args.log_level))
246-
manticore_client.reassign_manticore_loggers()
247260

248261
if args.truffle:
249262
truffle_controller = truffle.Truffle(parent_logger=ETHENO.logger)

etheno/etheno.py

Lines changed: 7 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,14 @@
44
VERSION_ID=67
55

66
import logging
7-
import sha3
87
from threading import Thread
9-
import time
108

119
from flask import Flask, g, jsonify, request, abort
1210
from flask.views import MethodView
1311

14-
from manticore.ethereum import ManticoreEVM
15-
import manticore
16-
1712
from . import logger
1813
from . import threadwrapper
19-
from .client import EthenoClient, JSONRPCError, RpcProxyClient, SelfPostingClient, DATA, QUANTITY, transaction_receipt_succeeded, jsonrpc
14+
from .client import JSONRPCError, SelfPostingClient
2015
from .utils import format_hex_address
2116

2217
app = Flask(__name__)
@@ -30,17 +25,6 @@ def to_account_address(raw_address):
3025
addr = "%x" % raw_address
3126
return "0x%s%s" % ('0'*(40 - len(addr)), addr)
3227

33-
def encode_hex(data):
34-
if data is None:
35-
return None
36-
elif isinstance(data, int) or isinstance(data, long):
37-
encoded = hex(data)
38-
if encoded[-1] == 'L':
39-
encoded = encoded[:-1]
40-
return encoded
41-
else:
42-
return "0x%s" % data.encode('hex')
43-
4428
_CONTROLLER = threadwrapper.MainThreadController()
4529

4630
@app.route('/shutdown')
@@ -53,124 +37,6 @@ def _etheno_shutdown():
5337
shutdown()
5438
return ''
5539

56-
class ManticoreClient(EthenoClient):
57-
def __init__(self, manticore=None):
58-
self._assigned_manticore = manticore
59-
self._manticore = None
60-
self.contracts = []
61-
self.short_name = 'Manticore'
62-
self._accounts_to_create = []
63-
64-
@property
65-
def manticore(self):
66-
if self._manticore is None:
67-
if self._assigned_manticore is None:
68-
# we do lazy evaluation of ManticoreClient.manticore so self.log_directory will be assigned already
69-
if self.log_directory is None:
70-
workspace = None
71-
else:
72-
workspace = self.log_directory
73-
self._assigned_manticore = ManticoreEVM(workspace_url=workspace)
74-
self._manticore = threadwrapper.MainThreadWrapper(self._assigned_manticore, _CONTROLLER)
75-
self._finalize_manticore()
76-
return self._manticore
77-
78-
def _finalize_manticore(self):
79-
if not self._manticore:
80-
return
81-
for balance, address in self._accounts_to_create:
82-
self._manticore.create_account(balance=balance, address=address)
83-
self._accounts_to_create = []
84-
self.logger.cleanup_empty = True
85-
86-
def create_account(self, balance, address):
87-
self._accounts_to_create.append((balance, address))
88-
self._finalize_manticore()
89-
90-
def reassign_manticore_loggers(self):
91-
# Manticore uses a global to track its loggers:
92-
for name in manticore.utils.log.all_loggers:
93-
manticore_logger = logging.getLogger(name)
94-
for handler in list(manticore_logger.handlers):
95-
manticore_logger.removeHandler(handler)
96-
logger.EthenoLogger(name, parent=self.logger, cleanup_empty=True)
97-
98-
@jsonrpc(from_addr = QUANTITY, to = QUANTITY, gas = QUANTITY, gasPrice = QUANTITY, value = QUANTITY, data = DATA, nonce = QUANTITY, RETURN = DATA)
99-
def eth_sendTransaction(self, from_addr, to = None, gas = 90000, gasPrice = None, value = 0, data = None, nonce = None, rpc_client_result = None):
100-
if to is None or to == 0:
101-
# we are creating a new contract
102-
if rpc_client_result is not None:
103-
tx_hash = rpc_client_result['result']
104-
while True:
105-
receipt = self.etheno.master_client.post({
106-
'id' : "%s_receipt" % rpc_client_result['id'],
107-
'method' : 'eth_getTransactionReceipt',
108-
'params' : [tx_hash]
109-
})
110-
if 'result' in receipt and receipt['result']:
111-
address = int(receipt['result']['contractAddress'], 16)
112-
break
113-
# The transaction is still pending
114-
time.sleep(1.0)
115-
else:
116-
address = None
117-
contract_address = self.manticore.create_contract(owner = from_addr, balance = value, init=data)
118-
self.contracts.append(contract_address)
119-
self.logger.info(f"Manticore contract created: {encode_hex(contract_address.address)}")
120-
#self.logger.info("Block number: %s" % self.manticore.world.block_number())
121-
else:
122-
self.manticore.transaction(address = to, data = data, caller=from_addr, value = value)
123-
# Just mimic the result from the master client
124-
# We need to return something valid to appease the differential tester
125-
return rpc_client_result
126-
127-
@jsonrpc(TX_HASH = QUANTITY)
128-
def eth_getTransactionReceipt(self, tx_hash, rpc_client_result = None):
129-
# Mimic the result from the master client
130-
# to appease the differential tester
131-
return rpc_client_result
132-
133-
def multi_tx_analysis(self, contract_address = None, tx_limit=None, tx_use_coverage=True, args=None):
134-
if contract_address is None:
135-
for contract_address in self.contracts:
136-
self.multi_tx_analysis(contract_address = contract_address, tx_limit = tx_limit, tx_use_coverage = tx_use_coverage, args = args)
137-
return
138-
139-
tx_account = self.etheno.accounts
140-
141-
prev_coverage = 0
142-
current_coverage = 0
143-
tx_no = 0
144-
while (current_coverage < 100 or not tx_use_coverage) and not self.manticore.is_shutdown():
145-
try:
146-
self.logger.info("Starting symbolic transaction: %d" % tx_no)
147-
148-
# run_symbolic_tx
149-
symbolic_data = self.manticore.make_symbolic_buffer(320)
150-
symbolic_value = self.manticore.make_symbolic_value()
151-
self.manticore.transaction(caller=tx_account[min(tx_no, len(tx_account) - 1)],
152-
address=contract_address,
153-
data=symbolic_data,
154-
value=symbolic_value)
155-
self.logger.info("%d alive states, %d terminated states" % (self.manticore.count_running_states(), self.manticore.count_terminated_states()))
156-
except NoAliveStates:
157-
break
158-
159-
# Check if the maximun number of tx was reached
160-
if tx_limit is not None and tx_no + 1 >= tx_limit:
161-
break
162-
163-
# Check if coverage has improved or not
164-
if tx_use_coverage:
165-
prev_coverage = current_coverage
166-
current_coverage = self.manticore.global_coverage(contract_address)
167-
found_new_coverage = prev_coverage < current_coverage
168-
169-
if not found_new_coverage:
170-
break
171-
172-
tx_no += 1
173-
17440
class EthenoPlugin(object):
17541
_etheno = None
17642
logger = None
@@ -299,6 +165,8 @@ def estimate_gas(self, transaction):
299165
return None
300166

301167
def post(self, data):
168+
self.logger.debug(f"Handling JSON RPC request {data}")
169+
302170
for plugin in self.plugins:
303171
plugin.before_post(data)
304172

@@ -332,6 +200,7 @@ def post(self, data):
332200
ret = e
333201

334202
self.rpc_client_result = ret
203+
self.logger.debug(f"Result from the master client ({self.master_client}): {ret}")
335204

336205
results = []
337206

@@ -357,6 +226,7 @@ def post(self, data):
357226
except JSONRPCError as e:
358227
self.logger.error(e)
359228
results.append(e)
229+
self.logger.debug(f"Result from client {client}: {results[-1]}")
360230

361231
if ret is None:
362232
return None
@@ -487,6 +357,8 @@ def post(self):
487357

488358
ret = ETHENO.post(data)
489359

360+
ETHENO.logger.debug(f"Returning {ret}")
361+
490362
if ret is None:
491363
return None
492364

etheno/ganache.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ def __init__(self, args=None, port=8546):
1313
super().__init__("http://127.0.0.1:%d/" % port)
1414
self.port = port
1515
if args is None:
16-
self.args = []
17-
else:
18-
self.args = ['/usr/bin/env', 'ganache-cli', '-d', '-p', str(port)] + args
16+
args = []
17+
self.args = ['/usr/bin/env', 'ganache-cli', '-d', '-p', str(port)] + args
1918
self.ganache = None
2019
self._client = None
2120
def start(self):

etheno/logger.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,21 +83,47 @@ def format(self, *args, **kwargs):
8383
else:
8484
return self._parent_formatter.format(*args, **kwargs)
8585

86+
ETHENO_LOGGERS = {}
87+
88+
_LOGGING_GETLOGGER = logging.getLogger
89+
def getLogger(name):
90+
if name in ETHENO_LOGGERS:
91+
# TODO: Only enable this if Etheno was run as a standalone application
92+
ret = ETHENO_LOGGERS[name]
93+
else:
94+
ret = _LOGGING_GETLOGGER(name)
95+
# ####BEGIN####
96+
# Horrible hack to workaround Manticore's global logging system.
97+
# This can be removed after https://github.com/trailofbits/manticore/issues/1369
98+
# is resolved.
99+
if name.startswith('manticore'):
100+
ret.propagate = False
101+
# ####END####
102+
return ret
103+
logging.getLogger = getLogger
104+
86105
class EthenoLogger(object):
87106
DEFAULT_FORMAT='$RESET$LEVELCOLOR$BOLD%(levelname)-8s $BLUE[$RESET$WHITE%(asctime)14s$BLUE$BOLD]$NAME$RESET %(message)s'
88107

89-
def __init__(self, name, log_level=None, parent=None, cleanup_empty=False):
108+
def __init__(self, name, log_level=None, parent=None, cleanup_empty=False, displayname=None):
109+
if name in ETHENO_LOGGERS:
110+
raise Exception(f'An EthenoLogger instance for name {name} already exists: {ETHENO_LOGGERS[name]}')
111+
ETHENO_LOGGERS[name] = self
90112
self._directory = None
91113
self.parent = parent
92114
self.cleanup_empty = cleanup_empty
93115
self.children = []
94116
self._descendant_handlers = []
117+
if displayname is None:
118+
self.displayname = name
119+
else:
120+
self.displayname = displayname
95121
if log_level is None:
96122
if parent is None:
97123
raise ValueError('A logger must be provided a parent if `log_level` is None')
98124
log_level = parent.log_level
99125
self._log_level = log_level
100-
self._logger = logging.getLogger(name)
126+
self._logger = _LOGGING_GETLOGGER(name)
101127
self._handlers = [logging.StreamHandler()]
102128
if log_level is not None:
103129
self.log_level = log_level
@@ -136,7 +162,7 @@ def directory(self):
136162
return self._directory
137163

138164
def _add_child(self, child):
139-
if child in self.children:
165+
if child in self.children or any(c for c in self.children if c.name == child.name):
140166
raise ValueError("Cannot double-add child logger %s to logger %s" % (child.name, self.name))
141167
self.children.append(child)
142168
if self.directory is not None:
@@ -155,7 +181,7 @@ def _name_format(self):
155181
ret = self.parent._name_format()
156182
else:
157183
ret = ''
158-
return ret + "[$RESET$WHITE%s$BLUE$BOLD]" % self._logger.name
184+
return ret + "[$RESET$WHITE%s$BLUE$BOLD]" % self.displayname
159185

160186
def addHandler(self, handler, include_descendants=True, set_log_level=True):
161187
if set_log_level:
@@ -249,6 +275,10 @@ def log_level(self, level):
249275
def __getattr__(self, name):
250276
return getattr(self._logger, name)
251277

278+
def __repr__(self):
279+
return f'{type(self).__name__}(name={self.name!r}, log_level={self.log_level!r}, parent={self.parent!r}, cleanup_empty={self.cleanup_empty!r}, displayname={self.displayname!r})'
280+
281+
252282
class StreamLogger(threading.Thread):
253283
def __init__(self, logger, *streams, newline_char=b'\n'):
254284
super().__init__(daemon=True)

0 commit comments

Comments
 (0)