Skip to content

Commit

Permalink
[WIP] Add generate_bls_to_execution_change
Browse files Browse the repository at this point in the history
  • Loading branch information
hwwhww committed Jan 10, 2023
1 parent d3fd1da commit 34fc753
Show file tree
Hide file tree
Showing 14 changed files with 473 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
validator_keys
bls_to_execution_changes

# Python testing & linting:
build/
Expand Down
180 changes: 180 additions & 0 deletions staking_deposit/cli/generate_bls_to_execution_change.py
Original file line number Diff line number Diff line change
@@ -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']))
69 changes: 65 additions & 4 deletions staking_deposit/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
)


Expand All @@ -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'
Expand All @@ -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:
Expand Down Expand Up @@ -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:
"""
Expand All @@ -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)})."
Expand All @@ -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]:
Expand All @@ -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-%i.json' % (validator_index, 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
2 changes: 2 additions & 0 deletions staking_deposit/deposit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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__':
Expand Down
39 changes: 39 additions & 0 deletions staking_deposit/intl/en/cli/generate_bls_to_execution_change.json
Original file line number Diff line number Diff line change
@@ -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."
}
}
3 changes: 3 additions & 0 deletions staking_deposit/intl/en/credentials.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
8 changes: 7 additions & 1 deletion staking_deposit/intl/en/utils/validation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
}
Loading

0 comments on commit 34fc753

Please sign in to comment.