def initialize(self, password: str) -> str: """Initialize a new configuration and write installation files to disk.""" # Development if self.dev_mode: self.__temp_dir = TemporaryDirectory( prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name # Persistent else: self._ensure_config_root_exists() self.write_keyring(password=password) self._cache_runtime_filepaths() self.node_storage.initialize() if self.download_registry: self.registry_filepath = EthereumContractRegistry.download_latest_publication( ) # Validate if not self.__dev_mode: self.validate(no_registry=( not self.download_registry) or self.federated_only) # Success message = "Created nucypher installation files at {}".format( self.config_root) self.log.debug(message) return self.config_root
def _configure_registry(self, fetch_registry: bool = True): RegistryClass = EthereumContractRegistry._get_registry_class(local=self.client.is_local) if fetch_registry: registry = RegistryClass.from_latest_publication() else: registry = RegistryClass() self.registry = registry
def connect(cls, provider_uri: str = None, registry: EthereumContractRegistry = None, deployer: bool = False, compile: bool = False, poa: bool = False) -> 'Blockchain': if cls._instance is NO_BLOCKCHAIN_AVAILABLE: registry = registry or EthereumContractRegistry() compiler = SolidityCompiler() if compile is True else None InterfaceClass = BlockchainDeployerInterface if deployer is True else BlockchainInterface interface = InterfaceClass(provider_uri=provider_uri, registry=registry, compiler=compiler) if poa is True: interface.w3.middleware_onion.inject(geth_poa_middleware, layer=0) cls._instance = cls(interface=interface) else: if provider_uri is not None: existing_uri = cls._instance.interface.provider_uri if existing_uri != provider_uri: raise ValueError( "There is an existing blockchain connection to {}. " "Use Interface.add_provider to connect additional providers" .format(existing_uri)) return cls._instance
def connect_to_blockchain(configuration, recompile_contracts: bool = False): try: configuration.connect_to_blockchain( recompile_contracts=recompile_contracts) configuration.connect_to_contracts() except EthereumContractRegistry.NoRegistry: message = "Cannot configure blockchain character: No contract registry found; " \ "Did you mean to pass --federated-only?" raise EthereumContractRegistry.NoRegistry(message)
def _configure_registry(self, fetch_registry: bool = True) -> None: RegistryClass = EthereumContractRegistry._get_registry_class( local=self.client.is_local) if fetch_registry: registry = RegistryClass.from_latest_publication() else: registry = RegistryClass() self.registry = registry self.log.info("Using contract registry {}".format( self.registry.filepath))
def from_dict(cls, payload: dict, **overrides) -> 'BlockchainInterface': # Apply overrides payload.update({k: v for k, v in overrides.items() if v is not None}) registry = EthereumContractRegistry( registry_filepath=payload['registry_filepath']) blockchain = cls(provider_uri=payload['provider_uri'], registry=registry) return blockchain
def connect_to_blockchain(self, character_configuration, recompile_contracts: bool = False): try: character_configuration.connect_to_blockchain(recompile_contracts=recompile_contracts) character_configuration.connect_to_contracts() except EthereumContractRegistry.NoRegistry: message = "No contract registry found; Did you mean to pass --federated-only?" raise EthereumContractRegistry.NoRegistry(message) else: self.blockchain = character_configuration.blockchain self.accounts = self.blockchain.interface.w3.eth.accounts
def test_rollback(click_runner): """Roll 'em all back!""" mock_temporary_registry = EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH) blockchain = Blockchain.connect(registry=mock_temporary_registry) # Input Components yes = 'Y\n' # Stage Rollbacks old_secret = INSECURE_SECRETS[PLANNED_UPGRADES] rollback_secret = generate_insecure_secret() user_input = '0\n' + yes + old_secret + rollback_secret + rollback_secret contracts_to_rollback = ('MinersEscrow', # v4 -> v3 'PolicyManager', # v4 -> v3 'MiningAdjudicator', # v4 -> v3 ) # Execute Rollbacks for contract_name in contracts_to_rollback: command = ('rollback', '--contract-name', contract_name, '--registry-infile', MOCK_REGISTRY_FILEPATH, '--provider-uri', TEST_PROVIDER_URI, '--poa') result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) assert result.exit_code == 0 records = blockchain.interface.registry.search(contract_name=contract_name) assert len(records) == 4 *old_records, v3, v4 = records current_target, rollback_target = v4, v3 _name, current_target_address, *abi = current_target _name, rollback_target_address, *abi = rollback_target assert current_target_address != rollback_target_address # Ensure the proxy targets the rollback target (previous version) with pytest.raises(BlockchainInterface.UnknownContract): blockchain.interface.get_proxy(target_address=current_target_address, proxy_name='Dispatcher') proxy = blockchain.interface.get_proxy(target_address=rollback_target_address, proxy_name='Dispatcher') # Deeper - Ensure the proxy targets the old deployment on-chain targeted_address = proxy.functions.target().call() assert targeted_address != current_target assert targeted_address == rollback_target_address
def get_blockchain_interface(self) -> None: if self.federated_only: raise CharacterConfiguration.ConfigurationError( "Cannot connect to blockchain in federated mode") registry = None if self.registry_filepath: registry = EthereumContractRegistry( registry_filepath=self.registry_filepath) self.blockchain = BlockchainInterface( provider_uri=self.provider_uri, poa=self.poa, registry=registry, provider_process=self.provider_process)
def from_configuration_file(cls, config: NodeConfiguration) -> 'BlockchainInterface': # Parse payload = parse_blockchain_config(filepath=config.config_file_location) # Init deps compiler = SolidityCompiler() if payload['compile'] else None registry = EthereumContractRegistry.from_config(config=config) interface_class = BlockchainInterface if not payload['deploy'] else BlockchainDeployerInterface # init class interface = interface_class(timeout=payload['timeout'], provider_uri=payload['provider_uri'], compiler=compiler, registry=registry) return interface
def from_config(cls, filepath=None) -> 'BlockchainInterface': # Parse filepath = filepath if filepath is None else DEFAULT_INI_FILEPATH payload = parse_blockchain_config(filepath=filepath) # Init deps compiler = SolidityCompiler() if payload['compile'] else None registry = EthereumContractRegistry.from_config(filepath=filepath) interface_class = BlockchainInterface if not payload['deploy'] else BlockchainDeployerInterface # init class circumflex = interface_class(timeout=payload['timeout'], provider_uri=payload['provider_uri'], compiler=compiler, registry=registry) return circumflex
def make_testerchain(provider_uri, solidity_compiler): # Destroy existing blockchain BlockchainInterface.disconnect() TesterBlockchain.sever_connection() registry = EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH) deployer_interface = BlockchainDeployerInterface(compiler=solidity_compiler, registry=registry, provider_uri=provider_uri) # Create new blockchain testerchain = TesterBlockchain(interface=deployer_interface, eth_airdrop=True, free_transactions=False, poa=True) # Set the deployer address from a freshly created test account deployer_interface.deployer_address = testerchain.etherbase_account return testerchain
def initialize(self, password: str) -> str: """Initialize a new configuration and write installation files to disk.""" # Development if self.dev_mode: if password is DEVELOPMENT_CONFIGURATION: self.abort_on_learning_error = True self.save_metadata = False self.reload_metadata = False alphabet = string.ascii_letters + string.digits password = ''.join(secrets.choice(alphabet) for _ in range(32)) else: raise self.ConfigurationError( "Password cannot be specified for development configurations." ) self.__temp_dir = TemporaryDirectory( prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name # Persistent else: self.write_config_root() self.write_keyring(password=password) self._cache_runtime_filepaths() self.node_storage.initialize() if self.download_registry: self.registry_filepath = EthereumContractRegistry.download_latest_publication( ) # Validate if not self.__dev_mode: self.validate(no_registry=( not self.download_registry) or self.federated_only) # Success message = "Created nucypher installation files at {}".format( self.config_root) self.log.debug(message) return self.config_root
def connect_to_blockchain(self, character_configuration, recompile_contracts: bool = False, full_sync: bool = True): try: character_configuration.connect_to_blockchain( recompile_contracts=recompile_contracts, full_sync=full_sync) character_configuration.connect_to_contracts() except EthereumContractRegistry.NoRegistry: _registry_filepath = EthereumContractRegistry.from_latest_publication( ) except Exception as e: if self.debug: raise click.secho(str(e), fg='red', bold=True) raise click.Abort() # Success else: self.blockchain = character_configuration.blockchain self.accounts = self.blockchain.interface.w3.eth.accounts
def alice( click_config, action, # Mode dev, force, dry_run, # Network teacher_uri, min_stake, federated_only, network, discovery_port, controller_port, # Filesystem config_root, config_file, # Blockchain pay_with, provider_uri, geth, sync, poa, registry_filepath, hw_wallet, # Alice bob_encrypting_key, bob_verifying_key, label, m, n, value, rate, duration, expiration, message_kit, ): """ "Alice the Policy Authority" management commands. """ # # Validate # if federated_only and geth: raise click.BadOptionUsage( option_name="--geth", message="Federated only cannot be used with the --geth flag") # Banner emitter = click_config.emitter emitter.clear() emitter.banner(ALICE_BANNER) # # Managed Ethereum Client # ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') # # Eager Actions (No Authentication Required) # if action == 'init': """Create a brand-new persistent Alice""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if not provider_uri and not federated_only: raise click.BadOptionUsage( option_name='--provider', message= "--provider is required to create a new decentralized alice.") if not config_root: # Flag config_root = click_config.config_file # Envvar if not pay_with and not federated_only: registry = None if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(sync_now=sync, fetch_registry=False) pay_with = select_client_account(emitter=emitter, blockchain=blockchain) new_alice_config = AliceConfiguration.generate( password=get_nucypher_password(confirm=True), config_root=config_root, checksum_address=pay_with, domains={network} if network else None, federated_only=federated_only, download_registry=click_config.no_registry, registry_filepath=registry_filepath, provider_process=ETH_NODE, poa=poa, provider_uri=provider_uri, m=m, n=n, duration=duration, rate=rate) painting.paint_new_installation_help( emitter, new_configuration=new_alice_config) return # Exit elif action == "view": """Paint an existing configuration to the console""" configuration_file_location = config_file or AliceConfiguration.default_filepath( ) response = AliceConfiguration._read_configuration_file( filepath=configuration_file_location) return emitter.ipc( response=response, request_id=0, duration=0) # FIXME: what are request_id and duration here? # # Get Alice Configuration # if dev: alice_config = AliceConfiguration( dev_mode=True, network_middleware=click_config.middleware, domains={network}, provider_process=ETH_NODE, provider_uri=provider_uri, federated_only=True) else: try: alice_config = AliceConfiguration.from_configuration_file( dev_mode=False, filepath=config_file, domains={network} if network else None, network_middleware=click_config.middleware, rest_port=discovery_port, checksum_address=pay_with, provider_process=ETH_NODE, provider_uri=provider_uri) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=AliceConfiguration, config_file=config_file) if action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher alice destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) return actions.destroy_configuration(emitter, character_config=alice_config, force=force) # # Produce Alice # # TODO: OH MY. client_password = None if not alice_config.federated_only: if (not hw_wallet or not dev) and not click_config.json_ipc: client_password = get_client_password( checksum_address=alice_config.checksum_address) ALICE = actions.make_cli_character(character_config=alice_config, click_config=click_config, dev=dev, teacher_uri=teacher_uri, min_stake=min_stake, client_password=client_password) # # Admin Actions # if action == "run": """Start Alice Controller""" try: # RPC if click_config.json_ipc: rpc_controller = ALICE.make_rpc_controller() _transport = rpc_controller.make_control_transport() rpc_controller.start() return # HTTP else: emitter.message( f"Alice Verifying Key {bytes(ALICE.stamp).hex()}", color="green", bold=True) controller = ALICE.make_web_controller( crash_on_error=click_config.debug) ALICE.log.info('Starting HTTP Character Web Controller') emitter.message( f'Running HTTP Alice Controller at http://localhost:{controller_port}' ) return controller.start(http_port=controller_port, dry_run=dry_run) # Handle Crash except Exception as e: alice_config.log.critical(str(e)) emitter.message(f"{e.__class__.__name__} {e}", color='red', bold=True) if click_config.debug: raise # Crash :-( return # # Alice API # elif action == "public-keys": response = ALICE.controller.public_keys() return response elif action == "derive-policy-pubkey": # Validate if not label: raise click.BadOptionUsage( option_name='label', message= "--label is required for deriving a policy encrypting key.") # Request return ALICE.controller.derive_policy_encrypting_key(label=label) elif action == "grant": # Validate if not all((bob_verifying_key, bob_encrypting_key, label)): raise click.BadArgumentUsage( message= "--bob-verifying-key, --bob-encrypting-key, and --label are " "required options to grant (optionally --m, --n, and --expiration)." ) # Request grant_request = { 'bob_encrypting_key': bob_encrypting_key, 'bob_verifying_key': bob_verifying_key, 'label': label, 'm': m, 'n': n, 'expiration': expiration, } if not ALICE.federated_only: grant_request.update({'value': value}) return ALICE.controller.grant(request=grant_request) elif action == "revoke": # Validate if not label and bob_verifying_key: raise click.BadArgumentUsage( message= f"--label and --bob-verifying-key are required options for revoke." ) # Request revoke_request = { 'label': label, 'bob_verifying_key': bob_verifying_key } return ALICE.controller.revoke(request=revoke_request) elif action == "decrypt": # Validate if not all((label, message_kit)): input_specification, output_specification = ALICE.controller.get_specifications( interface_name=action) required_fields = ', '.join(input_specification) raise click.BadArgumentUsage( f'{required_fields} are required flags to decrypt') # Request request_data = {'label': label, 'message_kit': message_kit} response = ALICE.controller.decrypt(request=request_data) return response else: raise click.BadArgumentUsage(f"No such argument {action}")
def ursula( click_config, action, dev, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, staker_address, worker_address, federated_only, poa, config_root, config_file, provider_uri, geth, registry_filepath, interactive, sync, ) -> None: """ "Ursula the Untrusted" PRE Re-encryption node management commands. \b Actions ------------------------------------------------- \b init Create a new Ursula node configuration. view View the Ursula node's configuration. run Run an "Ursula" node. save-metadata Manually write node metadata to disk without running forget Forget all known nodes. destroy Delete Ursula node configuration. confirm-activity Manually confirm-activity for the current period. """ emitter = click_config.emitter # # Validate # if federated_only: if geth: raise click.BadOptionUsage( option_name="--geth", message="Federated only cannot be used with the --geth flag") if staker_address: raise click.BadOptionUsage( option_name='--federated-only', message="Staking address cannot be used in federated mode.") # Banner emitter.banner(URSULA_BANNER.format(worker_address or '')) # # Pre-Launch Warnings # if dev: emitter.echo("WARNING: Running in Development mode", color='yellow', verbosity=1) if force: emitter.echo("WARNING: Force is enabled", color='yellow', verbosity=1) # # Internal Ethereum Client # ETH_NODE = NO_BLOCKCHAIN_CONNECTION if geth: ETH_NODE = actions.get_provider_process() provider_uri = ETH_NODE.provider_uri(scheme='file') # # Eager Actions # if action == "init": """Create a brand-new persistent Ursula""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if (not staker_address or not worker_address) and not federated_only: # Connect to Blockchain fetch_registry = registry_filepath is None and not click_config.no_registry registry = None if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(fetch_registry=fetch_registry, sync_now=sync, emitter=emitter) if not staker_address: prompt = "Select staker account" staker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not worker_address: prompt = "Select worker account" worker_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = actions.determine_external_ip_address(emitter, force=force) download_registry = not federated_only and not click_config.no_registry ursula_config = UrsulaConfiguration.generate( password=get_nucypher_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_address=staker_address, worker_address=worker_address, download_registry=download_registry, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, poa=poa) painting.paint_new_installation_help(emitter, new_configuration=ursula_config) return # # Make Ursula # if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, download_registry=False, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, checksum_address=staker_address, worker_address=worker_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) else: try: ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, registry_filepath=registry_filepath, provider_process=ETH_NODE, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, poa=poa, federated_only=federated_only) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=UrsulaConfiguration, config_file=config_file) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Configured Pre-Authentication Actions # # Handle destruction and forget *before* network bootstrap and character initialization below if action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode - There is nothing to destroy." raise click.BadOptionUsage(option_name='--dev', message=message) actions.destroy_configuration(emitter, character_config=ursula_config, force=force) return elif action == "forget": actions.forget(emitter, configuration=ursula_config) return # # Make Ursula # client_password = None if not ursula_config.federated_only: if not dev and not click_config.json_ipc: client_password = get_client_password( checksum_address=ursula_config.worker_address, envvar="NUCYPHER_WORKER_ETH_PASSWORD") try: URSULA = actions.make_cli_character(character_config=ursula_config, click_config=click_config, min_stake=min_stake, teacher_uri=teacher_uri, dev=dev, lonely=lonely, client_password=client_password) except NucypherKeyring.AuthenticationFailed as e: emitter.echo(str(e), color='red', bold=True) click.get_current_context().exit(1) # TODO: Exit codes (not only for this, but for other exceptions) # # Authenticated Action Switch # if action == 'run': """Seed, Produce, Run!""" # GO! try: # Ursula Deploy Warnings emitter.message(f"Starting Ursula on {URSULA.rest_interface}", color='green', bold=True) emitter.message(f"Connecting to {','.join(ursula_config.domains)}", color='green', bold=True) emitter.message("Working ~ Keep Ursula Online!", color='blue', bold=True) if interactive: stdio.StandardIO( UrsulaCommandProtocol(ursula=URSULA, emitter=emitter)) if dry_run: return # <-- ABORT - (Last Chance) # Run - Step 3 node_deployer = URSULA.get_deployer() node_deployer.addServices() node_deployer.catalogServers(node_deployer.hendrix) node_deployer.run() # <--- Blocking Call (Reactor) # Handle Crash except Exception as e: ursula_config.log.critical(str(e)) emitter.message(f"{e.__class__.__name__} {e}", color='red', bold=True) raise # Crash :-( # Graceful Exit finally: emitter.message("Stopping Ursula", color='green') ursula_config.cleanup() emitter.message("Ursula Stopped", color='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" metadata_path = ursula.write_node_metadata(node=URSULA) emitter.message( f"Successfully saved node metadata to {metadata_path}.", color='green') return elif action == "view": """Paint an existing configuration to the console""" if not URSULA.federated_only: emitter.echo("BLOCKCHAIN ----------\n") painting.paint_contract_status(emitter=emitter, blockchain=URSULA.blockchain) current_block = URSULA.blockchain.w3.eth.blockNumber emitter.echo(f'Block # {current_block}') # TODO: 1231 emitter.echo(f'NU Balance (staker): {URSULA.token_balance}') emitter.echo( f'ETH Balance (worker): {URSULA.blockchain.client.get_balance(URSULA.worker_address)}' ) emitter.echo( f'Current Gas Price {URSULA.blockchain.client.gas_price}') emitter.echo("CONFIGURATION --------") response = UrsulaConfiguration._read_configuration_file( filepath=config_file or ursula_config.config_file_location) return emitter.ipc( response=response, request_id=0, duration=0 ) # FIXME: #1216 - what are request_id and duration here? elif action == 'confirm-activity': receipt = URSULA.confirm_activity() confirmed_period = URSULA.staking_agent.get_current_period() + 1 date = datetime_at_period(period=confirmed_period) # TODO: Double-check dates here emitter.echo( f'\nActivity confirmed for period #{confirmed_period} ' f'(starting at {date})', bold=True, color='blue') painting.paint_receipt_summary( emitter=emitter, receipt=receipt, chain_name=URSULA.blockchain.client.chain_name) # TODO: Check ActivityConfirmation event (see #1193) return else: raise click.BadArgumentUsage("No such argument {}".format(action))
def bob(click_config, action, teacher_uri, min_stake, controller_port, discovery_port, federated_only, network, config_root, config_file, checksum_address, provider_uri, registry_filepath, dev, force, poa, dry_run, label, policy_encrypting_key, alice_verifying_key, message_kit, sync): """ "Bob" management commands. \b Actions ------------------------------------------------- \b init Create a brand new persistent Bob view View existing Bob's configuration. run Start Bob's controller. destroy Delete existing Bob's configuration. public-keys Obtain Bob's public verification and encryption keys. retrieve Obtain plaintext from encrypted data, if access was granted. """ # # Validate # # Banner emitter = click_config.emitter emitter.clear() emitter.banner(BOB_BANNER) # # Eager Actions # if action == 'init': """Create a brand-new persistent Bob""" if dev: raise click.BadArgumentUsage( "Cannot create a persistent development character") if not config_root: # Flag config_root = click_config.config_file # Envvar if not checksum_address and not federated_only: registry = None if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(sync_now=sync, emitter=emitter) checksum_address = select_client_account(emitter=emitter, blockchain=blockchain) download_registry = not federated_only and not click_config.no_registry new_bob_config = BobConfiguration.generate( password=get_nucypher_password(confirm=True), config_root=config_root or DEFAULT_CONFIG_ROOT, checksum_address=checksum_address, domains={network} if network else None, federated_only=federated_only, download_registry=download_registry, registry_filepath=registry_filepath, provider_uri=provider_uri) return painting.paint_new_installation_help( emitter, new_configuration=new_bob_config) # # Make Bob # if dev: bob_config = BobConfiguration( dev_mode=True, domains={network}, provider_uri=provider_uri, federated_only=True, checksum_address=checksum_address, network_middleware=click_config.middleware) else: try: bob_config = BobConfiguration.from_configuration_file( filepath=config_file, domains={network} if network else None, checksum_address=checksum_address, rest_port=discovery_port, provider_uri=provider_uri, network_middleware=click_config.middleware) except FileNotFoundError: return actions.handle_missing_configuration_file( character_config_class=BobConfiguration, config_file=config_file) BOB = actions.make_cli_character(character_config=bob_config, click_config=click_config, dev=dev, teacher_uri=teacher_uri, min_stake=min_stake) # # Admin Action # if action == "run": # RPC if click_config.json_ipc: rpc_controller = BOB.make_rpc_controller() _transport = rpc_controller.make_control_transport() rpc_controller.start() return # Echo Public Keys emitter.message(f"Bob Verifying Key {bytes(BOB.stamp).hex()}", color='green', bold=True) bob_encrypting_key = bytes(BOB.public_keys(DecryptingPower)).hex() emitter.message(f"Bob Encrypting Key {bob_encrypting_key}", color="blue", bold=True) # Start Controller controller = BOB.make_web_controller(crash_on_error=click_config.debug) BOB.log.info('Starting HTTP Character Web Controller') return controller.start(http_port=controller_port, dry_run=dry_run) elif action == "view": """Paint an existing configuration to the console""" response = BobConfiguration._read_configuration_file( filepath=config_file or bob_config.config_file_location) return BOB.controller.emitter.ipc( response, request_id=0, duration=0 ) # FIXME: #1216 - what are request_id and duration here? elif action == "destroy": """Delete Bob's character configuration files from the disk""" # Validate if dev: message = "'nucypher bob destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) # Request return actions.destroy_configuration(emitter, character_config=bob_config) # # Bob API Actions # elif action == "public-keys": response = BOB.controller.public_keys() return response elif action == "retrieve": # Validate if not all( (label, policy_encrypting_key, alice_verifying_key, message_kit)): input_specification, output_specification = BOB.control.get_specifications( interface_name='retrieve') required_fields = ', '.join(input_specification) raise click.BadArgumentUsage( f'{required_fields} are required flags to retrieve') # Request bob_request_data = { 'label': label, 'policy_encrypting_key': policy_encrypting_key, 'alice_verifying_key': alice_verifying_key, 'message_kit': message_kit, } response = BOB.controller.retrieve(request=bob_request_data) return response else: raise click.BadArgumentUsage(f"No such argument {action}")
def deploy(click_config, action, poa, provider_uri, geth, enode, deployer_address, contract_name, allocation_infile, allocation_outfile, registry_infile, registry_outfile, no_compile, amount, recipient_address, config_root, sync, force): """Manage contract and registry deployment""" ETH_NODE = None # # Validate # # Ensure config root exists, because we need a default place to put output files. config_root = config_root or DEFAULT_CONFIG_ROOT if not os.path.exists(config_root): os.makedirs(config_root) # # Connect to Blockchain # # Establish a contract registry from disk if specified registry, registry_filepath = None, (registry_outfile or registry_infile) if registry_filepath is not None: registry = EthereumContractRegistry( registry_filepath=registry_filepath) if geth: # Spawn geth child process ETH_NODE = NuCypherGethDevnetProcess(config_root=config_root) ETH_NODE.ensure_account_exists(password=click_config.get_password( confirm=True)) if not ETH_NODE.initialized: ETH_NODE.initialize_blockchain() ETH_NODE.start() # TODO: Graceful shutdown provider_uri = ETH_NODE.provider_uri # Deployment-tuned blockchain connection blockchain = Blockchain.connect(provider_uri=provider_uri, poa=poa, registry=registry, compile=not no_compile, deployer=True, fetch_registry=False, sync=sync) # # Deployment Actor # if not deployer_address: for index, address in enumerate(blockchain.interface.w3.eth.accounts): click.secho(f"{index} --- {address}") choices = click.IntRange(0, len(blockchain.interface.w3.eth.accounts)) deployer_address_index = click.prompt("Select deployer address", default=0, type=choices) deployer_address = blockchain.interface.w3.eth.accounts[ deployer_address_index] # Verify Address if not force: click.confirm("Selected {} - Continue?".format(deployer_address), abort=True) deployer = Deployer(blockchain=blockchain, deployer_address=deployer_address) # Verify ETH Balance click.secho(f"\n\nDeployer ETH balance: {deployer.eth_balance}") if deployer.eth_balance == 0: click.secho("Deployer address has no ETH.", fg='red', bold=True) raise click.Abort() if not blockchain.interface.is_local: # (~ dev mode; Assume accounts are already unlocked) password = click.prompt("Enter ETH node password", hide_input=True) blockchain.interface.w3.geth.personal.unlockAccount( deployer_address, password) # Add ETH Bootnode or Peer if enode: if geth: blockchain.interface.w3.geth.admin.addPeer(enode) click.secho(f"Added ethereum peer {enode}") else: raise NotImplemented # TODO: other backends # # Action switch # if action == 'upgrade': if not contract_name: raise click.BadArgumentUsage( message="--contract-name is required when using --upgrade") existing_secret = click.prompt( 'Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) deployer.upgrade_contract(contract_name=contract_name, existing_plaintext_secret=existing_secret, new_plaintext_secret=new_secret) elif action == 'rollback': existing_secret = click.prompt( 'Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) deployer.rollback_contract(contract_name=contract_name, existing_plaintext_secret=existing_secret, new_plaintext_secret=new_secret) elif action == "contracts": registry_filepath = deployer.blockchain.interface.registry.filepath if os.path.isfile(registry_filepath): click.secho( f"\nThere is an existing contract registry at {registry_filepath}.\n" f"Did you mean 'nucypher-deploy upgrade'?\n", fg='yellow') click.confirm( "Optionally, destroy existing local registry and continue?", abort=True) click.confirm( f"Confirm deletion of contract registry '{registry_filepath}'?", abort=True) os.remove(registry_filepath) # # Deploy Single Contract # if contract_name: # TODO: Handle secret collection for single contract deployment try: deployer_func = deployer.deployers[contract_name] except KeyError: message = f"No such contract {contract_name}. Available contracts are {deployer.deployers.keys()}" click.secho(message, fg='red', bold=True) raise click.Abort() else: # Deploy single contract _txs, _agent = deployer_func() # TODO: Painting for single contract deployment if ETH_NODE: ETH_NODE.stop() return # # Stage Deployment # # Track tx hashes, and new agents __deployment_transactions = dict() __deployment_agents = dict() secrets = click_config.collect_deployment_secrets() click.clear() click.secho(NU_BANNER) w3 = deployer.blockchain.interface.w3 click.secho(f"Current Time ........ {maya.now().iso8601()}") click.secho( f"Web3 Provider ....... {deployer.blockchain.interface.provider_uri}" ) click.secho(f"Block ............... {w3.eth.blockNumber}") click.secho(f"Gas Price ........... {w3.eth.gasPrice}") click.secho(f"Deployer Address .... {deployer.checksum_address}") click.secho(f"ETH ................. {deployer.eth_balance}") click.secho( f"CHAIN ID............. {deployer.blockchain.interface.chain_id}") click.secho( f"CHAIN................ {deployer.blockchain.interface.chain_name}" ) # Ask - Last chance to gracefully abort if not force: click.secho( "\nDeployment successfully staged. Take a deep breath. \n", fg='green') if click.prompt("Type 'DEPLOY' to continue") != 'DEPLOY': raise click.Abort() # Delay - Last chance to crash and abort click.secho(f"Starting deployment in 3 seconds...", fg='red') time.sleep(1) click.secho(f"2...", fg='yellow') time.sleep(1) click.secho(f"1...", fg='green') time.sleep(1) click.secho(f"Deploying...", bold=True) # # DEPLOY < ------- # txhashes, deployers = deployer.deploy_network_contracts( miner_secret=secrets.miner_secret, policy_secret=secrets.policy_secret, adjudicator_secret=secrets.mining_adjudicator_secret, user_escrow_proxy_secret=secrets.escrow_proxy_secret) # Success __deployment_transactions.update(txhashes) # # Paint # total_gas_used = 0 # TODO: may be faulty for contract_name, transactions in __deployment_transactions.items(): # Paint heading heading = '\n{} ({})'.format( contract_name, deployers[contract_name].contract_address) click.secho(heading, bold=True) click.echo('*' * (42 + 3 + len(contract_name))) for tx_name, txhash in transactions.items(): # Wait for inclusion in the blockchain try: receipt = deployer.blockchain.wait_for_receipt( txhash=txhash) except TimeExhausted: raise # TODO: Option to wait longer or retry # Examine Receipt # TODO: This currently cannot receive failed transactions if receipt['status'] == 1: click.secho("OK", fg='green', nl=False, bold=True) else: click.secho("Failed", fg='red', nl=False, bold=True) # Accumulate gas total_gas_used += int(receipt['gasUsed']) # Paint click.secho(" | {}".format(tx_name), fg='yellow', nl=False) click.secho(" | {}".format(txhash.hex()), fg='yellow', nl=False) click.secho(" ({} gas)".format(receipt['cumulativeGasUsed'])) click.secho("Block #{} | {}\n".format( receipt['blockNumber'], receipt['blockHash'].hex())) # Paint outfile paths click.secho( "Cumulative Gas Consumption: {} gas".format(total_gas_used), bold=True, fg='blue') registry_outfile = deployer.blockchain.interface.registry.filepath click.secho('Generated registry {}'.format(registry_outfile), bold=True, fg='blue') # Save transaction metadata receipts_filepath = deployer.save_deployment_receipts( transactions=__deployment_transactions) click.secho(f"Saved deployment receipts to {receipts_filepath}", fg='blue', bold=True) # # Publish Contract Registry # if not deployer.blockchain.interface.is_local: if click.confirm("Publish new contract registry?"): try: response = registry.publish( ) # TODO: Handle non-200 response and dehydrate except EthereumContractRegistry.RegistryError as e: click.secho("Registry publication failed.", fg='red', bold=True) click.secho(str(e)) raise click.Abort() click.secho(f"Published new contract registry.", fg='green') elif action == "allocations": if not allocation_infile: allocation_infile = click.prompt("Enter allocation data filepath") click.confirm("Continue deploying and allocating?", abort=True) deployer.deploy_beneficiaries_from_file( allocation_data_filepath=allocation_infile, allocation_outfile=allocation_outfile) elif action == "transfer": token_agent = NucypherTokenAgent(blockchain=blockchain) click.confirm( f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?", abort=True) txhash = token_agent.transfer( amount=amount, sender_address=token_agent.contract_address, target_address=recipient_address) click.secho(f"OK | {txhash}") elif action == "publish-registry": registry = deployer.blockchain.interface.registry click.confirm( f"Publish {registry.filepath} to GitHub (Authentication Required)?", abort=True) try: response = registry.publish( ) # TODO: Handle non-200 response and dehydrate except EthereumContractRegistry.RegistryError as e: click.secho(str(e)) raise click.Abort() click.secho(f"Published new contract registry.", fg='green') elif action == "destroy-registry": registry_filepath = deployer.blockchain.interface.registry.filepath click.confirm( f"Are you absolutely sure you want to destroy the contract registry at {registry_filepath}?", abort=True) os.remove(registry_filepath) click.secho(f"Successfully destroyed {registry_filepath}", fg='red') else: raise click.BadArgumentUsage(message=f"Unknown action '{action}'") if ETH_NODE: ETH_NODE.stop()
def __init__(self, provider_uri: str = None, provider=None, auto_connect: bool = True, timeout: int = None, registry: EthereumContractRegistry = None, compiler: SolidityCompiler = None) -> None: """ A blockchain "network interface"; The circumflex wraps entirely around the bounds of contract operations including compilation, deployment, and execution. Filesystem Configuration Node Client EVM ================ ====================== =============== ===================== =========================== Solidity Files -- SolidityCompiler --- --- HTTPProvider ------ ... | | | | *BlockchainInterface* -- IPCProvider ----- External EVM (geth, parity...) | | | | | | Registry File -- ContractRegistry --- | ---- TestProvider ----- EthereumTester | | | | | | PyEVM (Development Chain) Runtime Files -- -------- Blockchain | | | | Key Files ------ NodeConfiguration -------- Agent ... (Contract API) | | | | | | ---------- Actor ... (Blockchain-Character API) | | | | Config File --- Character ... (Public API) | Human The BlockchainInterface is the junction of the solidity compiler, a contract registry, and a collection of web3 network providers as a means of interfacing with the ethereum blockchain to execute or deploy contract code on the network. Compiler and Registry Usage ----------------------------- Contracts are freshly re-compiled if an instance of SolidityCompiler is passed; otherwise, The registry will read contract data saved to disk that is be used to retrieve contact address and op-codes. Optionally, A registry instance can be passed instead. Provider Usage --------------- https: // github.com / ethereum / eth - tester # available-backends * HTTP Provider - supply endpiont_uri * Websocket Provider - supply endpoint uri and websocket=True * IPC Provider - supply IPC path * Custom Provider - supply an iterable of web3.py provider instances """ self.log = Logger("blockchain-interface") # type: Logger # # Providers # self.w3 = NO_BLOCKCHAIN_CONNECTION self.__provider = provider or NO_BLOCKCHAIN_CONNECTION self.provider_uri = NO_BLOCKCHAIN_CONNECTION self.timeout = timeout if timeout is not None else self.__default_timeout if provider_uri and provider: raise self.InterfaceError("Pass a provider URI string, or a list of provider instances.") elif provider_uri: self.provider_uri = provider_uri self.add_provider(provider_uri=provider_uri) elif provider: self.provider_uri = MANUAL_PROVIDERS_SET self.add_provider(provider) else: self.log.warn("No provider supplied for new blockchain interface; Using defaults") # if a SolidityCompiler class instance was passed, compile from solidity source code recompile = True if compiler is not None else False self.__recompile = recompile self.__sol_compiler = compiler # Setup the registry and base contract factory cache registry = registry if registry is not None else EthereumContractRegistry() self.registry = registry self.log.info("Using contract registry {}".format(self.registry.filepath)) if self.__recompile is True: # Execute the compilation if we're recompiling # Otherwise read compiled contract data from the registry interfaces = self.__sol_compiler.compile() __raw_contract_cache = interfaces else: __raw_contract_cache = NO_COMPILATION_PERFORMED self.__raw_contract_cache = __raw_contract_cache # Auto-connect self.autoconnect = auto_connect if self.autoconnect is True: self.connect()
def initialize(self, password: str, download_registry: bool = True) -> str: """Initialize a new configuration and write installation files to disk.""" # # Create Base System Filepaths # if self.__dev_mode: self.__temp_dir = TemporaryDirectory(prefix=self.TEMP_CONFIGURATION_DIR_PREFIX) self.config_root = self.__temp_dir.name else: # Production Configuration try: os.mkdir(self.config_root, mode=0o755) except FileExistsError: if os.listdir(self.config_root): message = "There are existing files located at {}".format(self.config_root) self.log.debug(message) except FileNotFoundError: os.makedirs(self.config_root, mode=0o755) # Generate Installation Subdirectories self._cache_runtime_filepaths() # # Node Storage # self.node_storage.initialize() # # Keyring # if not self.dev_mode: if not os.path.isdir(self.keyring_dir): os.mkdir(self.keyring_dir, mode=0o700) # TODO: Keyring backend entry point - COS self.write_keyring(password=password) # # Registry # if download_registry and not self.federated_only: self.registry_filepath = EthereumContractRegistry.download_latest_publication() # # Verify # if not self.__dev_mode: self.validate(config_root=self.config_root, no_registry=(not download_registry) or self.federated_only) # # Success # message = "Created nucypher installation files at {}".format(self.config_root) self.log.debug(message) return self.config_root
def ursula( click_config, action, debug, dev, quiet, dry_run, force, lonely, network, teacher_uri, min_stake, rest_host, rest_port, db_filepath, checksum_address, federated_only, poa, config_root, config_file, metadata_dir, # TODO: Start nodes from an additional existing metadata dir provider_uri, no_registry, registry_filepath) -> None: """ Manage and run an "Ursula" PRE node. \b Actions ------------------------------------------------- \b run Run an "Ursula" node. init Create a new Ursula node configuration. view View the Ursula node's configuration. forget Forget all known nodes. save-metadata Manually write node metadata to disk without running destroy Delete Ursula node configuration. """ # # Boring Setup Stuff # if not quiet: click.secho(URSULA_BANNER) log = Logger('ursula.cli') if debug and quiet: raise click.BadOptionUsage( option_name="quiet", message="--debug and --quiet cannot be used at the same time.") if debug: click_config.log_to_sentry = False click_config.log_to_file = True globalLogPublisher.removeObserver(logToSentry) # Sentry GlobalConsoleLogger.set_log_level("debug") elif quiet: globalLogPublisher.removeObserver(logToSentry) globalLogPublisher.removeObserver(SimpleObserver) globalLogPublisher.removeObserver(getJsonFileObserver()) # # Pre-Launch Warnings # if not quiet: if dev: click.secho("WARNING: Running in development mode", fg='yellow') if force: click.secho("WARNING: Force is enabled", fg='yellow') # # Unauthenticated Configurations # if action == "init": """Create a brand-new persistent Ursula""" if dev and not quiet: click.secho("WARNING: Using temporary storage area", fg='yellow') if not config_root: # Flag config_root = click_config.config_file # Envvar if not rest_host: rest_host = click.prompt( "Enter Ursula's public-facing IPv4 address") ursula_config = UrsulaConfiguration.generate( password=click_config.get_password(confirm=True), config_root=config_root, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, domains={network} if network else None, federated_only=federated_only, checksum_public_address=checksum_address, no_registry=federated_only or no_registry, registry_filepath=registry_filepath, provider_uri=provider_uri, poa=poa) if not quiet: click.secho("Generated keyring {}".format( ursula_config.keyring_dir), fg='green') click.secho("Saved configuration file {}".format( ursula_config.config_file_location), fg='green') # Give the use a suggestion as to what to do next... how_to_run_message = "\nTo run an Ursula node from the default configuration filepath run: \n\n'{}'\n" suggested_command = 'nucypher ursula run' if config_root is not None: config_file_location = os.path.join( config_root, config_file or UrsulaConfiguration.CONFIG_FILENAME) suggested_command += ' --config-file {}'.format( config_file_location) click.secho(how_to_run_message.format(suggested_command), fg='green') return # FIN else: click.secho("OK") elif action == "destroy": """Delete all configuration files from the disk""" if dev: message = "'nucypher ursula destroy' cannot be used in --dev mode" raise click.BadOptionUsage(option_name='--dev', message=message) destroy_system_configuration(config_class=UrsulaConfiguration, config_file=config_file, network=network, config_root=config_root, force=force, log=log) if not quiet: click.secho("Destroyed {}".format(config_root)) return # Development Configuration if dev: ursula_config = UrsulaConfiguration( dev_mode=True, domains={TEMPORARY_DOMAIN}, poa=poa, registry_filepath=registry_filepath, provider_uri=provider_uri, checksum_public_address=checksum_address, federated_only=federated_only, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath) # Authenticated Configurations else: # Deserialize network domain name if override passed if network: domain_constant = getattr(constants, network.upper()) domains = {domain_constant} else: domains = None ursula_config = UrsulaConfiguration.from_configuration_file( filepath=config_file, domains=domains, registry_filepath=registry_filepath, provider_uri=provider_uri, rest_host=rest_host, rest_port=rest_port, db_filepath=db_filepath, # TODO: Handle Boolean overrides # poa=poa, # federated_only=federated_only, ) try: # Unlock Keyring if not quiet: click.secho('Decrypting keyring...', fg='blue') ursula_config.keyring.unlock(password=click_config.get_password() ) # Takes ~3 seconds, ~1GB Ram except CryptoError: raise ursula_config.keyring.AuthenticationFailed if not ursula_config.federated_only: try: ursula_config.connect_to_blockchain(recompile_contracts=False) ursula_config.connect_to_contracts() except EthereumContractRegistry.NoRegistry: message = "Cannot configure blockchain character: No contract registry found; " \ "Did you mean to pass --federated-only?" raise EthereumContractRegistry.NoRegistry(message) click_config.ursula_config = ursula_config # Pass Ursula's config onto staking sub-command # # Launch Warnings # if not quiet: if ursula_config.federated_only: click.secho("WARNING: Running in Federated mode", fg='yellow') # # Action Switch # if action == 'run': """Seed, Produce, Run!""" # # Seed - Step 1 # teacher_nodes = list() if teacher_uri: node = Ursula.from_teacher_uri( teacher_uri=teacher_uri, min_stake=min_stake, federated_only=ursula_config.federated_only) teacher_nodes.append(node) # # Produce - Step 2 # ursula = ursula_config(known_nodes=teacher_nodes, lonely=lonely) # GO! try: # # Run - Step 3 # click.secho("Connecting to {}".format(','.join( str(d) for d in ursula_config.domains)), fg='blue', bold=True) click.secho("Running Ursula {} on {}".format( ursula, ursula.rest_interface), fg='green', bold=True) if not debug: stdio.StandardIO(UrsulaCommandProtocol(ursula=ursula)) if dry_run: # That's all folks! return ursula.get_deployer().run() # <--- Blocking Call (Reactor) except Exception as e: ursula_config.log.critical(str(e)) click.secho("{} {}".format(e.__class__.__name__, str(e)), fg='red') raise # Crash :-( finally: if not quiet: click.secho("Stopping Ursula") ursula_config.cleanup() if not quiet: click.secho("Ursula Stopped", fg='red') return elif action == "save-metadata": """Manually save a node self-metadata file""" ursula = ursula_config.produce(ursula_config=ursula_config) metadata_path = ursula.write_node_metadata(node=ursula) if not quiet: click.secho("Successfully saved node metadata to {}.".format( metadata_path), fg='green') return elif action == "view": """Paint an existing configuration to the console""" paint_configuration( config_filepath=config_file or ursula_config.config_file_location) return elif action == "forget": """Forget all known nodes via storages""" click.confirm("Permanently delete all known node data?", abort=True) ursula_config.forget_nodes() message = "Removed all stored node node metadata and certificates" click.secho(message=message, fg='red') return else: raise click.BadArgumentUsage("No such argument {}".format(action))
def stake( click_config, action, config_root, config_file, # Mode force, offline, hw_wallet, # Blockchain poa, registry_filepath, provider_uri, sync, # Stake staking_address, worker_address, withdraw_address, value, duration, index, policy_reward, staking_reward, ) -> None: """ Manage stakes and other staker-related operations. \b Actions ------------------------------------------------- new-stakeholder Create a new stakeholder configuration list List active stakes for current stakeholder accounts Show ETH and NU balances for stakeholder's accounts sync Synchronize stake data with on-chain information set-worker Bound a worker to a staker detach-worker Detach worker currently bound to a staker init Create a new stake divide Create a new stake from part of an existing one collect-reward Withdraw staking reward """ # Banner emitter = click_config.emitter emitter.clear() emitter.banner(NU_BANNER) if action == 'new-stakeholder': if not provider_uri: raise click.BadOptionUsage( option_name='--provider', message="--provider is required to create a new stakeholder.") registry = None fetch_registry = True if registry_filepath: registry = EthereumContractRegistry( registry_filepath=registry_filepath) fetch_registry = False blockchain = BlockchainInterface(provider_uri=provider_uri, registry=registry, poa=poa) blockchain.connect(sync_now=sync, fetch_registry=fetch_registry) new_stakeholder = StakeHolder(config_root=config_root, offline_mode=offline, blockchain=blockchain) filepath = new_stakeholder.to_configuration_file(override=force) emitter.echo(f"Wrote new stakeholder configuration to {filepath}", color='green') return # Exit # # Make Staker # STAKEHOLDER = StakeHolder.from_configuration_file( filepath=config_file, provider_uri=provider_uri, registry_filepath=registry_filepath, offline=offline, sync_now=sync) # # Eager Actions # if action == 'list': if not STAKEHOLDER.stakes: emitter.echo(f"There are no active stakes") else: painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) return elif action == 'accounts': for address, balances in STAKEHOLDER.account_balances.items(): emitter.echo( f"{address} | {Web3.fromWei(balances['ETH'], 'ether')} ETH | {NU.from_nunits(balances['NU'])}" ) return # Exit elif action == 'sync': emitter.echo("Reading on-chain stake data...") STAKEHOLDER.read_onchain_stakes() STAKEHOLDER.to_configuration_file(override=True) emitter.echo("OK!", color='green') return # Exit elif action in ('set-worker', 'detach-worker'): if not staking_address: staking_address = select_stake(stakeholder=STAKEHOLDER, emitter=emitter).owner_address if action == 'set-worker': if not worker_address: worker_address = click.prompt("Enter worker address", type=EIP55_CHECKSUM_ADDRESS) elif action == 'detach-worker': if worker_address: raise click.BadOptionUsage( message= "detach-worker cannot be used together with --worker-address option" ) worker_address = BlockchainInterface.NULL_ADDRESS password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) receipt = STAKEHOLDER.set_worker(staker_address=staking_address, password=password, worker_address=worker_address) emitter.echo(f"OK | Receipt: {receipt['transactionHash'].hex()}", color='green') return # Exit elif action == 'init': """Initialize a new stake""" # # Get Staking Account # password = None if not staking_address: staking_address = select_client_account( blockchain=STAKEHOLDER.blockchain, prompt="Select staking account", emitter=emitter) if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = click.prompt( f"Enter password to unlock {staking_address}", hide_input=True, confirmation_prompt=False) # # Stage Stake # if not value: min_locked = STAKEHOLDER.economics.minimum_allowed_locked value = click.prompt( f"Enter stake value in NU", type=STAKE_VALUE, default=NU.from_nunits(min_locked).to_tokens()) value = NU.from_tokens(value) if not duration: prompt = f"Enter stake duration ({STAKEHOLDER.economics.minimum_locked_periods} periods minimum)" duration = click.prompt(prompt, type=STAKE_DURATION) start_period = STAKEHOLDER.staking_agent.get_current_period() end_period = start_period + duration # # Review # if not force: painting.paint_staged_stake(emitter=emitter, stakeholder=STAKEHOLDER, staking_address=staking_address, stake_value=value, duration=duration, start_period=start_period, end_period=end_period) confirm_staged_stake(staker_address=staking_address, value=value, duration=duration) # Last chance to bail click.confirm("Publish staged stake to the blockchain?", abort=True) # Execute new_stake = STAKEHOLDER.initialize_stake( amount=value, duration=duration, checksum_address=staking_address, password=password) painting.paint_staking_confirmation( emitter=emitter, ursula=STAKEHOLDER, transactions=new_stake.transactions) return # Exit elif action == 'divide': """Divide an existing stake by specifying the new target value and end period""" if staking_address and index is not None: staker = STAKEHOLDER.get_active_staker(address=staking_address) current_stake = staker.stakes[index] else: current_stake = select_stake(stakeholder=STAKEHOLDER, emitter=emitter) # # Stage Stake # # Value if not value: value = click.prompt( f"Enter target value (must be less than or equal to {str(current_stake.value)})", type=STAKE_VALUE) value = NU(value, 'NU') # Duration if not duration: extension = click.prompt("Enter number of periods to extend", type=STAKE_EXTENSION) else: extension = duration if not force: painting.paint_staged_stake_division(emitter=emitter, stakeholder=STAKEHOLDER, original_stake=current_stake, target_value=value, extension=extension) click.confirm("Is this correct?", abort=True) # Execute password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password( checksum_address=current_stake.owner_address) modified_stake, new_stake = STAKEHOLDER.divide_stake( address=current_stake.owner_address, index=current_stake.index, value=value, duration=extension, password=password) emitter.echo('Successfully divided stake', color='green', verbosity=1) emitter.echo(f'Receipt ........... {new_stake.receipt}', verbosity=1) # Show the resulting stake list painting.paint_stakes(emitter=emitter, stakes=STAKEHOLDER.stakes) return # Exit elif action == 'collect-reward': """Withdraw staking reward to the specified wallet address""" password = None if not hw_wallet and not STAKEHOLDER.blockchain.client.is_local: password = get_client_password(checksum_address=staking_address) STAKEHOLDER.collect_rewards(staker_address=staking_address, withdraw_address=withdraw_address, password=password, staking=staking_reward, policy=policy_reward) else: ctx = click.get_current_context() click.UsageError(message=f"Unknown action '{action}'.", ctx=ctx).show() return # Exit
def __init__(self, network_name: str = None, provider_uri: str = None, providers: list = None, autoconnect: bool = True, timeout: int = None, registry: EthereumContractRegistry = None, compiler: SolidityCompiler=None) -> None: """ A blockchain "network inerface"; The circumflex wraps entirely around the bounds of contract operations including compilation, deployment, and execution. Solidity Files -- SolidityCompiler --- --- HTTPProvider -- | | | | | -- External EVM (geth, etc.) | *BlockchainInterface* -- IPCProvider -- | | | | | | Registry File -- ContractRegistry -- | ---- TestProvider -- EthereumTester | | | | Pyevm (development chain) Blockchain | Agent ... (Contract API) | Character / Actor The circumflex is the junction of the solidity compiler, a contract registry, and a collection of web3 network __providers as a means of interfacing with the ethereum blockchain to execute or deploy contract code on the network. Compiler and Registry Usage ----------------------------- Contracts are freshly re-compiled if an instance of SolidityCompiler is passed; otherwise, The registry will read contract data saved to disk that is be used to retrieve contact address and op-codes. Optionally, A registry instance can be passed instead. Provider Usage --------------- https: // github.com / ethereum / eth - tester # available-backends * HTTP Provider - supply endpiont_uri * Websocket Provider - supply endpoint uri and websocket=True * IPC Provider - supply IPC path * Custom Provider - supply an iterable of web3.py provider instances """ self.__network = network_name if network_name is not None else self.__default_network self.timeout = timeout if timeout is not None else self.__default_timeout # # Providers # self.w3 = constants.NO_BLOCKCHAIN_CONNECTION self.__providers = providers if providers is not None else constants.NO_BLOCKCHAIN_CONNECTION if provider_uri and providers: raise self.InterfaceError("Pass a provider URI string, or a list of provider instances.") elif provider_uri: self.provider_uri = provider_uri self.add_provider(provider_uri=provider_uri) elif providers: self.provider_uri = constants.MANUAL_PROVIDERS_SET for provider in providers: self.add_provider(provider) else: # TODO: Emit a warning / log: No provider supplied for blockchain interface pass # if a SolidityCompiler class instance was passed, compile from solidity source code recompile = True if compiler is not None else False self.__recompile = recompile self.__sol_compiler = compiler # Setup the registry and base contract factory cache registry = registry if registry is not None else EthereumContractRegistry().from_config() self._registry = registry if self.__recompile is True: # Execute the compilation if we're recompiling, otherwise read compiled contract data from the registry interfaces = self.__sol_compiler.compile() self.__raw_contract_cache = interfaces # Auto-connect self.autoconnect = autoconnect if self.autoconnect is True: self.connect()
def deploy(click_config, action, poa, provider_uri, deployer_address, contract_name, allocation_infile, allocation_outfile, registry_infile, registry_outfile, no_compile, amount, recipient_address, config_root, force): """Manage contract and registry deployment""" # Ensure config root exists, because we need a default place to put outfiles. config_root = config_root or DEFAULT_CONFIG_ROOT if not os.path.exists(config_root): os.makedirs(config_root) # Establish a contract Registry registry, registry_filepath = None, (registry_outfile or registry_infile) if registry_filepath is not None: registry = EthereumContractRegistry( registry_filepath=registry_filepath) # Connect to Blockchain blockchain = Blockchain.connect(provider_uri=provider_uri, registry=registry, deployer=True, compile=not no_compile, poa=poa) # OK - Let's init a Deployment actor if not deployer_address: etherbase = blockchain.interface.w3.eth.accounts[0] deployer_address = etherbase # TODO: Make this required instead, perhaps interactive click.confirm( "Deployer Address is {} - Continue?".format(deployer_address), abort=True) deployer = Deployer(blockchain=blockchain, deployer_address=deployer_address) # The Big Three if action == "contracts": secrets = click_config.collect_deployment_secrets() # Track tx hashes, and new agents __deployment_transactions = dict() __deployment_agents = dict() if force: deployer.blockchain.interface.registry._destroy() try: txhashes, agents = deployer.deploy_network_contracts( miner_secret=bytes(secrets.miner_secret, encoding='utf-8'), policy_secret=bytes(secrets.policy_secret, encoding='utf-8'), adjudicator_secret=bytes(secrets.mining_adjudicator_secret, encoding='utf-8')) except BlockchainInterface.InterfaceError: raise # TODO: Handle registry management here (contract may already exist) else: __deployment_transactions.update(txhashes) # User Escrow Proxy deployer.deploy_escrow_proxy( secret=bytes(secrets.escrow_proxy_secret, encoding='utf-8')) click.secho("Deployed!", fg='green', bold=True) # # Deploy Single Contract # if contract_name: try: deployer_func = deployer.deployers[contract_name] except KeyError: message = "No such contract {}. Available contracts are {}".format( contract_name, deployer.deployers.keys()) click.secho(message, fg='red', bold=True) raise click.Abort() else: _txs, _agent = deployer_func() registry_outfile = deployer.blockchain.interface.registry.filepath click.secho( '\nDeployment Transaction Hashes for {}'.format(registry_outfile), bold=True, fg='blue') for contract_name, transactions in __deployment_transactions.items(): heading = '\n{} ({})'.format( contract_name, agents[contract_name].contract_address) click.secho(heading, bold=True) click.echo('*' * (42 + 3 + len(contract_name))) total_gas_used = 0 for tx_name, txhash in transactions.items(): receipt = deployer.blockchain.wait_for_receipt(txhash=txhash) total_gas_used += int(receipt['gasUsed']) if receipt['status'] == 1: click.secho("OK", fg='green', nl=False, bold=True) else: click.secho("Failed", fg='red', nl=False, bold=True) click.secho(" | {}".format(tx_name), fg='yellow', nl=False) click.secho(" | {}".format(txhash.hex()), fg='yellow', nl=False) click.secho(" ({} gas)".format(receipt['cumulativeGasUsed'])) click.secho("Block #{} | {}\n".format( receipt['blockNumber'], receipt['blockHash'].hex())) click.secho( "Cumulative Gas Consumption: {} gas\n".format(total_gas_used), bold=True, fg='blue') elif action == "allocations": if not allocation_infile: allocation_infile = click.prompt("Enter allocation data filepath") click.confirm("Continue deploying and allocating?", abort=True) deployer.deploy_beneficiaries_from_file( allocation_data_filepath=allocation_infile, allocation_outfile=allocation_outfile) elif action == "transfer": token_agent = NucypherTokenAgent(blockchain=blockchain) click.confirm( f"Transfer {amount} from {token_agent.contract_address} to {recipient_address}?", abort=True) txhash = token_agent.transfer( amount=amount, sender_address=token_agent.contract_address, target_address=recipient_address) click.secho(f"OK | {txhash}") return elif action == "destroy-registry": registry_filepath = deployer.blockchain.interface.registry.filepath click.confirm( f"Are you absolutely sure you want to destroy the contract registry at {registry_filepath}?", abort=True) os.remove(registry_filepath) click.secho(f"Successfully destroyed {registry_filepath}", fg='red') else: raise click.BadArgumentUsage(message=f"Unknown action '{action}'")
def deploy(action, poa, etherscan, provider_uri, gas, deployer_address, contract_name, allocation_infile, allocation_outfile, registry_infile, registry_outfile, amount, recipient_address, config_root, hw_wallet, force, dev): """ Manage contract and registry deployment. \b Actions ----------------------------------------------------------------------------- contracts Compile and deploy contracts. allocations Deploy pre-allocation contracts. upgrade Upgrade NuCypher existing proxy contract deployments. rollback Rollback a proxy contract's target. status Echo owner information and bare contract metadata. transfer-tokens Transfer tokens to another address. transfer-ownership Transfer ownership of contracts to another address. """ emitter = StdoutEmitter() # # Validate # # Ensure config root exists, because we need a default place to put output files. config_root = config_root or DEFAULT_CONFIG_ROOT if not os.path.exists(config_root): os.makedirs(config_root) # # Pre-Launch Warnings # if not hw_wallet: emitter.echo("WARNING: --no-hw-wallet is enabled.", color='yellow') if etherscan: emitter.echo( "WARNING: --etherscan is enabled. " "A browser tab will be opened with deployed contracts and TXs as provided by Etherscan.", color='yellow') else: emitter.echo( "WARNING: --etherscan is disabled. " "If you want to see deployed contracts and TXs in your browser, activate --etherscan.", color='yellow') # # Connect to Registry # # Establish a contract registry from disk if specified registry_filepath = registry_outfile or registry_infile if dev: # TODO: Need a way to detect a geth--dev registry filepath here. (then deprecate the --dev flag) registry_filepath = os.path.join(DEFAULT_CONFIG_ROOT, 'dev_contract_registry.json') registry = EthereumContractRegistry(registry_filepath=registry_filepath) emitter.echo(f"Using contract registry filepath {registry.filepath}") # # Connect to Blockchain # blockchain = BlockchainDeployerInterface(provider_uri=provider_uri, poa=poa, registry=registry) try: blockchain.connect(fetch_registry=False, sync_now=False) except BlockchainDeployerInterface.ConnectionFailed as e: emitter.echo(str(e), color='red', bold=True) raise click.Abort() # # Make Authenticated Deployment Actor # # Verify Address & collect password if not deployer_address: prompt = "Select deployer account" deployer_address = select_client_account(emitter=emitter, blockchain=blockchain, prompt=prompt) if not force: click.confirm("Selected {} - Continue?".format(deployer_address), abort=True) password = None if not hw_wallet and not blockchain.client.is_local: password = get_client_password(checksum_address=deployer_address) # Produce Actor DEPLOYER = DeployerActor(blockchain=blockchain, client_password=password, deployer_address=deployer_address) # Verify ETH Balance emitter.echo(f"\n\nDeployer ETH balance: {DEPLOYER.eth_balance}") if DEPLOYER.eth_balance == 0: emitter.echo("Deployer address has no ETH.", color='red', bold=True) raise click.Abort() # # Action switch # if action == 'upgrade': if not contract_name: raise click.BadArgumentUsage( message="--contract-name is required when using --upgrade") existing_secret = click.prompt( 'Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) DEPLOYER.upgrade_contract(contract_name=contract_name, existing_plaintext_secret=existing_secret, new_plaintext_secret=new_secret) return # Exit elif action == 'rollback': if not contract_name: raise click.BadArgumentUsage( message="--contract-name is required when using --rollback") existing_secret = click.prompt( 'Enter existing contract upgrade secret', hide_input=True) new_secret = click.prompt('Enter new contract upgrade secret', hide_input=True, confirmation_prompt=True) DEPLOYER.rollback_contract(contract_name=contract_name, existing_plaintext_secret=existing_secret, new_plaintext_secret=new_secret) return # Exit elif action == "contracts": # # Deploy Single Contract (Amend Registry) # if contract_name: try: contract_deployer = DEPLOYER.deployers[contract_name] except KeyError: message = f"No such contract {contract_name}. Available contracts are {DEPLOYER.deployers.keys()}" emitter.echo(message, color='red', bold=True) raise click.Abort() else: emitter.echo(f"Deploying {contract_name}") if contract_deployer._upgradeable: secret = DEPLOYER.collect_deployment_secret( deployer=contract_deployer) receipts, agent = DEPLOYER.deploy_contract( contract_name=contract_name, plaintext_secret=secret) else: receipts, agent = DEPLOYER.deploy_contract( contract_name=contract_name, gas_limit=gas) paint_contract_deployment( contract_name=contract_name, contract_address=agent.contract_address, receipts=receipts, emitter=emitter, chain_name=blockchain.client.chain_name, open_in_browser=etherscan) return # Exit # # Deploy Automated Series (Create Registry) # # Confirm filesystem registry writes. registry_filepath = DEPLOYER.blockchain.registry.filepath if os.path.isfile(registry_filepath): emitter.echo( f"\nThere is an existing contract registry at {registry_filepath}.\n" f"Did you mean 'nucypher-deploy upgrade'?\n", color='yellow') click.confirm("*DESTROY* existing local registry and continue?", abort=True) os.remove(registry_filepath) # Stage Deployment secrets = DEPLOYER.collect_deployment_secrets() paint_staged_deployment(deployer=DEPLOYER, emitter=emitter) # Confirm Trigger Deployment if not actions.confirm_deployment(emitter=emitter, deployer=DEPLOYER): raise click.Abort() # Delay - Last chance to abort via KeyboardInterrupt paint_deployment_delay(emitter=emitter) # Execute Deployment deployment_receipts = DEPLOYER.deploy_network_contracts( secrets=secrets, emitter=emitter, interactive=not force, etherscan=etherscan) # Paint outfile paths registry_outfile = DEPLOYER.blockchain.registry.filepath emitter.echo('Generated registry {}'.format(registry_outfile), bold=True, color='blue') # Save transaction metadata receipts_filepath = DEPLOYER.save_deployment_receipts( receipts=deployment_receipts) emitter.echo(f"Saved deployment receipts to {receipts_filepath}", color='blue', bold=True) return # Exit elif action == "allocations": if not allocation_infile: allocation_infile = click.prompt("Enter allocation data filepath") click.confirm("Continue deploying and allocating?", abort=True) DEPLOYER.deploy_beneficiaries_from_file( allocation_data_filepath=allocation_infile, allocation_outfile=allocation_outfile) return # Exit elif action == "transfer": token_agent = NucypherTokenAgent(blockchain=blockchain) missing_options = list() if recipient_address is None: missing_options.append("--recipient-address") if amount is None: missing_options.append("--amount") if missing_options: raise click.BadOptionUsage( f"Need {' and '.join(missing_options)} to transfer tokens.") click.confirm( f"Transfer {amount} from {deployer_address} to {recipient_address}?", abort=True) receipt = token_agent.transfer(amount=amount, sender_address=deployer_address, target_address=recipient_address) emitter.echo(f"OK | Receipt: {receipt['transactionHash'].hex()}") return # Exit else: raise click.BadArgumentUsage(message=f"Unknown action '{action}'")
def test_upgrade_contracts(click_runner): # # Setup # # Connect to the blockchain with a blank temporary file-based registry mock_temporary_registry = EthereumContractRegistry(registry_filepath=MOCK_REGISTRY_FILEPATH) blockchain = Blockchain.connect(registry=mock_temporary_registry) # Check the existing state of the registry before the meat and potatoes expected_registrations = 9 with open(MOCK_REGISTRY_FILEPATH, 'r') as file: raw_registry_data = file.read() registry_data = json.loads(raw_registry_data) assert len(registry_data) == expected_registrations # # Input Components # cli_action = 'upgrade' base_command = ('--registry-infile', MOCK_REGISTRY_FILEPATH, '--provider-uri', TEST_PROVIDER_URI, '--poa') # Generate user inputs yes = 'Y\n' # :-) upgrade_inputs = dict() for version, insecure_secret in INSECURE_SECRETS.items(): next_version = version + 1 old_secret = INSECURE_SECRETS[version] try: new_secret = INSECURE_SECRETS[next_version] except KeyError: continue # addr-----secret----new deploy secret (2x for confirmation) user_input = '0\n' + yes + old_secret + (new_secret * 2) upgrade_inputs[next_version] = user_input # # Stage Upgrades # contracts_to_upgrade = ('MinersEscrow', # v1 -> v2 'PolicyManager', # v1 -> v2 'MiningAdjudicator', # v1 -> v2 'UserEscrowProxy', # v1 -> v2 'MinersEscrow', # v2 -> v3 'MinersEscrow', # v3 -> v4 'MiningAdjudicator', # v2 -> v3 'PolicyManager', # v2 -> v3 'UserEscrowProxy', # v2 -> v3 'UserEscrowProxy', # v3 -> v4 'PolicyManager', # v3 -> v4 'MiningAdjudicator', # v3 -> v4 ) # NOTE: Keep all versions the same in this test (all version 4, for example) # Each contract starts at version 1 version_tracker = {name: 1 for name in contracts_to_upgrade} # # Upgrade Contracts # for contract_name in contracts_to_upgrade: # Assemble CLI command command = (cli_action, '--contract-name', contract_name, *base_command) # Select upgrade interactive input scenario current_version = version_tracker[contract_name] new_version = current_version + 1 user_input = upgrade_inputs[new_version] # Execute upgrade (Meat) result = click_runner.invoke(deploy, command, input=user_input, catch_exceptions=False) assert result.exit_code == 0 # TODO: Console painting # Mutate the version tracking version_tracker[contract_name] += 1 expected_registrations += 1 # Verify the registry is updated (Potatoes) with open(MOCK_REGISTRY_FILEPATH, 'r') as file: # Read the registry file directly, bypassing its interfaces raw_registry_data = file.read() registry_data = json.loads(raw_registry_data) assert len(registry_data) == expected_registrations # Check that there is more than one entry, since we've deployed a "version 2" expected_enrollments = current_version + 1 registered_names = [r[0] for r in registry_data] enrollments = registered_names.count(contract_name) assert enrollments > 1, f"New contract is not enrolled in {MOCK_REGISTRY_FILEPATH}" assert enrollments == expected_enrollments, f"Incorrect number of records enrolled for {contract_name}. " \ f"Expected {expected_enrollments} got {enrollments}." # Ensure deployments are different addresses records = blockchain.interface.registry.search(contract_name=contract_name) assert len(records) == expected_enrollments old, new = records[-2:] # Get the last two entries old_name, old_address, *abi = old # Previous version new_name, new_address, *abi = new # New version assert old_name == new_name # TODO: Inspect ABI? assert old_address != new_address # Select proxy (Dispatcher vs Linker) if contract_name == "UserEscrowProxy": proxy_name = "UserEscrowLibraryLinker" else: proxy_name = 'Dispatcher' # Ensure the proxy targets the new deployment proxy = blockchain.interface.get_proxy(target_address=new_address, proxy_name=proxy_name) targeted_address = proxy.functions.target().call() assert targeted_address != old_address assert targeted_address == new_address