diff --git a/etheno/__main__.py b/etheno/__main__.py index 06d2030..4b6c511 100644 --- a/etheno/__main__.py +++ b/etheno/__main__.py @@ -35,11 +35,11 @@ def main(argv=None): help='Allow the web server to accept external connections') parser.add_argument('-p', '--port', type=int, default=GETH_DEFAULT_RPC_PORT, help='Port on which to run the JSON RPC webserver (default=%d)' % GETH_DEFAULT_RPC_PORT) - parser.add_argument('-a', '--accounts', type=int, default=None, + parser.add_argument('-a', '--accounts', type=int, default=10, help='Number of accounts to create in the client (default=10)') parser.add_argument('-b', '--balance', type=float, default=100.0, help='Default balance (in Ether) to seed to each account (default=100.0)') - parser.add_argument('-c', '--gas-price', type=int, default=None, + parser.add_argument('-c', '--gas-price', type=int, default=200000000000, help='Default gas price (default=20000000000)') parser.add_argument('-i', '--network-id', type=int, default=None, help='Specify a network ID (default is the network ID of the master client)') @@ -74,6 +74,9 @@ def main(argv=None): parser.add_argument('--geth-port', type=int, default=None, help='Port on which to run Geth (defaults to the closest available port to the port specified ' 'with --port plus one)') + parser.add_argument('-u', '--geth-unlock-accounts', action='store_true', default=False, help='Unlock accounts in geth') + parser.add_argument('--geth-ipcpath', type=str, default=None, + help='Custom path for geth ipc file') parser.add_argument('-pa', '--parity', action='store_true', default=False, help='Run Parity as a JSON RPC client') parser.add_argument('--parity-port', type=int, default=None, help='Port on which to run Parity (defaults to the closest available port to the port ' @@ -115,7 +118,7 @@ def main(argv=None): if argv is None: argv = sys.argv - + args = parser.parse_args(argv[1:]) if args.version: @@ -149,7 +152,7 @@ def main(argv=None): "your own deleting this directory!" % abspath) sys.exit(1) clear_directory(args.log_dir) - + ETHENO.logger.save_to_directory(args.log_dir) if not args.log_file: # Also create a unified log in the log dir: @@ -174,34 +177,40 @@ def main(argv=None): ETHENO.logger.error('Etheno failed to install Echidna. Please install it manually ' 'https://github.com/trailofbits/echidna') sys.exit(1) - + if args.genesis is None: # Set defaults since no genesis was supplied if args.accounts is None: args.accounts = 10 if args.gas_price is None: args.gas_price = 20000000000 - + accounts = [] if args.genesis: with open(args.genesis, 'rb') as f: genesis = json.load(f) if 'config' not in genesis: + ETHENO.logger.warn("No `config` entry in genesis json") genesis['config'] = {} if 'alloc' not in genesis: + ETHENO.logger.warn("No `alloc` entry in genesis json") genesis['alloc'] = {} if args.network_id is None: - args.network_id = genesis['config'].get('chainId', None) + args.network_id = genesis['config'].get('chainId') if args.constantinople_block is None: - args.constantinople_block = genesis['config'].get('constantinopleBlock', None) + args.constantinople_block = genesis['config'].get('constantinopleBlock') args.constantinople = args.constantinople_block is not None for addr, bal in genesis['alloc'].items(): pkey = None if 'privateKey' in bal: - pkey = bal['privateKey'] - accounts.append(Account(address=int(addr, 16), balance=decode_value(bal['balance']), - private_key=decode_value(pkey))) + pkey = bal.get('privateKey') + if pkey: + pkey = decode_value(pkey) + ETHENO.logger.info("Account %s w/ balance %s found", addr, bal['balance']) + accounts.append(Account(address=int(addr, 16), + balance=decode_value(bal['balance']), + private_key=pkey)) else: # We will generate it further below once we've resolved all of the parameters genesis = None @@ -226,7 +235,7 @@ def main(argv=None): if args.ganache and args.master: parser.print_help() sys.stderr.write('\nError: You cannot specify both --ganache and --master at the same time!\n') - sys.exit(1) + sys.exit(1) elif args.ganache: if args.ganache_port is None: args.ganache_port = find_open_port(args.port + 1) @@ -254,7 +263,7 @@ def main(argv=None): elif args.raw and not args.geth and not args.parity: ETHENO.master_client = RawTransactionClient(RpcProxyClient(args.raw[0]), accounts) args.raw = args.raw[1:] - + if args.network_id is None: if ETHENO.master_client: args.network_id = int(ETHENO.master_client.post({ @@ -283,11 +292,12 @@ def main(argv=None): geth_instance = geth.GethClient(genesis=genesis, port=args.geth_port) geth_instance.etheno = ETHENO - for account in accounts: - # TODO: Make some sort of progress bar here - geth_instance.logger.info("Unlocking Geth account %s" % format_hex_address(account.address, True)) - geth_instance.import_account(account.private_key) - geth_instance.start(unlock_accounts=True) + if args.geth_unlock_accounts: + for account in accounts: + # TODO: Make some sort of progress bar here + geth_instance.logger.info("Unlocking Geth account %s" % format_hex_address(account.address, True)) + geth_instance.import_account(account.private_key) + geth_instance.start(unlock_accounts=args.geth_unlock_accounts) if ETHENO.master_client is None: ETHENO.master_client = geth_instance else: @@ -309,7 +319,7 @@ def main(argv=None): if ETHENO.master_client is None: ETHENO.master_client = parity_instance else: - ETHENO.add_client(AddressSynchronizingClient(parity_instance)) + ETHENO.add_client(AddressSynchronizingClient(parity_instance)) for client in args.client: ETHENO.add_client(AddressSynchronizingClient(RpcProxyClient(client))) diff --git a/etheno/genesis.py b/etheno/genesis.py index f5dacb7..023045f 100644 --- a/etheno/genesis.py +++ b/etheno/genesis.py @@ -119,8 +119,10 @@ def geth_to_parity(genesis): def make_accounts(num_accounts, default_balance = None): ret = [] + if not num_accounts: + raise ValueError("No accounts passed in genesis.json") if num_accounts > len(DEFAULT_PRIVATE_KEYS): - raise Exception('TODO: Too many accounts') + raise ValueError('TODO: Too many accounts') for i in range(num_accounts): acct = w3.eth.account.from_key(DEFAULT_PRIVATE_KEYS[i]) ret.append(Account(address=int(acct.address, 16), private_key=int(acct.privateKey.hex(), 16), balance=default_balance)) diff --git a/etheno/geth.py b/etheno/geth.py index 1d699d6..e488c7b 100644 --- a/etheno/geth.py +++ b/etheno/geth.py @@ -55,16 +55,24 @@ def etheno_set(self): raise e def import_account(self, private_key): + if not private_key: + self.logger.warn("Skipping import of account without private key") + return + content = format_hex_address(private_key).encode('utf-8') + bytes([ord('\n')]) import_dir = os.path.join(self.log_directory, 'private_keys') keyfile = self.logger.make_constant_logged_file(content, prefix='private', suffix='.key', dir=import_dir) while True: args = ['/usr/bin/env', 'geth', 'account', 'import', '--datadir', self.logger.to_log_path(self.datadir), '--password', self.logger.to_log_path(self.passwords), self.logger.to_log_path(keyfile)] + self.add_to_run_script(args) + geth = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=self.log_directory) + geth.communicate() if geth.wait() == 0: return # This sometimes happens with geth, I have no idea why, so just try again + self.logger.warn("Could not import account using command `%s` ..retrying", " ".join(args)) def post(self, data): # geth takes a while to unlock all of the accounts, so check to see if that caused an error and just wait a bit @@ -89,8 +97,9 @@ def get_start_command(self, unlock_accounts=True): verbosity = 3 else: verbosity = 4 - base_args = ['/usr/bin/env', 'geth', '--nodiscover', '--rpc', '--rpcport', "%d" % self.port, '--networkid', "%d" % self.genesis['config']['chainId'], '--datadir', self.logger.to_log_path(self.datadir), '--mine', '--etherbase', format_hex_address(self.miner_account.address), f"--verbosity={verbosity}", '--minerthreads=1'] + base_args = ['/usr/bin/env', 'geth', '--nodiscover', '--http', '--http.rpcprefix', '/', '--rpc.allow-unprotected-txs', '--http.port', "%d" % self.port, '--networkid', "%d" % self.genesis['config']['chainId'], '--datadir', self.logger.to_log_path(self.datadir), '--mine', '--miner.etherbase', format_hex_address(self.miner_account.address), f"--verbosity={verbosity}", '--miner.threads=1'] if unlock_accounts: + # FIXME Account unlock with HTTP access is now forbidden, we should catch that direcly during arg parsing addresses = filter(lambda a : a != format_hex_address(self.miner_account.address), map(format_hex_address, self.genesis['alloc'])) unlock_args = ['--unlock', ','.join(addresses), '--password', self.passwords] else: diff --git a/etheno/utils.py b/etheno/utils.py index a8ecc41..4f6320b 100644 --- a/etheno/utils.py +++ b/etheno/utils.py @@ -41,6 +41,8 @@ def decode_hex(data: Optional[str]) -> Optional[bytes]: def decode_value(v: Union[str, int]) -> int: + if v is None: + return 0 if isinstance(v, int): return v elif v.startswith('0x') or (frozenset(['a', 'b', 'c', 'd', 'e', 'f']) & frozenset(v)): @@ -92,7 +94,7 @@ def find_open_port(starting_port: int = 1025) -> int: def clear_directory(path: str): """ - Deletes the contents of a directory, but not the directory itself. + Deletes the contents of a directory, but not the directory itself. This is safe to use on symlinked directories. Symlinks will be deleted, but the files and directories they point to will not be deleted. If `path` itself is a symlink, the symlink will be deleted.