Implement the 'deposits exit' command; Remove 'deposits create'

This commit is contained in:
Zahary Karadjov 2020-11-27 21:48:33 +02:00 committed by zah
parent a2364ce1bc
commit 3c0dfc2fbe
12 changed files with 274 additions and 47 deletions

View File

@ -1,7 +1,7 @@
{.push raises: [Defect].}
import
strutils, os, options, unicode,
strutils, os, options, unicode, uri,
chronicles, chronicles/options as chroniclesOptions,
confutils, confutils/defs, confutils/std/net, stew/shims/net as stewNet,
stew/io2, unicodedb/properties, normalize,
@ -12,12 +12,11 @@ import
network_metadata, filepath
export
uri,
defaultEth2TcpPort, enabledLogLevel, ValidIpAddress,
defs, parseCmdArg, completeCmdArg, network_metadata
type
ValidatorKeyPath* = TypedInputFile[ValidatorPrivKey, Txt, "privkey"]
BNStartUpCmd* = enum
noCommand
createTestnet
@ -31,9 +30,10 @@ type
list = "Lists details about all wallets"
DepositsCmd* {.pure.} = enum
create = "Creates validator keystores and deposits"
# create = "Creates validator keystores and deposits"
`import` = "Imports password-protected keystores interactively"
status = "Displays status information about all deposits"
# status = "Displays status information about all deposits"
exit = "Submits a validator voluntary exit"
VCStartUpCmd* = enum
VCNoCommand
@ -325,6 +325,7 @@ type
of deposits:
case depositsCmd* {.command.}: DepositsCmd
#[
of DepositsCmd.create:
totalDeposits* {.
defaultValue: 1
@ -357,13 +358,28 @@ type
desc: "Output wallet file"
name: "new-wallet-file" }: Option[OutFile]
of DepositsCmd.status:
discard
#]#
of DepositsCmd.`import`:
importedDepositsDir* {.
argument
desc: "A directory with keystores to import" }: Option[InputDir]
of DepositsCmd.status:
discard
of DepositsCmd.exit:
exitedValidator* {.
name: "validator"
desc: "Validator index or a public key of the exited validator" }: string
rpcUrlForExit* {.
name: "rpc-url"
defaultValue: parseUri("wss://localhost:" & $defaultEth2RpcPort)
desc: "URL of the beacon node JSON-RPC service" }: Uri
exitAtEpoch* {.
name: "epoch"
desc: "The desired exit epoch" }: Option[uint64]
of record:
case recordCmd* {.command.}: RecordCmd
@ -501,6 +517,13 @@ func parseCmdArg*(T: type BlockHashOrNumber, input: TaintedString): T
func completeCmdArg*(T: type BlockHashOrNumber, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type Uri, input: TaintedString): T
{.raises: [ValueError, Defect].} =
parseUri(input.string)
func completeCmdArg*(T: type Uri, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type Checkpoint, input: TaintedString): T
{.raises: [ValueError, Defect].} =
let sepIdx = find(input.string, ':')
@ -569,9 +592,11 @@ func outWalletName*(conf: BeaconNodeConf): Option[WalletName] =
of WalletsCmd.restore: conf.restoredWalletNameFlag
of WalletsCmd.list: fail()
of deposits:
case conf.depositsCmd
of DepositsCmd.create: conf.newWalletNameFlag
else: fail()
# TODO: Uncomment when the deposits create command is restored
#case conf.depositsCmd
#of DepositsCmd.create: conf.newWalletNameFlag
#else: fail()
fail()
else:
fail()
@ -586,9 +611,11 @@ func outWalletFile*(conf: BeaconNodeConf): Option[OutFile] =
of WalletsCmd.restore: conf.restoredWalletFileFlag
of WalletsCmd.list: fail()
of deposits:
case conf.depositsCmd
of DepositsCmd.create: conf.newWalletFileFlag
else: fail()
# TODO: Uncomment when the deposits create command is restored
#case conf.depositsCmd
#of DepositsCmd.create: conf.newWalletFileFlag
#else: fail()
fail()
else:
fail()

View File

