Skip to content

Commit

Permalink
Merge branch 'master' into hwwhww/fork_version
Browse files Browse the repository at this point in the history
  • Loading branch information
hwwhww committed May 27, 2020
2 parents c59e89e + 1f6f63b commit a784a73
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 96 deletions.
10 changes: 7 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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__
132 changes: 69 additions & 63 deletions eth2deposit/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,31 @@
Keystore,
ScryptKeystore,
)
from eth2deposit.utils.constants import BLS_WITHDRAWAL_PREFIX
from eth2deposit.utils.constants import (
BLS_WITHDRAWAL_PREFIX,
)
from eth2deposit.utils.crypto import SHA256
from eth2deposit.utils.ssz import (
compute_deposit_domain,
compute_signing_root,
DepositData,
DepositMessage,
Deposit,
)


class ValidatorCredentials:
class Credential:
def __init__(self, *, mnemonic: str, index: int, amount: int, fork_version: bytes):
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
self.fork_version = fork_version

Expand Down Expand Up @@ -56,62 +66,58 @@ 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) -> DepositMessage:
return DepositMessage(
pubkey=self.signing_pk,
withdrawal_credentials=self.withdrawal_credentials,
amount=self.amount,
)

def mnemonic_to_credentials(*, mnemonic: str, num_keys: int,
amounts: List[int], fork_version: bytes, 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],
fork_version=fork_version,
) 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, fork_version: bytes) -> Deposit:
'''
Given a DepositMessage, it signs its root and returns a Deposit
'''
assert bls.PrivToPub(sk) == deposit_data.pubkey
domain = compute_deposit_domain(fork_version)
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 signed_deposit(self) -> DepositData:
domain = compute_deposit_domain(fork_version=self.fork_version)
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)
)
signed_deposit_datum = sign_deposit_data(deposit_datum, credential.signing_sk, credential.fork_version)
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})
datum_dict.update({'fork_version': credential.fork_version})
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


class CredentialList:
def __init__(self, credentials: List[Credential]):
self.credentials = credentials

@classmethod
def from_mnemonic(cls,
*,
mnemonic: str,
num_keys: int,
amounts: List[int],
fork_version: bytes,
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], fork_version=fork_version)
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})
datum_dict.update({'fork_version': credential.fork_version})
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))
15 changes: 6 additions & 9 deletions eth2deposit/deposit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,18 +86,18 @@ def main(num_validators: int, mnemonic_language: str, folder: str, chain: str, p
click.clear()
click.echo(RHINO_0)
click.echo('Creating your keys.')
credentials = mnemonic_to_credentials(
credentials = CredentialList.from_mnemonic(
mnemonic=mnemonic,
num_keys=num_validators,
amounts=amounts,
fork_version=setting.GENESIS_FORK_VERSION,
)
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)
Expand Down
16 changes: 9 additions & 7 deletions eth2deposit/key_handling/key_derivation/mnemonic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
from unicodedata import normalize
from secrets import randbits
from typing import (
List,
Optional,
Sequence,
Tuple,
)

from eth2deposit.utils.crypto import (
Expand All @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion eth2deposit/key_handling/key_derivation/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
"""
Expand Down
3 changes: 2 additions & 1 deletion eth2deposit/key_handling/keystore.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions eth2deposit/utils/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'')
4 changes: 1 addition & 3 deletions eth2deposit/utils/ssz.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,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),
Expand All @@ -72,7 +70,7 @@ class DepositMessage(Serializable):
]


class Deposit(Serializable):
class DepositData(Serializable):
fields = [
('pubkey', bytes48),
('withdrawal_credentials', bytes32),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from eth2deposit.utils.ssz import (
compute_deposit_domain,
compute_signing_root,
Deposit,
DepositData,
DepositMessage,
)
from eth2deposit.utils.constants import (
Expand All @@ -22,11 +22,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
Expand All @@ -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_deposit_domain(fork_version)
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
15 changes: 13 additions & 2 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)

Expand All @@ -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)
Expand Down

0 comments on commit a784a73

Please sign in to comment.