Skip to content

Commit

Permalink
Add --verify-committee-file command (#24)
Browse files Browse the repository at this point in the history
* Add --verify-committee-file command

* Fix linting

* Update

* Update

* Add public keys creation, verification commands

* Update message for create shard pubkeys

Co-authored-by: Dmitri Tsumak <[email protected]>
  • Loading branch information
unxnn and tsudmi authored Mar 6, 2022
1 parent 0d3504d commit b493c5b
Show file tree
Hide file tree
Showing 34 changed files with 321 additions and 85 deletions.
24 changes: 12 additions & 12 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ local.env
*.spec
validator_keys/
committee/
keys/
24 changes: 0 additions & 24 deletions operator_cli/main.py

This file was deleted.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
Expand Down
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@

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,
generate_merkle_deposit_datum,
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")
Expand Down
77 changes: 77 additions & 0 deletions stakewise_cli/commands/create_shard_pubkeys.py
Original file line number Diff line number Diff line change
@@ -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')}",
)
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
86 changes: 86 additions & 0 deletions stakewise_cli/commands/verify_shard_pubkeys.py
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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)
2 changes: 1 addition & 1 deletion operator_cli/contracts.py → stakewise_cli/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading

0 comments on commit b493c5b

Please sign in to comment.