Skip to content

Commit

Permalink
Add Keymanager API graffiti endpoints. (#6054)
Browse files Browse the repository at this point in the history
* Initial commit.

* Add more tests.

* Fix API mistypes.

* Fix mistypes in tests.

* Fix one more mistype.

* Fix affected tests because of error code 401.

* Add GetGraffitiResponse object.

* Add more tests.

* Fix compilation errors.

* Recover old behavior.

* Recover old behavior.

* Fix mistype.

* Test could not know default graffiti value.

* Make VC use adopted graffiti settings.

* Make BN use adopted graffiti settings.

* Update Alltests.

* Fix test.

* Revert "Fix test."

This reverts commit c735f85.

* Workaround {.push raises.} requirement.

* Fix comment.

* Update Alltests.
  • Loading branch information
cheatfate authored Mar 14, 2024
1 parent c3016a9 commit 72c8445
Show file tree
Hide file tree
Showing 15 changed files with 612 additions and 68 deletions.
13 changes: 12 additions & 1 deletion AllTests-mainnet.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,17 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
+ validateSyncCommitteeMessage - Duplicate pubkey OK
```
OK: 2/2 Fail: 0/2 Skip: 0/2
## Graffiti management [Beacon Node] [Preset: mainnet]
```diff
+ Configuring the graffiti [Beacon Node] [Preset: mainnet] OK
+ Invalid Authorization Header [Beacon Node] [Preset: mainnet] OK
+ Invalid Authorization Token [Beacon Node] [Preset: mainnet] OK
+ Missing Authorization header [Beacon Node] [Preset: mainnet] OK
+ Obtaining the graffiti of a missing validator returns 404 [Beacon Node] [Preset: mainnet] OK
+ Obtaining the graffiti of an unconfigured validator returns the suggested default [Beacon OK
+ Setting the graffiti on a missing validator creates a record for it [Beacon Node] [Preset: OK
```
OK: 7/7 Fail: 0/7 Skip: 0/7
## Honest validator
```diff
+ General pubsub topics OK
Expand Down Expand Up @@ -1006,4 +1017,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2
OK: 9/9 Fail: 0/9 Skip: 0/9

---TOTAL---
OK: 675/680 Fail: 0/680 Skip: 5/680
OK: 682/687 Fail: 0/687 Skip: 5/687
6 changes: 6 additions & 0 deletions beacon_chain/conf.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,12 @@ func defaultFeeRecipient*(conf: AnyConf): Opt[Eth1Address] =
# https://github.com/nim-lang/Nim/issues/19802
(static(Opt.none Eth1Address))

func defaultGraffitiBytes*(conf: AnyConf): GraffitiBytes =
if conf.graffiti.isSome:
conf.graffiti.get
else:
defaultGraffitiBytes()

proc loadJwtSecret(
rng: var HmacDrbgContext,
dataDir: string,
Expand Down
8 changes: 7 additions & 1 deletion beacon_chain/consensus_object_pools/common_tools.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ from ../spec/eth2_apis/dynamic_fee_recipients import
DynamicFeeRecipientsStore, getDynamicFeeRecipient
from ../validators/keystore_management import
getPerValidatorDefaultFeeRecipient, getSuggestedGasLimit,
getSuggestedFeeRecipient
getSuggestedFeeRecipient, getSuggestedGraffiti
from ../spec/beaconstate import has_eth1_withdrawal_credential
from ../spec/presets import Eth1Address

Expand Down Expand Up @@ -64,3 +64,9 @@ proc getGasLimit*(configValidatorsDir: string,
pubkey: ValidatorPubKey): uint64 =
getSuggestedGasLimit(configValidatorsDir, pubkey, configGasLimit).valueOr:
configGasLimit

proc getGraffiti*(configValidatorsDir: string,
configGraffiti: GraffitiBytes,
pubkey: ValidatorPubKey): GraffitiBytes =
getSuggestedGraffiti(configValidatorsDir, pubkey, configGraffiti).valueOr:
configGraffiti
1 change: 1 addition & 0 deletions beacon_chain/nimbus_beacon_node.nim
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,7 @@ proc init*(T: type BeaconNode,
config.secretsDir,
config.defaultFeeRecipient,
config.suggestedGasLimit,
config.defaultGraffitiBytes,
config.getPayloadBuilderAddress,
getValidatorAndIdx,
getBeaconTime,
Expand Down
1 change: 1 addition & 0 deletions beacon_chain/nimbus_validator_client.nim
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ proc asyncInit(vc: ValidatorClientRef): Future[ValidatorClientRef] {.async.} =
vc.config.secretsDir,
vc.config.defaultFeeRecipient,
vc.config.suggestedGasLimit,
vc.config.defaultGraffitiBytes,
Opt.none(string),
nil,
vc.beaconClock.getBeaconTimeFn,
Expand Down
6 changes: 6 additions & 0 deletions beacon_chain/rpc/rest_constants.nim
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ const
"Bad request. Request was malformed and could not be processed"
InvalidGasLimitRequestError* =
"Bad request. Request was malformed and could not be processed"
InvalidGraffitiRequestError* =
"Bad request. Request was malformed and could not be processed"
VoluntaryExitValidationError* =
"Invalid voluntary exit, it will never pass validation so it's rejected"
VoluntaryExitValidationSuccess* =
Expand Down Expand Up @@ -253,3 +255,7 @@ const
"Invalid blob index"
InvalidBroadcastValidationType* =
"Invalid broadcast_validation type value"
PathNotFoundError* =
"Path not found"
FileReadError* =
"Error reading file"
131 changes: 110 additions & 21 deletions beacon_chain/rpc/rest_key_management_api.nim
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
# please keep imports clear of `rest_utils` or any other module which imports
# beacon node's specific networking code.

import std/[tables, strutils, uri]
import std/[tables, strutils, uri,]
import chronos, chronicles, confutils,
results, stew/[base10, io2], blscurve, presto
import ".."/spec/[keystore, crypto]
Expand Down Expand Up @@ -375,10 +375,12 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
ethaddress: ethaddress.get))
else:
case ethaddress.error
of noConfigFile:
keymanagerApiError(Http404, PathNotFoundError)
of noSuchValidator:
keymanagerApiError(Http404, "No matching validator found")
keymanagerApiError(Http404, ValidatorNotFoundError)
of malformedConfigFile:
keymanagerApiError(Http500, "Error reading fee recipient file")
keymanagerApiError(Http500, FileReadError)

# https://ethereum.github.io/keymanager-APIs/#/Fee%20Recipient/SetFeeRecipient
router.api2(MethodPost, "/eth/v1/validator/{pubkey}/feerecipient") do (
Expand All @@ -402,7 +404,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
status = host.setFeeRecipient(pubkey, feeRecipientReq.ethaddress)

if status.isOk:
RestApiResponse.response("", Http202, "text/plain")
RestApiResponse.response(Http202)
else:
keymanagerApiError(
Http500, "Failed to set fee recipient: " & status.error)
Expand All @@ -412,17 +414,22 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
pubkey: ValidatorPubKey) -> RestApiResponse:
let authStatus = checkAuthorization(request, host)
if authStatus.isErr():
return authErrorResponse authStatus.error
let
pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)
res = host.removeFeeRecipientFile(pubkey)
return keymanagerApiError(Http401, InvalidAuthorizationError)

let pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)

if not(host.checkValidatorKeystoreDir(pubkey)):
return keymanagerApiError(Http404, ValidatorNotFoundError)
if not(host.checkConfigFile(ConfigFileKind.FeeRecipientFile, pubkey)):
return keymanagerApiError(Http404, PathNotFoundError)

let res = host.removeFeeRecipientFile(pubkey)
if res.isOk:
RestApiResponse.response("", Http204, "text/plain")
RestApiResponse.response(Http204)
else:
keymanagerApiError(
Http500, "Failed to remove fee recipient file: " & res.error)
Http403, "Failed to remove fee recipient file: " & res.error)

