diff --git a/.gitignore b/.gitignore index 2c6c39f..7f10f4d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -# Eth2 specs: -eth2.0-specs/ +validator_keys # Python testing & linting: venv/ diff --git a/Makefile b/Makefile index 4a6fd07..a1ee6f0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,7 @@ clean: find . -name .mypy_cache -exec rm -rf {} \; find . -name .pytest_cache -exec rm -rf {} \; -install_test: +install: python3 -m venv venv; . venv/bin/activate; pip3 install -r requirements.txt test: diff --git a/requirements.txt b/requirements.txt index eb0cfbd..d1bd984 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,8 @@ py_ecc >= 2.0.0 pycryptodome>=3.6.6 # For AES, scrypt dataclasses == 0.6 +click == 7.0 +ssz==0.2.3 # Testing pytest>=4.4 diff --git a/src/deposit.py b/src/deposit.py new file mode 100644 index 0000000..3f15e94 --- /dev/null +++ b/src/deposit.py @@ -0,0 +1,72 @@ +import os +import click + +from key_handling.key_derivation.mnemonic import ( + get_languages, + get_mnemonic, +) +from utils.credentials import ( + mnemonic_to_credentials, + export_keystores, + export_deposit_data_json, +) +from utils.constants import WORD_LISTS_PATH + +words_path = os.path.join(os.getcwd(), WORD_LISTS_PATH) +languages = get_languages(words_path) + + +def generate_mnemonic(language: str, words_path: str) -> str: + mnemonic = get_mnemonic(language=language, words_path=words_path) + test_mnemonic = '' + while mnemonic != test_mnemonic: + click.clear() + click.echo('This is your seed phrase. Write it down and store it safely, it is the ONLY way to retrieve your deposit.') # noqa: E501 + click.echo('\n\n%s\n\n' % mnemonic) + click.pause('Press any key when you have written down your mnemonic.') + + click.clear() + test_mnemonic = click.prompt('Please type your mnemonic (separated by spaces) to confirm you have written it down\n\n') # noqa: E501 + test_mnemonic = test_mnemonic.lower() + click.clear() + return mnemonic + + +@click.command() +@click.option( + '--num_validators', + prompt='Please choose how many validators you wish to run', + required=True, + type=int, +) +@click.password_option(prompt='Type the password that secures your validator keystore(s)') +@click.option( + '--mnemonic_language', + prompt='Please choose your mnemonic language', + type=click.Choice(languages, case_sensitive=False), # type: ignore + default='english', +) +@click.option( + '--folder', + type=click.Path(exists=True, file_okay=False, dir_okay=True), + default=os.getcwd() +) +def main(num_validators: int, mnemonic_language: str, password: str, folder: str): + mnemonic = generate_mnemonic(mnemonic_language, words_path) + amounts = [32 * 10 ** 9] * num_validators + folder = os.path.join(folder, 'validator_keys') + if not os.path.exists(folder): + os.mkdir(folder) + click.clear() + click.echo('Creating your keys.') + credentials = mnemonic_to_credentials(mnemonic=mnemonic, num_keys=num_validators, amounts=amounts) + click.echo('Saving your keystores.') + export_keystores(credentials=credentials, password=password, folder=folder) + click.echo('Creating your deposits.') + export_deposit_data_json(credentials=credentials, folder=folder) + click.echo('\nSuccess!\nYour keys can be found at: %s' % folder) + click.pause('\n\nPress any key.') + + +if __name__ == '__main__': + main() diff --git a/src/key_derivation/__init__.py b/src/key_handling/__init__.py similarity index 100% rename from src/key_derivation/__init__.py rename to src/key_handling/__init__.py diff --git a/src/key_handling/key_derivation/__init__.py b/src/key_handling/key_derivation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/key_derivation/mnemonic.py b/src/key_handling/key_derivation/mnemonic.py similarity index 79% rename from src/key_derivation/mnemonic.py rename to src/key_handling/key_derivation/mnemonic.py index 425a17a..064c621 100644 --- a/src/key_derivation/mnemonic.py +++ b/src/key_handling/key_derivation/mnemonic.py @@ -1,4 +1,4 @@ -from os import walk +import os from unicodedata import normalize from secrets import randbits from typing import ( @@ -11,11 +11,9 @@ from utils.crypto import ( PBKDF2, ) -word_lists_path = './key_derivation/word_lists/' - -def _get_word_list(language: str): - return open('%s%s.txt' % (word_lists_path, language)).readlines() +def _get_word_list(language: str, path: str): + return open('%s%s.txt' % (path, language)).readlines() def _get_word(*, word_list, index: int) -> str: @@ -32,16 +30,16 @@ def get_seed(*, mnemonic: str, password: str='') -> bytes: return PBKDF2(password=mnemonic, salt=salt, dklen=64, c=2048, prf='sha512') -def get_languages(path: str=word_lists_path) -> List[str]: +def get_languages(path) -> List[str]: """ Walk the `path` and list all the languages with word-lists available. """ - (_, _, filenames) = next(walk(path)) + (_, _, filenames) = next(os.walk(path)) filenames = [name[:-4] for name in filenames] return filenames -def get_mnemonic(*, language: str, entropy: Optional[bytes]=None) -> str: +def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str: """ Returns a mnemonic string in a given `language` based on `entropy`. """ @@ -55,7 +53,7 @@ def get_mnemonic(*, language: str, entropy: Optional[bytes]=None) -> str: entropy_bits += checksum entropy_length += checksum_length mnemonic = [] - word_list = _get_word_list(language) + word_list = _get_word_list(language, words_path) for i in range(entropy_length // 11 - 1, -1, -1): index = (entropy_bits >> i * 11) & 2**11 - 1 word = _get_word(word_list=word_list, index=index) diff --git a/src/key_derivation/path.py b/src/key_handling/key_derivation/path.py similarity index 82% rename from src/key_derivation/path.py rename to src/key_handling/key_derivation/path.py index 507fa3a..101b6d6 100644 --- a/src/key_derivation/path.py +++ b/src/key_handling/key_derivation/path.py @@ -18,11 +18,11 @@ def path_to_nodes(path: str) -> List[int]: return [int(index) for index in indices] -def mnemonic_and_path_to_key(*, mnemonic: str, password: str, path: str) -> int: +def mnemonic_and_path_to_key(*, mnemonic: str, path: str, password: str='') -> int: """ Returns the SK at position `path` secures with `password` derived from `mnemonic`. """ - seed = get_seed(mnemonic=mnemonic, password='') + seed = get_seed(mnemonic=mnemonic, password=password) sk = derive_master_SK(seed) for node in path_to_nodes(path): sk = derive_child_SK(parent_SK=sk, index=node) diff --git a/src/key_derivation/tree.py b/src/key_handling/key_derivation/tree.py similarity index 100% rename from src/key_derivation/tree.py rename to src/key_handling/key_derivation/tree.py diff --git a/src/key_derivation/word_lists/chinese_simplified.txt b/src/key_handling/key_derivation/word_lists/chinese_simplified.txt similarity index 100% rename from src/key_derivation/word_lists/chinese_simplified.txt rename to src/key_handling/key_derivation/word_lists/chinese_simplified.txt diff --git a/src/key_derivation/word_lists/chinese_traditional.txt b/src/key_handling/key_derivation/word_lists/chinese_traditional.txt similarity index 100% rename from src/key_derivation/word_lists/chinese_traditional.txt rename to src/key_handling/key_derivation/word_lists/chinese_traditional.txt diff --git a/src/key_derivation/word_lists/czech.txt b/src/key_handling/key_derivation/word_lists/czech.txt similarity index 100% rename from src/key_derivation/word_lists/czech.txt rename to src/key_handling/key_derivation/word_lists/czech.txt diff --git a/src/key_derivation/word_lists/english.txt b/src/key_handling/key_derivation/word_lists/english.txt similarity index 100% rename from src/key_derivation/word_lists/english.txt rename to src/key_handling/key_derivation/word_lists/english.txt diff --git a/src/key_derivation/word_lists/italian.txt b/src/key_handling/key_derivation/word_lists/italian.txt similarity index 100% rename from src/key_derivation/word_lists/italian.txt rename to src/key_handling/key_derivation/word_lists/italian.txt diff --git a/src/key_derivation/word_lists/korean.txt b/src/key_handling/key_derivation/word_lists/korean.txt similarity index 100% rename from src/key_derivation/word_lists/korean.txt rename to src/key_handling/key_derivation/word_lists/korean.txt diff --git a/src/key_derivation/word_lists/spanish.txt b/src/key_handling/key_derivation/word_lists/spanish.txt similarity index 100% rename from src/key_derivation/word_lists/spanish.txt rename to src/key_handling/key_derivation/word_lists/spanish.txt diff --git a/src/keystore.py b/src/key_handling/keystore.py similarity index 100% rename from src/keystore.py rename to src/key_handling/keystore.py diff --git a/src/utils/constants.py b/src/utils/constants.py new file mode 100644 index 0000000..0afb0d9 --- /dev/null +++ b/src/utils/constants.py @@ -0,0 +1,4 @@ +DOMAIN_DEPOSIT = bytes.fromhex('03000000') +GENESIS_FORK_VERSION = bytes.fromhex('00000000') + +WORD_LISTS_PATH = 'src/key_handling/key_derivation/word_lists/' diff --git a/src/utils/credentials.py b/src/utils/credentials.py new file mode 100644 index 0000000..7db2f4f --- /dev/null +++ b/src/utils/credentials.py @@ -0,0 +1,87 @@ +import os +import time +import json +from typing import List +from py_ecc.bls import G2ProofOfPossession as bls + +from key_handling.key_derivation.path import mnemonic_and_path_to_key +from key_handling.keystore import ScryptKeystore +from utils.crypto import SHA256 +from utils.ssz import ( + compute_domain, + compute_signing_root, + DepositData, + SignedDepositData, +) + + +class ValidatorCredentials: + def __init__(self, *, mnemonic: str, index: int, amount: int): + self.signing_key_path = 'm/12381/3600/%s/0' % index + self.signing_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=self.signing_key_path) + self.withdrawal_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=self.signing_key_path + '/0') + self.amount = amount + + @property + def signing_pk(self): + return bls.PrivToPub(self.signing_sk) + + @property + def withdrawal_pk(self): + return bls.PrivToPub(self.withdrawal_sk) + + def signing_keystore(self, password: str) -> ScryptKeystore: + secret = self.signing_sk.to_bytes(32, 'big') + return ScryptKeystore.encrypt(secret=secret, password=password, path=self.signing_key_path) + + def save_signing_keystore(self, password: str, folder: str): + keystore = self.signing_keystore(password) + filefolder = os.path.join(folder, 'keystore-%s-%i.json' % (keystore.path.replace('/', '_'), time.time())) + keystore.save(filefolder) + + +def mnemonic_to_credentials(*, mnemonic: str, num_keys: int, + amounts: List[int], start_index: int=0,) -> List[ValidatorCredentials]: + assert len(amounts) == num_keys + key_indices = range(start_index, start_index + num_keys) + credentials = [ValidatorCredentials(mnemonic=mnemonic, index=index, amount=amounts[index]) + for index in key_indices] + return credentials + + +def export_keystores(*, credentials: List[ValidatorCredentials], password: str, folder: str): + for credential in credentials: + credential.save_signing_keystore(password=password, folder=folder) + + +def sign_deposit_data(deposit_data: DepositData, sk: int) -> SignedDepositData: + ''' + Given a DepositData, it signs its root and returns a SignedDepositData + ''' + assert bls.PrivToPub(sk) == deposit_data.pubkey + domain = compute_domain() + signing_root = compute_signing_root(deposit_data, domain) + signed_deposit_data = SignedDepositData( + **deposit_data.as_dict(), + signature=bls.Sign(sk, signing_root) + ) + return signed_deposit_data + + +def export_deposit_data_json(*, credentials: List[ValidatorCredentials], folder: str): + deposit_data: List[dict] = [] + domain = compute_domain() + for credential in credentials: + deposit_datum = DepositData( + pubkey=credential.signing_pk, + withdrawal_credentials=SHA256(credential.withdrawal_pk), + amount=credential.amount, + ) + signed_deposit_datum = sign_deposit_data(deposit_datum, credential.signing_sk) + datum_dict = signed_deposit_datum.as_dict() + datum_dict.update({'deposit_data_root': signed_deposit_datum.hash_tree_root}) + deposit_data.append(datum_dict) + + filefolder = os.path.join(folder, 'deposit_data-%i.json' % time.time()) + with open(filefolder, 'w') as f: + json.dump(deposit_data, f, default=lambda x: x.hex()) diff --git a/src/utils/serialization.py b/src/utils/serialization.py deleted file mode 100644 index 097a7b0..0000000 --- a/src/utils/serialization.py +++ /dev/null @@ -1,34 +0,0 @@ -from ssz import ( - ByteVector, - Serializable, -) - -from utils.constants import ( - DOMAIN_DEPOSIT, - GENESIS_FORK_VERSION, -) - - -class SigningRoot(Serializable): - fields = [ - ('object_root', ByteVector(32)), - ('domain', ByteVector(8)) - ] - - -def compute_domain(domain_type: bytes=DOMAIN_DEPOSIT, fork_version: bytes=GENESIS_FORK_VERSION) -> bytes: - """ - Return the domain for the ``domain_type`` and ``fork_version``. - """ - return domain_type + fork_version - - -def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: - """ - Return the signing root of an object by calculating the root of the object-domain tree. - """ - domain_wrapped_object = SigningRoot.create( - object_root=ssz_object.get_hash_tree_root(), - domain=domain, - ) - return domain_wrapped_object.get_hash_tree_root() diff --git a/src/utils/ssz.py b/src/utils/ssz.py new file mode 100644 index 0000000..1617d74 --- /dev/null +++ b/src/utils/ssz.py @@ -0,0 +1,60 @@ +from ssz import ( + ByteVector, + Serializable, + uint64, + bytes32, + bytes48, + bytes96 +) + +from utils.constants import ( + DOMAIN_DEPOSIT, + GENESIS_FORK_VERSION, +) + +bytes8 = ByteVector(8) + +# Crypto Domain SSZ + +class SigningRoot(Serializable): + fields = [ + ('object_root', bytes32), + ('domain', bytes8) + ] + + +def compute_domain(domain_type: bytes=DOMAIN_DEPOSIT, fork_version: bytes=GENESIS_FORK_VERSION) -> bytes: + """ + Return the domain for the ``domain_type`` and ``fork_version``. + """ + return domain_type + fork_version + + +def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: + """ + Return the signing root of an object by calculating the root of the object-domain tree. + """ + domain_wrapped_object = SigningRoot( + object_root=ssz_object.hash_tree_root, + domain=domain, + ) + return domain_wrapped_object.hash_tree_root + + +# DepositData SSZ + +class DepositData(Serializable): + fields = [ + ('pubkey', bytes48), + ('withdrawal_credentials', bytes32), + ('amount', uint64), + ] + + +class SignedDepositData(Serializable): + fields = [ + ('pubkey', bytes48), + ('withdrawal_credentials', bytes32), + ('amount', uint64), + ('signature', bytes96) + ]