Skip to content

Commit

Permalink
Merge pull request eth-educators#4 from valefar-on-discord/exit-messa…
Browse files Browse the repository at this point in the history
…ge-generation

Add ability to generate exit message
  • Loading branch information
remyroy committed May 1, 2024
2 parents 0315d9a + a0f124d commit 36a86b9
Show file tree
Hide file tree
Showing 28 changed files with 1,041 additions and 125 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
validator_keys
bls_to_execution_changes
exit_transactions
validator_keys

# Python testing & linting:
build/
Expand Down
192 changes: 114 additions & 78 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion staking_deposit/cli/existing_mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def load_mnemonic_arguments_decorator(function: Callable[..., Any]) -> Callable[
default='',
help=lambda: load_text(['arg_mnemonic_password', 'help'], func='existing_mnemonic'),
hidden=True,
param_decls='--mnemonic-password',
param_decls='--mnemonic_password',
prompt=False,
),
]
Expand Down
126 changes: 126 additions & 0 deletions staking_deposit/cli/exit_transaction_keystore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import click
import os

from typing import Any
from staking_deposit.exit_transaction import exit_transaction_generation, export_exit_transaction_json
from staking_deposit.key_handling.keystore import Keystore
from staking_deposit.settings import (
MAINNET,
NON_PRATER_CHAIN_KEYS,
get_chain_setting,
)
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.utils.validation import validate_int_range, validate_keystore_file, verify_signed_exit_json


FUNC_NAME = 'exit_transaction_keystore'


