Skip to content

Commit fc26bd9

Browse files
authored
Add support for compounding validators and the related withdrawal credentials from EIP 7251 (#228)
* Initial implementation for compounding validators and support for EIP 7251 * Adding amount option to new-mnemonic and existing-mnemonic commands * Fix for passing decimal values for amount from CLI * Fix for function renamed * Refactor all the prompt_if arguments from captive_prompt_callback in a generic callable * Improved help and message when using compounding validators * Adding documentation about the new compounding and amount options * Unit tests for compounding features and related fixes
1 parent cc61fdc commit fc26bd9

26 files changed

Lines changed: 698 additions & 70 deletions

docs/src/existing_mnemonic.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ Uses an existing BIP-39 mnemonic phrase for key generation.
2323

2424
- **`--withdrawal_address`**: The Ethereum address that will be used in withdrawal. It typically starts with '0x' followed by 40 hexadecimal characters. Please make sure you have full control over the address you choose here. Once you set a withdrawal address on chain, it cannot be changed.
2525

26+
- **`--compounding / --regular-withdrawal`**: Generates compounding validators with 0x02 withdrawal credentials for a 2048 ETH maximum effective balance or generate regular validators with 0x01 withdrawal credentials for a 32 ETH maximum effective balance. Use of this option requires a withdrawal address. This feature is only supported on networks that have undergone the Pectra fork. Defaults to regular withdrawal.
27+
28+
- **`--amount`**: The amount to deposit to these validators in ether denomination. Must be at least 1 ether and can not have greater precision than 1 gwei. Use of this option requires compounding validators. Defaults to 32 ether.
29+
2630
- **`--pbkdf2`**: Will use pbkdf2 key derivation instead of scrypt for generated keystore files as defined in [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335#decryption-key). This can be a good alternative if you intend to work with a large number of keys, as it can improve performance however it is less secure. You should only use this option if you understand the associated risks and have familiarity with encryption.
2731

2832
- **`--folder`**: The folder where keystore and deposit data files will be saved.

docs/src/new_mnemonic.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Generates a new BIP-39 mnemonic along with validator keystore and deposit files
1717

1818
- **`--withdrawal_address`**: The Ethereum address that will be used in withdrawal. It typically starts with '0x' followed by 40 hexadecimal characters. Please make sure you have full control over the address you choose here. Once you set a withdrawal address on chain, it cannot be changed.
1919

20+
- **`--compounding / --regular-withdrawal`**: Generates compounding validators with 0x02 withdrawal credentials for a 2048 ETH maximum effective balance or generate regular validators with 0x01 withdrawal credentials for a 32 ETH maximum effective balance. Use of this option requires a withdrawal address. This feature is only supported on networks that have undergone the Pectra fork. Defaults to regular withdrawal.
21+
22+
- **`--amount`**: The amount to deposit to these validators in ether denomination. Must be at least 1 ether and can not have greater precision than 1 gwei. Use of this option requires compounding validators. Defaults to 32 ether.
23+
2024
- **`--pbkdf2`**: Will use pbkdf2 key encryption instead of scrypt for generated keystore files as defined in [EIP-2335](https://eips.ethereum.org/EIPS/eip-2335#decryption-key). This can be a good alternative if you intend to work with a large number of keys, as it can improve performance. pbkdf2 encryption is, however, less secure than scrypt. You should only use this option if you understand the associated risks and have familiarity with encryption.
2125

2226
- **`--folder`**: The folder where keystore and deposit data files will be saved.

docs/src/partial_deposit.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ If you wish to create a validator with 0x00 credentials, you must use the **[new
1818

1919
- **`--withdrawal_address`**: The withdrawal address of the existing validator or the desired withdrawal address.
2020

21+
- **`--compounding / --regular-withdrawal`**: Generates compounding validators with 0x02 withdrawal credentials for a 2048 ETH maximum effective balance or generate regular validators with 0x01 withdrawal credentials for a 32 ETH maximum effective balance. Use of this option requires a withdrawal address. This feature is only supported on networks that have undergone the Pectra fork. Defaults to regular withdrawal.
22+
2123
- **`--output_folder`**: The folder path for the `deposit-*` JSON file.
2224

2325
- **`--devnet_chain_setting`**: The custom chain setting of a devnet or testnet. Note that it will override your `--chain` choice. This should be a JSON string containing an object with the following keys: network_name, genesis_fork_version, exit_fork_version and genesis_validator_root.

ethstaker_deposit/cli/existing_mnemonic.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
captive_prompt_callback,
2020
choice_prompt_func,
2121
jit_option,
22+
prompt_if_none,
2223
)
2324
from ethstaker_deposit.utils.intl import fuzzy_reverse_dict_lookup, get_first_options, load_text
2425
from ethstaker_deposit.utils.validation import validate_int_range
@@ -39,7 +40,7 @@ def load_mnemonic_arguments_decorator(function: Callable[..., Any]) -> Callable[
3940
captive_prompt_callback(
4041
lambda mnemonic: validate_mnemonic(mnemonic=mnemonic, language=c.params.get('mnemonic_language')),
4142
prompt=lambda: load_text(['arg_mnemonic', 'prompt'], func='existing_mnemonic'),
42-
prompt_if_none=True,
43+
prompt_if=prompt_if_none,
4344
)(c, _, mnemonic),
4445
help=lambda: load_text(['arg_mnemonic', 'help'], func='existing_mnemonic'),
4546
param_decls='--mnemonic',
@@ -105,7 +106,7 @@ def validate_mnemonic_language(ctx: click.Context, param: Any, language: str) ->
105106
lambda num: validate_int_range(num, 0, 2**32),
106107
lambda: load_text(['arg_validator_start_index', 'prompt'], func='existing_mnemonic'),
107108
lambda: load_text(['arg_validator_start_index', 'confirm'], func='existing_mnemonic'),
108-
prompt_if_none=True,
109+
prompt_if=prompt_if_none,
109110
),
110111
default=0,
111112
help=lambda: load_text(['arg_validator_start_index', 'help'], func='existing_mnemonic'),

ethstaker_deposit/cli/exit_transaction_keystore.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
captive_prompt_callback,
1919
choice_prompt_func,
2020
jit_option,
21+
prompt_if_none,
22+
prompt_if_other_is_none,
2123
)
2224
from ethstaker_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
2325
from ethstaker_deposit.utils.intl import (
@@ -45,7 +47,7 @@
4547
lambda: load_text(['arg_exit_transaction_keystore_chain', 'prompt'], func=FUNC_NAME),
4648
ALL_CHAIN_KEYS
4749
),
48-
prompt_if_other_is_none='devnet_chain_setting',
50+
prompt_if=prompt_if_other_is_none('devnet_chain_setting'),
4951
default=MAINNET,
5052
),
5153
default=MAINNET,
@@ -57,7 +59,7 @@
5759
callback=captive_prompt_callback(
5860
lambda file: validate_keystore_file(file),
5961
lambda: load_text(['arg_exit_transaction_keystore_keystore', 'prompt'], func=FUNC_NAME),
60-
prompt_if_none=True,
62+
prompt_if=prompt_if_none,
6163
),
6264
help=lambda: load_text(['arg_exit_transaction_keystore_keystore', 'help'], func=FUNC_NAME),
6365
param_decls='--keystore',

ethstaker_deposit/cli/exit_transaction_mnemonic.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
captive_prompt_callback,
1919
choice_prompt_func,
2020
jit_option,
21+
prompt_if_other_is_none,
2122
)
2223
from ethstaker_deposit.utils.constants import DEFAULT_EXIT_TRANSACTION_FOLDER_NAME
2324
from ethstaker_deposit.utils.intl import (
@@ -61,7 +62,7 @@ def _exit_verifier(kwargs: Dict[str, Any]) -> bool:
6162
lambda: load_text(['arg_exit_transaction_mnemonic_chain', 'prompt'], func=FUNC_NAME),
6263
ALL_CHAIN_KEYS
6364
),
64-
prompt_if_other_is_none='devnet_chain_setting',
65+
prompt_if=prompt_if_other_is_none('devnet_chain_setting'),
6566
default=MAINNET,
6667
),
6768
default=MAINNET,
@@ -140,6 +141,7 @@ def exit_transaction_mnemonic(
140141
'amount': 0,
141142
'chain_setting': chain_setting,
142143
'hex_withdrawal_address': None,
144+
'compounding': False,
143145
} for index in key_indices]
144146

