Skip to content

Commit

Permalink
Merge key-manager to operator (#152)
Browse files Browse the repository at this point in the history
* Merge key-manager to operator

* Add PG Deps

* Merge commands

* Fix

* Move tests

* Move tests

---------

Signed-off-by: antares-sw <[email protected]>
  • Loading branch information
antares-sw authored Aug 29, 2023
1 parent 5a3118a commit 4d7f39f
Show file tree
Hide file tree
Showing 19 changed files with 1,202 additions and 153 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ GIT_SHA
build
database
*.db
.DS_Store
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ ENV PATH="$POETRY_HOME/bin:$VENV_PATH/bin:/root/.cargo/bin:$PATH"
FROM python-base as builder-base

RUN apk upgrade --no-cache
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev curl libgcc libstdc++
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev openssl-dev curl libgcc libstdc++ postgresql-libs postgresql-dev
RUN curl https://sh.rustup.rs -sSf | \
sh -s -- --default-toolchain stable -y
RUN rm -rf /var/cache/apt/*
Expand Down
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,80 @@ Keystores for vault {vault} successfully recovered to {keystores_dir}
> Note: For security purposes, make sure to protect your mnemonic as it can be used to generate your validator keys.
> Always verify the network and endpoints before running the command.
### Web3Signer infrastructure commands
#### 1. Update database
The command encrypts and loads validator keys from keystore files into the database
```bash
./v3-operator update-db --db-url postgresql://postgres:postgres@localhost:5432/web3signer --keystores-dir ./data/keystores --keystores-password-file ./data/keystores/password.txt
Loading keystores... [####################################] 10/10
Encrypting database keys...
Generated 10 validator keys, upload them to the database? [Y/n]: Y
The database contains 10 validator keys.
Save decryption key: '<DECRYPTION KEYS>'
```
##### update-db options
- `--keystores-dir` - The directory with validator keys in the EIP-2335 standard. Defaults to ./data/keystores.
- `--keystores-password-file` - The path to file with password for encrypting the keystores. Defaults to ./data/keystores/password.txt.
- `--db-url` - The database connection address.
- `--encryption-key` - The key for encrypting database record. If you are upload new keystores use the same encryption key.
- `--no-confirm` - Skips confirmation messages when provided.
**NB! You must store the decryption key in a secure place.
It will allow you to upload new keystores in the existing database**
#### 2. Sync validator configs
Creates validator configuration files for Lighthouse, Prysm, and Teku clients to sign data using keys form database.
```bash
./v3-operator sync-validator
Enter the recipient address for MEV & priority fees: 0xB31...1
Enter the endpoint of the web3signer service: https://web3signer-example.com
Enter the database connection string, ex. 'postgresql://username:pass@hostname/dbname': postgresql://postgres:postgres@localhost/web3signer
Enter the total number of validators connected to the web3signer: 30
Enter the validator index to generate the configuration files: 5
Done. Generated configs with 50 keys for validator #5.
Validator definitions for Lighthouse saved to data/configs/validator_definitions.yml file.
Signer keys for Teku\Prysm saved to data/configs/signer_keys.yml file.
Proposer config for Teku\Prysm saved to data/configs/proposer_config.json file.
```
##### sync-validator options
- `--validator-index` - The validator index to generate the configuration files.
- `--total-validators` - The total number of validators connected to the web3signer.
- `--db-url` - The database connection address.
- `--web3signer-endpoint` - The endpoint of the web3signer service.
- `--fee-recipient` - The recipient address for MEV & priority fees.
- `--disable-proposal-builder` - Disable proposal builder for Teku and Prysm clients.
- `--output-dir` - The directory to save configuration files. Defaults to ./data/configs.
#### 3. Sync Web3Signer config
The command is running by the init container in web3signer pods.
Fetch and decrypt keys for web3signer and store them as keypairs in the output_dir.
Set `DECRYPTION_KEY` env, use value generated by `update-db` command
```bash
./v3-operator sync-web3signer
Enter the folder where web3signer keystores will be saved: /data/web3signer
Enter the database connection string, ex. 'postgresql://username:pass@hostname/dbname': postgresql://postgres:postgres@localhost/web3signer
Web3Signer now uses 7 private keys.
```
##### sync-web3signer options
- `--db-url` - The database connection address.
- `--output-dir` - The folder where Web3Signer keystores will be saved.
- `--decryption-key-env` - The environment variable with the decryption key for private keys in the database.
## Monitoring Operator with Prometheus
Operator supports monitoring using Prometheus by providing a `/metrics` endpoint that Prometheus can scrape to gather
Expand Down
338 changes: 186 additions & 152 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ click = "==8.1.3"
tomli = "~2"
eciespy = "==0.3.13"
prometheus-client = "==0.17.0"
psycopg2 = "==2.9.7"
pyyaml = "==6.0.1"

[tool.poetry.group.dev.dependencies]
pylint = "==2.16.2"
Expand All @@ -33,6 +35,7 @@ pyinstaller = "==5.7.0"
faker = "==18.10.1"
flake8-datetime-utcnow-plugin = "==0.1.2"
flake8-print = "==5.0.0"
types-pyyaml = "==6.0.12.11"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
183 changes: 183 additions & 0 deletions src/commands/sync_validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import json
from pathlib import Path

import click
import yaml
from eth_typing import HexStr

from src.common.validators import validate_db_uri, validate_eth_address
from src.key_manager.database import Database, check_db_connection

VALIDATOR_DEFINITIONS_FILENAME = 'validator_definitions.yml'
SIGNER_KEYS_FILENAME = 'signer_keys.yml'
PROPOSER_CONFIG_FILENAME = 'proposer_config.json'


@click.option(
'--validator-index',
help='The validator index to generate the configuration files.',
prompt='Enter the validator index to generate the configuration files',
type=int,
)
@click.option(
'--total-validators',
help='The total number of validators connected to the web3signer.',
prompt='Enter the total number of validators connected to the web3signer',
type=click.IntRange(min=1),
)
@click.option(
'--db-url',
help='The database connection address.',
prompt="Enter the database connection string, ex. 'postgresql://username:pass@hostname/dbname'",
callback=validate_db_uri,
)
@click.option(
'--web3signer-endpoint',
help='The endpoint of the web3signer service.',
prompt='Enter the endpoint of the web3signer service',
type=str,
)
@click.option(
'--fee-recipient',
help='The recipient address for MEV & priority fees.',
prompt='Enter the recipient address for MEV & priority fees',
type=str,
callback=validate_eth_address,
)
@click.option(
'--disable-proposal-builder',
is_flag=True,
default=False,
help='Disable proposal builder for Teku and Prysm clients.',
)
@click.option(
'--output-dir',
required=False,
help='The directory to save configuration files. Defaults to ./data/configs.',
default='./data/configs',
type=click.Path(exists=False, file_okay=False, dir_okay=True),
)
@click.command(
help='Creates validator configuration files for Lighthouse, '
'Prysm, and Teku clients to sign data using keys from database.'
)
# pylint: disable-next=too-many-arguments,too-many-locals
def sync_validator(
validator_index: int,
total_validators: int,
db_url: str,
web3signer_endpoint: str,
fee_recipient: str,
disable_proposal_builder: bool,
output_dir: str,
) -> None:
check_db_connection(db_url)
check_validator_index(validator_index, total_validators)

database = Database(db_url=db_url)
public_keys_count = database.fetch_public_keys_count()

keys_per_validator = public_keys_count // total_validators

start_index = keys_per_validator * validator_index
if validator_index == total_validators - 1:
end_index = public_keys_count
else:
end_index = start_index + keys_per_validator

public_keys = database.fetch_public_keys_by_range(start_index=start_index, end_index=end_index)

if not public_keys:
raise click.ClickException('Database does not contain in range')

Path.mkdir(Path(output_dir), exist_ok=True, parents=True)

# lighthouse
validator_definitions_filepath = str(Path(output_dir, VALIDATOR_DEFINITIONS_FILENAME))
_generate_lighthouse_config(
public_keys=public_keys,
web3signer_url=web3signer_endpoint,
fee_recipient=fee_recipient,
filepath=validator_definitions_filepath,
)

# teku/prysm
signer_keys_filepath = str(Path(output_dir, SIGNER_KEYS_FILENAME))
_generate_signer_keys_config(public_keys=public_keys, filepath=signer_keys_filepath)

proposer_config_filepath = str(Path(output_dir, PROPOSER_CONFIG_FILENAME))
_generate_proposer_config(
fee_recipient=fee_recipient,
proposal_builder_enabled=not disable_proposal_builder,
filepath=proposer_config_filepath,
)

click.clear()
click.secho(
f'Done. '
f'Generated configs with {len(public_keys)} keys for validator #{validator_index}.\n'
f'Validator definitions for Lighthouse saved to {validator_definitions_filepath} file.\n'
f'Signer keys for Teku\\Prysm saved to {signer_keys_filepath} file.\n'
f'Proposer config for Teku\\Prysm saved to {proposer_config_filepath} file.\n',
bold=True,
fg='green',
)


def _generate_lighthouse_config(
public_keys: list[HexStr],
web3signer_url: str,
fee_recipient: str,
filepath: str,
) -> None:
"""
Generate config for Lighthouse clients
"""
items = [
{
'enabled': True,
'voting_public_key': public_key,
'type': 'web3signer',
'url': web3signer_url,
'suggested_fee_recipient': fee_recipient,
}
for public_key in public_keys
]

with open(filepath, 'w', encoding='utf-8') as f:
yaml.dump(items, f, explicit_start=True)


def _generate_signer_keys_config(public_keys: list[HexStr], filepath: str) -> None:
"""
Generate config for Teku and Prysm clients
"""
keys = ','.join([f'"{public_key}"' for public_key in public_keys])
config = f"""validators-external-signer-public-keys: [{keys}]"""
with open(filepath, 'w', encoding='utf-8') as f:
f.write(config)


def _generate_proposer_config(
fee_recipient: str,
proposal_builder_enabled: bool,
filepath: str,
) -> None:
"""
Generate proposal config for Teku and Prysm clients
"""
config = {
'default_config': {
'fee_recipient': fee_recipient,
'builder': {
'enabled': proposal_builder_enabled,
},
},
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(config, f, ensure_ascii=False, indent=4)


def check_validator_index(validator_index, total_validators):
if not total_validators or total_validators <= validator_index:
raise click.BadParameter('validator index must be less than total validators')
99 changes: 99 additions & 0 deletions src/commands/sync_web3signer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import glob
import os
from os import mkdir
from os.path import exists
from typing import List

import click
import yaml
from eth_utils import add_0x_prefix
from web3 import Web3
from web3.types import HexStr

from src.common.contrib import is_lists_equal
from src.common.validators import validate_db_uri, validate_env_name
from src.key_manager.database import Database, check_db_connection
from src.key_manager.encryptor import Encryptor

DECRYPTION_KEY_ENV = 'DECRYPTION_KEY'


@click.option(
'--db-url',
help='The database connection address.',
prompt="Enter the database connection string, ex. 'postgresql://username:pass@hostname/dbname'",
callback=validate_db_uri,
)
@click.option(
'--output-dir',
help='The folder where web3signer keystores will be saved.',
prompt='Enter the folder where web3signer keystores will be saved',
type=click.Path(exists=False, file_okay=False, dir_okay=True),
)
@click.option(
'--decryption-key-env',
help='The environment variable with the decryption key for private keys in the database.',
default=DECRYPTION_KEY_ENV,
callback=validate_env_name,
)
@click.command(help='Synchronizes web3signer private keys from the database')
# pylint: disable-next=too-many-locals
def sync_web3signer(db_url: str, output_dir: str, decryption_key_env: str) -> None:
"""
The command is running by the init container in web3signer pods.
Fetch and decrypt keys for web3signer and store them as keypairs in the output_dir.
"""
check_db_connection(db_url)

database = Database(db_url=db_url)
keys_records = database.fetch_keys()

# decrypt private keys
decryption_key = os.environ[decryption_key_env]
decryptor = Encryptor(decryption_key)
private_keys: List[str] = []
for key_record in keys_records:
key = decryptor.decrypt(data=key_record.private_key, nonce=key_record.nonce)
key_hex = Web3.to_hex(int(key))
# pylint: disable-next=unsubscriptable-object
key_hex = HexStr(key_hex[2:].zfill(64)) # pad missing leading zeros
private_keys.append(add_0x_prefix(key_hex))

if not exists(output_dir):
mkdir(output_dir)

# check current keys
current_keys = []
for filename in glob.glob(os.path.join(output_dir, '*.yaml')):
with open(filename, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f.read())
current_keys.append(content.get('privateKey'))

if is_lists_equal(current_keys, private_keys):
click.secho(
'Keys already synced to the last version.\n',
bold=True,
fg='green',
)
return

# save key files
for index, private_key in enumerate(private_keys):
filename = f'key_{index}.yaml'
with open(os.path.join(output_dir, filename), 'w', encoding='utf-8') as f:
f.write(_generate_key_file(private_key))

click.secho(
f'Web3Signer now uses {len(private_keys)} private keys.\n',
bold=True,
fg='green',
)


def _generate_key_file(private_key: str) -> str:
item = {
'type': 'file-raw',
'keyType': 'BLS',
'privateKey': private_key,
}
return yaml.dump(item)
Loading

0 comments on commit 4d7f39f

Please sign in to comment.