Implement more of the KeyStore spec and integrate it in the beacon node

This commit is contained in:
Zahary Karadjov 2020-06-01 22:48:20 +03:00 committed by zah
parent 1fc9413c48
commit 17343442ea
20 changed files with 476 additions and 225 deletions

View File

@ -998,7 +998,7 @@ programMain:
of createTestnet:
var deposits: seq[Deposit]
for i in config.firstValidator.int ..< config.totalValidators.int:
let depositFile = config.validatorsDir /
let depositFile = config.testnetDepositsDir /
validatorFileBaseName(i) & ".deposit.json"
try:
deposits.add Json.loadFile(depositFile, Deposit)
@ -1096,15 +1096,13 @@ programMain:
node.start()
of makeDeposits:
createDir(config.depositsDir)
createDir(config.outValidatorsDir)
let
quickstartDeposits = generateDeposits(
config.totalQuickstartDeposits, config.depositsDir, false)
randomDeposits = generateDeposits(
config.totalRandomDeposits, config.depositsDir, true,
firstIdx = config.totalQuickstartDeposits)
deposits = generateDeposits(
config.totalDeposits,
config.outValidatorsDir,
config.outSecretsDir).tryGet
if config.web3Url.len > 0 and config.depositContractAddress.len > 0:
if config.minDelay > config.maxDelay:
@ -1121,8 +1119,9 @@ programMain:
depositContract = config.depositContractAddress
waitFor sendDeposits(
quickstartDeposits & randomDeposits,
deposits,
config.web3Url,
config.depositContractAddress,
config.depositPrivateKey,
delayGenerator)

View File