145147
with concurrent.futures.ProcessPoolExecutor() as executor:

ethstaker_deposit/cli/generate_bls_to_execution_change.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
captive_prompt_callback,
3333
choice_prompt_func,
3434
jit_option,
35+
prompt_if_none,
36+
prompt_if_other_is_none,
3537
)
3638
from ethstaker_deposit.exceptions import ValidationError
3739
from ethstaker_deposit.utils.intl import (
@@ -81,7 +83,7 @@ def _validate_credentials_match(kwargs: Dict[str, Any]) -> Optional[ValidationEr
8183
lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
8284
ALL_CHAIN_KEYS
8385
),
84-
prompt_if_other_is_none='devnet_chain_setting',
86+
prompt_if=prompt_if_other_is_none('devnet_chain_setting'),
8587
default=MAINNET,
8688
),
8789
default=MAINNET,
@@ -114,7 +116,7 @@ def _validate_credentials_match(kwargs: Dict[str, Any]) -> Optional[ValidationEr
114116
lambda bls_withdrawal_credentials_list:
115117
validate_bls_withdrawal_credentials_list(bls_withdrawal_credentials_list),
116118
lambda: load_text(['arg_bls_withdrawal_credentials_list', 'prompt'], func=FUNC_NAME),
117-
prompt_if_none=True,
119+
prompt_if=prompt_if_none,
118120
),
119121
help=lambda: load_text(['arg_bls_withdrawal_credentials_list', 'help'], func=FUNC_NAME),
120122
param_decls='--bls_withdrawal_credentials_list',
@@ -126,7 +128,7 @@ def _validate_credentials_match(kwargs: Dict[str, Any]) -> Optional[ValidationEr
126128
lambda: load_text(['arg_withdrawal_address', 'prompt'], func=FUNC_NAME),
127129
lambda: load_text(['arg_withdrawal_address', 'confirm'], func=FUNC_NAME),
128130
lambda: load_text(['arg_withdrawal_address', 'mismatch'], func=FUNC_NAME),
129-
prompt_if_none=True,
131+
prompt_if=prompt_if_none,
130132
),
131133
help=lambda: load_text(['arg_withdrawal_address', 'help'], func=FUNC_NAME),
132134
param_decls=['--withdrawal_address'],
@@ -180,6 +182,7 @@ def generate_bls_to_execution_change(
180182
chain_setting=chain_setting,
181183
start_index=validator_start_index,
182184
hex_withdrawal_address=withdrawal_address,
185+
compounding=False,
183186
)
184187

