From d3fd1da1fca7a308f427982fff29dfccc7c805e7 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Wed, 4 Jan 2023 17:31:09 +0800 Subject: [PATCH 01/35] Extract `load_mnemonic_arguments_decorator` func --- staking_deposit/cli/existing_mnemonic.py | 56 ++++++++++++------- staking_deposit/cli/generate_keys.py | 13 +---- .../intl/en/cli/generate_keys.json | 4 -- staking_deposit/intl/en/utils/validation.json | 4 ++ .../intl/tr/cli/generate_keys.json | 4 -- staking_deposit/intl/tr/utils/validation.json | 4 ++ 6 files changed, 44 insertions(+), 41 deletions(-) diff --git a/staking_deposit/cli/existing_mnemonic.py b/staking_deposit/cli/existing_mnemonic.py index 97e090f6..8aa87394 100644 --- a/staking_deposit/cli/existing_mnemonic.py +++ b/staking_deposit/cli/existing_mnemonic.py @@ -1,6 +1,7 @@ import click from typing import ( Any, + Callable, ) from staking_deposit.exceptions import ValidationError @@ -22,6 +23,39 @@ ) +def load_mnemonic_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]: + ''' + This is a decorator that, when applied to a parent-command, implements the + to obtain the necessary arguments for the generate_keys() subcommand. + ''' + decorators = [ + jit_option( + callback=validate_mnemonic, + help=lambda: load_text(['arg_mnemonic', 'help'], func='existing_mnemonic'), + param_decls='--mnemonic', + prompt=lambda: load_text(['arg_mnemonic', 'prompt'], func='existing_mnemonic'), + type=str, + ), + jit_option( + callback=captive_prompt_callback( + lambda x: x, + lambda: load_text(['arg_mnemonic_password', 'prompt'], func='existing_mnemonic'), + lambda: load_text(['arg_mnemonic_password', 'confirm'], func='existing_mnemonic'), + lambda: load_text(['arg_mnemonic_password', 'mismatch'], func='existing_mnemonic'), + True, + ), + default='', + help=lambda: load_text(['arg_mnemonic_password', 'help'], func='existing_mnemonic'), + hidden=True, + param_decls='--mnemonic-password', + prompt=False, + ), + ] + for decorator in reversed(decorators): + function = decorator(function) + return function + + def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str: mnemonic = reconstruct_mnemonic(mnemonic, WORD_LISTS_PATH) if mnemonic is not None: @@ -33,27 +67,7 @@ def validate_mnemonic(ctx: click.Context, param: Any, mnemonic: str) -> str: @click.command( help=load_text(['arg_existing_mnemonic', 'help'], func='existing_mnemonic'), ) -@jit_option( - callback=validate_mnemonic, - help=lambda: load_text(['arg_mnemonic', 'help'], func='existing_mnemonic'), - param_decls='--mnemonic', - prompt=lambda: load_text(['arg_mnemonic', 'prompt'], func='existing_mnemonic'), - type=str, -) -@jit_option( - callback=captive_prompt_callback( - lambda x: x, - lambda: load_text(['arg_mnemonic_password', 'prompt'], func='existing_mnemonic'), - lambda: load_text(['arg_mnemonic_password', 'confirm'], func='existing_mnemonic'), - lambda: load_text(['arg_mnemonic_password', 'mismatch'], func='existing_mnemonic'), - True, - ), - default='', - help=lambda: load_text(['arg_mnemonic_password', 'help'], func='existing_mnemonic'), - hidden=True, - param_decls='--mnemonic-password', - prompt=False, -) +@load_mnemonic_arguments_decorator @jit_option( callback=captive_prompt_callback( lambda num: validate_int_range(num, 0, 2**32), diff --git a/staking_deposit/cli/generate_keys.py b/staking_deposit/cli/generate_keys.py index 4caa3b7b..e28d9ae8 100644 --- a/staking_deposit/cli/generate_keys.py +++ b/staking_deposit/cli/generate_keys.py @@ -6,7 +6,6 @@ ) from eth_typing import HexAddress -from eth_utils import is_hex_address, to_normalized_address from staking_deposit.credentials import ( CredentialList, @@ -16,6 +15,7 @@ verify_deposit_data_json, validate_int_range, validate_password_strength, + validate_eth1_withdrawal_address, ) from staking_deposit.utils.constants import ( MAX_DEPOSIT_AMOUNT, @@ -43,17 +43,6 @@ def get_password(text: str) -> str: return click.prompt(text, hide_input=True, show_default=False, type=str) -def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: str) -> HexAddress: - if address is None: - return None - if not is_hex_address(address): - raise ValueError(load_text(['err_invalid_ECDSA_hex_addr'])) - - normalized_address = to_normalized_address(address) - click.echo('\n%s\n' % load_text(['msg_ECDSA_addr_withdrawal'])) - return normalized_address - - def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]: ''' This is a decorator that, when applied to a parent-command, implements the diff --git a/staking_deposit/intl/en/cli/generate_keys.json b/staking_deposit/intl/en/cli/generate_keys.json index c1f33b3b..a6a71f30 100644 --- a/staking_deposit/intl/en/cli/generate_keys.json +++ b/staking_deposit/intl/en/cli/generate_keys.json @@ -1,8 +1,4 @@ { - "validate_eth1_withdrawal_address": { - "err_invalid_ECDSA_hex_addr": "The given Eth1 address is not in hexadecimal encoded form.", - "msg_ECDSA_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" - }, "generate_keys_arguments_decorator": { "num_validators": { "help": "The number of new validator keys you want to generate (you can always generate more later)", diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 50145b22..80a24110 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -10,5 +10,9 @@ }, "validate_choice": { "err_invalid_choice": "That is not one of the valid choices. Please retype your choice." + }, + "validate_eth1_withdrawal_address": { + "err_invalid_ECDSA_hex_addr": "The given Eth1 address is not in hexadecimal encoded form.", + "msg_ECDSA_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" } } \ No newline at end of file diff --git a/staking_deposit/intl/tr/cli/generate_keys.json b/staking_deposit/intl/tr/cli/generate_keys.json index bcfc3bb5..975a3e4f 100644 --- a/staking_deposit/intl/tr/cli/generate_keys.json +++ b/staking_deposit/intl/tr/cli/generate_keys.json @@ -1,8 +1,4 @@ { - "validate_eth1_withdrawal_address": { - "err_invalid_ECDSA_hex_addr": "Girilen Eth1 adresi onaltılık sistemde kodlanmamıştır.", - "msg_ECDSA_addr_withdrawal": "**[Uyarı] bir Eth1 adresini varlık çekme adresi olarak giriyorsunuz. Lütfen bu adresin kontrolünün sizde olduğundan emin olun.**" - }, "generate_keys_arguments_decorator": { "num_validators": { "help": "Oluşturmak istediğiniz yeni doğrulayıcı anahtarlarının sayısını giriniz (daha fazlasını daha sonra da oluşturabilirsiniz)", diff --git a/staking_deposit/intl/tr/utils/validation.json b/staking_deposit/intl/tr/utils/validation.json index 73fcef98..bdbf3d60 100644 --- a/staking_deposit/intl/tr/utils/validation.json +++ b/staking_deposit/intl/tr/utils/validation.json @@ -10,5 +10,9 @@ }, "validate_choice": { "err_invalid_choice": "Geçerli bir seçim değil. Lütfen seçiminizi tekrar girin." + }, + "validate_eth1_withdrawal_address": { + "err_invalid_ECDSA_hex_addr": "Girilen Eth1 adresi onaltılık sistemde kodlanmamıştır.", + "msg_ECDSA_addr_withdrawal": "**[Uyarı] bir Eth1 adresini varlık çekme adresi olarak giriyorsunuz. Lütfen bu adresin kontrolünün sizde olduğundan emin olun.**" } } From 7404b69e6c069a0887232bfcd092df3e66d09268 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 6 Jan 2023 18:17:28 +0800 Subject: [PATCH 02/35] [WIP] Add generate_bls_to_execution_change --- .gitignore | 1 + .../cli/generate_bls_to_execution_change.py | 180 ++++++++++++++++++ staking_deposit/credentials.py | 69 ++++++- staking_deposit/deposit.py | 2 + .../cli/generate_bls_to_execution_change.json | 39 ++++ staking_deposit/intl/en/credentials.json | 3 + staking_deposit/intl/en/utils/validation.json | 8 +- staking_deposit/settings.py | 23 ++- staking_deposit/utils/constants.py | 9 + staking_deposit/utils/ssz.py | 53 +++++- staking_deposit/utils/validation.py | 33 +++- tests/test_cli/helpers.py | 26 ++- tests/test_cli/test_existing_menmonic.py | 2 +- .../test_generate_bls_to_execution_change.py | 51 +++++ 14 files changed, 473 insertions(+), 26 deletions(-) create mode 100644 staking_deposit/cli/generate_bls_to_execution_change.py create mode 100644 staking_deposit/intl/en/cli/generate_bls_to_execution_change.json create mode 100644 tests/test_cli/test_generate_bls_to_execution_change.py diff --git a/.gitignore b/.gitignore index e4add855..d8c216b7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ validator_keys +bls_to_execution_changes # Python testing & linting: build/ diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py new file mode 100644 index 00000000..17262723 --- /dev/null +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -0,0 +1,180 @@ +import os +import click +from typing import ( + Any, +) + +from eth_typing import HexAddress + +from staking_deposit.credentials import ( + CredentialList, +) +from staking_deposit.utils.validation import ( + validate_bls_withdrawal_credentials, + validate_bls_withdrawal_credentials_matching, + validate_eth1_withdrawal_address, + validate_int_range, +) +from staking_deposit.utils.constants import ( + BTEC_FORK_VERSIONS, + CAPELLA, + DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, + MAX_DEPOSIT_AMOUNT, +) +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.settings import ( + ALL_CHAINS, + MAINNET, + PRATER, + get_chain_setting, +) +from .existing_mnemonic import ( + load_mnemonic_arguments_decorator, +) + + +def get_password(text: str) -> str: + return click.prompt(text, hide_input=True, show_default=False, type=str) + + +FUNC_NAME = 'generate_bls_to_execution_change' + + +@click.command() +@jit_option( + default=os.getcwd(), + help=lambda: load_text(['arg_bls_to_execution_changes_folder', 'help'], func=FUNC_NAME), + param_decls='--bls_to_execution_changes_folder', + type=click.Path(exists=True, file_okay=False, dir_okay=True), +) +@jit_option( + callback=captive_prompt_callback( + lambda x: closest_match(x, list(ALL_CHAINS.keys())), + choice_prompt_func( + lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME), + list(ALL_CHAINS.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) + ), +) +@jit_option( + callback=captive_prompt_callback( + lambda x: closest_match(x, list(BTEC_FORK_VERSIONS.keys())), + choice_prompt_func( + lambda: load_text(['arg_fork', 'prompt'], func=FUNC_NAME), + list(BTEC_FORK_VERSIONS.keys()) + ), + ), + default=CAPELLA, + help=lambda: load_text(['arg_fork', 'help'], func=FUNC_NAME), + param_decls='--fork', + prompt=choice_prompt_func( + lambda: load_text(['arg_fork', 'prompt'], func=FUNC_NAME), + list(key for key in BTEC_FORK_VERSIONS.keys()) + ), +) +@load_mnemonic_arguments_decorator +@jit_option( + callback=captive_prompt_callback( + lambda num: validate_int_range(num, 0, 2**32), + lambda: load_text(['arg_validator_start_index', 'prompt'], func=FUNC_NAME), + lambda: load_text(['arg_validator_start_index', 'confirm'], func=FUNC_NAME), + ), + default=0, + help=lambda: load_text(['arg_validator_start_index', 'help'], func=FUNC_NAME), + param_decls="--validator_start_index", + prompt=lambda: load_text(['arg_validator_start_index', '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( + callback=captive_prompt_callback( + lambda bls_withdrawal_credentials: validate_bls_withdrawal_credentials(bls_withdrawal_credentials), + lambda: load_text(['arg_bls_withdrawal_credentials', 'prompt'], func=FUNC_NAME), + ), + help=lambda: load_text(['arg_bls_withdrawal_credentials', 'help'], func=FUNC_NAME), + param_decls='--bls_withdrawal_credentials', + prompt=lambda: load_text(['arg_bls_withdrawal_credentials', 'prompt'], func=FUNC_NAME), +) +@jit_option( + callback=captive_prompt_callback( + lambda address: validate_eth1_withdrawal_address(None, None, address), + lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME), + ), + help=lambda: load_text(['arg_execution_address', 'help'], func=FUNC_NAME), + param_decls='--execution_address', + prompt=lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME), +) +@click.pass_context +def generate_bls_to_execution_change( + ctx: click.Context, + bls_to_execution_changes_folder: str, + chain: str, + fork: str, + mnemonic: str, + mnemonic_password: str, + validator_start_index: int, + validator_index: int, + bls_withdrawal_credentials: bytes, + execution_address: HexAddress, + **kwargs: Any) -> None: + # Generate folder + bls_to_execution_changes_folder = os.path.join( + bls_to_execution_changes_folder, + DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, + ) + if not os.path.exists(bls_to_execution_changes_folder): + os.mkdir(bls_to_execution_changes_folder) + + # Get chain setting + chain_setting = get_chain_setting(chain) + + # Get FORK_VERSION + fork_version = BTEC_FORK_VERSIONS[fork] + + # TODO: generate multiple? + num_validators = 1 + amounts = [MAX_DEPOSIT_AMOUNT] * num_validators + + credentials = CredentialList.from_mnemonic( + mnemonic=mnemonic, + mnemonic_password=mnemonic_password, + num_keys=num_validators, + amounts=amounts, + chain_setting=chain_setting, + start_index=validator_start_index, + hex_eth1_withdrawal_address=execution_address, + btec_fork_version=fork_version, + ) + + if len(credentials.credentials) != 1: + raise ValueError(f"It should only generate one credential, but get {len(credentials.credentials)}.") + + # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated + validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials, credentials.credentials[0]) + + credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_index) + + click.pause(load_text(['msg_pause'])) diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index 80a4ef86..54ce2086 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -3,7 +3,7 @@ from enum import Enum import time import json -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Any from eth_typing import Address, HexAddress from eth_utils import to_canonical_address @@ -27,9 +27,12 @@ from staking_deposit.utils.intl import load_text from staking_deposit.utils.ssz import ( compute_deposit_domain, + compute_bls_to_execution_change_domain, compute_signing_root, + BLSToExecutionChange, DepositData, DepositMessage, + SignedBLSToExecutionChange, ) @@ -45,7 +48,8 @@ class Credential: """ def __init__(self, *, mnemonic: str, mnemonic_password: str, index: int, amount: int, chain_setting: BaseChainSetting, - hex_eth1_withdrawal_address: Optional[HexAddress]): + hex_eth1_withdrawal_address: Optional[HexAddress], + btec_fork_version: Optional[bytes]=None): # Set path as EIP-2334 format # https://eips.ethereum.org/EIPS/eip-2334 purpose = '12381' @@ -61,6 +65,7 @@ def __init__(self, *, mnemonic: str, mnemonic_password: str, self.amount = amount self.chain_setting = chain_setting self.hex_eth1_withdrawal_address = hex_eth1_withdrawal_address + self.btec_fork_version = btec_fork_version @property def signing_pk(self) -> bytes: @@ -158,6 +163,48 @@ def verify_keystore(self, keystore_filefolder: str, password: str) -> bool: secret_bytes = saved_keystore.decrypt(password) return self.signing_sk == int.from_bytes(secret_bytes, 'big') + def get_bls_to_execution_change(self, validator_index: int) -> SignedBLSToExecutionChange: + if self.eth1_withdrawal_address is None: + raise ValueError("The execution address should NOT be empty.") + + if self.btec_fork_version is None: + raise ValueError("The BLSToExecutionChange signing fork version should NOT be empty.") + + message = BLSToExecutionChange( + validator_index=validator_index, + from_bls_pubkey=self.withdrawal_pk, + to_execution_address=self.eth1_withdrawal_address, + ) + domain = compute_bls_to_execution_change_domain( + fork_version=self.btec_fork_version, + genesis_validators_root=self.chain_setting.GENESIS_VALIDATORS_ROOT, + ) + signing_root = compute_signing_root(message, domain) + signature = bls.Sign(self.withdrawal_sk, signing_root) + + return SignedBLSToExecutionChange( + message=message, + signature=signature, + ) + + def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, bytes]: + result_dict: Dict[str, Any] = {} + signed_bls_to_execution_change = self.get_bls_to_execution_change(validator_index) + message = { + 'valdiator_index': signed_bls_to_execution_change.message.validator_index, + 'from_bls_pubkey': signed_bls_to_execution_change.message.from_bls_pubkey.hex(), + 'to_execution_address': signed_bls_to_execution_change.message.to_execution_address.hex(), + } + result_dict.update({'message': message}) + result_dict.update({'signature': signed_bls_to_execution_change.signature}) + + # meta + result_dict.update({'fork_version': self.btec_fork_version}) + result_dict.update({'network_name': self.chain_setting.NETWORK_NAME}) + result_dict.update({'genesis_validators_root': self.chain_setting.GENESIS_VALIDATORS_ROOT}) + result_dict.update({'deposit_cli_version': DEPOSIT_CLI_VERSION}) + return result_dict + class CredentialList: """ @@ -175,7 +222,8 @@ def from_mnemonic(cls, amounts: List[int], chain_setting: BaseChainSetting, start_index: int, - hex_eth1_withdrawal_address: Optional[HexAddress]) -> 'CredentialList': + hex_eth1_withdrawal_address: Optional[HexAddress], + btec_fork_version: Optional[bytes]=None) -> 'CredentialList': if len(amounts) != num_keys: raise ValueError( f"The number of keys ({num_keys}) doesn't equal to the corresponding deposit amounts ({len(amounts)})." @@ -185,7 +233,8 @@ def from_mnemonic(cls, show_percent=False, show_pos=True) as indices: return cls([Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password, index=index, amount=amounts[index - start_index], chain_setting=chain_setting, - hex_eth1_withdrawal_address=hex_eth1_withdrawal_address) + hex_eth1_withdrawal_address=hex_eth1_withdrawal_address, + btec_fork_version=btec_fork_version) for index in indices]) def export_keystores(self, password: str, folder: str) -> List[str]: @@ -210,3 +259,15 @@ def verify_keystores(self, keystore_filefolders: List[str], password: str) -> bo length=len(self.credentials), show_percent=False, show_pos=True) as items: return all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) for credential, filefolder in items) + + def export_bls_to_execution_change_json(self, folder: str, validator_index: int) -> str: + with click.progressbar(self.credentials, label=load_text(['msg_bls_to_execution_change_creation']), + show_percent=False, show_pos=True) as credentials: + bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_index) for cred in credentials] + + filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time()) + with open(filefolder, 'w') as f: + json.dump(bls_to_execution_changes, f, default=lambda x: x.hex()) + if os.name == 'posix': + os.chmod(filefolder, int('440', 8)) # Read for owner & group + return filefolder diff --git a/staking_deposit/deposit.py b/staking_deposit/deposit.py index c224bfd2..9bb3da21 100644 --- a/staking_deposit/deposit.py +++ b/staking_deposit/deposit.py @@ -2,6 +2,7 @@ import sys from staking_deposit.cli.existing_mnemonic import existing_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 ( captive_prompt_callback, @@ -53,6 +54,7 @@ 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) if __name__ == '__main__': diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json new file mode 100644 index 00000000..464a965e --- /dev/null +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -0,0 +1,39 @@ +{ + "generate_bls_to_execution_change": { + "arg_execution_address": { + "help": "The 20-byte (Eth1) execution address that will be used in withdrawal", + "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials", + "confirm": "Repeat your execution address for confirmation." + }, + "arg_validator_index": { + "help": "The validator index of the certain validator", + "prompt": "Please enter the validator index of your validator" + }, + "arg_bls_withdrawal_credentials": { + "help": "The 32-byte old BLS withdrawal credentials of the certain validator", + "prompt": "Please enter the old BLS withdrawal credentials of your validator" + }, + "arg_validator_start_index": { + "help": "The index (key number) of the signging key you want to use with this mnemonic", + "prompt": "Please enter the index (key number) of the signging key you want to use with this mnemonic.", + "confirm": "Please repeat the index to confirm" + }, + "arg_chain": { + "help": "The name of Ethereum PoS chain you are targeting. Use \"mainnet\" if you are depositing ETH", + "prompt": "Please choose the (mainnet or testnet) network/chain name" + }, + "arg_fork": { + "help": "The fork name of the fork you want to signing the message with.", + "prompt": "Please choose the fork name of the fork you want to signing the message with." + }, + "arg_validator_keys_folder": { + "help": "The folder path for the keystore(s). Pointing to `./validator_keys` by default." + }, + "arg_bls_to_execution_changes_folder": { + "help": "The folder path for the keystore(s). Pointing to `./bls_to_execution_changes` by default." + }, + "msg_key_creation": "Creating your SignedBLSToExecutionChange.", + "msg_creation_success": "\nSuccess!\nYour SignedBLSToExecutionChange data can be found at: ", + "msg_pause": "\n\nPress any key." + } +} diff --git a/staking_deposit/intl/en/credentials.json b/staking_deposit/intl/en/credentials.json index da6f6559..85d72612 100644 --- a/staking_deposit/intl/en/credentials.json +++ b/staking_deposit/intl/en/credentials.json @@ -8,6 +8,9 @@ "export_deposit_data_json": { "msg_depositdata_creation": "Creating your depositdata:\t" }, + "export_bls_to_execution_change_json": { + "msg_bls_to_execution_change_creation": "Creating your SignedBLSToExecutionChange:\t" + }, "verify_keystores": { "msg_keystore_verification": "Verifying your keystores:\t" } diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 80a24110..80296adc 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -14,5 +14,11 @@ "validate_eth1_withdrawal_address": { "err_invalid_ECDSA_hex_addr": "The given Eth1 address is not in hexadecimal encoded form.", "msg_ECDSA_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" + }, + "validate_bls_withdrawal_credentials": { + "err_not_bls_form": "The given withdrawal credentials is not in BLS_WITHDRAWAL_PREFIX form." + }, + "validate_bls_withdrawal_credentials_matching": { + "err_not_matching": "The given withdrawal credentials is matching the old BLS withdrawal credentials that mnemonic generated." } -} \ No newline at end of file +} diff --git a/staking_deposit/settings.py b/staking_deposit/settings.py index 3f09010e..e3f4ab39 100644 --- a/staking_deposit/settings.py +++ b/staking_deposit/settings.py @@ -7,6 +7,7 @@ class BaseChainSetting(NamedTuple): NETWORK_NAME: str GENESIS_FORK_VERSION: bytes + GENESIS_VALIDATORS_ROOT: bytes MAINNET = 'mainnet' @@ -16,17 +17,29 @@ class BaseChainSetting(NamedTuple): KILN = 'kiln' SEPOLIA = 'sepolia' +# FIXME: use the real testnet genesis_validators_root +GENESIS_VALIDATORS_ROOT_STUB = bytes.fromhex('4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95') # Mainnet setting -MainnetSetting = BaseChainSetting(NETWORK_NAME=MAINNET, GENESIS_FORK_VERSION=bytes.fromhex('00000000')) +MainnetSetting = BaseChainSetting( + NETWORK_NAME=MAINNET, GENESIS_FORK_VERSION=bytes.fromhex('00000000'), + GENESIS_VALIDATORS_ROOT=bytes.fromhex('4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95')) # Ropsten setting -RopstenSetting = BaseChainSetting(NETWORK_NAME=ROPSTEN, GENESIS_FORK_VERSION=bytes.fromhex('80000069')) +RopstenSetting = BaseChainSetting( + NETWORK_NAME=ROPSTEN, GENESIS_FORK_VERSION=bytes.fromhex('80000069'), + GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) # Goerli setting -GoerliSetting = BaseChainSetting(NETWORK_NAME=GOERLI, GENESIS_FORK_VERSION=bytes.fromhex('00001020')) +GoerliSetting = BaseChainSetting( + NETWORK_NAME=GOERLI, GENESIS_FORK_VERSION=bytes.fromhex('00001020'), + GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) # Merge Testnet (spec v1.1.9) -KilnSetting = BaseChainSetting(NETWORK_NAME=KILN, GENESIS_FORK_VERSION=bytes.fromhex('70000069')) +KilnSetting = BaseChainSetting( + NETWORK_NAME=KILN, GENESIS_FORK_VERSION=bytes.fromhex('70000069'), + GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) # Sepolia setting -SepoliaSetting = BaseChainSetting(NETWORK_NAME=SEPOLIA, GENESIS_FORK_VERSION=bytes.fromhex('90000069')) +SepoliaSetting = BaseChainSetting( + NETWORK_NAME=SEPOLIA, GENESIS_FORK_VERSION=bytes.fromhex('90000069'), + GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) ALL_CHAINS: Dict[str, BaseChainSetting] = { diff --git a/staking_deposit/utils/constants.py b/staking_deposit/utils/constants.py index 12f6122a..a055275e 100644 --- a/staking_deposit/utils/constants.py +++ b/staking_deposit/utils/constants.py @@ -9,6 +9,7 @@ # Execution-spec constants taken from https://github.com/ethereum/consensus-specs/blob/dev/specs/phase0/beacon-chain.md DOMAIN_DEPOSIT = bytes.fromhex('03000000') +DOMAIN_BLS_TO_EXECUTION_CHANGE = bytes.fromhex('0A000000') BLS_WITHDRAWAL_PREFIX = bytes.fromhex('00') ETH1_ADDRESS_WITHDRAWAL_PREFIX = bytes.fromhex('01') @@ -20,11 +21,19 @@ # File/folder constants WORD_LISTS_PATH = os.path.join('staking_deposit', 'key_handling', 'key_derivation', 'word_lists') DEFAULT_VALIDATOR_KEYS_FOLDER_NAME = 'validator_keys' +DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME = 'bls_to_execution_changes' # Internationalisation constants INTL_CONTENT_PATH = os.path.join('staking_deposit', 'intl') +# BLSToExecutionChange signing fork versions +CAPELLA = 'capella' +BTEC_FORK_VERSIONS: Dict[str, bytes] = { + CAPELLA: bytes.fromhex('03000000'), +} + + def _add_index_to_options(d: Dict[str, List[str]]) -> Dict[str, List[str]]: ''' Adds the (1 indexed) index (in the dict) to the first element of value list. diff --git a/staking_deposit/utils/ssz.py b/staking_deposit/utils/ssz.py index e079a007..513b0096 100644 --- a/staking_deposit/utils/ssz.py +++ b/staking_deposit/utils/ssz.py @@ -8,11 +8,13 @@ bytes96 ) from staking_deposit.utils.constants import ( + DOMAIN_BLS_TO_EXECUTION_CHANGE, DOMAIN_DEPOSIT, ZERO_BYTES32, ) bytes8 = ByteVector(8) +bytes20 = ByteVector(20) # Crypto Domain SSZ @@ -31,6 +33,18 @@ class ForkData(Serializable): ] +def compute_fork_data_root(current_version: bytes, genesis_validators_root: bytes) -> bytes: + """ + Return the appropriate ForkData root for a given deposit version. + """ + if len(current_version) != 4: + raise ValueError(f"Fork version should be in 4 bytes. Got {len(current_version)}.") + return ForkData( + current_version=current_version, + genesis_validators_root=genesis_validators_root, + ).hash_tree_root + + def compute_deposit_domain(fork_version: bytes) -> bytes: """ Deposit-only `compute_domain` @@ -42,17 +56,23 @@ def compute_deposit_domain(fork_version: bytes) -> bytes: return domain_type + fork_data_root[:28] +def compute_bls_to_execution_change_domain(fork_version: bytes, genesis_validators_root: bytes) -> bytes: + """ + BLS_TO_EXECUTION_CHANGE-only `compute_domain` + """ + if len(fork_version) != 4: + raise ValueError(f"Fork version should be in 4 bytes. Got {len(fork_version)}.") + domain_type = DOMAIN_BLS_TO_EXECUTION_CHANGE + fork_data_root = compute_fork_data_root(fork_version, genesis_validators_root) + return domain_type + fork_data_root[:28] + + def compute_deposit_fork_data_root(current_version: bytes) -> bytes: """ Return the appropriate ForkData root for a given deposit version. """ genesis_validators_root = ZERO_BYTES32 # For deposit, it's fixed value - if len(current_version) != 4: - raise ValueError(f"Fork version should be in 4 bytes. Got {len(current_version)}.") - return ForkData( - current_version=current_version, - genesis_validators_root=genesis_validators_root, - ).hash_tree_root + return compute_fork_data_root(current_version, genesis_validators_root) def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: @@ -91,3 +111,24 @@ class DepositData(Serializable): ('amount', uint64), ('signature', bytes96) ] + + +class BLSToExecutionChange(Serializable): + """ + Ref: https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#blstoexecutionchange + """ + fields = [ + ('validator_index', uint64), + ('from_bls_pubkey', bytes48), + ('to_execution_address', bytes20), + ] + + +class SignedBLSToExecutionChange(Serializable): + """ + Ref: https://github.com/ethereum/consensus-specs/blob/dev/specs/capella/beacon-chain.md#signedblstoexecutionchange + """ + fields = [ + ('message', BLSToExecutionChange), + ('signature', bytes96), + ] diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index d3bdd7ab..4b762ba2 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -1,11 +1,13 @@ import click import json +from typing import Any, Dict, Sequence + from eth_typing import ( BLSPubkey, BLSSignature, + HexAddress, ) -from typing import Any, Dict, Sequence - +from eth_utils import is_hex_address, to_normalized_address from py_ecc.bls import G2ProofOfPossession as bls from staking_deposit.exceptions import ValidationError @@ -112,3 +114,30 @@ def validate_int_range(num: Any, low: int, high: int) -> int: return num_int except (ValueError, AssertionError): raise ValidationError(load_text(['err_not_positive_integer'])) + + +def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: str) -> HexAddress: + if address is None: + return None + if not is_hex_address(address): + raise ValueError(load_text(['err_invalid_ECDSA_hex_addr'])) + + normalized_address = to_normalized_address(address) + click.echo('\n%s\n' % load_text(['msg_ECDSA_addr_withdrawal'])) + return normalized_address + + +def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> bytes: + bls_withdrawal_credentials_bytes = bytes.fromhex(bls_withdrawal_credentials) + try: + assert len(bls_withdrawal_credentials_bytes) == 32 + assert bls_withdrawal_credentials_bytes[:1] == BLS_WITHDRAWAL_PREFIX + except (ValueError, AssertionError): + raise ValidationError(load_text(['err_not_bls_form'])) + + return bls_withdrawal_credentials_bytes + + +def validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials: bytes, credential: Credential) -> None: + if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: + raise ValidationError(load_text(['err_not_matching'])) diff --git a/tests/test_cli/helpers.py b/tests/test_cli/helpers.py index 127e04a1..8d5aa8b9 100644 --- a/tests/test_cli/helpers.py +++ b/tests/test_cli/helpers.py @@ -1,19 +1,31 @@ import os from staking_deposit.key_handling.keystore import Keystore -from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME +from staking_deposit.utils.constants import ( + DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, + DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, +) def clean_key_folder(my_folder_path: str) -> None: - validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) - if not os.path.exists(validator_keys_folder_path): + sub_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + clean_folder(my_folder_path, sub_folder_path) + + +def clean_btec_folder(my_folder_path: str) -> None: + sub_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME) + clean_folder(my_folder_path, sub_folder_path) + + +def clean_folder(primary_folder_path: str, sub_folder_path: str) -> None: + if not os.path.exists(sub_folder_path): return - _, _, key_files = next(os.walk(validator_keys_folder_path)) + _, _, key_files = next(os.walk(sub_folder_path)) for key_file_name in key_files: - os.remove(os.path.join(validator_keys_folder_path, key_file_name)) - os.rmdir(validator_keys_folder_path) - os.rmdir(my_folder_path) + os.remove(os.path.join(sub_folder_path, key_file_name)) + os.rmdir(sub_folder_path) + os.rmdir(primary_folder_path) def get_uuid(key_file: str) -> str: diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index bcd5a8a1..4e62d65d 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -9,7 +9,7 @@ from staking_deposit.deposit import cli from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, ETH1_ADDRESS_WITHDRAWAL_PREFIX -from.helpers import clean_key_folder, get_permissions, get_uuid +from .helpers import clean_key_folder, get_permissions, get_uuid def test_existing_mnemonic_bls_withdrawal() -> None: diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py new file mode 100644 index 00000000..3a1c0e51 --- /dev/null +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -0,0 +1,51 @@ +import os + +from click.testing import CliRunner + +from staking_deposit.deposit import cli +from staking_deposit.utils.constants import DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME +from .helpers import ( + clean_btec_folder, + get_permissions, +) + + +def test_existing_mnemonic_bls_withdrawal() -> None: + # Prepare folder + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_btec_folder(my_folder_path) + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + runner = CliRunner() + inputs = ['0'] # confirm `validator_start_index` + data = '\n'.join(inputs) + arguments = [ + '--language', 'english', + 'generate-bls-to-execution-change', + '--bls_to_execution_changes_folder', my_folder_path, + '--chain', 'mainnet', + '--fork', 'capella', + '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 + '--bls_withdrawal_credentials', '00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '--validator_start_index', '0', + '--validator_index', '1', + '--execution_address', '3434343434343434343434343434343434343434', + ] + result = runner.invoke(cli, arguments, input=data) + assert result.exit_code == 0 + + # Check files + bls_to_execution_changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME) + _, _, btec_files = next(os.walk(bls_to_execution_changes_folder_path)) + + # TODO verify file content + assert len(set(btec_files)) == 1 + + # Verify file permissions + if os.name == 'posix': + for file_name in btec_files: + assert get_permissions(bls_to_execution_changes_folder_path, file_name) == '0o440' + + # Clean up + clean_btec_folder(my_folder_path) From d68a1c7ad8de71330acd2a04659e43d846286e77 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sun, 15 Jan 2023 23:22:30 +0800 Subject: [PATCH 03/35] Always use `GENESIS_FORK_VERSION` to sign BLSToExecutionChange and fix typo --- .../cli/generate_bls_to_execution_change.py | 23 ------------------- staking_deposit/credentials.py | 18 ++++----------- staking_deposit/utils/constants.py | 7 ------ .../test_generate_bls_to_execution_change.py | 1 - 4 files changed, 5 insertions(+), 44 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 17262723..d3c39f19 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -16,8 +16,6 @@ validate_int_range, ) from staking_deposit.utils.constants import ( - BTEC_FORK_VERSIONS, - CAPELLA, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, MAX_DEPOSIT_AMOUNT, ) @@ -72,22 +70,6 @@ def get_password(text: str) -> str: list(key for key in ALL_CHAINS.keys() if key != PRATER) ), ) -@jit_option( - callback=captive_prompt_callback( - lambda x: closest_match(x, list(BTEC_FORK_VERSIONS.keys())), - choice_prompt_func( - lambda: load_text(['arg_fork', 'prompt'], func=FUNC_NAME), - list(BTEC_FORK_VERSIONS.keys()) - ), - ), - default=CAPELLA, - help=lambda: load_text(['arg_fork', 'help'], func=FUNC_NAME), - param_decls='--fork', - prompt=choice_prompt_func( - lambda: load_text(['arg_fork', 'prompt'], func=FUNC_NAME), - list(key for key in BTEC_FORK_VERSIONS.keys()) - ), -) @load_mnemonic_arguments_decorator @jit_option( callback=captive_prompt_callback( @@ -132,7 +114,6 @@ def generate_bls_to_execution_change( ctx: click.Context, bls_to_execution_changes_folder: str, chain: str, - fork: str, mnemonic: str, mnemonic_password: str, validator_start_index: int, @@ -151,9 +132,6 @@ def generate_bls_to_execution_change( # Get chain setting chain_setting = get_chain_setting(chain) - # Get FORK_VERSION - fork_version = BTEC_FORK_VERSIONS[fork] - # TODO: generate multiple? num_validators = 1 amounts = [MAX_DEPOSIT_AMOUNT] * num_validators @@ -166,7 +144,6 @@ def generate_bls_to_execution_change( chain_setting=chain_setting, start_index=validator_start_index, hex_eth1_withdrawal_address=execution_address, - btec_fork_version=fork_version, ) if len(credentials.credentials) != 1: diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index 54ce2086..53423853 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -48,8 +48,7 @@ class Credential: """ def __init__(self, *, mnemonic: str, mnemonic_password: str, index: int, amount: int, chain_setting: BaseChainSetting, - hex_eth1_withdrawal_address: Optional[HexAddress], - btec_fork_version: Optional[bytes]=None): + hex_eth1_withdrawal_address: Optional[HexAddress]): # Set path as EIP-2334 format # https://eips.ethereum.org/EIPS/eip-2334 purpose = '12381' @@ -65,7 +64,6 @@ def __init__(self, *, mnemonic: str, mnemonic_password: str, self.amount = amount self.chain_setting = chain_setting self.hex_eth1_withdrawal_address = hex_eth1_withdrawal_address - self.btec_fork_version = btec_fork_version @property def signing_pk(self) -> bytes: @@ -167,16 +165,13 @@ def get_bls_to_execution_change(self, validator_index: int) -> SignedBLSToExecut if self.eth1_withdrawal_address is None: raise ValueError("The execution address should NOT be empty.") - if self.btec_fork_version is None: - raise ValueError("The BLSToExecutionChange signing fork version should NOT be empty.") - message = BLSToExecutionChange( validator_index=validator_index, from_bls_pubkey=self.withdrawal_pk, to_execution_address=self.eth1_withdrawal_address, ) domain = compute_bls_to_execution_change_domain( - fork_version=self.btec_fork_version, + fork_version=self.chain_setting.GENESIS_FORK_VERSION, genesis_validators_root=self.chain_setting.GENESIS_VALIDATORS_ROOT, ) signing_root = compute_signing_root(message, domain) @@ -191,7 +186,7 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by result_dict: Dict[str, Any] = {} signed_bls_to_execution_change = self.get_bls_to_execution_change(validator_index) message = { - 'valdiator_index': signed_bls_to_execution_change.message.validator_index, + 'validator_index': signed_bls_to_execution_change.message.validator_index, 'from_bls_pubkey': signed_bls_to_execution_change.message.from_bls_pubkey.hex(), 'to_execution_address': signed_bls_to_execution_change.message.to_execution_address.hex(), } @@ -199,7 +194,6 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by result_dict.update({'signature': signed_bls_to_execution_change.signature}) # meta - result_dict.update({'fork_version': self.btec_fork_version}) result_dict.update({'network_name': self.chain_setting.NETWORK_NAME}) result_dict.update({'genesis_validators_root': self.chain_setting.GENESIS_VALIDATORS_ROOT}) result_dict.update({'deposit_cli_version': DEPOSIT_CLI_VERSION}) @@ -222,8 +216,7 @@ def from_mnemonic(cls, amounts: List[int], chain_setting: BaseChainSetting, start_index: int, - hex_eth1_withdrawal_address: Optional[HexAddress], - btec_fork_version: Optional[bytes]=None) -> 'CredentialList': + hex_eth1_withdrawal_address: Optional[HexAddress]) -> 'CredentialList': if len(amounts) != num_keys: raise ValueError( f"The number of keys ({num_keys}) doesn't equal to the corresponding deposit amounts ({len(amounts)})." @@ -233,8 +226,7 @@ def from_mnemonic(cls, show_percent=False, show_pos=True) as indices: return cls([Credential(mnemonic=mnemonic, mnemonic_password=mnemonic_password, index=index, amount=amounts[index - start_index], chain_setting=chain_setting, - hex_eth1_withdrawal_address=hex_eth1_withdrawal_address, - btec_fork_version=btec_fork_version) + hex_eth1_withdrawal_address=hex_eth1_withdrawal_address) for index in indices]) def export_keystores(self, password: str, folder: str) -> List[str]: diff --git a/staking_deposit/utils/constants.py b/staking_deposit/utils/constants.py index a055275e..cd64ecde 100644 --- a/staking_deposit/utils/constants.py +++ b/staking_deposit/utils/constants.py @@ -27,13 +27,6 @@ INTL_CONTENT_PATH = os.path.join('staking_deposit', 'intl') -# BLSToExecutionChange signing fork versions -CAPELLA = 'capella' -BTEC_FORK_VERSIONS: Dict[str, bytes] = { - CAPELLA: bytes.fromhex('03000000'), -} - - def _add_index_to_options(d: Dict[str, List[str]]) -> Dict[str, List[str]]: ''' Adds the (1 indexed) index (in the dict) to the first element of value list. diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index 3a1c0e51..42e9a1c0 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -25,7 +25,6 @@ def test_existing_mnemonic_bls_withdrawal() -> None: 'generate-bls-to-execution-change', '--bls_to_execution_changes_folder', my_folder_path, '--chain', 'mainnet', - '--fork', 'capella', '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 '--bls_withdrawal_credentials', '00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', '--validator_start_index', '0', From 6a4ba1108f279ba1e74b38dd0b3781ceb0d1fcd9 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 01:37:16 +0800 Subject: [PATCH 04/35] Add `0x` prefix to the bytes type fields of the output JSON file --- staking_deposit/credentials.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index 53423853..2dfe18df 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -187,15 +187,15 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by signed_bls_to_execution_change = self.get_bls_to_execution_change(validator_index) message = { 'validator_index': signed_bls_to_execution_change.message.validator_index, - 'from_bls_pubkey': signed_bls_to_execution_change.message.from_bls_pubkey.hex(), - 'to_execution_address': signed_bls_to_execution_change.message.to_execution_address.hex(), + 'from_bls_pubkey': '0x' + signed_bls_to_execution_change.message.from_bls_pubkey.hex(), + 'to_execution_address': '0x' + signed_bls_to_execution_change.message.to_execution_address.hex(), } result_dict.update({'message': message}) - result_dict.update({'signature': signed_bls_to_execution_change.signature}) + result_dict.update({'signature': '0x' + signed_bls_to_execution_change.signature.hex()}) # meta result_dict.update({'network_name': self.chain_setting.NETWORK_NAME}) - result_dict.update({'genesis_validators_root': self.chain_setting.GENESIS_VALIDATORS_ROOT}) + result_dict.update({'genesis_validators_root': '0x' + self.chain_setting.GENESIS_VALIDATORS_ROOT.hex()}) result_dict.update({'deposit_cli_version': DEPOSIT_CLI_VERSION}) return result_dict @@ -259,7 +259,7 @@ def export_bls_to_execution_change_json(self, folder: str, validator_index: int) filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time()) with open(filefolder, 'w') as f: - json.dump(bls_to_execution_changes, f, default=lambda x: x.hex()) + json.dump(bls_to_execution_changes, f) if os.name == 'posix': os.chmod(filefolder, int('440', 8)) # Read for owner & group return filefolder From 47bf1aa849741c073f40735b39f576d589cc8955 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 16:42:18 +0800 Subject: [PATCH 05/35] WIP validation --- .../cli/generate_bls_to_execution_change.py | 8 +++-- .../cli/generate_bls_to_execution_change.json | 3 +- staking_deposit/intl/en/utils/validation.json | 3 ++ staking_deposit/utils/validation.py | 33 ++++++++++++++++++- 4 files changed, 43 insertions(+), 4 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index d3c39f19..83288186 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -14,6 +14,7 @@ validate_bls_withdrawal_credentials_matching, validate_eth1_withdrawal_address, validate_int_range, + verify_bls_to_execution_change_json, ) from staking_deposit.utils.constants import ( DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, @@ -24,6 +25,7 @@ choice_prompt_func, jit_option, ) +from staking_deposit.exceptions import ValidationError from staking_deposit.utils.intl import ( closest_match, load_text, @@ -75,7 +77,6 @@ def get_password(text: str) -> str: callback=captive_prompt_callback( lambda num: validate_int_range(num, 0, 2**32), lambda: load_text(['arg_validator_start_index', 'prompt'], func=FUNC_NAME), - lambda: load_text(['arg_validator_start_index', 'confirm'], func=FUNC_NAME), ), default=0, help=lambda: load_text(['arg_validator_start_index', 'help'], func=FUNC_NAME), @@ -152,6 +153,9 @@ def generate_bls_to_execution_change( # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials, credentials.credentials[0]) - credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_index) + btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_index) + + if not verify_bls_to_execution_change_json(btec_file, credentials.credentials): + raise ValidationError(load_text(['err_verify_btec'])) click.pause(load_text(['msg_pause'])) diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index 464a965e..8a045b4a 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -34,6 +34,7 @@ }, "msg_key_creation": "Creating your SignedBLSToExecutionChange.", "msg_creation_success": "\nSuccess!\nYour SignedBLSToExecutionChange data can be found at: ", - "msg_pause": "\n\nPress any key." + "msg_pause": "\n\nPress any key.", + "err_verify_btec": "Failed to verify the bls_to_execution_change JSON files." } } diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 80296adc..78faa46c 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -20,5 +20,8 @@ }, "validate_bls_withdrawal_credentials_matching": { "err_not_matching": "The given withdrawal credentials is matching the old BLS withdrawal credentials that mnemonic generated." + }, + "verify_bls_to_execution_change_json": { + "msg_bls_to_execution_change_verification": "Verifying your BLSToExecutionChange file:\t" } } diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 4b762ba2..d2e3d08d 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -7,7 +7,7 @@ BLSSignature, HexAddress, ) -from eth_utils import is_hex_address, to_normalized_address +from eth_utils import is_hex_address, to_normalized_address, decode_hex from py_ecc.bls import G2ProofOfPossession as bls from staking_deposit.exceptions import ValidationError @@ -30,6 +30,10 @@ from staking_deposit.utils.crypto import SHA256 +# +# Deposit +# + def verify_deposit_data_json(filefolder: str, credentials: Sequence[Credential]) -> bool: """ Validate every deposit found in the deposit-data JSON file folder. @@ -126,6 +130,33 @@ def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: st click.echo('\n%s\n' % load_text(['msg_ECDSA_addr_withdrawal'])) return normalized_address +# +# BLSToExecutionChange +# + + +def verify_bls_to_execution_change_json(filefolder: str, credentials: Sequence[Credential]) -> bool: + """ + Validate every BLSToExecutionChange found in the bls_to_execution_change JSON file folder. + """ + with open(filefolder, 'r') as f: + btec_json = json.load(f) + with click.progressbar(btec_json, label=load_text(['msg_bls_to_execution_change_verification']), + show_percent=False, show_pos=True) as btecs: + return all([validate_bls_to_execution_change(btec, credential) for btec, credential in zip(btecs, credentials)]) + return False + + +def validate_bls_to_execution_change(btec_dict: Dict[str, Any], credential: Credential) -> bool: + # TODO + # FIXME + ... + # validator_index = btec_dict['message']['validator_index'] + # from_bls_pubkey = BLSPubkey(decode_hex(btec_dict['message']['from_bls_pubkey'])) + # to_execution_address = decode_hex(btec_dict['message']['to_execution_address']) + + return True + def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> bytes: bls_withdrawal_credentials_bytes = bytes.fromhex(bls_withdrawal_credentials) From 63fdc5ec4f2408a40e29a9bca96cc5499de9859c Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 17:16:18 +0800 Subject: [PATCH 06/35] Add `--devnet_chain_setting` for devnet testing --- .../cli/generate_bls_to_execution_change.py | 18 ++++++++++++++++++ staking_deposit/settings.py | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 83288186..8a21965f 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -1,5 +1,6 @@ import os import click +import json from typing import ( Any, ) @@ -35,6 +36,7 @@ MAINNET, PRATER, get_chain_setting, + get_devnet_chain_setting, ) from .existing_mnemonic import ( load_mnemonic_arguments_decorator, @@ -110,6 +112,12 @@ def get_password(text: str) -> str: param_decls='--execution_address', prompt=lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME), ) +@jit_option( + # Only for devnet tests + default=None, + help="[DEVNET ONLY] Set specific GENESIS_FORK_VERSION value", + param_decls='--devnet_chain_setting', +) @click.pass_context def generate_bls_to_execution_change( ctx: click.Context, @@ -121,6 +129,7 @@ def generate_bls_to_execution_change( validator_index: int, bls_withdrawal_credentials: bytes, execution_address: HexAddress, + devnet_chain_setting: dict, **kwargs: Any) -> None: # Generate folder bls_to_execution_changes_folder = os.path.join( @@ -133,6 +142,15 @@ def generate_bls_to_execution_change( # Get chain setting chain_setting = get_chain_setting(chain) + if devnet_chain_setting is not None: + click.echo('\n%s\n' % '**[Warning] Using devnet chain setting to generate the SignedBLSToExecutionChange.**\t') + devnet_chain_setting_dict = json.loads(devnet_chain_setting) + chain_setting = get_devnet_chain_setting( + network_name=devnet_chain_setting_dict['network_name'], + genesis_fork_version=devnet_chain_setting_dict['genesis_fork_version'], + genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'], + ) + # TODO: generate multiple? num_validators = 1 amounts = [MAX_DEPOSIT_AMOUNT] * num_validators diff --git a/staking_deposit/settings.py b/staking_deposit/settings.py index e3f4ab39..524ad501 100644 --- a/staking_deposit/settings.py +++ b/staking_deposit/settings.py @@ -1,5 +1,5 @@ from typing import Dict, NamedTuple - +from eth_utils import decode_hex DEPOSIT_CLI_VERSION = '2.3.0' @@ -54,3 +54,11 @@ class BaseChainSetting(NamedTuple): def get_chain_setting(chain_name: str = MAINNET) -> BaseChainSetting: return ALL_CHAINS[chain_name] + + +def get_devnet_chain_setting(network_name: str, genesis_fork_version: str, genesis_validator_root: str): + return BaseChainSetting( + NETWORK_NAME=network_name, + GENESIS_FORK_VERSION=decode_hex(genesis_fork_version), + GENESIS_VALIDATORS_ROOT=decode_hex(genesis_validator_root), + ) From c5651ead26cbe388d37442dcf9cd8ec19427fa1b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 17:44:57 +0800 Subject: [PATCH 07/35] Improve `validate_bls_to_execution_change` --- .../cli/generate_bls_to_execution_change.py | 11 +++- staking_deposit/settings.py | 4 +- staking_deposit/utils/validation.py | 64 +++++++++++++++---- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 8a21965f..1675b3f5 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -129,7 +129,7 @@ def generate_bls_to_execution_change( validator_index: int, bls_withdrawal_credentials: bytes, execution_address: HexAddress, - devnet_chain_setting: dict, + devnet_chain_setting: str, **kwargs: Any) -> None: # Generate folder bls_to_execution_changes_folder = os.path.join( @@ -173,7 +173,14 @@ def generate_bls_to_execution_change( btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_index) - if not verify_bls_to_execution_change_json(btec_file, credentials.credentials): + json_file_validation_result = verify_bls_to_execution_change_json( + btec_file, + credentials.credentials, + input_validator_index=validator_index, + input_execution_address=execution_address, + chain_setting=chain_setting, + ) + if not json_file_validation_result: raise ValidationError(load_text(['err_verify_btec'])) click.pause(load_text(['msg_pause'])) diff --git a/staking_deposit/settings.py b/staking_deposit/settings.py index 524ad501..267aeb09 100644 --- a/staking_deposit/settings.py +++ b/staking_deposit/settings.py @@ -56,7 +56,9 @@ def get_chain_setting(chain_name: str = MAINNET) -> BaseChainSetting: return ALL_CHAINS[chain_name] -def get_devnet_chain_setting(network_name: str, genesis_fork_version: str, genesis_validator_root: str): +def get_devnet_chain_setting(network_name: str, + genesis_fork_version: str, + genesis_validator_root: str) -> BaseChainSetting: return BaseChainSetting( NETWORK_NAME=network_name, GENESIS_FORK_VERSION=decode_hex(genesis_fork_version), diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index d2e3d08d..52440575 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -13,10 +13,12 @@ from staking_deposit.exceptions import ValidationError from staking_deposit.utils.intl import load_text from staking_deposit.utils.ssz import ( - compute_deposit_domain, - compute_signing_root, + BLSToExecutionChange, DepositData, DepositMessage, + compute_bls_to_execution_change_domain, + compute_deposit_domain, + compute_signing_root, ) from staking_deposit.credentials import ( Credential, @@ -28,6 +30,7 @@ ETH1_ADDRESS_WITHDRAWAL_PREFIX, ) from staking_deposit.utils.crypto import SHA256 +from staking_deposit.settings import BaseChainSetting # @@ -135,7 +138,12 @@ def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: st # -def verify_bls_to_execution_change_json(filefolder: str, credentials: Sequence[Credential]) -> bool: +def verify_bls_to_execution_change_json(filefolder: str, + credentials: Sequence[Credential], + *, + input_validator_index: int, + input_execution_address: str, + chain_setting: BaseChainSetting) -> bool: """ Validate every BLSToExecutionChange found in the bls_to_execution_change JSON file folder. """ @@ -143,17 +151,51 @@ def verify_bls_to_execution_change_json(filefolder: str, credentials: Sequence[C btec_json = json.load(f) with click.progressbar(btec_json, label=load_text(['msg_bls_to_execution_change_verification']), show_percent=False, show_pos=True) as btecs: - return all([validate_bls_to_execution_change(btec, credential) for btec, credential in zip(btecs, credentials)]) + return all([ + validate_bls_to_execution_change( + btec, credential, + input_validator_index=input_validator_index, + input_execution_address=input_execution_address, + chain_setting=chain_setting) + for btec, credential in zip(btecs, credentials) + ]) return False -def validate_bls_to_execution_change(btec_dict: Dict[str, Any], credential: Credential) -> bool: - # TODO - # FIXME - ... - # validator_index = btec_dict['message']['validator_index'] - # from_bls_pubkey = BLSPubkey(decode_hex(btec_dict['message']['from_bls_pubkey'])) - # to_execution_address = decode_hex(btec_dict['message']['to_execution_address']) +def validate_bls_to_execution_change(btec_dict: Dict[str, Any], + credential: Credential, + *, + input_validator_index: int, + input_execution_address: str, + chain_setting: BaseChainSetting) -> bool: + validator_index = btec_dict['message']['validator_index'] + from_bls_pubkey = BLSPubkey(decode_hex(btec_dict['message']['from_bls_pubkey'])) + to_execution_address = decode_hex(btec_dict['message']['to_execution_address']) + signature = BLSSignature(decode_hex(btec_dict['signature'])) + + if validator_index != input_validator_index: + return False + if from_bls_pubkey != credential.withdrawal_pk: + return False + if ( + to_execution_address != credential.eth1_withdrawal_address + or to_execution_address != decode_hex(input_execution_address) + ): + return False + + message = BLSToExecutionChange( + validator_index=validator_index, + from_bls_pubkey=from_bls_pubkey, + to_execution_address=to_execution_address, + ) + domain = compute_bls_to_execution_change_domain( + fork_version=chain_setting.GENESIS_FORK_VERSION, + genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT, + ) + signing_root = compute_signing_root(message, domain) + + if not bls.Verify(BLSPubkey(credential.withdrawal_pk), signing_root, signature): + return False return True From 2b5c21890ed7158f85f17458230fc2c187cc3bb9 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 21:59:26 +0800 Subject: [PATCH 08/35] Add arguments description to README.md --- README.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/README.md b/README.md index eec5bd7b..03400998 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - [`new-mnemonic` Arguments](#new-mnemonic-arguments) - [`existing-mnemonic` Arguments](#existing-mnemonic-arguments) - [Successful message](#successful-message) + - [`generate-bls-to-execution-change` Arguments](#generate-bls-to-execution-change-arguments) - [Option 2. Build `deposit-cli` with native Python](#option-2-build-deposit-cli-with-native-python) - [Step 0. Python version checking](#step-0-python-version-checking) - [Step 1. Installation](#step-1-installation-1) @@ -128,6 +129,7 @@ The CLI offers different commands depending on what you want to do with the tool | ------- | ----------- | | `new-mnemonic` | (Recommended) This command is used to generate keystores with a new mnemonic. | | `existing-mnemonic` | This command is used to re-generate or derive new keys from your existing mnemonic. Use this command, if (i) you have already generated keys with this CLI before, (ii) you want to reuse your mnemonic that you know is secure that you generated elsewhere (reusing your eth1 mnemonic .etc), or (iii) you lost your keystores and need to recover your keys. | +| `` ###### `new-mnemonic` Arguments @@ -168,6 +170,22 @@ Success! Your keys can be found at: ``` +###### `generate-bls-to-execution-change` Arguments + +You can use `bls-to-execution-change --help` to see all arguments. Note that if there are missing arguments that the CLI needs, it will ask you for them. + +| Argument | Type | Description | +| -------- | -------- | -------- | +| `--bls_to_execution_changes_folder` | String. Pointing to `./bls_to_execution_changes` by default | The folder path for the `bls_to_execution_change-*` JSON file(s) | +| `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | +| `--mnemonic` | String. mnemonic split by space. | The mnemonic you used to create withdrawal credentials. | +| `--mnemonic_password` | Optional string. Empty by default. | The mnemonic password you used in your key generation. Note: It's not the keystore password. | +| `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. | +| `--validator_index` | Non-negative integer | The index number of your validator in beacon chain state. | +| `--bls_withdrawal_credentials` | String. | The old BLS withdrawal credentials of the given validator. It is for confirming you are using the correct keys. | +| `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. | +| `--devnet_chain_setting` | String. JSON string `'{"network_name": "", "genesis_fork_version": "", "genesis_validator_root": ""}'` | The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. | + #### Option 2. Build `deposit-cli` with native Python ##### Step 0. Python version checking @@ -228,6 +246,7 @@ See [here](#commands) See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments +See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments ###### Successful message See [here](#successful-message) @@ -295,6 +314,7 @@ See [here](#commands) See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments +See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments #### Option 4. Use Docker image @@ -378,6 +398,7 @@ See [here](#commands) See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments +See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments #### Option 2. Build `deposit-cli` with native Python @@ -440,6 +461,7 @@ See [here](#commands) See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments +See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments #### Option 3. Build `deposit-cli` with `virtualenv` @@ -504,6 +526,7 @@ See [here](#commands) See [here](#new-mnemonic-arguments) for `new-mnemonic` arguments See [here](#existing-mnemonic-arguments) for `existing-mnemonic` arguments +See [here](#generate-bls-to-execution-change-arguments) for `generate-bls-to-execution-change` arguments ## Development From 1f8bab5db1097e43704e223afd4ff3a82be2e39a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 22:13:21 +0800 Subject: [PATCH 09/35] Echo the folder path --- staking_deposit/cli/generate_bls_to_execution_change.py | 2 ++ .../intl/en/cli/generate_bls_to_execution_change.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 1675b3f5..3dae6528 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -183,4 +183,6 @@ def generate_bls_to_execution_change( if not json_file_validation_result: raise ValidationError(load_text(['err_verify_btec'])) + click.echo(load_text(['msg_creation_success']) + str(bls_to_execution_changes_folder)) + click.pause(load_text(['msg_pause'])) diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index 8a045b4a..87bfa7f1 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -33,7 +33,7 @@ "help": "The folder path for the keystore(s). Pointing to `./bls_to_execution_changes` by default." }, "msg_key_creation": "Creating your SignedBLSToExecutionChange.", - "msg_creation_success": "\nSuccess!\nYour SignedBLSToExecutionChange data can be found at: ", + "msg_creation_success": "\nSuccess!\nYour SignedBLSToExecutionChange JSON file can be found at: ", "msg_pause": "\n\nPress any key.", "err_verify_btec": "Failed to verify the bls_to_execution_change JSON files." } From f5e2803274a2dbd5cc9349e180b550d1a1046cad Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 16 Jan 2023 22:30:12 +0800 Subject: [PATCH 10/35] Arrange JSON fields by adding `metadata` --- staking_deposit/credentials.py | 12 ++++++++---- staking_deposit/utils/validation.py | 5 ++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index 2dfe18df..a46632df 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -193,10 +193,14 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by result_dict.update({'message': message}) result_dict.update({'signature': '0x' + signed_bls_to_execution_change.signature.hex()}) - # meta - result_dict.update({'network_name': self.chain_setting.NETWORK_NAME}) - result_dict.update({'genesis_validators_root': '0x' + self.chain_setting.GENESIS_VALIDATORS_ROOT.hex()}) - result_dict.update({'deposit_cli_version': DEPOSIT_CLI_VERSION}) + # metadata + metadata: Dict[str, Any] = { + 'network_name': self.chain_setting.NETWORK_NAME, + 'genesis_validators_root': '0x' + self.chain_setting.GENESIS_VALIDATORS_ROOT.hex(), + 'deposit_cli_version': DEPOSIT_CLI_VERSION, + } + + result_dict.update({'metadata': metadata}) return result_dict diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 52440575..a0650e06 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -172,6 +172,7 @@ def validate_bls_to_execution_change(btec_dict: Dict[str, Any], from_bls_pubkey = BLSPubkey(decode_hex(btec_dict['message']['from_bls_pubkey'])) to_execution_address = decode_hex(btec_dict['message']['to_execution_address']) signature = BLSSignature(decode_hex(btec_dict['signature'])) + genesis_validators_root = decode_hex(btec_dict['metadata']['genesis_validators_root']) if validator_index != input_validator_index: return False @@ -182,6 +183,8 @@ def validate_bls_to_execution_change(btec_dict: Dict[str, Any], or to_execution_address != decode_hex(input_execution_address) ): return False + if genesis_validators_root != chain_setting.GENESIS_VALIDATORS_ROOT: + return False message = BLSToExecutionChange( validator_index=validator_index, @@ -190,7 +193,7 @@ def validate_bls_to_execution_change(btec_dict: Dict[str, Any], ) domain = compute_bls_to_execution_change_domain( fork_version=chain_setting.GENESIS_FORK_VERSION, - genesis_validators_root=chain_setting.GENESIS_VALIDATORS_ROOT, + genesis_validators_root=genesis_validators_root, ) signing_root = compute_signing_root(message, domain) From 245ee7b63a1a24ed1e3f9600e8ce02d45d2e9e29 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sun, 22 Jan 2023 13:33:53 +0100 Subject: [PATCH 11/35] Normalize input `bls_withdrawal_credentials` and add `test_existing_mnemonic_bls_withdrawal_interactive` --- staking_deposit/utils/validation.py | 11 +++- tests/test_cli/helpers.py | 14 +++++ .../test_generate_bls_to_execution_change.py | 53 +++++++++++++++---- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index a0650e06..8d447f82 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -203,8 +203,17 @@ def validate_bls_to_execution_change(btec_dict: Dict[str, Any], return True -def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> bytes: +def normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials: str) -> bytes: + if bls_withdrawal_credentials.startswith('0x'): + bls_withdrawal_credentials = bls_withdrawal_credentials[2:] + bls_withdrawal_credentials_bytes = bytes.fromhex(bls_withdrawal_credentials) + return bls_withdrawal_credentials_bytes + + +def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> bytes: + bls_withdrawal_credentials_bytes = normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials) + try: assert len(bls_withdrawal_credentials_bytes) == 32 assert bls_withdrawal_credentials_bytes[:1] == BLS_WITHDRAWAL_PREFIX diff --git a/tests/test_cli/helpers.py b/tests/test_cli/helpers.py index 8d5aa8b9..4342791b 100644 --- a/tests/test_cli/helpers.py +++ b/tests/test_cli/helpers.py @@ -35,3 +35,17 @@ def get_uuid(key_file: str) -> str: def get_permissions(path: str, file_name: str) -> str: return oct(os.stat(os.path.join(path, file_name)).st_mode & 0o777) + + +def verify_file_permission(os_ref, folder_path, files): + if os_ref.name == 'posix': + for file_name in files: + assert get_permissions(folder_path, file_name) == '0o440' + + +def prepare_testing_folder(os_ref, testing_folder_name='TESTING_TEMP_FOLDER'): + my_folder_path = os_ref.path.join(os_ref.getcwd(), testing_folder_name) + clean_btec_folder(my_folder_path) + if not os_ref.path.exists(my_folder_path): + os_ref.mkdir(my_folder_path) + return my_folder_path diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index 42e9a1c0..b83af59c 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -6,19 +6,17 @@ from staking_deposit.utils.constants import DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME from .helpers import ( clean_btec_folder, - get_permissions, + prepare_testing_folder, + verify_file_permission, ) def test_existing_mnemonic_bls_withdrawal() -> None: # Prepare folder - my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') - clean_btec_folder(my_folder_path) - if not os.path.exists(my_folder_path): - os.mkdir(my_folder_path) + my_folder_path = prepare_testing_folder(os) runner = CliRunner() - inputs = ['0'] # confirm `validator_start_index` + inputs = [] data = '\n'.join(inputs) arguments = [ '--language', 'english', @@ -26,7 +24,7 @@ def test_existing_mnemonic_bls_withdrawal() -> None: '--bls_to_execution_changes_folder', my_folder_path, '--chain', 'mainnet', '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 - '--bls_withdrawal_credentials', '00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '--bls_withdrawal_credentials', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', '--validator_start_index', '0', '--validator_index', '1', '--execution_address', '3434343434343434343434343434343434343434', @@ -42,9 +40,44 @@ def test_existing_mnemonic_bls_withdrawal() -> None: assert len(set(btec_files)) == 1 # Verify file permissions - if os.name == 'posix': - for file_name in btec_files: - assert get_permissions(bls_to_execution_changes_folder_path, file_name) == '0o440' + verify_file_permission(os, folder_path=bls_to_execution_changes_folder_path, files=btec_files) + + # Clean up + clean_btec_folder(my_folder_path) + + +def test_existing_mnemonic_bls_withdrawal_interactive() -> None: + # Prepare folder + my_folder_path = prepare_testing_folder(os) + + runner = CliRunner() + inputs = [ + 'mainnet', # network/chain + 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 + '0', # validator_start_index + '1', # validator_index + '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '0x3434343434343434343434343434343434343434', + + ] + data = '\n'.join(inputs) + arguments = [ + '--language', 'english', + 'generate-bls-to-execution-change', + '--bls_to_execution_changes_folder', my_folder_path, + ] + result = runner.invoke(cli, arguments, input=data) + assert result.exit_code == 0 + + # Check files + bls_to_execution_changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME) + _, _, btec_files = next(os.walk(bls_to_execution_changes_folder_path)) + + # TODO verify file content + assert len(set(btec_files)) == 1 + + # Verify file permissions + verify_file_permission(os, folder_path=bls_to_execution_changes_folder_path, files=btec_files) # Clean up clean_btec_folder(my_folder_path) From 3b7e1446e2065e4ed920a05ceb54dc51a30ffb34 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 23 Jan 2023 15:07:55 +0100 Subject: [PATCH 12/35] Generate multiple changes --- .../cli/generate_bls_to_execution_change.py | 49 +++++++++++-------- staking_deposit/credentials.py | 7 +-- .../cli/generate_bls_to_execution_change.json | 15 +++--- staking_deposit/utils/validation.py | 21 +++++++- .../test_generate_bls_to_execution_change.py | 4 +- 5 files changed, 59 insertions(+), 37 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 3dae6528..a531d902 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -3,6 +3,7 @@ import json from typing import ( Any, + Sequence, ) from eth_typing import HexAddress @@ -11,11 +12,12 @@ CredentialList, ) from staking_deposit.utils.validation import ( - validate_bls_withdrawal_credentials, + validate_bls_withdrawal_credentials_list, validate_bls_withdrawal_credentials_matching, validate_eth1_withdrawal_address, validate_int_range, verify_bls_to_execution_change_json, + validate_validator_indices, ) from staking_deposit.utils.constants import ( DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME, @@ -87,22 +89,24 @@ def get_password(text: str) -> str: ) @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), + lambda validator_indices: validate_validator_indices(validator_indices), + lambda: load_text(['arg_validator_indices', '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), + help=lambda: load_text(['arg_validator_indices', 'help'], func=FUNC_NAME), + param_decls='--validator_indices', + prompt=lambda: load_text(['arg_validator_indices', 'prompt'], func=FUNC_NAME), ) @jit_option( callback=captive_prompt_callback( - lambda bls_withdrawal_credentials: validate_bls_withdrawal_credentials(bls_withdrawal_credentials), - lambda: load_text(['arg_bls_withdrawal_credentials', 'prompt'], func=FUNC_NAME), + lambda bls_withdrawal_credentials_list: + validate_bls_withdrawal_credentials_list(bls_withdrawal_credentials_list), + lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME), ), - help=lambda: load_text(['arg_bls_withdrawal_credentials', 'help'], func=FUNC_NAME), - param_decls='--bls_withdrawal_credentials', - prompt=lambda: load_text(['arg_bls_withdrawal_credentials', 'prompt'], func=FUNC_NAME), + help=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'help'], func=FUNC_NAME), + param_decls='--bls_withdrawal_credentials_list', + prompt=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME), ) +# TODO: retype confirmation @jit_option( callback=captive_prompt_callback( lambda address: validate_eth1_withdrawal_address(None, None, address), @@ -126,8 +130,8 @@ def generate_bls_to_execution_change( mnemonic: str, mnemonic_password: str, validator_start_index: int, - validator_index: int, - bls_withdrawal_credentials: bytes, + validator_indices: Sequence[int], + bls_withdrawal_credentials_list: Sequence[bytes], execution_address: HexAddress, devnet_chain_setting: str, **kwargs: Any) -> None: @@ -151,8 +155,13 @@ def generate_bls_to_execution_change( genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'], ) - # TODO: generate multiple? - num_validators = 1 + if len(validator_indices) != len(bls_withdrawal_credentials_list): + raise ValueError( + "The size of `validator_indinces` (%d) should be as same as `bls_withdrawal_credentials_list` (%d)." + % (len(validator_indices), len(bls_withdrawal_credentials_list)) + ) + + num_validators = len(validator_indices) amounts = [MAX_DEPOSIT_AMOUNT] * num_validators credentials = CredentialList.from_mnemonic( @@ -165,18 +174,16 @@ def generate_bls_to_execution_change( hex_eth1_withdrawal_address=execution_address, ) - if len(credentials.credentials) != 1: - raise ValueError(f"It should only generate one credential, but get {len(credentials.credentials)}.") - # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated - validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials, credentials.credentials[0]) + for i, credential in enumerate(credentials.credentials): + validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials_list[i], credential) - btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_index) + btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_indices) json_file_validation_result = verify_bls_to_execution_change_json( btec_file, credentials.credentials, - input_validator_index=validator_index, + input_validator_indices=validator_indices, input_execution_address=execution_address, chain_setting=chain_setting, ) diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index a46632df..5455d2a1 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -3,7 +3,7 @@ from enum import Enum import time import json -from typing import Dict, List, Optional, Any +from typing import Dict, List, Optional, Any, Sequence from eth_typing import Address, HexAddress from eth_utils import to_canonical_address @@ -256,10 +256,11 @@ def verify_keystores(self, keystore_filefolders: List[str], password: str) -> bo return all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) for credential, filefolder in items) - def export_bls_to_execution_change_json(self, folder: str, validator_index: int) -> str: + def export_bls_to_execution_change_json(self, folder: str, validator_indinces: Sequence[int]) -> str: with click.progressbar(self.credentials, label=load_text(['msg_bls_to_execution_change_creation']), show_percent=False, show_pos=True) as credentials: - bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_index) for cred in credentials] + bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_indinces[i]) + for i, cred in enumerate(credentials)] filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time()) with open(filefolder, 'w') as f: diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index 87bfa7f1..569647dd 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -5,13 +5,13 @@ "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials", "confirm": "Repeat your execution address for confirmation." }, - "arg_validator_index": { - "help": "The validator index of the certain validator", - "prompt": "Please enter the validator index of your validator" + "arg_validator_indices": { + "help": "A list of the validator indices of the certain validator(s)", + "prompt": "Please enter a list of the validator indices of your validator(s). Split multiple items with whitespaces." }, - "arg_bls_withdrawal_credentials": { - "help": "The 32-byte old BLS withdrawal credentials of the certain validator", - "prompt": "Please enter the old BLS withdrawal credentials of your validator" + "arg_bls_withdrawal_credentials_list": { + "help": "A list of 32-byte old BLS withdrawal credentials of the certain validator(s)", + "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces." }, "arg_validator_start_index": { "help": "The index (key number) of the signging key you want to use with this mnemonic", @@ -26,9 +26,6 @@ "help": "The fork name of the fork you want to signing the message with.", "prompt": "Please choose the fork name of the fork you want to signing the message with." }, - "arg_validator_keys_folder": { - "help": "The folder path for the keystore(s). Pointing to `./validator_keys` by default." - }, "arg_bls_to_execution_changes_folder": { "help": "The folder path for the keystore(s). Pointing to `./bls_to_execution_changes` by default." }, diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 8d447f82..a279cbed 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -1,5 +1,6 @@ import click import json +import re from typing import Any, Dict, Sequence from eth_typing import ( @@ -124,6 +125,7 @@ def validate_int_range(num: Any, low: int, high: int) -> int: def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: str) -> HexAddress: + # TODO: checksum if address is None: return None if not is_hex_address(address): @@ -141,7 +143,7 @@ def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: st def verify_bls_to_execution_change_json(filefolder: str, credentials: Sequence[Credential], *, - input_validator_index: int, + input_validator_indices: Sequence[int], input_execution_address: str, chain_setting: BaseChainSetting) -> bool: """ @@ -157,7 +159,7 @@ def verify_bls_to_execution_change_json(filefolder: str, input_validator_index=input_validator_index, input_execution_address=input_execution_address, chain_setting=chain_setting) - for btec, credential in zip(btecs, credentials) + for btec, credential, input_validator_index in zip(btecs, credentials, input_validator_indices) ]) return False @@ -223,6 +225,21 @@ def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> byte return bls_withdrawal_credentials_bytes +def normalize_input_list(input: str) -> Sequence[str]: + return re.split(r'; |, | |,|;', input) + + +def validate_bls_withdrawal_credentials_list(input_bls_withdrawal_credentials_list: str) -> Sequence[bytes]: + bls_withdrawal_credentials_list = normalize_input_list(input_bls_withdrawal_credentials_list) + return [validate_bls_withdrawal_credentials(cred) for cred in bls_withdrawal_credentials_list] + + +def validate_validator_indices(input_validator_indices: str) -> Sequence[int]: + + normalized_list = normalize_input_list(input_validator_indices) + return [validate_int_range(int(index), 0, 2**32) for index in normalized_list] + + def validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials: bytes, credential: Credential) -> None: if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: raise ValidationError(load_text(['err_not_matching'])) diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index b83af59c..439f6e16 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -24,9 +24,9 @@ def test_existing_mnemonic_bls_withdrawal() -> None: '--bls_to_execution_changes_folder', my_folder_path, '--chain', 'mainnet', '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 - '--bls_withdrawal_credentials', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', '--validator_start_index', '0', - '--validator_index', '1', + '--validator_indices', '1', '--execution_address', '3434343434343434343434343434343434343434', ] result = runner.invoke(cli, arguments, input=data) From 676279e5b3b3d6e7a923044dd466208f5d885898 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 23 Jan 2023 15:29:44 +0100 Subject: [PATCH 13/35] Add checksum address validation --- staking_deposit/intl/en/utils/validation.json | 3 ++- staking_deposit/intl/tr/utils/validation.json | 2 +- staking_deposit/utils/validation.py | 7 ++++--- tests/test_cli/test_existing_menmonic.py | 2 +- tests/test_cli/test_generate_bls_to_execution_change.py | 2 +- tests/test_cli/test_new_mnemonic.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 78faa46c..1a0eeb4e 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -13,7 +13,8 @@ }, "validate_eth1_withdrawal_address": { "err_invalid_ECDSA_hex_addr": "The given Eth1 address is not in hexadecimal encoded form.", - "msg_ECDSA_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" + "err_invalid_ECDSA_hex_addr_checksum": "The given Eth1 address is not in checksum form.", + "msg_ECDSA_hex_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" }, "validate_bls_withdrawal_credentials": { "err_not_bls_form": "The given withdrawal credentials is not in BLS_WITHDRAWAL_PREFIX form." diff --git a/staking_deposit/intl/tr/utils/validation.json b/staking_deposit/intl/tr/utils/validation.json index bdbf3d60..2c73e99c 100644 --- a/staking_deposit/intl/tr/utils/validation.json +++ b/staking_deposit/intl/tr/utils/validation.json @@ -13,6 +13,6 @@ }, "validate_eth1_withdrawal_address": { "err_invalid_ECDSA_hex_addr": "Girilen Eth1 adresi onaltılık sistemde kodlanmamıştır.", - "msg_ECDSA_addr_withdrawal": "**[Uyarı] bir Eth1 adresini varlık çekme adresi olarak giriyorsunuz. Lütfen bu adresin kontrolünün sizde olduğundan emin olun.**" + "msg_ECDSA_hex_addr_withdrawal": "**[Uyarı] bir Eth1 adresini varlık çekme adresi olarak giriyorsunuz. Lütfen bu adresin kontrolünün sizde olduğundan emin olun.**" } } diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index a279cbed..49e41700 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -8,7 +8,7 @@ BLSSignature, HexAddress, ) -from eth_utils import is_hex_address, to_normalized_address, decode_hex +from eth_utils import is_hex_address, is_checksum_address, to_normalized_address, decode_hex from py_ecc.bls import G2ProofOfPossession as bls from staking_deposit.exceptions import ValidationError @@ -125,14 +125,15 @@ def validate_int_range(num: Any, low: int, high: int) -> int: def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: str) -> HexAddress: - # TODO: checksum if address is None: return None if not is_hex_address(address): raise ValueError(load_text(['err_invalid_ECDSA_hex_addr'])) + if not is_checksum_address(address): + raise ValueError(load_text(['err_invalid_ECDSA_hex_addr_checksum'])) normalized_address = to_normalized_address(address) - click.echo('\n%s\n' % load_text(['msg_ECDSA_addr_withdrawal'])) + click.echo('\n%s\n' % load_text(['msg_ECDSA_hex_addr_withdrawal'])) return normalized_address # diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index 4e62d65d..9570fe99 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -67,7 +67,7 @@ def test_existing_mnemonic_eth1_address_withdrawal() -> None: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '2', '2', '5', 'mainnet', 'MyPassword', 'MyPassword'] data = '\n'.join(inputs) - eth1_withdrawal_address = '0x00000000219ab540356cbb839cbe05303d7705fa' + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' arguments = [ '--language', 'english', 'existing-mnemonic', diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index 439f6e16..3d61e27b 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -27,7 +27,7 @@ def test_existing_mnemonic_bls_withdrawal() -> None: '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', '--validator_start_index', '0', '--validator_indices', '1', - '--execution_address', '3434343434343434343434343434343434343434', + '--execution_address', '0x3434343434343434343434343434343434343434', ] result = runner.invoke(cli, arguments, input=data) assert result.exit_code == 0 diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index fb70a255..6a7bb9b4 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -72,7 +72,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] data = '\n'.join(inputs) - eth1_withdrawal_address = '0x00000000219ab540356cbb839cbe05303d7705fa' + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' arguments = [ '--language', 'english', 'new-mnemonic', From 75e8ea0513c7980216257e1e645298252cbf7222 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 23 Jan 2023 15:45:46 +0100 Subject: [PATCH 14/35] Add test case of multiple validator indices --- .../cli/generate_bls_to_execution_change.json | 4 +-- .../test_generate_bls_to_execution_change.py | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index 569647dd..64890867 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -7,11 +7,11 @@ }, "arg_validator_indices": { "help": "A list of the validator indices of the certain validator(s)", - "prompt": "Please enter a list of the validator indices of your validator(s). Split multiple items with whitespaces." + "prompt": "Please enter a list of the validator indices of your validator(s). Split multiple items with whitespaces or commas." }, "arg_bls_withdrawal_credentials_list": { "help": "A list of 32-byte old BLS withdrawal credentials of the certain validator(s)", - "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces." + "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas." }, "arg_validator_start_index": { "help": "The index (key number) of the signging key you want to use with this mnemonic", diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index 3d61e27b..94e7048b 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -81,3 +81,38 @@ def test_existing_mnemonic_bls_withdrawal_interactive() -> None: # Clean up clean_btec_folder(my_folder_path) + + +def test_existing_mnemonic_bls_withdrawal_multiple() -> None: + # Prepare folder + my_folder_path = prepare_testing_folder(os) + + runner = CliRunner() + inputs = [] + data = '\n'.join(inputs) + arguments = [ + '--language', 'english', + 'generate-bls-to-execution-change', + '--bls_to_execution_changes_folder', my_folder_path, + '--chain', 'mainnet', + '--mnemonic', 'sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry', # noqa: E501 + '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de, 0x00a75d83f169fa6923f3dd78386d9608fab710d8f7fcf71ba9985893675d5382', # noqa: E501 + '--validator_start_index', '0', + '--validator_indices', '1,2', + '--execution_address', '0x3434343434343434343434343434343434343434', + ] + result = runner.invoke(cli, arguments, input=data) + assert result.exit_code == 0 + + # Check files + bls_to_execution_changes_folder_path = os.path.join(my_folder_path, DEFAULT_BLS_TO_EXECUTION_CHANGES_FOLDER_NAME) + _, _, btec_files = next(os.walk(bls_to_execution_changes_folder_path)) + + # TODO verify file content + assert len(set(btec_files)) == 1 + + # Verify file permissions + verify_file_permission(os, folder_path=bls_to_execution_changes_folder_path, files=btec_files) + + # Clean up + clean_btec_folder(my_folder_path) From 0be70aa42fc1399c51a54ecd1a816cc1e17895c4 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 23 Jan 2023 16:35:34 +0100 Subject: [PATCH 15/35] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 03400998..96f5e73d 100644 --- a/README.md +++ b/README.md @@ -181,8 +181,8 @@ You can use `bls-to-execution-change --help` to see all arguments. Note that if | `--mnemonic` | String. mnemonic split by space. | The mnemonic you used to create withdrawal credentials. | | `--mnemonic_password` | Optional string. Empty by default. | The mnemonic password you used in your key generation. Note: It's not the keystore password. | | `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. | -| `--validator_index` | Non-negative integer | The index number of your validator in beacon chain state. | -| `--bls_withdrawal_credentials` | String. | The old BLS withdrawal credentials of the given validator. It is for confirming you are using the correct keys. | +| `--validator_indices` | String of integer(s) | A list of the index number of your validator(s) in beacon chain state. Split multiple items with whitespaces or commas. | +| `--bls_withdrawal_credentials_list` | String of hexstring(s). | A list of the old BLS withdrawal credentials of the given validator(s). It is for confirming you are using the correct keys. Split multiple items with whitespaces or commas. | | `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. | | `--devnet_chain_setting` | String. JSON string `'{"network_name": "", "genesis_fork_version": "", "genesis_validator_root": ""}'` | The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. | From 962ce8c204c50dfe22ca3293c9cdf865eeff51ee Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Thu, 26 Jan 2023 14:17:23 +0100 Subject: [PATCH 16/35] Add retype confirmation and fix typo --- staking_deposit/cli/generate_bls_to_execution_change.py | 5 +++-- staking_deposit/credentials.py | 6 +++--- .../intl/en/cli/generate_bls_to_execution_change.json | 9 +++++---- staking_deposit/utils/validation.py | 2 +- tests/test_cli/test_generate_bls_to_execution_change.py | 3 +++ 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index a531d902..73387867 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -106,11 +106,12 @@ def get_password(text: str) -> str: param_decls='--bls_withdrawal_credentials_list', prompt=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME), ) -# TODO: retype confirmation @jit_option( callback=captive_prompt_callback( lambda address: validate_eth1_withdrawal_address(None, None, address), lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME), + lambda: load_text(['arg_execution_address', 'confirm'], func=FUNC_NAME), + lambda: load_text(['arg_execution_address', 'mismatch'], func=FUNC_NAME), ), help=lambda: load_text(['arg_execution_address', 'help'], func=FUNC_NAME), param_decls='--execution_address', @@ -157,7 +158,7 @@ def generate_bls_to_execution_change( if len(validator_indices) != len(bls_withdrawal_credentials_list): raise ValueError( - "The size of `validator_indinces` (%d) should be as same as `bls_withdrawal_credentials_list` (%d)." + "The size of `validator_indices` (%d) should be as same as `bls_withdrawal_credentials_list` (%d)." % (len(validator_indices), len(bls_withdrawal_credentials_list)) ) diff --git a/staking_deposit/credentials.py b/staking_deposit/credentials.py index 5455d2a1..9d70ead8 100644 --- a/staking_deposit/credentials.py +++ b/staking_deposit/credentials.py @@ -186,7 +186,7 @@ def get_bls_to_execution_change_dict(self, validator_index: int) -> Dict[str, by result_dict: Dict[str, Any] = {} signed_bls_to_execution_change = self.get_bls_to_execution_change(validator_index) message = { - 'validator_index': signed_bls_to_execution_change.message.validator_index, + 'validator_index': str(signed_bls_to_execution_change.message.validator_index), 'from_bls_pubkey': '0x' + signed_bls_to_execution_change.message.from_bls_pubkey.hex(), 'to_execution_address': '0x' + signed_bls_to_execution_change.message.to_execution_address.hex(), } @@ -256,10 +256,10 @@ def verify_keystores(self, keystore_filefolders: List[str], password: str) -> bo return all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) for credential, filefolder in items) - def export_bls_to_execution_change_json(self, folder: str, validator_indinces: Sequence[int]) -> str: + def export_bls_to_execution_change_json(self, folder: str, validator_indices: Sequence[int]) -> str: with click.progressbar(self.credentials, label=load_text(['msg_bls_to_execution_change_creation']), show_percent=False, show_pos=True) as credentials: - bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_indinces[i]) + bls_to_execution_changes = [cred.get_bls_to_execution_change_dict(validator_indices[i]) for i, cred in enumerate(credentials)] filefolder = os.path.join(folder, 'bls_to_execution_change-%i.json' % time.time()) diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index 64890867..bf6da18f 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -2,8 +2,9 @@ "generate_bls_to_execution_change": { "arg_execution_address": { "help": "The 20-byte (Eth1) execution address that will be used in withdrawal", - "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials", - "confirm": "Repeat your execution address for confirmation." + "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials. Note that you CANNOT change it once you have set it on chain.", + "confirm": "Repeat your execution address for confirmation.", + "mismatch": "Error: the two entered values do not match. Please type again." }, "arg_validator_indices": { "help": "A list of the validator indices of the certain validator(s)", @@ -14,8 +15,8 @@ "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas." }, "arg_validator_start_index": { - "help": "The index (key number) of the signging key you want to use with this mnemonic", - "prompt": "Please enter the index (key number) of the signging key you want to use with this mnemonic.", + "help": "The index (key number) of the signing key you want to use with this mnemonic", + "prompt": "Please enter the index (key number) of the signing key you want to use with this mnemonic.", "confirm": "Please repeat the index to confirm" }, "arg_chain": { diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 49e41700..13f72765 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -171,7 +171,7 @@ def validate_bls_to_execution_change(btec_dict: Dict[str, Any], input_validator_index: int, input_execution_address: str, chain_setting: BaseChainSetting) -> bool: - validator_index = btec_dict['message']['validator_index'] + validator_index = int(btec_dict['message']['validator_index']) from_bls_pubkey = BLSPubkey(decode_hex(btec_dict['message']['from_bls_pubkey'])) to_execution_address = decode_hex(btec_dict['message']['to_execution_address']) signature = BLSSignature(decode_hex(btec_dict['signature'])) diff --git a/tests/test_cli/test_generate_bls_to_execution_change.py b/tests/test_cli/test_generate_bls_to_execution_change.py index 94e7048b..bdcb22cf 100644 --- a/tests/test_cli/test_generate_bls_to_execution_change.py +++ b/tests/test_cli/test_generate_bls_to_execution_change.py @@ -20,6 +20,7 @@ def test_existing_mnemonic_bls_withdrawal() -> None: data = '\n'.join(inputs) arguments = [ '--language', 'english', + '--non_interactive', 'generate-bls-to-execution-change', '--bls_to_execution_changes_folder', my_folder_path, '--chain', 'mainnet', @@ -58,6 +59,7 @@ def test_existing_mnemonic_bls_withdrawal_interactive() -> None: '1', # validator_index '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', '0x3434343434343434343434343434343434343434', + '0x3434343434343434343434343434343434343434', ] data = '\n'.join(inputs) @@ -92,6 +94,7 @@ def test_existing_mnemonic_bls_withdrawal_multiple() -> None: data = '\n'.join(inputs) arguments = [ '--language', 'english', + '--non_interactive', 'generate-bls-to-execution-change', '--bls_to_execution_changes_folder', my_folder_path, '--chain', 'mainnet', From 1912868eb4c0863f2b4636c16bacdacf269d8159 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 27 Jan 2023 11:50:19 +0100 Subject: [PATCH 17/35] Remove shutdown testnets and fill Sepolia and Goerli `genesis_validators_root` --- staking_deposit/settings.py | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/staking_deposit/settings.py b/staking_deposit/settings.py index 267aeb09..fdcb962a 100644 --- a/staking_deposit/settings.py +++ b/staking_deposit/settings.py @@ -11,43 +11,28 @@ class BaseChainSetting(NamedTuple): MAINNET = 'mainnet' -ROPSTEN = 'ropsten' GOERLI = 'goerli' PRATER = 'prater' -KILN = 'kiln' SEPOLIA = 'sepolia' -# FIXME: use the real testnet genesis_validators_root -GENESIS_VALIDATORS_ROOT_STUB = bytes.fromhex('4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95') - # Mainnet setting MainnetSetting = BaseChainSetting( NETWORK_NAME=MAINNET, GENESIS_FORK_VERSION=bytes.fromhex('00000000'), GENESIS_VALIDATORS_ROOT=bytes.fromhex('4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95')) -# Ropsten setting -RopstenSetting = BaseChainSetting( - NETWORK_NAME=ROPSTEN, GENESIS_FORK_VERSION=bytes.fromhex('80000069'), - GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) # Goerli setting GoerliSetting = BaseChainSetting( NETWORK_NAME=GOERLI, GENESIS_FORK_VERSION=bytes.fromhex('00001020'), - GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) -# Merge Testnet (spec v1.1.9) -KilnSetting = BaseChainSetting( - NETWORK_NAME=KILN, GENESIS_FORK_VERSION=bytes.fromhex('70000069'), - GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) + GENESIS_VALIDATORS_ROOT=bytes.fromhex('043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb')) # Sepolia setting SepoliaSetting = BaseChainSetting( NETWORK_NAME=SEPOLIA, GENESIS_FORK_VERSION=bytes.fromhex('90000069'), - GENESIS_VALIDATORS_ROOT=GENESIS_VALIDATORS_ROOT_STUB) + GENESIS_VALIDATORS_ROOT=bytes.fromhex('d8ea171f3c94aea21ebc42a1ed61052acf3f9209c00e4efbaaddac09ed9b8078')) ALL_CHAINS: Dict[str, BaseChainSetting] = { MAINNET: MainnetSetting, - ROPSTEN: RopstenSetting, GOERLI: GoerliSetting, PRATER: GoerliSetting, # Prater is the old name of the Prater/Goerli testnet - KILN: KilnSetting, SEPOLIA: SepoliaSetting, } From 33f847b34971ead9e091857e3195218d67b09fcc Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 10 Feb 2023 23:25:34 +0800 Subject: [PATCH 18/35] Update README.md Co-authored-by: Paul Wackerow <54227730+wackerow@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 96f5e73d..8b7398b2 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ You can use `bls-to-execution-change --help` to see all arguments. Note that if | `--mnemonic` | String. mnemonic split by space. | The mnemonic you used to create withdrawal credentials. | | `--mnemonic_password` | Optional string. Empty by default. | The mnemonic password you used in your key generation. Note: It's not the keystore password. | | `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. | -| `--validator_indices` | String of integer(s) | A list of the index number of your validator(s) in beacon chain state. Split multiple items with whitespaces or commas. | +| `--validator_indices` | String of integer(s) | A list of the chosen validator index number(s) as identified on the beacon chain. Split multiple items with whitespaces or commas. | | `--bls_withdrawal_credentials_list` | String of hexstring(s). | A list of the old BLS withdrawal credentials of the given validator(s). It is for confirming you are using the correct keys. Split multiple items with whitespaces or commas. | | `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. | | `--devnet_chain_setting` | String. JSON string `'{"network_name": "", "genesis_fork_version": "", "genesis_validator_root": ""}'` | The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. | From 7df39ba95b05b22eba071db1666c7620f212fb52 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 10 Feb 2023 23:27:41 +0800 Subject: [PATCH 19/35] Apply @james-prysm's suggestions --- .../cli/generate_bls_to_execution_change.py | 4 +++- staking_deposit/deposit.py | 1 + .../en/cli/generate_bls_to_execution_change.json | 3 +++ staking_deposit/intl/en/utils/validation.json | 1 + staking_deposit/utils/validation.py | 15 +++++++++++++-- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 73387867..5599bea5 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -52,7 +52,9 @@ def get_password(text: str) -> str: FUNC_NAME = 'generate_bls_to_execution_change' -@click.command() +@click.command( + help=load_text(['arg_generate_bls_to_execution_change', 'help'], func=FUNC_NAME), +) @jit_option( default=os.getcwd(), help=lambda: load_text(['arg_bls_to_execution_changes_folder', 'help'], func=FUNC_NAME), diff --git a/staking_deposit/deposit.py b/staking_deposit/deposit.py index 9bb3da21..421e1c0d 100644 --- a/staking_deposit/deposit.py +++ b/staking_deposit/deposit.py @@ -59,4 +59,5 @@ def cli(ctx: click.Context, language: str, non_interactive: bool) -> None: if __name__ == '__main__': check_python_version() + print('\n***Using the tool on an offline and secure device is highly recommended to keep your mnemonic safe.***\n') cli() diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index bf6da18f..c59b9291 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -1,5 +1,8 @@ { "generate_bls_to_execution_change": { + "arg_generate_bls_to_execution_change" :{ + "help": "Generating the SignedBLSToExecutionChange data to enable withdrawals on Ethereum Beacon Chain." + }, "arg_execution_address": { "help": "The 20-byte (Eth1) execution address that will be used in withdrawal", "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials. Note that you CANNOT change it once you have set it on chain.", diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 1a0eeb4e..caf04e05 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -17,6 +17,7 @@ "msg_ECDSA_hex_addr_withdrawal": "**[Warning] you are setting an Eth1 address as your withdrawal address. Please ensure that you have control over this address.**" }, "validate_bls_withdrawal_credentials": { + "err_is_already_eth1_form": "The given withdrawal credentials is already in ETH1_ADDRESS_WITHDRAWAL_PREFIX form. Have you already set the EL (eth1) withdrawal addresss?", "err_not_bls_form": "The given withdrawal credentials is not in BLS_WITHDRAWAL_PREFIX form." }, "validate_bls_withdrawal_credentials_matching": { diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 13f72765..a166816f 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -214,14 +214,25 @@ def normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials: st return bls_withdrawal_credentials_bytes +def is_eth1_address_withdrawal_credentials(withdrawal_credentials: bytes) -> bool: + return ( + len(withdrawal_credentials) == 32 + and withdrawal_credentials[:1] == ETH1_ADDRESS_WITHDRAWAL_PREFIX + and withdrawal_credentials[1:12] == b'\x00' * 11 + ) + + def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> bytes: bls_withdrawal_credentials_bytes = normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials) + if is_eth1_address_withdrawal_credentials(bls_withdrawal_credentials_bytes): + raise ValidationError('\n' + load_text(['err_is_already_eth1_form']) + '\n') + try: assert len(bls_withdrawal_credentials_bytes) == 32 assert bls_withdrawal_credentials_bytes[:1] == BLS_WITHDRAWAL_PREFIX except (ValueError, AssertionError): - raise ValidationError(load_text(['err_not_bls_form'])) + raise ValidationError('\n' + load_text(['err_not_bls_form']) + '\n') return bls_withdrawal_credentials_bytes @@ -243,4 +254,4 @@ def validate_validator_indices(input_validator_indices: str) -> Sequence[int]: def validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials: bytes, credential: Credential) -> None: if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: - raise ValidationError(load_text(['err_not_matching'])) + raise ValidationError('\n' + load_text(['err_not_matching']) + '\n') From 27af99fa215bb60da8b68961792325d29eca1d8c Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Fri, 10 Feb 2023 23:30:35 +0800 Subject: [PATCH 20/35] Update param description --- README.md | 7 +++---- .../intl/en/cli/generate_bls_to_execution_change.json | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8b7398b2..50d68083 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,6 @@ The CLI offers different commands depending on what you want to do with the tool | ------- | ----------- | | `new-mnemonic` | (Recommended) This command is used to generate keystores with a new mnemonic. | | `existing-mnemonic` | This command is used to re-generate or derive new keys from your existing mnemonic. Use this command, if (i) you have already generated keys with this CLI before, (ii) you want to reuse your mnemonic that you know is secure that you generated elsewhere (reusing your eth1 mnemonic .etc), or (iii) you lost your keystores and need to recover your keys. | -| `` ###### `new-mnemonic` Arguments @@ -141,7 +140,7 @@ You can use `new-mnemonic --help` to see all arguments. Note that if there are m | `--mnemonic_language` | String. Options: `简体中文`, `繁體中文`, `český jazyk`, `English`, `Italiano`, `한국어`, `Português`, `Español`. Default to `English` | The mnemonic language | | `--folder` | String. Pointing to `./validator_keys` by default | The folder path for the keystore(s) and deposit(s) | | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | -| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [EIP-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | +| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | ###### `existing-mnemonic` Arguments @@ -153,7 +152,7 @@ You can use `existing-mnemonic --help` to see all arguments. Note that if there | `--num_validators` | Non-negative integer | The number of new signing keys you want to generate. Note that the child key(s) are generated via the same master key. | | `--folder` | String. Pointing to `./validator_keys` by default | The folder path for the keystore(s) and deposit(s) | | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | -| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [EIP-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | +| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | ###### Successful message @@ -180,7 +179,7 @@ You can use `bls-to-execution-change --help` to see all arguments. Note that if | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | | `--mnemonic` | String. mnemonic split by space. | The mnemonic you used to create withdrawal credentials. | | `--mnemonic_password` | Optional string. Empty by default. | The mnemonic password you used in your key generation. Note: It's not the keystore password. | -| `--validator_start_index` | Non-negative integer | The index of the first validator's keys you generated withdrawal credentials with the mnemonic. | +| `--validator_start_index` | Non-negative integer | The index position for the keys to start generating withdrawal credentials in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | | `--validator_indices` | String of integer(s) | A list of the chosen validator index number(s) as identified on the beacon chain. Split multiple items with whitespaces or commas. | | `--bls_withdrawal_credentials_list` | String of hexstring(s). | A list of the old BLS withdrawal credentials of the given validator(s). It is for confirming you are using the correct keys. Split multiple items with whitespaces or commas. | | `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. | diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index c59b9291..a3591dee 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -10,16 +10,16 @@ "mismatch": "Error: the two entered values do not match. Please type again." }, "arg_validator_indices": { - "help": "A list of the validator indices of the certain validator(s)", - "prompt": "Please enter a list of the validator indices of your validator(s). Split multiple items with whitespaces or commas." + "help": "A list of the validator index number(s) of the certain validator(s)", + "prompt": "Please enter a list of the validator index number(s) of your validator(s) as identified on the beacon chain. Split multiple items with whitespaces or commas." }, "arg_bls_withdrawal_credentials_list": { "help": "A list of 32-byte old BLS withdrawal credentials of the certain validator(s)", "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas." }, "arg_validator_start_index": { - "help": "The index (key number) of the signing key you want to use with this mnemonic", - "prompt": "Please enter the index (key number) of the signing key you want to use with this mnemonic.", + "help": "The index position for the keys to start generating withdrawal credentials in ERC-2334 format", + "prompt": "Please enter the index position for the keys to start generating withdrawal credentials in ERC-2334 format.", "confirm": "Please repeat the index to confirm" }, "arg_chain": { From 9b86724c0db55854f77bbf9af631605139ff4203 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Feb 2023 00:30:25 +0800 Subject: [PATCH 21/35] Run test_btec_script.py in CI --- .circleci/config.yml | 3 ++ test_btec_script.py | 85 ++++++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 3 files changed, 89 insertions(+) create mode 100755 test_btec_script.py diff --git a/.circleci/config.yml b/.circleci/config.yml index 85ce3e15..6a99824d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,6 +105,9 @@ jobs: - run: name: Run deposit script on Windows" command: python ./test_deposit_script.py + - run: + name: Run btec script on Windows" + command: python ./test_btec_script.py build-linux-amd64: machine: image: ubuntu-2004:202201-02 diff --git a/test_btec_script.py b/test_btec_script.py new file mode 100755 index 00000000..6e6e22b7 --- /dev/null +++ b/test_btec_script.py @@ -0,0 +1,85 @@ +import asyncio +import os + +# For not importing staking_deposit here +DEFAULT_VALIDATOR_KEYS_FOLDER_NAME = 'bls_to_execution_changes' + + +async def main(): + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + if os.name == 'nt': # Windows + run_script_cmd = 'sh deposit.sh' + else: # Mac or Linux + run_script_cmd = './deposit.sh' + + install_cmd = run_script_cmd + ' install' + print('[INFO] Creating subprocess 1: installation:' , install_cmd) + proc = await asyncio.create_subprocess_shell( + install_cmd, + ) + await proc.wait() + print('[INFO] Installed') + + cmd_args = [ + run_script_cmd, + '--language', 'english', + '--non_interactive', + 'generate-bls-to-execution-change', + '--bls_to_execution_changes_folder', my_folder_path, + '--chain', 'mainnet', + '--mnemonic', '\"sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry\"', + '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '--validator_start_index', '0', + '--validator_indices', '1', + '--execution_address', '0x3434343434343434343434343434343434343434', + ] + proc = await asyncio.create_subprocess_shell( + ' '.join(cmd_args), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + seed_phrase = '' + parsing = False + async for out in proc.stdout: + output = out.decode('utf-8').rstrip() + if output.startswith("***Using the tool"): + parsing = True + elif output.startswith("This is your mnemonic"): + parsing = True + elif output.startswith("Please type your mnemonic"): + parsing = False + elif parsing: + seed_phrase += output + if len(seed_phrase) > 0: + encoded_phrase = seed_phrase.encode() + proc.stdin.write(encoded_phrase) + proc.stdin.write(b'\n') + print(output) + + async for out in proc.stderr: + output = out.decode('utf-8').rstrip() + print(f'[stderr] {output}') + + assert len(seed_phrase) > 0 + + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + # Clean up + for key_file_name in key_files: + os.remove(os.path.join(validator_keys_folder_path, key_file_name)) + os.rmdir(validator_keys_folder_path) + os.rmdir(my_folder_path) + + +if os.name == 'nt': # Windows + loop = asyncio.ProactorEventLoop() + asyncio.set_event_loop(loop) + loop.run_until_complete(main()) +else: + asyncio.run(main()) diff --git a/tox.ini b/tox.ini index 85356efe..843680d1 100644 --- a/tox.ini +++ b/tox.ini @@ -32,6 +32,7 @@ deps= -r{toxinidir}/requirements_test.txt # for async-pytest commands= python {toxinidir}/test_deposit_script.py + python {toxinidir}/test_btec_script.py [testenv:py310-core] deps={[common-core]deps} From ca5062a25cc7a8b09cef76ef8a04021c126ffb1a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Feb 2023 00:41:11 +0800 Subject: [PATCH 22/35] Kick the cache --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6a99824d..90e75859 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,12 +30,12 @@ jobs: steps: - checkout - restore_cache: - key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v2 + key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v3 - run: name: Install requirements in venv command: make venv_build_test - save_cache: - key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v2 + key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v3 paths: - ./venv venv_pytest: @@ -45,7 +45,7 @@ jobs: steps: - checkout - restore_cache: - key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v2 + key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v3 - run: name: Run tests with venv command: make venv_test @@ -61,7 +61,7 @@ jobs: steps: - checkout - restore_cache: - key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v2 + key: venv-deps2-{{ arch }}-{{ .Branch }}-{{ checksum "requirements.txt" }}-{{ checksum "requirements_test.txt" }}-{{ checksum "setup.py" }}-{{ checksum "Makefile" }}-v3 - run: name: Run linter with venv command: make venv_lint From d83c3121faf2798c0140c254df5a61c0840dee30 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sat, 11 Feb 2023 00:50:41 +0800 Subject: [PATCH 23/35] Run test_binary_btec_script.py in CI --- .circleci/config.yml | 24 ++++-- test_binary_btec_script.py | 80 +++++++++++++++++++ ...script.py => test_binary_deposit_script.py | 0 3 files changed, 96 insertions(+), 8 deletions(-) create mode 100755 test_binary_btec_script.py rename test_binary_script.py => test_binary_deposit_script.py (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 90e75859..797c0720 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -138,9 +138,11 @@ jobs: export TEST_FOLDER_NAME=TMP_TEST_FOLDER mkdir ${TEST_FOLDER_NAME} cp -r ${BUILD_FILE_NAME} ${TEST_FOLDER_NAME} - cp test_binary_script.py ${TEST_FOLDER_NAME} + cp test_binary_deposit_script.py ${TEST_FOLDER_NAME} + cp test_binary_btec_script.py ${TEST_FOLDER_NAME} cd ${TEST_FOLDER_NAME} - python test_binary_script.py ./${BUILD_FILE_NAME}; + python test_binary_deposit_script.py ./${BUILD_FILE_NAME}; + python test_binary_btec_script.py ./${BUILD_FILE_NAME}; - run: name: Compress the file command: | @@ -184,9 +186,11 @@ jobs: export TEST_FOLDER_NAME=TMP_TEST_FOLDER mkdir ${TEST_FOLDER_NAME} cp -r ${BUILD_FILE_NAME} ${TEST_FOLDER_NAME} - cp test_binary_script.py ${TEST_FOLDER_NAME} + cp test_binary_deposit_script.py ${TEST_FOLDER_NAME} + cp test_binary_btec_script.py ${TEST_FOLDER_NAME} cd ${TEST_FOLDER_NAME} - python test_binary_script.py ./${BUILD_FILE_NAME}; + python test_binary_deposit_script.py ./${BUILD_FILE_NAME}; + python test_binary_btec_script.py ./${BUILD_FILE_NAME}; - run: name: Compress the file command: | @@ -228,9 +232,11 @@ jobs: $TEST_FOLDER_NAME = "TMP_TEST_FOLDER" mkdir ${TEST_FOLDER_NAME} Copy-item ${BUILD_FILE_NAME} -destination ${TEST_FOLDER_NAME} -recurse - copy test_binary_script.py ${TEST_FOLDER_NAME} + copy test_binary_deposit_script.py ${TEST_FOLDER_NAME} + copy test_binary_btec_script.py ${TEST_FOLDER_NAME} cd ${TEST_FOLDER_NAME} - python test_binary_script.py ${BUILD_FILE_NAME} + python test_binary_deposit_script.py ${BUILD_FILE_NAME} + python test_binary_btec_script.py ${BUILD_FILE_NAME} - run: name: Compress the file command: | @@ -274,9 +280,11 @@ jobs: export TEST_FOLDER_NAME=TMP_TEST_FOLDER mkdir ${TEST_FOLDER_NAME} cp -r ${BUILD_FILE_NAME} ${TEST_FOLDER_NAME} - cp test_binary_script.py ${TEST_FOLDER_NAME} + cp test_binary_deposit_script.py ${TEST_FOLDER_NAME} + cp test_binary_btec_script.py ${TEST_FOLDER_NAME} cd ${TEST_FOLDER_NAME} - python3 test_binary_script.py ./${BUILD_FILE_NAME}; + python3 test_binary_deposit_script.py ./${BUILD_FILE_NAME}; + python3 test_binary_btec_script.py ./${BUILD_FILE_NAME}; - run: name: Compress the file command: | diff --git a/test_binary_btec_script.py b/test_binary_btec_script.py new file mode 100755 index 00000000..6fb92d32 --- /dev/null +++ b/test_binary_btec_script.py @@ -0,0 +1,80 @@ +import asyncio +import os +import sys + + +# For not importing staking_deposit here +DEFAULT_VALIDATOR_KEYS_FOLDER_NAME = 'bls_to_execution_changes' + + +async def main(argv): + binary_file_path = argv[1] + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + if os.name == 'nt': # Windows + run_script_cmd = ".\\" + binary_file_path + '\deposit.exe' + else: # Mac or Linux + run_script_cmd = './' + binary_file_path + '/deposit' + + cmd_args = [ + run_script_cmd, + '--language', 'english', + '--non_interactive', + 'generate-bls-to-execution-change', + '--bls_to_execution_changes_folder', my_folder_path, + '--chain', 'mainnet', + '--mnemonic', '\"sister protect peanut hill ready work profit fit wish want small inflict flip member tail between sick setup bright duck morning sell paper worry\"', + '--bls_withdrawal_credentials_list', '0x00bd0b5a34de5fb17df08410b5e615dda87caf4fb72d0aac91ce5e52fc6aa8de', + '--validator_start_index', '0', + '--validator_indices', '1', + '--execution_address', '0x3434343434343434343434343434343434343434', + ] + proc = await asyncio.create_subprocess_shell( + ' '.join(cmd_args), + stdin=asyncio.subprocess.PIPE, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + seed_phrase = '' + parsing = False + async for out in proc.stdout: + output = out.decode('utf-8').rstrip() + if output.startswith("***Using the tool"): + parsing = True + elif output.startswith("This is your mnemonic"): + parsing = True + elif output.startswith("Please type your mnemonic"): + parsing = False + elif parsing: + seed_phrase += output + if len(seed_phrase) > 0: + encoded_phrase = seed_phrase.encode() + proc.stdin.write(encoded_phrase) + proc.stdin.write(b'\n') + print(output) + + async for out in proc.stderr: + output = out.decode('utf-8').rstrip() + print(f'[stderr] {output}') + + assert len(seed_phrase) > 0 + + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + # Clean up + for key_file_name in key_files: + os.remove(os.path.join(validator_keys_folder_path, key_file_name)) + os.rmdir(validator_keys_folder_path) + os.rmdir(my_folder_path) + + +if os.name == 'nt': # Windows + loop = asyncio.ProactorEventLoop() + asyncio.set_event_loop(loop) + loop.run_until_complete(main(sys.argv)) +else: + asyncio.run(main(sys.argv)) diff --git a/test_binary_script.py b/test_binary_deposit_script.py similarity index 100% rename from test_binary_script.py rename to test_binary_deposit_script.py From 2c3fb22f68387f6a89e9866fef50a0b90b74061b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Feb 2023 15:47:05 +0800 Subject: [PATCH 24/35] Strip brackets and extra spaces --- staking_deposit/utils/validation.py | 2 ++ tests/test_utils/test_validation.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index a166816f..736a87f6 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -238,6 +238,8 @@ def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> byte def normalize_input_list(input: str) -> Sequence[str]: + input = input.strip('[({})]') + input = re.sub(' +', ' ', input) return re.split(r'; |, | |,|;', input) diff --git a/tests/test_utils/test_validation.py b/tests/test_utils/test_validation.py index c0b9c89f..9b918462 100644 --- a/tests/test_utils/test_validation.py +++ b/tests/test_utils/test_validation.py @@ -5,6 +5,7 @@ from staking_deposit.exceptions import ValidationError from staking_deposit.utils.validation import ( + normalize_input_list, validate_int_range, validate_password_strength, ) @@ -43,3 +44,19 @@ def test_validate_int_range(num: Any, low: int, high: int, valid: bool) -> None: else: with pytest.raises(ValidationError): validate_int_range(num, low, high) + + +@pytest.mark.parametrize( + 'input, result', + [ + ('1', ['1']), + ('1,2,3', ['1', '2', '3']), + ('[1,2,3]', ['1', '2', '3']), + ('(1,2,3)', ['1', '2', '3']), + ('{1,2,3}', ['1', '2', '3']), + ('1 2 3', ['1', '2', '3']), + ('1 2 3', ['1', '2', '3']), + ] +) +def test_normalize_input_list(input, result): + assert normalize_input_list(input) == result From db3e628f1c3edc8e57c6236fe8c5745d288911ab Mon Sep 17 00:00:00 2001 From: John Westlund Date: Sat, 18 Feb 2023 10:48:01 -0800 Subject: [PATCH 25/35] fix error message grammar --- staking_deposit/intl/en/utils/validation.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index caf04e05..00579c8b 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -21,7 +21,7 @@ "err_not_bls_form": "The given withdrawal credentials is not in BLS_WITHDRAWAL_PREFIX form." }, "validate_bls_withdrawal_credentials_matching": { - "err_not_matching": "The given withdrawal credentials is matching the old BLS withdrawal credentials that mnemonic generated." + "err_not_matching": "The given withdrawal credentials does not match the old BLS withdrawal credentials that mnemonic generated." }, "verify_bls_to_execution_change_json": { "msg_bls_to_execution_change_verification": "Verifying your BLSToExecutionChange file:\t" From aae55a6886af8e25a9d115a6bd3d5f1895ed3272 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Mar 2023 21:44:48 +0800 Subject: [PATCH 26/35] Apply PR feedback by catching the exceptions. Improve error handling and messages. --- .../cli/generate_bls_to_execution_change.py | 6 +++++- .../cli/generate_bls_to_execution_change.json | 2 +- staking_deposit/intl/en/utils/validation.json | 7 +++++++ staking_deposit/utils/click.py | 2 +- staking_deposit/utils/validation.py | 21 ++++++++++++------- 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 5599bea5..1cba20a0 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -179,7 +179,11 @@ def generate_bls_to_execution_change( # Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated for i, credential in enumerate(credentials.credentials): - validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials_list[i], credential) + try: + validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials_list[i], credential) + except ValidationError as e: + click.echo('\n[Error] ' + str(e)) + return btec_file = credentials.export_bls_to_execution_change_json(bls_to_execution_changes_folder, validator_indices) diff --git a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json index a3591dee..cca5adfd 100644 --- a/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json +++ b/staking_deposit/intl/en/cli/generate_bls_to_execution_change.json @@ -15,7 +15,7 @@ }, "arg_bls_withdrawal_credentials_list": { "help": "A list of 32-byte old BLS withdrawal credentials of the certain validator(s)", - "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas." + "prompt": "Please enter a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas. The withdrawal credentials are in hexadecimal encoded form." }, "arg_validator_start_index": { "help": "The index position for the keys to start generating withdrawal credentials in ERC-2334 format", diff --git a/staking_deposit/intl/en/utils/validation.json b/staking_deposit/intl/en/utils/validation.json index 00579c8b..3674fd71 100644 --- a/staking_deposit/intl/en/utils/validation.json +++ b/staking_deposit/intl/en/utils/validation.json @@ -25,5 +25,12 @@ }, "verify_bls_to_execution_change_json": { "msg_bls_to_execution_change_verification": "Verifying your BLSToExecutionChange file:\t" + }, + "normalize_bls_withdrawal_credentials_to_bytes" :{ + "err_incorrect_hex_form": "The given input is not in hexadecimal encoded form." + + }, + "normalize_input_list": { + "err_incorrect_list": "The given input should be a list of the old BLS withdrawal credentials of your validator(s). Split multiple items with whitespaces or commas." } } diff --git a/staking_deposit/utils/click.py b/staking_deposit/utils/click.py index 8b9b2ae3..693ce9aa 100644 --- a/staking_deposit/utils/click.py +++ b/staking_deposit/utils/click.py @@ -104,7 +104,7 @@ def callback(ctx: click.Context, param: Any, user_input: str) -> Any: raise ValidationError(confirmation_mismatch_msg()) return processed_input except ValidationError as e: - click.echo(e) + click.echo('\n[Error] ' + str(e)) user_input = click.prompt(prompt(), hide_input=hide_input) return callback diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 736a87f6..2203fdd8 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -210,7 +210,10 @@ def normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials: st if bls_withdrawal_credentials.startswith('0x'): bls_withdrawal_credentials = bls_withdrawal_credentials[2:] - bls_withdrawal_credentials_bytes = bytes.fromhex(bls_withdrawal_credentials) + try: + bls_withdrawal_credentials_bytes = bytes.fromhex(bls_withdrawal_credentials) + except Exception: + raise ValidationError(load_text(['err_incorrect_hex_form']) + '\n') return bls_withdrawal_credentials_bytes @@ -226,21 +229,25 @@ def validate_bls_withdrawal_credentials(bls_withdrawal_credentials: str) -> byte bls_withdrawal_credentials_bytes = normalize_bls_withdrawal_credentials_to_bytes(bls_withdrawal_credentials) if is_eth1_address_withdrawal_credentials(bls_withdrawal_credentials_bytes): - raise ValidationError('\n' + load_text(['err_is_already_eth1_form']) + '\n') + raise ValidationError(load_text(['err_is_already_eth1_form']) + '\n') try: assert len(bls_withdrawal_credentials_bytes) == 32 assert bls_withdrawal_credentials_bytes[:1] == BLS_WITHDRAWAL_PREFIX except (ValueError, AssertionError): - raise ValidationError('\n' + load_text(['err_not_bls_form']) + '\n') + raise ValidationError(load_text(['err_not_bls_form']) + '\n') return bls_withdrawal_credentials_bytes def normalize_input_list(input: str) -> Sequence[str]: - input = input.strip('[({})]') - input = re.sub(' +', ' ', input) - return re.split(r'; |, | |,|;', input) + try: + input = input.strip('[({})]') + input = re.sub(' +', ' ', input) + result = re.split(r'; |, | |,|;', input) + except Exception: + raise ValidationError(load_text(['err_incorrect_list']) + '\n') + return result def validate_bls_withdrawal_credentials_list(input_bls_withdrawal_credentials_list: str) -> Sequence[bytes]: @@ -256,4 +263,4 @@ def validate_validator_indices(input_validator_indices: str) -> Sequence[int]: def validate_bls_withdrawal_credentials_matching(bls_withdrawal_credentials: bytes, credential: Credential) -> None: if bls_withdrawal_credentials[1:] != SHA256(credential.withdrawal_pk)[1:]: - raise ValidationError('\n' + load_text(['err_not_matching']) + '\n') + raise ValidationError(load_text(['err_not_matching']) + '\n') From f79bbac359bb897861d0103715c4032cadc29e9b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Mar 2023 22:19:00 +0800 Subject: [PATCH 27/35] Improve error handling --- staking_deposit/utils/validation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/staking_deposit/utils/validation.py b/staking_deposit/utils/validation.py index 2203fdd8..ab6f9849 100644 --- a/staking_deposit/utils/validation.py +++ b/staking_deposit/utils/validation.py @@ -128,9 +128,9 @@ def validate_eth1_withdrawal_address(cts: click.Context, param: Any, address: st if address is None: return None if not is_hex_address(address): - raise ValueError(load_text(['err_invalid_ECDSA_hex_addr'])) + raise ValidationError(load_text(['err_invalid_ECDSA_hex_addr'])) if not is_checksum_address(address): - raise ValueError(load_text(['err_invalid_ECDSA_hex_addr_checksum'])) + raise ValidationError(load_text(['err_invalid_ECDSA_hex_addr_checksum'])) normalized_address = to_normalized_address(address) click.echo('\n%s\n' % load_text(['msg_ECDSA_hex_addr_withdrawal'])) From 9aed0276ec4cb9e24d5830db2ccdf70464143bc5 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Mar 2023 23:12:57 +0800 Subject: [PATCH 28/35] Confirm deposit `eth1_withdrawal_address` input --- staking_deposit/cli/generate_keys.py | 9 +++++++-- staking_deposit/intl/en/cli/generate_keys.json | 7 +++++-- staking_deposit/utils/click.py | 2 +- tests/test_cli/test_existing_menmonic.py | 3 ++- tests/test_cli/test_new_mnemonic.py | 15 +++++++++++---- 5 files changed, 26 insertions(+), 10 deletions(-) diff --git a/staking_deposit/cli/generate_keys.py b/staking_deposit/cli/generate_keys.py index e28d9ae8..7ed641d8 100644 --- a/staking_deposit/cli/generate_keys.py +++ b/staking_deposit/cli/generate_keys.py @@ -95,9 +95,14 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[ prompt=lambda: load_text(['keystore_password', 'prompt'], func='generate_keys_arguments_decorator'), ), jit_option( - callback=validate_eth1_withdrawal_address, + callback=captive_prompt_callback( + lambda address: validate_eth1_withdrawal_address(None, None, address), + lambda: load_text(['arg_execution_address', 'prompt'], func='generate_keys_arguments_decorator'), + lambda: load_text(['arg_execution_address', 'confirm'], func='generate_keys_arguments_decorator'), + lambda: load_text(['arg_execution_address', 'mismatch'], func='generate_keys_arguments_decorator'), + ), default=None, - help=lambda: load_text(['eth1_withdrawal_address', 'help'], func='generate_keys_arguments_decorator'), + help=lambda: load_text(['arg_execution_address', 'help'], func='generate_keys_arguments_decorator'), param_decls='--eth1_withdrawal_address', ), ] diff --git a/staking_deposit/intl/en/cli/generate_keys.json b/staking_deposit/intl/en/cli/generate_keys.json index a6a71f30..f49b65d6 100644 --- a/staking_deposit/intl/en/cli/generate_keys.json +++ b/staking_deposit/intl/en/cli/generate_keys.json @@ -17,8 +17,11 @@ "confirm": "Repeat your keystore password for confirmation", "mismatch": "Error: the two entered values do not match. Please type again." }, - "eth1_withdrawal_address": { - "help": "If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key." + "arg_execution_address": { + "help": "The 20-byte (Eth1) execution address that will be used in withdrawal", + "prompt": "Please enter the 20-byte execution address for the new withdrawal credentials. Note that you CANNOT change it once you have set it on chain.", + "confirm": "Repeat your execution address for confirmation.", + "mismatch": "Error: the two entered values do not match. Please type again." } }, "generate_keys": { diff --git a/staking_deposit/utils/click.py b/staking_deposit/utils/click.py index 693ce9aa..8ade0e02 100644 --- a/staking_deposit/utils/click.py +++ b/staking_deposit/utils/click.py @@ -98,7 +98,7 @@ def callback(ctx: click.Context, param: Any, user_input: str) -> Any: try: processed_input = processing_func(user_input) # Logic for confirming user input: - if confirmation_prompt is not None and processed_input != '': + if confirmation_prompt is not None and processed_input not in ('', None): confirmation_input = click.prompt(confirmation_prompt(), hide_input=hide_input) if processing_func(confirmation_input) != processed_input: raise ValidationError(confirmation_mismatch_msg()) diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index 9570fe99..dc4d96ae 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -62,12 +62,13 @@ def test_existing_mnemonic_eth1_address_withdrawal() -> None: os.mkdir(my_folder_path) runner = CliRunner() + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' inputs = [ 'TREZOR', + eth1_withdrawal_address, 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', '2', '2', '5', 'mainnet', 'MyPassword', 'MyPassword'] data = '\n'.join(inputs) - eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' arguments = [ '--language', 'english', 'existing-mnemonic', diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 6a7bb9b4..602d4350 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -10,7 +10,7 @@ from staking_deposit.cli import new_mnemonic from staking_deposit.deposit import cli from staking_deposit.key_handling.key_derivation.mnemonic import abbreviate_words -from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, ETH1_ADDRESS_WITHDRAWAL_PREFIX +from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, ETH1_ADDRESS_WITHDRAWAL_PREFIX, BLS_WITHDRAWAL_PREFIX from staking_deposit.utils.intl import load_text from .helpers import clean_key_folder, get_permissions, get_uuid @@ -69,10 +69,10 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: os.mkdir(my_folder_path) runner = CliRunner() - inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' + inputs = [eth1_withdrawal_address, 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] data = '\n'.join(inputs) - eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' arguments = [ '--language', 'english', 'new-mnemonic', @@ -112,7 +112,7 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: @pytest.mark.asyncio -async def test_script() -> None: +async def test_script_bls_withdrawal() -> None: my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') if not os.path.exists(my_folder_path): os.mkdir(my_folder_path) @@ -164,7 +164,14 @@ async def test_script() -> None: assert len(seed_phrase) > 0 # Check files + deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + with open(validator_keys_folder_path + '/' + deposit_file, 'r') as f: + deposits_dict = json.load(f) + for deposit in deposits_dict: + withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) + assert withdrawal_credentials[:2] == BLS_WITHDRAWAL_PREFIX + _, _, key_files = next(os.walk(validator_keys_folder_path)) all_uuid = [ From ee052fa42434abbb13edef994edc4c152a6f95ca Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Mar 2023 23:20:02 +0800 Subject: [PATCH 29/35] Add more description of mnemonic language per @infosecual's suggestion --- README.md | 2 +- staking_deposit/intl/en/cli/new_mnemonic.json | 4 ++-- tests/test_cli/test_new_mnemonic.py | 12 +++++++++--- tests/test_utils/test_intl.py | 2 +- 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 50d68083..d2930db9 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,7 @@ You can use `new-mnemonic --help` to see all arguments. Note that if there are m | Argument | Type | Description | | -------- | -------- | -------- | | `--num_validators` | Non-negative integer | The number of signing keys you want to generate. Note that the child key(s) are generated via the same master key. | -| `--mnemonic_language` | String. Options: `简体中文`, `繁體中文`, `český jazyk`, `English`, `Italiano`, `한국어`, `Português`, `Español`. Default to `English` | The mnemonic language | +| `--mnemonic_language` | String. Options: `简体中文`, `繁體中文`, `český jazyk`, `English`, `Italiano`, `한국어`, `Português`, `Español`. Default to `English` | The language of the mnemonic word list | | `--folder` | String. Pointing to `./validator_keys` by default | The folder path for the keystore(s) and deposit(s) | | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | | `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | diff --git a/staking_deposit/intl/en/cli/new_mnemonic.json b/staking_deposit/intl/en/cli/new_mnemonic.json index feee3ac2..9a0d3d89 100644 --- a/staking_deposit/intl/en/cli/new_mnemonic.json +++ b/staking_deposit/intl/en/cli/new_mnemonic.json @@ -5,8 +5,8 @@ }, "arg_mnemonic_language": { "default": "english", - "help": "The language that your mnemonic is in", - "prompt": "Please choose your mnemonic language" + "help": "The language of the mnemonic word list", + "prompt": "Please choose the language of the mnemonic word list" }, "msg_mnemonic_presentation": "This is your mnemonic (seed phrase). Write it down and store it safely. It is the ONLY way to retrieve your deposit.", "msg_press_any_key": "Press any key when you have written down your mnemonic.", diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 602d4350..67ef5054 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -10,7 +10,11 @@ from staking_deposit.cli import new_mnemonic from staking_deposit.deposit import cli from staking_deposit.key_handling.key_derivation.mnemonic import abbreviate_words -from staking_deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, ETH1_ADDRESS_WITHDRAWAL_PREFIX, BLS_WITHDRAWAL_PREFIX +from staking_deposit.utils.constants import ( + BLS_WITHDRAWAL_PREFIX, + DEFAULT_VALIDATOR_KEYS_FOLDER_NAME, + ETH1_ADDRESS_WITHDRAWAL_PREFIX, +) from staking_deposit.utils.intl import load_text from .helpers import clean_key_folder, get_permissions, get_uuid @@ -164,13 +168,15 @@ async def test_script_bls_withdrawal() -> None: assert len(seed_phrase) > 0 # Check files - deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] with open(validator_keys_folder_path + '/' + deposit_file, 'r') as f: deposits_dict = json.load(f) for deposit in deposits_dict: withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) - assert withdrawal_credentials[:2] == BLS_WITHDRAWAL_PREFIX + assert withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX _, _, key_files = next(os.walk(validator_keys_folder_path)) diff --git a/tests/test_utils/test_intl.py b/tests/test_utils/test_intl.py index de4ffc9c..6a1a09fc 100644 --- a/tests/test_utils/test_intl.py +++ b/tests/test_utils/test_intl.py @@ -18,7 +18,7 @@ @pytest.mark.parametrize( 'params, file_path, func, lang, found_str', [ (['arg_mnemonic_language', 'prompt'], os.path.join('staking_deposit', 'cli', 'new_mnemonic.json'), - 'new_mnemonic', 'en', 'Please choose your mnemonic language'), + 'new_mnemonic', 'en', 'Please choose the language of the mnemonic word list'), (['arg_mnemonic_language', 'prompt'], os.path.join('staking_deposit', 'cli', 'new_mnemonic.json'), 'new_mnemonic', 'ja', 'ニーモニックの言語を選択してください'), ] From 69f778d5dea5ecbca15958ac391ee4bd87536ddc Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Mon, 13 Mar 2023 23:44:56 +0800 Subject: [PATCH 30/35] Make `--non_interactive` flag visible and add notes in docs --- README.md | 13 +++++++++++-- staking_deposit/deposit.py | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d2930db9..36d8126c 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ - [Option 1. Download binary executable file](#option-1-download-binary-executable-file) - [Step 1. Installation](#step-1-installation) - [Step 2. Create keys and `deposit_data-*.json`](#step-2-create-keys-and-deposit_data-json) - - [language Argument](#language-argument) + - [`language` Argument](#language-argument) + - [`--non_interactive` flag](#--non_interactive-flag) - [Commands](#commands) - [`new-mnemonic` Arguments](#new-mnemonic-arguments) - [`existing-mnemonic` Arguments](#existing-mnemonic-arguments) @@ -113,7 +114,7 @@ or run the following command to enter the interactive CLI and generate keys from ./deposit existing-mnemonic ``` -###### language Argument +###### `language` Argument The Launchpad offers many language/internationalization options. If you wish to select one as a CLI argument, it must be passed in before one of the commands is chosen. @@ -121,6 +122,14 @@ The Launchpad offers many language/internationalization options. If you wish to | -------- | -------- | -------- | | `--language` | String. Options: `العربية`, `ελληνικά`, `English`, `Français`, `Bahasa melayu`, `Italiano`, `日本語`, `한국어`, `Português do Brasil`, `român`, `简体中文`. Default to `English` | The language you wish to use the CLI in. | +###### `--non_interactive` flag + +**Warning: with this flag, there will be no confirmation step(s) to verify the input value(s). Please use it carefully.** + +| Argument | Type | Description | +| -------- | -------- | -------- | +| `--non_interactive` | Flag | Run CLI in non-interactive mode. | + ###### Commands The CLI offers different commands depending on what you want to do with the tool. diff --git a/staking_deposit/deposit.py b/staking_deposit/deposit.py index 421e1c0d..66acaa91 100644 --- a/staking_deposit/deposit.py +++ b/staking_deposit/deposit.py @@ -44,8 +44,8 @@ def check_python_version() -> None: '--non_interactive', default=False, is_flag=True, - help='Disables interactive prompts.', - hidden=True, + help='Disables interactive prompts. Warning: with this flag, there will be no confirmation step(s) to verify the input value(s). Please use it carefully.', # noqa: E501 + hidden=False, ) def cli(ctx: click.Context, language: str, non_interactive: bool) -> None: config.language = language From ed092fcc0bde5830072ffe533d35340793a3570c Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Mar 2023 03:01:20 +0800 Subject: [PATCH 31/35] clean folders first --- tests/test_cli/test_new_mnemonic.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 67ef5054..17a7fd8e 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -117,7 +117,9 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: @pytest.mark.asyncio async def test_script_bls_withdrawal() -> None: + # Prepare folder my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) if not os.path.exists(my_folder_path): os.mkdir(my_folder_path) @@ -176,6 +178,7 @@ async def test_script_bls_withdrawal() -> None: deposits_dict = json.load(f) for deposit in deposits_dict: withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) + print('withdrawal_credentials', withdrawal_credentials) assert withdrawal_credentials[:1] == BLS_WITHDRAWAL_PREFIX _, _, key_files = next(os.walk(validator_keys_folder_path)) @@ -198,7 +201,9 @@ async def test_script_bls_withdrawal() -> None: @pytest.mark.asyncio async def test_script_abbreviated_mnemonic() -> None: + # Prepare folder my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) if not os.path.exists(my_folder_path): os.mkdir(my_folder_path) From 27669bdfb4a9da49dbeab3e9f2804497bdacc534 Mon Sep 17 00:00:00 2001 From: Stephen Hicks Date: Sun, 12 Mar 2023 16:24:38 -0400 Subject: [PATCH 32/35] Validate checksum for eth1_withdrawal_address If the given address is checksummed, the checksum will be validated and will error out eagerly if it's incorrect. --- staking_deposit/cli/generate_keys.py | 1 - tests/test_cli/test_existing_menmonic.py | 30 ++++++++++++++++++++++ tests/test_cli/test_new_mnemonic.py | 32 ++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/staking_deposit/cli/generate_keys.py b/staking_deposit/cli/generate_keys.py index 7ed641d8..69fa9b0b 100644 --- a/staking_deposit/cli/generate_keys.py +++ b/staking_deposit/cli/generate_keys.py @@ -6,7 +6,6 @@ ) from eth_typing import HexAddress - from staking_deposit.credentials import ( CredentialList, ) diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index dc4d96ae..971667a6 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -108,6 +108,36 @@ def test_existing_mnemonic_eth1_address_withdrawal() -> None: clean_key_folder(my_folder_path) +def test_existing_mnemonic_eth1_address_withdrawal_bad_checksum() -> None: + # Prepare folder + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + runner = CliRunner() + inputs = [ + 'TREZOR', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', + '2', '2', '5', 'mainnet', 'MyPassword', 'MyPassword'] + data = '\n'.join(inputs) + # Note: final 'A' needed to be an 'a' + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' + arguments = [ + '--language', 'english', + 'existing-mnemonic', + '--folder', my_folder_path, + '--mnemonic-password', 'TREZOR', + '--eth1_withdrawal_address', eth1_withdrawal_address, + ] + result = runner.invoke(cli, arguments, input=data) + + assert result.exit_code == 1 + + # Clean up + clean_key_folder(my_folder_path) + + @pytest.mark.asyncio async def test_script() -> None: my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 17a7fd8e..7c3d5451 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -115,6 +115,38 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: clean_key_folder(my_folder_path) +def test_new_mnemonic_eth1_address_withdrawal_bad_checksum(monkeypatch) -> None: + # monkeypatch get_mnemonic + def mock_get_mnemonic(language, words_path, entropy=None) -> str: + return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic) + + # Prepare folder + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + runner = CliRunner() + inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] + data = '\n'.join(inputs) + # Note: final 'A' needed to be an 'a' + eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' + arguments = [ + '--language', 'english', + 'new-mnemonic', + '--folder', my_folder_path, + '--eth1_withdrawal_address', eth1_withdrawal_address, + ] + result = runner.invoke(cli, arguments, input=data) + assert result.exit_code == 1 + + # Clean up + clean_key_folder(my_folder_path) + + @pytest.mark.asyncio async def test_script_bls_withdrawal() -> None: # Prepare folder From 491df810d5cdebf5a551405dd91115f80dea8d2a Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Mar 2023 03:57:02 +0800 Subject: [PATCH 33/35] Update the checksum tests --- tests/test_cli/test_existing_menmonic.py | 39 +++++++++++++++++++++--- tests/test_cli/test_new_mnemonic.py | 39 +++++++++++++++++++++--- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/tests/test_cli/test_existing_menmonic.py b/tests/test_cli/test_existing_menmonic.py index 971667a6..d41ce464 100644 --- a/tests/test_cli/test_existing_menmonic.py +++ b/tests/test_cli/test_existing_menmonic.py @@ -116,24 +116,53 @@ def test_existing_mnemonic_eth1_address_withdrawal_bad_checksum() -> None: os.mkdir(my_folder_path) runner = CliRunner() + + # NOTE: final 'A' needed to be an 'a' + wrong_eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' + correct_eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' + inputs = [ 'TREZOR', + correct_eth1_withdrawal_address, correct_eth1_withdrawal_address, 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', - '2', '2', '5', 'mainnet', 'MyPassword', 'MyPassword'] + '2', '2', '5', 'mainnet', 'MyPassword', 'MyPassword' + ] data = '\n'.join(inputs) - # Note: final 'A' needed to be an 'a' - eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' arguments = [ '--language', 'english', 'existing-mnemonic', '--folder', my_folder_path, '--mnemonic-password', 'TREZOR', - '--eth1_withdrawal_address', eth1_withdrawal_address, + '--eth1_withdrawal_address', wrong_eth1_withdrawal_address, ] result = runner.invoke(cli, arguments, input=data) - assert result.exit_code == 1 + assert result.exit_code == 0 + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] + with open(validator_keys_folder_path + '/' + deposit_file, 'r') as f: + deposits_dict = json.load(f) + for deposit in deposits_dict: + withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) + assert withdrawal_credentials == ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + b'\x00' * 11 + decode_hex(correct_eth1_withdrawal_address) + ) + + all_uuid = [ + get_uuid(validator_keys_folder_path + '/' + key_file) + for key_file in key_files + if key_file.startswith('keystore') + ] + assert len(set(all_uuid)) == 5 + + # Verify file permissions + if os.name == 'posix': + for file_name in key_files: + assert get_permissions(validator_keys_folder_path, file_name) == '0o440' # Clean up clean_key_folder(my_folder_path) diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index 7c3d5451..dc4e9626 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -129,19 +129,48 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: os.mkdir(my_folder_path) runner = CliRunner() - inputs = ['english', '1', 'mainnet', 'MyPassword', 'MyPassword', + + # NOTE: final 'A' needed to be an 'a' + wrong_eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' + correct_eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' + + inputs = [correct_eth1_withdrawal_address, correct_eth1_withdrawal_address, + 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] data = '\n'.join(inputs) - # Note: final 'A' needed to be an 'a' - eth1_withdrawal_address = '0x00000000219ab540356cBB839Cbe05303d7705FA' arguments = [ '--language', 'english', 'new-mnemonic', '--folder', my_folder_path, - '--eth1_withdrawal_address', eth1_withdrawal_address, + '--eth1_withdrawal_address', wrong_eth1_withdrawal_address, ] result = runner.invoke(cli, arguments, input=data) - assert result.exit_code == 1 + assert result.exit_code == 0 + + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] + with open(validator_keys_folder_path + '/' + deposit_file, 'r') as f: + deposits_dict = json.load(f) + for deposit in deposits_dict: + withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) + assert withdrawal_credentials == ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + b'\x00' * 11 + decode_hex(correct_eth1_withdrawal_address) + ) + + all_uuid = [ + get_uuid(validator_keys_folder_path + '/' + key_file) + for key_file in key_files + if key_file.startswith('keystore') + ] + assert len(set(all_uuid)) == 1 + + # Verify file permissions + if os.name == 'posix': + for file_name in key_files: + assert get_permissions(validator_keys_folder_path, file_name) == '0o440' # Clean up clean_key_folder(my_folder_path) From 4064f12287366129223ddc2f65367aa3e457159c Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Mar 2023 02:17:35 +0800 Subject: [PATCH 34/35] Make `--execution_address` and `--eth1_withdrawal_address` compatible --- README.md | 6 +- .../cli/generate_bls_to_execution_change.py | 2 +- staking_deposit/cli/generate_keys.py | 6 +- staking_deposit/utils/click.py | 8 +- tests/test_cli/test_new_mnemonic.py | 87 +++++++++++++++++++ 5 files changed, 100 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 36d8126c..d9373cae 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ You can use `new-mnemonic --help` to see all arguments. Note that if there are m | `--mnemonic_language` | String. Options: `简体中文`, `繁體中文`, `český jazyk`, `English`, `Italiano`, `한국어`, `Português`, `Español`. Default to `English` | The language of the mnemonic word list | | `--folder` | String. Pointing to `./validator_keys` by default | The folder path for the keystore(s) and deposit(s) | | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | -| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | +| `--execution_address` (or `--eth1_withdrawal_address`) | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | ###### `existing-mnemonic` Arguments @@ -161,7 +161,7 @@ You can use `existing-mnemonic --help` to see all arguments. Note that if there | `--num_validators` | Non-negative integer | The number of new signing keys you want to generate. Note that the child key(s) are generated via the same master key. | | `--folder` | String. Pointing to `./validator_keys` by default | The folder path for the keystore(s) and deposit(s) | | `--chain` | String. `mainnet` by default | The chain setting for the signing domain. | -| `--eth1_withdrawal_address` | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | +| `--execution_address` (or `--eth1_withdrawal_address`) | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | ###### Successful message @@ -191,7 +191,7 @@ You can use `bls-to-execution-change --help` to see all arguments. Note that if | `--validator_start_index` | Non-negative integer | The index position for the keys to start generating withdrawal credentials in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | | `--validator_indices` | String of integer(s) | A list of the chosen validator index number(s) as identified on the beacon chain. Split multiple items with whitespaces or commas. | | `--bls_withdrawal_credentials_list` | String of hexstring(s). | A list of the old BLS withdrawal credentials of the given validator(s). It is for confirming you are using the correct keys. Split multiple items with whitespaces or commas. | -| `--execution_address` | String. 20-byte Execution (Eth1) address in hexadecimal encoded form | The execution (Eth1) address you want to change to for withdrawals. | +| `--execution_address` (or `--eth1_withdrawal_address`) | String. Eth1 address in hexadecimal encoded form | If this field is set and valid, the given Eth1 address will be used to create the withdrawal credentials. Otherwise, it will generate withdrawal credentials with the mnemonic-derived withdrawal public key in [ERC-2334 format](https://eips.ethereum.org/EIPS/eip-2334#eth2-specific-parameters). | | `--devnet_chain_setting` | String. JSON string `'{"network_name": "", "genesis_fork_version": "", "genesis_validator_root": ""}'` | The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. | #### Option 2. Build `deposit-cli` with native Python diff --git a/staking_deposit/cli/generate_bls_to_execution_change.py b/staking_deposit/cli/generate_bls_to_execution_change.py index 1cba20a0..0b3b0f49 100644 --- a/staking_deposit/cli/generate_bls_to_execution_change.py +++ b/staking_deposit/cli/generate_bls_to_execution_change.py @@ -116,7 +116,7 @@ def get_password(text: str) -> str: lambda: load_text(['arg_execution_address', 'mismatch'], func=FUNC_NAME), ), help=lambda: load_text(['arg_execution_address', 'help'], func=FUNC_NAME), - param_decls='--execution_address', + param_decls=['--execution_address', '--eth1_withdrawal_address'], prompt=lambda: load_text(['arg_execution_address', 'prompt'], func=FUNC_NAME), ) @jit_option( diff --git a/staking_deposit/cli/generate_keys.py b/staking_deposit/cli/generate_keys.py index 69fa9b0b..9163d55c 100644 --- a/staking_deposit/cli/generate_keys.py +++ b/staking_deposit/cli/generate_keys.py @@ -102,7 +102,7 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[ ), default=None, help=lambda: load_text(['arg_execution_address', 'help'], func='generate_keys_arguments_decorator'), - param_decls='--eth1_withdrawal_address', + param_decls=['--execution_address', '--eth1_withdrawal_address'], ), ] for decorator in reversed(decorators): @@ -114,7 +114,7 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[ @click.pass_context def generate_keys(ctx: click.Context, validator_start_index: int, num_validators: int, folder: str, chain: str, keystore_password: str, - eth1_withdrawal_address: HexAddress, **kwargs: Any) -> None: + execution_address: HexAddress, **kwargs: Any) -> None: mnemonic = ctx.obj['mnemonic'] mnemonic_password = ctx.obj['mnemonic_password'] amounts = [MAX_DEPOSIT_AMOUNT] * num_validators @@ -132,7 +132,7 @@ def generate_keys(ctx: click.Context, validator_start_index: int, amounts=amounts, chain_setting=chain_setting, start_index=validator_start_index, - hex_eth1_withdrawal_address=eth1_withdrawal_address, + hex_eth1_withdrawal_address=execution_address, ) keystore_filefolders = credentials.export_keystores(password=keystore_password, folder=folder) deposits_file = credentials.export_deposit_data_json(folder=folder) diff --git a/staking_deposit/utils/click.py b/staking_deposit/utils/click.py index 8ade0e02..ceb36d64 100644 --- a/staking_deposit/utils/click.py +++ b/staking_deposit/utils/click.py @@ -25,7 +25,7 @@ class JITOption(click.Option): ''' def __init__( self, - param_decls: str, + param_decls: Union[str, Sequence[str]], default: Union[Callable[[], Any], None, Any] = None, help: Union[Callable[[], str], str, None] = None, prompt: Union[Callable[[], str], str, None] = None, @@ -36,8 +36,12 @@ def __init__( self.callable_help = help self.callable_prompt = prompt + # `click.Option.Argument.param_decls` takes a list of flags or argument names. + if isinstance(param_decls, str): + param_decls = [_value_of(param_decls)] + return super().__init__( - param_decls=[_value_of(param_decls)], + param_decls=param_decls, default=_value_of(default), help=_value_of(help), prompt=_value_of(prompt), diff --git a/tests/test_cli/test_new_mnemonic.py b/tests/test_cli/test_new_mnemonic.py index dc4e9626..0e091dd3 100644 --- a/tests/test_cli/test_new_mnemonic.py +++ b/tests/test_cli/test_new_mnemonic.py @@ -176,6 +176,93 @@ def mock_get_mnemonic(language, words_path, entropy=None) -> str: clean_key_folder(my_folder_path) +def test_new_mnemonic_eth1_address_withdrawal_alias(monkeypatch) -> None: + # monkeypatch get_mnemonic + def mock_get_mnemonic(language, words_path, entropy=None) -> str: + return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic) + + # Prepare folder + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + runner = CliRunner() + execution_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' + inputs = [execution_address, 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] + data = '\n'.join(inputs) + arguments = [ + '--language', 'english', + 'new-mnemonic', + '--folder', my_folder_path, + '--execution_address', execution_address, # execution_address and eth1_withdrawal_address are aliases + ] + result = runner.invoke(cli, arguments, input=data) + assert result.exit_code == 0 + + # Check files + validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) + _, _, key_files = next(os.walk(validator_keys_folder_path)) + + deposit_file = [key_file for key_file in key_files if key_file.startswith('deposit_data')][0] + with open(validator_keys_folder_path + '/' + deposit_file, 'r') as f: + deposits_dict = json.load(f) + for deposit in deposits_dict: + withdrawal_credentials = bytes.fromhex(deposit['withdrawal_credentials']) + assert withdrawal_credentials == ( + ETH1_ADDRESS_WITHDRAWAL_PREFIX + b'\x00' * 11 + decode_hex(execution_address) + ) + + all_uuid = [ + get_uuid(validator_keys_folder_path + '/' + key_file) + for key_file in key_files + if key_file.startswith('keystore') + ] + assert len(set(all_uuid)) == 1 + + # Verify file permissions + if os.name == 'posix': + for file_name in key_files: + assert get_permissions(validator_keys_folder_path, file_name) == '0o440' + + # Clean up + clean_key_folder(my_folder_path) + + +def test_new_mnemonic_eth1_address_withdrawal_double_params(monkeypatch) -> None: + # monkeypatch get_mnemonic + def mock_get_mnemonic(language, words_path, entropy=None) -> str: + return "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + monkeypatch.setattr(new_mnemonic, "get_mnemonic", mock_get_mnemonic) + + # Prepare folder + my_folder_path = os.path.join(os.getcwd(), 'TESTING_TEMP_FOLDER') + clean_key_folder(my_folder_path) + if not os.path.exists(my_folder_path): + os.mkdir(my_folder_path) + + runner = CliRunner() + execution_address = '0x00000000219ab540356cBB839Cbe05303d7705Fa' + inputs = [execution_address, 'english', '1', 'mainnet', 'MyPassword', 'MyPassword', + 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'] + data = '\n'.join(inputs) + arguments = [ + '--language', 'english', + 'new-mnemonic', + '--folder', my_folder_path, + '--execution_address', execution_address, + '--eth1_withdrawal_address', execution_address, # double param + ] + result = runner.invoke(cli, arguments, input=data) + + # FIXME: Should not allow it + assert result.exit_code == 0 + + @pytest.mark.asyncio async def test_script_bls_withdrawal() -> None: # Prepare folder From c3df6f0b7f42d37768a13306f1cccaa50e2d8417 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 14 Mar 2023 04:16:12 +0800 Subject: [PATCH 35/35] Bump version to v2.5.0 --- setup.py | 2 +- staking_deposit/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index e0b96e07..3f34b5be 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setup( name="staking_deposit", - version='2.4.0', + version='2.5.0', py_modules=["staking_deposit"], packages=find_packages(exclude=('tests', 'docs')), python_requires=">=3.8,<4", diff --git a/staking_deposit/settings.py b/staking_deposit/settings.py index 45cf6fc7..9cd24d95 100644 --- a/staking_deposit/settings.py +++ b/staking_deposit/settings.py @@ -1,7 +1,7 @@ from typing import Dict, NamedTuple from eth_utils import decode_hex -DEPOSIT_CLI_VERSION = '2.4.0' +DEPOSIT_CLI_VERSION = '2.5.0' class BaseChainSetting(NamedTuple):