diff --git a/stakewise_cli/commands/create_referrals_proposal.py b/stakewise_cli/commands/create_referrals_proposal.py index 0a7f4ee..a1213ef 100644 --- a/stakewise_cli/commands/create_referrals_proposal.py +++ b/stakewise_cli/commands/create_referrals_proposal.py @@ -9,14 +9,7 @@ from stakewise_cli.coingecko import get_average_range_price from stakewise_cli.eth1 import get_block_timestamp, get_referrals, get_web3_client from stakewise_cli.ipfs import upload_to_ipfs -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, - NETWORKS, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET, NETWORKS from stakewise_cli.proposals import generate_referrals_swise_specification from stakewise_cli.queries import get_ethereum_gql_client, get_stakewise_gql_client @@ -36,7 +29,7 @@ help="The network to generate the referral proposal for", prompt="Enter the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/commands/sync_db.py b/stakewise_cli/commands/sync_db.py index 4603126..8004fba 100644 --- a/stakewise_cli/commands/sync_db.py +++ b/stakewise_cli/commands/sync_db.py @@ -2,13 +2,7 @@ from eth_typing import ChecksumAddress from stakewise_cli.eth2 import prompt_beacon_client, validate_mnemonic -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET from stakewise_cli.storages.database import Database, check_db_connection from stakewise_cli.validators import validate_db_uri, validate_operator_address from stakewise_cli.web3signer import Web3SignerManager @@ -21,7 +15,7 @@ help="The network you are targeting.", prompt="Please choose the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/commands/sync_local.py b/stakewise_cli/commands/sync_local.py index fdf2deb..669c7c1 100644 --- a/stakewise_cli/commands/sync_local.py +++ b/stakewise_cli/commands/sync_local.py @@ -4,13 +4,7 @@ from eth_typing import ChecksumAddress from stakewise_cli.eth2 import validate_mnemonic -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET from stakewise_cli.storages.local import LocalStorage from stakewise_cli.validators import validate_operator_address @@ -22,7 +16,7 @@ help="The network of ETH2 you are targeting.", prompt="Please choose the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/commands/sync_validator_keys.py b/stakewise_cli/commands/sync_validator_keys.py index 2a083b5..a6b740c 100644 --- a/stakewise_cli/commands/sync_validator_keys.py +++ b/stakewise_cli/commands/sync_validator_keys.py @@ -1,11 +1,13 @@ +import json import os from os import mkdir from os.path import exists, join -from typing import List +from typing import Dict, List import click import yaml +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET, NETWORKS from stakewise_cli.storages.database import Database, check_db_connection from stakewise_cli.utils import is_lists_equal from stakewise_cli.validators import validate_db_uri, validate_env_name @@ -13,10 +15,21 @@ PUBLIC_KEYS_CSV_FILENAME = "validator_keys.csv" LIGHTHOUSE_CONFIG_FILENAME = "validator_definitions.yml" SIGNER_CONFIG_FILENAME = "signer_keys.yml" +SIGNER_PROPOSER_CONFIG_FILENAME = "proposerConfig.json" WEB3SIGNER_URL_ENV = "WEB3SIGNER_URL" @click.command(help="Synchronizes validator public keys from the database") +@click.option( + "--network", + default=MAINNET, + help="The network to generate the deposit data for", + prompt="Enter the network name", + type=click.Choice( + AVAILABLE_NETWORKS, + case_sensitive=False, + ), +) @click.option( "--db-url", help="The database connection address.", @@ -39,8 +52,18 @@ default=WEB3SIGNER_URL_ENV, callback=validate_env_name, ) +@click.option( + "--solo-fees-file", + help="The path to solo validator's fee distribution path", + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) def sync_validator_keys( - db_url: str, index: int, output_dir: str, web3signer_url_env: str + network: str, + db_url: str, + index: int, + output_dir: str, + web3signer_url_env: str, + solo_fees_file: str, ) -> None: """ The command is running by the init container in validator pods. @@ -67,10 +90,19 @@ def sync_validator_keys( ) return + default_fee_recipient = NETWORKS[network]["FEE_DISTRIBUTION_CONTRACT_ADDRESS"] + solo_fee_mapping = {} + if solo_fees_file: + with open(solo_fees_file) as f: + solo_fee_mapping = json.load(f) + # save lighthouse config web3signer_url = os.environ[web3signer_url_env] lighthouse_config = _generate_lighthouse_config( - public_keys=keys, web3signer_url=web3signer_url + public_keys=keys, + web3signer_url=web3signer_url, + default_fee_recipient=default_fee_recipient, + solo_fee_mapping=solo_fee_mapping, ) with open(join(output_dir, LIGHTHOUSE_CONFIG_FILENAME), "w") as f: f.write(lighthouse_config) @@ -79,6 +111,12 @@ def sync_validator_keys( signer_keys_config = _generate_signer_keys_config(public_keys=keys) with open(join(output_dir, SIGNER_CONFIG_FILENAME), "w") as f: f.write(signer_keys_config) + proposer_config = _generate_proposer_config( + default_fee_recipient=default_fee_recipient, + solo_fee_mapping=solo_fee_mapping, + ) + with open(join(output_dir, SIGNER_PROPOSER_CONFIG_FILENAME), "w") as f: + f.write(proposer_config) click.secho( f"The validator now uses {len(keys)} public keys.\n", @@ -87,7 +125,12 @@ def sync_validator_keys( ) -def _generate_lighthouse_config(public_keys: List[str], web3signer_url: str) -> str: +def _generate_lighthouse_config( + public_keys: List[str], + web3signer_url: str, + default_fee_recipient: str, + solo_fee_mapping: Dict[str, str], +) -> str: """ Generate config for Lighthouse clients """ @@ -97,6 +140,9 @@ def _generate_lighthouse_config(public_keys: List[str], web3signer_url: str) -> "voting_public_key": public_key, "type": "web3signer", "url": web3signer_url, + "suggested_fee_recipient": solo_fee_mapping.get( + public_key, default_fee_recipient + ), } for public_key in public_keys ] @@ -121,3 +167,31 @@ def _generate_signer_keys_config(public_keys: List[str]) -> str: """ keys = ",".join([f'"{public_key}"' for public_key in public_keys]) return f"""validators-external-signer-public-keys: [{keys}]""" + + +def _generate_proposer_config( + default_fee_recipient: str, solo_fee_mapping: Dict[str, str] +) -> str: + """ + Generate config for Teku and Prysm clients + """ + config = { + "proposer_config": { + **{ + public_key: { + "fee_recipient": fee_recipient, + "builder": { + "enabled": True, + }, + } + for public_key, fee_recipient in solo_fee_mapping.items() + } + }, + "default_config": { + "fee_recipient": default_fee_recipient, + "builder": { + "enabled": True, + }, + }, + } + return json.dumps(config) diff --git a/stakewise_cli/commands/sync_vault.py b/stakewise_cli/commands/sync_vault.py index ce0254e..5942f51 100644 --- a/stakewise_cli/commands/sync_vault.py +++ b/stakewise_cli/commands/sync_vault.py @@ -5,13 +5,7 @@ from requests.exceptions import ConnectionError from stakewise_cli.eth2 import prompt_beacon_client, validate_mnemonic -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET from stakewise_cli.settings import VAULT_VALIDATORS_MOUNT_POINT from stakewise_cli.storages.vault import Vault from stakewise_cli.validators import validate_operator_address @@ -38,7 +32,7 @@ def get_kubernetes_api_server() -> str: help="The network of ETH2 you are targeting.", prompt="Please choose the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/commands/upload_deposit_data.py b/stakewise_cli/commands/upload_deposit_data.py index 75ec03f..5e6c21b 100644 --- a/stakewise_cli/commands/upload_deposit_data.py +++ b/stakewise_cli/commands/upload_deposit_data.py @@ -11,14 +11,7 @@ from stakewise_cli.eth2 import get_registered_public_keys, verify_deposit_data from stakewise_cli.ipfs import upload_to_ipfs from stakewise_cli.merkle_tree import MerkleTree -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, - NETWORKS, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET, NETWORKS from stakewise_cli.queries import get_ethereum_gql_client, get_stakewise_gql_client from stakewise_cli.typings import Bytes4, Bytes32, Gwei, MerkleDepositData from stakewise_cli.validators import validate_operator_address_prompt @@ -124,7 +117,7 @@ def process_file( help="The network of ETH2 you are targeting.", prompt="Please choose the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/commands/verify_deposit_data.py b/stakewise_cli/commands/verify_deposit_data.py index 2e8efdd..2a59d11 100644 --- a/stakewise_cli/commands/verify_deposit_data.py +++ b/stakewise_cli/commands/verify_deposit_data.py @@ -8,14 +8,7 @@ from stakewise_cli.eth2 import get_deposit_data_roots, get_registered_public_keys from stakewise_cli.ipfs import ipfs_fetch from stakewise_cli.merkle_tree import MerkleTree -from stakewise_cli.networks import ( - GNOSIS_CHAIN, - GOERLI, - HARBOUR_GOERLI, - HARBOUR_MAINNET, - MAINNET, - NETWORKS, -) +from stakewise_cli.networks import AVAILABLE_NETWORKS, MAINNET, NETWORKS from stakewise_cli.queries import get_ethereum_gql_client from stakewise_cli.typings import Bytes32, Gwei @@ -32,7 +25,7 @@ help="The network deposit data was generated for.", prompt="Please choose the network name", type=click.Choice( - [MAINNET, GOERLI, HARBOUR_MAINNET, HARBOUR_GOERLI, GNOSIS_CHAIN], + AVAILABLE_NETWORKS, case_sensitive=False, ), ) diff --git a/stakewise_cli/networks.py b/stakewise_cli/networks.py index 22af8f1..c633c2d 100644 --- a/stakewise_cli/networks.py +++ b/stakewise_cli/networks.py @@ -6,6 +6,9 @@ HARBOUR_GOERLI = "harbour_goerli" GNOSIS_CHAIN = "gnosis" +# aliases +PRATER = "prater" + NETWORKS = { MAINNET: dict( STAKEWISE_SUBGRAPH_URL=config( @@ -29,6 +32,7 @@ DAO_ENS_NAME="stakewise.eth", ENS_RESOLVER_CONTRACT_ADDRESS="0x4976fb03C32e5B8cfe2b6cCB31c09Ba78EBaBa41", SWISE_TOKEN_CONTRACT_ADDRESS="0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2", + FEE_DISTRIBUTION_CONTRACT_ADDRESS="0x6b333B20fBae3c5c0969dd02176e30802e2fbBdB", OPERATORS_COMMITTEE_ENS_KEY="operators_committee", IS_POA=False, ), @@ -54,6 +58,7 @@ DAO_ENS_NAME="stakewise.eth", ENS_RESOLVER_CONTRACT_ADDRESS="0x4B1488B7a6B320d2D721406204aBc3eeAa9AD329", SWISE_TOKEN_CONTRACT_ADDRESS="0x0e2497aACec2755d831E4AFDEA25B4ef1B823855", + FEE_DISTRIBUTION_CONTRACT_ADDRESS="0x6A9d30e05C6832E868390F155388c7d97A6faEAC", OPERATORS_COMMITTEE_ENS_KEY="operators_committee", IS_POA=True, ), @@ -79,6 +84,7 @@ DAO_ENS_NAME="", ENS_RESOLVER_CONTRACT_ADDRESS="", SWISE_TOKEN_CONTRACT_ADDRESS="0x48C3399719B582dD63eB5AADf12A40B4C3f52FA2", + FEE_DISTRIBUTION_CONTRACT_ADDRESS="0x0000000000000000000000000000000000000000", OPERATORS_COMMITTEE_ENS_KEY="", IS_POA=False, ), @@ -104,6 +110,7 @@ DAO_ENS_NAME="", ENS_RESOLVER_CONTRACT_ADDRESS="", SWISE_TOKEN_CONTRACT_ADDRESS="0x0e2497aACec2755d831E4AFDEA25B4ef1B823855", + FEE_DISTRIBUTION_CONTRACT_ADDRESS="0x0000000000000000000000000000000000000000", OPERATORS_COMMITTEE_ENS_KEY="", IS_POA=True, ), @@ -127,7 +134,13 @@ DAO_ENS_NAME="", ENS_RESOLVER_CONTRACT_ADDRESS="", SWISE_TOKEN_CONTRACT_ADDRESS="0xfdA94F056346d2320d4B5E468D6Ad099b2277746", + FEE_DISTRIBUTION_CONTRACT_ADDRESS="0x0000000000000000000000000000000000000000", OPERATORS_COMMITTEE_ENS_KEY="", IS_POA=True, ), } + +# Alias +NETWORKS[PRATER] = NETWORKS[GOERLI] + +AVAILABLE_NETWORKS = NETWORKS.keys() diff --git a/stakewise_cli/tests/test_sync_validator_keys.py b/stakewise_cli/tests/test_sync_validator_keys.py index cf533a1..e99d21f 100644 --- a/stakewise_cli/tests/test_sync_validator_keys.py +++ b/stakewise_cli/tests/test_sync_validator_keys.py @@ -9,6 +9,9 @@ from stakewise_cli.commands.sync_validator_keys import sync_validator_keys from stakewise_cli.eth2 import WORD_LISTS_PATH, get_mnemonic_signing_key +from stakewise_cli.networks import MAINNET, NETWORKS + +from .factories import faker w3 = Web3() @@ -38,17 +41,25 @@ def get_public_keys(mnemonic, keys_count): class TestCommand(unittest.TestCase): def test_sync_validator_keys(self, *mocks): db_url = "postgresql://username:pass@hostname/dbname" + network = MAINNET index = 1 runner = CliRunner() + solo_pub_key, solo_address = faker.public_key(), faker.eth_address() args = [ + "--network", + network, "--index", index, "--db-url", db_url, "--output-dir", "./valdata", + "--solo-fees-file", + "./solo-fees.json", ] with runner.isolated_filesystem(): + with open("./solo-fees.json", "w") as f: + f.writelines('{"%s":"%s"}' % (solo_pub_key, solo_address)) result = runner.invoke(sync_validator_keys, args) assert result.exit_code == 0 @@ -56,11 +67,13 @@ def test_sync_validator_keys(self, *mocks): f"The validator now uses {keys_count} public keys." == result.output.strip() ) + with open("./valdata/validator_definitions.yml") as f: s = """---""" for public_key in public_keys: s += f""" - enabled: true + suggested_fee_recipient: \'{NETWORKS[network]["FEE_DISTRIBUTION_CONTRACT_ADDRESS"]}\' type: web3signer url: {web3_signer_url} voting_public_key: \'{public_key}\'""" @@ -72,6 +85,19 @@ def test_sync_validator_keys(self, *mocks): ff = f.read() assert ff == s, (ff, s) + with open("./valdata/proposerConfig.json") as f: + s = ( + '{"proposer_config": {"%s": {"fee_recipient": "%s", "builder": {"enabled": true}}}, "default_config": {"fee_recipient": "%s", "builder": {"enabled": true}}}' + % ( + solo_pub_key, + solo_address, + NETWORKS[network]["FEE_DISTRIBUTION_CONTRACT_ADDRESS"], + ) + ) + ff = f.read() + + assert ff == s, (ff, s) + result = runner.invoke(sync_validator_keys, args) assert result.exit_code == 0 assert "Keys already synced to the last version." == result.output.strip()