Support for complete node reinitialization/relocation using an existing mnemonic

pull/3554/head
KPrasch 2024-08-13 21:26:42 +02:00 committed by derekpierre
parent e4694c9ea1
commit 1e08388565
No known key found for this signature in database
4 changed files with 67 additions and 27 deletions

View File

@ -11,7 +11,6 @@ from nucypher.cli.literature import (
GENERIC_PASSWORD_PROMPT,
PASSWORD_COLLECTION_NOTICE,
)
from nucypher.config.base import CharacterConfiguration
from nucypher.config.constants import NUCYPHER_ENVVAR_KEYSTORE_PASSWORD
from nucypher.crypto.keystore import _WORD_COUNT, Keystore
from nucypher.utilities.emitters import StdoutEmitter
@ -36,7 +35,7 @@ def get_client_password(checksum_address: str, envvar: str = None, confirm: bool
return client_password
def unlock_signer_account(config: CharacterConfiguration, json_ipc: bool) -> None:
def unlock_signer_account(config, json_ipc: bool) -> None:
# TODO: Remove this block after deprecating 'operator_address'
from nucypher.config.characters import UrsulaConfiguration
@ -67,7 +66,11 @@ def get_nucypher_password(emitter, confirm: bool = False, envvar=NUCYPHER_ENVVAR
return keystore_password
def unlock_nucypher_keystore(emitter: StdoutEmitter, password: str, character_configuration: CharacterConfiguration) -> bool:
def unlock_nucypher_keystore(
emitter: StdoutEmitter,
password: str,
character_configuration,
) -> bool:
"""Unlocks a nucypher keystore and attaches it to the supplied configuration if successful."""
emitter.message(DECRYPTING_CHARACTER_KEYSTORE.format(name=character_configuration.NAME.capitalize()), color='yellow')

View File

@ -164,7 +164,7 @@ class UrsulaConfigOptions:
# TODO: Exit codes (not only for this, but for other exceptions)
return click.get_current_context().exit(1)
def generate_config(self, emitter, config_root, force, key_material):
def generate_config(self, emitter, config_root, force, key_material, with_mnemonic):
if self.dev:
raise RuntimeError(
@ -193,6 +193,7 @@ class UrsulaConfigOptions:
return UrsulaConfiguration.generate(
password=get_nucypher_password(emitter=emitter, confirm=True),
key_material=bytes.fromhex(key_material) if key_material else None,
with_mnemonic=with_mnemonic,
config_root=config_root,
rest_host=self.rest_host,
rest_port=self.rest_port,
@ -323,7 +324,14 @@ def ursula():
@option_config_root
@group_general_config
@option_key_material
def init(general_config, config_options, force, config_root, key_material):
@click.option(
"--with-mnemonic",
help="Initialize with a mnemonic phrase instead of generating a new keypair from scratch",
is_flag=True,
)
def init(
general_config, config_options, force, config_root, key_material, with_mnemonic
):
"""Create a new Ursula node configuration."""
emitter = setup_emitter(general_config, config_options.operator_address)
_pre_launch_warnings(emitter, dev=None, force=force)
@ -350,15 +358,24 @@ def init(general_config, config_options, force, config_root, key_material):
return click.get_current_context().exit(1)
click.clear()
emitter.echo(
"Hello Operator, welcome on board :-) \n\n"
"NOTE: Initializing a new Ursula node configuration is a one-time operation\n"
"for the lifetime of your node. This is a two-step process:\n\n"
"1. Creating a password to encrypt your operator keys\n"
"2. Securing a taco node seed phase\n\n"
"Please follow the prompts.",
color="cyan",
)
if with_mnemonic:
emitter.echo(
"Hello Operator, welcome back :-) \n\n"
"You are about to initialize a new Ursula node configuration using an existing mnemonic phrase.\n"
"Have your mnemonic phrase ready and ensure you are in a secure environment.\n"
"Please follow the prompts.",
color="cyan",
)
else:
emitter.echo(
"Hello Operator, welcome on board :-) \n\n"
"NOTE: Initializing a new Ursula node configuration is a one-time operation\n"
"for the lifetime of your node. This is a two-step process:\n\n"
"1. Creating a password to encrypt your operator keys\n"
"2. Securing a taco node seed phase\n\n"
"Please follow the prompts.",
color="cyan",
)
if not config_options.eth_endpoint:
raise click.BadOptionUsage(
@ -381,7 +398,11 @@ def init(general_config, config_options, force, config_root, key_material):
message="Select TACo Domain",
)
ursula_config = config_options.generate_config(
emitter=emitter, config_root=config_root, force=force, key_material=key_material
emitter=emitter,
config_root=config_root,
force=force,
key_material=key_material,
with_mnemonic=with_mnemonic,
)
filepath = ursula_config.to_configuration_file()
paint_new_installation_help(
@ -460,7 +481,6 @@ def audit(config_file, keystore_filepath, view_mnemonic):
required=False,
)
def recover(config_file, keystore_filepath):
# TODO: Combine with work in PR #2682
emitter = StdoutEmitter()
if keystore_filepath and config_file:
raise click.BadOptionUsage(
@ -510,7 +530,7 @@ def destroy(general_config, config_options, config_file, force):
is_flag=True,
)
def public_keys(config_file, keystore_filepath, from_mnemonic):
"""Display the public key of a keystore."""
"""Display the public keys of a keystore."""
emitter = StdoutEmitter()
if sum(1 for i in (keystore_filepath, config_file, from_mnemonic) if i) > 1:

View File

@ -48,9 +48,7 @@ Path to Logs: {USER_LOG_DIR}
- Never share secret keys with anyone!
- Backup your keystore! Character keys are required to interact with the protocol!
- Remember your password! Without the password, it's impossible to decrypt the key!
"""
- Remember your password! Without the password, it's impossible to decrypt the key!"""
)
if character_name == 'ursula':

View File

@ -24,6 +24,7 @@ from nucypher.blockchain.eth.registry import (
)
from nucypher.blockchain.eth.signers import Signer
from nucypher.characters.lawful import Ursula
from nucypher.cli.actions.auth import collect_mnemonic
from nucypher.config import constants
from nucypher.config.util import cast_paths_from
from nucypher.crypto.keystore import Keystore
@ -583,18 +584,23 @@ class CharacterConfiguration(BaseConfiguration):
Warning: This method allows mutation and may result in an inconsistent configuration.
"""
# config file should exist and we we override -> no need for modifier
# config file should exist and we override -> no need for modifier
return super().update(filepath=self.config_file_location, **kwargs)
@classmethod
def generate(
cls, password: str, key_material: Optional[bytes] = None, *args, **kwargs
cls,
password: str,
key_material: Optional[bytes] = None,
with_mnemonic: bool = False,
*args,
**kwargs,
):
"""
Generates local directories, private keys, and initial configuration for a new node.
"""
"""Generates local directories, private keys, and initial configuration for a new node."""
node_config = cls(dev_mode=False, *args, **kwargs)
node_config.initialize(key_material=key_material, password=password)
node_config.initialize(
key_material=key_material, with_mnemonic=with_mnemonic, password=password
)
node_config.keystore.unlock(password)
return node_config
@ -789,7 +795,12 @@ class CharacterConfiguration(BaseConfiguration):
power_ups.append(power_up)
return power_ups
def initialize(self, password: str, key_material: Optional[bytes] = None) -> Path:
def initialize(
self,
password: str,
key_material: Optional[bytes] = None,
with_mnemonic: bool = False,
) -> Path:
"""Initialize a new configuration and write installation files to disk."""
# Development
@ -806,6 +817,7 @@ class CharacterConfiguration(BaseConfiguration):
key_material=key_material,
password=password,
interactive=self.MNEMONIC_KEYSTORE,
with_mnemonic=with_mnemonic,
)
self._cache_runtime_filepaths()
@ -824,6 +836,7 @@ class CharacterConfiguration(BaseConfiguration):
password: str,
key_material: Optional[bytes] = None,
interactive: bool = True,
with_mnemonic: bool = False,
) -> Keystore:
if key_material:
self.__keystore = Keystore.import_secure(
@ -831,6 +844,12 @@ class CharacterConfiguration(BaseConfiguration):
password=password,
keystore_dir=self.keystore_dir,
)
elif with_mnemonic:
self.__keystore = Keystore.restore(
password=password,
keystore_dir=self.keystore_dir,
words=collect_mnemonic(self.emitter),
)
else:
if interactive:
self.__keystore = Keystore.generate(