# https://ethereum.github.io/keymanager-APIs/#/Gas%20Limit/getGasLimit
router.api2(MethodGet, "/eth/v1/validator/{pubkey}/gas_limit") do (
Expand All @@ -442,10 +449,12 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
gas_limit: gasLimit.get))
else:
case gasLimit.error
of noConfigFile:
keymanagerApiError(Http404, PathNotFoundError)
of noSuchValidator:
keymanagerApiError(Http404, "No matching validator found")
keymanagerApiError(Http404, ValidatorNotFoundError)
of malformedConfigFile:
keymanagerApiError(Http500, "Error reading gas limit file")
keymanagerApiError(Http500, FileReadError)

# https://ethereum.github.io/keymanager-APIs/#/Gas%20Limit/setGasLimit
router.api2(MethodPost, "/eth/v1/validator/{pubkey}/gas_limit") do (
Expand All @@ -469,7 +478,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
status = host.setGasLimit(pubkey, gasLimitReq.gas_limit)

if status.isOk:
RestApiResponse.response("", Http202, "text/plain")
RestApiResponse.response(Http202)
else:
keymanagerApiError(
Http500, "Failed to set gas limit: " & status.error)
Expand All @@ -479,17 +488,22 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
pubkey: ValidatorPubKey) -> RestApiResponse:
let authStatus = checkAuthorization(request, host)
if authStatus.isErr():
return authErrorResponse authStatus.error
let
pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)
res = host.removeGasLimitFile(pubkey)
return keymanagerApiError(Http401, InvalidAuthorizationError)

let pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)

if not(host.checkValidatorKeystoreDir(pubkey)):
return keymanagerApiError(Http404, ValidatorNotFoundError)
if not(host.checkConfigFile(ConfigFileKind.GasLimitFile, pubkey)):
return keymanagerApiError(Http404, PathNotFoundError)

let res = host.removeGasLimitFile(pubkey)
if res.isOk:
RestApiResponse.response("", Http204, "text/plain")
RestApiResponse.response(Http204)
else:
keymanagerApiError(
Http500, "Failed to remove gas limit file: " & res.error)
Http403, "Failed to remove gas limit file: " & res.error)

# TODO: These URLs will be changed once we submit a proposal for
# /eth/v2/remotekeys that supports distributed keys.
Expand Down Expand Up @@ -609,3 +623,78 @@ proc installKeymanagerHandlers*(router: var RestRouter, host: KeymanagerHost) =
signature: signature
)
RestApiResponse.jsonResponse(response)

# https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/getGraffiti
router.api2(MethodGet, "/eth/v1/validator/{pubkey}/graffiti") do (
pubkey: ValidatorPubKey) -> RestApiResponse:
let authStatus = checkAuthorization(request, host)
if authStatus.isErr():
return authErrorResponse authStatus.error

let
pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)
graffiti = host.getSuggestedGraffiti(pubkey)

if graffiti.isOk:
RestApiResponse.jsonResponse(
GraffitiResponse(pubkey: pubkey,
graffiti: GraffitiString.init(graffiti.get)))
else:
case graffiti.error
of noConfigFile:
keymanagerApiError(Http404, PathNotFoundError)
of noSuchValidator:
keymanagerApiError(Http404, ValidatorNotFoundError)
of malformedConfigFile:
keymanagerApiError(Http500, FileReadError)

# https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/setGraffiti
router.api2(MethodPost, "/eth/v1/validator/{pubkey}/graffiti") do (
pubkey: ValidatorPubKey,
contentBody: Option[ContentBody]) -> RestApiResponse:
let authStatus = checkAuthorization(request, host)
if authStatus.isErr():
return authErrorResponse authStatus.error

let
pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)
req =
block:
if contentBody.isNone():
return keymanagerApiError(Http400, InvalidGraffitiRequestError)
decodeBody(SetGraffitiRequest, contentBody.get()).valueOr:
return keymanagerApiError(Http400, InvalidGraffitiRequestError)

if not(host.checkValidatorKeystoreDir(pubkey)):
return keymanagerApiError(Http404, ValidatorNotFoundError)

let status = host.setGraffiti(pubkey, GraffitiBytes.init(req.graffiti))
if status.isOk:
RestApiResponse.response(Http202)
else:
keymanagerApiError(
Http500, "Failed to set graffiti: " & status.error)

# https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/deleteGraffiti
router.api2(MethodDelete, "/eth/v1/validator/{pubkey}/graffiti") do (
pubkey: ValidatorPubKey) -> RestApiResponse:
let authStatus = checkAuthorization(request, host)
if authStatus.isErr():
return keymanagerApiError(Http401, InvalidAuthorizationError)

let pubkey = pubkey.valueOr:
return keymanagerApiError(Http400, InvalidValidatorPublicKey)

