Add Keymanager API graffiti endpoints. (#6054)

* 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 c735f855d3cb9c4a1c8e8af29d3f4438d068e31f.

* Workaround {.push raises.} requirement.

* Fix comment.

* Update Alltests.
This commit is contained in:
Eugene Kabanov 2024-03-14 05:44:00 +02:00 committed by GitHub
parent c3016a9bc5
commit 72c844534f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 611 additions and 67 deletions

View File

@ -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
@ -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

View File

@ -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,

View File

@ -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
@ -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

View File

@ -823,6 +823,7 @@ proc init*(T: type BeaconNode,
config.secretsDir,
config.defaultFeeRecipient,
config.suggestedGasLimit,
config.defaultGraffitiBytes,
config.getPayloadBuilderAddress,
getValidatorAndIdx,
getBeaconTime,

View File

@ -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,

View File

@ -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* =
@ -253,3 +255,7 @@ const
"Invalid blob index"
InvalidBroadcastValidationType* =
"Invalid broadcast_validation type value"
PathNotFoundError* =
"Path not found"
FileReadError* =
"Error reading file"

View File

@ -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]
@ -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 (
@ -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)
@ -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 (
@ -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 (
@ -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)
@ -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.
@ -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)

View File

@ -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
@ -109,6 +109,8 @@ RestJson.useDefaultSerializationFor(
KeystoreInfo,
ListFeeRecipientResponse,
ListGasLimitResponse,
GetGraffitiResponse,
GraffitiResponse,
PendingAttestation,
PostKeystoresResponse,
PrepareBeaconProposer,
@ -161,6 +163,7 @@ RestJson.useDefaultSerializationFor(
SPDIR_Validator,
SetFeeRecipientRequest,
SetGasLimitRequest,
SetGraffitiRequest,
SignedAggregateAndProof,
SignedBLSToExecutionChange,
SignedBeaconBlockHeader,
@ -314,7 +317,8 @@ type
SignedValidatorRegistrationV1 |
SignedVoluntaryExit |
Web3SignerRequest |
RestNimbusTimestamp1
RestNimbusTimestamp1 |
SetGraffitiRequest
EncodeOctetTypes* =
altair.SignedBeaconBlock |
@ -367,7 +371,8 @@ type
SomeForkedLightClientObject |
seq[SomeForkedLightClientObject] |
RestNimbusTimestamp1 |
RestNimbusTimestamp2
RestNimbusTimestamp2 |
GetGraffitiResponse
DecodeConsensysTypes* =
ProduceBlockResponseV2 | ProduceBlindedBlockResponse
@ -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,

View File

@ -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.}

View File

@ -5,7 +5,11 @@
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.push raises: [].}
import
std/typetraits,
stew/byteutils,
".."/[crypto, keystore],
../../validators/slashing_protection_common
@ -99,9 +103,48 @@ type
KeymanagerGenericError* = object
message*: string
GraffitiString* = distinct array[MAX_GRAFFITI_SIZE, byte]
GraffitiResponse* = object
pubkey*: ValidatorPubKey
graffiti*: GraffitiString
GetGraffitiResponse* = object
data*: GraffitiResponse
SetGraffitiRequest* = object
graffiti*: GraffitiString
proc `<`*(x, y: KeystoreInfo | RemoteKeystoreInfo): bool =
for a, b in fields(x, y):
let c = cmp(a, b)
if c < 0: return true
if c > 0: return false
return false
func init*(T: type GraffitiString,
input: string): Result[GraffitiString, string] =
var res: GraffitiString
if len(input) > MAX_GRAFFITI_SIZE:
return err("The graffiti value should be 32 characters or less")
distinctBase(res)[0 ..< len(input)] = toBytes(input)
ok(res)
func init*(T: type GraffitiBytes, input: GraffitiString): GraffitiBytes =
var res: GraffitiBytes
distinctBase(res)[0 ..< MAX_GRAFFITI_SIZE] =
distinctBase(input)[0 ..< MAX_GRAFFITI_SIZE]
res
func init*(T: type GraffitiString, input: GraffitiBytes): GraffitiString =
var res: GraffitiString
distinctBase(res)[0 ..< MAX_GRAFFITI_SIZE] =
distinctBase(input)[0 ..< MAX_GRAFFITI_SIZE]
res
func `$`*(input: GraffitiString): string =
var res = newStringOfCap(MAX_GRAFFITI_SIZE)
for ch in distinctBase(input):
if ch != byte(0):
res.add(char(ch))
res

View File

@ -411,11 +411,7 @@ proc publishBlockV2(vc: ValidatorClientRef, currentSlot, slot: Slot,
validator: AttachedValidator) {.async.} =
let
genesisRoot = vc.beaconGenesis.genesis_validators_root
graffiti =
if vc.config.graffiti.isSome():
vc.config.graffiti.get()
else:
defaultGraffitiBytes()
graffiti = vc.getGraffitiBytes(validator)
vindex = validator.index.get()
logScope:
@ -633,11 +629,7 @@ proc publishBlock(vc: ValidatorClientRef, currentSlot, slot: Slot,
validator: AttachedValidator) {.async.} =
let
genesisRoot = vc.beaconGenesis.genesis_validators_root
graffiti =
if vc.config.graffiti.isSome():
vc.config.graffiti.get()
else:
defaultGraffitiBytes()
graffiti = vc.getGraffitiBytes(validator)
fork = vc.forkAtEpoch(slot.epoch)
vindex = validator.index.get()

View File

@ -1513,3 +1513,8 @@ proc `+`*(slot: Slot, epochs: Epoch): Slot =
func finish_slot*(epoch: Epoch): Slot =
## Return the last slot of ``epoch``.
Slot((epoch + 1).start_slot() - 1)
proc getGraffitiBytes*(vc: ValidatorClientRef,
validator: AttachedValidator): GraffitiBytes =
getGraffiti(vc.config.validatorsDir, vc.config.defaultGraffitiBytes(),
validator.pubkey)

View File

@ -33,7 +33,8 @@ import
validator],
../consensus_object_pools/[
spec_cache, blockchain_dag, block_clearance, attestation_pool,
sync_committee_msg_pool, validator_change_pool, consensus_manager],
sync_committee_msg_pool, validator_change_pool, consensus_manager,
common_tools],
../el/el_manager,
../networking/eth2_network,
../sszdump, ../sync/sync_manager,
@ -221,6 +222,11 @@ proc getValidatorForDuties*(
node.attachedValidators[].getValidatorForDuties(
key.toPubKey(), slot, slashingSafe)
proc getGraffitiBytes*(
node: BeaconNode, validator: AttachedValidator): GraffitiBytes =
getGraffiti(node.config.validatorsDir, node.config.defaultGraffitiBytes(),
validator.pubkey)
proc isSynced*(node: BeaconNode, head: BlockRef): bool =
## TODO This function is here as a placeholder for some better heurestics to
## determine if we're in sync and should be producing blocks and
@ -797,7 +803,7 @@ proc getBlindedBlockParts[
proc getBuilderBid[SBBB: deneb_mev.SignedBlindedBeaconBlock](
node: BeaconNode, payloadBuilderClient: RestClientRef, head: BlockRef,
validator_pubkey: ValidatorPubKey, slot: Slot, randao: ValidatorSig,
validator_index: ValidatorIndex):
graffitiBytes: GraffitiBytes, validator_index: ValidatorIndex):
Future[BlindedBlockResult[SBBB]] {.async: (raises: [CancelledError]).} =
## Returns the unsigned blinded block obtained from the Builder API.
## Used by the BN's own validators, but not the REST server
@ -808,7 +814,7 @@ proc getBuilderBid[SBBB: deneb_mev.SignedBlindedBeaconBlock](
let blindedBlockParts = await getBlindedBlockParts[EPH](
node, payloadBuilderClient, head, validator_pubkey, slot, randao,
validator_index, node.graffitiBytes)
validator_index, graffitiBytes)
if blindedBlockParts.isErr:
# Not signed yet, fine to try to fall back on EL
beacon_block_builder_missed_with_fallback.inc()
@ -949,7 +955,8 @@ proc collectBids(
if usePayloadBuilder:
when not (EPS is bellatrix.ExecutionPayloadForSigning):
getBuilderBid[SBBB](node, payloadBuilderClient, head,
validator_pubkey, slot, randao, validator_index)
validator_pubkey, slot, randao, graffitiBytes,
validator_index)
else:
let fut = newFuture[BlindedBlockResult[SBBB]]("builder-bid")
fut.complete(BlindedBlockResult[SBBB].err(
@ -1027,12 +1034,13 @@ proc proposeBlockAux(
genesis_validators_root: Eth2Digest,
localBlockValueBoost: uint8): Future[BlockRef] {.async: (raises: [CancelledError]).} =
let
graffitiBytes = node.getGraffitiBytes(validator)
payloadBuilderClient =
node.getPayloadBuilderClient(validator_index.distinctBase).valueOr(nil)
collectedBids = await collectBids(
SBBB, EPS, node, payloadBuilderClient, validator.pubkey, validator_index,
node.graffitiBytes, head, slot, randao)
graffitiBytes, head, slot, randao)
useBuilderBlock =
if collectedBids.builderBid.isSome():

View File

@ -15,7 +15,7 @@ import
nimbus_security_resources,
".."/spec/[eth2_merkleization, keystore, crypto],
".."/spec/datatypes/base,
stew/io2, libp2p/crypto/crypto as lcrypto,
stew/[io2, byteutils], libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
".."/[conf, filepath, beacon_clock],
".."/networking/network_metadata,
@ -40,6 +40,7 @@ const
RemoteKeystoreFileName* = "remote_keystore.json"
FeeRecipientFilename = "suggested_fee_recipient.hex"
GasLimitFilename = "suggested_gas_limit.json"
GraffitiBytesFilename = "graffiti.hex"
BuilderConfigPath = "payload_builder.json"
KeyNameSize = 98 # 0x + hexadecimal key representation 96 characters.
MaxKeystoreFileSize = 65536
@ -91,6 +92,7 @@ type
secretsDir*: string
defaultFeeRecipient*: Opt[Eth1Address]
defaultGasLimit*: uint64
defaultGraffiti*: GraffitiBytes
defaultBuilderAddress*: Opt[string]
getValidatorAndIdxFn*: ValidatorPubKeyToDataFn
getBeaconTimeFn*: GetBeaconTimeFn
@ -104,6 +106,10 @@ type
QueryResult = Result[seq[KeystoreData], string]
ConfigFileKind* {.pure.} = enum
KeystoreFile, RemoteKeystoreFile, FeeRecipientFile, GasLimitFile,
BuilderConfigFile, GraffitiFile
const
minPasswordLen = 12
minPasswordEntropy = 60.0
@ -125,6 +131,7 @@ func init*(T: type KeymanagerHost,
secretsDir: string,
defaultFeeRecipient: Opt[Eth1Address],
defaultGasLimit: uint64,
defaultGraffiti: GraffitiBytes,
defaultBuilderAddress: Opt[string],
getValidatorAndIdxFn: ValidatorPubKeyToDataFn,
getBeaconTimeFn: GetBeaconTimeFn,
@ -140,6 +147,7 @@ func init*(T: type KeymanagerHost,
secretsDir: secretsDir,
defaultFeeRecipient: defaultFeeRecipient,
defaultGasLimit: defaultGasLimit,
defaultGraffiti: defaultGraffiti,
defaultBuilderAddress: defaultBuilderAddress,
getValidatorAndIdxFn: getValidatorAndIdxFn,
getBeaconTimeFn: getBeaconTimeFn,
@ -805,23 +813,36 @@ iterator listLoadableKeystores*(config: AnyConf,
type
ValidatorConfigFileStatus* = enum
noSuchValidator
noConfigFile
malformedConfigFile
func validatorKeystoreDir(
validatorsDir: string, pubkey: ValidatorPubKey): string =
validatorsDir / pubkey.fsName
func feeRecipientPath(validatorsDir: string,
pubkey: ValidatorPubKey): string =
validatorsDir.validatorKeystoreDir(pubkey) / FeeRecipientFilename
proc checkValidatorKeystoreDir(validatorsDir: string,
pubkey: ValidatorPubKey): bool =
dirExists(validatorsDir.validatorKeystoreDir(pubkey))
func gasLimitPath(validatorsDir: string,
pubkey: ValidatorPubKey): string =
validatorsDir.validatorKeystoreDir(pubkey) / GasLimitFilename
func configFilePath*(validatorsDir: string, kind: ConfigFileKind,
pubkey: ValidatorPubKey): string =
case kind
of ConfigFileKind.KeystoreFile:
validatorsDir.validatorKeystoreDir(pubkey) / KeystoreFileName
of ConfigFileKind.RemoteKeystoreFile:
validatorsDir.validatorKeystoreDir(pubkey) / RemoteKeystoreFileName
of ConfigFileKind.FeeRecipientFile:
validatorsDir.validatorKeystoreDir(pubkey) / FeeRecipientFilename
of ConfigFileKind.GasLimitFile:
validatorsDir.validatorKeystoreDir(pubkey) / GasLimitFilename
of ConfigFileKind.BuilderConfigFile:
validatorsDir.validatorKeystoreDir(pubkey) / BuilderConfigPath
of ConfigFileKind.GraffitiFile:
validatorsDir.validatorKeystoreDir(pubkey) / GraffitiBytesFilename
func builderConfigPath(validatorsDir: string,
pubkey: ValidatorPubKey): string =
validatorsDir.validatorKeystoreDir(pubkey) / BuilderConfigPath
proc checkConfigFile*(validatorsDir: string, kind: ConfigFileKind,
pubkey: ValidatorPubKey): bool =
fileExists(validatorsDir.configFilePath(kind, pubkey))
proc getSuggestedFeeRecipient*(
validatorsDir: string, pubkey: ValidatorPubKey,
@ -833,7 +854,8 @@ proc getSuggestedFeeRecipient*(
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
return err noSuchValidator
let feeRecipientPath = validatorsDir.feeRecipientPath(pubkey)
let feeRecipientPath =
validatorsDir.configFilePath(ConfigFileKind.FeeRecipientFile, pubkey)
if not fileExists(feeRecipientPath):
return ok defaultFeeRecipient
@ -861,7 +883,8 @@ proc getSuggestedGasLimit*(
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
return err noSuchValidator
let gasLimitPath = validatorsDir.gasLimitPath(pubkey)
let gasLimitPath =
validatorsDir.configFilePath(ConfigFileKind.GasLimitFile, pubkey)
if not fileExists(gasLimitPath):
return ok defaultGasLimit
try:
@ -877,6 +900,34 @@ proc getSuggestedGasLimit*(
err = exc.msg
err malformedConfigFile
proc getSuggestedGraffiti*(
validatorsDir: string,
pubkey: ValidatorPubKey,
defaultGraffitiBytes: GraffitiBytes
): Result[GraffitiBytes, ValidatorConfigFileStatus] =
# In this particular case, an error might be by design. If the file exists,
# but doesn't load or parse that is more urgent. People might prefer not to
# override their default suggested gas limit per validator, so don't warn.
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
return err noSuchValidator
let graffitiPath =
validatorsDir.configFilePath(ConfigFileKind.GraffitiFile, pubkey)
if not fileExists(graffitiPath):
return ok defaultGraffitiBytes
let data = readAllChars(graffitiPath).valueOr:
warn "Failed to load graffiti file; falling back to default graffiti",
reason = ioErrorMsg(error), error = int(error)
return err malformedConfigFile
try:
ok GraffitiBytes.init(data)
except ValueError as exc:
warn "Invalid local graffiti file", graffitiPath,
reason = exc.msg
return err malformedConfigFile
type
BuilderConfig = object
payloadBuilderEnable: bool
@ -892,7 +943,8 @@ proc getBuilderConfig*(
if not dirExists(validatorsDir.validatorKeystoreDir(pubkey)):
return err noSuchValidator
let builderConfigPath = validatorsDir.builderConfigPath(pubkey)
let builderConfigPath =
validatorsDir.configFilePath(ConfigFileKind.BuilderConfigFile, pubkey)
if not fileExists(builderConfigPath):
return ok defaultBuilderAddress
@ -1403,33 +1455,49 @@ func validatorKeystoreDir(host: KeymanagerHost,
pubkey: ValidatorPubKey): string =
host.validatorsDir.validatorKeystoreDir(pubkey)
proc checkValidatorKeystoreDir*(host: KeymanagerHost,
pubkey: ValidatorPubKey): bool =
host.validatorsDir.checkValidatorKeystoreDir(pubkey)
proc checkConfigFile*(host: KeymanagerHost, kind: ConfigFileKind,
pubkey: ValidatorPubKey): bool =
fileExists(host.validatorsDir.configFilePath(kind, pubkey))
func feeRecipientPath(host: KeymanagerHost,
pubkey: ValidatorPubKey): string =
host.validatorsDir.feeRecipientPath(pubkey)
host.validatorsDir.configFilePath(ConfigFileKind.FeeRecipientFile, pubkey)
func gasLimitPath(host: KeymanagerHost,
pubkey: ValidatorPubKey): string =
host.validatorsDir.gasLimitPath(pubkey)
host.validatorsDir.configFilePath(ConfigFileKind.GasLimitFile, pubkey)
func graffitiPath(host: KeymanagerHost,
pubkey: ValidatorPubKey): string =
host.validatorsDir.configFilePath(ConfigFileKind.GraffitiFile, pubkey)
proc removeFeeRecipientFile*(host: KeymanagerHost,
pubkey: ValidatorPubKey): Result[void, string] =
let path = host.feeRecipientPath(pubkey)
if fileExists(path):
let res = io2.removeFile(path)
if res.isErr:
return err res.error.ioErrorMsg
return ok()
io2.removeFile(path).isOkOr:
return err($uint(error) & " " & ioErrorMsg(error))
ok()
proc removeGasLimitFile*(host: KeymanagerHost,
pubkey: ValidatorPubKey): Result[void, string] =
let path = host.gasLimitPath(pubkey)
if fileExists(path):
let res = io2.removeFile(path)
if res.isErr:
return err res.error.ioErrorMsg
io2.removeFile(path).isOkOr:
return err($uint(error) & " " & ioErrorMsg(error))
ok()
return ok()
proc removeGraffitiFile*(host: KeymanagerHost,
pubkey: ValidatorPubKey): Result[void, string] =
let path = host.graffitiPath(pubkey)
if fileExists(path):
io2.removeFile(path).isOkOr:
return err($uint(error) & " " & ioErrorMsg(error))
ok()
proc setFeeRecipient*(host: KeymanagerHost, pubkey: ValidatorPubKey, feeRecipient: Eth1Address): Result[void, string] =
let validatorKeystoreDir = host.validatorKeystoreDir(pubkey)
@ -1451,6 +1519,23 @@ proc setGasLimit*(host: KeymanagerHost,
io2.writeFile(validatorKeystoreDir / GasLimitFilename, $gasLimit)
.mapErr(proc(e: auto): string = "Failed to write gas limit file: " & $e)
proc setGraffiti*(host: KeymanagerHost,
pubkey: ValidatorPubKey,
graffiti: GraffitiBytes): Result[void, string] =
let
validatorKeystoreDir = host.validatorKeystoreDir(pubkey)
path = host.graffitiPath(pubkey)
? secureCreatePath(validatorKeystoreDir)
.mapErr(proc(e: auto): string =
"Could not create wallet directory [" & validatorKeystoreDir & "], " &
"reason: (" & $int(e) & ") " & ioErrorMsg(e))
io2.writeFile(path, to0xHex(distinctBase(graffiti)))
.mapErr(proc(e: auto): string =
"Failed to write graffiti file," &
" reason: (" & $int(e) & ") " & ioErrorMsg(e))
from ".."/spec/beaconstate import has_eth1_withdrawal_credential
proc getValidatorWithdrawalAddress*(
@ -1499,6 +1584,12 @@ proc getSuggestedGasLimit*(
pubkey: ValidatorPubKey): Result[uint64, ValidatorConfigFileStatus] =
host.validatorsDir.getSuggestedGasLimit(pubkey, host.defaultGasLimit)
proc getSuggestedGraffiti*(
host: KeymanagerHost,
pubkey: ValidatorPubKey
): Result[GraffitiBytes, ValidatorConfigFileStatus] =
host.validatorsDir.getSuggestedGraffiti(pubkey, host.defaultGraffiti)
proc getBuilderConfig*(
host: KeymanagerHost, pubkey: ValidatorPubKey):
Result[Opt[string], ValidatorConfigFileStatus] =

View File

@ -6,6 +6,9 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
{.used.}
{.push raises: [].}
# TODO (cheatfate): This test is going to be rewritten from scratch.
{.pop.}
import
std/[typetraits, os, options, json, sequtils, uri, algorithm],
@ -1044,7 +1047,7 @@ proc runTests(keymanager: KeymanagerToTest) {.async.} =
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 403
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
asyncTest "Obtaining the fee recipient of a missing validator returns 404" & testFlavour:
@ -1206,7 +1209,7 @@ proc runTests(keymanager: KeymanagerToTest) {.async.} =
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 403
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
asyncTest "Obtaining the gas limit of a missing validator returns 404" & testFlavour:
@ -1261,6 +1264,255 @@ proc runTests(keymanager: KeymanagerToTest) {.async.} =
check:
finalResultFromApi == defaultGasLimit
suite "Graffiti management" & testFlavour:
asyncTest "Missing Authorization header" & testFlavour:
let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key")
block:
let
response = await client.getGraffitiPlain(pubkey)
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.setGraffitiPlain(
pubkey,
default SetGraffitiRequest)
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.deleteGraffitiPlain(pubkey)
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
asyncTest "Invalid Authorization Header" & testFlavour:
let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key")
block:
let
response = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "UnknownAuthScheme X")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.setGraffitiPlain(
pubkey,
default SetGraffitiRequest,
extraHeaders = @[("Authorization", "UnknownAuthScheme X")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.deleteGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "UnknownAuthScheme X")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
asyncTest "Invalid Authorization Token" & testFlavour:
let pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key")
block:
let
response = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer InvalidToken")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 403
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.setGraffitiPlain(
pubkey,
default SetGraffitiRequest,
extraHeaders = @[("Authorization", "Bearer InvalidToken")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 403
responseJson["message"].getStr() == InvalidAuthorizationError
block:
let
response = await client.deleteGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer InvalidToken")])
responseJson = Json.decode(response.data, JsonNode)
check:
response.status == 401
responseJson["message"].getStr() == InvalidAuthorizationError
asyncTest "Obtaining the graffiti of a missing validator returns 404" &
testFlavour:
let
pubkey =
ValidatorPubKey.fromHex(unusedPublicKeys[0]).expect("valid key")
response = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
response.status == 404
asyncTest "Setting the graffiti on a missing validator creates " &
"a record for it" & testFlavour:
let
pubkey =
ValidatorPubKey.fromHex(unusedPublicKeys[1]).expect("valid key")
graffiti =
SetGraffitiRequest(
graffiti: GraffitiString.init("🚀\"🍻\"🚀").get())
let response =
await client.setGraffitiPlain(pubkey, graffiti,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
response.status == 202
let fromApi =
await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
fromApi.status == 200
let res =
decodeBytes(GetGraffitiResponse, fromApi.data, fromApi.contentType)
check:
res.isOk()
res.get().data.pubkey == pubkey
$res.get().data.graffiti == "🚀\"🍻\"🚀"
asyncTest "Obtaining the graffiti of an unconfigured validator returns " &
"the suggested default" & testFlavour:
let
pubkey = ValidatorPubKey.fromHex(oldPublicKeys[0]).expect("valid key")
fromApi = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
fromApi.status == 200
let res =
decodeBytes(GetGraffitiResponse, fromApi.data, fromApi.contentType)
check:
res.isOk()
res.get().data.pubkey == pubkey
asyncTest "Configuring the graffiti" & testFlavour:
let
pubkey = ValidatorPubKey.fromHex(oldPublicKeys[1]).expect("valid key")
firstGraffiti = "🚀"
secondGraffiti = "🚀🚀"
firstRequest =
SetGraffitiRequest(
graffiti: GraffitiString.init(firstGraffiti).get())
secondRequest =
SetGraffitiRequest(
graffiti: GraffitiString.init(secondGraffiti).get())
block:
let response =
await client.setGraffitiPlain(pubkey, firstRequest,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
response.status == 202
block:
let resApi = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
resApi.status == 200
let res =
decodeBytes(GetGraffitiResponse, resApi.data, resApi.contentType)
check:
res.isOk()
res.get().data.pubkey == pubkey
$res.get().data.graffiti == firstGraffiti
block:
let response =
await client.setGraffitiPlain(pubkey, secondRequest,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
response.status == 202
block:
let resApi = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
resApi.status == 200
let res =
decodeBytes(GetGraffitiResponse, resApi.data, resApi.contentType)
check:
res.isOk()
res.get().data.pubkey == pubkey
$res.get().data.graffiti == secondGraffiti
block:
let response = await client.deleteGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
response.status == 204
block:
let resApi = await client.getGraffitiPlain(
pubkey,
extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)])
check:
resApi.status == 200
let res =
decodeBytes(GetGraffitiResponse, resApi.data, resApi.contentType)
check:
res.isOk()
res.get().data.pubkey == pubkey
suite "ImportRemoteKeys/ListRemoteKeys/DeleteRemoteKeys" & testFlavour:
asyncTest "Importing list of remote keys" & testFlavour:
let