Skip to content

Commit

Permalink
Merge pull request #60 from crytic/59-event-summary
Browse files Browse the repository at this point in the history
Export an event summary
  • Loading branch information
ESultanik authored Mar 27, 2019
2 parents 214591f + 98e0ba8 commit 2c5642d
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 19 deletions.
6 changes: 5 additions & 1 deletion etheno/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from .echidna import echidna_exists, EchidnaPlugin, install_echidna
from .etheno import app, EthenoView, GETH_DEFAULT_RPC_PORT, ETHENO, VERSION_NAME
from .genesis import Account, make_accounts, make_genesis
from .jsonrpc import JSONRPCExportPlugin
from .jsonrpc import EventSummaryExportPlugin, JSONRPCExportPlugin
from .synchronization import AddressSynchronizingClient, RawTransactionClient
from .utils import clear_directory, decode_value, find_open_port, format_hex_address, ynprompt
from . import Etheno
Expand Down Expand Up @@ -62,6 +62,7 @@ def main(argv = None):
parser.add_argument('--log-file', type=str, default=None, help='Path to save all log output to a single file')
parser.add_argument('--log-dir', type=str, default=None, help='Path to a directory in which to save all log output, divided by logging source')
parser.add_argument('-d', '--dump-jsonrpc', type=str, default=None, help='Path to a JSON file in which to dump all raw JSON RPC calls; if `--log-dir` is provided, the raw JSON RPC calls will additionally be dumped to `rpc.json` in the log directory.')
parser.add_argument('-x', '--export-summary', type=str, default=None, help='Path to a JSON file in which to export an event summary')
parser.add_argument('-v', '--version', action='store_true', default=False, help='Print version information and exit')
parser.add_argument('client', type=str, nargs='*', help='JSON RPC client URLs to multiplex; if no client is specified for --master, the first client in this list will default to the master (format="http://foo.com:8545/")')
parser.add_argument('-s', '--master', type=str, default=None, help='A JSON RPC client to use as the master (format="http://foo.com:8545/")')
Expand Down Expand Up @@ -111,6 +112,9 @@ def main(argv = None):
if args.dump_jsonrpc is not None:
ETHENO.add_plugin(JSONRPCExportPlugin(args.dump_jsonrpc))

if args.export_summary is not None:
ETHENO.add_plugin(EventSummaryExportPlugin(args.export_summary))

# First, see if we need to install Echidna:
if args.echidna:
if not echidna_exists():
Expand Down
136 changes: 118 additions & 18 deletions etheno/jsonrpc.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,132 @@
import json
from typing import TextIO, Union
from typing import Dict, TextIO, Union

from .etheno import EthenoPlugin
from .utils import format_hex_address

class JSONRPCExportPlugin(EthenoPlugin):
class JSONExporter:
def __init__(self, out_stream: Union[str, TextIO]):
self._was_path = isinstance(out_stream, str)
if self._was_path:
self._output = open(out_stream, 'w', encoding='utf8')
self.output = open(out_stream, 'w', encoding='utf8')
else:
self._output = out_stream
self._output.write('[')
self.output = out_stream
self.output.write('[')
self._count = 0
self._finalized = False

def finalize(self):
if self._finalized:
return
if self._count:
self.output.write('\n')
self.output.write(']')
self.output.flush()
if self._was_path:
self.output.close()
self._finalized = True

def before_post(self, post_data):
def write_entry(self, entry):
if self._finalized:
return
if self._count > 0:
self._output.write(',')
self.output.write(',')
self._count += 1
self._output.write('\n')
json.dump(post_data, self._output)
self._output.flush()
self.output.write('\n')
json.dump(entry, self.output)
self.output.flush()


class JSONRPCExportPlugin(EthenoPlugin):
def __init__(self, out_stream: Union[str, TextIO]):
self._exporter = JSONExporter(out_stream)

def after_post(self, post_data, client_results):
self._exporter.write_entry([post_data, client_results])

