From 806536a040cc8faded9f5333787f21ecbbbd15c2 Mon Sep 17 00:00:00 2001 From: zah Date: Wed, 13 Jul 2022 17:45:04 +0300 Subject: [PATCH] [Keymanager API] Support for the feerecipient end-points (#3864) Other changes: * The Keymanager error responses differ from the Beacon API responses. 'keymanagerApiError' replaces the former usages of 'jsonError'. * Return status code 401 and 403 for authorization errors in accordance to the spec. * Eliminate inconsistencies in the REST JSON parsing. Some of the code paths allowed missing fields. * Added logging of serialization failure details at DEBUG level. --- AllTests-mainnet.md | 13 +- beacon_chain/rpc/rest_beacon_api.nim | 1 - beacon_chain/rpc/rest_constants.nim | 4 +- beacon_chain/rpc/rest_key_management_api.nim | 169 +++++++++--- beacon_chain/rpc/rest_utils.nim | 2 + .../eth2_apis/eth2_rest_serialization.nim | 98 ++++--- .../spec/eth2_apis/rest_keymanager_calls.nim | 91 ++++++- .../spec/eth2_apis/rest_keymanager_types.nim | 10 + .../validators/keystore_management.nim | 84 +++++- beacon_chain/validators/validator_duties.nim | 42 +-- tests/test_keymanager_api.nim | 256 ++++++++++++++---- 11 files changed, 575 insertions(+), 195 deletions(-) diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index daec79829..ca4c4ba48 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -168,6 +168,17 @@ OK: 3/3 Fail: 0/3 Skip: 0/3 + addExitMessage/getVoluntaryExitMessage OK ``` OK: 3/3 Fail: 0/3 Skip: 0/3 +## Fee recipient management [Preset: mainnet] +```diff ++ Configuring the fee recpient [Preset: mainnet] OK ++ Invalid Authorization Header [Preset: mainnet] OK ++ Invalid Authorization Token [Preset: mainnet] OK ++ Missing Authorization header [Preset: mainnet] OK ++ Obtaining the fee recpient of a missing validator returns 404 [Preset: mainnet] OK ++ Obtaining the fee recpient of an unconfigured validator returns the suggested default [Pre OK ++ Setting the fee recipient on a missing validator creates a record for it [Preset: mainnet] OK +``` +OK: 7/7 Fail: 0/7 Skip: 0/7 ## FinalizedBlocks [Preset: mainnet] ```diff + Basic ops [Preset: mainnet] OK @@ -569,4 +580,4 @@ OK: 1/1 Fail: 0/1 Skip: 0/1 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 314/319 Fail: 0/319 Skip: 5/319 +OK: 321/326 Fail: 0/326 Skip: 5/326 diff --git a/beacon_chain/rpc/rest_beacon_api.nim b/beacon_chain/rpc/rest_beacon_api.nim index 58053d9fa..6d7f7b1d2 100644 --- a/beacon_chain/rpc/rest_beacon_api.nim +++ b/beacon_chain/rpc/rest_beacon_api.nim @@ -345,7 +345,6 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) = let vindex = block: - let vid = validator_id.get() case vid.kind of ValidatorQueryKind.Key: let optIndices = keysToIndices(node.restKeysCache, state, [vid.key]) diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index 89d878a91..340de2d36 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -50,6 +50,8 @@ const "Proposer slashing object was broadcasted" InvalidVoluntaryExitObjectError* = "Unable to decode voluntary exit object(s)" + InvalidFeeRecipientRequestError* = + "Bad request. Request was malformed and could not be processed" VoluntaryExitValidationError* = "Invalid voluntary exit, it will never pass validation so it's rejected" VoluntaryExitValidationSuccess* = @@ -195,7 +197,7 @@ const "Invalid validator's public key(s) found" BadRequestFormatError* = "Bad request format" - InvalidAuthorization* = + InvalidAuthorizationError* = "Invalid Authorization Header" PrunedStateError* = "Trying to access a pruned historical state" diff --git a/beacon_chain/rpc/rest_key_management_api.nim b/beacon_chain/rpc/rest_key_management_api.nim index 3ab9c4fe7..7f679a477 100644 --- a/beacon_chain/rpc/rest_key_management_api.nim +++ b/beacon_chain/rpc/rest_key_management_api.nim @@ -50,6 +50,24 @@ proc listRemoteDistributedValidators*(node: BeaconNode): seq[DistributedKeystore ) validators +proc keymanagerApiError(status: HttpCode, msg: string): RestApiResponse = + let data = + block: + var default: string + try: + var defstrings: seq[string] + var stream = memoryOutput() + var writer = JsonWriter[RestJson].init(stream) + writer.beginRecord() + writer.writeField("message", msg) + writer.endRecord() + stream.getOutput(string) + except SerializationError: + default + except IOError: + default + RestApiResponse.error(status, data, "application/json") + proc checkAuthorization*(request: HttpRequestRef, node: BeaconNode): Result[void, AuthorizationError] = let authorizations = request.headers.getList("authorization") @@ -65,6 +83,15 @@ proc checkAuthorization*(request: HttpRequestRef, else: return err noAuthorizationHeader +proc authErrorResponse(error: AuthorizationError): RestApiResponse = + let status = case error: + of missingBearerScheme, noAuthorizationHeader: + Http401 + of incorrectToken: + Http403 + + keymanagerApiError(status, InvalidAuthorizationError) + proc validateUri*(url: string): Result[Uri, cstring] = let surl = parseUri(url) if surl.scheme notin ["http", "https"]: @@ -106,8 +133,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = router.api(MethodGet, "/api/eth/v1/keystores") do () -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let response = GetKeystoresResponse(data: listLocalValidators(node)) return RestApiResponse.jsonResponsePlain(response) @@ -116,16 +142,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let request = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(KeystoresAndSlashingProtection, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, - $dres.error()) + return keymanagerApiError(Http400, InvalidKeystoreObjects) dres.get() if request.slashing_protection.isSome(): @@ -133,13 +157,13 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection) if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest != slashing_protection.metadata.genesis_validators_root.Eth2Digest: - return RestApiResponse.jsonError(Http400, + return keymanagerApiError(Http400, "The slashing protection database and imported file refer to " & "different blockchains.") let res = inclSPDIR(node.attachedValidators.slashingProtection, slashing_protection) if res == siFailure: - return RestApiResponse.jsonError(Http500, + return keymanagerApiError(Http500, "Internal server error; Failed to import slashing protection data") var response: PostKeystoresResponse @@ -169,16 +193,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let keys = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, - $dres.error()) + return keymanagerApiError(Http400, InvalidValidatorPublicKey) dres.get().pubkeys var @@ -230,8 +252,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = router.api(MethodGet, "/api/eth/v1/remotekeys") do () -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let response = GetRemoteKeystoresResponse(data: listRemoteValidators(node)) return RestApiResponse.jsonResponsePlain(response) @@ -240,16 +261,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let keys = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(ImportRemoteKeystoresBody, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, - $dres.error()) + return keymanagerApiError(Http400, InvalidKeystoreObjects) dres.get().remote_keys var response: PostKeystoresResponse @@ -274,16 +293,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let keys = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, - $dres.error()) + return keymanagerApiError(Http400, InvalidValidatorPublicKey) dres.get().pubkeys var response: DeleteRemoteKeystoresResponse @@ -292,13 +309,78 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = response.data.add(status) return RestApiResponse.jsonResponsePlain(response) + # https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/ListFeeRecipient + router.api(MethodGet, "/api/eth/v1/validator/{pubkey}/feerecipient") do ( + pubkey: ValidatorPubKey) -> RestApiResponse: + let authStatus = checkAuthorization(request, node) + if authStatus.isErr(): + return authErrorResponse authStatus.error + let + pubkey = pubkey.valueOr: + return keymanagerApiError(Http400, InvalidValidatorPublicKey) + ethaddress = node.config.getSuggestedFeeRecipient(pubkey) + + return if ethaddress.isOk: + RestApiResponse.jsonResponse(ListFeeRecipientResponse( + pubkey: pubkey, + ethaddress: ethaddress.get)) + else: + case ethaddress.error + of noSuchValidator: + keymanagerApiError(Http404, "No matching validator found") + of invalidFeeRecipientFile: + keymanagerApiError(Http500, "Error reading fee recipient file") + + # https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/SetFeeRecipient + router.api(MethodPost, "/api/eth/v1/validator/{pubkey}/feerecipient") do ( + pubkey: ValidatorPubKey, + contentBody: Option[ContentBody]) -> RestApiResponse: + let authStatus = checkAuthorization(request, node) + if authStatus.isErr(): + return authErrorResponse authStatus.error + let + pubkey= pubkey.valueOr: + return keymanagerApiError(Http400, InvalidValidatorPublicKey) + feeRecipientReq = + block: + if contentBody.isNone(): + return keymanagerApiError(Http400, InvalidFeeRecipientRequestError) + let dres = decodeBody(SetFeeRecipientRequest, contentBody.get()) + if dres.isErr(): + return keymanagerApiError(Http400, InvalidFeeRecipientRequestError) + dres.get() + + status = node.config.setFeeRecipient(pubkey, feeRecipientReq.ethaddress) + + return if status.isOk: + RestApiResponse.response("", Http202, "text/plain") + else: + keymanagerApiError( + Http500, "Failed to set fee recipient: " & status.error) + + # https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/DeleteFeeRecipient + router.api(MethodDelete, "/api/eth/v1/validator/{pubkey}/feerecipient") do ( + pubkey: ValidatorPubKey) -> RestApiResponse: + let authStatus = checkAuthorization(request, node) + if authStatus.isErr(): + return authErrorResponse authStatus.error + let + pubkey = pubkey.valueOr: + return keymanagerApiError(Http400, InvalidValidatorPublicKey) + res = removeFeeRecipientFile(node.config, pubkey) + + return if res.isOk: + RestApiResponse.response("", Http204, "text/plain") + else: + keymanagerApiError( + Http500, "Failed to remove fee recipient file: " & res.error) + # TODO: These URLs will be changed once we submit a proposal for # /api/eth/v2/remotekeys that supports distributed keys. router.api(MethodGet, "/api/eth/v1/remotekeys/distributed") do () -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let response = GetDistributedKeystoresResponse(data: listRemoteDistributedValidators(node)) return RestApiResponse.jsonResponsePlain(response) @@ -308,16 +390,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let keys = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(ImportDistributedKeystoresBody, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, - $dres.error()) + return keymanagerApiError(Http400, InvalidKeystoreObjects) dres.get.remote_keys var response: PostKeystoresResponse @@ -339,16 +419,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = contentBody: Option[ContentBody]) -> RestApiResponse: let authStatus = checkAuthorization(request, node) if authStatus.isErr(): - return RestApiResponse.jsonError(Http401, InvalidAuthorization, - $authStatus.error()) + return authErrorResponse authStatus.error let keys = block: if contentBody.isNone(): - return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) + return keymanagerApiError(Http404, EmptyRequestBodyError) let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) if dres.isErr(): - return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, - $dres.error()) + return keymanagerApiError(Http400, InvalidValidatorPublicKey) dres.get.pubkeys var response: DeleteRemoteKeystoresResponse @@ -388,6 +466,21 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = "/eth/v1/remotekeys", "/api/eth/v1/remotekeys") + router.redirect( + MethodGet, + "/eth/v1/validator/{pubkey}/feerecipient", + "/api/eth/v1/validator/{pubkey}/feerecipient") + + router.redirect( + MethodPost, + "/eth/v1/validator/{pubkey}/feerecipient", + "/api/eth/v1/validator/{pubkey}/feerecipient") + + router.redirect( + MethodDelete, + "/eth/v1/validator/{pubkey}/feerecipient", + "/api/eth/v1/validator/{pubkey}/feerecipient") + router.redirect( MethodGet, "/eth/v1/remotekeys/distributed", diff --git a/beacon_chain/rpc/rest_utils.nim b/beacon_chain/rpc/rest_utils.nim index 2eaec039f..f626f3e2d 100644 --- a/beacon_chain/rpc/rest_utils.nim +++ b/beacon_chain/rpc/rest_utils.nim @@ -48,6 +48,8 @@ proc validate(key: string, value: string): int = 0 of "{block_root}": 0 + of "{pubkey}": + int(value.len != 98) else: 1 diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 60068afa7..957a48b78 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -69,6 +69,8 @@ const [byte('a'), byte('l'), byte('t'), byte('a'), byte('i'), byte('r')] type + EmptyBody* = object + RestGenericError* = object code*: uint64 message*: string @@ -81,29 +83,31 @@ type EncodeTypes* = AttesterSlashing | + DeleteKeystoresBody | + EmptyBody | + ImportDistributedKeystoresBody | + ImportRemoteKeystoresBody | + KeystoresAndSlashingProtection | ProposerSlashing | - phase0.SignedBeaconBlock | - altair.SignedBeaconBlock | - bellatrix.SignedBeaconBlock | + SetFeeRecipientRequest | SignedBlindedBeaconBlock | SignedValidatorRegistrationV1 | SignedVoluntaryExit | Web3SignerRequest | - KeystoresAndSlashingProtection | - DeleteKeystoresBody | - ImportRemoteKeystoresBody | - ImportDistributedKeystoresBody + altair.SignedBeaconBlock | + bellatrix.SignedBeaconBlock | + phase0.SignedBeaconBlock EncodeArrays* = - seq[ValidatorIndex] | seq[Attestation] | + seq[RemoteKeystoreInfo] | + seq[RestCommitteeSubscription] | + seq[RestSignedContributionAndProof] | + seq[RestSyncCommitteeMessage] | + seq[RestSyncCommitteeSubscription] | seq[SignedAggregateAndProof] | seq[SignedValidatorRegistrationV1] | - seq[RestCommitteeSubscription] | - seq[RestSyncCommitteeSubscription] | - seq[RestSyncCommitteeMessage] | - seq[RestSignedContributionAndProof] | - seq[RemoteKeystoreInfo] + seq[ValidatorIndex] DecodeTypes* = DataEnclosedObject | @@ -111,20 +115,22 @@ type DataRootEnclosedObject | DataVersionEnclosedObject | GetBlockV2Response | + GetDistributedKeystoresResponse | GetKeystoresResponse | GetRemoteKeystoresResponse | - GetDistributedKeystoresResponse | - GetStateV2Response | GetStateForkResponse | + GetStateV2Response | + KeymanagerGenericError | + KeystoresAndSlashingProtection | + ListFeeRecipientResponse | ProduceBlockResponseV2 | RestDutyError | - RestValidator | RestGenericError | + RestValidator | Web3SignerErrorResponse | Web3SignerKeysResponse | Web3SignerSignatureResponse | - Web3SignerStatusResponse | - KeystoresAndSlashingProtection + Web3SignerStatusResponse SszDecodeTypes* = GetPhase0StateSszResponse | @@ -470,11 +476,10 @@ template hexOriginal(data: openArray[byte]): string = to0xHex(data) proc decodeJsonString*[T](t: typedesc[T], - data: JsonString, - requireAllFields = true): Result[T, cstring] = + data: JsonString): Result[T, cstring] = try: ok(RestJson.decode(string(data), T, - requireAllFields = requireAllFields, + requireAllFields = true, allowUnknownFields = true)) except SerializationError: err("Unable to deserialize data") @@ -1558,8 +1563,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(Web3SignerAggregationSlotData, - data.get(), true) + let res = decodeJsonString(Web3SignerAggregationSlotData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `aggregation_slot` format") @@ -1574,7 +1578,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(AggregateAndProof, data.get(), true) + let res = decodeJsonString(AggregateAndProof, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `aggregate_and_proof` format") @@ -1590,7 +1594,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(AttestationData, data.get(), true) + let res = decodeJsonString(AttestationData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `attestation` format") @@ -1606,7 +1610,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(phase0.BeaconBlock, data.get(), true) + let res = decodeJsonString(phase0.BeaconBlock, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `block` format") @@ -1622,7 +1626,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(Web3SignerForkedBeaconBlock, data.get(), true) + let res = decodeJsonString(Web3SignerForkedBeaconBlock, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `beacon_block` format") @@ -1636,7 +1640,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `deposit` is missing") let data = block: - let res = decodeJsonString(Web3SignerDepositData, data.get(), true) + let res = decodeJsonString(Web3SignerDepositData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `deposit` format") @@ -1652,8 +1656,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(Web3SignerRandaoRevealData, data.get(), - true) + let res = decodeJsonString(Web3SignerRandaoRevealData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `randao_reveal` format") @@ -1669,7 +1672,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(VoluntaryExit, data.get(), true) + let res = decodeJsonString(VoluntaryExit, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `voluntary_exit` format") @@ -1686,8 +1689,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(Web3SignerSyncCommitteeMessageData, - data.get(), true) + let res = decodeJsonString(Web3SignerSyncCommitteeMessageData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `sync_committee_message` format") @@ -1705,8 +1707,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(SyncAggregatorSelectionData, - data.get(), true) + let res = decodeJsonString(SyncAggregatorSelectionData, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `sync_aggregator_selection_data` format") @@ -1724,8 +1725,7 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue("Field `fork_info` is missing") let data = block: - let res = decodeJsonString(ContributionAndProof, - data.get(), true) + let res = decodeJsonString(ContributionAndProof, data.get()) if res.isErr(): reader.raiseUnexpectedValue( "Incorrect field `contribution_and_proof` format") @@ -2068,7 +2068,10 @@ proc readValue*(reader: var JsonReader[RestJson], for item in strKeystores: let key = try: - RestJson.decode(item, Keystore, allowUnknownFields = true) + RestJson.decode(item, + Keystore, + requireAllFields = true, + allowUnknownFields = true) except SerializationError as exc: # TODO re-raise the exception by adjusting the column index, so the user # will get an accurate syntax error within the larger message @@ -2080,7 +2083,10 @@ proc readValue*(reader: var JsonReader[RestJson], if strSlashing.isSome(): let db = try: - RestJson.decode(strSlashing.get(), SPDIR, allowUnknownFields = true) + RestJson.decode(strSlashing.get(), + SPDIR, + requireAllFields = true, + allowUnknownFields = true) except SerializationError as exc: reader.raiseUnexpectedValue("Invalid slashing protection format") some(db) @@ -2172,8 +2178,12 @@ proc decodeBody*[T](t: typedesc[T], return err("Unsupported content type") let data = try: - RestJson.decode(body.data, T, allowUnknownFields = true) + RestJson.decode(body.data, T, + requireAllFields = true, + allowUnknownFields = true) except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg("") return err("Unable to deserialize data") except CatchableError: return err("Unexpected deserialization error") @@ -2222,8 +2232,12 @@ proc decodeBytes*[T: DecodeTypes](t: typedesc[T], value: openArray[byte], case contentType of "application/json": try: - ok RestJson.decode(value, T, allowUnknownFields = true) - except SerializationError: + ok RestJson.decode(value, T, + requireAllFields = true, + allowUnknownFields = true) + except SerializationError as exc: + debug "Failed to deserialize REST JSON data", + err = exc.formatMsg("") err("Serialization error") else: err("Content-Type not supported") diff --git a/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim index 41da21940..907f7e5eb 100644 --- a/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim @@ -20,6 +20,19 @@ UUID.serializesAsBaseIn RestJson KeyPath.serializesAsBaseIn RestJson WalletName.serializesAsBaseIn RestJson +proc raiseKeymanagerGenericError*(resp: RestPlainResponse) {. + noreturn, raises: [RestError, Defect].} = + let error = + block: + let res = decodeBytes(KeymanagerGenericError, resp.data, resp.contentType) + if res.isErr(): + let msg = "Incorrect response error format (" & $resp.status & + ") [" & $res.error() & "]" + raise newException(RestError, msg) + res.get() + let msg = "Error response (" & $resp.status & ") [" & error.message & "]" + raise newException(RestError, msg) + proc listKeysPlain*(): RestPlainResponse {. rest, endpoint: "/eth/v1/keystores", meth: MethodGet.} @@ -49,7 +62,7 @@ proc listKeys*(client: RestClientRef, raise newException(RestError, $keystoresRes.error) return keystoresRes.get() of 401, 403, 500: - raiseGenericError(resp) + raiseKeymanagerGenericError(resp) else: raiseUnknownStatusError(resp) @@ -69,6 +82,23 @@ proc deleteRemoteKeysPlain*(body: DeleteKeystoresBody): RestPlainResponse {. meth: MethodDelete.} ## https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/DeleteRemoteKeys +proc listFeeRecipientPlain*(pubkey: ValidatorPubKey): RestPlainResponse {. + rest, endpoint: "/eth/v1/validator/{pubkey}/feerecipient", + meth: MethodGet.} + ## https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/ListFeeRecipient + +proc setFeeRecipientPlain*(pubkey: ValidatorPubKey, + body: SetFeeRecipientRequest): RestPlainResponse {. + rest, endpoint: "/eth/v1/validator/{pubkey}/feerecipient", + meth: MethodPost.} + ## https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/SetFeeRecipient + +proc deleteFeeRecipientPlain*(pubkey: ValidatorPubKey, + body: EmptyBody): RestPlainResponse {. + rest, endpoint: "/eth/v1/validator/{pubkey}/feerecipient", + meth: MethodDelete.} + ## https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/DeleteFeeRecipient + proc listRemoteDistributedKeysPlain*(): RestPlainResponse {. rest, endpoint: "/eth/v1/remotekeys/distributed", meth: MethodGet.} @@ -82,7 +112,6 @@ proc deleteRemoteDistributedKeysPlain*(body: DeleteKeystoresBody): RestPlainResp rest, endpoint: "/eth/v1/remotekeys/distributed", meth: MethodDelete.} - proc listRemoteKeys*(client: RestClientRef, token: string): Future[GetRemoteKeystoresResponse] {. async.} = @@ -91,12 +120,66 @@ proc listRemoteKeys*(client: RestClientRef, case resp.status: of 200: - let res = decodeBytes(GetRemoteKeystoresResponse, resp.data, + let res = decodeBytes(GetRemoteKeystoresResponse, + resp.data, resp.contentType) if res.isErr(): raise newException(RestError, $res.error()) return res.get() of 401, 403, 500: - raiseGenericError(resp) + raiseKeymanagerGenericError(resp) + else: + raiseUnknownStatusError(resp) + +proc listFeeRecipient*(client: RestClientRef, + pubkey: ValidatorPubKey, + token: string): Future[Eth1Address] {.async.} = + let resp = await client.listFeeRecipientPlain( + pubkey, + extraHeaders = @[("Authorization", "Bearer " & token)]) + + case resp.status: + of 200: + let res = decodeBytes(DataEnclosedObject[ListFeeRecipientResponse], + resp.data, + resp.contentType) + if res.isErr: + raise newException(RestError, $res.error) + return res.get.data.ethaddress + of 401, 403, 404, 500: + raiseKeymanagerGenericError(resp) + else: + raiseUnknownStatusError(resp) + +proc setFeeRecipient*(client: RestClientRef, + pubkey: ValidatorPubKey, + feeRecipient: Eth1Address, + token: string) {.async.} = + let resp = await client.setFeeRecipientPlain( + pubkey, + SetFeeRecipientRequest(ethaddress: feeRecipient), + extraHeaders = @[("Authorization", "Bearer " & token)]) + + case resp.status: + of 202: + discard + of 400, 401, 403, 404, 500: + raiseKeymanagerGenericError(resp) + else: + raiseUnknownStatusError(resp) + +proc deleteFeeRecipient*(client: RestClientRef, + pubkey: ValidatorPubKey, + token: string) {.async.} = + let resp = await client.deleteFeeRecipientPlain( + pubkey, + EmptyBody(), + extraHeaders = @[("Authorization", "Bearer " & token)]) + + case resp.status: + of 204: + discard + of 401, 403, 404, 500: + raiseKeymanagerGenericError(resp) else: raiseUnknownStatusError(resp) diff --git a/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim b/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim index 23bfa8853..e56562c72 100644 --- a/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim @@ -65,6 +65,13 @@ type DeleteRemoteKeystoresResponse* = object data*: seq[RemoteKeystoreStatus] + SetFeeRecipientRequest* = object + ethaddress*: Eth1Address + + ListFeeRecipientResponse* = object + pubkey*: ValidatorPubKey + ethaddress*: Eth1Address + KeystoreStatus* = enum error = "error" notActive = "not_active" @@ -78,6 +85,9 @@ type missingBearerScheme = "Bearer Authentication is not included in request" incorrectToken = "Authentication token is incorrect" + KeymanagerGenericError* = object + message*: string + proc `<`*(x, y: KeystoreInfo | RemoteKeystoreInfo): bool = for a, b in fields(x, y): var c = cmp(a, b) diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index 3787e1c65..04b6b5bf0 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -33,9 +33,7 @@ const KeystoreFileName* = "keystore.json" RemoteKeystoreFileName* = "remote_keystore.json" NetKeystoreFileName* = "network_keystore.json" - DisableFileName* = ".disable" - DisableFileContent* = "Please do not remove this file manually. " & - "This can lead to slashing of this validator's key." + FeeRecipientFilename* = "suggested_fee_recipient.hex" KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters. type @@ -520,6 +518,9 @@ proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string, ok(RemoveValidatorStatus.deleted) +func fsName(pubkey: ValidatorPubKey|CookedPubKey): string = + "0x" & pubkey.toHex() + proc removeValidatorFiles*(conf: AnyConf, keyName: string, kind: KeystoreKind): KmResult[RemoveValidatorStatus] {.raises: [Defect].} = @@ -534,8 +535,7 @@ proc removeValidator*(pool: var ValidatorPool, conf: AnyConf, return ok(RemoveValidatorStatus.notFound) if validator.kind.toKeystoreKind() != kind: return ok(RemoveValidatorStatus.notFound) - let publicKeyName: string = "0x" & publicKey.toHex() - let res = removeValidatorFiles(conf, publicKeyName, kind) + let res = removeValidatorFiles(conf, publicKey.fsName, kind) if res.isErr(): return err(res.error()) pool.removeValidator(publicKey) @@ -793,7 +793,7 @@ proc saveKeystore*(rng: var HmacDrbgContext, mode = Secure): Result[void, KeystoreGenerationError] = let keypass = KeystorePass.init(password) - keyName = "0x" & signingPubKey.toHex() + keyName = signingPubKey.fsName keystoreDir = validatorsDir / keyName keystoreFile = keystoreDir / KeystoreFileName @@ -832,7 +832,7 @@ proc saveKeystore*(validatorsDir: string, desc = ""): Result[void, KeystoreGenerationError] {.raises: [Defect].} = let - keyName = "0x" & publicKey.toHex() + keyName = publicKey.fsName keystoreDir = validatorsDir / keyName keystoreFile = keystoreDir / RemoteKeystoreFileName keystoreDesc = if len(desc) == 0: none[string]() else: some(desc) @@ -888,7 +888,7 @@ proc importKeystore*(pool: var ValidatorPool, conf: AnyConf, {.raises: [Defect].} = let publicKey = keystore.pubkey - keyName = "0x" & publicKey.toHex() + keyName = publicKey.fsName validatorsDir = conf.validatorsDir() keystoreDir = validatorsDir / keyName keystoreFile = keystoreDir / RemoteKeystoreFileName @@ -933,7 +933,7 @@ proc importKeystore*(pool: var ValidatorPool, AddValidatorFailure.init(AddValidatorStatus.failed, res.error())) let publicKey = privateKey.toPubKey() - keyName = "0x" & publicKey.toHex() + keyName = publicKey.fsName validatorsDir = conf.validatorsDir() secretsDir = conf.secretsDir() secretFile = secretsDir / keyName @@ -987,6 +987,72 @@ proc generateDistirbutedStore*(rng: var HmacDrbgContext, # actual validator saveKeystore(remoteValidatorDir, pubKey, signers, threshold) +func validatorKeystoreDir(conf: AnyConf, pubkey: ValidatorPubKey): string = + conf.validatorsDir / pubkey.fsName + +func feeRecipientPath*(conf: AnyConf, pubkey: ValidatorPubKey): string = + conf.validatorKeystoreDir(pubkey) / FeeRecipientFilename + +proc removeFeeRecipientFile*(conf: AnyConf, pubkey: ValidatorPubKey): Result[void, string] = + let path = conf.feeRecipientPath(pubkey) + if fileExists(path): + let res = io2.removeFile(path) + if res.isErr: + return err res.error.ioErrorMsg + + return ok() + +proc setFeeRecipient*(conf: AnyConf, pubkey: ValidatorPubKey, feeRecipient: Eth1Address): Result[void, string] = + let validatorKeystoreDir = conf.validatorKeystoreDir(pubkey) + + ? secureCreatePath(validatorKeystoreDir).mapErr(proc(e: auto): string = + "Could not create wallet directory [" & validatorKeystoreDir & "]: " & $e) + + io2.writeFile(validatorKeystoreDir / FeeRecipientFilename, $feeRecipient) + .mapErr(proc(e: auto): string = "Failed to write fee recipient file: " & $e) + +func defaultFeeRecipient*(conf: AnyConf): Eth1Address = + if conf.suggestedFeeRecipient.isSome: + conf.suggestedFeeRecipient.get + else: + # https://github.com/nim-lang/Nim/issues/19802 + (static(default(Eth1Address))) + +type + FeeRecipientStatus* = enum + noSuchValidator + invalidFeeRecipientFile + +proc getSuggestedFeeRecipient*( + conf: AnyConf, + pubkey: ValidatorPubKey): Result[Eth1Address, FeeRecipientStatus] = + let validatorDir = conf.validatorKeystoreDir(pubkey) + + # In this particular case, an error might be by design. If the file exists, + # but doesn't load or parse that's a more urgent matter to fix. Many people + # people might prefer, however, not to override their default suggested fee + # recipients per validator, so don't warn very loudly, if at all. + if not dirExists(validatorDir): + return err noSuchValidator + + let feeRecipientPath = validatorDir / FeeRecipientFilename + if not fileExists(feeRecipientPath): + return ok conf.defaultFeeRecipient + + try: + # Avoid being overly flexible initially. Trailing whitespace is common + # enough it probably should be allowed, but it is reasonable to simply + # disallow the mostly-pointless flexibility of leading whitespace. + ok Eth1Address.fromHex(strutils.strip( + readFile(feeRecipientPath), leading = false, trailing = true)) + except CatchableError as exc: + # Because the nonexistent validator case was already checked, any failure + # at this point is serious enough to alert the user. + warn "getSuggestedFeeRecipient: failed loading fee recipient file; falling back to default fee recipient", + feeRecipientPath, + err = exc.msg + err invalidFeeRecipientFile + proc generateDeposits*(cfg: RuntimeConfig, rng: var HmacDrbgContext, seed: KeySeed, diff --git a/beacon_chain/validators/validator_duties.nim b/beacon_chain/validators/validator_duties.nim index f908e24f3..70a01a359 100644 --- a/beacon_chain/validators/validator_duties.nim +++ b/beacon_chain/validators/validator_duties.nim @@ -337,44 +337,6 @@ proc get_execution_payload( asConsensusExecutionPayload( await execution_engine.getPayload(payload_id.get)) -proc getSuggestedFeeRecipient(node: BeaconNode, pubkey: ValidatorPubKey): - Eth1Address = - template defaultSuggestedFeeRecipient(): Eth1Address = - if node.config.suggestedFeeRecipient.isSome: - node.config.suggestedFeeRecipient.get - else: - # https://github.com/nim-lang/Nim/issues/19802 - (static(default(Eth1Address))) - - const feeRecipientFilename = "suggested_fee_recipient.hex" - let - keyName = "0x" & pubkey.toHex() - feeRecipientPath = - node.config.validatorsDir() / keyName / feeRecipientFilename - - # In this particular case, an error might be by design. If the file exists, - # but doesn't load or parse that's a more urgent matter to fix. Many people - # people might prefer, however, not to override their default suggested fee - # recipients per validator, so don't warn very loudly, if at all. - if not fileExists(feeRecipientPath): - debug "getSuggestedFeeRecipient: did not find fee recipient file; using default fee recipient", - feeRecipientPath - return defaultSuggestedFeeRecipient() - - try: - # Avoid being overly flexible initially. Trailing whitespace is common - # enough it probably should be allowed, but it is reasonable to simply - # disallow the mostly-pointless flexibility of leading whitespace. - Eth1Address.fromHex(strip( - readFile(feeRecipientPath), leading = false, trailing = true)) - except CatchableError as exc: - # Because the nonexistent validator case was already checked, any failure - # at this point is serious enough to alert the user. - warn "getSuggestedFeeRecipient: failed loading fee recipient file; falling back to default fee recipient", - feeRecipientPath, - err = exc.msg - defaultSuggestedFeeRecipient() - proc getExecutionPayload( node: BeaconNode, proposalState: auto, pubkey: ValidatorPubKey): Future[ExecutionPayload] {.async.} = @@ -410,9 +372,11 @@ proc getExecutionPayload( terminalBlockHash latestFinalized = node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck) + feeRecipient = node.config.getSuggestedFeeRecipient(pubkey).valueOr: + node.config.defaultFeeRecipient payload_id = (await forkchoice_updated( proposalState.bellatrixData.data, latestHead, latestFinalized, - node.getSuggestedFeeRecipient(pubkey), + feeRecipient, node.consensusManager.eth1Monitor)) payload = awaitWithTimeout( get_execution_payload(payload_id, node.consensusManager.eth1Monitor), diff --git a/tests/test_keymanager_api.nim b/tests/test_keymanager_api.nim index 88449c3d8..9f74793ca 100644 --- a/tests/test_keymanager_api.nim +++ b/tests/test_keymanager_api.nim @@ -35,6 +35,7 @@ const tokenFilePath = dataDir / "keymanager-token.txt" keymanagerPort = 47000 correctTokenValue = "some secret token" + defaultFeeRecipient = Eth1Address.fromHex("0x000000000000000000000000000000000000DEAD") newPrivateKeys = [ "0x598c9b81749ba7bb8eb37781027359e3ffe87d0e1579e21c453ce22af0c05e35", "0x14e4470a1d8913ec0602048af78addf0fd7a37f591dd3feda828d10a10c0f6ff", @@ -66,8 +67,16 @@ const "0xa782e5161ba8e9ac135b0db3203a8c23aa61e19be6b9c198393d8b2b902bad8139863d9cf26bc2cbdc3b747bafc64606", "0xb33f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7189e" ] + unusedPublicKeys = [ + "0xc22f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7232d", + "0x0bbca63e35c7a159fc2f187d300cad9ef5f5e73e55f78c391e7bc2c2feabc2d9d63dfe99edd7058ad0ab9d7f14aade5f" + ] + newPublicKeysUrl = HttpHostUri(parseUri("http://127.0.0.1/remote")) +func specifiedFeeRecipient(x: int): Eth1Address = + copyMem(addr result, unsafeAddr x, sizeof x) + proc contains*(keylist: openArray[KeystoreInfo], key: ValidatorPubKey): bool = for item in keylist: if item.validating_pubkey == key: @@ -159,6 +168,7 @@ proc startSingleNodeNetwork {.raises: [CatchableError, Defect].} = "--keymanager-address=127.0.0.1", "--keymanager-port=" & $keymanagerPort, "--keymanager-token-file=" & tokenFilePath, + "--suggested-fee-recipient=" & $defaultFeeRecipient, "--light-client-enable=off", "--light-client-data-serve=off", "--light-client-data-import-mode=none", @@ -552,9 +562,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -564,9 +572,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -575,10 +581,8 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError expect RestError: let keystores = await client.listKeys("Invalid Token") @@ -660,9 +664,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -673,9 +675,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -685,10 +685,8 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError suite "DeleteKeys requests" & preset(): asyncTest "Deleting not existing key" & preset(): @@ -710,9 +708,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -723,9 +719,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -735,10 +729,8 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError suite "ListRemoteKeys requests" & preset(): asyncTest "Correct token provided" & preset(): @@ -757,9 +749,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -769,9 +759,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -780,14 +768,174 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError expect RestError: let keystores = await client.listKeys("Invalid Token") + suite "Fee recipient management" & preset(): + asyncTest "Missing Authorization header" & preset(): + let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key") + + block: + let + response = await client.listFeeRecipientPlain(pubkey) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + block: + let + response = await client.setFeeRecipientPlain( + pubkey, + default SetFeeRecipientRequest) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + block: + let + response = await client.deleteFeeRecipientPlain(pubkey, EmptyBody()) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + asyncTest "Invalid Authorization Header" & preset(): + let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key") + + block: + let + response = await client.listFeeRecipientPlain( + pubkey, + extraHeaders = @[("Authorization", "UnknownAuthScheme X")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + block: + let + response = await client.setFeeRecipientPlain( + pubkey, + default SetFeeRecipientRequest, + extraHeaders = @[("Authorization", "UnknownAuthScheme X")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + + block: + let + response = await client.deleteFeeRecipientPlain( + pubkey, + EmptyBody(), + extraHeaders = @[("Authorization", "UnknownAuthScheme X")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["message"].getStr() == InvalidAuthorizationError + + asyncTest "Invalid Authorization Token" & preset(): + let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key") + + block: + let + response = await client.listFeeRecipientPlain( + pubkey, + extraHeaders = @[("Authorization", "Bearer InvalidToken")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError + + block: + let + response = await client.setFeeRecipientPlain( + pubkey, + default SetFeeRecipientRequest, + extraHeaders = @[("Authorization", "Bearer InvalidToken")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError + + block: + let + response = await client.deleteFeeRecipientPlain( + pubkey, + EmptyBody(), + extraHeaders = @[("Authorization", "Bearer InvalidToken")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError + + asyncTest "Obtaining the fee recpient of a missing validator returns 404" & preset(): + let + pubkey = ValidatorPubKey.fromHex(unusedPublicKeys[0]).expect("valid key") + response = await client.listFeeRecipientPlain( + pubkey, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + + check: + response.status == 404 + + asyncTest "Setting the fee recipient on a missing validator creates a record for it" & preset(): + let + pubkey = ValidatorPubKey.fromHex(unusedPublicKeys[1]).expect("valid key") + feeRecipient = specifiedFeeRecipient(1) + + await client.setFeeRecipient(pubkey, feeRecipient, correctTokenValue) + let resultFromApi = await client.listFeeRecipient(pubkey, correctTokenValue) + + check: + resultFromApi == feeRecipient + + asyncTest "Obtaining the fee recpient of an unconfigured validator returns the suggested default" & preset(): + let + pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key") + resultFromApi = await client.listFeeRecipient(pubkey, correctTokenValue) + + check: + resultFromApi == defaultFeeRecipient + + asyncTest "Configuring the fee recpient" & preset(): + let + pubkey = ValidatorPubKey.fromHex(oldPublicKeys[1]).expect("valid key") + firstFeeRecipient = specifiedFeeRecipient(2) + + await client.setFeeRecipient(pubkey, firstFeeRecipient, correctTokenValue) + + let firstResultFromApi = await client.listFeeRecipient(pubkey, correctTokenValue) + check: + firstResultFromApi == firstFeeRecipient + + let secondFeeRecipient = specifiedFeeRecipient(3) + await client.setFeeRecipient(pubkey, secondFeeRecipient, correctTokenValue) + + let secondResultFromApi = await client.listFeeRecipient(pubkey, correctTokenValue) + check: + secondResultFromApi == secondFeeRecipient + + await client.deleteFeeRecipient(pubkey, correctTokenValue) + let finalResultFromApi = await client.listFeeRecipient(pubkey, correctTokenValue) + check: + finalResultFromApi == defaultFeeRecipient + suite "ImportRemoteKeys/ListRemoteKeys/DeleteRemoteKeys" & preset(): asyncTest "Importing list of remote keys" & preset(): let @@ -866,9 +1014,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -879,9 +1025,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -891,10 +1035,8 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError suite "DeleteRemoteKeys requests" & preset(): asyncTest "Deleting not existing key" & preset(): @@ -949,9 +1091,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Header" & preset(): let @@ -962,9 +1102,7 @@ proc runTests {.async.} = check: response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $missingBearerScheme + responseJson["message"].getStr() == InvalidAuthorizationError asyncTest "Invalid Authorization Token" & preset(): let @@ -974,10 +1112,8 @@ proc runTests {.async.} = responseJson = Json.decode(response.data, JsonNode) check: - response.status == 401 - responseJson["code"].getStr() == "401" - responseJson["message"].getStr() == InvalidAuthorization - responseJson["stacktraces"][0].getStr() == $incorrectToken + response.status == 403 + responseJson["message"].getStr() == InvalidAuthorizationError bnStatus = BeaconNodeStatus.Stopping