From 40c77e5928be312aa7381693535016602848c43b Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Mon, 7 Feb 2022 22:36:09 +0200 Subject: [PATCH] Remote KeyManager API and number of fixes/tests for KeyManager API (#3360) * Initial commit. * Fix current test suite. * Fix keymanager api test. * Fix wss_sim. * Add more keystore_management tests. * Recover deleted isEmptyDir(). * Add `HttpHostUri` distinct type. Move keymanager calls away from rest_beacon_calls to rest_keymanager_calls. Add REST serialization of RemoteKeystore and Keystore object. Add tests for Remote Keystore management API. Add tests for Keystore management API (Add keystore). Fix serialzation issues. * Fix test to use HttpHostUri instead of Uri. * Add links to specification in comments. * Remove debugging echoes. --- beacon_chain/nimbus_beacon_node.nim | 2 +- beacon_chain/rpc/rest_key_management_api.nim | 219 ++++-- .../eth2_apis/eth2_rest_serialization.nim | 346 +++++++++- .../spec/eth2_apis/rest_beacon_calls.nim | 56 +- .../spec/eth2_apis/rest_beacon_client.nim | 6 +- beacon_chain/spec/eth2_apis/rest_common.nim | 31 + .../spec/eth2_apis/rest_keymanager_calls.nim | 88 +++ .../spec/eth2_apis/rest_keymanager_types.nim | 23 +- beacon_chain/spec/keystore.nim | 179 +++-- .../validators/keystore_management.nim | 421 ++++++++--- beacon_chain/validators/validator_duties.nim | 10 +- beacon_chain/validators/validator_pool.nim | 8 +- research/wss_sim.nim | 3 +- tests/test_keymanager_api.nim | 483 ++++++++++++- tests/test_keystore_management.nim | 652 +++++++++++++++++- 15 files changed, 2223 insertions(+), 304 deletions(-) create mode 100644 beacon_chain/spec/eth2_apis/rest_common.nim create mode 100644 beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim diff --git a/beacon_chain/nimbus_beacon_node.nim b/beacon_chain/nimbus_beacon_node.nim index 3c2faec4f..1cf8abfac 100644 --- a/beacon_chain/nimbus_beacon_node.nim +++ b/beacon_chain/nimbus_beacon_node.nim @@ -43,7 +43,7 @@ import blockchain_dag, block_quarantine, block_clearance, attestation_pool, sync_committee_msg_pool, exit_pool, spec_cache], ./eth1/eth1_monitor, - ./spec/eth2_apis/rest_beacon_calls + ./spec/eth2_apis/[rest_beacon_calls, rest_common] from eth/common/eth_types import BlockHashOrNumber diff --git a/beacon_chain/rpc/rest_key_management_api.nim b/beacon_chain/rpc/rest_key_management_api.nim index fbccdf463..e9544b77e 100644 --- a/beacon_chain/rpc/rest_key_management_api.nim +++ b/beacon_chain/rpc/rest_key_management_api.nim @@ -4,34 +4,39 @@ # * 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. -import std/[tables, os, strutils] +import std/[tables, os, strutils, uri] import chronos, chronicles, confutils, stew/[base10, results, io2], bearssl, blscurve import ".."/validators/slashing_protection import ".."/[conf, version, filepath, beacon_node] import ".."/spec/[keystore, crypto] import ".."/rpc/rest_utils -import ".."/validators/[keystore_management, validator_pool] +import ".."/validators/[keystore_management, validator_pool, validator_duties] import ".."/spec/eth2_apis/rest_keymanager_types -export - rest_utils, - results +export rest_utils, results -proc listValidators*(validatorsDir, - secretsDir: string): seq[KeystoreInfo] - {.raises: [Defect].} = +proc listLocalValidators*(node: BeaconNode): seq[KeystoreInfo] {. + raises: [Defect].} = var validators: seq[KeystoreInfo] + 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 - try: - for el in listLoadableKeystores(validatorsDir, secretsDir, true): - validators.add KeystoreInfo(validating_pubkey: el.pubkey, - derivation_path: el.path.string, - readonly: false) - except OSError as err: - error "Failure to list the validator directories", - validatorsDir, secretsDir, err = err.msg - +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) + ) validators proc checkAuthorization*(request: HttpRequestRef, @@ -49,6 +54,14 @@ proc checkAuthorization*(request: HttpRequestRef, else: return err noAuthorizationHeader +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) + proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = # https://ethereum.github.io/keymanager-APIs/#/Keymanager/ListKeys router.api(MethodGet, "/api/eth/v1/keystores") do () -> RestApiResponse: @@ -56,9 +69,7 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = if authStatus.isErr(): return RestApiResponse.jsonError(Http401, InvalidAuthorization, $authStatus.error()) - let response = GetKeystoresResponse( - data: listValidators(node.config.validatorsDir(), - node.config.secretsDir())) + let response = GetKeystoresResponse(data: listLocalValidators(node)) return RestApiResponse.jsonResponsePlain(response) # https://ethereum.github.io/keymanager-APIs/#/Keymanager/ImportKeystores @@ -78,34 +89,39 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = $dres.error()) dres.get() - let nodeSPDIR = toSPDIR(node.attachedValidators.slashingProtection) - if nodeSPDIR.metadata.genesis_validators_root.Eth2Digest != - request.slashing_protection.metadata.genesis_validators_root.Eth2Digest: - return RestApiResponse.jsonError( - Http400, - "The slashing protection database and imported file refer to different blockchains.") + 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") - var - response: PostKeystoresResponse - inclRes = inclSPDIR(node.attachedValidators.slashingProtection, - request.slashing_protection) - if inclRes == siFailure: - return RestApiResponse.jsonError( - Http500, - "Internal server error; Failed to import slashing protection data") + var response: PostKeystoresResponse for index, item in request.keystores.pairs(): let res = importKeystore(node.attachedValidators[], node.network.rng[], node.config, item, request.passwords[index]) if res.isErr(): - response.data.add(RequestItemStatus(status: $KeystoreStatus.error, - message: $res.error())) - - elif res.value() == AddValidatorStatus.existingArtifacts: - response.data.add(RequestItemStatus(status: $KeystoreStatus.duplicate)) - + 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)) else: - response.data.add(RequestItemStatus(status: $KeystoreStatus.imported)) + node.addLocalValidators([res.get()]) + response.data.add( + RequestItemStatus(status: $KeystoreStatus.imported)) return RestApiResponse.jsonResponsePlain(response) @@ -136,19 +152,21 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = for index, key in keys.pairs(): let - res = removeValidator(node.attachedValidators[], node.config, key) + res = removeValidator(node.attachedValidators[], node.config, key, + KeystoreKind.Local) pubkey = key.blob.PubKey0x.PubKeyBytes if res.isOk: case res.value() of RemoveValidatorStatus.deleted: - keysAndDeleteStatus.add(pubkey, - RequestItemStatus(status: $KeystoreStatus.deleted)) + keysAndDeleteStatus.add( + pubkey, RequestItemStatus(status: $KeystoreStatus.deleted)) - # At first all keys with status missing directory after removal receive status 'not_found' - of RemoveValidatorStatus.missingDir: - keysAndDeleteStatus.add(pubkey, - RequestItemStatus(status: $KeystoreStatus.notFound)) + # At first all keys with status missing directory after removal receive + # status 'not_found' + of RemoveValidatorStatus.notFound: + keysAndDeleteStatus.add( + pubkey, RequestItemStatus(status: $KeystoreStatus.notFound)) else: keysAndDeleteStatus.add(pubkey, RequestItemStatus(status: $KeystoreStatus.error, @@ -169,6 +187,96 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = return RestApiResponse.jsonResponsePlain(response) + # https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ListRemoteKeys + router.api(MethodGet, "/api/eth/v1/remotekey") do () -> RestApiResponse: + 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 + router.api(MethodPost, "/api/eth/v1/remotekey") do ( + 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 + router.api(MethodDelete, "/api/eth/v1/remotekey") do ( + 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) + router.redirect( MethodGet, "/eth/v1/keystores", @@ -183,3 +291,18 @@ proc installKeymanagerHandlers*(router: var RestRouter, node: BeaconNode) = MethodPost, "/eth/v1/keystores/delete", "/api/eth/v1/keystores/delete") + + router.redirect( + MethodGet, + "/eth/v1/remotekey", + "/api/eth/v1/remotekey") + + router.redirect( + MethodPost, + "/eth/v1/remotekey", + "/api/eth/v1/remotekey") + + router.redirect( + MethodDelete, + "/eth/v1/remotekey", + "/api/eth/v1/remotekey") diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 8f31747f4..8fb22cb2f 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -6,15 +6,17 @@ import std/typetraits import stew/[assign2, results, base10, byteutils], presto/common, - libp2p/peerid, - serialization, json_serialization, json_serialization/std/[options, net, sets] -import ".."/[eth2_ssz_serialization, forks], + libp2p/peerid, serialization, json_serialization, + json_serialization/std/[options, net, sets] +import ".."/[eth2_ssz_serialization, forks, keystore], ".."/datatypes/[phase0, altair, bellatrix], + ".."/../validators/slashing_protection_common, "."/[rest_types, rest_keymanager_types] +import nimcrypto/utils as ncrutils export eth2_ssz_serialization, results, peerid, common, serialization, - json_serialization, options, net, sets, rest_types + json_serialization, options, net, sets, rest_types, slashing_protection_common from web3/ethtypes import BlockHash export ethtypes.BlockHash @@ -54,7 +56,8 @@ type SignedVoluntaryExit | Web3SignerRequest | KeystoresAndSlashingProtection | - DeleteKeystoresBody + DeleteKeystoresBody | + ImportRemoteKeystoresBody EncodeArrays* = seq[ValidatorIndex] | @@ -63,7 +66,8 @@ type seq[RestCommitteeSubscription] | seq[RestSyncCommitteeSubscription] | seq[RestSyncCommitteeMessage] | - seq[RestSignedContributionAndProof] + seq[RestSignedContributionAndProof] | + seq[RemoteKeystoreInfo] DecodeTypes* = DataEnclosedObject | @@ -71,6 +75,7 @@ type DataRootEnclosedObject | GetBlockV2Response | GetKeystoresResponse | + GetRemoteKeystoresResponse | GetStateV2Response | GetStateForkResponse | ProduceBlockResponseV2 | @@ -1265,6 +1270,331 @@ proc readValue*(reader: var JsonReader[RestJson], syncCommitteeContributionAndProof: data ) +## RemoteKeystoreStatus +proc writeValue*(writer: var JsonWriter[RestJson], + value: RemoteKeystoreStatus) {.raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("status", $value.status) + if value.message.isSome(): + writer.writeField("message", value.message.get()) + writer.endRecord() + +proc readValue*(reader: var JsonReader[RestJson], + value: var RemoteKeystoreStatus) {. + raises: [IOError, SerializationError, Defect].} = + var message: Option[string] + var status: Option[KeystoreStatus] + + for fieldName in readObjectFields(reader): + case fieldName + of "message": + if message.isSome(): + reader.raiseUnexpectedField("Multiple `message` fields found", + "RemoteKeystoreStatus") + message = some(reader.readValue(string)) + of "status": + if status.isSome(): + reader.raiseUnexpectedField("Multiple `status` fields found", + "RemoteKeystoreStatus") + let res = reader.readValue(string) + status = some( + case res + of "error": + KeystoreStatus.error + of "not_active": + KeystoreStatus.notActive + of "not_found": + KeystoreStatus.notFound + of "deleted": + KeystoreStatus.deleted + of "duplicate": + KeystoreStatus.duplicate + of "imported": + KeystoreStatus.imported + else: + reader.raiseUnexpectedValue("Invalid `status` value") + ) + else: + # We ignore all unknown fields. + discard + if status.isNone(): + reader.raiseUnexpectedValue("Field `status` is missing") + value = RemoteKeystoreStatus(status: status.get(), message: message) + +## ScryptSalt +proc readValue*(reader: var JsonReader[RestJson], value: var ScryptSalt) {. + raises: [SerializationError, IOError, Defect].} = + let res = ncrutils.fromHex(reader.readValue(string)) + if len(res) == 0: + reader.raiseUnexpectedValue("Invalid scrypt salt value") + value = ScryptSalt(res) + +## Pbkdf2Params +proc writeValue*(writer: var JsonWriter[RestJson], value: Pbkdf2Params) {. + raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("dklen", JsonString(Base10.toString(value.dklen))) + writer.writeField("c", JsonString(Base10.toString(value.c))) + writer.writeField("prf", value.prf) + writer.writeField("salt", value.salt) + writer.endRecord() + +proc readValue*(reader: var JsonReader[RestJson], value: var Pbkdf2Params) {. + raises: [SerializationError, IOError, Defect].} = + var + dklen: Option[uint64] + c: Option[uint64] + prf: Option[PrfKind] + salt: Option[Pbkdf2Salt] + + for fieldName in readObjectFields(reader): + case fieldName + of "dklen": + if dklen.isSome(): + reader.raiseUnexpectedField("Multiple `dklen` fields found", + "Pbkdf2Params") + dklen = some(reader.readValue(uint64)) + of "c": + if c.isSome(): + reader.raiseUnexpectedField("Multiple `c` fields found", + "Pbkdf2Params") + c = some(reader.readValue(uint64)) + of "prf": + if prf.isSome(): + reader.raiseUnexpectedField("Multiple `prf` fields found", + "Pbkdf2Params") + prf = some(reader.readValue(PrfKind)) + of "salt": + if salt.isSome(): + reader.raiseUnexpectedField("Multiple `salt` fields found", + "Pbkdf2Params") + salt = some(reader.readValue(Pbkdf2Salt)) + else: + # Ignore unknown field names. + discard + + if dklen.isNone(): + reader.raiseUnexpectedValue("Field `dklen` is missing") + if c.isNone(): + reader.raiseUnexpectedValue("Field `c` is missing") + if prf.isNone(): + reader.raiseUnexpectedValue("Field `prf` is missing") + if salt.isNone(): + reader.raiseUnexpectedValue("Field `salt` is missing") + + value = Pbkdf2Params( + dklen: dklen.get(), + c: c.get(), + prf: prf.get(), + salt: salt.get() + ) + +## ScryptParams +proc writeValue*(writer: var JsonWriter[RestJson], value: ScryptParams) {. + raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("dklen", JsonString(Base10.toString(value.dklen))) + writer.writeField("n", JsonString(Base10.toString(uint64(value.n)))) + writer.writeField("p", JsonString(Base10.toString(uint64(value.p)))) + writer.writeField("r", JsonString(Base10.toString(uint64(value.r)))) + writer.writeField("salt", value.salt) + writer.endRecord() + +proc readValue*(reader: var JsonReader[RestJson], value: var ScryptParams) {. + raises: [SerializationError, IOError, Defect].} = + var + dklen: Option[uint64] + n, p, r: Option[int] + salt: Option[ScryptSalt] + + for fieldName in readObjectFields(reader): + case fieldName + of "dklen": + if dklen.isSome(): + reader.raiseUnexpectedField("Multiple `dklen` fields found", + "ScryptParams") + dklen = some(reader.readValue(uint64)) + of "n": + if n.isSome(): + reader.raiseUnexpectedField("Multiple `n` fields found", + "ScryptParams") + let res = reader.readValue(int) + if res < 0: + reader.raiseUnexpectedValue("Unexpected negative `n` value") + n = some(res) + of "p": + if p.isSome(): + reader.raiseUnexpectedField("Multiple `p` fields found", + "ScryptParams") + let res = reader.readValue(int) + if res < 0: + reader.raiseUnexpectedValue("Unexpected negative `p` value") + p = some(res) + of "r": + if r.isSome(): + reader.raiseUnexpectedField("Multiple `r` fields found", + "ScryptParams") + let res = reader.readValue(int) + if res < 0: + reader.raiseUnexpectedValue("Unexpected negative `r` value") + r = some(res) + of "salt": + if salt.isSome(): + reader.raiseUnexpectedField("Multiple `salt` fields found", + "ScryptParams") + salt = some(reader.readValue(ScryptSalt)) + else: + # Ignore unknown field names. + discard + + if dklen.isNone(): + reader.raiseUnexpectedValue("Field `dklen` is missing") + if n.isNone(): + reader.raiseUnexpectedValue("Field `n` is missing") + if p.isNone(): + reader.raiseUnexpectedValue("Field `p` is missing") + if r.isNone(): + reader.raiseUnexpectedValue("Field `r` is missing") + if salt.isNone(): + reader.raiseUnexpectedValue("Field `salt` is missing") + + value = ScryptParams( + dklen: dklen.get(), + n: n.get(), p: p.get(), r: r.get(), + salt: salt.get() + ) + +## Keystore +proc writeValue*(writer: var JsonWriter[RestJson], value: Keystore) {. + raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("crypto", value.crypto) + if not(isNil(value.description)): + writer.writeField("description", value.description[]) + writer.writeField("pubkey", value.pubkey) + writer.writeField("path", string(value.path)) + writer.writeField("uuid", value.uuid) + writer.writeField("version", JsonString( + Base10.toString(uint64(value.version)))) + writer.endRecord() + +proc readValue*(reader: var JsonReader[RestJson], value: var Keystore) {. + raises: [SerializationError, IOError, Defect].} = + var + crypto: Option[Crypto] + description: Option[string] + pubkey: Option[ValidatorPubKey] + path: Option[KeyPath] + uuid: Option[string] + version: Option[int] + + for fieldName in readObjectFields(reader): + case fieldName + of "crypto": + if crypto.isSome(): + reader.raiseUnexpectedField("Multiple `crypto` fields found", + "Keystore") + crypto = some(reader.readValue(Crypto)) + of "description": + let res = reader.readValue(string) + if description.isSome(): + description = some(description.get() & "\n" & res) + else: + description = some(res) + of "pubkey": + if pubkey.isSome(): + reader.raiseUnexpectedField("Multiple `pubkey` fields found", + "Keystore") + pubkey = some(reader.readValue(ValidatorPubKey)) + of "path": + if path.isSome(): + reader.raiseUnexpectedField("Multiple `path` fields found", + "Keystore") + let res = validateKeyPath(reader.readValue(string)) + if res.isErr(): + reader.raiseUnexpectedValue("Invalid `path` value") + path = some(res.get()) + of "uuid": + if uuid.isSome(): + reader.raiseUnexpectedField("Multiple `uuid` fields found", + "Keystore") + uuid = some(reader.readValue(string)) + of "version": + if version.isSome(): + reader.raiseUnexpectedField("Multiple `version` fields found", + "Keystore") + let res = reader.readValue(int) + if res < 0: + reader.raiseUnexpectedValue("Unexpected negative `version` value") + version = some(res) + else: + # Ignore unknown field names. + discard + + if crypto.isNone(): + reader.raiseUnexpectedValue("Field `crypto` is missing") + if pubkey.isNone(): + reader.raiseUnexpectedValue("Field `pubkey` is missing") + if path.isNone(): + reader.raiseUnexpectedValue("Field `path` is missing") + if uuid.isNone(): + reader.raiseUnexpectedValue("Field `uuid` is missing") + if version.isNone(): + reader.raiseUnexpectedValue("Field `version` is missing") + + value = Keystore( + crypto: crypto.get(), + pubkey: pubkey.get(), + path: path.get(), + uuid: uuid.get(), + description: if description.isNone(): nil else: newClone(description.get()), + version: version.get(), + ) + +## KeystoresAndSlashingProtection +proc writeValue*(writer: var JsonWriter[RestJson], + value: KeystoresAndSlashingProtection) {. + raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("keystores", value.keystores) + writer.writeField("passwords", value.passwords) + if value.slashing_protection.isSome(): + writer.writeField("slashing_protection", value.slashing_protection) + writer.endRecord() + +proc readValue*(reader: var JsonReader[RestJson], + value: var KeystoresAndSlashingProtection) {. + raises: [SerializationError, IOError, Defect].} = + var + keystores: seq[Keystore] + passwords: seq[string] + slashing: Option[SPDIR] + + for fieldName in readObjectFields(reader): + case fieldName + of "keystores": + keystores = reader.readValue(seq[Keystore]) + of "passwords": + passwords = reader.readValue(seq[string]) + of "slashing_protection": + if slashing.isSome(): + reader.raiseUnexpectedField( + "Multiple `slashing_protection` fields found", + "KeystoresAndSlashingProtection") + slashing = some(reader.readValue(SPDIR)) + else: + # Ignore unknown field names. + discard + + if len(keystores) == 0: + reader.raiseUnexpectedValue("Missing `keystores` value") + if len(passwords) == 0: + reader.raiseUnexpectedValue("Missing `passwords` value") + + value = KeystoresAndSlashingProtection( + keystores: keystores, passwords: passwords, slashing_protection: slashing + ) + proc parseRoot(value: string): Result[Eth2Digest, cstring] = try: ok(Eth2Digest(data: hexToByteArray[32](value))) @@ -1277,8 +1607,8 @@ proc decodeBody*[T](t: typedesc[T], return err("Unsupported content type") let data = try: - RestJson.decode(cast[string](body.data), T) - except SerializationError: + RestJson.decode(body.data, T) + except SerializationError as exc: return err("Unable to deserialize data") except CatchableError: return err("Unexpected deserialization error") diff --git a/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim b/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim index 4369196ba..20cd9f0d4 100644 --- a/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_beacon_calls.nim @@ -11,14 +11,10 @@ import ".."/".."/validators/slashing_protection_common, ".."/datatypes/[phase0, altair], ".."/[helpers, forks, keystore, eth2_ssz_serialization], - "."/[rest_types, rest_keymanager_types, eth2_rest_serialization] + "."/[rest_types, rest_common, eth2_rest_serialization] export chronos, client, rest_types, eth2_rest_serialization -UUID.serializesAsBaseIn RestJson -KeyPath.serializesAsBaseIn RestJson -WalletName.serializesAsBaseIn RestJson - proc getGenesis*(): RestResponse[GetGenesisResponse] {. rest, endpoint: "/eth/v1/beacon/genesis", meth: MethodGet.} @@ -119,24 +115,6 @@ proc getBlockPlain*(block_id: BlockIdent): RestPlainResponse {. meth: MethodGet.} ## https://ethereum.github.io/beacon-APIs/#/Beacon/getBlock -proc raiseGenericError*(resp: RestPlainResponse) - {.noreturn, raises: [RestError, Defect].} = - let error = - block: - let res = decodeBytes(RestGenericError, 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 raiseUnknownStatusError(resp: RestPlainResponse) - {.noreturn, raises: [RestError, Defect].} = - let msg = "Unknown response status error (" & $resp.status & ")" - raise newException(RestError, msg) - proc getBlock*(client: RestClientRef, block_id: BlockIdent, restAccept = preferSSZ): Future[ForkedSignedBeaconBlock] {.async.} = # TODO restAccept should be "" by default, but for some reason that doesn't @@ -292,35 +270,3 @@ proc submitPoolVoluntaryExit*(body: SignedVoluntaryExit): RestPlainResponse {. rest, endpoint: "/eth/v1/beacon/pool/voluntary_exits", meth: MethodPost.} ## https://ethereum.github.io/beacon-APIs/#/Beacon/submitPoolVoluntaryExit - -proc listKeysPlain*(): RestPlainResponse {. - rest, endpoint: "/eth/v1/keystores", - meth: MethodGet.} - ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/ListKeys - -proc importKeystoresPlain*(body: KeystoresAndSlashingProtection): RestPlainResponse {. - rest, endpoint: "/eth/v1/keystores", - meth: MethodPost.} - ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/ImportKeystores - -proc deleteKeysPlain*(body: DeleteKeystoresBody): RestPlainResponse {. - rest, endpoint: "/eth/v1/keystores/delete", - meth: MethodPost.} - ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/DeleteKeys - -proc listKeys*(client: RestClientRef, - token: string): Future[GetKeystoresResponse] {.async.} = - let resp = await client.listKeysPlain( - extraHeaders = @[("Authorization", "Bearer " & token)]) - - case resp.status: - of 200: - let keystoresRes = decodeBytes( - GetKeystoresResponse, resp.data, resp.contentType) - if keystoresRes.isErr(): - raise newException(RestError, $keystoresRes.error) - return keystoresRes.get() - of 401, 403, 500: - raiseGenericError(resp) - else: - raiseUnknownStatusError(resp) diff --git a/beacon_chain/spec/eth2_apis/rest_beacon_client.nim b/beacon_chain/spec/eth2_apis/rest_beacon_client.nim index 5679dc1c1..320da6ade 100644 --- a/beacon_chain/spec/eth2_apis/rest_beacon_client.nim +++ b/beacon_chain/spec/eth2_apis/rest_beacon_client.nim @@ -10,10 +10,12 @@ import chronos, presto/client, "."/[ rest_beacon_calls, rest_config_calls, rest_debug_calls, - rest_node_calls, rest_validator_calls + rest_node_calls, rest_validator_calls, rest_keymanager_calls, + rest_common ] export chronos, client, rest_beacon_calls, rest_config_calls, rest_debug_calls, - rest_node_calls, rest_validator_calls + rest_node_calls, rest_validator_calls, rest_keymanager_calls, + rest_common diff --git a/beacon_chain/spec/eth2_apis/rest_common.nim b/beacon_chain/spec/eth2_apis/rest_common.nim new file mode 100644 index 000000000..f12c843c2 --- /dev/null +++ b/beacon_chain/spec/eth2_apis/rest_common.nim @@ -0,0 +1,31 @@ +# Copyright (c) 2018-2022 Status Research & Development GmbH +# 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. + +{.push raises: [Defect].} + +import + chronos, presto/client, chronicles, + "."/[rest_types, eth2_rest_serialization] + +export chronos, client, rest_types, eth2_rest_serialization + +proc raiseGenericError*(resp: RestPlainResponse) {. + noreturn, raises: [RestError, Defect].} = + let error = + block: + let res = decodeBytes(RestGenericError, 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 raiseUnknownStatusError*(resp: RestPlainResponse) {. + noreturn, raises: [RestError, Defect].} = + let msg = "Unknown response status error (" & $resp.status & ")" + raise newException(RestError, msg) diff --git a/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim new file mode 100644 index 000000000..cd801e0b6 --- /dev/null +++ b/beacon_chain/spec/eth2_apis/rest_keymanager_calls.nim @@ -0,0 +1,88 @@ +# Copyright (c) 2018-2022 Status Research & Development GmbH +# 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. + +{.push raises: [Defect].} + +import + chronos, presto/client, chronicles, + ".."/".."/validators/slashing_protection_common, + ".."/datatypes/[phase0, altair], + ".."/[helpers, forks, keystore, eth2_ssz_serialization], + "."/[rest_types, rest_common, rest_keymanager_types, eth2_rest_serialization] + +export chronos, client, rest_types, eth2_rest_serialization, + rest_keymanager_types + +UUID.serializesAsBaseIn RestJson +KeyPath.serializesAsBaseIn RestJson +WalletName.serializesAsBaseIn RestJson + +proc listKeysPlain*(): RestPlainResponse {. + rest, endpoint: "/eth/v1/keystores", + meth: MethodGet.} + ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/ListKeys + +proc importKeystoresPlain*(body: KeystoresAndSlashingProtection + ): RestPlainResponse {. + rest, endpoint: "/eth/v1/keystores", + meth: MethodPost.} + ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/ImportKeystores + +proc deleteKeysPlain*(body: DeleteKeystoresBody): RestPlainResponse {. + rest, endpoint: "/eth/v1/keystores/delete", + meth: MethodPost.} + ## https://ethereum.github.io/keymanager-APIs/#/Keymanager/DeleteKeys + +proc listKeys*(client: RestClientRef, + token: string): Future[GetKeystoresResponse] {.async.} = + let resp = await client.listKeysPlain( + extraHeaders = @[("Authorization", "Bearer " & token)]) + + case resp.status: + of 200: + let keystoresRes = decodeBytes( + GetKeystoresResponse, resp.data, resp.contentType) + if keystoresRes.isErr(): + raise newException(RestError, $keystoresRes.error) + return keystoresRes.get() + of 401, 403, 500: + raiseGenericError(resp) + else: + raiseUnknownStatusError(resp) + +proc listRemoteKeysPlain*(): RestPlainResponse {. + rest, endpoint: "/eth/v1/remotekey", + meth: MethodGet.} + ## https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ListRemoteKeys + +proc importRemoteKeysPlain*(body: ImportRemoteKeystoresBody + ): RestPlainResponse {. + rest, endpoint: "/eth/v1/remotekey", + meth: MethodPost.} + ## https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/ImportRemoteKeys + +proc deleteRemoteKeysPlain*(body: DeleteKeystoresBody): RestPlainResponse {. + rest, endpoint: "/eth/v1/remotekey", + meth: MethodDelete.} + ## https://ethereum.github.io/keymanager-APIs/#/Remote%20Key%20Manager/DeleteRemoteKeys + +proc listRemoteKeys*(client: RestClientRef, + token: string): Future[GetRemoteKeystoresResponse] {. + async.} = + let resp = await client.listRemoteKeysPlain( + extraHeaders = @[("Authorization", "Bearer " & token)]) + + case resp.status: + of 200: + let res = decodeBytes(GetRemoteKeystoresResponse, resp.data, + resp.contentType) + if res.isErr(): + raise newException(RestError, $res.error()) + return res.get() + of 401, 403, 500: + raiseGenericError(resp) + else: + raiseUnknownStatusError(resp) diff --git a/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim b/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim index 1f2604a05..309944735 100644 --- a/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_keymanager_types.nim @@ -1,5 +1,5 @@ import - std/[tables, strutils], + std/[tables, strutils, uri], ".."/[crypto, keystore], ../../validators/slashing_protection_common @@ -9,6 +9,10 @@ type derivation_path*: string readonly*: bool + RemoteKeystoreInfo* = object + pubkey*: ValidatorPubKey + url*: HttpHostUri + RequestItemStatus* = object status*: string message*: string @@ -16,7 +20,7 @@ type KeystoresAndSlashingProtection* = object keystores*: seq[Keystore] passwords*: seq[string] - slashing_protection*: SPDIR + slashing_protection*: Option[SPDIR] DeleteKeystoresBody* = object pubkeys*: seq[ValidatorPubKey] @@ -24,6 +28,12 @@ type GetKeystoresResponse* = object data*: seq[KeystoreInfo] + GetRemoteKeystoresResponse* = object + data*: seq[RemoteKeystoreInfo] + + ImportRemoteKeystoresBody* = object + remote_keys*: seq[RemoteKeystoreInfo] + PostKeystoresResponse* = object data*: seq[RequestItemStatus] @@ -31,6 +41,13 @@ type data*: seq[RequestItemStatus] slashing_protection*: SPDIR + RemoteKeystoreStatus* = object + status*: KeystoreStatus + message*: Option[string] + + DeleteRemoteKeystoresResponse* = object + data*: seq[RemoteKeystoreStatus] + KeystoreStatus* = enum error = "error" notActive = "not_active" @@ -44,7 +61,7 @@ type missingBearerScheme = "Bearer Authentication is not included in request" incorrectToken = "Authentication token is incorrect" -proc `<`*(x, y: KeystoreInfo): bool = +proc `<`*(x, y: KeystoreInfo | RemoteKeystoreInfo): bool = for a, b in fields(x, y): var c = cmp(a, b) if c < 0: return true diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 5a722d395..bcd169003 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -14,7 +14,7 @@ import # Third-party libraries normalize, # Status libraries - stew/[results, bitops2], stew/shims/macros, + stew/[results, bitops2, base10], stew/shims/macros, bearssl, eth/keyfile/uuid, blscurve, json_serialization, nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt], # Local modules @@ -72,9 +72,9 @@ type ScryptSalt* = distinct seq[byte] ScryptParams* = object - dklen: uint64 - n, p, r: int - salt: ScryptSalt + dklen*: uint64 + n*, p*, r*: int + salt*: ScryptSalt Pbkdf2Salt* = distinct seq[byte] @@ -130,6 +130,8 @@ type RemoteKeystoreFlag* {.pure.} = enum IgnoreSSLVerification + HttpHostUri* = distinct Uri + KeystoreData* = object version*: uint64 pubkey*: ValidatorPubKey @@ -140,7 +142,7 @@ type path*: KeyPath uuid*: string of KeystoreKind.Remote: - remoteUrl*: Uri + remoteUrl*: HttpHostUri flags*: set[RemoteKeystoreFlag] NetKeystore* = object @@ -158,7 +160,7 @@ type description*: Option[string] remoteType*: RemoteSignerType pubkey*: ValidatorPubKey - remote*: Uri + remote*: HttpHostUri flags*: set[RemoteKeystoreFlag] KsResult*[T] = Result[T, string] @@ -181,7 +183,7 @@ type signingKey*: ValidatorPrivKey withdrawalKey*: ValidatorPrivKey - SimpleHexEncodedTypes = ScryptSalt|ChecksumBytes|CipherBytes + SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes const keyLen = 32 @@ -217,6 +219,15 @@ CipherFunctionKind.serializesAsTextInJson PrfKind.serializesAsTextInJson KdfKind.serializesAsTextInJson +template `$`*(u: HttpHostUri): string = + `$`(Uri(u)) + +template `==`*(lhs, rhs: HttpHostUri): bool = + Uri(lhs) == Uri(rhs) + +template `<`*(lhs, rhs: HttpHostUri): bool = + $Uri(lhs) < $Uri(rhs) + template `$`*(m: Mnemonic): string = string(m) @@ -491,8 +502,8 @@ proc readValue*(r: var JsonReader, value: var Aes128CtrIv) r.raiseUnexpectedValue( "The aes-128-ctr IV must be a valid hex string") -proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T) - {.raises: [SerializationError, IOError, Defect].} = +proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T) {. + raises: [SerializationError, IOError, Defect].} = value = T ncrutils.fromHex(r.readValue(string)) if len(seq[byte](value)) == 0: r.raiseUnexpectedValue("Valid hex string expected") @@ -531,77 +542,121 @@ proc readValue*(r: var JsonReader, value: var Kdf) r.raiseUnexpectedValue( "The Kdf value should have sub-fields named 'function' and 'params'") -proc readValue*(r: var JsonReader, value: var RemoteKeystore) +# HttpHostUri +proc readValue*(reader: var JsonReader, value: var HttpHostUri) {. + raises: [IOError, SerializationError, Defect].} = + let svalue = reader.readValue(string) + let res = parseUri(svalue) + if res.scheme != "http" and res.scheme != "https": + reader.raiseUnexpectedValue("Incorrect URL scheme") + if len(res.hostname) == 0: + reader.raiseUnexpectedValue("Missing URL hostname") + value = HttpHostUri(res) + +proc writeValue*(writer: var JsonWriter, value: HttpHostUri) {. + raises: [IOError, Defect].} = + writer.writeValue($distinctBase(value)) + +# RemoteKeystore +proc writeValue*(writer: var JsonWriter, value: RemoteKeystore) {. + raises: [IOError, Defect].} = + writer.beginRecord() + writer.writeField("version", value.version) + writer.writeField("pubkey", "0x" & value.pubkey.toHex()) + writer.writeField("remote", $distinctBase(value.remote)) + case value.remoteType + of RemoteSignerType.Web3Signer: + writer.writeField("type", "web3signer") + if value.description.isSome(): + writer.writeField("description", value.description.get()) + if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags: + writer.writeField("ignore_ssl_verification", true) + writer.endRecord() + +template writeValue*(w: var JsonWriter, + value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) = + writeJsonHexString(w.stream, distinctBase value) + +proc readValue*(reader: var JsonReader, value: var RemoteKeystore) {.raises: [SerializationError, IOError, Defect].} = var - versionWasPresent = false + version: Option[uint64] description: Option[string] - remote: Option[Uri] + remote: Option[HttpHostUri] remoteType: Option[string] ignoreSslVerification: Option[bool] pubkey: Option[ValidatorPubKey] - for fieldName in readObjectFields(r): + + for fieldName in readObjectFields(reader): case fieldName: of "pubkey": if pubkey.isSome(): - r.raiseUnexpectedField("Multiple `pubkey` fields found", - "RemoteKeystore") - let res = r.readValue(ValidatorPubKey) - pubkey = some(res) - value.pubkey = res + reader.raiseUnexpectedField("Multiple `pubkey` fields found", + "RemoteKeystore") + pubkey = some(reader.readValue(ValidatorPubKey)) of "remote": if remote.isSome(): - r.raiseUnexpectedField("Multiple `remote` fields found", - "RemoteKeystore") - let res = r.readValue(Uri) - remote = some(res) - value.remote = res + reader.raiseUnexpectedField("Multiple `remote` fields found", + "RemoteKeystore") + remote = some(reader.readValue(HttpHostUri)) of "version": - if versionWasPresent: - r.raiseUnexpectedField("Multiple `version` fields found", - "RemoteKeystore") - value.version = r.readValue(uint64) - versionWasPresent = true + if version.isSome(): + reader.raiseUnexpectedField("Multiple `version` fields found", + "RemoteKeystore") + version = some(reader.readValue(uint64)) of "description": - let res = r.readValue(string) - if value.description.isSome(): - value.description = some(value.description.get() & "\n" & res) + let res = reader.readValue(string) + if description.isSome(): + description = some(description.get() & "\n" & res) else: - value.description = some(res) + description = some(res) of "ignore_ssl_verification": if ignoreSslVerification.isSome(): - r.raiseUnexpectedField("Multiple conflicting options found", - "RemoteKeystore") - let res = r.readValue(bool) - ignoreSslVerification = some(res) - if res: - value.flags.incl(RemoteKeystoreFlag.IgnoreSSLVerification) - else: - value.flags.excl(RemoteKeystoreFlag.IgnoreSSLVerification) + reader.raiseUnexpectedField("Multiple conflicting options found", + "RemoteKeystore") + ignoreSslVerification = some(reader.readValue(bool)) of "type": if remoteType.isSome(): - r.raiseUnexpectedField("Multiple `type` fields found", + reader.raiseUnexpectedField("Multiple `type` fields found", "RemoteKeystore") - let res = r.readValue(string) - remoteType = some(res) - case res - of "web3signer": - value.remoteType = RemoteSignerType.Web3Signer - else: - r.raiseUnexpectedValue("Unsupported remote signer `type` value") + remoteType = some(reader.readValue(string)) else: # Ignore unknown field names. discard - if not versionWasPresent: - r.raiseUnexpectedValue("Field version is missing") + if version.isNone(): + reader.raiseUnexpectedValue("Field `version` is missing") if remote.isNone(): - r.raiseUnexpectedValue("Field remote is missing") + reader.raiseUnexpectedValue("Field `remote` is missing") if pubkey.isNone(): - r.raiseUnexpectedValue("Field pubkey is missing") - # Set default remote signer type to `Web3Signer`. - if remoteType.isNone(): - value.remoteType = RemoteSignerType.Web3Signer + reader.raiseUnexpectedValue("Field `pubkey` is missing") + + let keystoreType = + if remoteType.isSome(): + let res = remoteType.get() + case res.toLowerAscii() + of "web3signer": + RemoteSignerType.Web3Signer + else: + reader.raiseUnexpectedValue("Unsupported remote signer `type` value") + else: + RemoteSignerType.Web3Signer + + let keystoreFlags = + block: + var res: set[RemoteKeystoreFlag] + if ignoreSslVerification.isSome(): + res.incl(RemoteKeystoreFlag.IgnoreSSLVerification) + res + + value = RemoteKeystore( + version: version.get(), + remote: remote.get(), + pubkey: pubkey.get(), + description: description, + remoteType: keystoreType, + flags: keystoreFlags + ) template writeValue*(w: var JsonWriter, value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) = @@ -839,6 +894,20 @@ proc createKeystore*(kdfKind: KdfKind, uuid: $uuid, version: 4) +proc createRemoteKeystore*(pubKey: ValidatorPubKey, remoteUri: HttpHostUri, + version = 1'u64, description = "", + remoteType = RemoteSignerType.Web3Signer, + flags: set[RemoteKeystoreFlag] = {}): RemoteKeystore = + RemoteKeystore( + version: version, + description: if len(description) > 0: some(description) + else: none[string](), + remoteType: remoteType, + pubkey: pubKey, + remote: remoteUri, + flags: flags + ) + proc createWallet*(kdfKind: KdfKind, rng: var BrHmacDrbgContext, seed: KeySeed, diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index e1e3faf8c..280dcd2be 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -34,6 +34,7 @@ const DisableFileName* = ".disable" 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. type WalletPathPair* = object @@ -48,13 +49,19 @@ type KmResult*[T] = Result[T, cstring] - RemoveValidatorStatus* = enum + RemoveValidatorStatus* {.pure.} = enum deleted = "Deleted" - missingDir = "Could not find keystore directory to remove" + notFound = "Not found" - AddValidatorStatus* = enum - added = "Validator added" + AddValidatorStatus* {.pure.} = enum existingArtifacts = "Keystore artifacts already exists" + failed = "Validator not added" + + AddValidatorFailure* = object + status*: AddValidatorStatus + message*: string + + ImportResult*[T] = Result[T, AddValidatorFailure] const minPasswordLen = 12 @@ -100,6 +107,26 @@ func init*(T: type KeystoreData, remoteUrl: keystore.remote ) +func init*(T: type KeystoreData, cookedKey: CookedPubKey, + remoteUrl: HttpHostUri): T = + KeystoreData( + kind: KeystoreKind.Remote, + pubkey: cookedKey.toPubKey(), + version: 1'u64, + remoteUrl: remoteUrl + ) + +func init(T: type AddValidatorFailure, status: AddValidatorStatus, + msg = ""): AddValidatorFailure {.raises: [Defect].} = + AddValidatorFailure(status: status, message: msg) + +func toKeystoreKind*(kind: ValidatorKind): KeystoreKind {.raises: [Defect].} = + case kind + of ValidatorKind.Local: + KeystoreKind.Local + of ValidatorKind.Remote: + KeystoreKind.Remote + proc checkAndCreateDataDir*(dataDir: string): bool = when defined(posix): let requiredPerms = 0o700 @@ -344,24 +371,34 @@ proc loadKeystoreUnsafe*(validatorsDir, secretsDir, proc loadRemoteKeystoreImpl(validatorsDir, keyName: string): Option[KeystoreData] = - let remoteKeystorePath = validatorsDir / keyName / RemoteKeystoreFileName - let privateItem = + let keystorePath = validatorsDir / keyName / RemoteKeystoreFileName + + if not(checkSensitiveFilePermissions(keystorePath)): + error "Remote keystorage file has insecure permissions", + key_path = keystorePath + return + + let keyStore = block: - let keystore = + let remoteKeystore = try: - Json.decode(remoteKeystorePath, RemoteKeystore) - except SerializationError as e: - error "Failed to read remote keystore file", - keystore_path = remoteKeystorePath, - err_msg = e.formatMsg(remoteKeystorePath) + Json.loadFile(keystorePath, RemoteKeystore) + except IOError as err: + error "Failed to read remote keystore file", err = err.msg, + path = keystorePath return - let res = init(KeystoreData, keystore) + except SerializationError as e: + error "Invalid remote keystore file", + path = keystorePath, + err_msg = e.formatMsg(keystorePath) + return + let res = init(KeystoreData, remoteKeystore) if res.isErr(): - error "Invalid validator's public key in keystore file", - keystore_path = remoteKeystorePath + error "Invalid remote keystore file", + path = keystorePath return res.get() - some(privateItem) + some(keyStore) proc loadKeystoreImpl(validatorsDir, secretsDir, keyName: string, nonInteractive: bool): Option[KeystoreData] = @@ -433,43 +470,108 @@ proc loadKeystore*(validatorsDir, secretsDir, keyName: string, error "Unable to find any keystore files", keystorePath none[KeystoreData]() -proc removeValidatorFiles*(validatorsDir, - secretsDir , - publicKeyDir: string): KmResult[RemoveValidatorStatus] {. +proc removeValidatorFiles*(validatorsDir, secretsDir, keyName: string, + kind: KeystoreKind + ): KmResult[RemoveValidatorStatus] {. raises: [Defect].} = - let keystoreDir = validatorsDir / publicKeyDir - let keystoreFile = keystoreDir / KeystoreFileName - let secretFile = secretsDir / publicKeyDir + let + keystoreDir = validatorsDir / keyName + keystoreFile = + case kind + of KeystoreKind.Local: + keystoreDir / KeystoreFileName + of KeystoreKind.Remote: + keystoreDir / RemoteKeystoreFileName + secretFile = secretsDir / keyName - if not (dirExists(keystoreDir)): - return ok(missingDir) - try: - removeDir(keystoreDir, false) - except OSError: - return err("Could not remove keystore directory") + if not(existsDir(keystoreDir)): + return ok(RemoveValidatorStatus.notFound) - let res = io2.removeFile(secretFile) - if res.isErr(): - return err("Could not remove password file") + if not(existsFile(keystoreFile)): + return ok(RemoveValidatorStatus.notFound) - ok deleted + case kind + of KeystoreKind.Local: + block: + let res = io2.removeFile(keystoreFile) + if res.isErr(): + return err("Could not remove keystore file") + block: + let res = io2.removeFile(secretFile) + if res.isErr() and existsFile(secretFile): + return err("Could not remove password file") + # We remove folder with all subfolders and files inside. + try: + removeDir(keystoreDir, false) + except OSError: + return err("Could not remove keystore directory") + of KeystoreKind.Remote: + block: + let res = io2.removeFile(keystoreFile) + if res.isErr(): + return err("Could not remove keystore file") + # We remove folder with all subfolders and files inside. + try: + removeDir(keystoreDir, false) + except OSError: + return err("Could not remove keystore directory") + + ok(RemoveValidatorStatus.deleted) + +proc removeValidatorFiles*(conf: AnyConf, keyName: string, + kind: KeystoreKind + ): KmResult[RemoveValidatorStatus] {. + raises: [Defect].} = + removeValidatorFiles(conf.validatorsDir(), conf.secretsDir(), keyName, kind) proc removeValidator*(pool: var ValidatorPool, conf: AnyConf, - publicKey: ValidatorPubKey): KmResult[RemoveValidatorStatus] {. + publicKey: ValidatorPubKey, + kind: KeystoreKind): KmResult[RemoveValidatorStatus] {. raises: [Defect].} = - let publicKeyHex: string = "0x" & publicKey.toHex() - let res = removeValidatorFiles(conf.validatorsDir(), - conf.secretsDir(), - publicKeyHex) + let validator = pool.getValidator(publicKey) + if isNil(validator): + return ok(RemoveValidatorStatus.notFound) + if validator.kind.toKeystoreKind() != kind: + return ok(RemoveValidatorStatus.notFound) + let publicKeyName: string = "0x" & publicKey.toHex() + let res = removeValidatorFiles(conf, publicKeyName, kind) if res.isErr(): return err(res.error()) - pool.removeValidator(publicKey) - ok res.value() + ok(res.value()) -iterator listLoadableKeystores*(validatorsDir, - secretsDir: string, - nonInteractive: bool): KeystoreData = +proc checkKeyName*(keyName: string): bool = + const keyAlphabet = {'a'..'f', 'A'..'F', '0'..'9'} + if len(keyName) != KeyNameSize: + return false + if keyName[0] != '0' and keyName[1] != 'x': + return false + for index in 2 ..< len(keyName): + if keyName[index] notin keyAlphabet: + return false + true + +proc existsKeystore*(keystoreDir: string, keyKind: KeystoreKind): bool {. + raises: [Defect].} = + case keyKind + of KeystoreKind.Local: + existsFile(keystoreDir / KeystoreFileName) + of KeystoreKind.Remote: + existsFile(keystoreDir / RemoteKeystoreFileName) + +proc existsKeystore*(keystoreDir: string, + keysMask: set[KeystoreKind]): bool {.raises: [Defect].} = + if KeystoreKind.Local in keysMask: + if existsKeystore(keystoreDir, KeystoreKind.Local): + return true + if KeystoreKind.Remote in keysMask: + if existsKeystore(keystoreDir, KeystoreKind.Remote): + return true + false + +iterator listLoadableKeystores*(validatorsDir, secretsDir: string, + nonInteractive: bool, + keysMask: set[KeystoreKind]): KeystoreData = try: for kind, file in walkDir(validatorsDir): if kind == pcDir: @@ -479,26 +581,22 @@ iterator listLoadableKeystores*(validatorsDir, keystoreDir = validatorsDir / keyName keystoreFile = keystoreDir / KeystoreFileName - if not(fileExists(keystoreFile)): + if not(checkKeyName(keyName)): + # Skip folders which name do not satisfy "0x[a-fA-F0-9]{96, 96}". + continue + + if not(existsKeystore(keystoreDir, keysMask)): # Skip folders which do not have keystore file inside. continue let secretFile = secretsDir / keyName - keystore = loadKeystore(validatorsDir, secretsDir, keyName, nonInteractive) - + keystore = loadKeystore(validatorsDir, secretsDir, keyName, + nonInteractive) if keystore.isSome(): - let pubkey = keystore.get().privateKey.toPubKey().toPubKey() - - yield KeystoreData(kind: KeystoreKind.Local, - privateKey: keystore.get().privateKey, - description: keystore.get().description, - path: keystore.get().path, - uuid: keystore.get().uuid, - version: keystore.get().version, - pubkey: pubkey) + yield keystore.get() else: - fatal "Unable to load keystore", keystore_file = keystoreFile + fatal "Unable to load keystore", keystore = file quit 1 except OSError as err: @@ -509,7 +607,8 @@ iterator listLoadableKeystores*(validatorsDir, iterator listLoadableKeystores*(config: AnyConf): KeystoreData = for el in listLoadableKeystores(config.validatorsDir(), config.secretsDir(), - config.nonInteractive): + config.nonInteractive, + {KeystoreKind.Local, KeystoreKind.Remote}): yield el type @@ -519,6 +618,8 @@ type FailedToCreateSecretsDir FailedToCreateSecretFile FailedToCreateKeystoreFile + DuplicateKeystoreDir + DuplicateKeystoreFile KeystoreGenerationError* = object case kind*: KeystoreGenerationErrorKind @@ -526,7 +627,9 @@ type FailedToCreateValidatorsDir, FailedToCreateSecretsDir, FailedToCreateSecretFile, - FailedToCreateKeystoreFile: + FailedToCreateKeystoreFile, + DuplicateKeystoreDir, + DuplicateKeystoreFile: error*: string proc mapErrTo*[T, E](r: Result[T, E], v: static KeystoreGenerationErrorKind): @@ -555,7 +658,8 @@ proc loadNetKeystore*(keyStorePath: string, if insecurePwd.isSome(): warn "Using insecure password to unlock networking key" - let decrypted = decryptNetKeystore(keystore, KeystorePass.init insecurePwd.get) + let decrypted = decryptNetKeystore(keystore, + KeystorePass.init(insecurePwd.get())) if decrypted.isOk: return some(decrypted.get()) else: @@ -607,11 +711,10 @@ proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string, key_path = keyStorePath res.mapErrTo(FailedToCreateKeystoreFile) -proc createValidatorFiles*( - secretsDir, validatorsDir, - keystoreDir, secretFile, - passwordAsString, keystoreFile, - encodedStorage: string): Result[void, KeystoreGenerationError] = +proc createValidatorFiles*(secretsDir, validatorsDir, keystoreDir, secretFile, + passwordAsString, keystoreFile, + encodedStorage: string + ): Result[void, KeystoreGenerationError] = var success = false # becomes true when everything is created successfully @@ -641,17 +744,45 @@ proc createValidatorFiles*( discard io2.removeDir(keystoreDir) # secretFile: - ? secureWriteFile(secretFile, passwordAsString).mapErrTo(FailedToCreateSecretFile) + ? secureWriteFile(secretFile, + passwordAsString).mapErrTo(FailedToCreateSecretFile) defer: if not success: discard io2.removeFile(secretFile) # keystoreFile: - ? secureWriteFile(keystoreFile, encodedStorage).mapErrTo(FailedToCreateKeystoreFile) + ? secureWriteFile(keystoreFile, + encodedStorage).mapErrTo(FailedToCreateKeystoreFile) success = true ok() +proc createValidatorFiles*(validatorsDir, keystoreDir, keystoreFile, + encodedStorage: string + ): Result[void, KeystoreGenerationError] = + var + success = false # becomes true when everything is created successfully + + # validatorsDir: + let validatorsDirExisted: bool = dirExists(validatorsDir) + if not(validatorsDirExisted): + ? secureCreatePath(validatorsDir).mapErrTo(FailedToCreateValidatorsDir) + defer: + if not (success or validatorsDirExisted): + discard io2.removeDir(validatorsDir) + + # keystoreDir: + ? secureCreatePath(keystoreDir).mapErrTo(FailedToCreateKeystoreDir) + defer: + if not success: + discard io2.removeDir(keystoreDir) + + # keystoreFile: + ? secureWriteFile(keystoreFile, + encodedStorage).mapErrTo(FailedToCreateKeystoreFile) + success = true + ok() + proc saveKeystore*(rng: var BrHmacDrbgContext, validatorsDir, secretsDir: string, signingKey: ValidatorPrivKey, @@ -663,35 +794,118 @@ proc saveKeystore*(rng: var BrHmacDrbgContext, keypass = KeystorePass.init(password) keyName = "0x" & signingPubKey.toHex() keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / KeystoreFileName - if not existsDir(keystoreDir): + if existsDir(keystoreDir): + return err(KeystoreGenerationError(kind: DuplicateKeystoreDir, + error: "Keystore directory already exists")) + if existsFile(keystoreFile): + return err(KeystoreGenerationError(kind: DuplicateKeystoreFile, + error: "Keystore file already exists")) - let - keyStore = createKeystore(kdfPbkdf2, rng, signingKey, + let keyStore = createKeystore(kdfPbkdf2, rng, signingKey, keypass, signingKeyPath, mode = mode) - keystoreFile = keystoreDir / KeystoreFileName - var encodedStorage: string - try: - encodedStorage = Json.encode(keyStore) - except SerializationError as e: - error "Could not serialize keystorage", key_path = keystoreFile - return err(KeystoreGenerationError( - kind: FailedToCreateKeystoreFile, error: e.msg)) + var encodedStorage: string + try: + encodedStorage = Json.encode(keyStore) + except SerializationError as e: + error "Could not serialize keystorage", key_path = keystoreFile + return err(KeystoreGenerationError( + kind: FailedToCreateKeystoreFile, error: e.msg)) - ? createValidatorFiles(secretsDir, validatorsDir, - keystoreDir, - secretsDir / keyName, keypass.str, - keystoreFile, encodedStorage) + ? createValidatorFiles(secretsDir, validatorsDir, + keystoreDir, + secretsDir / keyName, keypass.str, + keystoreFile, encodedStorage) ok() +proc saveKeystore*(validatorsDir: string, + publicKey: ValidatorPubKey, url: HttpHostUri, + version = 1'u64, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = ""): Result[void, KeystoreGenerationError] {. + raises: [Defect].} = + let + keyName = "0x" & publicKey.toHex() + keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / RemoteKeystoreFileName + keystoreDesc = if len(desc) == 0: none[string]() else: some(desc) + keyStore = RemoteKeystore( + version: version, description: keystoreDesc, remoteType: remoteType, + pubkey: publicKey, remote: url, flags: flags + ) + + if existsDir(keystoreDir): + return err(KeystoreGenerationError(kind: DuplicateKeystoreDir, + error: "Keystore directory already exists")) + if existsFile(keystoreFile): + return err(KeystoreGenerationError(kind: DuplicateKeystoreFile, + error: "Keystore file already exists")) + + let encodedStorage = + try: + Json.encode(keyStore) + except SerializationError as exc: + error "Could not serialize keystorage", key_path = keystoreFile + return err(KeystoreGenerationError( + kind: FailedToCreateKeystoreFile, error: exc.msg)) + + ? createValidatorFiles(validatorsDir, keystoreDir, keystoreFile, + encodedStorage) + ok() + +proc saveKeystore*(conf: AnyConf, publicKey: ValidatorPubKey, url: HttpHostUri, + version = 1'u64, + flags: set[RemoteKeystoreFlag] = {}, + remoteType = RemoteSignerType.Web3Signer, + desc = ""): Result[void, KeystoreGenerationError] {. + raises: [Defect].} = + saveKeystore(conf.validatorsDir(), publicKey, url, version, flags, + remoteType, desc) + +proc importKeystore*(pool: var ValidatorPool, conf: AnyConf, + keystore: RemoteKeystore): ImportResult[KeystoreData] {. + raises: [Defect].} = + let + publicKey = keystore.pubkey + keyName = "0x" & publicKey.toHex() + validatorsDir = conf.validatorsDir() + keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / RemoteKeystoreFileName + + # We check `publicKey`. + let cookedKey = + block: + let res = publicKey.load() + if res.isNone(): + return err( + AddValidatorFailure.init(AddValidatorStatus.failed, + "Invalid validator's public key")) + res.get() + + # We check `publicKey` in memory storage first. + if publicKey in pool: + return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) + + # We check `publicKey` in filesystem. + if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}): + return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) + + let res = saveKeystore(conf, publicKey, keystore.remote) + if res.isErr(): + return err(AddValidatorFailure.init(AddValidatorStatus.failed, + $res.error())) + ok(KeystoreData.init(cookedKey, keystore.remote)) + proc importKeystore*(pool: var ValidatorPool, rng: var BrHmacDrbgContext, conf: AnyConf, keystore: Keystore, - password: string): KmResult[AddValidatorStatus] - {.raises: [Defect].} = + password: string): ImportResult[KeystoreData] {. + raises: [Defect].} = let keypass = KeystorePass.init(password) let privateKey = block: @@ -699,31 +913,33 @@ proc importKeystore*(pool: var ValidatorPool, if res.isOk(): res.get() else: - return err("Keystore decryption failed") + return err( + AddValidatorFailure.init(AddValidatorStatus.failed, res.error())) + let + publicKey = privateKey.toPubKey() + keyName = "0x" & publicKey.toHex() + validatorsDir = conf.validatorsDir() + secretsDir = conf.secretsDir() + secretFile = secretsDir / keyName + keystoreDir = validatorsDir / keyName + keystoreFile = keystoreDir / KeystoreFileName - let publicKey = privateKey.toPubKey() - let keyName = "0x" & publicKey.toHex() + # We check `publicKey` in memory storage first. + if publicKey.toPubKey() in pool: + return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) - let validatorsDir = conf.validatorsDir() - let secretsDir = conf.secretsDir() + # We check `publicKey` in filesystem. + if existsKeystore(keystoreDir, {KeystoreKind.Local, KeystoreKind.Remote}): + return err(AddValidatorFailure.init(AddValidatorStatus.existingArtifacts)) - let secretFile = secretsDir / keyName - let keystoreDir = validatorsDir / keyName - let keystoreFile = keystoreDir / KeystoreFileName - - if fileExists(keystoreFile) or fileExists(secretFile): - return ok(AddValidatorStatus.existingArtifacts) - - let res = saveKeystore(rng, - validatorsDir, secretsDir, - privateKey, publicKey, - keystoreDir.KeyPath, password) + let res = saveKeystore(rng, validatorsDir, secretsDir, + privateKey, publicKey, keystore.path, password) if res.isErr(): - return err("Keystore Generation Error") + return err(AddValidatorFailure.init(AddValidatorStatus.failed, + $res.error())) - pool.addLocalValidator(KeystoreData.init(privateKey, keystore)) - ok AddValidatorStatus.added + ok(KeystoreData.init(privateKey, keystore)) proc generateDeposits*(cfg: RuntimeConfig, rng: var BrHmacDrbgContext, @@ -731,7 +947,8 @@ proc generateDeposits*(cfg: RuntimeConfig, firstValidatorIdx, totalNewValidators: int, validatorsDir: string, secretsDir: string, - mode = Secure): Result[seq[DepositData], KeystoreGenerationError] = + mode = Secure): Result[seq[DepositData], + KeystoreGenerationError] = var deposits: seq[DepositData] notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir diff --git a/beacon_chain/validators/validator_duties.nim b/beacon_chain/validators/validator_duties.nim index d49546fe8..a59a3aa96 100644 --- a/beacon_chain/validators/validator_duties.nim +++ b/beacon_chain/validators/validator_duties.nim @@ -84,15 +84,14 @@ proc findValidator(validators: auto, pubkey: ValidatorPubKey): else: some(idx.ValidatorIndex) -proc addLocalValidator(node: BeaconNode, - validators: auto, +proc addLocalValidator(node: BeaconNode, validators: auto, item: KeystoreData) = let pubkey = item.pubkey index = findValidator(validators, pubkey) node.attachedValidators[].addLocalValidator(item, index) -proc addRemoteValidator(node: BeaconNode, validators: auto, +proc addRemoteValidator(pool: var ValidatorPool, validators: auto, item: KeystoreData) = let httpFlags = block: @@ -108,7 +107,7 @@ proc addRemoteValidator(node: BeaconNode, validators: auto, remote_url = $item.remoteUrl, validator = item.pubkey return let index = findValidator(validators, item.pubkey) - node.attachedValidators[].addRemoteValidator(item, client.get(), index) + pool.addRemoteValidator(item, client.get(), index) proc addLocalValidators*(node: BeaconNode, validators: openArray[KeystoreData]) = @@ -120,7 +119,8 @@ proc addRemoteValidators*(node: BeaconNode, validators: openArray[KeystoreData]) = withState(node.dag.headState.data): for item in validators: - node.addRemoteValidator(state.data.validators.asSeq(), item) + node.attachedValidators[].addRemoteValidator( + state.data.validators.asSeq(), item) proc addValidators*(node: BeaconNode) = let (localValidators, remoteValidators) = diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index 0180edd56..c6e172e5c 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -114,8 +114,12 @@ proc removeValidator*(pool: var ValidatorPool, pubkey: ValidatorPubKey) = let validator = pool.validators.getOrDefault(pubkey) if not(isNil(validator)): pool.validators.del(pubkey) - notice "Local or remote validator detached", pubkey, - validator = shortLog(validator) + case validator.kind + of ValidatorKind.Local: + notice "Local validator detached", pubkey, validator = shortLog(validator) + of ValidatorKind.Remote: + notice "Remote validator detached", pubkey, + validator = shortLog(validator) validators.set(pool.count().int64) proc updateValidator*(pool: var ValidatorPool, pubkey: ValidatorPubKey, diff --git a/research/wss_sim.nim b/research/wss_sim.nim index d6ac29e01..2a9827e34 100644 --- a/research/wss_sim.nim +++ b/research/wss_sim.nim @@ -53,7 +53,8 @@ cli do(validatorsDir: string, secretsDir: string, validators: Table[ValidatorIndex, ValidatorPrivKey] validatorKeys: Table[ValidatorPubKey, ValidatorPrivKey] - for item in listLoadableKeystores(validatorsDir, secretsDir, true): + for item in listLoadableKeystores(validatorsDir, secretsDir, true, + {KeystoreKind.Local}): let pubkey = item.privateKey.toPubKey().toPubKey() idx = findValidator(getStateField(state[], validators).toSeq, pubkey) diff --git a/tests/test_keymanager_api.nim b/tests/test_keymanager_api.nim index 98b4eeea4..314ac9572 100644 --- a/tests/test_keymanager_api.nim +++ b/tests/test_keymanager_api.nim @@ -8,7 +8,7 @@ import ../beacon_chain/spec/[crypto, keystore, eth2_merkleization], ../beacon_chain/spec/datatypes/base, - ../beacon_chain/spec/eth2_apis/[rest_beacon_client, rest_keymanager_types], + ../beacon_chain/spec/eth2_apis/[rest_keymanager_calls, rest_keymanager_types], ../beacon_chain/validators/[keystore_management, slashing_protection_common], ../beacon_chain/networking/network_metadata, ../beacon_chain/rpc/rest_key_management_api, @@ -28,6 +28,48 @@ const tokenFilePath = dataDir / "keymanager-token.txt" keymanagerPort = 47000 correctTokenValue = "some secret token" + newPrivateKeys = [ + "0x598c9b81749ba7bb8eb37781027359e3ffe87d0e1579e21c453ce22af0c05e35", + "0x14e4470a1d8913ec0602048af78addf0fd7a37f591dd3feda828d10a10c0f6ff", + "0x3b4498c4e26f83702ceeed5e32600ecb3e71f08fc4561215d0f0ced13bf5dbdf", + "0x3cae8cf27c7e12549486f5613974661285d40596907a7fc39bac7c55a56660ab", + "0x71fd9bb8eadcf64df9cc8e716652709492c16518f73f87c770a54fe8c80ac5ae", + "0x4be74b7b0b0058dea2d4744e0069486500770f68296ac9b9bbd26df6749ed0ca", + "0x10052305a5fda7805fb1e762fe6cbc47e43c5a54f34f008fa79c48fee1749db7", + "0x3630f086fb9f1136fe077751031a16630e43d65ff64bb9fd3708adff81df5926" + ] + oldPublicKeys = [ + "0x94effccb0514f0f110a9680827e4f3769e53349e3b1c177e8c4f38b0e52e7842a4990212fe2edd2ce48b9b0bd02f3b04", + "0x950bcb136ef15e737cd28cc8ba94a5584e30cf6cfa4f3d16215acbe46917633c09630208f379898a898b29bd59b2bd34", + "0xaa96fddc809e0678b192cebd3a64873a339c7352eafaa88ab13bac84244e19b9afe2de8282320f5e0e7c155573f80ac3", + "0xa0f1da63e35c7a159fc2f187d300cad9ef5f5e73e55f78c391e7bc2c2feabc2d9d63dfe99edd7058ad0ab9d7f14a1e1a", + "0x9315ea03755881989b0d34e9594520d2ebca4d2f0fd955dafe42948a91840a2e812d1d61f26684c603a60c99e3537151", + "0x88c9737238fa23ed8e485e17349c523fe3fe848eab173959d34e7f7f2c731fb896ab7c0b0877a40782a5cd529dc7b080", + "0x995e1d9d9d467ca25b981a7ca0880e932ac418e5ebed9a834f3ead3fbec267986e28eb0243c562ae3b1995a600c1495c", + "0x945ab594e8c9cf3d6251b86fddf6fbf970c1835cd14113098554f135a6c2cf7f21d2f7a08ae33726785a59ae4910fa51", + ] + oldPublicKeysUrl = HttpHostUri(parseUri("http://127.0.0.1/local")) + newPublicKeys = [ + "0x80eadf027ad564a2f004616fa58f3add9caa700b20e9bf7e0b101be61406feb79f5e28ec8a5bb2a0689cc7b4c807afba", + "0x8c6585f39fd3d2ed950ba4958f0050ec68e4e7e3200147687fa101bcf98977ebe144b03edc45906faae144549f11d8b9", + "0xb3939c9ecfb3679de8aa7f81e8dfb9eaa51e958d165e8b963aa88767217ce03316e4bad74e7a475ed6009365d297e0cd", + "0xb093029010dd400f49350db77b13e70c3d75f5286c2cc5d7f1d0865e251cc547764de85371583eba2b1810cf36a4feb1", + "0x8893a6f03de181cc93537ebb89ed242f65f3722fe22cd7aaab71a4149a792b231e23e1575c12efb0d2934e6d7b755431", + "0x88c475e022971f0698b50aa2c9dd91df8b1c9f1079cbe7b2243bb5dee3a5cb5c46e170f90165efecdc794e14ae5b8fd9", + "0xa782e5161ba8e9ac135b0db3203a8c23aa61e19be6b9c198393d8b2b902bad8139863d9cf26bc2cbdc3b747bafc64606", + "0xb33f17216dda29dba1a9257e75b3dd8446c9ea217b563c20950c43f64300f7bd3d5f0dfa02274cab988e594552b7189e" + ] + newPublicKeysUrl = HttpHostUri(parseUri("http://127.0.0.1/remote")) + +proc contains*(keylist: openArray[KeystoreInfo], key: ValidatorPubKey): bool = + for item in keylist: + if item.validating_pubkey == key: + return true + false + +proc contains*(keylist: openArray[KeystoreInfo], key: string): bool = + let pubkey = ValidatorPubKey.fromHex(key).tryGet() + contains(keylist, pubkey) proc startSingleNodeNetwork = let @@ -65,6 +107,13 @@ proc startSingleNodeNetwork = Json.saveFile(depositsFile, launchPadDeposits) notice "Deposit data written", filename = depositsFile + for item in oldPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + let res = saveKeystore(validatorsDir, key, oldPublicKeysUrl) + if res.isErr(): + fatal "Failed to create remote keystore file", err = res.error + quit 1 + let tokenFileRes = secureWriteFile(tokenFilePath, correctTokenValue) if tokenFileRes.isErr: fatal "Failed to create token file", err = deposits.error @@ -125,6 +174,40 @@ const iv = hexToSeqByte "264daa3f303d7259501c93d997d84fe6" secretNetBytes = hexToSeqByte "08021220fe442379443d6e2d7d75d3a58f96fbb35f0a9c7217796825fc9040e3b89c5736" +proc listLocalValidators(validatorsDir, + secretsDir: string): seq[KeystoreInfo] {. + raises: [Defect].} = + var validators: seq[KeystoreInfo] + + try: + for el in listLoadableKeystores(validatorsDir, secretsDir, true, + {KeystoreKind.Local}): + validators.add KeystoreInfo(validating_pubkey: el.pubkey, + derivation_path: el.path.string, + readonly: false) + except OSError as err: + error "Failure to list the validator directories", + validatorsDir, secretsDir, err = err.msg + + validators + +proc listRemoteValidators(validatorsDir, + secretsDir: string): seq[RemoteKeystoreInfo] {. + raises: [Defect].} = + var validators: seq[RemoteKeystoreInfo] + + try: + for el in listLoadableKeystores(validatorsDir, secretsDir, true, + {KeystoreKind.Remote}): + validators.add RemoteKeystoreInfo(pubkey: el.pubkey, + url: el.remoteUrl) + + except OSError as err: + error "Failure to list the validator directories", + validatorsDir, secretsDir, err = err.msg + + validators + proc runTests {.async.} = while bnStatus != BeaconNodeStatus.Running: await sleepAsync(1.seconds) @@ -136,6 +219,8 @@ proc runTests {.async.} = rng = keys.newRng() privateKey = ValidatorPrivKey.fromRaw(secretBytes).get + localList = listLocalValidators(validatorsDir, secretsDir) + newKeystore = createKeystore( kdfPbkdf2, rng[], privateKey, KeystorePass.init password, @@ -143,18 +228,102 @@ proc runTests {.async.} = description = "This is a test keystore that uses PBKDF2 to secure the secret", path = validateKeyPath("m/12381/60/0/0").expect("Valid Keypath")) + importKeystoresBody1 = + block: + var + res1: seq[Keystore] + res2: seq[string] + for key in newPrivateKeys: + let privateKey = ValidatorPrivKey.fromHex(key).tryGet() + let store = createKeystore(kdfPbkdf2, rng[], privateKey, + KeystorePass.init password, salt = salt, iv = iv, + description = "Test keystore", + path = validateKeyPath("m/12381/60/0/0").expect("Valid Keypath")) + res1.add(store) + res2.add(password) + KeystoresAndSlashingProtection( + keystores: res1, + passwords: res2, + ) + + deleteKeysBody1 = + block: + var res: seq[ValidatorPubKey] + for item in newPrivateKeys: + let privateKey = ValidatorPrivKey.fromHex(item).tryGet() + let publicKey = privateKey.toPubKey().toPubKey() + res.add(publicKey) + DeleteKeystoresBody( + pubkeys: res + ) + importKeystoresBody = KeystoresAndSlashingProtection( keystores: @[newKeystore], passwords: @[password], - slashing_protection: SPDIR()) + ) deleteKeysBody = DeleteKeystoresBody( pubkeys: @[privateKey.toPubKey.toPubKey]) + importRemoteKeystoresBody = + block: + var res: seq[RemoteKeystoreInfo] + # Adding keys which are already present in filesystem + for item in oldPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + res.add(RemoteKeystoreInfo(pubkey: key, url: newPublicKeysUrl)) + # Adding keys which are new + for item in newPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + res.add(RemoteKeystoreInfo(pubkey: key, url: newPublicKeysUrl)) + # Adding non-remote keys which are already present in filesystem + res.add(RemoteKeystoreInfo(pubkey: localList[0].validating_pubkey, + url: newPublicKeysUrl)) + res.add(RemoteKeystoreInfo(pubkey: localList[1].validating_pubkey, + url: newPublicKeysUrl)) + ImportRemoteKeystoresBody(remote_keys: res) + + deleteRemoteKeystoresBody1 = + block: + var res: seq[ValidatorPubKey] + for item in oldPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + res.add(key) + DeleteKeystoresBody(pubkeys: res) + + deleteRemoteKeystoresBody2 = + block: + var res: seq[ValidatorPubKey] + for item in newPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + res.add(key) + DeleteKeystoresBody(pubkeys: res) + + deleteRemoteKeystoresBody3 = + block: + DeleteKeystoresBody( + pubkeys: @[ + ValidatorPubKey.fromHex(newPublicKeys[0]).tryGet(), + ValidatorPubKey.fromHex(newPublicKeys[1]).tryGet() + ] + ) + + deleteRemoteKeystoresBody4 = + block: + DeleteKeystoresBody( + pubkeys: @[ + ValidatorPubKey.fromHex(oldPublicKeys[0]).tryGet(), + ValidatorPubKey.fromHex(oldPublicKeys[1]).tryGet(), + localList[0].validating_pubkey, + localList[1].validating_pubkey + ] + ) + suite "ListKeys requests" & preset(): asyncTest "Correct token provided" & preset(): let - filesystemKeystores = sorted(listValidators(validatorsDir, secretsDir)) + filesystemKeystores = sorted( + listLocalValidators(validatorsDir, secretsDir)) apiKeystores = sorted((await client.listKeys(correctTokenValue)).data) check filesystemKeystores == apiKeystores @@ -198,6 +367,75 @@ proc runTests {.async.} = let keystores = await client.listKeys("Invalid Token") suite "ImportKeystores requests" & preset(): + asyncTest "ImportKeystores/ListKeystores/DeleteKeystores" & preset(): + let + response1 = await client.importKeystoresPlain( + importKeystoresBody1, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson1 = Json.decode(response1.data, JsonNode) + + check response1.status == 200 + for i in 0 ..< 8: + check: + responseJson1["data"][i]["status"].getStr() == "imported" + responseJson1["data"][i]["message"].getStr() == "" + + let + filesystemKeystores1 = sorted( + listLocalValidators(validatorsDir, secretsDir)) + apiKeystores1 = sorted((await client.listKeys(correctTokenValue)).data) + + check: + filesystemKeystores1 == apiKeystores1 + importKeystoresBody1.keystores[0].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[1].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[2].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[3].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[4].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[5].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[6].pubkey in filesystemKeystores1 + importKeystoresBody1.keystores[7].pubkey in filesystemKeystores1 + + let + response2 = await client.importKeystoresPlain( + importKeystoresBody1, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson2 = Json.decode(response2.data, JsonNode) + + check response2.status == 200 + for i in 0 ..< 8: + check: + responseJson2["data"][i]["status"].getStr() == "duplicate" + responseJson2["data"][i]["message"].getStr() == "" + + let + response3 = await client.deleteKeysPlain( + deleteKeysBody1, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson3 = Json.decode(response3.data, JsonNode) + + check response3.status == 200 + for i in 0 ..< 8: + check: + responseJson3["data"][i]["status"].getStr() == "deleted" + responseJson3["data"][i]["message"].getStr() == "" + + let + filesystemKeystores2 = sorted( + listLocalValidators(validatorsDir, secretsDir)) + apiKeystores2 = sorted((await client.listKeys(correctTokenValue)).data) + + check: + filesystemKeystores2 == apiKeystores2 + deleteKeysBody1.pubkeys[0] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[1] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[2] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[3] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[4] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[5] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[6] notin filesystemKeystores2 + deleteKeysBody1.pubkeys[7] notin filesystemKeystores2 + asyncTest "Missing Authorization header" & preset(): let response = await client.importKeystoresPlain(importKeystoresBody) @@ -285,6 +523,245 @@ proc runTests {.async.} = responseJson["message"].getStr() == InvalidAuthorization responseJson["stacktraces"][0].getStr() == $incorrectToken + suite "ListRemoteKeys requests" & preset(): + asyncTest "Correct token provided" & preset(): + let + filesystemKeystores = sorted( + listRemoteValidators(validatorsDir, secretsDir)) + apiKeystores = sorted(( + await client.listRemoteKeys(correctTokenValue)).data) + + check filesystemKeystores == apiKeystores + + asyncTest "Missing Authorization header" & preset(): + let + response = await client.listRemoteKeysPlain() + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + + asyncTest "Invalid Authorization Header" & preset(): + let + response = await client.listRemoteKeysPlain( + extraHeaders = @[("Authorization", "UnknownAuthScheme X")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $missingBearerScheme + + asyncTest "Invalid Authorization Token" & preset(): + let + response = await client.listRemoteKeysPlain( + extraHeaders = @[("Authorization", "Bearer InvalidToken")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $incorrectToken + + expect RestError: + let keystores = await client.listKeys("Invalid Token") + + suite "ImportRemoteKeys/ListRemoteKeys/DeleteRemoteKeys" & preset(): + asyncTest "Importing list of remote keys" & preset(): + let + response1 = await client.importRemoteKeysPlain( + importRemoteKeystoresBody, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson1 = Json.decode(response1.data, JsonNode) + + check: + response1.status == 200 + for i in [0, 1, 2, 3, 4, 5, 6, 7, 16, 17]: + check: + responseJson1["data"][i]["status"].getStr() == "duplicate" + responseJson1["data"][i]["message"].getStr() == "" + for i in 8 ..< 16: + check: + responseJson1["data"][i]["status"].getStr() == "imported" + responseJson1["data"][i]["message"].getStr() == "" + + let + filesystemKeystores1 = sorted( + listRemoteValidators(validatorsDir, secretsDir)) + apiKeystores1 = sorted(( + await client.listRemoteKeys(correctTokenValue)).data) + + check: + filesystemKeystores1 == apiKeystores1 + + for item in newPublicKeys: + let key = ValidatorPubKey.fromHex(item).tryGet() + let found = + block: + var res = false + for keystore in filesystemKeystores1: + if keystore.pubkey == key: + res = true + break + res + check found == true + + let + response2 = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody2, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson2 = Json.decode(response2.data, JsonNode) + + check: + response2.status == 200 + responseJson2["data"][0]["status"].getStr() == "deleted" + responseJson2["data"][1]["status"].getStr() == "deleted" + responseJson2["data"][2]["status"].getStr() == "deleted" + responseJson2["data"][3]["status"].getStr() == "deleted" + responseJson2["data"][4]["status"].getStr() == "deleted" + responseJson2["data"][5]["status"].getStr() == "deleted" + responseJson2["data"][6]["status"].getStr() == "deleted" + responseJson2["data"][7]["status"].getStr() == "deleted" + + let + filesystemKeystores2 = sorted( + listRemoteValidators(validatorsDir, secretsDir)) + apiKeystores2 = sorted(( + await client.listRemoteKeys(correctTokenValue)).data) + + check: + filesystemKeystores2 == apiKeystores2 + + for keystore in filesystemKeystores2: + let key = "0x" & keystore.pubkey.toHex() + check: + key notin newPublicKeys + + asyncTest "Missing Authorization header" & preset(): + let + response = await client.importRemoteKeysPlain(importRemoteKeystoresBody) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + + asyncTest "Invalid Authorization Header" & preset(): + let + response = await client.importRemoteKeysPlain( + importRemoteKeystoresBody, + extraHeaders = @[("Authorization", "Basic XYZ")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $missingBearerScheme + + asyncTest "Invalid Authorization Token" & preset(): + let + response = await client.importRemoteKeysPlain( + importRemoteKeystoresBody, + extraHeaders = @[("Authorization", "Bearer InvalidToken")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $incorrectToken + + suite "DeleteRemoteKeys requests" & preset(): + asyncTest "Deleting not existing key" & preset(): + let + response = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody3, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 200 + responseJson["data"][0]["status"].getStr() == "not_found" + responseJson["data"][1]["status"].getStr() == "not_found" + + asyncTest "Deleting existing local key and remote key" & preset(): + let + response = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody4, + extraHeaders = @[("Authorization", "Bearer " & correctTokenValue)]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 200 + responseJson["data"][0]["status"].getStr() == "deleted" + responseJson["data"][1]["status"].getStr() == "deleted" + responseJson["data"][2]["status"].getStr() == "not_found" + responseJson["data"][3]["status"].getStr() == "not_found" + + let + filesystemKeystores = sorted( + listRemoteValidators(validatorsDir, secretsDir)) + apiKeystores = sorted(( + await client.listRemoteKeys(correctTokenValue)).data) + + check: + filesystemKeystores == apiKeystores + + let + removedKey0 = ValidatorPubKey.fromHex(oldPublicKeys[0]).tryGet() + removedKey1 = ValidatorPubKey.fromHex(oldPublicKeys[1]).tryGet() + + for item in apiKeystores: + check: + removedKey0 != item.pubkey + removedKey1 != item.pubkey + + asyncTest "Missing Authorization header" & preset(): + let + response = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody1) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $noAuthorizationHeader + + asyncTest "Invalid Authorization Header" & preset(): + let + response = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody1, + extraHeaders = @[("Authorization", "Basic XYZ")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $missingBearerScheme + + asyncTest "Invalid Authorization Token" & preset(): + let + response = await client.deleteRemoteKeysPlain( + deleteRemoteKeystoresBody1, + extraHeaders = @[("Authorization", "Bearer XYZ")]) + responseJson = Json.decode(response.data, JsonNode) + + check: + response.status == 401 + responseJson["code"].getStr() == "401" + responseJson["message"].getStr() == InvalidAuthorization + responseJson["stacktraces"][0].getStr() == $incorrectToken + bnStatus = BeaconNodeStatus.Stopping proc main() {.async.} = diff --git a/tests/test_keystore_management.nim b/tests/test_keystore_management.nim index e6b3aa243..a5f10819f 100644 --- a/tests/test_keystore_management.nim +++ b/tests/test_keystore_management.nim @@ -1,7 +1,7 @@ {.used.} import - std/[os, options, json, typetraits], + std/[os, options, json, typetraits, uri, algorithm], unittest2, chronos, chronicles, stint, json_serialization, blscurve, eth/keys, nimcrypto/utils, libp2p/crypto/crypto as lcrypto, @@ -24,9 +24,6 @@ proc directoryItemsCount(dir: string): int {.raises: [OSError].} = for el in walkDir(dir): result += 1 -proc isEmptyDir(dir: string): bool = - directoryItemsCount(dir) == 0 - proc validatorPubKeysInDir(dir: string): seq[string] = for kind, file in walkDir(dir): if kind == pcDir: @@ -47,6 +44,13 @@ let cfg = defaultRuntimeConfig validatorDirRes = secureCreatePath(testValidatorsDir) +proc namesEqual(a, b: openarray[string]): bool = + sorted(a) == sorted(b) + +when not defined(windows): + proc isEmptyDir(dir: string): bool = + directoryItemsCount(dir) == 0 + if validatorDirRes.isErr(): warn "Could not create validators folder", path = testValidatorsDir, err = ioErrorMsg(validatorDirRes.error) @@ -70,14 +74,59 @@ if deposits.isErr: let validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) -suite "removeValidatorFiles": +const + MultiplePassword = string.fromBytes( + hexToSeqByte("7465737470617373776f7264f09f9491")) + MultipleSalt = hexToSeqByte( + "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3") + MultipleIv = hexToSeqByte("264daa3f303d7259501c93d997d84fe6") + MultipleRemoteUri = HttpHostUri(parseUri("https://127.0.0.1/eth/web3signer")) + + MultiplePrivateKeys = [ + "3b89cdf5c62b423dab64dd69476c6c74bdbccc684abc89f3b392ac1f679e06c3", + "5140621611300ed419f901d8c56baf32d89d876272bbb3ab16e1c9f0884487d4" + ] + +var + MultipleKeystoreNames: seq[string] + MultipleSigningKeys: seq[ValidatorPrivKey] + MultipleLocalKeystores: seq[Keystore] + MultipleLocalKeystoreJsons: seq[string] + MultipleRemoteKeystores: seq[RemoteKeystore] + MultipleRemoteKeystoreJsons: seq[string] + +for key in MultiplePrivateKeys: + let + nsecret = ValidatorPrivKey.fromRaw(hexToSeqByte(key)).get() + npubkey = nsecret.toPubKey().toPubKey() + keystoreName = "0x" & npubkey.toHex() + + localKeystore = createKeystore( + kdfPbkdf2, rng[], nsecret, + KeystorePass.init MultiplePassword, + salt = MultipleSalt, iv = MultipleIv, + description = "This is a test keystore.", + path = validateKeyPath("m/12381/60/0/0").expect("Valid Keypath")) + localKeystoreJson = Json.encode(localKeystore) + + remoteKeystore = createRemoteKeystore(npubkey, MultipleRemoteUri) + remoteKeystoreJson = Json.encode(remoteKeystore) + + MultipleSigningKeys.add(nsecret) + MultipleKeystoreNames.add(keystoreName) + MultipleLocalKeystores.add(localKeystore) + MultipleLocalKeystoreJsons.add(localKeystoreJson) + MultipleRemoteKeystores.add(remoteKeystore) + MultipleRemoteKeystoreJsons.add(remoteKeystoreJson) + +suite "removeValidatorFiles()": test "Remove validator files": let validatorsCountBefore = directoryItemsCount(testValidatorsDir) secretsCountBefore = directoryItemsCount(testSecretsDir) firstValidator = validatorPubKeys[0] removeValidatorFilesRes = removeValidatorFiles( - testValidatorsDir, testSecretsDir, firstValidator) + testValidatorsDir, testSecretsDir, firstValidator, KeystoreKind.Local) validatorsCountAfter = directoryItemsCount(testValidatorsDir) secretsCountAfter = directoryItemsCount(testSecretsDir) @@ -94,31 +143,263 @@ suite "removeValidatorFiles": let nonexistentValidator = "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - res = removeValidatorFiles(testValidatorsDir, testSecretsDir, nonexistentValidator) + res = removeValidatorFiles(testValidatorsDir, testSecretsDir, + nonexistentValidator, KeystoreKind.Local) - check(res.isOk and res.value == RemoveValidatorStatus.missingDir) + check(res.isOk and res.value == RemoveValidatorStatus.notFound) test "Remove validator files twice": let secondValidator = validatorPubKeys[1] - res1 = removeValidatorFiles(testValidatorsDir, testSecretsDir, secondValidator) - res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, secondValidator) + res1 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + secondValidator, KeystoreKind.Local) + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + secondValidator, KeystoreKind.Local) check: not fileExists(testValidatorsDir / secondValidator) not fileExists(testSecretsDir / secondValidator) res1.isOk and res1.value() == RemoveValidatorStatus.deleted - res2.isOk and res2.value() == RemoveValidatorStatus.missingDir + res2.isOk and res2.value() == RemoveValidatorStatus.notFound os.removeDir testValidatorsDir os.removeDir testSecretsDir -suite "createValidatorFiles": +suite "removeValidatorFiles() multiple keystore types": + setup: + let + curKeystoreDir0 {.used.} = testValidatorsDir / MultipleKeystoreNames[0] + curSecretsFile0 {.used.} = testSecretsDir / MultipleKeystoreNames[0] + remoteKeystoreFile0 {.used.} = curKeystoreDir0 / RemoteKeystoreFileName + localKeystoreFile0 {.used.} = curKeystoreDir0 / KeystoreFileName + curSigningKey0 {.used.} = MultipleSigningKeys[0] + curCookedKey0 {.used.} = curSigningKey0.toPubKey() + curPublicKey0 {.used.} = curCookedKey0.toPubKey() + + curKeystoreDir1 {.used.} = testValidatorsDir / MultipleKeystoreNames[1] + curSecretsFile1 {.used.} = testSecretsDir / MultipleKeystoreNames[1] + remoteKeystoreFile1 {.used.} = curKeystoreDir1 / RemoteKeystoreFileName + localKeystoreFile1 {.used.} = curKeystoreDir1 / KeystoreFileName + curSigningKey1 {.used.} = MultipleSigningKeys[1] + curCookedKey1 {.used.} = curSigningKey1.toPubKey() + curPublicKey1 {.used.} = curCookedKey1.toPubKey() + + curSigningPath {.used.} = + validateKeyPath("m/12381/60/0/0").expect("Valid Keypath") + + teardown: + os.removeDir testValidatorsDir + os.removeDir testSecretsDir + + test "Remove [LOCAL] when [LOCAL] is present": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[0], KeystoreKind.Local) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.deleted + + validatorsCount1 == 1 + secretsCount1 == 1 + validatorsCount2 == 0 + secretsCount2 == 0 + + not(dirExists(curKeystoreDir0)) + not(fileExists(remoteKeystoreFile0)) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, []) + + test "Remove [LOCAL] when [LOCAL] is missing": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[1], KeystoreKind.Local) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.notFound + + validatorsCount1 == 1 + secretsCount1 == 1 + validatorsCount2 == 1 + secretsCount2 == 1 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, [MultipleKeystoreNames[0]]) + + test "Remove [REMOTE] when [REMOTE] is present": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[0], KeystoreKind.Remote) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.deleted + + validatorsCount1 == 1 + secretsCount1 == 0 + validatorsCount2 == 0 + secretsCount2 == 0 + + not(dirExists(curKeystoreDir0)) + not(fileExists(remoteKeystoreFile0)) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, []) + + test "Remove [REMOTE] when [REMOTE] is missing": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[1], KeystoreKind.Remote) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.notFound + + validatorsCount1 == 1 + secretsCount1 == 0 + validatorsCount2 == 1 + secretsCount2 == 0 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, [MultipleKeystoreNames[0]]) + + test "Remove [LOCAL] when [REMOTE] is present": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[0], KeystoreKind.Local) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.notFound + + validatorsCount1 == 1 + secretsCount1 == 0 + validatorsCount2 == 1 + secretsCount2 == 0 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, [MultipleKeystoreNames[0]]) + + test "Remove [REMOTE] when [LOCAL] is present": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + validatorsCount1 = directoryItemsCount(testValidatorsDir) + secretsCount1 = directoryItemsCount(testSecretsDir) + validatorPubKeys1 = validatorPubKeysInDir(testValidatorsDir) + + res2 = removeValidatorFiles(testValidatorsDir, testSecretsDir, + MultipleKeystoreNames[0], KeystoreKind.Remote) + + validatorsCount2 = directoryItemsCount(testValidatorsDir) + secretsCount2 = directoryItemsCount(testSecretsDir) + validatorPubKeys2 = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + res2.value == RemoveValidatorStatus.notFound + + validatorsCount1 == 1 + secretsCount1 == 1 + validatorsCount2 == 1 + secretsCount2 == 1 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + namesEqual(validatorPubKeys1, [MultipleKeystoreNames[0]]) + namesEqual(validatorPubKeys2, [MultipleKeystoreNames[0]]) + + os.removeDir testValidatorsDir + os.removeDir testSecretsDir + +suite "createValidatorFiles()": setup: const password = string.fromBytes hexToSeqByte("7465737470617373776f7264f09f9491") secretBytes = hexToSeqByte "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f" - secretNetBytes = hexToSeqByte "08021220fe442379443d6e2d7d75d3a58f96fbb35f0a9c7217796825fc9040e3b89c5736" salt = hexToSeqByte "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" iv = hexToSeqByte "264daa3f303d7259501c93d997d84fe6" @@ -131,18 +412,19 @@ suite "createValidatorFiles": salt=salt, iv=iv, description = "This is a test keystore that uses PBKDF2 to secure the secret.", path = validateKeyPath("m/12381/60/0/0").expect("Valid Keypath")) - keystoreJsonContents = Json.encode(keystore) + keystoreJsonContents {.used.} = Json.encode(keystore) hexEncodedPubkey = "0x" & keystore.pubkey.toHex() - keystoreDir = testValidatorsDir / hexEncodedPubkey - secretFile = testSecretsDir / hexEncodedPubkey - keystoreFile = testValidatorsDir / hexEncodedPubkey / KeystoreFileName + keystoreDir {.used.} = testValidatorsDir / hexEncodedPubkey + secretFile {.used.} = testSecretsDir / hexEncodedPubkey + keystoreFile {.used.} = testValidatorsDir / hexEncodedPubkey / + KeystoreFileName teardown: os.removeDir testValidatorsDir os.removeDir testSecretsDir - test "Add keystore files": + test "Add keystore files [LOCAL]": let res = createValidatorFiles(testSecretsDir, testValidatorsDir, keystoreDir, @@ -166,7 +448,9 @@ suite "createValidatorFiles": secretFile.contentEquals password keystoreFile.contentEquals keystoreJsonContents - test "Add keystore files twice": + namesEqual(validatorPubKeys, [hexEncodedPubkey]) + + test "Add keystore files twice [LOCAL]": let res1 = createValidatorFiles(testSecretsDir, testValidatorsDir, keystoreDir, @@ -194,6 +478,76 @@ suite "createValidatorFiles": secretFile.contentEquals password keystoreFile.contentEquals keystoreJsonContents + namesEqual(validatorPubKeys, [hexEncodedPubkey]) + + test "Add keystore files [REMOTE]": + let + curKeystoreDir = testValidatorsDir / MultipleKeystoreNames[0] + curSecretsFile = testSecretsDir / MultipleKeystoreNames[0] + remoteKeystoreFile = curKeystoreDir / RemoteKeystoreFileName + localKeystoreFile = curKeystoreDir / KeystoreFileName + + res = createValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res.isOk + + validatorsCount == 1 + secretsCount == 0 + + dirExists(curKeystoreDir) + fileExists(remoteKeystoreFile) + not(fileExists(localKeystoreFile)) + not(fileExists(curSecretsFile)) + + remoteKeystoreFile.contentEquals MultipleRemoteKeystoreJsons[0] + + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + + test "Add keystore files twice [REMOTE]": + let + curKeystoreDir = testValidatorsDir / MultipleKeystoreNames[0] + curSecretsFile = testSecretsDir / MultipleKeystoreNames[0] + remoteKeystoreFile = curKeystoreDir / RemoteKeystoreFileName + localKeystoreFile = curKeystoreDir / KeystoreFileName + + res1 = createValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) + + res2 = createValidatorFiles(testValidatorsDir, curKeystoreDir, + remoteKeystoreFile, + MultipleRemoteKeystoreJsons[0]) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk # The second call should just overwrite the results of the first + + validatorsCount == 1 + secretsCount == 0 + + dirExists(curKeystoreDir) + fileExists(remoteKeystoreFile) + not(fileExists(localKeystoreFile)) + not(fileExists(curSecretsFile)) + + remoteKeystoreFile.contentEquals MultipleRemoteKeystoreJsons[0] + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + # TODO The following tests are disabled on Windows because the io2 module # doesn't implement the permission/mode parameter at the moment: when not defined(windows): @@ -298,3 +652,263 @@ suite "createValidatorFiles": secretsCountBefore == secretsCountAfter os.removeDir testDataDir + +suite "saveKeystore()": + setup: + let + curKeystoreDir0 = testValidatorsDir / MultipleKeystoreNames[0] + curSecretsFile0 = testSecretsDir / MultipleKeystoreNames[0] + remoteKeystoreFile0 = curKeystoreDir0 / RemoteKeystoreFileName + localKeystoreFile0 = curKeystoreDir0 / KeystoreFileName + curSigningKey0 = MultipleSigningKeys[0] + curCookedKey0 = curSigningKey0.toPubKey() + curPublicKey0 {.used.} = curCookedKey0.toPubKey() + + curKeystoreDir1 = testValidatorsDir / MultipleKeystoreNames[1] + curSecretsFile1 {.used.} = testSecretsDir / MultipleKeystoreNames[1] + remoteKeystoreFile1 {.used.} = curKeystoreDir1 / RemoteKeystoreFileName + localKeystoreFile1 {.used.} = curKeystoreDir1 / KeystoreFileName + curSigningKey1 = MultipleSigningKeys[1] + curCookedKey1 = curSigningKey1.toPubKey() + curPublicKey1 {.used.} = curCookedKey1.toPubKey() + + curSigningPath {.used.} = + validateKeyPath("m/12381/60/0/0").expect("Valid Keypath") + + teardown: + os.removeDir testValidatorsDir + os.removeDir testSecretsDir + + test "Save [LOCAL] keystore after [LOCAL] keystore with same id": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + res2 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isErr + res2.error().kind == DuplicateKeystoreDir + + validatorsCount == 1 + secretsCount == 1 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + + test "Save [REMOTE] keystore after [REMOTE] keystore with same id": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + res2 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isErr + res2.error().kind == DuplicateKeystoreDir + + validatorsCount == 1 + secretsCount == 0 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + + test "Save [REMOTE] keystore after [LOCAL] keystore with same id": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + res2 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isErr + res2.error().kind == DuplicateKeystoreDir + + validatorsCount == 1 + secretsCount == 1 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + + test "Save [LOCAL] keystore after [REMOTE] keystore with same id": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + res2 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isErr + res2.error().kind == DuplicateKeystoreDir + + validatorsCount == 1 + secretsCount == 0 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + namesEqual(validatorPubKeys, [MultipleKeystoreNames[0]]) + + test "Save [LOCAL] keystore after [LOCAL] keystore with different id": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + res2 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey1, curCookedKey1, curSigningPath, + "", mode = Fast) + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + + validatorsCount == 2 + secretsCount == 2 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + dirExists(curKeystoreDir1) + not(fileExists(remoteKeystoreFile1)) + fileExists(localKeystoreFile1) + fileExists(curSecretsFile1) + + namesEqual(validatorPubKeys, + [MultipleKeystoreNames[0], MultipleKeystoreNames[1]]) + + test "Save [REMOTE] keystore after [REMOTE] keystore with different id": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + res2 = saveKeystore(testValidatorsDir, curPublicKey1, MultipleRemoteUri) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + + validatorsCount == 2 + secretsCount == 0 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + dirExists(curKeystoreDir1) + fileExists(remoteKeystoreFile1) + not(fileExists(localKeystoreFile1)) + not(fileExists(curSecretsFile1)) + + namesEqual(validatorPubKeys, + [MultipleKeystoreNames[0], MultipleKeystoreNames[1]]) + + test "Save [LOCAL] keystore after [REMOTE] keystore with different id": + let + res1 = saveKeystore(testValidatorsDir, curPublicKey0, MultipleRemoteUri) + res2 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey1, curCookedKey1, curSigningPath, + "", mode = Fast) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + + validatorsCount == 2 + secretsCount == 1 + + dirExists(curKeystoreDir0) + fileExists(remoteKeystoreFile0) + not(fileExists(localKeystoreFile0)) + not(fileExists(curSecretsFile0)) + + dirExists(curKeystoreDir1) + not(fileExists(remoteKeystoreFile1)) + fileExists(localKeystoreFile1) + fileExists(curSecretsFile1) + + namesEqual(validatorPubKeys, + [MultipleKeystoreNames[0], MultipleKeystoreNames[1]]) + + test "Save [REMOTE] keystore after [LOCAL] keystore with different id": + let + res1 = saveKeystore(rng[], testValidatorsDir, testSecretsDir, + curSigningKey0, curCookedKey0, curSigningPath, + "", mode = Fast) + res2 = saveKeystore(testValidatorsDir, curPublicKey1, MultipleRemoteUri) + + validatorsCount = directoryItemsCount(testValidatorsDir) + secretsCount = directoryItemsCount(testSecretsDir) + + validatorPubKeys = validatorPubKeysInDir(testValidatorsDir) + + check: + res1.isOk + res2.isOk + + validatorsCount == 2 + secretsCount == 1 + + dirExists(curKeystoreDir0) + not(fileExists(remoteKeystoreFile0)) + fileExists(localKeystoreFile0) + fileExists(curSecretsFile0) + + dirExists(curKeystoreDir1) + fileExists(remoteKeystoreFile1) + not(fileExists(localKeystoreFile1)) + not(fileExists(curSecretsFile1)) + + namesEqual(validatorPubKeys, + [MultipleKeystoreNames[0], MultipleKeystoreNames[1]])