Skip to content

Commit

Permalink
Add ability to generate exit message
Browse files Browse the repository at this point in the history
  • Loading branch information
valefar-on-discord committed Apr 13, 2024
1 parent b09771e commit 53b10d9
Show file tree
Hide file tree
Showing 17 changed files with 731 additions and 90 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
validator_keys
bls_to_execution_changes
exit_transactions
validator_keys

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

Large diffs are not rendered by default.

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

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


FUNC_NAME = 'exit_transaction_keystore'


@click.command(
help=load_text(['arg_exit_transaction_keystore', 'help'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, list(ALL_CHAINS.keys())),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.keys())
),
),
default=MAINNET,
help=lambda: load_text(['arg_exit_transaction_keystore_chain', 'help'], func=FUNC_NAME),
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
# Since `prater` is alias of `goerli`, do not show `prater` in the prompt message.
list(key for key in ALL_CHAINS.keys() if key != PRATER)
),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: x,
lambda: load_text(['arg_exit_transaction_keystore_keystore', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_exit_transaction_keystore_keystore', 'help'], func=FUNC_NAME),
param_decls='--keystore',
prompt=lambda: load_text(['arg_exit_transaction_keystore_keystore', 'prompt'], func=FUNC_NAME),
type=click.Path(exists=True, file_okay=True, dir_okay=False),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: x,
lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'prompt'], func=FUNC_NAME),
None,
lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'invalid'], func=FUNC_NAME),
True,
),
help=lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'help'], func=FUNC_NAME),
hide_input=True,
param_decls='--keystore_password',
prompt=lambda: load_text(['arg_exit_transaction_keystore_keystore_password', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
lambda: load_text(['arg_validator_index', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_validator_index', 'help'], func=FUNC_NAME),
param_decls='--validator_index',
prompt=lambda: load_text(['arg_validator_index', 'prompt'], func=FUNC_NAME),
)
@jit_option(
default=0,
help=lambda: load_text(['arg_exit_transaction_keystore_epoch', 'help'], func=FUNC_NAME),
param_decls='--epoch',
)
@jit_option(
default=os.getcwd(),
help=lambda: load_text(['arg_exit_transaction_keystore_output_folder', 'help'], func=FUNC_NAME),
param_decls='--output_folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.pass_context
def exit_transaction_keystore(
ctx: click.Context,
chain: str,
keystore: str,
keystore_password: str,
validator_index: int,
epoch: int,
output_folder: str,
**kwargs: Any) -> None:
saved_keystore = Keystore.from_file(keystore)

try:
secret_bytes = saved_keystore.decrypt(keystore_password)
except ValueError:
click.echo(load_text(['arg_exit_transaction_keystore_keystore_password', 'mismatch']))
exit(1)

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

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

saved_folder = export_exit_transaction_json(folder=output_folder, signed_exit=signed_exit)

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

from typing import Any, Sequence
from staking_deposit.cli.existing_mnemonic import load_mnemonic_arguments_decorator
from staking_deposit.credentials import Credential
from staking_deposit.exit_transaction import exit_transaction_generation, export_exit_transaction_json
from staking_deposit.settings import ALL_CHAINS, MAINNET, PRATER, get_chain_setting
from staking_deposit.utils.click import (
captive_prompt_callback,
choice_prompt_func,
jit_option,
)
from staking_deposit.utils.intl import (
closest_match,
load_text,
)
from staking_deposit.utils.validation import validate_int_range, validate_validator_indices


FUNC_NAME = 'exit_transaction_mnemonic'


@click.command(
help=load_text(['arg_exit_transaction_mnemonic', 'help'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda x: closest_match(x, list(ALL_CHAINS.keys())),
choice_prompt_func(
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
list(ALL_CHAINS.keys())
),
),
default=MAINNET,
help=lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'help'], func=FUNC_NAME),
param_decls='--chain',
prompt=choice_prompt_func(
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
# Since `prater` is alias of `goerli`, do not show `prater` in the prompt message.
list(key for key in ALL_CHAINS.keys() if key != PRATER)
),
)
@load_mnemonic_arguments_decorator
@jit_option(
callback=captive_prompt_callback(
lambda num: validate_int_range(num, 0, 2**32),
lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'prompt'], func=FUNC_NAME),
),
default=0,
help=lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'help'], func=FUNC_NAME),
param_decls="--validator_start_index",
prompt=lambda: load_text(['arg_exit_transaction_mnemonic_start_index', 'prompt'], func=FUNC_NAME),
)
@jit_option(
callback=captive_prompt_callback(
lambda validator_indices: validate_validator_indices(validator_indices),
lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'prompt'], func=FUNC_NAME),
),
help=lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'help'], func=FUNC_NAME),
param_decls='--validator_indices',
prompt=lambda: load_text(['arg_exit_transaction_mnemonic_indices', 'prompt'], func=FUNC_NAME),
)
@jit_option(
default=0,
help=lambda: load_text(['arg_exit_transaction_mnemonic_epoch', 'help'], func=FUNC_NAME),
param_decls='--epoch',
)
@jit_option(
default=os.getcwd(),
help=lambda: load_text(['arg_exit_transaction_mnemonic_output_folder', 'help'], func=FUNC_NAME),
param_decls='--output_folder',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
)
@click.pass_context
def exit_transaction_mnemonic(
ctx: click.Context,
chain: str,
mnemonic: str,
mnemonic_password: str,
validator_start_index: int,
validator_indices: Sequence[int],
epoch: int,
output_folder: str,
**kwargs: Any) -> None:

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

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

signing_key = credential.signing_sk

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

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

click.pause(load_text(['msg_pause']))
1 change: 1 addition & 0 deletions staking_deposit/cli/generate_bls_to_execution_change.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def generate_bls_to_execution_change(
chain_setting = get_devnet_chain_setting(
network_name=devnet_chain_setting_dict['network_name'],
genesis_fork_version=devnet_chain_setting_dict['genesis_fork_version'],
exit_fork_version=devnet_chain_setting_dict['exit_fork_version'],
genesis_validator_root=devnet_chain_setting_dict['genesis_validator_root'],
)

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

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


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

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


def exit_transaction_generation(
chain_settings: BaseChainSetting,
signing_key: int,
validator_index: int,
epoch: int) -> SignedVoluntaryExit:
message = VoluntaryExit(
epoch=epoch,
validator_index=validator_index
)

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

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

signed_exit = SignedVoluntaryExit(
message=message,
signature=signature,
)

return signed_exit


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

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

filefolder = os.path.join(
output_folder,
'signed_exit_transaction-%s-%i.json' % (signed_exit.message.validator_index, time.time())
)

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

0 comments on commit 53b10d9

Please sign in to comment.