@ -1,11 +1,11 @@
{.push raises: [Defect].}
import
os, options, strformat, strutils,
os, options, strformat,
chronicles, confutils, json_serialization,
confutils/defs, confutils/std/net,
chronicles/options as chroniclesOptions,
spec/[crypto]
spec/[crypto, keystore]
export
defs, enabledLogLevel, parseCmdArg, completeCmdArg
@ -39,11 +39,6 @@ type
desc: "The Eth1 network tracked by the beacon node."
name: "eth1-network" }: Eth1Network
quickStart* {.
defaultValue: false
desc: "Run in quickstart mode"
name: "quick-start" }: bool
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data."
@ -60,6 +55,10 @@ type
desc: "Address of the deposit contract."
name: "deposit-contract" }: string
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration."
name: "non-interactive" }: bool
case cmd* {.
command
defaultValue: noCommand }: BNStartUpCmd
@ -106,6 +105,14 @@ type
abbr: "v"
name: "validator" }: seq[ValidatorKeyPath]
validatorsDir* {.
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDir* {.
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
stateSnapshot* {.
desc: "Json file specifying a recent state snapshot."
abbr: "s"
@ -177,13 +184,12 @@ type
name: "dump" }: bool
of createTestnet:
validatorsDir* {.
desc: "Directory containing validator descriptors named 'vXXXXXXX.deposit.json'."
abbr: "d"
testnetDepositsDir* {.
desc: "Directory containing validator keystores."
name: "validators-dir" }: InputDir
totalValidators* {.
desc: "The number of validators in the newly created chain."
desc: "The number of validator deposits in the newly created chain."
name: "total-validators" }: uint64
firstValidator* {.
@ -209,7 +215,6 @@ type
genesisOffset* {.
defaultValue: 5
desc: "Seconds from now to add to genesis time."
abbr: "g"
name: "genesis-offset" }: int
outputGenesis* {.
@ -231,34 +236,34 @@ type
name: "keyfile" }: seq[ValidatorKeyPath]
of makeDeposits:
totalQuickstartDeposits* {.
defaultValue: 0
desc: "Number of quick-start deposits to generate."
name: "quickstart-deposits" }: int
totalDeposits* {.
defaultValue: 1
desc: "Number of deposits to generate."
name: "count" }: int
totalRandomDeposits* {.
defaultValue: 0
desc: "Number of secure random deposits to generate."
name: "random-deposits" }: int
depositsDir* {.
outValidatorsDir* {.
defaultValue: "validators"
desc: "Folder to write deposits to."
name: "deposits-dir" }: string
desc: "Output folder for validator keystores and deposits."
name: "out-validators-dir" }: string
outSecretsDir* {.
defaultValue: "secrets"
desc: "Output folder for randomly generated keystore passphrases."
name: "out-secrets-dir" }: string
depositPrivateKey* {.
defaultValue: ""
desc: "Private key of the controlling (sending) account",
desc: "Private key of the controlling (sending) account.",
name: "deposit-private-key" }: string
minDelay* {.
defaultValue: 0.0
desc: "Minimum possible delay between making two deposits (in seconds)"
desc: "Minimum possible delay between making two deposits (in seconds)."
name: "min-delay" }: float
maxDelay* {.
defaultValue: 0.0
desc: "Maximum possible delay between making two deposits (in seconds)"
desc: "Maximum possible delay between making two deposits (in seconds)."
name: "max-delay" }: float
ValidatorClientConf* = object
@ -273,6 +278,10 @@ type
abbr: "d"
name: "data-dir" }: OutDir
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration."
name: "non-interactive" }: bool
case cmd* {.
command
defaultValue: VCNoCommand }: VCStartUpCmd
@ -290,10 +299,18 @@ type
validators* {.
required
desc: "Path to a validator private key, as generated by makeDeposits."
desc: "Path to a validator key store, as generated by makeDeposits."
abbr: "v"
name: "validator" }: seq[ValidatorKeyPath]
validatorsDir* {.
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDir* {.
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
let dataDir = when defined(windows):
"AppData" / "Roaming" / "Nimbus"
@ -315,7 +332,7 @@ func dumpDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "dump"
func localValidatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "validators"
string conf.validatorsDir.get(InputDir(conf.dataDir / "validators"))
func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "db"
@ -328,26 +345,6 @@ func defaultListenAddress*(conf: BeaconNodeConf|ValidatorClientConf): ValidIpAdd
func defaultAdminListenAddress*(conf: BeaconNodeConf|ValidatorClientConf): ValidIpAddress =
(static ValidIpAddress.init("127.0.0.1"))
iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey =
for validatorKeyFile in conf.validators:
try:
yield validatorKeyFile.load
except CatchableError as err:
warn "Failed to load validator private key",
file = validatorKeyFile.string, err = err.msg
try:
for kind, file in walkDir(conf.localValidatorsDir):
if kind in {pcFile, pcLinkToFile} and
cmpIgnoreCase(".privkey", splitFile(file).ext) == 0:
try:
yield ValidatorPrivKey.init(readFile(file).string)
except CatchableError as err:
warn "Failed to load a validator private key", file, err = err.msg
except OSError as err:
warn "Cannot load validator keys",
dir = conf.localValidatorsDir, err = err.msg
template writeValue*(writer: var JsonWriter,
value: TypedInputFile|InputFile|InputDir|OutPath|OutDir|OutFile) =
writer.writeValue(string value)

View File

@ -3,7 +3,7 @@
import
stew/endians2, stint,
./extras, ./ssz/merkleization,
spec/[crypto, datatypes, digest, helpers]
spec/[crypto, datatypes, digest, helpers, keystore]
func get_eth1data_stub*(deposit_count: uint64, current_epoch: Epoch): Eth1Data =
# https://github.com/ethereum/eth2.0-pm/blob/e596c70a19e22c7def4fd3519e20ae4022349390/interop/mocked_eth1data/README.md
@ -16,7 +16,7 @@ func get_eth1data_stub*(deposit_count: uint64, current_epoch: Epoch): Eth1Data =
block_hash: hash_tree_root(hash_tree_root(voting_period).data),
)
func makeInteropPrivKey*(i: int): BlsResult[ValidatorPrivKey] =
func makeInteropPrivKey*(i: int): ValidatorPrivKey =
var bytes: array[32, byte]
bytes[0..7] = uint64(i).toBytesLE()
@ -28,19 +28,13 @@ func makeInteropPrivKey*(i: int): BlsResult[ValidatorPrivKey] =
privkeyBytes = eth2hash(bytes)
key = (UInt256.fromBytesLE(privkeyBytes.data) mod curveOrder).toBytesBE()
ValidatorPrivKey.fromRaw(key)
ValidatorPrivKey.fromRaw(key).get
const eth1BlockHash* = block:
var x: Eth2Digest
for v in x.data.mitems: v = 0x42
x
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials
func makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
var bytes = eth2hash(k.toRaw())
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
bytes
func makeDeposit*(
pubkey: ValidatorPubKey, privkey: ValidatorPrivKey, epoch = 0.Epoch,
amount: Gwei = MAX_EFFECTIVE_BALANCE.Gwei,

View File

@ -26,16 +26,6 @@ func round_step_down*(x: Natural, step: static Natural): int {.inline.} =
else:
result = x - x mod step
let ZeroHashes = block:
# hashes for a merkle tree full of zeros for leafs
var zh = @[Eth2Digest()]
for i in 1 ..< DEPOSIT_CONTRACT_TREE_DEPTH:
let nodehash = withEth2Hash:
h.update zh[i-1]
h.update zh[i-1]
zh.add nodehash
zh
type SparseMerkleTree*[Depth: static int] = object
## Sparse Merkle tree
# There is an extra "depth" layer to store leaf nodes
@ -67,7 +57,7 @@ proc merkleTreeFromLeaves*(
# with the zeroHash corresponding to the current depth
let nodeHash = withEth2Hash:
h.update result.nnznodes[depth-1][^1]
h.update ZeroHashes[depth-1]
h.update zeroHashes[depth-1]
result.nnznodes[depth].add nodeHash
proc getMerkleProof*[Depth: static int](
@ -85,7 +75,7 @@ proc getMerkleProof*[Depth: static int](
if nodeIdx < tree.nnznodes[depth].len:
result[depth] = tree.nnznodes[depth][nodeIdx]
else:
result[depth] = ZeroHashes[depth]
result[depth] = zeroHashes[depth]
proc attachMerkleProofs*(deposits: var seq[Deposit]) =
let deposit_data_roots = mapIt(deposits, it.data.hash_tree_root)

View File

@ -69,6 +69,8 @@ type
BlsResult*[T] = Result[T, cstring]
RandomSourceDepleted* = object of CatchableError
func `==`*(a, b: BlsValue): bool =
if a.kind != b.kind: return false
if a.kind == Real:
@ -82,6 +84,9 @@ template `==`*[N, T](a: BlsValue[N, T], b: T): bool =
template `==`*[N, T](a: T, b: BlsValue[N, T]): bool =
a == b.blsValue
template `==`*(a, b: ValidatorPrivKey): bool =
blscurve.SecretKey(a) == blscurve.SecretKey(b)
# API
# ----------------------------------------------------------------------
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.3/specs/phase0/beacon-chain.md#bls-signatures
@ -341,3 +346,17 @@ func init*(T: typedesc[ValidatorSig], data: array[RawSigSize, byte]): T {.noInit
if v.isErr:
raise (ref ValueError)(msg: $v.error)
return v[]
proc getRandomBytes*(n: Natural): seq[byte]
{.raises: [RandomSourceDepleted, Defect].} =
result = newSeq[byte](n)
if randomBytes(result) != result.len:
raise newException(RandomSourceDepleted, "Failed to generate random bytes")
proc getRandomBytesOrPanic*(output: var openarray[byte]) =
doAssert randomBytes(output) == output.len
proc getRandomBytesOrPanic*(n: Natural): seq[byte] =
result = newSeq[byte](n)
getRandomBytesOrPanic(result)

View File

@ -6,13 +6,13 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
json, math, strutils,
eth/keyfile/uuid,
stew/[results, byteutils],
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand],
./crypto
json, math, strutils, strformat,
eth/keyfile/uuid, stew/[results, byteutils, bitseqs, bitops2],
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand], blscurve,
datatypes, crypto, digest, helpers
export results
export
results
{.push raises: [Defect].}
@ -64,6 +64,22 @@ type
KsResult*[T] = Result[T, cstring]
Eth2KeyKind* = enum
signingKeyKind # Also known as voting key
withdrawalKeyKind
Mnemonic* = distinct string
KeyPath* = distinct string
KeyStorePass* = distinct string
KeyStoreContent* = distinct JsonString
KeySeed* = distinct seq[byte]
Credentials* = object
mnemonic*: Mnemonic
keyStore*: KeyStoreContent
signingKey*: ValidatorPrivKey
withdrawalKey*: ValidatorPrivKey
const
saltSize = 32
@ -80,6 +96,108 @@ const
prf: "hmac-sha256"
)
# https://eips.ethereum.org/EIPS/eip-2334
eth2KeyPurpose = 12381
eth2CoinType* = 3600
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
wordListLen = 2048
englishWords = split slurp("english_word_list.txt")
iterator pathNodesImpl(path: string): Natural
{.raises: [ValueError].} =
for elem in path.split("/"):
if elem == "m": continue
yield parseInt(elem)
func append*(path: KeyPath, pathNode: Natural): KeyPath =
KeyPath(path.string & "/" & $pathNode)
func validateKeyPath*(path: TaintedString): KeyPath
{.raises: [ValueError].} =
for elem in pathNodesImpl(path.string): discard elem
KeyPath path
iterator pathNodes(path: KeyPath): Natural =
try:
for elem in pathNodesImpl(path.string):
yield elem
except ValueError:
doAssert false, "Make sure you've validated the key path with `validateKeyPath`"
func makeKeyPath*(validatorIdx: Natural,
keyType: Eth2KeyKind): KeyPath =
# https://eips.ethereum.org/EIPS/eip-2334
let use = case keyType
of withdrawalKeyKind: "0"
of signingKeyKind: "0/0"
try:
KeyPath &"m/{eth2KeyPurpose}/{eth2CoinType}/{validatorIdx}/{use}"
except ValueError:
raiseAssert "All values above can be converted successfully to strings"
func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed =
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
let salt = "mnemonic-" & password.string
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
proc generateMnemonic*(words: openarray[string],
entropyParam: openarray[byte] = @[]): Mnemonic =
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
doAssert words.len == wordListLen
var entropy: seq[byte]
if entropyParam.len == 0:
entropy = getRandomBytesOrPanic(32)
else:
doAssert entropyParam.len >= 128 and
entropyParam.len <= 256 and
entropyParam.len mod 32 == 0
entropy = @entropyParam
let
checksumBits = entropy.len div 4 # ranges from 4 to 8
mnemonicWordCount = 12 + (checksumBits - 4) * 3
checksum = sha256.digest(entropy)
entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits))
var res = words[entropy.getBitsBE(0..10)]
for i in 1 ..< mnemonicWordCount:
let
firstBit = i*11
lastBit = firstBit + 10
res.add " "
res.add words[entropy.getBitsBE(firstBit..lastBit)]
Mnemonic res
proc deriveChildKey*(parentKey: ValidatorPrivKey,
index: Natural): ValidatorPrivKey =
doAssert derive_child_secretKey(SecretKey result,
SecretKey parentKey,
uint32 index)
proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey =
doAssert derive_master_secretKey(SecretKey result,
seq[byte] seed)
proc deriveMasterKey*(mnemonic: Mnemonic,
password: KeyStorePass): ValidatorPrivKey =
deriveMasterKey(getSeed(mnemonic, password))
proc deriveChildKey*(masterKey: ValidatorPrivKey,
path: KeyPath): ValidatorPrivKey =
result = masterKey
for idx in pathNodes(path):
result = deriveChildKey(result, idx)
proc keyFromPath*(mnemonic: Mnemonic,
password: KeyStorePass,
path: KeyPath): ValidatorPrivKey =
deriveChildKey(deriveMasterKey(mnemonic, password), path)
proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] =
var ctx: sha256
ctx.init()
@ -100,12 +218,11 @@ template hexToBytes(data, name: string): untyped =
except ValueError:
return err "ks: failed to parse " & name
proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] =
let ks =
try:
parseJson(data)
except Exception:
return err "ks: failed to parse keystore"
proc decryptKeystore*(data: KeyStoreContent,
password: KeyStorePass): KsResult[ValidatorPrivKey] =
# TODO: `parseJson` can raise a general `Exception`
let ks = try: parseJson(data.string)
except Exception: return err "ks: failed to parse keystore"
var
decKey: seq[byte]
@ -126,7 +243,7 @@ proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] =
kdfParams = crypto.kdf.params
salt = hexToBytes(kdfParams.salt, "salt")
decKey = sha256.pbkdf2(passphrase, salt, kdfParams.c, kdfParams.dklen)
decKey = sha256.pbkdf2(password.string, salt, kdfParams.c, kdfParams.dklen)
iv = hexToBytes(crypto.cipher.params.iv, "iv")
cipherMsg = hexToBytes(crypto.cipher.message, "cipher")
checksumMsg = hexToBytes(crypto.checksum.message, "checksum")
@ -151,41 +268,41 @@ proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] =
aesCipher.decrypt(cipherMsg, secret)
aesCipher.clear()
result = ok secret
ValidatorPrivKey.fromRaw(secret)
proc encryptKeystore*[T: KdfParams](secret: openarray[byte];
passphrase: string;
path="";
salt: openarray[byte] = @[];
iv: openarray[byte] = @[];
ugly=true): KsResult[string] =
proc encryptKeystore*(T: type[KdfParams],
privKey: ValidatorPrivkey,
password = KeyStorePass "",
path = KeyPath "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
ugly = true): KeyStoreContent =
var
secret = privKey.toRaw[^32..^1]
decKey: seq[byte]
aesCipher: CTR[aes128]
aesIv = newSeq[byte](aes128.sizeBlock)
kdfSalt = newSeq[byte](saltSize)
cipherMsg = newSeq[byte](secret.len)
if salt.len == saltSize:
if salt.len > 0:
doAssert salt.len == saltSize
kdfSalt = @salt
elif salt.len > 0:
return err "ks: invalid salt"
elif randomBytes(kdfSalt) != saltSize:
return err "ks: no random bytes for salt"
else:
getRandomBytesOrPanic(kdfSalt)
if iv.len == aes128.sizeBlock:
if iv.len > 0:
doAssert iv.len == aes128.sizeBlock
aesIv = @iv
elif iv.len > 0:
return err "ks: invalid iv"
elif randomBytes(aesIv) != aes128.sizeBlock:
return err "ks: no random bytes for iv"
else:
getRandomBytesOrPanic(aesIv)
when T is KdfPbkdf2:
decKey = sha256.pbkdf2(passphrase, kdfSalt, pbkdf2Params.c,
decKey = sha256.pbkdf2(password.string, kdfSalt, pbkdf2Params.c,
pbkdf2Params.dklen)
var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "")
kdf.params.salt = kdfSalt.toHex()
kdf.params.salt = byteutils.toHex(kdfSalt)
else:
return
@ -193,29 +310,75 @@ proc encryptKeystore*[T: KdfParams](secret: openarray[byte];
aesCipher.encrypt(secret, cipherMsg)
aesCipher.clear()
let pubkey = (? ValidatorPrivkey.fromRaw(secret)).toPubKey()
let pubkey = privKey.toPubKey()
let
sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
uuid = uuidGenerate().get
keystore = Keystore[T](
crypto: Crypto[T](
kdf: kdf,
checksum: Checksum(
function: "sha256",
message: sum.toHex()
message: byteutils.toHex(sum)
),
cipher: Cipher(
function: "aes-128-ctr",
params: CipherParams(iv: aesIv.toHex()),
message: cipherMsg.toHex()
params: CipherParams(iv: byteutils.toHex(aesIv)),
message: byteutils.toHex(cipherMsg)
)
),
pubkey: pubkey.toHex(),
path: path,
uuid: $(? uuidGenerate()),
version: 4
)
pubkey: toHex(pubkey),
path: path.string,
uuid: $uuid,
version: 4)
KeyStoreContent if ugly: $(%keystore)
else: pretty(%keystore, indent=4)
proc restoreCredentials*(mnemonic: Mnemonic,
password = KeyStorePass ""): Credentials =
let
withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
withdrawalKey = keyFromPath(mnemonic, password, withdrawalKeyPath)
signingKeyPath = withdrawalKeyPath.append 0
signingKey = deriveChildKey(withdrawalKey, 0)
Credentials(
mnemonic: mnemonic,
keyStore: encryptKeystore(KdfPbkdf2, signingKey, password, signingKeyPath),
signingKey: signingKey,
withdrawalKey: withdrawalKey)
proc generateCredentials*(entropy: openarray[byte] = @[],
password = KeyStorePass ""): Credentials =
let mnemonic = generateMnemonic(englishWords, entropy)
restoreCredentials(mnemonic, password)
# https://github.com/ethereum/eth2.0-specs/blob/v0.11.1/specs/phase0/deposit-contract.md#withdrawal-credentials
proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
var bytes = eth2hash(k.toRaw())
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
bytes
proc prepareDeposit*(credentials: Credentials,
amount = MAX_EFFECTIVE_BALANCE.Gwei): Deposit =
let
withdrawalPubKey = credentials.withdrawalKey.toPubKey
signingPubKey = credentials.signingKey.toPubKey
var
ret = Deposit(
data: DepositData(
amount: amount,
pubkey: signingPubKey,
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey)))
let domain = compute_domain(DOMAIN_DEPOSIT)
let signing_root = compute_signing_root(ret.getDepositMessage, domain)
ret.data.signature = bls_sign(credentials.signingKey, signing_root.data)
ret
result = ok(if ugly: $(%keystore)
else: pretty(%keystore, indent=4))

