nimbus-eth2/beacon_chain/keystore_management.nim

211 lines
7.1 KiB
Nim

import
os, strutils, terminal,
stew/byteutils, chronicles, chronos, web3, stint, json_serialization,
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
spec/[datatypes, digest, crypto, keystore],
conf, ssz/merkleization, merkle_minimal, network_metadata
export
keystore
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.}
{.push raises: [Defect].}
proc ethToWei(eth: UInt256): UInt256 =
eth * 1000000000000000000.u256
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
let passphrasePath = conf.secretsDir / 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
let res = decryptKeystore(keystoreContents, passphrase)
if res.isOk:
return res.get.some
else:
error "Failed to decrypt keystore", keystorePath, passphrasePath
return
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 & "\"\n"
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:
prompt = "Keystore decryption failed. Please try again"
dec remainingAttempts
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
let validatorsDir = conf.validatorsDir
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*(preset: RuntimePreset,
rng: var BrHmacDrbgContext,
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 getRandomBytes(rng, 32).toHex
let credentials = generateCredentials(rng, password = password)
let
keyName = intToStr(i, 6) & "_" & $(credentials.signingKey.toPubKey)
validatorDir = validatorsDir / 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(preset)
# 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(deposits)
try: Json.saveFile(depositFile, deposits[^1], pretty = true)
except: return err FailedToCreateDepositFile
ok deposits
proc loadDeposits*(depositsDir: string): seq[Deposit] =
try:
for kind, dir in walkDir(depositsDir):
if kind == pcDir:
let depositFile = dir / depositFileName
try:
result.add Json.loadFile(depositFile, Deposit)
except IOError as err:
error "Failed to open deposit file", depositFile, err = err.msg
quit 1
except SerializationError as err:
error "Invalid deposit file", error = formatMsg(err, depositFile)
quit 1
except OSError as err:
error "Deposits directory not accessible",
path = depositsDir, err = err.msg
quit 1
{.pop.}
# TODO: async functions should note take `seq` inputs because
# this leads to full copies.
proc sendDeposits*(deposits: seq[Deposit],
web3Url, privateKey: string,
depositContractAddress: Eth1Address,
delayGenerator: DelayGenerator = nil) {.async.} =
var web3 = await newWeb3(web3Url)
if privateKey.len != 0:
web3.privateKey = some(PrivateKey.fromHex(privateKey).tryGet)
else:
let accounts = await web3.provider.eth_accounts()
if accounts.len == 0:
error "No account offered by the web3 provider", web3Url
return
web3.defaultAccount = accounts[0]
let depositContract = web3.contractSender(DepositContract,
Address depositContractAddress)
for i, dp in deposits:
let status = await depositContract.deposit(
Bytes48(dp.data.pubKey.toRaw()),
Bytes32(dp.data.withdrawal_credentials.data),
Bytes96(dp.data.signature.toRaw()),
FixedBytes[32](hash_tree_root(dp.data).data)).send(value = 32.u256.ethToWei, gasPrice = 1)
info "Deposit sent", status = $status
if delayGenerator != nil:
await sleepAsync(delayGenerator())
proc sendDeposits*(config: BeaconNodeConf,
deposits: seq[Deposit],
delayGenerator: DelayGenerator = nil) {.async.} =
info "Sending deposits",
web3 = config.web3Url,
depositContract = config.depositContractAddress
await sendDeposits(
deposits,
config.web3Url,
config.depositPrivateKey,
config.depositContractAddress.get,
delayGenerator)