[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.
This commit is contained in:
zah 2022-07-13 17:45:04 +03:00 committed by GitHub
parent 456b5ebc48
commit 806536a040
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 575 additions and 195 deletions

View File

@ -168,6 +168,17 @@ OK: 3/3 Fail: 0/3 Skip: 0/3
+ addExitMessage/getVoluntaryExitMessage OK + addExitMessage/getVoluntaryExitMessage OK
``` ```
OK: 3/3 Fail: 0/3 Skip: 0/3 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] ## FinalizedBlocks [Preset: mainnet]
```diff ```diff
+ Basic ops [Preset: mainnet] OK + 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 OK: 9/9 Fail: 0/9 Skip: 0/9
---TOTAL--- ---TOTAL---
OK: 314/319 Fail: 0/319 Skip: 5/319 OK: 321/326 Fail: 0/326 Skip: 5/326

View File

@ -345,7 +345,6 @@ proc installBeaconApiHandlers*(router: var RestRouter, node: BeaconNode) =
let vindex = let vindex =
block: block:
let vid = validator_id.get()
case vid.kind case vid.kind
of ValidatorQueryKind.Key: of ValidatorQueryKind.Key:
let optIndices = keysToIndices(node.restKeysCache, state, [vid.key]) let optIndices = keysToIndices(node.restKeysCache, state, [vid.key])

View File

@ -50,6 +50,8 @@ const
"Proposer slashing object was broadcasted" "Proposer slashing object was broadcasted"
InvalidVoluntaryExitObjectError* = InvalidVoluntaryExitObjectError* =
"Unable to decode voluntary exit object(s)" "Unable to decode voluntary exit object(s)"
InvalidFeeRecipientRequestError* =
"Bad request. Request was malformed and could not be processed"
VoluntaryExitValidationError* = VoluntaryExitValidationError* =
"Invalid voluntary exit, it will never pass validation so it's rejected" "Invalid voluntary exit, it will never pass validation so it's rejected"
VoluntaryExitValidationSuccess* = VoluntaryExitValidationSuccess* =
@ -195,7 +197,7 @@ const
"Invalid validator's public key(s) found" "Invalid validator's public key(s) found"
BadRequestFormatError* = BadRequestFormatError* =
"Bad request format" "Bad request format"
InvalidAuthorization* = InvalidAuthorizationError* =
"Invalid Authorization Header" "Invalid Authorization Header"
PrunedStateError* = PrunedStateError* =
"Trying to access a pruned historical state" "Trying to access a pruned historical state"

View File