@click.command(
help=load_text(['arg_exit_transaction_keystore', 'help'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, NON_PRATER_CHAIN_KEYS),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
NON_PRATER_CHAIN_KEYS
),
),
default=MAINNET,
help=lambda: load_text(['arg_exit_transaction_keystore_chain', 'help'], func=FUNC_NAME),
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
NON_PRATER_CHAIN_KEYS
),
)
@jit_option(
callback=captive_prompt_callback(
lambda file: validate_keystore_file(file),
lambda: load_text(['arg_exit_transaction_keystore_keystore', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_exit_transaction_keystore_keystore', 'help'], func=FUNC_NAME),
param_decls='--keystore',
prompt=lambda: load_text(['arg_exit_transaction_keystore_keystore', 'prompt'], func=FUNC_NAME),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: x,
lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'prompt'], func=FUNC_NAME),
None,
lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'invalid'], func=FUNC_NAME),
True,
),
help=lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'help'], func=FUNC_NAME),
hide_input=True,
param_decls='--keystore_password',
prompt=lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
lambda: load_text(['arg_validator_index', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_validator_index', 'help'], func=FUNC_NAME),
param_decls='--validator_index',
prompt=lambda: load_text(['arg_validator_index', 'prompt'], func=FUNC_NAME),
)
@jit_option(
default=0,
help=lambda: load_text(['arg_exit_transaction_keystore_epoch', 'help'], func=FUNC_NAME),
param_decls='--epoch',
)
@jit_option(
default=os.getcwd(),
help=lambda: load_text(['arg_exit_transaction_keystore_output_folder', 'help'], func=FUNC_NAME),
param_decls='--output_folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.pass_context
def exit_transaction_keystore(
ctx: click.Context,
chain: str,
keystore: Keystore,
keystore_password: str,
validator_index: int,
epoch: int,
output_folder: str,
**kwargs: Any) -> None:
try:
secret_bytes = keystore.decrypt(keystore_password)
except ValueError:
click.echo(load_text(['arg_exit_transaction_keystore_keystore_password', 'mismatch']))
exit(1)

signing_key = int.from_bytes(secret_bytes, 'big')
chain_settings = get_chain_setting(chain)

signed_exit = exit_transaction_generation(
chain_settings=chain_settings,
signing_key=signing_key,
validator_index=validator_index,
epoch=epoch,
)

folder = os.path.join(output_folder, DEFAULT_EXIT_TRANSACTION_FOLDER_NAME)

click.echo(load_text(['msg_exit_transaction_creation']))
saved_folder = export_exit_transaction_json(folder=folder, signed_exit=signed_exit)

click.echo(load_text(['msg_verify_exit_transaction']))
if (not verify_signed_exit_json(saved_folder, keystore.pubkey, chain_settings)):
click.echo(['err_verify_exit_transaction'])

click.echo(load_text(['msg_creation_success']) + saved_folder)
click.pause(load_text(['msg_pause']))
132 changes: 132 additions & 0 deletions staking_deposit/cli/exit_transaction_mnemonic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import click
import os

from typing import Any, Sequence
from staking_deposit.cli.existing_mnemonic import load_mnemonic_arguments_decorator
from staking_deposit.credentials import Credential
from staking_deposit.exceptions import ValidationError
from staking_deposit.settings import (
MAINNET,
NON_PRATER_CHAIN_KEYS,
get_chain_setting,
)
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.utils.validation import validate_int_range, validate_validator_indices, verify_signed_exit_json


FUNC_NAME = 'exit_transaction_mnemonic'


@click.command(
help=load_text(['arg_exit_transaction_mnemonic', 'help'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, NON_PRATER_CHAIN_KEYS),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
NON_PRATER_CHAIN_KEYS
),
),
default=MAINNET,
help=lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'help'], func=FUNC_NAME),
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
NON_PRATER_CHAIN_KEYS
),
)
@load_mnemonic_arguments_decorator
@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'prompt'], func=FUNC_NAME),
),
default=0,
help=lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'help'], func=FUNC_NAME),
param_decls="--validator_start_index",
prompt=lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda validator_indices: validate_validator_indices(validator_indices),
lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'help'], func=FUNC_NAME),
param_decls='--validator_indices',
prompt=lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'prompt'], func=FUNC_NAME),
)
@jit_option(
default=0,
help=lambda: load_text(['arg_exit_transaction_mnemonic_epoch', 'help'], func=FUNC_NAME),
param_decls='--epoch',
)
@jit_option(
default=os.getcwd(),
help=lambda: load_text(['arg_exit_transaction_mnemonic_output_folder', 'help'], func=FUNC_NAME),
param_decls='--output_folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.pass_context
def exit_transaction_mnemonic(
ctx: click.Context,
chain: str,
mnemonic: str,
mnemonic_password: str,
validator_start_index: int,
validator_indices: Sequence[int],
epoch: int,
output_folder: str,
**kwargs: Any) -> None:

folder = os.path.join(output_folder, DEFAULT_EXIT_TRANSACTION_FOLDER_NAME)
chain_settings = get_chain_setting(chain)
num_keys = len(validator_indices)
key_indices = range(validator_start_index, validator_start_index + num_keys)

# We are not using CredentialList because from_mnemonic assumes key generation flow
credentials = [
Credential(
mnemonic=mnemonic,
mnemonic_password=mnemonic_password,
index=key_index,
amount=0, # Unneeded for this purpose
chain_setting=chain_settings,
hex_eth1_withdrawal_address=None
) for key_index in key_indices
]

with click.progressbar(zip(credentials, validator_indices),
label=load_text(['msg_exit_transaction_creation']),
show_percent=False,
length=num_keys,
show_pos=True) as items:
transaction_filefolders = [
credential.save_exit_transaction(validator_index=validator_index, epoch=epoch, folder=folder)
for credential, validator_index in items
]

with click.progressbar(zip(transaction_filefolders, credentials),
label=load_text(['msg_verify_exit_transaction']),
show_percent=False,
length=num_keys,
show_pos=True) as items:
if not all(
verify_signed_exit_json(file_folder=file,
pubkey=credential.signing_pk.hex(),
chain_settings=credential.chain_setting)
for file, credential in items
):
raise ValidationError(load_text(['err_verify_exit_transactions']))

click.echo(load_text(['msg_creation_success']) + folder)
click.pause(load_text(['msg_pause']))
11 changes: 5 additions & 6 deletions staking_deposit/cli/generate_bls_to_execution_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,8 @@
load_text,
)
from staking_deposit.settings import (
ALL_CHAINS,
MAINNET,
PRATER,
NON_PRATER_CHAIN_KEYS,
get_chain_setting,
get_devnet_chain_setting,
)
Expand All @@ -63,19 +62,18 @@ def get_password(text: str) -> str:
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, list(ALL_CHAINS.keys())),
lambda x: closest_match(x, NON_PRATER_CHAIN_KEYS),
choice_prompt_func(
lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.keys())
NON_PRATER_CHAIN_KEYS
),
),
default=MAINNET,
help=lambda: load_text(['arg_chain', 'help'], func=FUNC_NAME),
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
# Since `prater` is alias of `goerli`, do not show `prater` in the prompt message.
list(key for key in ALL_CHAINS.keys() if key != PRATER)
NON_PRATER_CHAIN_KEYS
),
)
@load_mnemonic_arguments_decorator
Expand Down Expand Up @@ -155,6 +153,7 @@ def generate_bls_to_execution_change(
chain_setting = get_devnet_chain_setting(
network_name=devnet_chain_setting_dict['network_name'],
genesis_fork_version=devnet_chain_setting_dict['genesis_fork_version'],
exit_fork_version=devnet_chain_setting_dict['exit_fork_version'],
genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'],
)

Expand Down
10 changes: 4 additions & 6 deletions staking_deposit/cli/generate_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,8 @@
load_text,
)
from staking_deposit.settings import (
ALL_CHAINS,
MAINNET,
PRATER,
NON_PRATER_CHAIN_KEYS,
get_chain_setting,
)