if not(host.checkValidatorKeystoreDir(pubkey)):
return keymanagerApiError(Http404, ValidatorNotFoundError)
if not(host.checkConfigFile(ConfigFileKind.GraffitiFile, pubkey)):
return keymanagerApiError(Http404, PathNotFoundError)

let res = host.removeGraffitiFile(pubkey)
if res.isOk:
RestApiResponse.response(Http204)
else:
keymanagerApiError(
Http403, "Failed to remove grafiti file: " & res.error)
23 changes: 20 additions & 3 deletions beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ from ".."/datatypes/deneb import BeaconState
export
eth2_ssz_serialization, results, peerid, common, serialization, chronicles,
json_serialization, net, sets, rest_types, slashing_protection_common,
jsonSerializationResults
jsonSerializationResults, rest_keymanager_types

from web3/primitives import BlockHash
export primitives.BlockHash
Expand Down Expand Up @@ -109,6 +109,8 @@ RestJson.useDefaultSerializationFor(
KeystoreInfo,
ListFeeRecipientResponse,
ListGasLimitResponse,
GetGraffitiResponse,
GraffitiResponse,
PendingAttestation,
PostKeystoresResponse,
PrepareBeaconProposer,
Expand Down Expand Up @@ -161,6 +163,7 @@ RestJson.useDefaultSerializationFor(
SPDIR_Validator,
SetFeeRecipientRequest,
SetGasLimitRequest,
SetGraffitiRequest,
SignedAggregateAndProof,
SignedBLSToExecutionChange,
SignedBeaconBlockHeader,
Expand Down Expand Up @@ -314,7 +317,8 @@ type
SignedValidatorRegistrationV1 |
SignedVoluntaryExit |
Web3SignerRequest |
RestNimbusTimestamp1
RestNimbusTimestamp1 |
SetGraffitiRequest

EncodeOctetTypes* =
altair.SignedBeaconBlock |
Expand Down Expand Up @@ -367,7 +371,8 @@ type
SomeForkedLightClientObject |
seq[SomeForkedLightClientObject] |
RestNimbusTimestamp1 |
RestNimbusTimestamp2
RestNimbusTimestamp2 |
GetGraffitiResponse

DecodeConsensysTypes* =
ProduceBlockResponseV2 | ProduceBlindedBlockResponse
Expand Down Expand Up @@ -3407,6 +3412,18 @@ proc parseRoot(value: string): Result[Eth2Digest, cstring] =
except ValueError:
err("Unable to decode root value")

## GraffitiString
proc writeValue*(writer: var JsonWriter[RestJson], value: GraffitiString) {.
raises: [IOError].} =
writeValue(writer, $value)

proc readValue*(reader: var JsonReader[RestJson], T: type GraffitiString): T {.
raises: [IOError, SerializationError].} =
let res = init(GraffitiString, reader.readValue(string))
if res.isErr():
reader.raiseUnexpectedValue res.error
res.get

proc decodeBody*(
t: typedesc[RestPublishedSignedBeaconBlock],
body: ContentBody,
Expand Down
16 changes: 16 additions & 0 deletions beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,22 @@ proc deleteGasLimitPlain *(pubkey: ValidatorPubKey,
meth: MethodDelete.}
## https://ethereum.github.io/keymanager-APIs/#/Gas%20Limit/deleteGasLimit

proc getGraffitiPlain*(pubkey: ValidatorPubKey): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/{pubkey}/graffiti",
meth: MethodGet.}
## https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/getGraffiti

proc setGraffitiPlain*(pubkey: ValidatorPubKey,
body: SetGraffitiRequest): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/{pubkey}/graffiti",
meth: MethodPost.}
## https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/setGraffiti

proc deleteGraffitiPlain*(pubkey: ValidatorPubKey): RestPlainResponse {.
rest, endpoint: "/eth/v1/validator/{pubkey}/graffiti",
meth: MethodDelete.}
## https://ethereum.github.io/keymanager-APIs/?urls.primaryName=dev#/Graffiti/deleteGraffiti

proc listRemoteDistributedKeysPlain*(): RestPlainResponse {.
rest, endpoint: "/eth/v1/remotekeys/distributed",
meth: MethodGet.}
Expand Down
Loading

0 comments on commit 72c8445

Please sign in to comment.