Skip to content

Commit

Permalink
Support for loading validator keys from Hashicorp Vault
Browse files Browse the repository at this point in the history
  • Loading branch information
mksh committed Sep 4, 2023
1 parent 669b3fe commit b65e5df
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 61 deletions.
36 changes: 29 additions & 7 deletions src/commands/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
update_unused_validator_keys_metric,
)
from src.validators.tasks import load_genesis_validators, register_validators
from src.validators.utils import load_deposit_data, load_keystores
from src.validators.utils import load_deposit_data, load_validator_keys

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -151,6 +151,22 @@
prompt='Enter the vault address',
help='Address of the vault to register validators for.',
)
@click.option(
'--hashicorp-vault-addr',
callback=validate_eth_address,
envvar='HASHICORP_VAULT_ADDR',
help='Address of the vault to register validators for.',
)
@click.option(
'--hashicorp-vault-token',
envvar='HASHICORP_VAULT_TOKEN',
help='Token for accessing Hashicopr vault.',
)
@click.option(
'--hashicorp-vault-key',
envvar='HASHICORP_VAULT_KEY',
help='Key of the secret where signing keys are stored. Compatible with format recognized by Web3Signer',
)
@click.command(help='Start operator service')
# pylint: disable-next=too-many-arguments,too-many-locals
def start(
Expand All @@ -170,6 +186,9 @@ def start(
hot_wallet_password_file: str | None,
max_fee_per_gas_gwei: int,
database_dir: str | None,
hashicorp_vault_addr: str | None,
hashicorp_vault_token: str | None,
hashicorp_vault_key: str | None,
) -> None:
vault_config = VaultConfig(vault, Path(data_dir))
if network is None:
Expand All @@ -193,6 +212,9 @@ def start(
hot_wallet_password_file=hot_wallet_password_file,
max_fee_per_gas_gwei=max_fee_per_gas_gwei,
database_dir=database_dir,
hashicorp_vault_addr=hashicorp_vault_addr,
hashicorp_vault_key=hashicorp_vault_key,
hashicorp_vault_token=hashicorp_vault_token,
)

try:
Expand All @@ -213,9 +235,9 @@ async def main() -> None:
# load network validators from ipfs dump
await load_genesis_validators()

# load keystores
keystores = load_keystores()
if not keystores:
# load validator keys
validator_keys = load_validator_keys()
if not validator_keys:
return

# load deposit data
Expand All @@ -235,7 +257,7 @@ async def main() -> None:
await metrics_server()

# process outdated exit signatures
asyncio.create_task(update_exit_signatures_periodically(keystores))
asyncio.create_task(update_exit_signatures_periodically(validator_keys))

logger.info('Started operator service')
with InterruptHandler() as interrupt_handler:
Expand All @@ -249,8 +271,8 @@ async def main() -> None:
# process new network validators
await network_validators_scanner.process_new_events(to_block)
# check and register new validators
await update_unused_validator_keys_metric(keystores, deposit_data)
await register_validators(keystores, deposit_data)
await update_unused_validator_keys_metric(validator_keys, deposit_data)
await register_validators(validator_keys, deposit_data)

# submit harvest vault transaction
if settings.harvest_vault:
Expand Down
40 changes: 31 additions & 9 deletions src/commands/validators_exit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from src.common.validators import validate_eth_address
from src.common.vault_config import VaultConfig
from src.config.settings import AVAILABLE_NETWORKS, NETWORKS, settings
from src.validators.typings import BLSPrivkey, Keystores
from src.validators.utils import load_keystores
from src.validators.typings import BLSPrivkey, ValidatorKeys
from src.validators.utils import load_validator_keys


@dataclass
class ExitKeystore:
class ExitKeyMaterial:
private_key: BLSPrivkey
public_key: HexStr
index: int
Expand Down Expand Up @@ -73,6 +73,22 @@ class ExitKeystore:
envvar='VERBOSE',
is_flag=True,
)
@click.option(
'--hashicorp-vault-addr',
callback=validate_eth_address,
envvar='HASHICORP_VAULT_ADDR',
help='Address of the vault to register validators for.',
)
@click.option(
'--hashicorp-vault-token',
envvar='HASHICORP_VAULT_TOKEN',
help='Token for accessing Hashicopr vault.',
)
@click.option(
'--hashicorp-vault-key',
envvar='HASHICORP_VAULT_KEY',
help='Key of the secret where signing keys are stored. Compatible with format recognized by Web3Signer',
)
@click.command(help='Performs a voluntary exit for active vault validators.')
# pylint: disable-next=too-many-arguments
def validators_exit(
Expand All @@ -82,6 +98,9 @@ def validators_exit(
consensus_endpoints: str,
data_dir: str,
verbose: bool,
hashicorp_vault_addr: str | None,
hashicorp_vault_token: str | None,
hashicorp_vault_key: str | None,
) -> None:
# pylint: disable=duplicate-code
vault_config = VaultConfig(vault, Path(data_dir))
Expand All @@ -95,6 +114,9 @@ def validators_exit(
vault_dir=vault_config.vault_dir,
consensus_endpoints=consensus_endpoints,
verbose=verbose,
hashicorp_vault_addr=hashicorp_vault_addr,
hashicorp_vault_key=hashicorp_vault_key,
hashicorp_vault_token=hashicorp_vault_token,
)
try:
asyncio.run(main(count))
Expand All @@ -103,12 +125,12 @@ def validators_exit(


async def main(count: int | None) -> None:
keystores = load_keystores()
keystores = load_validator_keys()
if not keystores:
raise click.ClickException('Keystores not found.')
fork = await consensus_client.get_consensus_fork()

exit_keystores = await _get_exit_keystores(keystores)
exit_keystores = await _get_exit_key_material(keystores)
if not exit_keystores:
raise click.ClickException('There are no active validators.')

Expand Down Expand Up @@ -164,10 +186,10 @@ def _get_exit_signature(
return exit_signature


async def _get_exit_keystores(keystores: Keystores) -> list[ExitKeystore]:
async def _get_exit_key_material(validator_keys: ValidatorKeys) -> list[ExitKeyMaterial]:
"""Fetches validators consensus info."""
results = []
public_keys = list(keystores.keys())
public_keys = list(validator_keys.keys())
exited_statuses = [x.value for x in EXITING_STATUSES]

for i in range(0, len(public_keys), settings.validators_fetch_chunk_size):
Expand All @@ -179,9 +201,9 @@ async def _get_exit_keystores(keystores: Keystores) -> list[ExitKeystore]:
continue

results.append(
ExitKeystore(
ExitKeyMaterial(
public_key=beacon_validator['validator']['pubkey'],
private_key=keystores[beacon_validator['validator']['pubkey']],
private_key=validator_keys[beacon_validator['validator']['pubkey']],
index=int(beacon_validator['index']),
)
)
Expand Down
14 changes: 13 additions & 1 deletion src/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from decouple import config as decouple_config
from web3 import Web3
from web3.types import ChecksumAddress

from src.config.networks import GOERLI, NETWORKS, NetworkConfig
from src.config.typings import HashicorpVaultSettings

DATA_DIR = Path.home() / '.stakewise'

Expand Down Expand Up @@ -47,6 +47,7 @@ class Settings(metaclass=Singleton):
ipfs_fetch_endpoints: list[str]
validators_fetch_chunk_size: int
sentry_dsn: str
hashicorp_vault: HashicorpVaultSettings | None

# pylint: disable-next=too-many-arguments,too-many-locals
def set(
Expand All @@ -67,6 +68,9 @@ def set(
hot_wallet_file: str | None = None,
hot_wallet_password_file: str | None = None,
database_dir: str | None = None,
hashicorp_vault_addr: str | None = None,
hashicorp_vault_token: str | None = None,
hashicorp_vault_key: str | None = None,
):
self.vault = Web3.to_checksum_address(vault)
self.vault_dir = vault_dir
Expand Down Expand Up @@ -96,6 +100,14 @@ def set(
else vault_dir / 'keystores' / 'password.txt'
)

# vault
if hashicorp_vault_addr:
self.hashicorp_vault = HashicorpVaultSettings(
hashicorp_vault_addr, hashicorp_vault_key, hashicorp_vault_token
)
else:
self.hashicorp_vault = None

# hot wallet
self.hot_wallet_file = (
Path(hot_wallet_file) if hot_wallet_file else vault_dir / 'wallet' / 'wallet.json'
Expand Down
8 changes: 8 additions & 0 deletions src/config/typings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from dataclasses import dataclass


@dataclass
class HashicorpVaultSettings:
addr: str
key: str
token: str
8 changes: 4 additions & 4 deletions src/exits/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from src.exits.typings import OraclesApproval, SignatureRotationRequest
from src.exits.utils import send_signature_rotation_requests
from src.validators.signing import get_exit_signature_shards
from src.validators.typings import Keystores
from src.validators.typings import ValidatorKeys

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -79,7 +79,7 @@ async def wait_oracle_signature_update(


async def update_exit_signatures(
keystores: Keystores, oracles: Oracles, outdated_indexes: list[int]
keystores: ValidatorKeys, oracles: Oracles, outdated_indexes: list[int]
) -> HexStr:
"""Fetches update signature requests from oracles."""
exit_rotation_batch_limit = oracles.validators_exit_rotation_batch_limit
Expand Down Expand Up @@ -133,7 +133,7 @@ async def _fetch_exit_signature_block(oracle_endpoint: str) -> BlockNumber | Non


async def get_oracles_approval(
oracles: Oracles, keystores: Keystores, validators: dict[int, HexStr]
oracles: Oracles, keystores: ValidatorKeys, validators: dict[int, HexStr]
) -> OraclesApproval:
"""Fetches approval from oracles."""
fork = await consensus_client.get_consensus_fork()
Expand Down Expand Up @@ -168,7 +168,7 @@ async def get_oracles_approval(
)


async def update_exit_signatures_periodically(keystores: Keystores):
async def update_exit_signatures_periodically(keystores: ValidatorKeys):
# Oracle may have lag if operator was stopped
# during `update_exit_signatures_periodically` process.
# Wait all oracles sync.
Expand Down
6 changes: 3 additions & 3 deletions src/validators/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from src.validators.database import NetworkValidatorCrud
from src.validators.typings import (
DepositData,
Keystores,
ValidatorKeys,
NetworkValidator,
OraclesApproval,
Validator,
Expand Down Expand Up @@ -162,7 +162,7 @@ async def check_deposit_data_root(deposit_data_root: str) -> None:


async def get_available_validators(
keystores: Keystores, deposit_data: DepositData, count: int
keystores: ValidatorKeys, deposit_data: DepositData, count: int
) -> list[Validator]:
"""Fetches vault's available validators."""
await check_deposit_data_root(deposit_data.tree.root)
Expand Down Expand Up @@ -195,7 +195,7 @@ async def get_available_validators(


async def update_unused_validator_keys_metric(
keystores: Keystores, deposit_data: DepositData
keystores: ValidatorKeys, deposit_data: DepositData
) -> int:
try:
await check_deposit_data_root(deposit_data.tree.root)
Expand Down
6 changes: 3 additions & 3 deletions src/validators/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from src.validators.typings import (
ApprovalRequest,
DepositData,
Keystores,
ValidatorKeys,
NetworkValidator,
OraclesApproval,
Validator,
Expand All @@ -36,7 +36,7 @@


# pylint: disable-next=too-many-locals
async def register_validators(keystores: Keystores, deposit_data: DepositData) -> None:
async def register_validators(keystores: ValidatorKeys, deposit_data: DepositData) -> None:
"""Registers vault validators."""
vault_balance, update_state_call = await get_withdrawable_assets()
if settings.network == GNOSIS:
Expand Down Expand Up @@ -124,7 +124,7 @@ async def register_validators(keystores: Keystores, deposit_data: DepositData) -

async def create_approval_request(
oracles: Oracles,
keystores: Keystores,
keystores: ValidatorKeys,
validators: list[Validator],
registry_root: Bytes32,
multi_proof: MultiProof,
Expand Down
2 changes: 1 addition & 1 deletion src/validators/typings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sw_utils.typings import Bytes32

BLSPrivkey = NewType('BLSPrivkey', bytes)
Keystores = NewType('Keystores', dict[HexStr, BLSPrivkey])
ValidatorKeys = NewType('Keystores', dict[HexStr, BLSPrivkey])


@dataclass
Expand Down
Loading

0 comments on commit b65e5df

Please sign in to comment.