From 6c01fa968f03b6384278eeaeaad5a6c0bfbaa1c0 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Thu, 21 May 2020 14:43:02 +0200 Subject: [PATCH 01/10] Creates CredentialList --- cli/deposit.py | 15 +-- eth2deposit/credentials.py | 103 +++++++++--------- .../{eth2_deposit_check.py => validation.py} | 4 +- 3 files changed, 59 insertions(+), 63 deletions(-) rename eth2deposit/utils/{eth2_deposit_check.py => validation.py} (92%) diff --git a/cli/deposit.py b/cli/deposit.py index 796969a0..a336360e 100644 --- a/cli/deposit.py +++ b/cli/deposit.py @@ -3,16 +3,13 @@ import click from eth2deposit.credentials import ( - mnemonic_to_credentials, - export_keystores, - export_deposit_data_json, - verify_keystores, + CredentialList, ) from eth2deposit.key_handling.key_derivation.mnemonic import ( get_languages, get_mnemonic, ) -from eth2deposit.utils.eth2_deposit_check import verify_deposit_data_json +from eth2deposit.utils.validation import verify_deposit_data_json from eth2deposit.utils.constants import ( WORD_LISTS_PATH, MAX_DEPOSIT_AMOUNT, @@ -78,13 +75,13 @@ def main(num_validators: int, mnemonic_language: str, folder: str, password: str click.clear() click.echo(RHINO_0) click.echo('Creating your keys.') - credentials = mnemonic_to_credentials(mnemonic=mnemonic, num_keys=num_validators, amounts=amounts) + credentials = CredentialList.from_mnemonic(mnemonic=mnemonic, num_keys=num_validators, amounts=amounts) click.echo('Saving your keystore(s).') - keystore_filefolders = export_keystores(credentials=credentials, password=password, folder=folder) + keystore_filefolders = credentials.export_keystores(password=password, folder=folder) click.echo('Creating your deposit(s).') - deposits_file = export_deposit_data_json(credentials=credentials, folder=folder) + deposits_file = credentials.export_deposit_data_json(folder=folder) click.echo('Verifying your keystore(s).') - assert verify_keystores(credentials=credentials, keystore_filefolders=keystore_filefolders, password=password) + assert credentials.verify_keystores(keystore_filefolders=keystore_filefolders, password=password) click.echo('Verifying your deposit(s).') assert verify_deposit_data_json(deposits_file) click.echo('\nSuccess!\nYour keys can be found at: %s' % folder) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index e6411b3c..a373be9e 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -18,7 +18,7 @@ ) -class ValidatorCredentials: +class Credential: 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) @@ -48,55 +48,54 @@ def verify_keystore(self, keystore_filefolder: str, password: str) -> bool: secret_bytes = saved_keystore.decrypt(password) return self.signing_sk == int.from_bytes(secret_bytes, 'big') - -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) -> List[str]: - return [credential.save_signing_keystore(password=password, folder=folder) for credential in credentials] - - -def sign_deposit_data(deposit_data: DepositMessage, sk: int) -> Deposit: - ''' - Given a DepositMessage, it signs its root and returns a Deposit - ''' - assert bls.PrivToPub(sk) == deposit_data.pubkey - domain = compute_domain() - signing_root = compute_signing_root(deposit_data, domain) - signed_deposit_data = Deposit( - **deposit_data.as_dict(), - signature=bls.Sign(sk, signing_root) - ) - return signed_deposit_data - - -def export_deposit_data_json(*, credentials: List[ValidatorCredentials], folder: str) -> str: - deposit_data: List[Dict[bytes, bytes]] = [] - for credential in credentials: - deposit_datum = DepositMessage( - pubkey=credential.signing_pk, - withdrawal_credentials=SHA256(credential.withdrawal_pk), - amount=credential.amount, + def sign_deposit_data(self, deposit_message: DepositMessage) -> Deposit: + ''' + Given a DepositMessage, it signs its root and returns a DepositData + ''' + assert bls.PrivToPub(self.signing_sk) == deposit_message.pubkey + domain = compute_domain() + signing_root = compute_signing_root(deposit_message, domain) + signed_deposit_data = Deposit( + **deposit_message.as_dict(), + signature=bls.Sign(self.signing_sk, signing_root) ) - signed_deposit_datum = sign_deposit_data(deposit_datum, credential.signing_sk) - datum_dict = signed_deposit_datum.as_dict() - datum_dict.update({'deposit_data_root': deposit_datum.hash_tree_root}) - datum_dict.update({'signed_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()) - return filefolder - - -def verify_keystores(*, credentials: List[ValidatorCredentials], - keystore_filefolders: List[str], password: str) -> bool: - return all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) - for credential, filefolder in zip(credentials, keystore_filefolders)) + return signed_deposit_data + + +class CredentialList: + def __init__(self, credentials: List[Credential]): + self.credentials = credentials + + @classmethod + def from_mnemonic(cls, *, mnemonic: str, num_keys: int, amounts: List[int], start_index: int=0) -> 'CredentialList': + assert len(amounts) == num_keys + key_indices = range(start_index, start_index + num_keys) + return cls([Credential(mnemonic=mnemonic, index=index, amount=amounts[index]) + for index in key_indices]) + + def export_keystores(self, password: str, folder: str) -> List[str]: + return [credential.save_signing_keystore(password=password, folder=folder) for credential in self.credentials] + + def export_deposit_data_json(self, folder: str) -> str: + deposit_data: List[Dict[bytes, bytes]] = [] + for credential in self.credentials: + deposit_datum = DepositMessage( + pubkey=credential.signing_pk, + withdrawal_credentials=SHA256(credential.withdrawal_pk), + amount=credential.amount, + ) + signed_deposit_datum = credential.sign_deposit_data(deposit_datum) + + datum_dict = signed_deposit_datum.as_dict() + datum_dict.update({'deposit_data_root': deposit_datum.hash_tree_root}) + datum_dict.update({'signed_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()) + return filefolder + + def verify_keystores(self, keystore_filefolders: List[str], password: str) -> bool: + return all(credential.verify_keystore(keystore_filefolder=filefolder, password=password) + for credential, filefolder in zip(self.credentials, keystore_filefolders)) diff --git a/eth2deposit/utils/eth2_deposit_check.py b/eth2deposit/utils/validation.py similarity index 92% rename from eth2deposit/utils/eth2_deposit_check.py rename to eth2deposit/utils/validation.py index fa077094..8c64295a 100644 --- a/eth2deposit/utils/eth2_deposit_check.py +++ b/eth2deposit/utils/validation.py @@ -23,11 +23,11 @@ def verify_deposit_data_json(filefolder: str) -> bool: with open(filefolder, 'r') as f: deposit_json = json.load(f) - return all([verify_deposit(deposit) for deposit in deposit_json]) + return all([validate_deposit(deposit) for deposit in deposit_json]) return False -def verify_deposit(deposit_data_dict: Dict[str, Any]) -> bool: +def validate_deposit(deposit_data_dict: Dict[str, Any]) -> bool: ''' Checks whether a deposit is valid based on the eth2 rules. https://github.com/ethereum/eth2.0-specs/blob/dev/specs/phase0/beacon-chain.md#deposits From 050f3ca10a358bac7d479cf7d843d95cb1c88a4c Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Fri, 22 May 2020 16:09:20 +0200 Subject: [PATCH 02/10] Credential.deposit methods --- eth2deposit/credentials.py | 42 ++++++++++++++++----------------- eth2deposit/utils/ssz.py | 6 ++--- eth2deposit/utils/validation.py | 17 ++++++++----- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index 0bb1f512..5873f29a 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -9,13 +9,16 @@ Keystore, ScryptKeystore, ) -from eth2deposit.utils.constants import BLS_WITHDRAWAL_PREFIX +from eth2deposit.utils.constants import ( + BLS_WITHDRAWAL_PREFIX, + DOMAIN_DEPOSIT, +) from eth2deposit.utils.crypto import SHA256 from eth2deposit.utils.ssz import ( compute_domain, compute_signing_root, - DepositMessage, - Deposit, + SignedDeposit, + UnsignedDeposit, ) @@ -55,18 +58,21 @@ def verify_keystore(self, keystore_filefolder: str, password: str) -> bool: secret_bytes = saved_keystore.decrypt(password) return self.signing_sk == int.from_bytes(secret_bytes, 'big') - def sign_deposit_data(self, deposit_data: DepositMessage) -> Deposit: - ''' - Given a DepositMessage, it signs its root and returns a Deposit - ''' - assert bls.PrivToPub(self.signing_sk) == deposit_data.pubkey - domain = compute_domain() - signing_root = compute_signing_root(deposit_data, domain) - signed_deposit_data = Deposit( - **deposit_data.as_dict(), + def unsigned_deposit(self) -> UnsignedDeposit: + return UnsignedDeposit( + pubkey=self.signing_pk, + withdrawal_credentials=self.withdrawal_credentials, + amount=self.amount, + ) + + def signed_deposit(self) -> SignedDeposit: + domain = compute_domain(domain_type=DOMAIN_DEPOSIT) + signing_root = compute_signing_root(self.unsigned_deposit(), domain) + signed_deposit = SignedDeposit( + **self.unsigned_deposit().as_dict(), signature=bls.Sign(self.signing_sk, signing_root) ) - return signed_deposit_data + return signed_deposit class CredentialList: @@ -86,15 +92,9 @@ def export_keystores(self, password: str, folder: str) -> List[str]: def export_deposit_data_json(self, folder: str) -> str: deposit_data: List[Dict[bytes, bytes]] = [] for credential in self.credentials: - deposit_datum = DepositMessage( - pubkey=credential.signing_pk, - withdrawal_credentials=credential.withdrawal_credentials, - amount=credential.amount, - ) - signed_deposit_datum = credential.sign_deposit_data(deposit_datum) - + signed_deposit_datum = credential.signed_deposit() datum_dict = signed_deposit_datum.as_dict() - datum_dict.update({'deposit_data_root': deposit_datum.hash_tree_root}) + datum_dict.update({'deposit_data_root': credential.unsigned_deposit().hash_tree_root}) datum_dict.update({'signed_deposit_data_root': signed_deposit_datum.hash_tree_root}) deposit_data.append(datum_dict) diff --git a/eth2deposit/utils/ssz.py b/eth2deposit/utils/ssz.py index f03f8a84..961bf3f8 100644 --- a/eth2deposit/utils/ssz.py +++ b/eth2deposit/utils/ssz.py @@ -42,9 +42,7 @@ def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: return domain_wrapped_object.hash_tree_root -# DepositMessage SSZ - -class DepositMessage(Serializable): +class UnsignedDeposit(Serializable): fields = [ ('pubkey', bytes48), ('withdrawal_credentials', bytes32), @@ -52,7 +50,7 @@ class DepositMessage(Serializable): ] -class Deposit(Serializable): +class SignedDeposit(Serializable): fields = [ ('pubkey', bytes48), ('withdrawal_credentials', bytes32), diff --git a/eth2deposit/utils/validation.py b/eth2deposit/utils/validation.py index 8c64295a..50bd9414 100644 --- a/eth2deposit/utils/validation.py +++ b/eth2deposit/utils/validation.py @@ -10,8 +10,8 @@ from eth2deposit.utils.ssz import ( compute_domain, compute_signing_root, - Deposit, - DepositMessage, + SignedDeposit, + UnsignedDeposit, ) from eth2deposit.utils.constants import ( DOMAIN_DEPOSIT, @@ -43,12 +43,17 @@ def validate_deposit(deposit_data_dict: Dict[str, Any]) -> bool: return False # Verify deposit signature && pubkey - deposit_message = DepositMessage(pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount) + unsigned_deposit = UnsignedDeposit(pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount) domain = compute_domain(domain_type=DOMAIN_DEPOSIT) - signing_root = compute_signing_root(deposit_message, domain) + signing_root = compute_signing_root(unsigned_deposit, domain) if not bls.Verify(pubkey, signing_root, signature): return False # Verify Deposit Root - deposit = Deposit(pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount, signature=signature) - return deposit.hash_tree_root == deposit_data_root + signed_deposit = SignedDeposit( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + signature=signature, + ) + return signed_deposit.hash_tree_root == deposit_data_root From f3eee88bc2af2c8b87e74f9a04c3507122bfa7b8 Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Fri, 22 May 2020 16:11:38 +0200 Subject: [PATCH 03/10] adds build/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7f10f4de..99f9e624 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ validator_keys # Python testing & linting: +build/ venv/ .pytest_cache .hypothesis From 20c1f43a50fc13cd83426b1d63944cab6ba49ff7 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Sun, 24 May 2020 00:52:54 +0800 Subject: [PATCH 04/10] Bump py_ecc to 4.0.0 (Eth2 spec v0.12.0, IETF BLS v2 + hash-to-curve v7) --- eth2deposit/credentials.py | 6 +++--- eth2deposit/key_handling/keystore.py | 2 +- requirements.txt | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index e25d3c5d..4072ac77 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -28,11 +28,11 @@ def __init__(self, *, mnemonic: str, index: int, amount: int): @property def signing_pk(self) -> bytes: - return bls.PrivToPub(self.signing_sk) + return bls.SkToPk(self.signing_sk) @property def withdrawal_pk(self) -> bytes: - return bls.PrivToPub(self.withdrawal_sk) + return bls.SkToPk(self.withdrawal_sk) @property def withdrawal_credentials(self) -> bytes: @@ -73,7 +73,7 @@ def sign_deposit_data(deposit_data: DepositMessage, sk: int) -> Deposit: ''' Given a DepositMessage, it signs its root and returns a Deposit ''' - assert bls.PrivToPub(sk) == deposit_data.pubkey + assert bls.SkToPk(sk) == deposit_data.pubkey domain = compute_domain() signing_root = compute_signing_root(deposit_data, domain) signed_deposit_data = Deposit( diff --git a/eth2deposit/key_handling/keystore.py b/eth2deposit/key_handling/keystore.py index 779f0f7d..7d70fae5 100644 --- a/eth2deposit/key_handling/keystore.py +++ b/eth2deposit/key_handling/keystore.py @@ -102,7 +102,7 @@ def encrypt(cls, *, secret: bytes, password: str, path: str='', cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params) keystore.crypto.cipher.message = cipher.encrypt(secret) keystore.crypto.checksum.message = SHA256(decryption_key[16:32] + keystore.crypto.cipher.message) - keystore.pubkey = bls.PrivToPub(int.from_bytes(secret, 'big')).hex() + keystore.pubkey = bls.SkToPk(int.from_bytes(secret, 'big')).hex() keystore.path = path return keystore diff --git a/requirements.txt b/requirements.txt index 4de43334..528a1a3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -py-ecc==2.0.0 \ - --hash=sha256:0ad540e3a3df332692e35dfac8e550592c345b249d54d6d6c9a91f57bd2a75af \ - --hash=sha256:c32cb3aa13b6d5a555f6cad629fb5d2d4af3cd607f239c3422654e1df208ed78 +py-ecc==4.0.0 \ + --hash=sha256:0712a1ebc2d45417088aa613f28518c1714c99d023998e50244c91e3acbb0d6c \ + --hash=sha256:a637edcce7e31ddefae0a3c1018f16e25c9428fcd524b1ac5ceeb2adfc433276 pycryptodome==3.9.7 \ --hash=sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f \ --hash=sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad \ @@ -63,3 +63,6 @@ toolz==0.10.0 \ six==1.14.0 \ --hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a \ --hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c +cached-property==1.5.1 \ + --hash=sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f \ + --hash=sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504 From d96670d4ffb7798f166544192b96a590ca9e34cc Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 25 May 2020 14:54:36 +0200 Subject: [PATCH 05/10] Revert to DepositMessage & DepositData as per spec --- eth2deposit/credentials.py | 12 ++++++------ eth2deposit/utils/ssz.py | 4 ++-- eth2deposit/utils/validation.py | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index 5873f29a..99ec2912 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -17,8 +17,8 @@ from eth2deposit.utils.ssz import ( compute_domain, compute_signing_root, - SignedDeposit, - UnsignedDeposit, + DepositData, + DepositMessage, ) @@ -58,17 +58,17 @@ def verify_keystore(self, keystore_filefolder: str, password: str) -> bool: secret_bytes = saved_keystore.decrypt(password) return self.signing_sk == int.from_bytes(secret_bytes, 'big') - def unsigned_deposit(self) -> UnsignedDeposit: - return UnsignedDeposit( + def unsigned_deposit(self) -> DepositMessage: + return DepositMessage( pubkey=self.signing_pk, withdrawal_credentials=self.withdrawal_credentials, amount=self.amount, ) - def signed_deposit(self) -> SignedDeposit: + def signed_deposit(self) -> DepositData: domain = compute_domain(domain_type=DOMAIN_DEPOSIT) signing_root = compute_signing_root(self.unsigned_deposit(), domain) - signed_deposit = SignedDeposit( + signed_deposit = DepositData( **self.unsigned_deposit().as_dict(), signature=bls.Sign(self.signing_sk, signing_root) ) diff --git a/eth2deposit/utils/ssz.py b/eth2deposit/utils/ssz.py index 961bf3f8..02460281 100644 --- a/eth2deposit/utils/ssz.py +++ b/eth2deposit/utils/ssz.py @@ -42,7 +42,7 @@ def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: return domain_wrapped_object.hash_tree_root -class UnsignedDeposit(Serializable): +class DepositMessage(Serializable): fields = [ ('pubkey', bytes48), ('withdrawal_credentials', bytes32), @@ -50,7 +50,7 @@ class UnsignedDeposit(Serializable): ] -class SignedDeposit(Serializable): +class DepositData(Serializable): fields = [ ('pubkey', bytes48), ('withdrawal_credentials', bytes32), diff --git a/eth2deposit/utils/validation.py b/eth2deposit/utils/validation.py index 50bd9414..eb41e586 100644 --- a/eth2deposit/utils/validation.py +++ b/eth2deposit/utils/validation.py @@ -10,8 +10,8 @@ from eth2deposit.utils.ssz import ( compute_domain, compute_signing_root, - SignedDeposit, - UnsignedDeposit, + DepositData, + DepositMessage, ) from eth2deposit.utils.constants import ( DOMAIN_DEPOSIT, @@ -43,14 +43,14 @@ def validate_deposit(deposit_data_dict: Dict[str, Any]) -> bool: return False # Verify deposit signature && pubkey - unsigned_deposit = UnsignedDeposit(pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount) + unsigned_deposit = DepositMessage(pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount) domain = compute_domain(domain_type=DOMAIN_DEPOSIT) signing_root = compute_signing_root(unsigned_deposit, domain) if not bls.Verify(pubkey, signing_root, signature): return False # Verify Deposit Root - signed_deposit = SignedDeposit( + signed_deposit = DepositData( pubkey=pubkey, withdrawal_credentials=withdrawal_credentials, amount=amount, From 23fdbdcd9e5f501bb8e8bf211bba51f2b3367cab Mon Sep 17 00:00:00 2001 From: Carl Beekhuizen Date: Mon, 25 May 2020 15:02:44 +0200 Subject: [PATCH 06/10] gitignore dist/ --- .gitignore | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 99f9e624..e43cf58b 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,11 @@ validator_keys # Python testing & linting: build/ +dist/ venv/ -.pytest_cache -.hypothesis -.mypy_cache +*.pytest_cache +*.hypothesis +*.mypy_cache +*.egg-info +*.egg __pycache__ \ No newline at end of file From 0258ea7d67c1e6bc6c859d8b125597eef19f4a7f Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 26 May 2020 15:58:11 +0800 Subject: [PATCH 07/10] Revert "Bump py_ecc to 4.0.0" --- eth2deposit/credentials.py | 6 +++--- eth2deposit/key_handling/keystore.py | 2 +- requirements.txt | 9 +++------ 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index 4072ac77..e25d3c5d 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -28,11 +28,11 @@ def __init__(self, *, mnemonic: str, index: int, amount: int): @property def signing_pk(self) -> bytes: - return bls.SkToPk(self.signing_sk) + return bls.PrivToPub(self.signing_sk) @property def withdrawal_pk(self) -> bytes: - return bls.SkToPk(self.withdrawal_sk) + return bls.PrivToPub(self.withdrawal_sk) @property def withdrawal_credentials(self) -> bytes: @@ -73,7 +73,7 @@ def sign_deposit_data(deposit_data: DepositMessage, sk: int) -> Deposit: ''' Given a DepositMessage, it signs its root and returns a Deposit ''' - assert bls.SkToPk(sk) == deposit_data.pubkey + assert bls.PrivToPub(sk) == deposit_data.pubkey domain = compute_domain() signing_root = compute_signing_root(deposit_data, domain) signed_deposit_data = Deposit( diff --git a/eth2deposit/key_handling/keystore.py b/eth2deposit/key_handling/keystore.py index 7d70fae5..779f0f7d 100644 --- a/eth2deposit/key_handling/keystore.py +++ b/eth2deposit/key_handling/keystore.py @@ -102,7 +102,7 @@ def encrypt(cls, *, secret: bytes, password: str, path: str='', cipher = AES_128_CTR(key=decryption_key[:16], **keystore.crypto.cipher.params) keystore.crypto.cipher.message = cipher.encrypt(secret) keystore.crypto.checksum.message = SHA256(decryption_key[16:32] + keystore.crypto.cipher.message) - keystore.pubkey = bls.SkToPk(int.from_bytes(secret, 'big')).hex() + keystore.pubkey = bls.PrivToPub(int.from_bytes(secret, 'big')).hex() keystore.path = path return keystore diff --git a/requirements.txt b/requirements.txt index 528a1a3e..4de43334 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -py-ecc==4.0.0 \ - --hash=sha256:0712a1ebc2d45417088aa613f28518c1714c99d023998e50244c91e3acbb0d6c \ - --hash=sha256:a637edcce7e31ddefae0a3c1018f16e25c9428fcd524b1ac5ceeb2adfc433276 +py-ecc==2.0.0 \ + --hash=sha256:0ad540e3a3df332692e35dfac8e550592c345b249d54d6d6c9a91f57bd2a75af \ + --hash=sha256:c32cb3aa13b6d5a555f6cad629fb5d2d4af3cd607f239c3422654e1df208ed78 pycryptodome==3.9.7 \ --hash=sha256:07024fc364869eae8d6ac0d316e089956e6aeffe42dbdcf44fe1320d96becf7f \ --hash=sha256:09b6d6bcc01a4eb1a2b4deeff5aa602a108ec5aed8ac75ae554f97d1d7f0a5ad \ @@ -63,6 +63,3 @@ toolz==0.10.0 \ six==1.14.0 \ --hash=sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a \ --hash=sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c -cached-property==1.5.1 \ - --hash=sha256:3a026f1a54135677e7da5ce819b0c690f156f37976f3e30c5430740725203d7f \ - --hash=sha256:9217a59f14a5682da7c4b8829deadbfc194ac22e9908ccf7c8820234e80a1504 From 72f0dd085257f6f92f160c5539ecc019051751b0 Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 26 May 2020 17:32:20 +0800 Subject: [PATCH 08/10] Fix key path --- eth2deposit/credentials.py | 14 +++++++++++--- .../key_handling/key_derivation/mnemonic.py | 16 +++++++++------- eth2deposit/key_handling/key_derivation/path.py | 2 +- eth2deposit/utils/crypto.py | 1 + 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index e25d3c5d..bb1f09d3 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -21,9 +21,17 @@ 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') + # Set path as EIP-2334 format + # https://eips.ethereum.org/EIPS/eip-2334 + purpose = '12381' + coin_type = '3600' + account = str(index) + withdrawal_key_path = f'm/{purpose}/{coin_type}/{account}/0' + self.signing_key_path = f'{withdrawal_key_path}/0' + + # Do NOT use password for seed generation. + self.withdrawal_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=withdrawal_key_path, password='') + self.signing_sk = mnemonic_and_path_to_key(mnemonic=mnemonic, path=self.signing_key_path, password='') self.amount = amount @property diff --git a/eth2deposit/key_handling/key_derivation/mnemonic.py b/eth2deposit/key_handling/key_derivation/mnemonic.py index ef8e93ca..e9f32c54 100644 --- a/eth2deposit/key_handling/key_derivation/mnemonic.py +++ b/eth2deposit/key_handling/key_derivation/mnemonic.py @@ -2,9 +2,9 @@ from unicodedata import normalize from secrets import randbits from typing import ( - List, Optional, Sequence, + Tuple, ) from eth2deposit.utils.crypto import ( @@ -22,27 +22,29 @@ def _get_word(*, word_list: Sequence[str], index: int) -> str: return word_list[index][:-1] -def get_seed(*, mnemonic: str, password: str='') -> bytes: +def get_seed(*, mnemonic: str, password: str) -> bytes: """ - Derives the seed for the pre-image root of the tree. + Derive the seed for the pre-image root of the tree. + + Ref: https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed """ mnemonic = normalize('NFKD', mnemonic) salt = normalize('NFKD', 'mnemonic' + password).encode('utf-8') return PBKDF2(password=mnemonic, salt=salt, dklen=64, c=2048, prf='sha512') -def get_languages(path: str) -> List[str]: +def get_languages(path: str) -> Tuple[str, ...]: """ Walk the `path` and list all the languages with word-lists available. """ (_, _, filenames) = next(os.walk(path)) - filenames = [name[:-4] for name in filenames] - return filenames + languages = tuple([name[:-4] for name in filenames]) + return languages def get_mnemonic(*, language: str, words_path: str, entropy: Optional[bytes]=None) -> str: """ - Returns a mnemonic string in a given `language` based on `entropy`. + Return a mnemonic string in a given `language` based on `entropy`. """ if entropy is None: entropy = randbits(256).to_bytes(32, 'big') diff --git a/eth2deposit/key_handling/key_derivation/path.py b/eth2deposit/key_handling/key_derivation/path.py index 101b6d62..3a7e713d 100644 --- a/eth2deposit/key_handling/key_derivation/path.py +++ b/eth2deposit/key_handling/key_derivation/path.py @@ -18,7 +18,7 @@ def path_to_nodes(path: str) -> List[int]: return [int(index) for index in indices] -def mnemonic_and_path_to_key(*, mnemonic: str, path: str, password: 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`. """ diff --git a/eth2deposit/utils/crypto.py b/eth2deposit/utils/crypto.py index b66befbd..7dda9868 100644 --- a/eth2deposit/utils/crypto.py +++ b/eth2deposit/utils/crypto.py @@ -38,4 +38,5 @@ def HKDF(*, salt: bytes, IKM: bytes, L: int) -> bytes: def AES_128_CTR(*, key: bytes, iv: bytes) -> Any: + assert len(key) == 16 return _AES.new(key=key, mode=_AES.MODE_CTR, initial_value=iv, nonce=b'') From 49a8bded42cef310fc8dd3ac22581a15421a461e Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 26 May 2020 21:00:53 +0800 Subject: [PATCH 09/10] Add uuid test --- tests/test_cli.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 4bfc6c10..c58effd2 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,6 +8,7 @@ from eth2deposit import deposit from eth2deposit.deposit import main from eth2deposit.utils.constants import DEFAULT_VALIDATOR_KEYS_FOLDER_NAME +from eth2deposit.key_handling.keystore import Keystore def clean_key_folder(my_folder_path): @@ -36,7 +37,7 @@ def get_mnemonic(language, words_path, entropy=None): os.mkdir(my_folder_path) runner = CliRunner() - inputs = ['1', 'english', 'MyPassword', 'MyPassword', 'fakephrase'] + inputs = ['5', 'english', 'MyPassword', 'MyPassword', 'fakephrase'] data = '\n'.join(inputs) result = runner.invoke(main, ['--folder', my_folder_path], input=data) @@ -45,7 +46,17 @@ def get_mnemonic(language, words_path, entropy=None): # Check files validator_keys_folder_path = os.path.join(my_folder_path, DEFAULT_VALIDATOR_KEYS_FOLDER_NAME) _, _, key_files = next(os.walk(validator_keys_folder_path)) - assert len(key_files) == 2 + + def get_uuid(key_file): + keystore = Keystore.from_json(key_file) + return keystore.uuid + + all_uuid = [ + get_uuid(validator_keys_folder_path + '/' + key_file) + for key_file in key_files + if key_file.startswith('keystore') + ] + assert len(set(all_uuid)) == 5 # Clean up clean_key_folder(my_folder_path) From e9fa2192df374e3d2116c7c53bff268c970f944b Mon Sep 17 00:00:00 2001 From: Hsiao-Wei Wang Date: Tue, 26 May 2020 21:09:20 +0800 Subject: [PATCH 10/10] Create uuid at execution time instead of class attribute --- eth2deposit/key_handling/keystore.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eth2deposit/key_handling/keystore.py b/eth2deposit/key_handling/keystore.py index 779f0f7d..dde53cc8 100644 --- a/eth2deposit/key_handling/keystore.py +++ b/eth2deposit/key_handling/keystore.py @@ -65,7 +65,7 @@ class Keystore(BytesDataclass): crypto: KeystoreCrypto = KeystoreCrypto() pubkey: str = '' path: str = '' - uuid: str = str(uuid4()) # Generate a new uuid + uuid: str = '' version: int = 4 def kdf(self, **kwargs: Any) -> bytes: @@ -96,6 +96,7 @@ def encrypt(cls, *, secret: bytes, password: str, path: str='', kdf_salt: bytes=randbits(256).to_bytes(32, 'big'), aes_iv: bytes=randbits(128).to_bytes(16, 'big')) -> 'Keystore': keystore = cls() + keystore.uuid = str(uuid4()) keystore.crypto.kdf.params['salt'] = kdf_salt decryption_key = keystore.kdf(password=password, **keystore.crypto.kdf.params) keystore.crypto.cipher.params['iv'] = aes_iv