185188
# Check if the given old bls_withdrawal_credentials is as same as the mnemonic generated

ethstaker_deposit/cli/generate_bls_to_execution_change_keystore.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
captive_prompt_callback,
2828
choice_prompt_func,
2929
jit_option,
30+
prompt_if_none,
31+
prompt_if_other_is_none,
3032
)
3133
from ethstaker_deposit.utils.intl import (
3234
closest_match,
@@ -53,7 +55,7 @@
5355
lambda: load_text(['arg_chain', 'prompt'], func=FUNC_NAME),
5456
ALL_CHAIN_KEYS
5557
),
56-
prompt_if_other_is_none='devnet_chain_setting',
58+
prompt_if=prompt_if_other_is_none('devnet_chain_setting'),
5759
default=MAINNET,
5860
),
5961
default=MAINNET,
@@ -65,7 +67,7 @@
6567
callback=captive_prompt_callback(
6668
lambda file: validate_keystore_file(file),
6769
lambda: load_text(['arg_bls_to_execution_changes_keystore_keystore', 'prompt'], func=FUNC_NAME),
68-
prompt_if_none=True,
70+
prompt_if=prompt_if_none,
6971
),
7072
help=lambda: load_text(['arg_bls_to_execution_changes_keystore_keystore', 'help'], func=FUNC_NAME),
7173
param_decls='--keystore',
@@ -99,7 +101,7 @@
99101
lambda: load_text(['arg_withdrawal_address', 'prompt'], func=FUNC_NAME),
100102
lambda: load_text(['arg_withdrawal_address', 'confirm'], func=FUNC_NAME),
101103
lambda: load_text(['arg_withdrawal_address', 'mismatch'], func=FUNC_NAME),
102-
prompt_if_none=True,
104+
prompt_if=prompt_if_none,
103105
),
104106
help=lambda: load_text(['arg_withdrawal_address', 'help'], func=FUNC_NAME),
105107
param_decls=['--withdrawal_address'],

ethstaker_deposit/cli/generate_keys.py

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,24 @@
1818
validate_int_range,
1919
validate_password_strength,
2020
validate_withdrawal_address,
21+
validate_yesno,
22+
validate_deposit_amount,
2123
validate_devnet_chain_setting,
2224
)
2325
from ethstaker_deposit.utils.constants import (
2426
DEFAULT_VALIDATOR_KEYS_FOLDER_NAME,
2527
MIN_ACTIVATION_AMOUNT,
28+
ETH2GWEI,
2629
)
2730
from ethstaker_deposit.utils.ascii_art import RHINO_0
2831
from ethstaker_deposit.utils.click import (
2932
captive_prompt_callback,
3033
choice_prompt_func,
3134
jit_option,
35+
prompt_if_none,
36+
prompt_if_other_is_none,
37+
prompt_if_other_exists,
38+
prompt_if_other_value,
3239
)
3340
from ethstaker_deposit.utils.intl import (
3441
closest_match,
@@ -43,6 +50,9 @@
4350
)
4451

