From 096883d236ef3e1b166e3ae75c2578bd27e7baa0 Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 27 Mar 2019 08:56:15 -0400 Subject: [PATCH 1/4] Also include the RPC result in the JSON dump (#57) --- etheno/jsonrpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/etheno/jsonrpc.py b/etheno/jsonrpc.py index 56b6605..cf8923e 100644 --- a/etheno/jsonrpc.py +++ b/etheno/jsonrpc.py @@ -13,12 +13,12 @@ def __init__(self, out_stream: Union[str, TextIO]): self._output.write('[') self._count = 0 - def before_post(self, post_data): + def after_post(self, post_data, client_results): if self._count > 0: self._output.write(',') self._count += 1 self._output.write('\n') - json.dump(post_data, self._output) + json.dump([post_data, client_results], self._output) self._output.flush() def finalize(self): From 8fdec551ca9d51c77faf559a0bb798cc7c62e96b Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 27 Mar 2019 10:18:33 -0400 Subject: [PATCH 2/4] Started implementing a new event summary plugin (#59) --- etheno/__main__.py | 6 ++- etheno/jsonrpc.py | 105 +++++++++++++++++++++++++++++++++++++-------- 2 files changed, 92 insertions(+), 19 deletions(-) diff --git a/etheno/__main__.py b/etheno/__main__.py index d2cf99b..98d3f7d 100644 --- a/etheno/__main__.py +++ b/etheno/__main__.py @@ -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 @@ -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/")') @@ -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(): diff --git a/etheno/jsonrpc.py b/etheno/jsonrpc.py index 56b6605..1e74ad3 100644 --- a/etheno/jsonrpc.py +++ b/etheno/jsonrpc.py @@ -1,32 +1,101 @@ import json -from typing import TextIO, Union +from typing import Dict, TextIO, Union from .etheno import EthenoPlugin -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 before_post(self, post_data): + 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 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 before_post(self, post_data): + self._exporter.write_entry(post_data) 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): + 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 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 + if 'to' not in result['result'] or result['result']['to'] is None: + # this transaction is creating a contract: + contract_address = result['result']['contractAddress'] + original_transaction = self._transactions[transaction_hash]['params'][0] + self.handle_contract_created(original_transaction['from'], contract_address, result['result']['gasUsed'], original_transaction['gasPrice'], original_transaction['data']) + + +class EventSummaryExportPlugin(EventSummaryPlugin): + def __init__(self, out_stream: Union[str, TextIO]): + super().__init__() + self._exporter = JSONExporter(out_stream) + + def handle_contract_created(self, creator_address: str, contract_address: str, gas_used: str, gas_price: str, data: str): + self._exporter.write_entry({ + 'event' : 'ContractCreated', + 'from' : creator_address, + 'contract_address' : contract_address, + 'gas_used' : gas_used, + 'gas_price' : gas_price, + 'data' : data + }) + super().handle_contract_created(creator_address, contract_address, gas_used, gas_price, data) + + 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}') From 0200d07395b98750f133acea8ab85550304f61d1 Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 27 Mar 2019 11:24:48 -0400 Subject: [PATCH 3/4] Log contract creations and function calls (#59) --- etheno/jsonrpc.py | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/etheno/jsonrpc.py b/etheno/jsonrpc.py index a99f0c3..fd27ad8 100644 --- a/etheno/jsonrpc.py +++ b/etheno/jsonrpc.py @@ -53,9 +53,12 @@ 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): + 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] @@ -72,11 +75,17 @@ def after_post(self, post_data, result): 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'] - original_transaction = self._transactions[transaction_hash]['params'][0] - self.handle_contract_created(original_transaction['from'], contract_address, result['result']['gasUsed'], original_transaction['gasPrice'], original_transaction['data']) + 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): @@ -84,16 +93,29 @@ def __init__(self, out_stream: Union[str, TextIO]): super().__init__() self._exporter = JSONExporter(out_stream) - def handle_contract_created(self, creator_address: str, contract_address: str, gas_used: str, gas_price: str, data: str): + 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 + '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_contract_created(creator_address, contract_address, gas_used, gas_price, data) + super().handle_function_call(from_address, to_address, gas_used, gas_price, data, value) def finalize(self): self._exporter.finalize() From 98e0ba8e0901e5c024abb1509508d4df7952c9ef Mon Sep 17 00:00:00 2001 From: Evan Sultanik Date: Wed, 27 Mar 2019 11:55:21 -0400 Subject: [PATCH 4/4] Record the initial set of accounts (#59) --- etheno/jsonrpc.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/etheno/jsonrpc.py b/etheno/jsonrpc.py index fd27ad8..06903dd 100644 --- a/etheno/jsonrpc.py +++ b/etheno/jsonrpc.py @@ -2,6 +2,7 @@ from typing import Dict, TextIO, Union from .etheno import EthenoPlugin +from .utils import format_hex_address class JSONExporter: def __init__(self, out_stream: Union[str, TextIO]): @@ -93,6 +94,14 @@ 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',