diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a4a36b9..3be85cc 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -15,9 +15,9 @@ jobs: PYTHON_VERSION: 3.8.12 BUILD_CMD: | export PYTHONHASHSEED=42 - export BUILD_FILE_NAME=operator-cli-${RELEASE_VERSION}-linux-amd64; + export BUILD_FILE_NAME=stakewise-cli-${RELEASE_VERSION}-linux-amd64; mkdir ${BUILD_FILE_NAME}; - poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data operator_cli ./operator_cli/main.py --name operator-cli --distpath ./${BUILD_FILE_NAME}; + poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data stakewise_cli ./stakewise_cli/main.py --name stakewise-cli --distpath ./${BUILD_FILE_NAME}; tar -zcvf ${BUILD_FILE_NAME}.tar.gz ./${BUILD_FILE_NAME}; mkdir /tmp/artifacts; cp ${BUILD_FILE_NAME}.tar.gz /tmp/artifacts; @@ -27,9 +27,9 @@ jobs: PYTHON_VERSION: 3.8.12 BUILD_CMD: | export PYTHONHASHSEED=42 - export BUILD_FILE_NAME=operator-cli-${RELEASE_VERSION}-darwin-amd64; + export BUILD_FILE_NAME=stakewise-cli-${RELEASE_VERSION}-darwin-amd64; mkdir ${BUILD_FILE_NAME}; - poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data operator_cli ./operator_cli/main.py --name operator-cli --distpath ./${BUILD_FILE_NAME}; + poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data stakewise_cli ./stakewise_cli/main.py --name stakewise-cli --distpath ./${BUILD_FILE_NAME}; tar -zcvf ${BUILD_FILE_NAME}.tar.gz ./${BUILD_FILE_NAME}; mkdir /tmp/artifacts; cp ${BUILD_FILE_NAME}.tar.gz /tmp/artifacts; @@ -39,9 +39,9 @@ jobs: PYTHON_VERSION: 3.8.10 BUILD_CMD: | $RELEASE_VERSION = $env:GITHUB_REF.replace('refs/tags/', '') - $BUILD_FILE_NAME = "operator-cli-" + $RELEASE_VERSION + "-windows-amd64" + $BUILD_FILE_NAME = "stakewise-cli-" + $RELEASE_VERSION + "-windows-amd64" $BUILD_FILE_NAME_PATH = ".\" + $BUILD_FILE_NAME - poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data operator_cli ./operator_cli/main.py --name operator-cli --distpath $BUILD_FILE_NAME_PATH + poetry run pyinstaller --onefile --hidden-import multiaddr.codecs.uint16be --hidden-import multiaddr.codecs.idna --collect-data stakewise_cli ./stakewise_cli/main.py --name stakewise-cli --distpath $BUILD_FILE_NAME_PATH $ZIP_FILE_NAME = $BUILD_FILE_NAME + ".zip" Compress-Archive -Path $BUILD_FILE_NAME_PATH -DestinationPath $ZIP_FILE_NAME mkdir \tmp\artifacts @@ -104,9 +104,9 @@ jobs: with: fail_on_unmatched_files: true files: | - /tmp/artifacts/ubuntu-latest/operator-cli-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz - /tmp/artifacts/ubuntu-latest/operator-cli-${{ steps.get_version.outputs.VERSION }}-linux-amd64.sha256 - /tmp/artifacts/macos-11/operator-cli-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz - /tmp/artifacts/macos-11/operator-cli-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.sha256 - /tmp/artifacts/windows-latest/operator-cli-${{ steps.get_version.outputs.VERSION }}-windows-amd64.zip - /tmp/artifacts/windows-latest/operator-cli-${{ steps.get_version.outputs.VERSION }}-windows-amd64.sha256 + /tmp/artifacts/ubuntu-latest/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-linux-amd64.tar.gz + /tmp/artifacts/ubuntu-latest/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-linux-amd64.sha256 + /tmp/artifacts/macos-11/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.tar.gz + /tmp/artifacts/macos-11/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-darwin-amd64.sha256 + /tmp/artifacts/windows-latest/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-windows-amd64.zip + /tmp/artifacts/windows-latest/stakewise-cli-${{ steps.get_version.outputs.VERSION }}-windows-amd64.sha256 diff --git a/.gitignore b/.gitignore index d16377e..bbd2101 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ local.env *.spec validator_keys/ committee/ +keys/ diff --git a/operator_cli/main.py b/operator_cli/main.py deleted file mode 100644 index 9edae58..0000000 --- a/operator_cli/main.py +++ /dev/null @@ -1,24 +0,0 @@ -import warnings - -import click - -warnings.filterwarnings("ignore") - -from operator_cli.commands.create_deposit_data import create_deposit_data # noqa: E402 -from operator_cli.commands.sync_local import sync_local # noqa: E402 -from operator_cli.commands.sync_vault import sync_vault # noqa: E402 -from operator_cli.commands.upload_deposit_data import upload_deposit_data # noqa: E402 - - -@click.group() -def cli() -> None: - pass - - -cli.add_command(create_deposit_data) -cli.add_command(upload_deposit_data) -cli.add_command(sync_vault) -cli.add_command(sync_local) - -if __name__ == "__main__": - cli() diff --git a/pyproject.toml b/pyproject.toml index 587b678..b232fbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "operator-cli" +name = "stakewise-cli" version = "1.0.0" description = "StakeWise Operator CLI is used to generate and manage ETH2 validator keys." authors = ["Dmitri Tsumak "] diff --git a/operator_cli/__init__.py b/stakewise_cli/__init__.py similarity index 100% rename from operator_cli/__init__.py rename to stakewise_cli/__init__.py diff --git a/operator_cli/commands/__init__.py b/stakewise_cli/commands/__init__.py similarity index 100% rename from operator_cli/commands/__init__.py rename to stakewise_cli/commands/__init__.py diff --git a/operator_cli/commands/create_deposit_data.py b/stakewise_cli/commands/create_deposit_data.py similarity index 90% rename from operator_cli/commands/create_deposit_data.py rename to stakewise_cli/commands/create_deposit_data.py index 1f4756a..d027770 100644 --- a/operator_cli/commands/create_deposit_data.py +++ b/stakewise_cli/commands/create_deposit_data.py @@ -3,9 +3,9 @@ import click -from operator_cli.committee_shares import create_committee_shares -from operator_cli.eth1 import generate_specification, validate_operator_address -from operator_cli.eth2 import ( +from stakewise_cli.committee_shares import create_committee_shares +from stakewise_cli.eth1 import generate_specification, validate_operator_address +from stakewise_cli.eth2 import ( LANGUAGES, VALIDATOR_DEPOSIT_AMOUNT, create_new_mnemonic, @@ -13,9 +13,9 @@ generate_unused_validator_keys, validate_mnemonic, ) -from operator_cli.ipfs import upload_deposit_data_to_ipfs -from operator_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI -from operator_cli.queries import get_ethereum_gql_client, get_stakewise_gql_client +from stakewise_cli.ipfs import upload_deposit_data_to_ipfs +from stakewise_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI +from stakewise_cli.queries import get_ethereum_gql_client, get_stakewise_gql_client @click.command(help="Creates deposit data and generates a forum post specification") diff --git a/stakewise_cli/commands/create_shard_pubkeys.py b/stakewise_cli/commands/create_shard_pubkeys.py new file mode 100644 index 0000000..80389fc --- /dev/null +++ b/stakewise_cli/commands/create_shard_pubkeys.py @@ -0,0 +1,77 @@ +import click +from Crypto.Cipher import AES, PKCS1_OAEP +from Crypto.PublicKey import RSA +from py_ecc.bls import G2ProofOfPossession +from web3 import Web3 + +from stakewise_cli.ipfs import upload_public_keys_to_ipfs + + +def validate_private_key(ctx, param, value) -> str: + try: + with open(value, "r") as f: + RSA.import_key(f.read()) + + return value + except: # noqa: E722 + pass + + raise click.BadParameter("Invalid Private Key") + + +@click.command(help="Creates public keys for operator shard") +@click.option( + "--private-key", + help="Path to the committee member private key file.", + prompt="Enter path to the committee member private key", + type=click.Path(exists=True, file_okay=True, dir_okay=False), + callback=validate_private_key, +) +@click.option( + "--shard", + help="Path to the operator shard file.", + prompt="Enter path to operator shard file", + type=click.Path(exists=True, file_okay=True, dir_okay=False), +) +def create_shard_pubkeys(shard: str, private_key: str) -> None: + try: + with open(private_key, "r") as f: + private_key = RSA.import_key(f.read()) + with open(shard, "rb") as f: + enc_session_key, nonce, tag, ciphertext = [ + f.read(x) for x in (private_key.size_in_bytes(), 16, 16, -1) + ] + except: # noqa: E722 + raise click.ClickException("Invalid operator shard file") + + # Decrypt the session key with the private RSA key + try: + cipher_rsa = PKCS1_OAEP.new(private_key) + session_key = cipher_rsa.decrypt(enc_session_key) + except: # noqa: E722 + raise click.ClickException( + "Failed to decrypt the session key. Please check whether the paths to private key and shard" + ) + + # Decrypt the data with the AES session key + cipher_aes = AES.new(session_key, AES.MODE_EAX, nonce) + try: + private_keys = cipher_aes.decrypt_and_verify(ciphertext, tag).split(b",") + except: # noqa: E722 + raise click.ClickException("Failed to decrypt the shard file. Is it corrupted?") + + public_keys = [] + with click.progressbar( + private_keys, + label="Deriving public keys for operator shard\t\t", + show_percent=False, + show_pos=True, + ) as _private_keys: + for priv_key in _private_keys: + public_keys.append(Web3.toHex(G2ProofOfPossession.SkToPk(int(priv_key)))) + + ipfs_hash = upload_public_keys_to_ipfs(public_keys) + click.echo( + f'Please share the IPFS hash in "operators" Discord chat:' + f" {click.style(ipfs_hash, fg='green')}", + ) diff --git a/operator_cli/commands/sync_local.py b/stakewise_cli/commands/sync_local.py similarity index 91% rename from operator_cli/commands/sync_local.py rename to stakewise_cli/commands/sync_local.py index 22f39aa..7568698 100644 --- a/operator_cli/commands/sync_local.py +++ b/stakewise_cli/commands/sync_local.py @@ -6,9 +6,9 @@ from requests.exceptions import ConnectionError, HTTPError from web3 import Web3 -from operator_cli.eth2 import get_beacon_client, validate_mnemonic -from operator_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI -from operator_cli.storages.local import LocalStorage +from stakewise_cli.eth2 import get_beacon_client, validate_mnemonic +from stakewise_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI +from stakewise_cli.storages.local import LocalStorage def validate_operator_address(ctx, param, value): diff --git a/operator_cli/commands/sync_vault.py b/stakewise_cli/commands/sync_vault.py similarity index 94% rename from operator_cli/commands/sync_vault.py rename to stakewise_cli/commands/sync_vault.py index 38bf3e0..cca0851 100644 --- a/operator_cli/commands/sync_vault.py +++ b/stakewise_cli/commands/sync_vault.py @@ -6,10 +6,10 @@ from requests.exceptions import ConnectionError, HTTPError from web3 import Web3 -from operator_cli.eth2 import get_beacon_client, validate_mnemonic -from operator_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI -from operator_cli.settings import VAULT_VALIDATORS_MOUNT_POINT -from operator_cli.storages.vault import Vault +from stakewise_cli.eth2 import get_beacon_client, validate_mnemonic +from stakewise_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI +from stakewise_cli.settings import VAULT_VALIDATORS_MOUNT_POINT +from stakewise_cli.storages.vault import Vault def get_vault_client() -> VaultClient: diff --git a/operator_cli/commands/upload_deposit_data.py b/stakewise_cli/commands/upload_deposit_data.py similarity index 95% rename from operator_cli/commands/upload_deposit_data.py rename to stakewise_cli/commands/upload_deposit_data.py index ec239ee..33465b6 100644 --- a/operator_cli/commands/upload_deposit_data.py +++ b/stakewise_cli/commands/upload_deposit_data.py @@ -9,17 +9,17 @@ from gql import Client from web3 import Web3 -from operator_cli.eth1 import generate_specification, validate_operator_address -from operator_cli.eth2 import verify_deposit_data -from operator_cli.ipfs import upload_deposit_data_to_ipfs -from operator_cli.merkle_tree import MerkleTree -from operator_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI -from operator_cli.queries import ( +from stakewise_cli.eth1 import generate_specification, validate_operator_address +from stakewise_cli.eth2 import verify_deposit_data +from stakewise_cli.ipfs import upload_deposit_data_to_ipfs +from stakewise_cli.merkle_tree import MerkleTree +from stakewise_cli.networks import GNOSIS_CHAIN, GOERLI, MAINNET, NETWORKS, PERM_GOERLI +from stakewise_cli.queries import ( REGISTRATIONS_QUERY, get_ethereum_gql_client, get_stakewise_gql_client, ) -from operator_cli.typings import Bytes4, Bytes32, Gwei, MerkleDepositData +from stakewise_cli.typings import Bytes4, Bytes32, Gwei, MerkleDepositData w3 = Web3() diff --git a/stakewise_cli/commands/verify_shard_pubkeys.py b/stakewise_cli/commands/verify_shard_pubkeys.py new file mode 100644 index 0000000..8e82424 --- /dev/null +++ b/stakewise_cli/commands/verify_shard_pubkeys.py @@ -0,0 +1,86 @@ +import click +from eth_typing import BLSPubkey +from web3 import Web3 + +from stakewise_cli.committee_shares import reconstruct_shared_bls_public_key +from stakewise_cli.ipfs import ipfs_fetch + + +@click.command(help="Verifies public keys for operator shards") +@click.option( + "--deposit-data-ipfs-hash", + help="IPFS hash for operator deposit data to verify.", + prompt="Enter IPFS hash for operator deposit data to verify", +) +@click.option( + "--shards-count", + help="Total number of shards to verify. With 5 committee members, the number must be at least 3.", + prompt="Enter total number of shards to verify", + type=int, +) +def verify_shard_pubkeys(deposit_data_ipfs_hash: str, shards_count: int) -> None: + submitted = 0 + shards = {} + while True: + if submitted == shards_count: + break + + index = click.prompt( + text=( + "Enter committee member position number " + "(index in stakewise.eth ENS record)" + ), + type=click.INT, + ) + if index in shards: + click.echo("The IPFS hash for such index was already submitted") + continue + + public_keys_ipfs_hash = click.prompt( + text=( + f"Enter the shard public keys IPFS hash for {index} committee member" + f" ({submitted + 1}/{shards_count})" + ), + type=click.STRING, + ).strip() + + try: + pub_keys = ipfs_fetch(public_keys_ipfs_hash) + shards[index] = [Web3.toBytes(hexstr=k) for k in pub_keys] + except: # noqa: E722 + click.secho( + f"Failed to fetch IPFS data at {public_keys_ipfs_hash}. Please try again.", + fg="red", + ) + continue + + submitted += 1 + + try: + deposit_data = ipfs_fetch(deposit_data_ipfs_hash) + deposit_data_pub_keys = [ + Web3.toBytes(hexstr=d["public_key"]) for d in deposit_data + ] + except: # noqa: E722 + raise click.ClickException( + f"Failed to fetch IPFS data at {deposit_data_ipfs_hash}. Please try again." + ) + + with click.progressbar( + enumerate(deposit_data_pub_keys), + label="Reconstructing public keys from shards\t\t", + show_percent=False, + show_pos=True, + ) as _deposit_data_pub_keys: + for i, pub_key in _deposit_data_pub_keys: + pub_key_shards = {} + for committee_index in shards: + pub_key_shards[committee_index] = BLSPubkey(shards[committee_index][i]) + + reconstructed_pub_key = reconstruct_shared_bls_public_key(pub_key_shards) + if reconstructed_pub_key != pub_key: + raise click.ClickException( + f"Failed to reconstruct public key with index {i}" + ) + + click.secho("Successfully verified operator shards", fg="green") diff --git a/operator_cli/committee_shares.py b/stakewise_cli/committee_shares.py similarity index 83% rename from operator_cli/committee_shares.py rename to stakewise_cli/committee_shares.py index 258d631..419a250 100644 --- a/operator_cli/committee_shares.py +++ b/stakewise_cli/committee_shares.py @@ -8,13 +8,15 @@ from Crypto.Cipher._mode_eax import EaxMode from Crypto.PublicKey import RSA from Crypto.Random import get_random_bytes -from eth_typing import ChecksumAddress +from eth_typing import BLSPubkey, ChecksumAddress from gql import Client from py_ecc.bls.ciphersuites import G2ProofOfPossession -from py_ecc.optimized_bls12_381.optimized_curve import curve_order +from py_ecc.bls.g2_primitives import G1_to_pubkey, pubkey_to_G1 +from py_ecc.optimized_bls12_381.optimized_curve import Z1, add, curve_order, multiply +from py_ecc.utils import prime_field_inv -from operator_cli.eth1 import get_operator_allocation_id, get_operators_committee -from operator_cli.typings import BLSPrivkey, KeyPair +from stakewise_cli.eth1 import get_operator_allocation_id, get_operators_committee +from stakewise_cli.typings import BLSPrivkey, KeyPair PRIME = curve_order @@ -126,7 +128,7 @@ def create_committee_shares( secret = ",".join(str(share) for share in committee_final_shares[i][j]) rsa_pub_key = committee[i][j] member_handler = rsa_pub_key.split(" ")[-1] - filename = f"{member_handler}-{allocation_name}.bin" + filename = f"{member_handler}-{allocation_name}.shard" enc_session_key, nonce, tag, ciphertext = rsa_encrypt( recipient_public_key=rsa_pub_key, data=secret, @@ -140,3 +142,21 @@ def create_committee_shares( bar.update(1) return committee_paths + + +def reconstruct_shared_bls_public_key(public_keys: Dict[int, BLSPubkey]) -> BLSPubkey: + """ + Reconstructs shared BLS public key. + Copied from https://github.com/dankrad/python-ibft/blob/master/bls_threshold.py + """ + r = Z1 + for i, key in public_keys.items(): + key_point = pubkey_to_G1(key) + coef = 1 + for j in public_keys: + if j != i: + coef = ( + -coef * (j + 1) * prime_field_inv(i - j, curve_order) % curve_order + ) + r = add(r, multiply(key_point, coef)) + return G1_to_pubkey(r) diff --git a/operator_cli/contracts.py b/stakewise_cli/contracts.py similarity index 97% rename from operator_cli/contracts.py rename to stakewise_cli/contracts.py index 0198b85..af41526 100644 --- a/operator_cli/contracts.py +++ b/stakewise_cli/contracts.py @@ -2,7 +2,7 @@ from web3.contract import Contract from web3.middleware import geth_poa_middleware -from operator_cli.networks import NETWORKS +from stakewise_cli.networks import NETWORKS def get_web3_client(network: str) -> Web3: diff --git a/operator_cli/eth1.py b/stakewise_cli/eth1.py similarity index 95% rename from operator_cli/eth1.py rename to stakewise_cli/eth1.py index 20b3253..1bfd02d 100644 --- a/operator_cli/eth1.py +++ b/stakewise_cli/eth1.py @@ -7,10 +7,10 @@ from gql import Client as GqlClient from web3 import Web3 -from operator_cli.contracts import get_ens_node_id, get_ens_resolver, get_web3_client -from operator_cli.ipfs import ipfs_fetch -from operator_cli.networks import GNOSIS_CHAIN, MAINNET, NETWORKS -from operator_cli.queries import OPERATOR_QUERY, VALIDATORS_QUERY +from stakewise_cli.contracts import get_ens_node_id, get_ens_resolver, get_web3_client +from stakewise_cli.ipfs import ipfs_fetch +from stakewise_cli.networks import GNOSIS_CHAIN, MAINNET, NETWORKS +from stakewise_cli.queries import OPERATOR_QUERY, VALIDATORS_QUERY @backoff.on_exception(backoff.expo, Exception, max_time=180) diff --git a/operator_cli/eth2.py b/stakewise_cli/eth2.py similarity index 98% rename from operator_cli/eth2.py rename to stakewise_cli/eth2.py index 1aca08e..22cea75 100644 --- a/operator_cli/eth2.py +++ b/stakewise_cli/eth2.py @@ -30,9 +30,9 @@ from web3.beacon import Beacon from web3.types import Wei -from operator_cli.merkle_tree import MerkleTree -from operator_cli.queries import REGISTRATIONS_QUERY -from operator_cli.typings import ( +from stakewise_cli.merkle_tree import MerkleTree +from stakewise_cli.queries import REGISTRATIONS_QUERY +from stakewise_cli.typings import ( BLSPrivkey, Bytes4, Bytes32, diff --git a/operator_cli/ipfs.py b/stakewise_cli/ipfs.py similarity index 60% rename from operator_cli/ipfs.py rename to stakewise_cli/ipfs.py index e89f5f4..1b2ee67 100644 --- a/operator_cli/ipfs.py +++ b/stakewise_cli/ipfs.py @@ -5,15 +5,16 @@ import click import ipfshttpclient import requests +from eth_typing import HexStr -from operator_cli.settings import ( +from stakewise_cli.settings import ( IPFS_FETCH_ENDPOINTS, IPFS_PIN_ENDPOINTS, IPFS_PINATA_API_KEY, IPFS_PINATA_PIN_ENDPOINT, IPFS_PINATA_SECRET_KEY, ) -from operator_cli.typings import MerkleDepositData +from stakewise_cli.typings import MerkleDepositData def add_ipfs_prefix(ipfs_id: str) -> str: @@ -60,7 +61,7 @@ def upload_deposit_data_to_ipfs(deposit_datum: List[MerkleDepositData]) -> str: click.echo("Failed to submit deposit data to Pinata") if not ipfs_ids: - raise click.ClickException("Failed to submit claims to IPFS") + raise click.ClickException("Failed to submit deposit data to IPFS") ipfs_ids = set(map(add_ipfs_prefix, ipfs_ids)) if len(ipfs_ids) != 1: @@ -70,6 +71,49 @@ def upload_deposit_data_to_ipfs(deposit_datum: List[MerkleDepositData]) -> str: @backoff.on_exception(backoff.expo, Exception, max_time=180) +def upload_public_keys_to_ipfs(bls_public_keys: List[HexStr]) -> str: + """Submits BLS public keys to IPFS.""" + ipfs_ids = [] + for pin_endpoint in IPFS_PIN_ENDPOINTS: + try: + with ipfshttpclient.connect(pin_endpoint) as client: + ipfs_id = client.add_json(bls_public_keys) + client.pin.add(ipfs_id) + ipfs_ids.append(ipfs_id) + except Exception as e: + click.echo(e) + click.echo(f"Failed to submit public keys to {pin_endpoint}") + + if IPFS_PINATA_API_KEY and IPFS_PINATA_SECRET_KEY: + headers = { + "pinata_api_key": IPFS_PINATA_API_KEY, + "pinata_secret_api_key": IPFS_PINATA_SECRET_KEY, + "Content-Type": "application/json", + } + try: + response = requests.post( + headers=headers, + url=IPFS_PINATA_PIN_ENDPOINT, + data=json.dumps({"pinataContent": bls_public_keys}, sort_keys=True), + ) + response.raise_for_status() + ipfs_id = response.json()["IpfsHash"] + ipfs_ids.append(ipfs_id) + except Exception as e: # noqa: E722 + click.echo(e) + click.echo("Failed to submit public keys to Pinata") + + if not ipfs_ids: + raise click.ClickException("Failed to submit public keys to IPFS") + + ipfs_ids = set(map(add_ipfs_prefix, ipfs_ids)) + if len(ipfs_ids) != 1: + raise click.ClickException(f"Received different ipfs IDs: {','.join(ipfs_ids)}") + + return ipfs_ids.pop() + + +@backoff.on_exception(backoff.expo, Exception, max_time=60) def ipfs_fetch(ipfs_id: str) -> Any: """Fetches data from IPFS.""" ipfs_id = ipfs_id.replace("ipfs://", "").replace("/ipfs/", "") diff --git a/stakewise_cli/main.py b/stakewise_cli/main.py new file mode 100644 index 0000000..77cd932 --- /dev/null +++ b/stakewise_cli/main.py @@ -0,0 +1,32 @@ +import warnings + +import click + +warnings.filterwarnings("ignore") + +from stakewise_cli.commands.create_deposit_data import create_deposit_data # noqa: E402 +from stakewise_cli.commands.create_shard_pubkeys import ( # noqa: E402 + create_shard_pubkeys, +) +from stakewise_cli.commands.sync_local import sync_local # noqa: E402 +from stakewise_cli.commands.sync_vault import sync_vault # noqa: E402 +from stakewise_cli.commands.upload_deposit_data import upload_deposit_data # noqa: E402 +from stakewise_cli.commands.verify_shard_pubkeys import ( # noqa: E402 + verify_shard_pubkeys, +) + + +@click.group() +def cli() -> None: + pass + + +cli.add_command(create_deposit_data) +cli.add_command(upload_deposit_data) +cli.add_command(sync_vault) +cli.add_command(sync_local) +cli.add_command(create_shard_pubkeys) +cli.add_command(verify_shard_pubkeys) + +if __name__ == "__main__": + cli() diff --git a/operator_cli/merkle_tree.py b/stakewise_cli/merkle_tree.py similarity index 100% rename from operator_cli/merkle_tree.py rename to stakewise_cli/merkle_tree.py diff --git a/operator_cli/networks.py b/stakewise_cli/networks.py similarity index 100% rename from operator_cli/networks.py rename to stakewise_cli/networks.py diff --git a/operator_cli/queries.py b/stakewise_cli/queries.py similarity index 96% rename from operator_cli/queries.py rename to stakewise_cli/queries.py index 9c7ccd0..4b0a589 100644 --- a/operator_cli/queries.py +++ b/stakewise_cli/queries.py @@ -1,7 +1,7 @@ from gql import Client, gql from gql.transport.requests import RequestsHTTPTransport -from operator_cli.networks import NETWORKS +from stakewise_cli.networks import NETWORKS def get_ethereum_gql_client(network: str) -> Client: diff --git a/operator_cli/settings.py b/stakewise_cli/settings.py similarity index 100% rename from operator_cli/settings.py rename to stakewise_cli/settings.py diff --git a/operator_cli/storages/__init__.py b/stakewise_cli/storages/__init__.py similarity index 100% rename from operator_cli/storages/__init__.py rename to stakewise_cli/storages/__init__.py diff --git a/operator_cli/storages/local.py b/stakewise_cli/storages/local.py similarity index 96% rename from operator_cli/storages/local.py rename to stakewise_cli/storages/local.py index 517d7f0..5a01094 100644 --- a/operator_cli/storages/local.py +++ b/stakewise_cli/storages/local.py @@ -11,13 +11,13 @@ from staking_deposit.key_handling.keystore import ScryptKeystore from web3 import Web3 -from operator_cli.eth1 import ( +from stakewise_cli.eth1 import ( get_operator_deposit_data_ipfs_link, is_validator_registered, ) -from operator_cli.eth2 import generate_password, get_mnemonic_signing_key -from operator_cli.ipfs import ipfs_fetch -from operator_cli.queries import get_stakewise_gql_client +from stakewise_cli.eth2 import generate_password, get_mnemonic_signing_key +from stakewise_cli.ipfs import ipfs_fetch +from stakewise_cli.queries import get_stakewise_gql_client class LocalStorage(object): diff --git a/operator_cli/storages/vault.py b/stakewise_cli/storages/vault.py similarity index 98% rename from operator_cli/storages/vault.py rename to stakewise_cli/storages/vault.py index e7db3ee..5652cee 100644 --- a/operator_cli/storages/vault.py +++ b/stakewise_cli/storages/vault.py @@ -15,21 +15,21 @@ from web3 import Web3 from web3.beacon import Beacon -from operator_cli.eth1 import ( +from stakewise_cli.eth1 import ( get_operator_deposit_data_ipfs_link, is_validator_registered, ) -from operator_cli.eth2 import ( +from stakewise_cli.eth2 import ( EXITED_STATUSES, generate_password, get_mnemonic_signing_key, get_validators, ) -from operator_cli.ipfs import ipfs_fetch -from operator_cli.networks import NETWORKS -from operator_cli.queries import get_stakewise_gql_client -from operator_cli.settings import VAULT_VALIDATORS_MOUNT_POINT -from operator_cli.typings import SigningKey, VaultKeystore, VaultState +from stakewise_cli.ipfs import ipfs_fetch +from stakewise_cli.networks import NETWORKS +from stakewise_cli.queries import get_stakewise_gql_client +from stakewise_cli.settings import VAULT_VALIDATORS_MOUNT_POINT +from stakewise_cli.typings import SigningKey, VaultKeystore, VaultState VALIDATOR_POLICY = """ path "%s/%s/*" { diff --git a/operator_cli/typings.py b/stakewise_cli/typings.py similarity index 100% rename from operator_cli/typings.py rename to stakewise_cli/typings.py diff --git a/operator_cli/word_lists/chinese_simplified.txt b/stakewise_cli/word_lists/chinese_simplified.txt similarity index 100% rename from operator_cli/word_lists/chinese_simplified.txt rename to stakewise_cli/word_lists/chinese_simplified.txt diff --git a/operator_cli/word_lists/chinese_traditional.txt b/stakewise_cli/word_lists/chinese_traditional.txt similarity index 100% rename from operator_cli/word_lists/chinese_traditional.txt rename to stakewise_cli/word_lists/chinese_traditional.txt diff --git a/operator_cli/word_lists/czech.txt b/stakewise_cli/word_lists/czech.txt similarity index 100% rename from operator_cli/word_lists/czech.txt rename to stakewise_cli/word_lists/czech.txt diff --git a/operator_cli/word_lists/english.txt b/stakewise_cli/word_lists/english.txt similarity index 100% rename from operator_cli/word_lists/english.txt rename to stakewise_cli/word_lists/english.txt diff --git a/operator_cli/word_lists/italian.txt b/stakewise_cli/word_lists/italian.txt similarity index 100% rename from operator_cli/word_lists/italian.txt rename to stakewise_cli/word_lists/italian.txt diff --git a/operator_cli/word_lists/korean.txt b/stakewise_cli/word_lists/korean.txt similarity index 100% rename from operator_cli/word_lists/korean.txt rename to stakewise_cli/word_lists/korean.txt diff --git a/operator_cli/word_lists/portuguese.txt b/stakewise_cli/word_lists/portuguese.txt similarity index 100% rename from operator_cli/word_lists/portuguese.txt rename to stakewise_cli/word_lists/portuguese.txt diff --git a/operator_cli/word_lists/spanish.txt b/stakewise_cli/word_lists/spanish.txt similarity index 100% rename from operator_cli/word_lists/spanish.txt rename to stakewise_cli/word_lists/spanish.txt