Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to generate exit message #4

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
remyroy marked this conversation as resolved.
Show resolved Hide resolved
prompt=False,
),
]
Expand Down
117 changes: 117 additions & 0 deletions staking_deposit/cli/exit_transaction_keystore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
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 ALL_CHAINS, MAINNET, PRATER, get_chain_setting
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.utils.validation import validate_int_range


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, list(ALL_CHAINS.keys())),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.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),
# 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)
),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: x,
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: str,
keystore_password: str,
validator_index: int,
epoch: int,
output_folder: str,
**kwargs: Any) -> None:
saved_keystore = Keystore.from_file(keystore)
remyroy marked this conversation as resolved.
Show resolved Hide resolved

try:
secret_bytes = saved_keystore.decrypt(keystore_password)
except ValueError:
click.echo(load_text(['arg_exit_transaction_keystore_keystore_password', 'mismatch']))
remyroy marked this conversation as resolved.
Show resolved Hide resolved
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,
)

saved_folder = export_exit_transaction_json(folder=output_folder, signed_exit=signed_exit)
remyroy marked this conversation as resolved.
Show resolved Hide resolved

click.echo(load_text(['msg_creation_success']) + saved_folder)
click.pause(load_text(['msg_pause']))
115 changes: 115 additions & 0 deletions staking_deposit/cli/exit_transaction_mnemonic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
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.exit_transaction import exit_transaction_generation, export_exit_transaction_json
from staking_deposit.settings import ALL_CHAINS, MAINNET, PRATER, get_chain_setting
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.utils.validation import validate_int_range, validate_validator_indices


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, list(ALL_CHAINS.keys())),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.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),
# 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)
),
)
@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:

chain_settings = get_chain_setting(chain)
num_keys = len(validator_indices)
key_indices = range(validator_start_index, validator_start_index + num_keys)

click.echo(load_text(['msg_creation_start']))
# We assume that the list of validator indices are in order and increment the start index
for key_index, validator_index in zip(key_indices, validator_indices):
credential = Credential(
remyroy marked this conversation as resolved.
Show resolved Hide resolved
mnemonic=mnemonic,
mnemonic_password=mnemonic_password,
index=key_index,
amount=0, # Unneeded for this purpose
chain_setting=chain_settings,
hex_eth1_withdrawal_address=None
)

signing_key = credential.signing_sk

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

saved_folder = export_exit_transaction_json(folder=output_folder, signed_exit=signed_voluntary_exit)
click.echo(load_text(['msg_creation_success']) + saved_folder)

remyroy marked this conversation as resolved.
Show resolved Hide resolved
click.pause(load_text(['msg_pause']))
1 change: 1 addition & 0 deletions staking_deposit/cli/generate_bls_to_execution_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,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
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 @@ -55,6 +57,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)


if __name__ == '__main__':
Expand Down
68 changes: 68 additions & 0 deletions staking_deposit/exit_transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import json
import os
import time
from typing import Any, Dict
from py_ecc.bls import G2ProofOfPossession as bls

from staking_deposit.settings import BaseChainSetting
from staking_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
from staking_deposit.utils.ssz import (
SignedVoluntaryExit,
VoluntaryExit,
compute_signing_root,
compute_voluntary_exit_domain,
)


def exit_transaction_generation(
chain_settings: BaseChainSetting,
signing_key: int,
validator_index: int,
epoch: int) -> SignedVoluntaryExit:
message = VoluntaryExit( # type: ignore[no-untyped-call]
epoch=epoch,
validator_index=validator_index
)

domain = compute_voluntary_exit_domain(
fork_version=chain_settings.EXIT_FORK_VERSION,
genesis_validators_root=chain_settings.GENESIS_VALIDATORS_ROOT
)

signing_root = compute_signing_root(message, domain)
signature = bls.Sign(signing_key, signing_root)

signed_exit = SignedVoluntaryExit( # type: ignore[no-untyped-call]
message=message,
signature=signature,
)

return signed_exit


def export_exit_transaction_json(folder: str, signed_exit: SignedVoluntaryExit) -> str:
signed_exit_json: Dict[str, Any] = {}
message = {
'epoch': str(signed_exit.message.epoch), # type: ignore[attr-defined]
'validator_index': str(signed_exit.message.validator_index), # type: ignore[attr-defined]
}
signed_exit_json.update({'message': message})
signed_exit_json.update({'signature': '0x' + signed_exit.signature.hex()}) # type: ignore[attr-defined]

output_folder = os.path.join(
folder,
DEFAULT_EXIT_TRANSACTION_FOLDER_NAME,
)
if not os.path.exists(output_folder):
os.mkdir(output_folder)

filefolder = os.path.join(
output_folder,
'signed_exit_transaction-%s-%i.json' % (signed_exit.message.validator_index, time.time()) # type: ignore[attr-defined]
)

with open(filefolder, 'w') as f:
json.dump(signed_exit_json, f)
if os.name == 'posix':
os.chmod(filefolder, int('440', 8)) # Read for owner & group
return filefolder
32 changes: 32 additions & 0 deletions staking_deposit/intl/en/cli/exit_transaction_keystore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"exit_transaction_keystore": {
"arg_exit_transaction_keystore" :{
"help": "Generate an exit transaction that can be used to exit validators on Ethereum Beacon Chain."
},
"arg_exit_transaction_keystore_chain": {
"help": "The name of the Ethereum PoS chain your validator is running on. \"mainnet\" is the default.",
"prompt": "Please choose the (mainnet or testnet) network/chain name"
},
"arg_exit_transaction_keystore_epoch": {
"help": "The epoch of when the exit transaction will be valid. The transaction will always be valid by default."
},
"arg_exit_transaction_keystore_keystore": {
"help": "The keystore file associated with the validator you wish to exit.",
"prompt": "Please enter the location of your keystore file."
},
"arg_exit_transaction_keystore_keystore_password": {
"help": "The password that is used to encrypt the provided keystore. Note: It's not your mnemonic password. (It is recommended not to use this argument, and wait for the CLI to ask you for your password as otherwise it will appear in your shell history.)",
"prompt": "Enter the password that is used to encrypt the provided keystore.",
"mismatch": "Error: The password does not match the provided keystore. Please try again."
},
"arg_exit_transaction_keystore_output_folder": {
"help": "The folder path where the exit transactions will be saved to. Pointing to `./exit_transactions` by default."
},
"arg_validator_index": {
"help": "The validator index corresponding to the provided keystore.",
"prompt": "Please enter the validator index of your validator that corresponds to the provided keystore as identified on the beacon chain."
},
"msg_creation_success": "\nSuccess!\nYour SignedExitTransaction JSON file can be found at: ",
"msg_pause": "\n\nPress any key."
}
}
Loading