2022-01-05 13:41:39 +00:00
|
|
|
# Copyright (c) 2021-2022 Status Research & Development GmbH
|
2021-10-04 19:08:31 +00:00
|
|
|
# Licensed and distributed under either of
|
|
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
|
|
# * 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.
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
import std/[tables, os, strutils, uri]
|
2021-10-04 19:08:31 +00:00
|
|
|
import chronos, chronicles, confutils,
|
2022-01-05 13:41:39 +00:00
|
|
|
stew/[base10, results, io2], bearssl, blscurve
|
2021-12-22 12:37:31 +00:00
|
|
|
import ".."/validators/slashing_protection
|
2021-10-19 14:09:26 +00:00
|
|
|
import ".."/[conf, version, filepath, beacon_node]
|
2021-10-04 19:08:31 +00:00
|
|
|
import ".."/spec/[keystore, crypto]
|
|
|
|
import ".."/rpc/rest_utils
|
2022-02-07 20:36:09 +00:00
|
|
|
import ".."/validators/[keystore_management, validator_pool, validator_duties]
|
2021-12-22 12:37:31 +00:00
|
|
|
import ".."/spec/eth2_apis/rest_keymanager_types
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
export rest_utils, results
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc listLocalValidators*(node: BeaconNode): seq[KeystoreInfo] {.
|
|
|
|
raises: [Defect].} =
|
2021-12-22 12:37:31 +00:00
|
|
|
var validators: seq[KeystoreInfo]
|
2022-02-07 20:36:09 +00:00
|
|
|
for item in node.attachedValidators[].items():
|
|
|
|
if item.kind == ValidatorKind.Local:
|
|
|
|
validators.add KeystoreInfo(
|
|
|
|
validating_pubkey: item.pubkey,
|
|
|
|
derivation_path: string(item.data.path),
|
|
|
|
readonly: false
|
|
|
|
)
|
|
|
|
validators
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc listRemoteValidators*(node: BeaconNode): seq[RemoteKeystoreInfo] {.
|
|
|
|
raises: [Defect].} =
|
|
|
|
var validators: seq[RemoteKeystoreInfo]
|
|
|
|
for item in node.attachedValidators[].items():
|
|
|
|
if item.kind == ValidatorKind.Remote:
|
|
|
|
validators.add RemoteKeystoreInfo(
|
|
|
|
pubkey: item.pubkey,
|
|
|
|
url: HttpHostUri(item.data.remoteUrl)
|
|
|
|
)
|
2021-12-22 12:37:31 +00:00
|
|
|
validators
|
|
|
|
|
|
|
|
proc checkAuthorization*(request: HttpRequestRef,
|
|
|
|
node: BeaconNode): Result[void, AuthorizationError] =
|
|
|
|
let authorizations = request.headers.getList("authorization")
|
|
|
|
if authorizations.len > 0:
|
|
|
|
for authHeader in authorizations:
|
|
|
|
let parts = authHeader.split(' ', maxsplit = 1)
|
|
|
|
if parts.len == 2 and parts[0] == "Bearer":
|
|
|
|
if parts[1] == node.keymanagerToken.get:
|
|
|
|
return ok()
|
2021-10-04 19:08:31 +00:00
|
|
|
else:
|
2021-12-22 12:37:31 +00:00
|
|
|
return err incorrectToken
|
|
|
|
return err missingBearerScheme
|
2021-10-04 19:08:31 +00:00
|
|
|
else:
|
2021-12-22 12:37:31 +00:00
|
|
|
return err noAuthorizationHeader
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc validateUri*(url: string): Result[Uri, cstring] =
|
|
|
|
let surl = parseUri(url)
|
|
|
|
if surl.scheme notin ["http", "https"]:
|
|
|
|
return err("Incorrect URL scheme")
|
|
|
|
if len(surl.hostname) == 0:
|
|
|
|
return err("Empty URL hostname")
|
|
|
|
ok(surl)
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) =
|
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/ListKeys
|
|
|
|
router.api(MethodGet, "/api/eth/v1/keystores") do () -> RestApiResponse:
|
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
2022-02-07 20:36:09 +00:00
|
|
|
let response = GetKeystoresResponse(data: listLocalValidators(node))
|
2021-12-22 12:37:31 +00:00
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
|
|
|
|
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/ImportKeystores
|
|
|
|
router.api(MethodPost, "/api/eth/v1/keystores") do (
|
|
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
|
|
|
let request =
|
2021-10-04 19:08:31 +00:00
|
|
|
block:
|
|
|
|
if contentBody.isNone():
|
|
|
|
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
|
2021-12-22 12:37:31 +00:00
|
|
|
let dres = decodeBody(KeystoresAndSlashingProtection, contentBody.get())
|
2021-10-04 19:08:31 +00:00
|
|
|
if dres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects,
|
|
|
|
$dres.error())
|
|
|
|
dres.get()
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
if request.slashing_protection.isSome():
|
|
|
|
let slashing_protection = request.slashing_protection.get()
|
|
|
|
let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection)
|
|
|
|
if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest !=
|
|
|
|
slashing_protection.metadata.genesis_validators_root.Eth2Digest:
|
|
|
|
return RestApiResponse.jsonError(Http400,
|
|
|
|
"The slashing protection database and imported file refer to " &
|
|
|
|
"different blockchains.")
|
|
|
|
let res = inclSPDIR(node.attachedValidators.slashingProtection,
|
|
|
|
slashing_protection)
|
|
|
|
if res == siFailure:
|
|
|
|
return RestApiResponse.jsonError(Http500,
|
|
|
|
"Internal server error; Failed to import slashing protection data")
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
var response: PostKeystoresResponse
|
2021-12-22 12:37:31 +00:00
|
|
|
|
|
|
|
for index, item in request.keystores.pairs():
|
|
|
|
let res = importKeystore(node.attachedValidators[], node.network.rng[],
|
|
|
|
node.config, item, request.passwords[index])
|
2021-10-04 19:08:31 +00:00
|
|
|
if res.isErr():
|
2022-02-07 20:36:09 +00:00
|
|
|
let failure = res.error()
|
|
|
|
case failure.status
|
|
|
|
of AddValidatorStatus.failed:
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.error,
|
|
|
|
message: failure.message))
|
|
|
|
of AddValidatorStatus.existingArtifacts:
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.duplicate))
|
2021-12-22 12:37:31 +00:00
|
|
|
else:
|
2022-02-07 20:36:09 +00:00
|
|
|
node.addLocalValidators([res.get()])
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.imported))
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
|
|
|
|
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Keymanager/DeleteKeys
|
|
|
|
router.api(MethodPost, "/api/eth/v1/keystores/delete") do (
|
|
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
2021-10-04 19:08:31 +00:00
|
|
|
let keys =
|
|
|
|
block:
|
|
|
|
if contentBody.isNone():
|
|
|
|
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
|
2021-12-22 12:37:31 +00:00
|
|
|
let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
|
2021-10-04 19:08:31 +00:00
|
|
|
if dres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
|
|
|
|
$dres.error())
|
2021-12-22 12:37:31 +00:00
|
|
|
dres.get().pubkeys
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
var
|
|
|
|
response: DeleteKeystoresResponse
|
|
|
|
nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection)
|
|
|
|
# Hash table to keep the removal status of all keys form request
|
|
|
|
keysAndDeleteStatus = initTable[PubKeyBytes, RequestItemStatus]()
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
response.slashing_protection.metadata = nodeSPDIR.metadata
|
2021-10-04 19:08:31 +00:00
|
|
|
|
|
|
|
for index, key in keys.pairs():
|
2021-12-22 12:37:31 +00:00
|
|
|
let
|
2022-02-07 20:36:09 +00:00
|
|
|
res = removeValidator(node.attachedValidators[], node.config, key,
|
|
|
|
KeystoreKind.Local)
|
2021-12-22 12:37:31 +00:00
|
|
|
pubkey = key.blob.PubKey0x.PubKeyBytes
|
|
|
|
|
|
|
|
if res.isOk:
|
|
|
|
case res.value()
|
|
|
|
of RemoveValidatorStatus.deleted:
|
2022-02-07 20:36:09 +00:00
|
|
|
keysAndDeleteStatus.add(
|
|
|
|
pubkey, RequestItemStatus(status: $KeystoreStatus.deleted))
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# At first all keys with status missing directory after removal receive
|
|
|
|
# status 'not_found'
|
|
|
|
of RemoveValidatorStatus.notFound:
|
|
|
|
keysAndDeleteStatus.add(
|
|
|
|
pubkey, RequestItemStatus(status: $KeystoreStatus.notFound))
|
2021-12-22 12:37:31 +00:00
|
|
|
else:
|
|
|
|
keysAndDeleteStatus.add(pubkey,
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.error,
|
|
|
|
message: $res.error()))
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
# If we discover slashing protection data for a validator that was not
|
|
|
|
# found, this means the validator was active in the past, so we must
|
|
|
|
# respond with `not_active`:
|
|
|
|
for validator in nodeSPDIR.data:
|
|
|
|
keysAndDeleteStatus.withValue(validator.pubkey.PubKeyBytes, value) do:
|
|
|
|
response.slashing_protection.data.add(validator)
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
if value.status == $KeystoreStatus.notFound:
|
|
|
|
value.status = $KeystoreStatus.notActive
|
|
|
|
|
|
|
|
for index, key in keys.pairs():
|
|
|
|
response.data.add(keysAndDeleteStatus[key.blob.PubKey0x.PubKeyBytes])
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
2021-10-04 19:08:31 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ListRemoteKeys
|
2022-03-02 15:43:52 +00:00
|
|
|
router.api(MethodGet, "/api/eth/v1/remotekeys") do () -> RestApiResponse:
|
2022-02-07 20:36:09 +00:00
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
|
|
|
let response = GetRemoteKeystoresResponse(data: listRemoteValidators(node))
|
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
|
|
|
|
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ImportRemoteKeys
|
2022-03-02 15:43:52 +00:00
|
|
|
router.api(MethodPost, "/api/eth/v1/remotekeys") do (
|
2022-02-07 20:36:09 +00:00
|
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
|
|
|
let keys =
|
|
|
|
block:
|
|
|
|
if contentBody.isNone():
|
|
|
|
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
|
|
|
|
let dres = decodeBody(ImportRemoteKeystoresBody, contentBody.get())
|
|
|
|
if dres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400, InvalidKeystoreObjects,
|
|
|
|
$dres.error())
|
|
|
|
dres.get().remote_keys
|
|
|
|
|
|
|
|
var response: PostKeystoresResponse
|
|
|
|
|
|
|
|
for index, key in keys.pairs():
|
|
|
|
let keystore = RemoteKeystore(
|
|
|
|
version: 1'u64, remoteType: RemoteSignerType.Web3Signer,
|
|
|
|
pubkey: key.pubkey, remote: key.url
|
|
|
|
)
|
|
|
|
let res = importKeystore(node.attachedValidators[], node.config,
|
|
|
|
keystore)
|
|
|
|
if res.isErr():
|
|
|
|
case res.error().status
|
|
|
|
of AddValidatorStatus.failed:
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.error,
|
|
|
|
message: $res.error().message))
|
|
|
|
of AddValidatorStatus.existingArtifacts:
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.duplicate))
|
|
|
|
else:
|
|
|
|
node.addRemoteValidators([res.get()])
|
|
|
|
response.data.add(
|
|
|
|
RequestItemStatus(status: $KeystoreStatus.imported))
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
|
|
|
|
|
|
|
# https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/DeleteRemoteKeys
|
2022-03-02 15:43:52 +00:00
|
|
|
router.api(MethodDelete, "/api/eth/v1/remotekeys") do (
|
2022-02-07 20:36:09 +00:00
|
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
|
|
let authStatus = checkAuthorization(request, node)
|
|
|
|
if authStatus.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http401, InvalidAuthorization,
|
|
|
|
$authStatus.error())
|
|
|
|
let keys =
|
|
|
|
block:
|
|
|
|
if contentBody.isNone():
|
|
|
|
return RestApiResponse.jsonError(Http404, EmptyRequestBodyError)
|
|
|
|
let dres = decodeBody(DeleteKeystoresBody, contentBody.get())
|
|
|
|
if dres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400, InvalidValidatorPublicKey,
|
|
|
|
$dres.error())
|
|
|
|
dres.get().pubkeys
|
|
|
|
|
|
|
|
let response =
|
|
|
|
block:
|
|
|
|
var resp: DeleteRemoteKeystoresResponse
|
|
|
|
for index, key in keys.pairs():
|
|
|
|
let res = removeValidator(node.attachedValidators[], node.config, key,
|
|
|
|
KeystoreKind.Remote)
|
|
|
|
if res.isOk:
|
|
|
|
case res.value()
|
|
|
|
of RemoveValidatorStatus.deleted:
|
|
|
|
resp.data.add(
|
|
|
|
RemoteKeystoreStatus(status: KeystoreStatus.deleted))
|
|
|
|
of RemoveValidatorStatus.notFound:
|
|
|
|
resp.data.add(
|
|
|
|
RemoteKeystoreStatus(status: KeystoreStatus.notFound))
|
|
|
|
else:
|
|
|
|
resp.data.add(
|
|
|
|
RemoteKeystoreStatus(status: KeystoreStatus.error,
|
|
|
|
message: some($res.error())))
|
|
|
|
resp
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponsePlain(response)
|
|
|
|
|
2021-10-04 19:08:31 +00:00
|
|
|
router.redirect(
|
2021-12-22 12:37:31 +00:00
|
|
|
MethodGet,
|
|
|
|
"/eth/v1/keystores",
|
|
|
|
"/api/eth/v1/keystores")
|
2021-10-04 19:08:31 +00:00
|
|
|
|
|
|
|
router.redirect(
|
|
|
|
MethodPost,
|
2021-12-22 12:37:31 +00:00
|
|
|
"/eth/v1/keystores",
|
|
|
|
"/api/eth/v1/keystores")
|
2021-10-04 19:08:31 +00:00
|
|
|
|
|
|
|
router.redirect(
|
|
|
|
MethodPost,
|
2021-12-22 12:37:31 +00:00
|
|
|
"/eth/v1/keystores/delete",
|
|
|
|
"/api/eth/v1/keystores/delete")
|
2022-02-07 20:36:09 +00:00
|
|
|
|
|
|
|
router.redirect(
|
|
|
|
MethodGet,
|
2022-03-02 15:43:52 +00:00
|
|
|
"/eth/v1/remotekeys",
|
|
|
|
"/api/eth/v1/remotekeys")
|
2022-02-07 20:36:09 +00:00
|
|
|
|
|
|
|
router.redirect(
|
|
|
|
MethodPost,
|
2022-03-02 15:43:52 +00:00
|
|
|
"/eth/v1/remotekeys",
|
|
|
|
"/api/eth/v1/remotekeys")
|
2022-02-07 20:36:09 +00:00
|
|
|
|
|
|
|
router.redirect(
|
|
|
|
MethodDelete,
|
2022-03-02 15:43:52 +00:00
|
|
|
"/eth/v1/remotekeys",
|
|
|
|
"/api/eth/v1/remotekeys")
|