def finalize(self):
if self._count:
self._output.write('\n')
self._output.write(']')
self._output.flush()
if self._was_path:
self._output.close()
if hasattr(self._output, 'name'):
self.logger.info(f'Raw JSON RPC messages dumped to {self._output.name}')
self._exporter.finalize()
if hasattr(self._exporter.output, 'name'):
self.logger.info(f'Raw JSON RPC messages dumped to {self._exporter.output.name}')


class EventSummaryPlugin(EthenoPlugin):
def __init__(self):
self._transactions: Dict[int, Dict[str, object]] = {} # Maps transaction hashes to their eth_sendTransaction arguments

def handle_contract_created(self, creator_address: str, contract_address: str, gas_used: str, gas_price: str, data: str, value: str):
self.logger.info(f'Contract created at {contract_address} with {(len(data)-2)//2} bytes of data by account {creator_address} for {gas_used} gas with a gas price of {gas_price}')

def handle_function_call(self, from_address: str, to_address: str, gas_used: str, gas_price: str, data: str, value: str):
self.logger.info(f'Function call with {value} wei from {from_address} to {to_address} with {(len(data)-2)//2} bytes of data for {gas_used} gas with a gas price of {gas_price}')

def after_post(self, post_data, result):
if len(result):
result = result[0]
if 'method' not in post_data:
return
elif post_data['method'] == 'eth_sendTransaction' and 'result' in result:
try:
transaction_hash = int(result['result'], 16)
except ValueError:
return
self._transactions[transaction_hash] = post_data
elif post_data['method'] == 'eth_getTransactionReceipt':
transaction_hash = int(post_data['params'][0], 16)
if transaction_hash not in self._transactions:
self.logger.error(f'Received transaction receipt {result} for unknown transaction hash {post_data["params"][0]}')
return
original_transaction = self._transactions[transaction_hash]['params'][0]
if 'value' not in original_transaction or original_transaction['value'] is None:
value = '0x0'
else:
value = original_transaction['value']
if 'to' not in result['result'] or result['result']['to'] is None:
# this transaction is creating a contract:
contract_address = result['result']['contractAddress']
self.handle_contract_created(original_transaction['from'], contract_address, result['result']['gasUsed'], original_transaction['gasPrice'], original_transaction['data'], value)
else:
self.handle_function_call(original_transaction['from'], original_transaction['to'], result['result']['gasUsed'], original_transaction['gasPrice'], original_transaction['data'], value)


class EventSummaryExportPlugin(EventSummaryPlugin):
def __init__(self, out_stream: Union[str, TextIO]):
super().__init__()
self._exporter = JSONExporter(out_stream)

def run(self):
for address in self.etheno.accounts:
self._exporter.write_entry({
'event' : 'AccountCreated',
'address' : format_hex_address(address)
})
super().run()

def handle_contract_created(self, creator_address: str, contract_address: str, gas_used: str, gas_price: str, data: str, value: str):
self._exporter.write_entry({
'event' : 'ContractCreated',
'from' : creator_address,
'contract_address' : contract_address,
'gas_used' : gas_used,
'gas_price' : gas_price,
'data' : data,
'value' : value
})
super().handle_contract_created(creator_address, contract_address, gas_used, gas_price, data, value)

def handle_function_call(self, from_address: str, to_address: str, gas_used: str, gas_price: str, data: str, value: str):
self._exporter.write_entry({
'event' : 'FunctionCall',
'from' : from_address,
'to' : to_address,
'gas_used' : gas_used,
'gas_price' : gas_price,
'data' : data,
'value' : value
})
super().handle_function_call(from_address, to_address, gas_used, gas_price, data, value)

def finalize(self):
self._exporter.finalize()
if hasattr(self._exporter.output, 'name'):
self.logger.info(f'Event summary JSON saved to {self._exporter.output.name}')

0 comments on commit 2c5642d

Please sign in to comment.