View File

@ -19,3 +19,4 @@ import
export
merkleization, ssz_serialization, types

View File

@ -72,7 +72,7 @@ func computeZeroHashes: array[sizeof(Limit) * 8, Eth2Digest] =
for i in 1 .. result.high:
result[i] = mergeBranches(result[i - 1], result[i - 1])
const zeroHashes = computeZeroHashes()
const zeroHashes* = computeZeroHashes()
func addChunk(merkleizer: var SszChunksMerkleizer, data: openarray[byte]) =
doAssert data.len > 0 and data.len <= bytesPerChunk

View File

@ -21,7 +21,7 @@ import
eth2_network, eth2_discovery, validator_pool, beacon_node_types,
nimbus_binary_common,
version, ssz/merkleization,
sync_manager,
sync_manager, validator_keygen,
spec/eth2_apis/validator_callsigs_types,
eth2_json_rpc_serialization

View File

@ -20,7 +20,7 @@ import
# Local modules
spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network],
conf, time, validator_pool, state_transition,
attestation_pool, block_pool, eth2_network,
attestation_pool, block_pool, eth2_network, validator_keygen,
beacon_node_common, beacon_node_types, nimbus_binary_common,
mainchain_monitor, version, ssz/merkleization, interop,
attestation_aggregation, sync_manager, sszdump

