forked from ethereum/staking-deposit-cli
-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add ability to generate exit message
- Loading branch information
1 parent
b09771e
commit 53b10d9
Showing
17 changed files
with
731 additions
and
90 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
|
||
try: | ||
secret_bytes = saved_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, | ||
) | ||
|
||
saved_folder = export_exit_transaction_json(folder=output_folder, signed_exit=signed_exit) | ||
|
||
click.echo(load_text(['msg_creation_success']) + saved_folder) | ||
click.pause(load_text(['msg_pause'])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
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) | ||
|
||
click.pause(load_text(['msg_pause'])) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | ||
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( | ||
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), | ||
'validator_index': str(signed_exit.message.validator_index), | ||
} | ||
signed_exit_json.update({'message': message}) | ||
signed_exit_json.update({'signature': '0x' + signed_exit.signature.hex()}) | ||
|
||
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()) | ||
) | ||
|
||
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
32
staking_deposit/intl/en/cli/exit_transaction_keystore.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
} | ||
} |
Oops, something went wrong.