@ -50,6 +50,24 @@ proc listRemoteDistributedValidators*(node: BeaconNode): seq[DistributedKeystore
) )
validators 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, proc checkAuthorization*(request: HttpRequestRef,
node: BeaconNode): Result[void, AuthorizationError] = node: BeaconNode): Result[void, AuthorizationError] =
let authorizations = request.headers.getList("authorization") let authorizations = request.headers.getList("authorization")
@ -65,6 +83,15 @@ proc checkAuthorization*(request: HttpRequestRef,
else: else:
return err noAuthorizationHeader 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] = proc validateUri*(url: string): Result[Uri, cstring] =
let surl = parseUri(url) let surl = parseUri(url)
if surl.scheme notin ["http", "https"]: 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: router.api(MethodGet, "/api/eth/v1/keystores") do () -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let response = GetKeystoresResponse(data: listLocalValidators(node)) let response = GetKeystoresResponse(data: listLocalValidators(node))
return RestApiResponse.jsonResponsePlain(response) return RestApiResponse.jsonResponsePlain(response)
@ -116,16 +142,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let request = let request =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(KeystoresAndSlashingProtection, contentBody.get()) let dres = decodeBody(KeystoresAndSlashingProtection, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, return keymanagerApiError(Http400, InvalidKeystoreObjects)
$dres.error())
dres.get() dres.get()
if request.slashing_protection.isSome(): if request.slashing_protection.isSome():
@ -133,13 +157,13 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection) let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection)
if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest != if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest !=
slashing_protection.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 " & "The slashing protection database and imported file refer to " &
"different blockchains.") "different blockchains.")
let res = inclSPDIR(node.attachedValidators.slashingProtection, let res = inclSPDIR(node.attachedValidators.slashingProtection,
slashing_protection) slashing_protection)
if res == siFailure: if res == siFailure:
return RestApiResponse.jsonError(Http500, return keymanagerApiError(Http500,
"Internal server error; Failed to import slashing protection data") "Internal server error; Failed to import slashing protection data")
var response: PostKeystoresResponse var response: PostKeystoresResponse
@ -169,16 +193,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let keys = let keys =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, return keymanagerApiError(Http400, InvalidValidatorPublicKey)
$dres.error())
dres.get().pubkeys dres.get().pubkeys
var var
@ -230,8 +252,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
router.api(MethodGet, "/api/eth/v1/remotekeys") do () -> RestApiResponse: router.api(MethodGet, "/api/eth/v1/remotekeys") do () -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let response = GetRemoteKeystoresResponse(data: listRemoteValidators(node)) let response = GetRemoteKeystoresResponse(data: listRemoteValidators(node))
return RestApiResponse.jsonResponsePlain(response) return RestApiResponse.jsonResponsePlain(response)
@ -240,16 +261,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let keys = let keys =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(ImportRemoteKeystoresBody, contentBody.get()) let dres = decodeBody(ImportRemoteKeystoresBody, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, return keymanagerApiError(Http400, InvalidKeystoreObjects)
$dres.error())
dres.get().remote_keys dres.get().remote_keys
var response: PostKeystoresResponse var response: PostKeystoresResponse
@ -274,16 +293,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let keys = let keys =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, return keymanagerApiError(Http400, InvalidValidatorPublicKey)
$dres.error())
dres.get().pubkeys dres.get().pubkeys
var response: DeleteRemoteKeystoresResponse var response: DeleteRemoteKeystoresResponse
@ -292,13 +309,78 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
response.data.add(status) response.data.add(status)
return RestApiResponse.jsonResponsePlain(response) 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 # TODO: These URLs will be changed once we submit a proposal for
# /api/eth/v2/remotekeys that supports distributed keys. # /api/eth/v2/remotekeys that supports distributed keys.
router.api(MethodGet, "/api/eth/v1/remotekeys/distributed") do () -> RestApiResponse: router.api(MethodGet, "/api/eth/v1/remotekeys/distributed") do () -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let response = GetDistributedKeystoresResponse(data: listRemoteDistributedValidators(node)) let response = GetDistributedKeystoresResponse(data: listRemoteDistributedValidators(node))
return RestApiResponse.jsonResponsePlain(response) return RestApiResponse.jsonResponsePlain(response)
@ -308,16 +390,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let keys = let keys =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(ImportDistributedKeystoresBody, contentBody.get()) let dres = decodeBody(ImportDistributedKeystoresBody, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects, return keymanagerApiError(Http400, InvalidKeystoreObjects)
$dres.error())
dres.get.remote_keys dres.get.remote_keys
var response: PostKeystoresResponse var response: PostKeystoresResponse
@ -339,16 +419,14 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
contentBody: Option[ContentBody]) -> RestApiResponse: contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, node) let authStatus = checkAuthorization(request, node)
if authStatus.isErr(): if authStatus.isErr():
return RestApiResponse.jsonError(Http401, InvalidAuthorization, return authErrorResponse authStatus.error
$authStatus.error())
let keys = let keys =
block: block:
if contentBody.isNone(): if contentBody.isNone():
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError) return keymanagerApiError(Http404, EmptyRequestBodyError)
let dres = decodeBody(DeleteKeystoresBody, contentBody.get()) let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
if dres.isErr(): if dres.isErr():
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey, return keymanagerApiError(Http400, InvalidValidatorPublicKey)
$dres.error())
dres.get.pubkeys dres.get.pubkeys
var response: DeleteRemoteKeystoresResponse var response: DeleteRemoteKeystoresResponse
@ -388,6 +466,21 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
"/eth/v1/remotekeys", "/eth/v1/remotekeys",
"/api/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( router.redirect(
MethodGet, MethodGet,
"/eth/v1/remotekeys/distributed", "/eth/v1/remotekeys/distributed",

View File