4552

53+
min_activation_amount_eth = MIN_ACTIVATION_AMOUNT // ETH2GWEI
54+
55+
4656
def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[..., Any]:
4757
'''
4858
This is a decorator that, when applied to a parent-command, implements the
@@ -71,7 +81,7 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
7181
lambda: load_text(['chain', 'prompt'], func='generate_keys_arguments_decorator'),
7282
ALL_CHAIN_KEYS
7383
),
74-
prompt_if_other_is_none='devnet_chain_setting',
84+
prompt_if=prompt_if_other_is_none('devnet_chain_setting'),
7585
default=MAINNET,
7686
),
7787
default=MAINNET,
@@ -86,7 +96,7 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
8696
lambda: load_text(['keystore_password', 'confirm'], func='generate_keys_arguments_decorator'),
8797
lambda: load_text(['keystore_password', 'mismatch'], func='generate_keys_arguments_decorator'),
8898
True,
89-
prompt_if_none=True,
99+
prompt_if=prompt_if_none,
90100
),
91101
help=lambda: load_text(['keystore_password', 'help'], func='generate_keys_arguments_decorator'),
92102
hide_input=True,
@@ -100,13 +110,40 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
100110
lambda: load_text(['arg_withdrawal_address', 'confirm'], func='generate_keys_arguments_decorator'),
101111
lambda: load_text(['arg_withdrawal_address', 'mismatch'], func='generate_keys_arguments_decorator'),
102112
default="",
103-
prompt_if_none=True,
113+
prompt_if=prompt_if_none,
104114
),
105115
default="",
106116
help=lambda: load_text(['arg_withdrawal_address', 'help'], func='generate_keys_arguments_decorator'),
107117
param_decls=['--withdrawal_address', '--execution_address', '--eth1_withdrawal_address'],
108118
prompt=False, # the callback handles the prompt
109119
),
120+
jit_option(
121+
callback=captive_prompt_callback(
122+
lambda value: validate_yesno(None, None, value),
123+
lambda: load_text(['arg_compounding', 'prompt'], func='generate_keys_arguments_decorator'),
124+
default="False",
125+
prompt_if=prompt_if_other_exists('withdrawal_address'),
126+
),
127+
default=False,
128+
help=lambda: load_text(['arg_compounding', 'help'], func='generate_keys_arguments_decorator'),
129+
param_decls='--compounding/--regular-withdrawal',
130+
prompt=False, # the callback handles the prompt
131+
type=bool,
132+
show_default=True,
133+
),
134+
jit_option(
135+
callback=captive_prompt_callback(
136+
lambda amount: validate_deposit_amount(amount),
137+
lambda: load_text(['arg_amount', 'prompt'], func='generate_keys_arguments_decorator'),
138+
default=str(min_activation_amount_eth),
139+
prompt_if=prompt_if_other_value('compounding', True),
140+
),
141+
default=str(min_activation_amount_eth),
142+
help=lambda: load_text(['arg_amount', 'help'], func='generate_keys_arguments_decorator'),
143+
param_decls='--amount',
144+
prompt=False, # the callback handles the prompt
145+
show_default=True,
146+
),
110147
jit_option(
111148
default=False,
112149
is_flag=True,
@@ -130,11 +167,13 @@ def generate_keys_arguments_decorator(function: Callable[..., Any]) -> Callable[
130167
@click.pass_context
131168
def generate_keys(ctx: click.Context, validator_start_index: int,
132169
num_validators: int, folder: str, chain: str, keystore_password: str,
133-
withdrawal_address: HexAddress, pbkdf2: bool,
170+
withdrawal_address: HexAddress, compounding: bool, amount: int, pbkdf2: bool,
134171
devnet_chain_setting: Optional[BaseChainSetting], **kwargs: Any) -> None:
135172
mnemonic = ctx.obj['mnemonic']
136173
mnemonic_password = ctx.obj['mnemonic_password']
137-
amounts = [MIN_ACTIVATION_AMOUNT] * num_validators
174+
if withdrawal_address is None or not compounding:
175+
amount = MIN_ACTIVATION_AMOUNT
176+
amounts = [amount] * num_validators
138177
folder = os.path.join(folder, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME)
139178

140179
# Get chain setting
@@ -153,6 +192,7 @@ def generate_keys(ctx: click.Context, validator_start_index: int,
153192
chain_setting=chain_setting,
154193
start_index=validator_start_index,
155194
hex_withdrawal_address=withdrawal_address,
195+
compounding=compounding,
156196
use_pbkdf2=pbkdf2
157197
)
158198

0 commit comments

Comments
 (0)