View File

@ -1,76 +1,153 @@
import
os, strutils,
os, strutils, terminal,
chronicles, chronos, blscurve, nimcrypto, json_serialization, serialization,
web3, stint, eth/keys,
spec/[datatypes, digest, crypto], conf, ssz/merkleization, interop, merkle_minimal
web3, stint, eth/keys, confutils,
spec/[datatypes, digest, crypto, keystore], conf, ssz/merkleization, merkle_minimal
contract(DepositContract):
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32])
const
keystoreFileName* = "keystore.json"
depositFileName* = "deposit.json"
type
DelayGenerator* = proc(): chronos.Duration {.closure, gcsafe.}
proc writeTextFile(filename: string, contents: string) =
writeFile(filename, contents)
# echo "Wrote ", filename
proc writeFile(filename: string, value: auto) =
Json.saveFile(filename, value, pretty = true)
# echo "Wrote ", filename
{.push raises: [Defect].}
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
proc generateDeposits*(totalValidators: int,
outputDir: string,
randomKeys: bool,
firstIdx = 0): seq[Deposit] =
info "Generating deposits", totalValidators, outputDir, randomKeys
for i in 0 ..< totalValidators:
let
v = validatorFileBaseName(firstIdx + i)
depositFn = outputDir / v & ".deposit.json"
privKeyFn = outputDir / v & ".privkey"
proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf,
validatorsDir, keyName: string): Option[ValidatorPrivKey] =
let
keystorePath = validatorsDir / keyName / keystoreFileName
keystoreContents = KeyStoreContent:
try: readFile(keystorePath)
except IOError as err:
error "Failed to read keystore", err = err.msg, path = keystorePath
return
if existsFile(depositFn) and existsFile(privKeyFn):
try:
result.add Json.loadFile(depositFn, Deposit)
continue
except SerializationError as err:
debug "Rewriting unreadable deposit", err = err.formatMsg(depositFn)
discard
if conf.secretsDir.isSome:
let passphrasePath = conf.secretsDir.get / keyName
if fileExists(passphrasePath):
let
passphrase = KeyStorePass:
try: readFile(passphrasePath)
except IOError as err:
error "Failed to read passphrase file", err = err.msg, path = passphrasePath
return
var
privkey{.noInit.}: ValidatorPrivKey
pubKey{.noInit.}: ValidatorPubKey
let res = decryptKeystore(keystoreContents, passphrase)
if res.isOk:
return res.get.some
else:
error "Failed to decrypt keystore", keystorePath, passphrasePath
return
if randomKeys:
(pubKey, privKey) = crypto.newKeyPair().tryGet()
if conf.nonInteractive:
error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir",
keyName, validatorsDir, secretsDir = conf.secretsDir
return
var remainingAttempts = 3
var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\""
while remainingAttempts > 0:
let passphrase = KeyStorePass:
try: readPasswordFromStdin(prompt)
except IOError:
error "STDIN not readable. Cannot obtain KeyStore password"
return
let decrypted = decryptKeystore(keystoreContents, passphrase)
if decrypted.isOk:
return decrypted.get.some
else:
privKey = makeInteropPrivKey(i).tryGet()
pubKey = privKey.toPubKey()
prompt = "Keystore decryption failed. Please try again"
dec remainingAttempts
let dp = makeDeposit(pubKey, privKey)
iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey =
for validatorKeyFile in conf.validators:
try:
yield validatorKeyFile.load
except CatchableError as err:
error "Failed to load validator private key",
file = validatorKeyFile.string, err = err.msg
quit 1
result.add(dp)
let validatorsDir = conf.localValidatorsDir
try:
for kind, file in walkDir(validatorsDir):
if kind == pcDir:
let keyName = splitFile(file).name
let key = loadKeyStore(conf, validatorsDir, keyName)
if key.isSome:
yield key.get
else:
quit 1
except OSError as err:
error "Validator keystores directory not accessible",
path = validatorsDir, err = err.msg
quit 1
type
GenerateDepositsError = enum
RandomSourceDepleted,
FailedToCreateValidatoDir
FailedToCreateSecretFile
FailedToCreateKeystoreFile
FailedToCreateDepositFile
proc generateDeposits*(totalValidators: int,
validatorsDir: string,
secretsDir: string): Result[seq[Deposit], GenerateDepositsError] =
var deposits: seq[Deposit]
info "Generating deposits", totalValidators, validatorsDir, secretsDir
for i in 0 ..< totalValidators:
let password = KeyStorePass getRandomBytesOrPanic(32).toHex
let credentials = generateCredentials(password = password)
let
keyName = $(credentials.signingKey.toPubKey)
validatorDir = validatorsDir / keyName
passphraseFile = secretsDir / keyName
depositFile = validatorDir / depositFileName
keystoreFile = validatorDir / keystoreFileName
if existsDir(validatorDir) and existsFile(depositFile):
continue
try: createDir validatorDir
except OSError, IOError: return err FailedToCreateValidatoDir
try: writeFile(secretsDir / keyName, password.string)
except IOError: return err FailedToCreateSecretFile
try: writeFile(keystoreFile, credentials.keyStore.string)
except IOError: return err FailedToCreateKeystoreFile
deposits.add credentials.prepareDeposit()
# Does quadratic additional work, but fast enough, and otherwise more
# cleanly allows free intermixing of pre-existing and newly generated
# deposit and private key files. TODO: only generate new Merkle proof
# for the most recent deposit if this becomes bottleneck.
attachMerkleProofs(result)
attachMerkleProofs(deposits)
try: Json.saveFile(depositFile, deposits[^1], pretty = true)
except: return err FailedToCreateDepositFile
writeTextFile(privKeyFn, privKey.toHex())
writeFile(depositFn, result[result.len - 1])
ok deposits
proc sendDeposits*(
deposits: seq[Deposit],
web3Url, depositContractAddress, privateKey: string,
delayGenerator: DelayGenerator = nil) {.async.} =
{.pop.}
proc sendDeposits*(deposits: seq[Deposit],
web3Url, depositContractAddress, privateKey: string,
delayGenerator: DelayGenerator = nil) {.async.} =
var web3 = await newWeb3(web3Url)
if privateKey.len != 0:
web3.privateKey = PrivateKey.fromHex(privateKey).tryGet()
web3.privateKey = PrivateKey.fromHex(privateKey).tryGet
else:
let accounts = await web3.provider.eth_accounts()
if accounts.len == 0:
@ -79,9 +156,9 @@ proc sendDeposits*(
web3.defaultAccount = accounts[0]
let contractAddress = Address.fromHex(depositContractAddress)
let depositContract = web3.contractSender(DepositContract, contractAddress)
for i, dp in deposits:
let depositContract = web3.contractSender(DepositContract, contractAddress)
discard await depositContract.deposit(
Bytes48(dp.data.pubKey.toRaw()),
Bytes32(dp.data.withdrawal_credentials.data),
@ -91,17 +168,3 @@ proc sendDeposits*(
if delayGenerator != nil:
await sleepAsync(delayGenerator())
when isMainModule:
import confutils
cli do (totalValidators: int = 125000,
outputDir: string = "validators",
randomKeys: bool = false,
web3Url: string = "",
depositContractAddress: string = ""):
let deposits = generateDeposits(totalValidators, outputDir, randomKeys)
if web3Url.len() > 0 and depositContractAddress.len() > 0:
echo "Sending deposits to eth1..."
waitFor sendDeposits(deposits, web3Url, depositContractAddress, "")
echo "Done"

View File

@ -87,6 +87,7 @@ cli do (skipGoerliKey {.
.replace(")", "_")
dataDir = buildDir / "data" / dataDirName
validatorsDir = dataDir / "validators"
secretsDir = dataDir / "secrets"
beaconNodeBinary = buildDir / "beacon_node_" & dataDirName
var
nimFlags = "-d:chronicles_log_level=TRACE " & getEnv("NIM_PARAMS")
@ -137,7 +138,8 @@ cli do (skipGoerliKey {.
mode = Verbose
exec replace(&"""{beaconNodeBinary} makeDeposits
--random-deposits=1
--deposits-dir="{validatorsDir}"
--out-validators-dir="{validatorsDir}"
--out-secrets-dir="{secretsDir}"
--deposit-private-key={privKey}
--web3-url={web3Url}
{depositContractOpt}

View File

@ -114,8 +114,13 @@ fi
NETWORK="testnet${TESTNET}"
rm -rf "${DATA_DIR}"
DEPOSITS_DIR="${DATA_DIR}/deposits_dir"
mkdir -p "${DEPOSITS_DIR}"
SECRETS_DIR="${DATA_DIR}/secrets"
mkdir -p "${SECRETS_DIR}"
NETWORK_DIR="${DATA_DIR}/network_dir"
mkdir -p "${NETWORK_DIR}"
@ -134,17 +139,16 @@ NETWORK_NIM_FLAGS=$(scripts/load-testnet-nim-flags.sh ${NETWORK})
$MAKE LOG_LEVEL="${LOG_LEVEL}" NIMFLAGS="-d:insecure -d:testnet_servers_image ${NETWORK_NIM_FLAGS}" beacon_node
./build/beacon_node makeDeposits \
--quickstart-deposits=${QUICKSTART_VALIDATORS} \
--random-deposits=${RANDOM_VALIDATORS} \
--deposits-dir="${DEPOSITS_DIR}"
--count=${TOTAL_VALIDATORS} \
--out-validators-dir="${DEPOSITS_DIR}" \
--out-secrets-dir="${SECRETS_DIR}"
TOTAL_VALIDATORS="$(( $QUICKSTART_VALIDATORS + $RANDOM_VALIDATORS ))"
BOOTSTRAP_IP="127.0.0.1"
./build/beacon_node createTestnet \
--data-dir="${DATA_DIR}/node0" \
--validators-dir="${DEPOSITS_DIR}" \
--total-validators=${TOTAL_VALIDATORS} \
--last-user-validator=${QUICKSTART_VALIDATORS} \
--last-user-validator=${USER_VALIDATORS} \
--output-genesis="${NETWORK_DIR}/genesis.ssz" \
--output-bootstrap-file="${NETWORK_DIR}/bootstrap_nodes.txt" \
--bootstrap-address=${BOOTSTRAP_IP} \
@ -199,11 +203,12 @@ for NUM_NODE in $(seq 0 $(( ${NUM_NODES} - 1 ))); do
fi
# Copy validators to individual nodes.
# The first $NODES_WITH_VALIDATORS nodes split them equally between them, after skipping the first $QUICKSTART_VALIDATORS.
# The first $NODES_WITH_VALIDATORS nodes split them equally between them, after skipping the first $USER_VALIDATORS.
NODE_DATA_DIR="${DATA_DIR}/node${NUM_NODE}"
mkdir -p "${NODE_DATA_DIR}/validators"
if [[ $NUM_NODE -lt $NODES_WITH_VALIDATORS ]]; then
for KEYFILE in $(ls ${DEPOSITS_DIR}/*.privkey | tail -n +$(( $QUICKSTART_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do
# TODO: There are no longer privkey files
for KEYFILE in $(ls ${DEPOSITS_DIR}/*.privkey | tail -n +$(( $USER_VALIDATORS + ($VALIDATORS_PER_NODE * $NUM_NODE) + 1 )) | head -n $VALIDATORS_PER_NODE); do
cp -a "$KEYFILE" "${NODE_DATA_DIR}/validators/"
done
fi

View File

@ -46,6 +46,7 @@ ETH2_TESTNETS_ABS=$(cd "$ETH2_TESTNETS"; pwd)
NETWORK_DIR_ABS="$ETH2_TESTNETS_ABS/nimbus/$NETWORK"
DATA_DIR_ABS=$(mkdir -p "$DATA_DIR"; cd "$DATA_DIR"; pwd)
DEPOSITS_DIR_ABS="$DATA_DIR_ABS/deposits"
SECRETS_DIR_ABS="$DATA_DIR_ABS/secrets"
DEPOSIT_CONTRACT_ADDRESS=""
DEPOSIT_CONTRACT_ADDRESS_ARG=""
@ -54,6 +55,7 @@ if [ "$WEB3_URL" != "" ]; then
fi
mkdir -p "$DEPOSITS_DIR_ABS"
mkdir -p "$SECRETS_DIR_ABS"
if [ "$ETH1_PRIVATE_KEY" != "" ]; then
make deposit_contract
@ -82,17 +84,15 @@ echo "Building Docker image..."
make build
../build/beacon_node makeDeposits \
--quickstart-deposits=$QUICKSTART_VALIDATORS \
--random-deposits=$RANDOM_VALIDATORS \
--deposits-dir="$DEPOSITS_DIR_ABS"
TOTAL_VALIDATORS="$(( $QUICKSTART_VALIDATORS + $RANDOM_VALIDATORS ))"
--count=$TOTAL_VALIDATORS \
--out-validators-dir="$DEPOSITS_DIR_ABS" \
--out-secrets-dir="$SECRETS_DIR_ABS"
../build/beacon_node createTestnet \
--data-dir="$DATA_DIR_ABS" \
--validators-dir="$DEPOSITS_DIR_ABS" \
--total-validators=$TOTAL_VALIDATORS \
--last-user-validator=$QUICKSTART_VALIDATORS \
--last-user-validator=$USER_VALIDATORS \
--output-genesis="$NETWORK_DIR_ABS/genesis.ssz" \
--output-bootstrap-file="$NETWORK_DIR_ABS/bootstrap_nodes.txt" \
--bootstrap-address=$BOOTSTRAP_IP \
@ -116,7 +116,7 @@ if [[ $PUBLISH_TESTNET_RESETS != "0" ]]; then
--network=$NETWORK \
--deposits-dir="$DEPOSITS_DIR_ABS" \
--network-data-dir="$NETWORK_DIR_ABS" \
--user-validators=$QUICKSTART_VALIDATORS \
--user-validators=$USER_VALIDATORS \
--total-validators=$TOTAL_VALIDATORS \
> /tmp/reset-network.sh

View File

@ -1,5 +1,5 @@
CONST_PRESET=minimal
QUICKSTART_VALIDATORS=8
RANDOM_VALIDATORS=120
USER_VALIDATORS=10
TOTAL_VALIDATORS=256
BOOTSTRAP_PORT=9000
WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a

View File

@ -1,6 +1,6 @@
CONST_PRESET=mainnet
QUICKSTART_VALIDATORS=8
RANDOM_VALIDATORS=120
USER_VALIDATORS=10
TOTAL_VALIDATORS=256
BOOTSTRAP_PORT=9100
WEB3_URL=wss://goerli.infura.io/ws/v3/809a18497dd74102b5f37d25aae3c85a

View File

@ -9,6 +9,7 @@ source "$(dirname "$0")/vars.sh"
cd "$SIM_ROOT"
mkdir -p "$SIMULATION_DIR"
mkdir -p "$VALIDATORS_DIR"
mkdir -p "$SECRETS_DIR"
cd "$GIT_ROOT"
@ -118,8 +119,9 @@ if [ ! -f "${LAST_VALIDATOR}" ]; then
fi
$BEACON_NODE_BIN makeDeposits \
--quickstart-deposits="${NUM_VALIDATORS}" \
--deposits-dir="$VALIDATORS_DIR" \
--count="${NUM_VALIDATORS}" \
--out-validators-dir="$VALIDATORS_DIR" \
--out-secrets-dir="$SECRETS_DIR" \
$MAKE_DEPOSITS_WEB3_ARG $DELAY_ARGS \
--deposit-contract="${DEPOSIT_CONTRACT_ADDRESS}"

View File

@ -28,6 +28,7 @@ MASTER_NODE=$(( TOTAL_NODES - 1 ))
SIMULATION_DIR="${SIM_ROOT}/data"
METRICS_DIR="${SIM_ROOT}/prometheus"
VALIDATORS_DIR="${SIM_ROOT}/validators"
SECRETS_DIR="${SIM_ROOT}/secrets"
SNAPSHOT_FILE="${SIMULATION_DIR}/state_snapshot.ssz"
NETWORK_BOOTSTRAP_FILE="${SIMULATION_DIR}/bootstrap_nodes.txt"
BEACON_NODE_BIN="${GIT_ROOT}/build/beacon_node"

View File

@ -119,7 +119,7 @@ suiteReport "Interop":
timedTest "Mocked start private key":
for i, k in privateKeys:
let
key = makeInteropPrivKey(i)[]
key = makeInteropPrivKey(i)
v = k.parse(UInt256, 16)
check:
@ -144,9 +144,8 @@ suiteReport "Interop":
var deposits: seq[Deposit]
for i in 0..<64:
let
privKey = makeInteropPrivKey(i)[]
deposits.add(makeDeposit(privKey.toPubKey(), privKey))
let privKey = makeInteropPrivKey(i)
deposits.add makeDeposit(privKey.toPubKey(), privKey)
const genesis_time = 1570500000
var

View File

@ -10,7 +10,7 @@
import
unittest, ./testutil, json,
stew/byteutils,
../beacon_chain/spec/keystore
../beacon_chain/spec/[crypto, keystore]
from strutils import replace
@ -79,23 +79,27 @@ const
}""" #"
password = "testpassword"
secret = hexToSeqByte("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")
secretBytes = hexToSeqByte("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")
salt = hexToSeqByte("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
iv = hexToSeqByte("264daa3f303d7259501c93d997d84fe6")
suiteReport "Keystore":
setup:
let secret = ValidatorPrivKey.fromRaw(secretBytes).get
timedTest "Pbkdf2 decryption":
let decrypt = decryptKeystore(pbkdf2Vector, password)
let decrypt = decryptKeystore(KeyStoreContent pbkdf2Vector,
KeyStorePass password)
check decrypt.isOk
check secret == decrypt.get()
timedTest "Pbkdf2 encryption":
let encrypt = encryptKeystore[KdfPbkdf2](secret, password, salt=salt, iv=iv,
path="m/12381/60/0/0")
check encrypt.isOk
let encrypt = encryptKeystore(KdfPbkdf2, secret,
KeyStorePass password,
salt=salt, iv=iv,
path = validateKeyPath "m/12381/60/0/0")
var
encryptJson = parseJson(encrypt.get())
encryptJson = parseJson(encrypt.string)
pbkdf2Json = parseJson(pbkdf2Vector)
encryptJson{"uuid"} = %""
pbkdf2Json{"uuid"} = %""
@ -103,16 +107,27 @@ suiteReport "Keystore":
check encryptJson == pbkdf2Json
timedTest "Pbkdf2 errors":
check encryptKeystore[KdfPbkdf2](secret, "", salt = [byte 1]).isErr
check encryptKeystore[KdfPbkdf2](secret, "", iv = [byte 1]).isErr
expect Defect:
echo encryptKeystore(KdfPbkdf2, secret, salt = [byte 1]).string
check decryptKeystore(pbkdf2Vector, "wrong pass").isErr
check decryptKeystore(pbkdf2Vector, "").isErr
check decryptKeystore("{\"a\": 0}", "").isErr
check decryptKeystore("", "").isErr
expect Defect:
echo encryptKeystore(KdfPbkdf2, secret, iv = [byte 1]).string
check decryptKeystore(KeyStoreContent pbkdf2Vector,
KeyStorePass "wrong pass").isErr
check decryptKeystore(KeyStoreContent pbkdf2Vector,
KeyStorePass "").isErr
check decryptKeystore(KeyStoreContent "{\"a\": 0}",
KeyStorePass "").isErr
check decryptKeystore(KeyStoreContent "",
KeyStorePass "").isErr
template checkVariant(remove): untyped =
check decryptKeystore(pbkdf2Vector.replace(remove, ""), password).isErr
check decryptKeystore(KeyStoreContent pbkdf2Vector.replace(remove, ""),
KeyStorePass password).isErr
checkVariant "d4e5" # salt
checkVariant "18b1" # checksum
@ -122,4 +137,5 @@ suiteReport "Keystore":
var badKdf = parseJson(pbkdf2Vector)
badKdf{"crypto", "kdf", "function"} = %"invalid"
check decryptKeystore($badKdf, password).iserr
check decryptKeystore(KeyStoreContent $badKdf,
KeyStorePass password).iserr