@ -48,6 +48,8 @@ proc validate(key: string, value: string): int =
0 0
of "{block_root}": of "{block_root}":
0 0
of "{pubkey}":
int(value.len != 98)
else: else:
1 1

View File

@ -69,6 +69,8 @@ const
[byte('a'), byte('l'), byte('t'), byte('a'), byte('i'), byte('r')] [byte('a'), byte('l'), byte('t'), byte('a'), byte('i'), byte('r')]
type type
EmptyBody* = object
RestGenericError* = object RestGenericError* = object
code*: uint64 code*: uint64
message*: string message*: string
@ -81,29 +83,31 @@ type
EncodeTypes* = EncodeTypes* =
AttesterSlashing | AttesterSlashing |
DeleteKeystoresBody |
EmptyBody |
ImportDistributedKeystoresBody |
ImportRemoteKeystoresBody |
KeystoresAndSlashingProtection |
ProposerSlashing | ProposerSlashing |
phase0.SignedBeaconBlock | SetFeeRecipientRequest |
altair.SignedBeaconBlock |
bellatrix.SignedBeaconBlock |
SignedBlindedBeaconBlock | SignedBlindedBeaconBlock |
SignedValidatorRegistrationV1 | SignedValidatorRegistrationV1 |
SignedVoluntaryExit | SignedVoluntaryExit |
Web3SignerRequest | Web3SignerRequest |
KeystoresAndSlashingProtection | altair.SignedBeaconBlock |
DeleteKeystoresBody | bellatrix.SignedBeaconBlock |
ImportRemoteKeystoresBody | phase0.SignedBeaconBlock
ImportDistributedKeystoresBody
EncodeArrays* = EncodeArrays* =
seq[ValidatorIndex] |
seq[Attestation] | seq[Attestation] |
seq[RemoteKeystoreInfo] |
seq[RestCommitteeSubscription] |
seq[RestSignedContributionAndProof] |
seq[RestSyncCommitteeMessage] |
seq[RestSyncCommitteeSubscription] |
seq[SignedAggregateAndProof] | seq[SignedAggregateAndProof] |
seq[SignedValidatorRegistrationV1] | seq[SignedValidatorRegistrationV1] |
seq[RestCommitteeSubscription] | seq[ValidatorIndex]
seq[RestSyncCommitteeSubscription] |
seq[RestSyncCommitteeMessage] |
seq[RestSignedContributionAndProof] |
seq[RemoteKeystoreInfo]
DecodeTypes* = DecodeTypes* =
DataEnclosedObject | DataEnclosedObject |
@ -111,20 +115,22 @@ type
DataRootEnclosedObject | DataRootEnclosedObject |
DataVersionEnclosedObject | DataVersionEnclosedObject |
GetBlockV2Response | GetBlockV2Response |
GetDistributedKeystoresResponse |
GetKeystoresResponse | GetKeystoresResponse |
GetRemoteKeystoresResponse | GetRemoteKeystoresResponse |
GetDistributedKeystoresResponse |
GetStateV2Response |
GetStateForkResponse | GetStateForkResponse |
GetStateV2Response |
KeymanagerGenericError |
KeystoresAndSlashingProtection |
ListFeeRecipientResponse |
ProduceBlockResponseV2 | ProduceBlockResponseV2 |
RestDutyError | RestDutyError |
RestValidator |
RestGenericError | RestGenericError |
RestValidator |
Web3SignerErrorResponse | Web3SignerErrorResponse |
Web3SignerKeysResponse | Web3SignerKeysResponse |
Web3SignerSignatureResponse | Web3SignerSignatureResponse |
Web3SignerStatusResponse | Web3SignerStatusResponse
KeystoresAndSlashingProtection
SszDecodeTypes* = SszDecodeTypes* =
GetPhase0StateSszResponse | GetPhase0StateSszResponse |
@ -470,11 +476,10 @@ template hexOriginal(data: openArray[byte]): string =
to0xHex(data) to0xHex(data)
proc decodeJsonString*[T](t: typedesc[T], proc decodeJsonString*[T](t: typedesc[T],
data: JsonString, data: JsonString): Result[T, cstring] =
requireAllFields = true): Result[T, cstring] =
try: try:
ok(RestJson.decode(string(data), T, ok(RestJson.decode(string(data), T,
requireAllFields = requireAllFields, requireAllFields = true,
allowUnknownFields = true)) allowUnknownFields = true))
except SerializationError: except SerializationError:
err("Unable to deserialize data") err("Unable to deserialize data")
@ -1558,8 +1563,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(Web3SignerAggregationSlotData, let res = decodeJsonString(Web3SignerAggregationSlotData, data.get())
data.get(), true)
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `aggregation_slot` format") "Incorrect field `aggregation_slot` format")
@ -1574,7 +1578,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(AggregateAndProof, data.get(), true) let res = decodeJsonString(AggregateAndProof, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `aggregate_and_proof` format") "Incorrect field `aggregate_and_proof` format")
@ -1590,7 +1594,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(AttestationData, data.get(), true) let res = decodeJsonString(AttestationData, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `attestation` format") "Incorrect field `attestation` format")
@ -1606,7 +1610,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(phase0.BeaconBlock, data.get(), true) let res = decodeJsonString(phase0.BeaconBlock, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `block` format") "Incorrect field `block` format")
@ -1622,7 +1626,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(Web3SignerForkedBeaconBlock, data.get(), true) let res = decodeJsonString(Web3SignerForkedBeaconBlock, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `beacon_block` format") "Incorrect field `beacon_block` format")
@ -1636,7 +1640,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `deposit` is missing") reader.raiseUnexpectedValue("Field `deposit` is missing")
let data = let data =
block: block:
let res = decodeJsonString(Web3SignerDepositData, data.get(), true) let res = decodeJsonString(Web3SignerDepositData, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `deposit` format") "Incorrect field `deposit` format")
@ -1652,8 +1656,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(Web3SignerRandaoRevealData, data.get(), let res = decodeJsonString(Web3SignerRandaoRevealData, data.get())
true)
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `randao_reveal` format") "Incorrect field `randao_reveal` format")
@ -1669,7 +1672,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(VoluntaryExit, data.get(), true) let res = decodeJsonString(VoluntaryExit, data.get())
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `voluntary_exit` format") "Incorrect field `voluntary_exit` format")
@ -1686,8 +1689,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(Web3SignerSyncCommitteeMessageData, let res = decodeJsonString(Web3SignerSyncCommitteeMessageData, data.get())
data.get(), true)
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `sync_committee_message` format") "Incorrect field `sync_committee_message` format")
@ -1705,8 +1707,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(SyncAggregatorSelectionData, let res = decodeJsonString(SyncAggregatorSelectionData, data.get())
data.get(), true)
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `sync_aggregator_selection_data` format") "Incorrect field `sync_aggregator_selection_data` format")
@ -1724,8 +1725,7 @@ proc readValue*(reader: var JsonReader[RestJson],
reader.raiseUnexpectedValue("Field `fork_info` is missing") reader.raiseUnexpectedValue("Field `fork_info` is missing")
let data = let data =
block: block:
let res = decodeJsonString(ContributionAndProof, let res = decodeJsonString(ContributionAndProof, data.get())
data.get(), true)
if res.isErr(): if res.isErr():
reader.raiseUnexpectedValue( reader.raiseUnexpectedValue(
"Incorrect field `contribution_and_proof` format") "Incorrect field `contribution_and_proof` format")
@ -2068,7 +2068,10 @@ proc readValue*(reader: var JsonReader[RestJson],
for item in strKeystores: for item in strKeystores:
let key = let key =
try: try:
RestJson.decode(item, Keystore, allowUnknownFields = true) RestJson.decode(item,
Keystore,
requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc: except SerializationError as exc:
# TODO re-raise the exception by adjusting the column index, so the user # TODO re-raise the exception by adjusting the column index, so the user
# will get an accurate syntax error within the larger message # will get an accurate syntax error within the larger message
@ -2080,7 +2083,10 @@ proc readValue*(reader: var JsonReader[RestJson],
if strSlashing.isSome(): if strSlashing.isSome():
let db = let db =
try: try:
RestJson.decode(strSlashing.get(), SPDIR, allowUnknownFields = true) RestJson.decode(strSlashing.get(),
SPDIR,
requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc: except SerializationError as exc:
reader.raiseUnexpectedValue("Invalid slashing protection format") reader.raiseUnexpectedValue("Invalid slashing protection format")
some(db) some(db)
@ -2172,8 +2178,12 @@ proc decodeBody*[T](t: typedesc[T],
return err("Unsupported content type") return err("Unsupported content type")
let data = let data =
try: try:
RestJson.decode(body.data, T, allowUnknownFields = true) RestJson.decode(body.data, T,
requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc: except SerializationError as exc:
debug "Failed to deserialize REST JSON data",
err = exc.formatMsg("<data>")
return err("Unable to deserialize data") return err("Unable to deserialize data")
except CatchableError: except CatchableError:
return err("Unexpected deserialization error") return err("Unexpected deserialization error")
@ -2222,8 +2232,12 @@ proc decodeBytes*[T: DecodeTypes](t: typedesc[T], value: openArray[byte],
case contentType case contentType
of "application/json": of "application/json":
try: try:
ok RestJson.decode(value, T, allowUnknownFields = true) ok RestJson.decode(value, T,
except SerializationError: requireAllFields = true,
allowUnknownFields = true)
except SerializationError as exc:
debug "Failed to deserialize REST JSON data",
err = exc.formatMsg("<data>")
err("Serialization error") err("Serialization error")
else: else:
err("Content-Type not supported") err("Content-Type not supported")

View File

@ -20,6 +20,19 @@ UUID.serializesAsBaseIn RestJson
KeyPath.serializesAsBaseIn RestJson KeyPath.serializesAsBaseIn RestJson
WalletName.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 {. proc listKeysPlain*(): RestPlainResponse {.
rest, endpoint: "/eth/v1/keystores", rest, endpoint: "/eth/v1/keystores",
meth: MethodGet.} meth: MethodGet.}
@ -49,7 +62,7 @@ proc listKeys*(client: RestClientRef,
raise newException(RestError, $keystoresRes.error) raise newException(RestError, $keystoresRes.error)
return keystoresRes.get() return keystoresRes.get()
of 401, 403, 500: of 401, 403, 500:
raiseGenericError(resp) raiseKeymanagerGenericError(resp)
else: else:
raiseUnknownStatusError(resp) raiseUnknownStatusError(resp)
@ -69,6 +82,23 @@ proc deleteRemoteKeysPlain*(body: DeleteKeystoresBody): RestPlainResponse {.
meth: MethodDelete.} meth: MethodDelete.}
## https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/DeleteRemoteKeys ## 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 {. proc listRemoteDistributedKeysPlain*(): RestPlainResponse {.
rest, endpoint: "/eth/v1/remotekeys/distributed", rest, endpoint: "/eth/v1/remotekeys/distributed",
meth: MethodGet.} meth: MethodGet.}
@ -82,7 +112,6 @@ proc deleteRemoteDistributedKeysPlain*(body: DeleteKeystoresBody): RestPlainResp
rest, endpoint: "/eth/v1/remotekeys/distributed", rest, endpoint: "/eth/v1/remotekeys/distributed",
meth: MethodDelete.} meth: MethodDelete.}
proc listRemoteKeys*(client: RestClientRef, proc listRemoteKeys*(client: RestClientRef,
token: string): Future[GetRemoteKeystoresResponse] {. token: string): Future[GetRemoteKeystoresResponse] {.
async.} = async.} =
@ -91,12 +120,66 @@ proc listRemoteKeys*(client: RestClientRef,
case resp.status: case resp.status:
of 200: of 200:
let res = decodeBytes(GetRemoteKeystoresResponse, resp.data, let res = decodeBytes(GetRemoteKeystoresResponse,
resp.data,
resp.contentType) resp.contentType)
if res.isErr(): if res.isErr():
raise newException(RestError, $res.error()) raise newException(RestError, $res.error())
return res.get() return res.get()
of 401, 403, 500: 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: else:
raiseUnknownStatusError(resp) raiseUnknownStatusError(resp)

View File

@ -65,6 +65,13 @@ type
DeleteRemoteKeystoresResponse* = object DeleteRemoteKeystoresResponse* = object
data*: seq[RemoteKeystoreStatus] data*: seq[RemoteKeystoreStatus]
SetFeeRecipientRequest* = object
ethaddress*: Eth1Address
ListFeeRecipientResponse* = object
pubkey*: ValidatorPubKey
ethaddress*: Eth1Address
KeystoreStatus* = enum KeystoreStatus* = enum
error = "error" error = "error"
notActive = "not_active" notActive = "not_active"
@ -78,6 +85,9 @@ type
missingBearerScheme = "Bearer Authentication is not included in request" missingBearerScheme = "Bearer Authentication is not included in request"
incorrectToken = "Authentication token is incorrect" incorrectToken = "Authentication token is incorrect"
KeymanagerGenericError* = object
message*: string
proc `<`*(x, y: KeystoreInfo | RemoteKeystoreInfo): bool = proc `<`*(x, y: KeystoreInfo | RemoteKeystoreInfo): bool =
for a, b in fields(x, y): for a, b in fields(x, y):
var c = cmp(a, b) var c = cmp(a, b)

View File

@ -33,9 +33,7 @@ const
KeystoreFileName* = "keystore.json" KeystoreFileName* = "keystore.json"
RemoteKeystoreFileName* = "remote_keystore.json" RemoteKeystoreFileName* = "remote_keystore.json"
NetKeystoreFileName* = "network_keystore.json" NetKeystoreFileName* = "network_keystore.json"
DisableFileName* = ".disable" FeeRecipientFilename* = "suggested_fee_recipient.hex"
DisableFileContent* = "Please do not remove this file manually. " &
"This can lead to slashing of this validator's key."
KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters. KeyNameSize* = 98 # 0x + hexadecimal key representation 96 characters.
type type
@ -520,6 +518,9 @@ proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string,
ok(RemoveValidatorStatus.deleted) ok(RemoveValidatorStatus.deleted)
func fsName(pubkey: ValidatorPubKey|CookedPubKey): string =
"0x" & pubkey.toHex()
proc removeValidatorFiles*(conf: AnyConf, keyName: string, proc removeValidatorFiles*(conf: AnyConf, keyName: string,
kind: KeystoreKind): KmResult[RemoveValidatorStatus] kind: KeystoreKind): KmResult[RemoveValidatorStatus]
{.raises: [Defect].} = {.raises: [Defect].} =
@ -534,8 +535,7 @@ proc removeValidator*(pool: var ValidatorPool, conf: AnyConf,
return ok(RemoveValidatorStatus.notFound) return ok(RemoveValidatorStatus.notFound)
if validator.kind.toKeystoreKind() != kind: if validator.kind.toKeystoreKind() != kind:
return ok(RemoveValidatorStatus.notFound) return ok(RemoveValidatorStatus.notFound)
let publicKeyName: string = "0x" & publicKey.toHex() let res = removeValidatorFiles(conf, publicKey.fsName, kind)
let res = removeValidatorFiles(conf, publicKeyName, kind)
if res.isErr(): if res.isErr():
return err(res.error()) return err(res.error())
pool.removeValidator(publicKey) pool.removeValidator(publicKey)
@ -793,7 +793,7 @@ proc saveKeystore*(rng: var HmacDrbgContext,
mode = Secure): Result[void, KeystoreGenerationError] = mode = Secure): Result[void, KeystoreGenerationError] =
let let
keypass = KeystorePass.init(password) keypass = KeystorePass.init(password)
keyName = "0x" & signingPubKey.toHex() keyName = signingPubKey.fsName
keystoreDir = validatorsDir / keyName keystoreDir = validatorsDir / keyName
keystoreFile = keystoreDir / KeystoreFileName keystoreFile = keystoreDir / KeystoreFileName
@ -832,7 +832,7 @@ proc saveKeystore*(validatorsDir: string,
desc = ""): Result[void, KeystoreGenerationError] desc = ""): Result[void, KeystoreGenerationError]
{.raises: [Defect].} = {.raises: [Defect].} =
let let
keyName = "0x" & publicKey.toHex() keyName = publicKey.fsName
keystoreDir = validatorsDir / keyName keystoreDir = validatorsDir / keyName
keystoreFile = keystoreDir / RemoteKeystoreFileName keystoreFile = keystoreDir / RemoteKeystoreFileName
keystoreDesc = if len(desc) == 0: none[string]() else: some(desc) keystoreDesc = if len(desc) == 0: none[string]() else: some(desc)
@ -888,7 +888,7 @@ proc importKeystore*(pool: var ValidatorPool, conf: AnyConf,
{.raises: [Defect].} = {.raises: [Defect].} =
let let
publicKey = keystore.pubkey publicKey = keystore.pubkey
keyName = "0x" & publicKey.toHex() keyName = publicKey.fsName
validatorsDir = conf.validatorsDir() validatorsDir = conf.validatorsDir()
keystoreDir = validatorsDir / keyName keystoreDir = validatorsDir / keyName
keystoreFile = keystoreDir / RemoteKeystoreFileName keystoreFile = keystoreDir / RemoteKeystoreFileName
@ -933,7 +933,7 @@ proc importKeystore*(pool: var ValidatorPool,
AddValidatorFailure.init(AddValidatorStatus.failed, res.error())) AddValidatorFailure.init(AddValidatorStatus.failed, res.error()))
let let
publicKey = privateKey.toPubKey() publicKey = privateKey.toPubKey()
keyName = "0x" & publicKey.toHex() keyName = publicKey.fsName
validatorsDir = conf.validatorsDir() validatorsDir = conf.validatorsDir()
secretsDir = conf.secretsDir() secretsDir = conf.secretsDir()
secretFile = secretsDir / keyName secretFile = secretsDir / keyName
@ -987,6 +987,72 @@ proc generateDistirbutedStore*(rng: var HmacDrbgContext,
# actual validator # actual validator
saveKeystore(remoteValidatorDir, pubKey, signers, threshold) 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, proc generateDeposits*(cfg: RuntimeConfig,
rng: var HmacDrbgContext, rng: var HmacDrbgContext,
seed: KeySeed, seed: KeySeed,

View File

@ -337,44 +337,6 @@ proc get_execution_payload(
asConsensusExecutionPayload( asConsensusExecutionPayload(
await execution_engine.getPayload(payload_id.get)) 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( proc getExecutionPayload(
node: BeaconNode, proposalState: auto, pubkey: ValidatorPubKey): node: BeaconNode, proposalState: auto, pubkey: ValidatorPubKey):
Future[ExecutionPayload] {.async.} = Future[ExecutionPayload] {.async.} =
@ -410,9 +372,11 @@ proc getExecutionPayload(
terminalBlockHash terminalBlockHash
latestFinalized = latestFinalized =
node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck) node.dag.loadExecutionBlockRoot(node.dag.finalizedHead.blck)
feeRecipient = node.config.getSuggestedFeeRecipient(pubkey).valueOr:
node.config.defaultFeeRecipient
payload_id = (await forkchoice_updated( payload_id = (await forkchoice_updated(
proposalState.bellatrixData.data, latestHead, latestFinalized, proposalState.bellatrixData.data, latestHead, latestFinalized,
node.getSuggestedFeeRecipient(pubkey), feeRecipient,
node.consensusManager.eth1Monitor)) node.consensusManager.eth1Monitor))
payload = awaitWithTimeout( payload = awaitWithTimeout(
get_execution_payload(payload_id, node.consensusManager.eth1Monitor), get_execution_payload(payload_id, node.consensusManager.eth1Monitor),

View File

@ -35,6 +35,7 @@ const
tokenFilePath = dataDir / "keymanager-token.txt" tokenFilePath = dataDir / "keymanager-token.txt"
keymanagerPort = 47000 keymanagerPort = 47000
correctTokenValue = "some secret token" correctTokenValue = "some secret token"
defaultFeeRecipient = Eth1Address.fromHex("0x000000000000000000000000000000000000DEAD")
newPrivateKeys = [ newPrivateKeys = [
"0x598c9b81749ba7bb8eb37781027359e3ffe87d0e1579e21c453ce22af0c05e35", "0x598c9b81749ba7bb8eb37781027359e3ffe87d0e1579e21c453ce22af0c05e35",
"0x14e4470a1d8913ec0602048af78addf0fd7a37f591dd3feda828d10a10c0f6ff", "0x14e4470a1d8913ec0602048af78addf0fd7a37f591dd3feda828d10a10c0f6ff",
@ -66,8 +67,16 @@ const
"0xa782e5161ba8e9ac135b0db3203a8c23aa61e19be6b9c198393d8b2b902bad8139863d9cf26bc2cbdc3b747bafc64606", "0xa782e5161ba8e9ac135b0db3203a8c23aa61e19be6b9c198393d8b2b902bad8139863d9cf26bc2cbdc3b747bafc64606",
"0xb33f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7189e" "0xb33f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7189e"
] ]
unusedPublicKeys = [
"0xc22f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7232d",
"0x0bbca63e35c7a159fc2f187d300cad9ef5f5e73e55f78c391e7bc2c2feabc2d9d63dfe99edd7058ad0ab9d7f14aade5f"
]
newPublicKeysUrl = HttpHostUri(parseUri("http://127.0.0.1/remote")) 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 = proc contains*(keylist: openArray[KeystoreInfo], key: ValidatorPubKey): bool =
for item in keylist: for item in keylist:
if item.validating_pubkey == key: if item.validating_pubkey == key:
@ -159,6 +168,7 @@ proc startSingleNodeNetwork {.raises: [CatchableError, Defect].} =
"--keymanager-address=127.0.0.1", "--keymanager-address=127.0.0.1",
"--keymanager-port=" & $keymanagerPort, "--keymanager-port=" & $keymanagerPort,
"--keymanager-token-file=" & tokenFilePath, "--keymanager-token-file=" & tokenFilePath,
"--suggested-fee-recipient=" & $defaultFeeRecipient,
"--light-client-enable=off", "--light-client-enable=off",
"--light-client-data-serve=off", "--light-client-data-serve=off",
"--light-client-data-import-mode=none", "--light-client-data-import-mode=none",
@ -552,9 +562,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -564,9 +572,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -575,10 +581,8 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
expect RestError: expect RestError:
let keystores = await client.listKeys("Invalid Token") let keystores = await client.listKeys("Invalid Token")
@ -660,9 +664,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -673,9 +675,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -685,10 +685,8 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
suite "DeleteKeys requests" & preset(): suite "DeleteKeys requests" & preset():
asyncTest "Deleting not existing key" & preset(): asyncTest "Deleting not existing key" & preset():
@ -710,9 +708,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -723,9 +719,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -735,10 +729,8 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
suite "ListRemoteKeys requests" & preset(): suite "ListRemoteKeys requests" & preset():
asyncTest "Correct token provided" & preset(): asyncTest "Correct token provided" & preset():
@ -757,9 +749,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -769,9 +759,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -780,14 +768,174 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
expect RestError: expect RestError:
let keystores = await client.listKeys("Invalid Token") 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(): suite "ImportRemoteKeys/ListRemoteKeys/DeleteRemoteKeys" & preset():
asyncTest "Importing list of remote keys" & preset(): asyncTest "Importing list of remote keys" & preset():
let let
@ -866,9 +1014,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -879,9 +1025,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -891,10 +1035,8 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
suite "DeleteRemoteKeys requests" & preset(): suite "DeleteRemoteKeys requests" & preset():
asyncTest "Deleting not existing key" & preset(): asyncTest "Deleting not existing key" & preset():
@ -949,9 +1091,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader
asyncTest "Invalid Authorization Header" & preset(): asyncTest "Invalid Authorization Header" & preset():
let let
@ -962,9 +1102,7 @@ proc runTests {.async.} =
check: check:
response.status == 401 response.status == 401
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $missingBearerScheme
asyncTest "Invalid Authorization Token" & preset(): asyncTest "Invalid Authorization Token" & preset():
let let
@ -974,10 +1112,8 @@ proc runTests {.async.} =
responseJson = Json.decode(response.data, JsonNode) responseJson = Json.decode(response.data, JsonNode)
check: check:
response.status == 401 response.status == 403
responseJson["code"].getStr() == "401" responseJson["message"].getStr() == InvalidAuthorizationError
responseJson["message"].getStr() == InvalidAuthorization
responseJson["stacktraces"][0].getStr() == $incorrectToken
bnStatus = BeaconNodeStatus.Stopping bnStatus = BeaconNodeStatus.Stopping