From 927180f36fc60acc4048fdcf052d1e2faa6f1872 Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Wed, 14 Jun 2023 09:46:01 +0300 Subject: [PATCH] VC+BN: Validator voluntary exits through the Keymanager API (#5020) * Initial commit. * Address review comments. --- beacon_chain/nimbus_beacon_node.nim | 10 ++- beacon_chain/nimbus_validator_client.nim | 14 +++- beacon_chain/rpc/rest_constants.nim | 4 ++ beacon_chain/rpc/rest_key_management_api.nim | 70 ++++++++++++++++--- .../validators/keystore_management.nim | 15 +++- 5 files changed, 101 insertions(+), 12 deletions(-) diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 60d724595..f9a881ae0 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -666,6 +666,12 @@ proc init*(T: type BeaconNode, withState(dag.headState): getValidator(forkyState().data.validators.asSeq(), pubkey) + proc getForkForEpoch(epoch: Epoch): Opt[Fork] = + Opt.some(dag.forkAtEpoch(epoch)) + + proc getGenesisRoot(): Eth2Digest = + getStateField(dag.headState, genesis_validators_root) + let slashingProtectionDB = SlashingProtectionDB.init( @@ -685,7 +691,9 @@ proc init*(T: type BeaconNode, config.defaultFeeRecipient, config.suggestedGasLimit, getValidatorAndIdx, - getBeaconTime) + getBeaconTime, + getForkForEpoch, + getGenesisRoot) else: nil stateTtlCache = diff --git a/beacon_chain/nimbus_validator_client.nim b/beacon_chain/nimbus_validator_client.nim index c10e2a0ad..ca431817c 100644 --- a/beacon_chain/nimbus_validator_client.nim +++ b/beacon_chain/nimbus_validator_client.nim @@ -310,6 +310,15 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} = let keymanagerInitResult = initKeymanagerServer(vc.config, nil) + proc getForkForEpoch(epoch: Epoch): Opt[Fork] = + if len(vc.forks) > 0: + Opt.some(vc.forkAtEpoch(epoch)) + else: + Opt.none(Fork) + + proc getGenesisRoot(): Eth2Digest = + vc.beaconGenesis.genesis_validators_root + try: vc.fallbackService = await FallbackServiceRef.init(vc) vc.forkService = await ForkServiceRef.init(vc) @@ -329,7 +338,10 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} = vc.config.defaultFeeRecipient, vc.config.suggestedGasLimit, nil, - vc.beaconClock.getBeaconTimeFn) + vc.beaconClock.getBeaconTimeFn, + getForkForEpoch, + getGenesisRoot + ) except CatchableError as exc: warn "Unexpected error encountered while initializing", diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index ca3e8951d..d196492dc 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -115,6 +115,8 @@ const "Invalid subscription request object(s)" ValidatorNotFoundError* = "Could not find validator" + ValidatorIndexMissingError* = + "Validator missing index value" ValidatorStatusNotFoundError* = "Could not obtain validator's status" TooHighValidatorIndexValueError* = @@ -234,3 +236,5 @@ const "The given merkle proof is invalid" InvalidMerkleProofIndexError* = "The given merkle proof index is invalid" + FailedToObtainForkError* = + "Failed to obtain fork information" diff --git a/beacon_chain/rpc/rest_key_management_api.nim b/beacon_chain/rpc/rest_key_management_api.nim index 8186eb919..8e9335639 100644 --- a/beacon_chain/rpc/rest_key_management_api.nim +++ b/beacon_chain/rpc/rest_key_management_api.nim @@ -23,8 +23,8 @@ func validateKeymanagerApiQueries*(key: string, value: string): int = # There are no queries to validate return 0 -proc listLocalValidators*(validatorPool: ValidatorPool): seq[KeystoreInfo] - {.raises: [Defect].} = +proc listLocalValidators*(validatorPool: ValidatorPool): seq[KeystoreInfo] {. + raises: [Defect].} = var validators: seq[KeystoreInfo] for item in validatorPool: if item.kind == ValidatorKind.Local: @@ -35,8 +35,9 @@ proc listLocalValidators*(validatorPool: ValidatorPool): seq[KeystoreInfo] ) validators -proc listRemoteValidators*(validatorPool: ValidatorPool): seq[RemoteKeystoreInfo] - {.raises: [Defect].} = +proc listRemoteValidators*( + validatorPool: ValidatorPool): seq[RemoteKeystoreInfo] {. + raises: [Defect].} = var validators: seq[RemoteKeystoreInfo] for item in validatorPool: if item.kind == ValidatorKind.Remote and item.data.remotes.len == 1: @@ -46,8 +47,9 @@ proc listRemoteValidators*(validatorPool: ValidatorPool): seq[RemoteKeystoreInfo ) validators -proc listRemoteDistributedValidators*(validatorPool: ValidatorPool): seq[DistributedKeystoreInfo] - {.raises: [Defect].} = +proc listRemoteDistributedValidators*( + validatorPool: ValidatorPool): seq[DistributedKeystoreInfo] {. + raises: [Defect].} = var validators: seq[DistributedKeystoreInfo] for item in validatorPool: if item.kind == ValidatorKind.Remote and item.data.remotes.len > 1: @@ -75,8 +77,9 @@ proc keymanagerApiError(status: HttpCode, msg: string): RestApiResponse = default RestApiResponse.error(status, data, "application/json") -proc checkAuthorization*(request: HttpRequestRef, - host: KeymanagerHost): Result[void, AuthorizationError] = +proc checkAuthorization*( + request: HttpRequestRef, + host: KeymanagerHost): Result[void, AuthorizationError] = let authorizations = request.headers.getList("authorization") if authorizations.len > 0: for authHeader in authorizations: @@ -522,3 +525,54 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) = response.data.add handleRemoveValidatorReq(host, key) return RestApiResponse.jsonResponsePlain(response) + + # https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Voluntary%20Exit/signVoluntaryExit + router.api(MethodPost, "/eth/v1/validator/{pubkey}/voluntary_exit") do ( + pubkey: ValidatorPubKey, epoch: Option[Epoch], + contentBody: Option[ContentBody]) -> RestApiResponse: + + let authStatus = checkAuthorization(request, host) + if authStatus.isErr(): + return authErrorResponse(authStatus.error) + + let + qpubkey = pubkey.valueOr: + return keymanagerApiError(Http400, InvalidValidatorPublicKey) + qepoch = + if epoch.isSome(): + let res = epoch.get() + if res.isErr(): + return keymanagerApiError(Http400, InvalidEpochValueError) + res.get() + else: + host.getBeaconTimeFn().slotOrZero().epoch() + validator = + block: + let res = host.validatorPool[].getValidator(qpubkey).valueOr: + return keymanagerApiError(Http404, ValidatorNotFoundError) + if res.index.isNone(): + return keymanagerApiError(Http404, ValidatorIndexMissingError) + res + voluntaryExit = + VoluntaryExit(epoch: qepoch, + validator_index: uint64(validator.index.get())) + fork = host.getForkFn(qepoch).valueOr: + return keymanagerApiError(Http500, FailedToObtainForkError) + signature = + try: + let res = await validator.getValidatorExitSignature( + fork, host.getGenesisFn(), voluntaryExit) + if res.isErr(): + return keymanagerApiError(Http500, res.error()) + res.get() + except CancelledError as exc: + raise exc + except CatchableError as exc: + error "An unexpected error occurred while signing validator exit", + err_name = exc.name, err_msg = exc.msg + return keymanagerApiError(Http500, $exc.msg) + response = SignedVoluntaryExit( + message: voluntaryExit, + signature: signature + ) + return RestApiResponse.jsonResponse(response) diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index 6575c52c4..6abfffdfd 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -69,6 +69,11 @@ type proc (pubkey: ValidatorPubKey): Opt[ValidatorAndIndex] {.raises: [Defect], gcsafe.} + GetForkFn* = + proc (epoch: Epoch): Opt[Fork] {.raises: [Defect], gcsafe.} + GetGenesisFn* = + proc (): Eth2Digest {.raises: [Defect], gcsafe.} + KeymanagerHost* = object validatorPool*: ref ValidatorPool rng*: ref HmacDrbgContext @@ -79,6 +84,8 @@ type defaultGasLimit*: uint64 getValidatorAndIdxFn*: ValidatorPubKeyToDataFn getBeaconTimeFn*: GetBeaconTimeFn + getForkFn*: GetForkFn + getGenesisFn*: GetGenesisFn MultipleKeystoresDecryptor* = object previouslyUsedPassword*: string @@ -104,7 +111,9 @@ func init*(T: type KeymanagerHost, defaultFeeRecipient: Opt[Eth1Address], defaultGasLimit: uint64, getValidatorAndIdxFn: ValidatorPubKeyToDataFn, - getBeaconTimeFn: GetBeaconTimeFn): T = + getBeaconTimeFn: GetBeaconTimeFn, + getForkFn: GetForkFn, + getGenesisFn: GetGenesisFn): T = T(validatorPool: validatorPool, rng: rng, keymanagerToken: keymanagerToken, @@ -113,7 +122,9 @@ func init*(T: type KeymanagerHost, defaultFeeRecipient: defaultFeeRecipient, defaultGasLimit: defaultGasLimit, getValidatorAndIdxFn: getValidatorAndIdxFn, - getBeaconTimeFn: getBeaconTimeFn) + getBeaconTimeFn: getBeaconTimeFn, + getForkFn: getForkFn, + getGenesisFn: getGenesisFn) proc echoP*(msg: string) = ## Prints a paragraph aligned to 80 columns