211 lines
7.1 KiB
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)
|
|
|