@ -48,9 +48,19 @@ template genFromJsonForIntType(T: untyped) =
proc fromJson*(n: JsonNode, argName: string, result: var T) =
n.kind.expect(JInt, argName)
let asInt = n.getBiggestInt()
# signed -> unsigned conversions are unchecked
# https://github.com/nim-lang/RFCs/issues/175
when T is Epoch:
if asInt == -1:
# TODO: This is a major hack here. Since the json library
# cannot handle properly 0xffffffff when serializing and
# deserializing uint64 values, we detect one known wrong
# result, appering in most `Validator` records. To fix
# this issue, we'll have to switch to nim-json-serialization
# in nim-json-rpc or work towards implementing a fix upstream.
result = FAR_FUTURE_EPOCH
return
if asInt < 0:
# signed -> unsigned conversions are unchecked
# https://github.com/nim-lang/RFCs/issues/175
raise newException(
ValueError, "JSON-RPC input is an unexpected negative value")
result = T(asInt)

View File

@ -213,7 +213,7 @@ proc validateProposerSlashing*(
# https://github.com/ethereum/eth2.0-specs/blob/v1.0.0/specs/phase0/p2p-interface.md#voluntary_exit
proc validateVoluntaryExit*(
pool: var ExitPool, signed_voluntary_exit: SignedVoluntaryExit):
Result[bool, (ValidationResult, cstring)] =
Result[void, (ValidationResult, cstring)] =
# [IGNORE] The voluntary exit is the first valid voluntary exit received for
# the validator with index signed_voluntary_exit.message.validator_index.
if signed_voluntary_exit.message.validator_index >=
@ -241,4 +241,4 @@ proc validateVoluntaryExit*(
pool.voluntary_exits.addExitMessage(
signed_voluntary_exit, VOLUNTARY_EXITS_BOUND)
ok(true)
ok()

View File

@ -38,7 +38,7 @@ const
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLen = minPasswordLen)
proc echoP(msg: string) =
proc echoP*(msg: string) =
## Prints a paragraph aligned to 80 columns
echo ""
echo wrapWords(msg, 80)
@ -213,8 +213,8 @@ proc keyboardGetPassword[T](prompt: string, attempts: int,
dec(remainingAttempts)
err("Failed to decrypt keystore")
proc loadKeystore(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivKey] =
proc loadKeystore*(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivKey] =
let
keystorePath = validatorsDir / keyName / keystoreFileName
keystore =

View File

@ -4,10 +4,10 @@ import
eth/common/eth_types as commonEthTypes,
web3/[ethtypes, conversions],
chronicles,
spec/presets,
spec/datatypes,
json_serialization,
json_serialization/std/[options, sets, net], serialization/errors
json_serialization/std/[options, sets, net], serialization/errors,
ssz/navigator,
spec/[presets, datatypes, digest]
# ATTENTION! This file will produce a large C file, because we are inlining
# genesis states as C literals in the generated code (and blobs in the final
@ -219,3 +219,7 @@ proc getRuntimePresetForNetwork*(eth2Network: Option[string]): RuntimePreset =
if eth2Network.isSome:
return getMetadataForNetwork(eth2Network.get).runtimePreset
return defaultRuntimePreset
proc extractGenesisValidatorRootFromSnapshop*(snapshot: string): Eth2Digest =
sszMount(snapshot, BeaconState).genesis_validators_root[]

View File

@ -7,12 +7,12 @@
import
# Standard library
std/[os, tables, strutils, strformat, sequtils, times, math, terminal, osproc,
random],
std/[os, tables, strutils, strformat, sequtils, times, math,
terminal, osproc, random],
# Nimble packages
stew/[objects, byteutils, endians2, io2], stew/shims/macros,
chronos, confutils, metrics, json_rpc/[rpcserver, jsonmarshal],
chronos, confutils, metrics, json_rpc/[rpcclient, rpcserver, jsonmarshal],
chronicles, bearssl, blscurve,
json_serialization/std/[options, sets, net], serialization/errors,
@ -24,13 +24,14 @@ import
./rpc/[beacon_api, config_api, debug_api, event_api, nimbus_api, node_api,
validator_api],
spec/[datatypes, digest, crypto, beaconstate, helpers, network, presets],
spec/[weak_subjectivity],
spec/[weak_subjectivity, signatures],
spec/eth2_apis/beacon_rpc_client,
conf, time, beacon_chain_db, validator_pool, extras,
attestation_pool, exit_pool, eth2_network, eth2_discovery,
beacon_node_common, beacon_node_types, beacon_node_status,
block_pools/[chain_dag, quarantine, clearance, block_pools_types],
nimbus_binary_common, network_metadata,
eth1_monitor, version, ssz/[navigator, merkleization],
eth1_monitor, version, ssz/merkleization,
sync_protocol, request_manager, keystore_management, interop, statusbar,
sync_manager, validator_duties, filepath,
validator_slashing_protection, ./eth2_processor
@ -220,7 +221,7 @@ proc init*(T: type BeaconNode,
if genesisStateContents != nil:
let
networkGenesisValidatorsRoot =
sszMount(genesisStateContents[], BeaconState).genesis_validators_root[]
extractGenesisValidatorRootFromSnapshop(genesisStateContents[])
if networkGenesisValidatorsRoot != databaseGenesisValidatorsRoot:
fatal "The specified --data-dir contains data for a different network",
@ -1025,6 +1026,143 @@ when hasPrompt:
# var t: Thread[ptr Prompt]
# createThread(t, processPromptCommands, addr p)
proc handleValidatorExitCommand(config: BeaconNodeConf) {.async.} =
let port = try:
let value = parseInt(config.rpcUrlForExit.port)
if value < Port.low.int or value > Port.high.int:
raise newException(ValueError,
"The port number must be between " & $Port.low & " and " & $Port.high)
Port value
except CatchableError as err:
fatal "Invalid port number", err = err.msg
quit 1
let rpcClient = newRpcHttpClient()
try:
await connect(rpcClient, config.rpcUrlForExit.hostname, port)
except CatchableError as err:
fatal "Failed to connect to the beacon node RPC service", err = err.msg
quit 1
let (validator, validatorIdx, status, balance) = try:
await rpcClient.get_v1_beacon_states_stateId_validators_validatorId(
"head", config.exitedValidator)
except CatchableError as err:
fatal "Failed to obtain information for validator", err = err.msg
quit 1
let exitAtEpoch = if config.exitAtEpoch.isSome:
Epoch config.exitAtEpoch.get
else:
let headSlot = try:
await rpcClient.getBeaconHead()
except CatchableError as err:
fatal "Failed to obtain the current head slot", err = err.msg
quit 1
headSlot.epoch
let
validatorsDir = config.validatorsDir
validatorKeyAsStr = "0x" & $validator.pubkey
keystoreDir = validatorsDir / validatorKeyAsStr
if not dirExists(keystoreDir):
echo "The validator keystores directory '" & config.validatorsDir.string &
"' does not contain a keystore for the selected validator with public " &
"key '" & validatorKeyAsStr & "'."
quit 1
let signingKey = loadKeystore(
validatorsDir,
config.secretsDir,
validatorKeyAsStr,
config.nonInteractive)
if signingKey.isNone:
fatal "Unable to continue without decrypted signing key"
quit 1
let fork = try:
await rpcClient.get_v1_beacon_states_fork("head")
except CatchableError as err:
fatal "Failed to obtain the fork id of the head state", err = err.msg
quit 1
let genesisValidatorsRoot = try:
(await rpcClient.get_v1_beacon_genesis()).genesis_validators_root
except CatchableError as err:
fatal "Failed to obtain the genesis validators root of the network",
err = err.msg
quit 1
var signedExit = SignedVoluntaryExit(
message: VoluntaryExit(
epoch: exitAtEpoch,
validator_index: validatorIdx))
signedExit.signature = get_voluntary_exit_signature(
fork, genesisValidatorsRoot, signedExit.message, signingKey.get)
template ask(prompt: string): string =
try:
stdout.write prompt, ": "
stdin.readLine()
except IOError as err:
fatal "Failed to read user input from stdin"
quit 1
try:
echoP "PLEASE BEWARE!"
echoP "Publishing a voluntary exit is an irreversible operation! " &
"You won't be able to restart again with the same validator."
echoP "By requesting an exit now, you'll be exempt from penalties " &
"stemming from not performing your validator duties, but you " &
"won't be able to withdraw your deposited funds at the time " &
"being. This means that your funds will be effectively frozen " &
"until withdrawals are enabled in a future phase of the Eth2 " &
"rollout."
echoP "To understand more about the Eth2 roadmap, we recommend you " &
"have a look at\n" &
"https://ethereum.org/en/eth2/#roadmap"
echoP "You must not shut down your validator for at least 5 epochs " &
"(32 minutes) after requesting a validator exit, as you will " &
"still be required to perform validator duties until your exit " &
"has been processed. The number of epochs could be significantly " &
"higher depending on how many other validators are queued to exit."
echoP "As such, we recommend you keep track of your validator's status " &
"using an Eth2 block explorer before shutting down your beacon node."
const
confirmation = "I understand the implications of submitting a voluntary exit"
while true:
echoP "To proceed to submitting your voluntary exit, please type '" &
confirmation & "' (without the quotes) in the prompt below and " &
"press ENTER or type 'q' to quit."
echo ""
let choice = ask "Your choice"
if choice == "q":
quit 0
elif choice == confirmation:
let success = await rpcClient.post_v1_beacon_pool_voluntary_exits(signedExit)
if success:
echo "Successfully published voluntary exit for validator " &
$validatorIdx & "(" & validatorKeyAsStr[0..9] & ")."
quit 0
else:
echo "The voluntary exit was not submitted successfully. Please try again."
quit 1
except CatchableError as err:
fatal "Failed to send the signed exit message to the beacon node RPC"
quit 1
programMain:
var
config = makeBannerAndConfig(clientId, BeaconNodeConf)
@ -1210,6 +1348,7 @@ programMain:
of deposits:
case config.depositsCmd
#[
of DepositsCmd.create:
var seed: KeySeed
defer: burnMem(seed)
@ -1287,6 +1426,11 @@ programMain:
fatal "Failed to create launchpad deposit data file", err = err.msg
quit 1
of DepositsCmd.status:
echo "The status command is not implemented yet"
quit 1
#]#
of DepositsCmd.`import`:
let validatorKeysDir = config.importedDepositsDir.get:
let cwd = os.getCurrentDir()
@ -1304,9 +1448,8 @@ programMain:
validatorKeysDir.string,
config.validatorsDir, config.secretsDir)
of DepositsCmd.status:
echo "The status command is not implemented yet"
quit 1
of DepositsCmd.exit:
waitFor handleValidatorExitCommand(config)
of wallets:
case config.walletsCmd:

View File

@ -11,7 +11,7 @@ import
chronicles,
../beacon_node_common, ../eth2_json_rpc_serialization, ../eth2_network,
../validator_duties,
../block_pools/chain_dag,
../block_pools/chain_dag, ../exit_pool,
../spec/[crypto, digest, datatypes, validator],
../spec/eth2_apis/callsigs_types,
../ssz/merkleization,
@ -26,8 +26,11 @@ template unimplemented() =
raise (ref CatchableError)(msg: "Unimplemented")
proc parsePubkey(str: string): ValidatorPubKey =
if str.len != RawPubKeySize + 2: # +2 because of the `0x` prefix
raise newException(CatchableError, "Not a valid public key (too short)")
const expectedLen = RawPubKeySize * 2 + 2
if str.len != expectedLen: # +2 because of the `0x` prefix
raise newException(ValueError,
"A hex public key should be exactly " & $expectedLen & " characters. " &
$str.len & " provided")
let pubkeyRes = fromHex(ValidatorPubKey, str)
if pubkeyRes.isErr:
raise newException(CatchableError, "Not a valid public key")
@ -46,19 +49,20 @@ proc getValidatorInfoFromValidatorId(
if status notin allowedStatuses:
raise newException(CatchableError, "Invalid status requested")
var validatorIdx: uint64
let validator = if validatorId.startsWith("0x"):
let pubkey = parsePubkey(validatorId)
let idx = state.validators.asSeq.findIt(it.pubKey == pubkey)
if idx == -1:
raise newException(CatchableError, "Could not find validator")
validatorIdx = idx.uint64
state.validators[idx]
else:
var valIdx: BiggestUInt
if parseBiggestUInt(validatorId, valIdx) != validatorId.len:
if parseBiggestUInt(validatorId, validatorIdx) != validatorId.len:
raise newException(CatchableError, "Not a valid index")
if valIdx > state.validators.lenu64:
if validatorIdx > state.validators.lenu64:
raise newException(CatchableError, "Index out of bounds")
state.validators[valIdx]
state.validators[validatorIdx]
# time to determine the status of the validator - the code mimics
# whatever is detailed here: https://hackmd.io/ofFJ5gOmQpu1jjHilHbdQQ
@ -102,8 +106,10 @@ proc getValidatorInfoFromValidatorId(
if status != "" and status notin actual_status:
return none(BeaconStatesValidatorsTuple)
return some((validator: validator, status: actual_status,
balance: validator.effective_balance))
return some((validator: validator,
index: validatorIdx,
status: actual_status,
balance: validator.effective_balance))
proc getBlockDataFromBlockId(node: BeaconNode, blockId: string): BlockData =
result = case blockId:
@ -254,5 +260,13 @@ proc installBeaconApiHandlers*(rpcServer: RpcServer, node: BeaconNode) =
rpcServer.rpc("get_v1_beacon_pool_voluntary_exits") do () -> JsonNode:
unimplemented()
rpcServer.rpc("post_v1_beacon_pool_voluntary_exits") do () -> JsonNode:
unimplemented()
rpcServer.rpc("post_v1_beacon_pool_voluntary_exits") do (
exit: SignedVoluntaryExit) -> bool:
doAssert node.exitPool != nil
let validity = node.exitPool[].validateVoluntaryExit(exit)
if validity.isOk:
node.sendVoluntaryExit(exit)
else:
raise newException(ValueError, $(validity.error[1]))
return true

View File

@ -54,8 +54,8 @@ proc get_v1_beacon_blocks_blockId_attestations(blockId: string): seq[Attestation
# TODO GET /v1/beacon/pool/attestations
proc post_v1_beacon_pool_attestations(attestation: Attestation): bool
proc post_v1_beacon_pool_voluntary_exits(exit: SignedVoluntaryExit): bool
proc get_v1_config_fork_schedule(): seq[Fork]

View File

@ -0,0 +1,9 @@
import
os, json,
json_rpc/[rpcclient, jsonmarshal],
../../eth2_json_rpc_serialization,
../digest, ../datatypes,
callsigs_types
createRpcSigs(RpcClient, currentSourcePath.parentDir / "beacon_callsigs.nim")

View File

@ -30,6 +30,7 @@ type
BeaconStatesValidatorsTuple* = tuple
validator: Validator
index: uint64
status: string
balance: uint64

View File

@ -178,10 +178,24 @@ proc verify_deposit_signature*(preset: RuntimePreset,
blsVerify(deposit.pubkey, signing_root.data, deposit.signature)
proc verify_voluntary_exit_signature*(
fork: Fork, genesis_validators_root: Eth2Digest,
func get_voluntary_exit_signature*(
fork: Fork,
genesis_validators_root: Eth2Digest,
voluntary_exit: VoluntaryExit,
pubkey: ValidatorPubKey, signature: SomeSig): bool =
privkey: ValidatorPrivKey): ValidatorSig =
let
domain = get_domain(
fork, DOMAIN_VOLUNTARY_EXIT, voluntary_exit.epoch, genesis_validators_root)
signing_root = compute_signing_root(voluntary_exit, domain)
blsSign(privKey, signing_root.data)
proc verify_voluntary_exit_signature*(
fork: Fork,
genesis_validators_root: Eth2Digest,
voluntary_exit: VoluntaryExit,
pubkey: ValidatorPubKey,
signature: SomeSig): bool =
withTrust(signature):
let
domain = get_domain(

View File

@ -157,6 +157,11 @@ proc sendAttestation*(
beacon_attestations_sent.inc()
proc sendVoluntaryExit*(node: BeaconNode, exit: SignedVoluntaryExit) =
node.network.broadcast(
getVoluntaryExitsTopic(node.forkDIgest),
exit)
proc sendAttestation*(node: BeaconNode, attestation: Attestation) =
# For the validator API, which doesn't supply num_active_validators.
let attestationBlck =