Expand Down Expand Up @@ -65,10 +64,10 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
),
jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, list(ALL_CHAINS.keys())),
lambda x: closest_match(x, NON_PRATER_CHAIN_KEYS),
choice_prompt_func(
lambda: load_text(['chain', 'prompt'], func='generate_keys_arguments_decorator'),
list(ALL_CHAINS.keys())
NON_PRATER_CHAIN_KEYS
),
default=MAINNET,
),
Expand All @@ -77,8 +76,7 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['chain', 'prompt'], func='generate_keys_arguments_decorator'),
# Since `prater` is alias of `goerli`, do not show `prater` in the prompt message.
list(key for key in ALL_CHAINS.keys() if key != PRATER)
NON_PRATER_CHAIN_KEYS
),
),
jit_option(
Expand Down
13 changes: 13 additions & 0 deletions staking_deposit/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from py_ecc.bls import G2ProofOfPossession as bls

from staking_deposit.exceptions import ValidationError
from staking_deposit.exit_transaction import exit_transaction_generation, export_exit_transaction_json
from staking_deposit.key_handling.key_derivation.path import mnemonic_and_path_to_key
from staking_deposit.key_handling.keystore import (
Keystore,
Expand Down Expand Up @@ -210,6 +211,18 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by
result_dict.update({'metadata': metadata})
return result_dict

def save_exit_transaction(self, validator_index: int, epoch: int, folder: str) -> str:
signing_key = self.signing_sk

signed_voluntary_exit = exit_transaction_generation(
chain_settings=self.chain_setting,
signing_key=signing_key,
validator_index=validator_index,
epoch=epoch
)

return export_exit_transaction_json(folder=folder, signed_exit=signed_voluntary_exit)


class CredentialList:
"""
Expand Down
4 changes: 4 additions & 0 deletions staking_deposit/deposit.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import sys

from staking_deposit.cli.existing_mnemonic import existing_mnemonic
from staking_deposit.cli.exit_transaction_keystore import exit_transaction_keystore
from staking_deposit.cli.exit_transaction_mnemonic import exit_transaction_mnemonic
from staking_deposit.cli.generate_bls_to_execution_change import generate_bls_to_execution_change
from staking_deposit.cli.new_mnemonic import new_mnemonic
from staking_deposit.utils.click import (
Expand Down Expand Up @@ -56,6 +58,8 @@ def cli(ctx: click.Context, language: str, non_interactive: bool) -> None:
cli.add_command(existing_mnemonic)
cli.add_command(new_mnemonic)
cli.add_command(generate_bls_to_execution_change)
cli.add_command(exit_transaction_keystore)
cli.add_command(exit_transaction_mnemonic)


def run() -> None:
Expand Down
Loading

0 comments on commit 36a86b9

Please sign in to comment.