Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 101 additions & 11 deletions src/commands/node_manager_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@
import click
from eth_typing import ChecksumAddress
from sw_utils import InterruptHandler
from web3.types import Gwei

from src.common.clients import close_clients, setup_clients
from src.common.logging import LOG_LEVELS, setup_logging
from src.common.protocol_config import update_oracles_cache
from src.common.typings import ValidatorType
from src.common.utils import log_verbose
from src.common.validators import validate_eth_address
from src.config.networks import AVAILABLE_NETWORKS, NETWORKS
from src.common.validators import (
validate_eth_address,
validate_max_validator_balance_gwei,
)
from src.config.networks import AVAILABLE_NETWORKS, MAINNET, NETWORKS
from src.config.settings import (
DEFAULT_CONSENSUS_ENDPOINT,
DEFAULT_EXECUTION_ENDPOINT,
Expand All @@ -21,14 +27,68 @@
)
from src.node_manager.startup_check import startup_checks
from src.node_manager.tasks import NodeManagerTask
from src.validators.database import NetworkValidatorCrud
from src.validators.keystores.load import load_keystore

logger = logging.getLogger(__name__)


@click.option(
'--withdrawals-address',
'--keystores-password-file',
type=click.Path(exists=True, file_okay=True, dir_okay=False),
envvar='KEYSTORES_PASSWORD_FILE',
help='Absolute path to the password file for decrypting keystores.',
)
@click.option(
'--keystores-dir',
type=click.Path(exists=True, file_okay=False, dir_okay=True),
envvar='KEYSTORES_DIR',
help='Absolute path to the directory with all the encrypted keystores.',
)
@click.option(
'--wallet-password-file',
type=click.Path(exists=True, file_okay=True, dir_okay=False),
envvar='WALLET_PASSWORD_FILE',
help='Absolute path to the wallet password file.',
)
@click.option(
'--wallet-file',
type=click.Path(exists=True, file_okay=True, dir_okay=False),
envvar='WALLET_FILE',
help='Absolute path to the wallet.',
)
@click.option(
'--max-validator-balance-gwei',
type=int,
envvar='MAX_VALIDATOR_BALANCE_GWEI',
help=f'The maximum validator balance in Gwei. '
f'Default is {NETWORKS[MAINNET].MAX_VALIDATOR_BALANCE_GWEI} Gwei',
callback=validate_max_validator_balance_gwei,
Comment on lines +63 to +66
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI help states a default value, but the option does not set a Click default=..., so max_validator_balance_gwei will be None when omitted. If settings.configure(...) applies None literally, this can override the intended network default and break V2 deposit splitting. Set an explicit Click default (preferably derived from the selected network), or avoid passing the field to settings.configure when the option is unset.

Suggested change
envvar='MAX_VALIDATOR_BALANCE_GWEI',
help=f'The maximum validator balance in Gwei. '
f'Default is {NETWORKS[MAINNET].MAX_VALIDATOR_BALANCE_GWEI} Gwei',
callback=validate_max_validator_balance_gwei,
envvar='MAX_VALIDATOR_BALANCE_GWEI',
default=NETWORKS[MAINNET].MAX_VALIDATOR_BALANCE_GWEI,
help=f'The maximum validator balance in Gwei. '
f'Default is {NETWORKS[MAINNET].MAX_VALIDATOR_BALANCE_GWEI} Gwei',
callback=validate_max_validator_balance_gwei,
show_default=True,

Copilot uses AI. Check for mistakes.
)
@click.option(
'--validator-type',
help='Type of the validators to register:'
f' {ValidatorType.V1.value} or {ValidatorType.V2.value}.',
envvar='VALIDATOR_TYPE',
default=ValidatorType.V2.value,
type=click.Choice(
[x.value for x in ValidatorType],
case_sensitive=False,
),
callback=lambda ctx, param, value: ValidatorType(value),
show_default=True,
)
@click.option(
'--max-fee-per-gas-gwei',
type=int,
envvar='MAX_FEE_PER_GAS_GWEI',
help=f'Maximum fee per gas for transactions. '
f'Default is {NETWORKS[MAINNET].MAX_FEE_PER_GAS_GWEI} Gwei',
)
@click.option(
'--operator-address',
callback=validate_eth_address,
envvar='WITHDRAWALS_ADDRESS',
envvar='OPERATOR_ADDRESS',
prompt='Enter your operator withdrawals (cold wallet) address',
help='The operator withdrawals (cold wallet) address.',
)
Expand Down Expand Up @@ -96,7 +156,7 @@
show_default=True,
)
@click.command(help='Start node manager operator service')
# pylint: disable-next=too-many-arguments
# pylint: disable-next=too-many-arguments,too-many-locals
def node_manager_start(
consensus_endpoints: str,
execution_endpoints: str,
Expand All @@ -106,7 +166,14 @@ def node_manager_start(
log_level: str,
log_format: str,
network: str,
withdrawals_address: ChecksumAddress,
operator_address: ChecksumAddress,
max_fee_per_gas_gwei: int | None,
validator_type: ValidatorType,
max_validator_balance_gwei: int | None,
wallet_file: str | None,
wallet_password_file: str | None,
keystores_dir: str | None,
keystores_password_file: str | None,
) -> None:
network_config = NETWORKS[network]
vault = network_config.COMMUNITY_VAULT_CONTRACT_ADDRESS
Expand All @@ -122,27 +189,50 @@ def node_manager_start(
verbose=verbose,
log_level=log_level,
log_format=log_format,
max_fee_per_gas_gwei=max_fee_per_gas_gwei,
validator_type=validator_type,
max_validator_balance_gwei=(
Gwei(max_validator_balance_gwei) if max_validator_balance_gwei else None
),
Comment on lines +194 to +196
Copy link

Copilot AI Apr 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI help states a default value, but the option does not set a Click default=..., so max_validator_balance_gwei will be None when omitted. If settings.configure(...) applies None literally, this can override the intended network default and break V2 deposit splitting. Set an explicit Click default (preferably derived from the selected network), or avoid passing the field to settings.configure when the option is unset.

Copilot uses AI. Check for mistakes.
keystores_dir=keystores_dir,
keystores_password_file=keystores_password_file,
wallet_file=wallet_file,
wallet_password_file=wallet_password_file,
)

try:
asyncio.run(_start(withdrawals_address))
asyncio.run(_start(operator_address))
except Exception as e:
log_verbose(e)
sys.exit(1)


async def _start(withdrawals_address: ChecksumAddress) -> None:
async def _start(
operator_address: ChecksumAddress,
) -> None:
setup_logging()
await setup_clients()

if not settings.skip_startup_checks:
await startup_checks(withdrawals_address)
await startup_checks(operator_address)
try:
NetworkValidatorCrud().setup()

keystore = await load_keystore()

# start operator tasks
logger.info('Updating oracles cache...')
await update_oracles_cache()

logger.info(
'Started node manager service, polling eligibility for %s',
withdrawals_address,
operator_address,
)
with InterruptHandler() as interrupt_handler:
await NodeManagerTask(withdrawals_address).run(interrupt_handler)
task = NodeManagerTask(
operator_address=operator_address,
keystore=keystore,
)
await task.run(interrupt_handler)
finally:
await close_clients()
Loading