diff --git a/.gitignore b/.gitignore index 7f10f4d..e43cf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,12 @@ 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 diff --git a/eth2deposit/credentials.py b/eth2deposit/credentials.py index bb1f09d..44913fe 100644 --- a/eth2deposit/credentials.py +++ b/eth2deposit/credentials.py @@ -9,17 +9,20 @@ from eth2deposit.key_handling.keystore import ( 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, + DepositData, DepositMessage, - Deposit, ) -class ValidatorCredentials: +class Credential: def __init__(self, *, mnemonic: str, index: int, amount: int): # Set path as EIP-2334 format # https://eips.ethereum.org/EIPS/eip-2334 @@ -63,55 +66,51 @@ class ValidatorCredentials: 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=credential.withdrawal_credentials, - amount=credential.amount, + def unsigned_deposit(self) -> DepositMessage: + return DepositMessage( + pubkey=self.signing_pk, + withdrawal_credentials=self.withdrawal_credentials, + amount=self.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': 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 signed_deposit(self) -> DepositData: + domain = compute_domain(domain_type=DOMAIN_DEPOSIT) + signing_root = compute_signing_root(self.unsigned_deposit(), domain) + signed_deposit = DepositData( + **self.unsigned_deposit().as_dict(), + signature=bls.Sign(self.signing_sk, signing_root) + ) + return signed_deposit -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)) +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: + signed_deposit_datum = credential.signed_deposit() + datum_dict = signed_deposit_datum.as_dict() + 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) + + 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/deposit.py b/eth2deposit/deposit.py index 796969a..a336360 100644 --- a/eth2deposit/deposit.py +++ b/eth2deposit/deposit.py @@ -3,16 +3,13 @@ import sys 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/utils/ssz.py b/eth2deposit/utils/ssz.py index f03f8a8..0246028 100644 --- a/eth2deposit/utils/ssz.py +++ b/eth2deposit/utils/ssz.py @@ -42,8 +42,6 @@ def compute_signing_root(ssz_object: Serializable, domain: bytes) -> bytes: return domain_wrapped_object.hash_tree_root -# DepositMessage SSZ - class DepositMessage(Serializable): fields = [ ('pubkey', bytes48), @@ -52,7 +50,7 @@ class DepositMessage(Serializable): ] -class Deposit(Serializable): +class DepositData(Serializable): fields = [ ('pubkey', bytes48), ('withdrawal_credentials', bytes32), diff --git a/eth2deposit/utils/eth2_deposit_check.py b/eth2deposit/utils/validation.py similarity index 70% rename from eth2deposit/utils/eth2_deposit_check.py rename to eth2deposit/utils/validation.py index fa07709..eb41e58 100644 --- a/eth2deposit/utils/eth2_deposit_check.py +++ b/eth2deposit/utils/validation.py @@ -10,7 +10,7 @@ from py_ecc.bls import G2ProofOfPossession as bls from eth2deposit.utils.ssz import ( compute_domain, compute_signing_root, - Deposit, + DepositData, DepositMessage, ) from eth2deposit.utils.constants import ( @@ -23,11 +23,11 @@ from eth2deposit.utils.constants import ( 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 @@ -43,12 +43,17 @@ def verify_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 = DepositMessage(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 = DepositData( + pubkey=pubkey, + withdrawal_credentials=withdrawal_credentials, + amount=amount, + signature=signature, + ) + return signed_deposit.hash_tree_root == deposit_data_root