From 0ff86e953871c306c4be0a44d8db21fe37fefc7b Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Thu, 6 Apr 2023 16:16:21 +0300 Subject: [PATCH] web3signer refactoring and test suite. (#4775) * Refactor nimbus_signing_node to support Unix signals. * Fix SN unable to close REST server properly. * Fix `keys`, `deposit` and `validator_registration` endpoints issues. Add getValidatorExitSignature() and getDepositMessageSignature() to validator_pool. * Add /reload endpoint and implementation. Fix signData to not cancel `timer`. Fix validator_pool should clear attachedValidators table. * Diva protocol enhancement implementation. --- AllTests-mainnet.md | 38 +- Makefile | 2 +- beacon_chain/conf.nim | 5 + beacon_chain/nimbus_signing_node.nim | 442 +++-- beacon_chain/rpc/rest_constants.nim | 6 + .../eth2_apis/eth2_rest_serialization.nim | 25 +- .../eth2_apis/rest_remote_signer_calls.nim | 305 +++- beacon_chain/spec/eth2_apis/rest_types.nim | 27 +- beacon_chain/spec/keystore.nim | 7 +- .../validators/keystore_management.nim | 3 +- beacon_chain/validators/validator_pool.nim | 326 +++- tests/all_tests.nim | 1 + tests/test_signing_node.nim | 1595 +++++++++++++++++ vendor/nim-presto | 2 +- 14 files changed, 2418 insertions(+), 366 deletions(-) create mode 100644 tests/test_signing_node.nim diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index b0db09901..b8790e1ae 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -364,6 +364,42 @@ OK: 4/4 Fail: 0/4 Skip: 0/4 + Voluntary exit signatures OK ``` OK: 8/8 Fail: 0/8 Skip: 0/8 +## Nimbus remote signer/signing test (web3signer) +```diff ++ Connection timeout test OK ++ Connections pool stress test OK ++ Idle connection test OK ++ Public keys enumeration (/api/v1/eth2/publicKeys) test OK ++ Public keys reload (/reload) test OK ++ Signing BeaconBlock (getBlockSignature(altair)) OK ++ Signing BeaconBlock (getBlockSignature(bellatrix)) OK ++ Signing BeaconBlock (getBlockSignature(capella)) OK ++ Signing BeaconBlock (getBlockSignature(deneb)) OK ++ Signing BeaconBlock (getBlockSignature(phase0)) OK ++ Signing SC contribution and proof (getContributionAndProofSignature()) OK ++ Signing SC message (getSyncCommitteeMessage()) OK ++ Signing SC selection proof (getSyncCommitteeSelectionProof()) OK ++ Signing aggregate and proof (getAggregateAndProofSignature()) OK ++ Signing aggregation slot (getSlotSignature()) OK ++ Signing attestation (getAttestationSignature()) OK ++ Signing deposit message (getDepositMessageSignature()) OK ++ Signing phase0 block OK ++ Signing randao reveal (getEpochSignature()) OK ++ Signing validator registration (getBuilderSignature()) OK ++ Signing voluntary exit (getValidatorExitSignature()) OK ++ Waiting for signing node (/upcheck) test OK +``` +OK: 22/22 Fail: 0/22 Skip: 0/22 +## Nimbus remote signer/signing test (web3signer-diva) +```diff ++ Signing BeaconBlock (getBlockSignature(altair)) OK ++ Signing BeaconBlock (getBlockSignature(bellatrix)) OK ++ Signing BeaconBlock (getBlockSignature(capella)) OK ++ Signing BeaconBlock (getBlockSignature(deneb)) OK ++ Signing BeaconBlock (getBlockSignature(phase0)) OK ++ Waiting for signing node (/upcheck) test OK +``` +OK: 6/6 Fail: 0/6 Skip: 0/6 ## Old database versions [Preset: mainnet] ```diff + pre-1.1.0 OK @@ -640,4 +676,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 357/362 Fail: 0/362 Skip: 5/362 +OK: 385/390 Fail: 0/390 Skip: 5/390 diff --git a/Makefile b/Makefile index b132dfea6..7ec965a9a 100644 --- a/Makefile +++ b/Makefile @@ -340,7 +340,7 @@ FORCE_BUILD_ALONE_ALL_TESTS_DEPS := endif force_build_alone_all_tests: | $(FORCE_BUILD_ALONE_ALL_TESTS_DEPS) -all_tests: | build deps force_build_alone_all_tests +all_tests: | build deps nimbus_signing_node force_build_alone_all_tests + echo -e $(BUILD_MSG) "build/$@" && \ MAKE="$(MAKE)" V="$(V)" $(ENV_SCRIPT) scripts/compile_nim_program.sh \ $@ \ diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 907e063f7..cb30ed7cd 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -986,6 +986,11 @@ type desc: "A directory containing validator keystore passwords" name: "secrets-dir" .}: Option[InputDir] + expectedFeeRecipient* {. + desc: "Signatures for blocks will require proofs of the specified " & + "fee recipient" + name: "expected-fee-recipient".}: Option[Address] + serverIdent* {. desc: "Server identifier which will be used in HTTP Host header" name: "server-ident" .}: Option[string] diff --git a/beacon_chain/nimbus_signing_node.nim b/beacon_chain/nimbus_signing_node.nim index d41f09c5a..5009f5359 100644 --- a/beacon_chain/nimbus_signing_node.nim +++ b/beacon_chain/nimbus_signing_node.nim @@ -8,7 +8,7 @@ import std/[tables, os, strutils] import serialization, json_serialization, json_serialization/std/[options, net], chronos, presto, presto/secureserver, chronicles, confutils, - stew/[base10, results, byteutils, io2] + stew/[base10, results, byteutils, io2, bitops2] import "."/spec/datatypes/[base, altair, phase0], "."/spec/[crypto, digest, network, signatures, forks], "."/spec/eth2_apis/[rest_types, eth2_rest_serialization], @@ -36,36 +36,51 @@ type signingServer: SigningNodeServer keystoreCache: KeystoreCacheRef keysList: string + runKeystoreCachePruningLoopFut: Future[void] + sigintHandleFut: Future[void] + sigtermHandleFut: Future[void] -proc getRouter*(): RestRouter + SigningNodeRef* = ref SigningNode -proc router(sn: SigningNode): RestRouter = + SigningNodeError* = object of CatchableError + +proc validate(key: string, value: string): int = + case key + of "{validator_key}": + 0 + else: + 1 + +proc getRouter*(): RestRouter = + RestRouter.init(validate) + +proc router(sn: SigningNodeRef): RestRouter = case sn.signingServer.kind of SigningNodeKind.Secure: sn.signingServer.sserver.router of SigningNodeKind.NonSecure: sn.signingServer.nserver.router -proc start(sn: SigningNode) = +proc start(sn: SigningNodeRef) = case sn.signingServer.kind of SigningNodeKind.Secure: sn.signingServer.sserver.start() of SigningNodeKind.NonSecure: sn.signingServer.nserver.start() -proc stop(sn: SigningNode) {.async.} = +proc stop(sn: SigningNodeRef) {.async.} = case sn.signingServer.kind of SigningNodeKind.Secure: await sn.signingServer.sserver.stop() of SigningNodeKind.NonSecure: await sn.signingServer.nserver.stop() -proc close(sn: SigningNode) {.async.} = +proc close(sn: SigningNodeRef) {.async.} = case sn.signingServer.kind of SigningNodeKind.Secure: - await sn.signingServer.sserver.stop() + await sn.signingServer.sserver.closeWait() of SigningNodeKind.NonSecure: - await sn.signingServer.nserver.stop() + await sn.signingServer.nserver.closeWait() proc loadTLSCert(pathName: InputFile): Result[TLSCertificate, cstring] = let data = @@ -95,106 +110,49 @@ proc loadTLSKey(pathName: InputFile): Result[TLSPrivateKey, cstring] = return err("Invalid private key or incorrect file format") ok(key) -proc initValidators(sn: var SigningNode): bool = - info "Initializaing validators", path = sn.config.validatorsDir() - var publicKeyIdents: seq[string] - for keystore in listLoadableKeystores(sn.config, sn.keystoreCache): - # Not relevant in signing node - # TODO don't print when loading validators - let feeRecipient = default(Eth1Address) - case keystore.kind - of KeystoreKind.Local: - discard sn.attachedValidators.addValidator(keystore, - feeRecipient, - defaultGasLimit) - publicKeyIdents.add("\"0x" & keystore.pubkey.toHex() & "\"") - of KeystoreKind.Remote: - error "Signing node do not support remote validators", - validator_pubkey = keystore.pubkey - return false - sn.keysList = "[" & publicKeyIdents.join(", ") & "]" - true - -proc init(t: typedesc[SigningNode], config: SigningNodeConf): SigningNode = - var sn = SigningNode( - config: config, - keystoreCache: KeystoreCacheRef.init() - ) - - if not(initValidators(sn)): - fatal "Could not find/initialize local validators" - quit 1 - - asyncSpawn runKeystoreCachePruningLoop(sn.keystoreCache) - - let - address = initTAddress(config.bindAddress, config.bindPort) - serverFlags = {HttpServerFlags.QueryCommaSeparatedArray, - HttpServerFlags.NotifyDisconnect} - timeout = - if config.requestTimeout < 0: - warn "Negative value of request timeout, using default instead" - seconds(defaultSigningNodeRequestTimeout) - else: - seconds(config.requestTimeout) - serverIdent = - if config.serverIdent.isSome(): - config.serverIdent.get() - else: - NimbusSigningNodeIdent - - sn.signingServer = - if config.tlsEnabled: - if config.tlsCertificate.isNone(): - fatal "TLS certificate path is missing, please use --tls-cert option" - quit 1 - - if config.tlsPrivateKey.isNone(): - fatal "TLS private key path is missing, please use --tls-key option" - quit 1 - - let cert = - block: - let res = loadTLSCert(config.tlsCertificate.get()) - if res.isErr(): - fatal "Could not initialize SSL certificate", reason = $res.error() - quit 1 - res.get() - let key = - block: - let res = loadTLSKey(config.tlsPrivateKey.get()) - if res.isErr(): - fatal "Could not initialize SSL private key", reason = $res.error() - quit 1 - res.get() - let res = SecureRestServerRef.new(getRouter(), address, key, cert, - serverFlags = serverFlags, - httpHeadersTimeout = timeout, - serverIdent = serverIdent) - if res.isErr(): - fatal "HTTPS(REST) server could not be started", address = $address, - reason = $res.error() - quit 1 - SigningNodeServer(kind: SigningNodeKind.Secure, sserver: res.get()) - else: - let res = RestServerRef.new(getRouter(), address, - serverFlags = serverFlags, - httpHeadersTimeout = timeout, - serverIdent = serverIdent) - if res.isErr(): - fatal "HTTP(REST) server could not be started", address = $address, - reason = $res.error() - quit 1 - SigningNodeServer(kind: SigningNodeKind.NonSecure, nserver: res.get()) - sn +proc new(t: typedesc[SigningNodeRef], config: SigningNodeConf): SigningNodeRef = + when declared(waitSignal): + SigningNodeRef( + config: config, + sigintHandleFut: waitSignal(SIGINT), + sigtermHandleFut: waitSignal(SIGTERM), + keystoreCache: KeystoreCacheRef.init() + ) + else: + SigningNodeRef( + config: config, + sigintHandleFut: newFuture[void]("sigint_placeholder"), + sigtermHandleFut: newFuture[void]("sigterm_placeholder"), + keystoreCache: KeystoreCacheRef.init() + ) template errorResponse(code: HttpCode, message: string): RestApiResponse = RestApiResponse.response("{\"error\": \"" & message & "\"}", code) template signatureResponse(code: HttpCode, signature: string): RestApiResponse = - RestApiResponse.response("{\"signature\": \"0x" & signature & "\"}", code, "application/json") + RestApiResponse.response("{\"signature\": \"0x" & signature & "\"}", + code, "application/json") -proc installApiHandlers*(node: SigningNode) = +proc loadKeystores*(node: SigningNodeRef) = + var keysList: seq[string] + for keystore in listLoadableKeystores(node.config, node.keystoreCache): + # Not relevant in signing node + # TODO don't print when loading validators + let feeRecipient = default(Eth1Address) + case keystore.kind + of KeystoreKind.Local: + discard node.attachedValidators.addValidator(keystore, + feeRecipient, + defaultGasLimit) + keysList.add("\"0x" & keystore.pubkey.toHex() & "\"") + of KeystoreKind.Remote: + warn "Signing node do not support remote validators", + path = node.config.validatorsDir(), + validator_pubkey = keystore.pubkey + + node.keysList = "[" & keysList.join(", ") & "]" + +proc installApiHandlers*(node: SigningNodeRef) = var router = node.router() router.api(MethodGet, "/api/v1/eth2/publicKeys") do () -> RestApiResponse: @@ -205,6 +163,11 @@ proc installApiHandlers*(node: SigningNode) = return RestApiResponse.response("{\"status\": \"OK\"}", Http200, "application/json") + router.api(MethodPost, "/reload") do () -> RestApiResponse: + node.attachedValidators.close() + node.loadKeystores() + return RestApiResponse.response(Http200) + router.api(MethodPost, "/api/v1/eth2/sign/{validator_key}") do ( validator_key: ValidatorPubKey, contentBody: Option[ContentBody]) -> RestApiResponse: @@ -231,108 +194,137 @@ proc installApiHandlers*(node: SigningNode) = of Web3SignerRequestKind.AggregationSlot: let forkInfo = request.forkInfo.get() - cooked = get_slot_signature(forkInfo.fork, + signature = get_slot_signature(forkInfo.fork, forkInfo.genesis_validators_root, - request.aggregationSlot.slot, validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + request.aggregationSlot.slot, + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.AggregateAndProof: let forkInfo = request.forkInfo.get() - cooked = get_aggregate_and_proof_signature(forkInfo.fork, + signature = get_aggregate_and_proof_signature(forkInfo.fork, forkInfo.genesis_validators_root, request.aggregateAndProof, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.Attestation: let forkInfo = request.forkInfo.get() - cooked = get_attestation_signature(forkInfo.fork, + signature = get_attestation_signature(forkInfo.fork, forkInfo.genesis_validators_root, request.attestation, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.Block: let forkInfo = request.forkInfo.get() blck = request.blck - blockRoot = hash_tree_root(blck) - cooked = get_block_signature(forkInfo.fork, - forkInfo.genesis_validators_root, blck.slot, blockRoot, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + signature = get_block_signature(forkInfo.fork, + forkInfo.genesis_validators_root, blck.slot, hash_tree_root(blck), + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.BlockV2: - let - forkInfo = request.forkInfo.get() - forked = request.beaconBlock - blockRoot = hash_tree_root(forked) - cooked = - withBlck(forked): + if node.config.expectedFeeRecipient.isNone(): + let + forkInfo = request.forkInfo.get() + blockRoot = hash_tree_root(request.beaconBlock) + signature = withBlck(request.beaconBlock): get_block_signature(forkInfo.fork, forkInfo.genesis_validators_root, blck.slot, blockRoot, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() + return signatureResponse(Http200, signature) + + let (feeRecipientIndex, blockHeader) = + case request.beaconBlock.kind + of ConsensusFork.Phase0, ConsensusFork.Altair: + # `phase0` and `altair` blocks do not have `fee_recipient`, so + # we return an error. + return errorResponse(Http400, BlockIncorrectFork) + of ConsensusFork.Bellatrix: + (GeneralizedIndex(401), request.beaconBlock.bellatrixData) + of ConsensusFork.Capella: + (GeneralizedIndex(401), request.beaconBlock.capellaData) + of ConsensusFork.Deneb: + (GeneralizedIndex(401), request.beaconBlock.denebData) + + if request.proofs.isNone() or len(request.proofs.get()) == 0: + return errorResponse(Http400, MissingMerkleProofError) + + let proof = request.proofs.get()[0] + + if proof.index != feeRecipientIndex: + return errorResponse(Http400, InvalidMerkleProofIndexError) + + let feeRecipientRoot = hash_tree_root(distinctBase( + node.config.expectedFeeRecipient.get())) + + if not(is_valid_merkle_branch(feeRecipientRoot, proof.merkleProofs, + log2trunc(proof.index), + get_subtree_index(proof.index), + blockHeader.body_root)): + return errorResponse(Http400, InvalidMerkleProofError) + + let + forkInfo = request.forkInfo.get() + blockRoot = hash_tree_root(request.beaconBlock) + signature = withBlck(request.beaconBlock): + get_block_signature(forkInfo.fork, + forkInfo.genesis_validators_root, blck.slot, blockRoot, + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.Deposit: let data = DepositMessage(pubkey: request.deposit.pubkey, withdrawal_credentials: request.deposit.withdrawalCredentials, amount: request.deposit.amount) - cooked = get_deposit_signature(data, - request.deposit.genesisForkVersion, validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + signature = get_deposit_signature(data, + request.deposit.genesisForkVersion, + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.RandaoReveal: let forkInfo = request.forkInfo.get() - cooked = get_epoch_signature(forkInfo.fork, + signature = get_epoch_signature(forkInfo.fork, forkInfo.genesis_validators_root, request.randaoReveal.epoch, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.VoluntaryExit: let forkInfo = request.forkInfo.get() - cooked = get_voluntary_exit_signature(forkInfo.fork, + signature = get_voluntary_exit_signature(forkInfo.fork, forkInfo.genesis_validators_root, request.voluntaryExit, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.SyncCommitteeMessage: let forkInfo = request.forkInfo.get() msg = request.syncCommitteeMessage - cooked = get_sync_committee_message_signature(forkInfo.fork, + signature = get_sync_committee_message_signature(forkInfo.fork, forkInfo.genesis_validators_root, msg.slot, msg.beaconBlockRoot, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.SyncCommitteeSelectionProof: let forkInfo = request.forkInfo.get() msg = request.syncAggregatorSelectionData - subcommittee = SyncSubcommitteeIndex.init(msg.subcommittee_index).valueOr: - return errorResponse(Http400, InvalidSubCommitteeIndexValueError) - cooked = get_sync_committee_selection_proof(forkInfo.fork, + subcommittee = + SyncSubcommitteeIndex.init(msg.subcommittee_index).valueOr: + return errorResponse(Http400, InvalidSubCommitteeIndexValueError) + signature = get_sync_committee_selection_proof(forkInfo.fork, forkInfo.genesis_validators_root, msg.slot, subcommittee, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.SyncCommitteeContributionAndProof: let forkInfo = request.forkInfo.get() msg = request.syncCommitteeContributionAndProof - cooked = get_contribution_and_proof_signature( + signature = get_contribution_and_proof_signature( forkInfo.fork, forkInfo.genesis_validators_root, msg, - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) of Web3SignerRequestKind.ValidatorRegistration: let forkInfo = request.forkInfo.get() - cooked = get_builder_signature( - forkInfo.fork, ValidatorRegistrationV1( + signature = get_builder_signature(forkInfo.fork, + ValidatorRegistrationV1( fee_recipient: ExecutionAddress(data: distinctBase(Eth1Address.fromHex( request.validatorRegistration.feeRecipient))), @@ -340,34 +332,142 @@ proc installApiHandlers*(node: SigningNode) = timestamp: request.validatorRegistration.timestamp, pubkey: request.validatorRegistration.pubkey, ), - validator.data.privateKey) - signature = cooked.toValidatorSig().toHex() + validator.data.privateKey).toValidatorSig().toHex() signatureResponse(Http200, signature) -proc validate(key: string, value: string): int = - case key - of "{validator_key}": - 0 - else: - 1 - -proc getRouter*(): RestRouter = - RestRouter.init(validate) - -programMain: - let config = makeBannerAndConfig("Nimbus signing node " & fullVersionStr, - SigningNodeConf) - setupLogging(config.logLevel, config.logStdout, config.logFile) - - var sn = SigningNode.init(config) +proc asyncInit(sn: SigningNodeRef) {.async.} = notice "Launching signing node", version = fullVersionStr, - cmdParams = commandLineParams(), config, - validators_count = sn.attachedValidators.count() + cmdParams = commandLineParams(), config = sn.config + + info "Initializaing validators", path = sn.config.validatorsDir() + sn.loadKeystores() + + if sn.attachedValidators.count() == 0: + fatal "Could not find/initialize local validators" + raise newException(SigningNodeError, "") + + let + address = initTAddress(sn.config.bindAddress, sn.config.bindPort) + serverFlags = {HttpServerFlags.QueryCommaSeparatedArray, + HttpServerFlags.NotifyDisconnect} + timeout = + if sn.config.requestTimeout < 0: + warn "Negative value of request timeout, using default instead" + seconds(defaultSigningNodeRequestTimeout) + else: + seconds(sn.config.requestTimeout) + serverIdent = + if sn.config.serverIdent.isSome(): + sn.config.serverIdent.get() + else: + NimbusSigningNodeIdent + + sn.signingServer = + if sn.config.tlsEnabled: + if sn.config.tlsCertificate.isNone(): + fatal "TLS certificate path is missing, please use --tls-cert option" + raise newException(SigningNodeError, "") + + if sn.config.tlsPrivateKey.isNone(): + fatal "TLS private key path is missing, please use --tls-key option" + raise newException(SigningNodeError, "") + + let cert = + block: + let res = loadTLSCert(sn.config.tlsCertificate.get()) + if res.isErr(): + fatal "Could not initialize SSL certificate", + reason = $res.error() + raise newException(SigningNodeError, "") + res.get() + let key = + block: + let res = loadTLSKey(sn.config.tlsPrivateKey.get()) + if res.isErr(): + fatal "Could not initialize SSL private key", + reason = $res.error() + raise newException(SigningNodeError, "") + res.get() + let res = SecureRestServerRef.new(getRouter(), address, key, cert, + serverFlags = serverFlags, + httpHeadersTimeout = timeout, + serverIdent = serverIdent) + if res.isErr(): + fatal "HTTPS(REST) server could not be started", address = $address, + reason = $res.error() + raise newException(SigningNodeError, "") + SigningNodeServer(kind: SigningNodeKind.Secure, sserver: res.get()) + else: + let res = RestServerRef.new(getRouter(), address, + serverFlags = serverFlags, + httpHeadersTimeout = timeout, + serverIdent = serverIdent) + if res.isErr(): + fatal "HTTP(REST) server could not be started", address = $address, + reason = $res.error() + raise newException(SigningNodeError, "") + SigningNodeServer(kind: SigningNodeKind.NonSecure, nserver: res.get()) + +proc asyncRun*(sn: SigningNodeRef) {.async.} = + sn.runKeystoreCachePruningLoopFut = + runKeystorecachePruningLoop(sn.keystoreCache) sn.installApiHandlers() sn.start() + + var future = newFuture[void]("signing-node-mainLoop") try: - runForever() - finally: - waitFor sn.stop() - waitFor sn.close() - discard sn.stop() + await future + except CancelledError: + debug "Main loop interrupted" + except CatchableError as exc: + warn "Main loop failed with unexpected error", err_name = $exc.name, + reason = $exc.msg + + debug "Stopping main processing loop" + var pending: seq[Future[void]] + if not(sn.runKeystoreCachePruningLoopFut.finished()): + pending.add(cancelAndWait(sn.runKeystoreCachePruningLoopFut)) + pending.add(sn.stop()) + pending.add(sn.close()) + await allFutures(pending) + +template runWithSignals(sn: SigningNodeRef, body: untyped): bool = + let future = body + discard await race(future, sn.sigintHandleFut, sn.sigtermHandleFut) + if future.finished(): + if future.failed() or future.cancelled(): + let exc = future.readError() + debug "Signing node initialization failed" + var pending: seq[Future[void]] + if not(sn.sigintHandleFut.finished()): + pending.add(cancelAndWait(sn.sigintHandleFut)) + if not(sn.sigtermHandleFut.finished()): + pending.add(cancelAndWait(sn.sigtermHandleFut)) + await allFutures(pending) + false + else: + true + else: + let signal = if sn.sigintHandleFut.finished(): "SIGINT" else: "SIGTERM" + info "Got interrupt, trying to shutdown gracefully", signal = signal + var pending = @[cancelAndWait(future)] + if not(sn.sigintHandleFut.finished()): + pending.add(cancelAndWait(sn.sigintHandleFut)) + if not(sn.sigtermHandleFut.finished()): + pending.add(cancelAndWait(sn.sigtermHandleFut)) + await allFutures(pending) + false + +proc runSigningNode(config: SigningNodeConf) {.async.} = + let sn = SigningNodeRef.new(config) + if not sn.runWithSignals(asyncInit sn): + return + if not sn.runWithSignals(asyncRun sn): + return + +programMain: + let config = + makeBannerAndConfig("Nimbus signing node " & fullVersionStr, + SigningNodeConf) + setupLogging(config.logLevel, config.logStdout, config.logFile) + waitFor runSigningNode(config) diff --git a/beacon_chain/rpc/rest_constants.nim b/beacon_chain/rpc/rest_constants.nim index 9c4c94ba8..ca3e8951d 100644 --- a/beacon_chain/rpc/rest_constants.nim +++ b/beacon_chain/rpc/rest_constants.nim @@ -228,3 +228,9 @@ const "BLS to execution change was broadcast" AggregationSelectionNotImplemented* = "Attestation and sync committee aggreggation selection are not implemented" + MissingMerkleProofError* = + "Required merkle proof is missing" + InvalidMerkleProofError* = + "The given merkle proof is invalid" + InvalidMerkleProofIndexError* = + "The given merkle proof index is invalid" diff --git a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim index 52311703e..97882a4f1 100644 --- a/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim +++ b/beacon_chain/spec/eth2_apis/eth2_rest_serialization.nim @@ -1901,6 +1901,8 @@ proc writeValue*(writer: var JsonWriter[RestJson], # https://github.com/ConsenSys/web3signer/blob/41c0cbfabcb1fca9587b59e058b7eb29f152c60c/core/src/main/resources/openapi-specs/eth2/signing/schemas.yaml#L418-L497 writer.writeField("beacon_block", value.beaconBlock) + if isSome(value.proofs): + writer.writeField("proofs", value.proofs.get()) of Web3SignerRequestKind.Deposit: writer.writeField("type", "DEPOSIT") if isSome(value.signingRoot): @@ -1967,6 +1969,7 @@ proc readValue*(reader: var JsonReader[RestJson], forkInfo: Option[Web3SignerForkInfo] signingRoot: Option[Eth2Digest] data: Option[JsonString] + proofs: seq[Web3SignerMerkleProof] dataName: string for fieldName in readObjectFields(reader): @@ -2015,14 +2018,19 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedField("Multiple `signingRoot` fields found", "Web3SignerRequest") signingRoot = some(reader.readValue(Eth2Digest)) + of "proofs": + let newProofs = reader.readValue(seq[Web3SignerMerkleProof]) + proofs.add(newProofs) of "aggregation_slot", "aggregate_and_proof", "block", "beacon_block", "randao_reveal", "voluntary_exit", "sync_committee_message", - "sync_aggregator_selection_data", "contribution_and_proof", "attestation": + "sync_aggregator_selection_data", "contribution_and_proof", + "attestation", "deposit", "validator_registration": if data.isSome(): reader.raiseUnexpectedField("Multiple data fields found", "Web3SignerRequest") dataName = fieldName data = some(reader.readValue(JsonString)) + else: unrecognizedFieldWarning() @@ -2108,10 +2116,17 @@ proc readValue*(reader: var JsonReader[RestJson], reader.raiseUnexpectedValue( "Incorrect field `beacon_block` format") res.get() - Web3SignerRequest( - kind: Web3SignerRequestKind.BlockV2, - forkInfo: forkInfo, signingRoot: signingRoot, beaconBlock: data - ) + if len(proofs) > 0: + Web3SignerRequest( + kind: Web3SignerRequestKind.BlockV2, + forkInfo: forkInfo, signingRoot: signingRoot, beaconBlock: data, + proofs: Opt.some(proofs) + ) + else: + Web3SignerRequest( + kind: Web3SignerRequestKind.BlockV2, + forkInfo: forkInfo, signingRoot: signingRoot, beaconBlock: data + ) of Web3SignerRequestKind.Deposit: if dataName != "deposit": reader.raiseUnexpectedValue("Field `deposit` is missing") diff --git a/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim b/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim index c8b563688..027636970 100644 --- a/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim +++ b/beacon_chain/spec/eth2_apis/rest_remote_signer_calls.nim @@ -10,14 +10,23 @@ import chronicles, metrics, chronos, chronos/apps/http/httpclient, presto, presto/client, serialization, json_serialization, - json_serialization/std/[options, net, sets], + json_serialization/std/[net, sets], stew/[results, base10, byteutils], "."/[rest_types, eth2_rest_serialization] export chronos, httpclient, client, rest_types, eth2_rest_serialization, results type - Web3SignerResult*[T] = Result[T, string] + Web3SignerErrorKind* {.pure.} = enum + Error400, Error404, Error412, Error500, CommError, UnexpectedError, + UknownStatus, InvalidContentType, InvalidPlain, InvalidContent, + InvalidSignature, TimeoutError + + Web3SignerError* = object + kind*: Web3SignerErrorKind + message*: string + + Web3SignerResult*[T] = Result[T, Web3SignerError] Web3SignerDataResponse* = Web3SignerResult[CookedSig] declareCounter nbc_remote_signer_requests, @@ -50,7 +59,7 @@ declareCounter nbc_remote_signer_unknown_responses, declareCounter nbc_remote_signer_communication_errors, "Number of communication errors" -declareHistogram nbc_remote_signer_time, +declareHistogram nbc_remote_signer_duration, "Time(s) used to generate signature usign remote signer", buckets = [0.050, 0.100, 0.500, 1.0, 5.0, 10.0] @@ -59,6 +68,11 @@ proc getUpcheck*(): RestResponse[Web3SignerStatusResponse] {. meth: MethodGet, accept: "application/json" .} ## https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Server-Status +proc reload*(): RestPlainResponse {. + rest, endpoint: "/reload", + meth: MethodPost, accept: "application/json" .} + ## https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Reload-Signer-Keys/operation/RELOAD + proc getKeys*(): RestResponse[Web3SignerKeysResponse] {. rest, endpoint: "/api/v1/eth2/publicKeys", meth: MethodGet, accept: "application/json" .} @@ -70,132 +84,225 @@ proc signDataPlain*(identifier: ValidatorPubKey, meth: MethodPost, accept: "application/json" .} # https://consensys.github.io/web3signer/web3signer-eth2.html#tag/Signing +proc init(t: typedesc[Web3SignerError], kind: Web3SignerErrorKind, + message: string): Web3SignerError = + Web3SignerError(kind: kind, message: message) + proc signData*(client: RestClientRef, identifier: ValidatorPubKey, body: Web3SignerRequest ): Future[Web3SignerDataResponse] {.async.} = - let startSignTick = Moment.now() inc(nbc_remote_signer_requests) - let response = - try: - await client.signDataPlain(identifier, body, - restAcceptType = "application/json") - except RestError as exc: - let msg = "[" & $exc.name & "] " & $exc.msg - debug "Error occured while generating signature", - validator = shortLog(identifier), - remote_signer = $client.address.getUri(), - error_name = $exc.name, error_msg = $exc.msg, - signDur = Moment.now() - startSignTick - inc(nbc_remote_signer_communication_errors) - return Web3SignerDataResponse.err(msg) - except CatchableError as exc: - let msg = "[" & $exc.name & "] " & $exc.msg - debug "Unexpected error occured while generating signature", - validator = shortLog(identifier), - remote_signer = $client.address.getUri(), - error_name = $exc.name, error_msg = $exc.msg, - signDur = Moment.now() - startSignTick - inc(nbc_remote_signer_communication_errors) - return Web3SignerDataResponse.err(msg) - let res = + let + startSignMoment = Moment.now() + response = + try: + let + res = await client.signDataPlain(identifier, body, + restAcceptType = "application/json") + duration = Moment.now() - startSignMoment + nbc_remote_signer_duration.observe( + float(milliseconds(duration)) / 1000.0) + res + except RestError as exc: + return Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.CommError, $exc.msg)) + except CancelledError as exc: + raise exc + except CatchableError as exc: + return Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.UnexpectedError, $exc.msg)) + + return case response.status of 200: inc(nbc_remote_signer_200_responses) let sig = if response.contentType.isNone() or isWildCard(response.contentType.get().mediaType): + inc(nbc_remote_signer_failures) return Web3SignerDataResponse.err( - "Unable to decode signature from missing or incorrect content") + Web3SignerError.init( + Web3SignerErrorKind.InvalidContentType, + "Unable to decode signature from missing or incorrect content" + ) + ) else: let mediaType = response.contentType.get().mediaType if mediaType == TextPlainMediaType: - let asStr = fromBytes(string, response.data) - let sigFromText = fromHex(ValidatorSig, asStr) - if sigFromText.isErr: - return Web3SignerDataResponse.err( - "Unable to decode signature from plain text") - sigFromText.get.load + let + asStr = fromBytes(string, response.data) + sigFromText = fromHex(ValidatorSig, asStr).valueOr: + inc(nbc_remote_signer_failures) + return Web3SignerDataResponse.err( + Web3SignerError.init( + Web3SignerErrorKind.InvalidPlain, + "Unable to decode signature from plain text" + ) + ) + sigFromText.load() else: let res = decodeBytes(Web3SignerSignatureResponse, response.data, - response.contentType) - if res.isErr: - let msg = "Unable to decode remote signer response [" & - $res.error() & "]" + response.contentType).valueOr: inc(nbc_remote_signer_failures) - return Web3SignerDataResponse.err(msg) - res.get.signature.load + return Web3SignerDataResponse.err( + Web3SignerError.init( + Web3SignerErrorKind.InvalidContent, + "Unable to decode remote signer response [" & $error & "]" + ) + ) + res.signature.load() - if sig.isNone: - let msg = "Remote signer returns invalid signature" + if sig.isNone(): inc(nbc_remote_signer_failures) - return Web3SignerDataResponse.err(msg) + return Web3SignerDataResponse.err( + Web3SignerError.init( + Web3SignerErrorKind.InvalidSignature, + "Remote signer returns invalid signature" + ) + ) - Web3SignerDataResponse.ok(sig.get) + inc(nbc_remote_signer_signatures) + Web3SignerDataResponse.ok(sig.get()) of 400: inc(nbc_remote_signer_400_responses) - let res = decodeBytes(Web3SignerErrorResponse, response.data, - response.contentType) - let msg = - if res.isErr(): - "Remote signer returns 400 Bad Request Format Error" - else: - "Remote signer returns 400 Bad Request Format Error [" & - res.get().error & "]" - Web3SignerDataResponse.err(msg) + let message = + block: + let res = decodeBytes(Web3SignerErrorResponse, response.data, + response.contentType) + if res.isErr(): + "Remote signer returns 400 Bad Request Format Error" + else: + res.get().error + Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.Error400, message)) of 404: - let res = decodeBytes(Web3SignerErrorResponse, response.data, - response.contentType) - let msg = - if res.isErr(): - "Remote signer returns 404 Validator's Key Not Found Error" - else: - "Remote signer returns 404 Validator's Key Not Found Error [" & - res.get().error & "]" inc(nbc_remote_signer_404_responses) - Web3SignerDataResponse.err(msg) + let message = + block: + let res = decodeBytes(Web3SignerErrorResponse, response.data, + response.contentType) + if res.isErr(): + "Remote signer returns 404 Validator's Key Not Found Error" + else: + res.get().error + Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.Error404, message)) of 412: - let res = decodeBytes(Web3SignerErrorResponse, response.data, - response.contentType) - let msg = - if res.isErr(): - "Remote signer returns 412 Slashing Protection Error" - else: - "Remote signer returns 412 Slashing Protection Error [" & - res.get().error & "]" inc(nbc_remote_signer_412_responses) - Web3SignerDataResponse.err(msg) + let message = + block: + let res = decodeBytes(Web3SignerErrorResponse, response.data, + response.contentType) + if res.isErr(): + "Remote signer returns 412 Slashing Protection Error" + else: + res.get().error + Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.Error412, message)) of 500: - let res = decodeBytes(Web3SignerErrorResponse, response.data, - response.contentType) - let msg = - if res.isErr(): - "Remote signer returns 500 Internal Server Error" - else: - "Remote signer returns 500 Internal Server Error [" & - res.get().error & "]" inc(nbc_remote_signer_500_responses) - Web3SignerDataResponse.err(msg) + let message = + block: + let res = decodeBytes(Web3SignerErrorResponse, response.data, + response.contentType) + if res.isErr(): + "Remote signer returns 500 Internal Server Error" + else: + res.get().error + Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.Error500, message)) else: - let msg = "Remote signer returns unexpected status code " & - Base10.toString(uint64(response.status)) inc(nbc_remote_signer_unknown_responses) - Web3SignerDataResponse.err(msg) + let message = + block: + let res = decodeBytes(Web3SignerErrorResponse, response.data, + response.contentType) + if res.isErr(): + "Remote signer returns unexpected status code " & + Base10.toString(uint64(response.status)) + else: + res.get().error + Web3SignerDataResponse.err( + Web3SignerError.init(Web3SignerErrorKind.UknownStatus, message)) - if res.isOk(): - let delay = Moment.now() - startSignTick - inc(nbc_remote_signer_signatures) - nbc_remote_signer_time.observe(float(milliseconds(delay)) / 1000.0) - debug "Signature was successfully generated", - validator = shortLog(identifier), - remote_signer = $client.address.getUri(), - signDur = delay - else: - inc(nbc_remote_signer_failures) - debug "Signature generation was failed", - validator = shortLog(identifier), - remote_signer = $client.address.getUri(), - error_msg = res.error(), - signDur = Moment.now() - startSignTick +proc signData*( + client: RestClientRef, + identifier: ValidatorPubKey, + timerFut: Future[void], + attemptsCount: int, + body: Web3SignerRequest + ): Future[Web3SignerDataResponse] {.async.} = + doAssert(attemptsCount >= 1) - return res + const BackoffTimeouts = [ + 10.milliseconds, 100.milliseconds, 1.seconds, 2.seconds, 5.seconds + ] + + var + attempt = 0 + currentTimeout = 0 + + while true: + var + operationFut: Future[Web3SignerDataResponse] + lastError: Opt[Web3SignerError] + try: + operationFut = signData(client, identifier, body) + if isNil(timerFut): + await allFutures(operationFut) + else: + discard await race(timerFut, operationFut) + except CancelledError as exc: + if not(operationFut.finished()): + await operationFut.cancelAndWait() + raise exc + + if not(operationFut.finished()): + await operationFut.cancelAndWait() + if lastError.isSome(): + # We return last know error instead of timeout error. + return Web3SignerDataResponse.err(lastError.get()) + else: + return Web3SignerDataResponse.err( + Web3SignerError.init( + Web3SignerErrorKind.TimeoutError, + "Operation timed out" + ) + ) + else: + let resp = operationFut.read() + if resp.isOk(): + return resp + + case resp.error.kind + of Web3SignerErrorKind.Error404, + Web3SignerErrorKind.Error412, + Web3SignerErrorKind.Error500, + Web3SignerErrorKind.CommError, + Web3SignerErrorKind.UnexpectedError: + ## Non-critical errors + if attempt == attemptsCount: + # Number of attempts exceeded, so we return result we have. + return resp + else: + # We have some attempts left, so we show debug log about current + # attempt + debug "Unable to get signature using remote signer", + kind = resp.error.kind, reason = resp.error.message, + attempts_count = attemptsCount, attempt = attempt + lastError = Opt.some(resp.error) + inc(attempt) + await sleepAsync(BackoffTimeouts[currentTimeout]) + if currentTimeout < len(BackoffTimeouts) - 1: + inc currentTimeout + of Web3SignerErrorKind.Error400, + Web3SignerErrorKind.UknownStatus, + Web3SignerErrorKind.InvalidContentType, + Web3SignerErrorKind.InvalidPlain, + Web3SignerErrorKind.InvalidContent, + Web3SignerErrorKind.InvalidSignature: + # Critical errors + return resp + of Web3SignerErrorKind.TimeoutError: + raiseAssert "Timeout error should not be happened" diff --git a/beacon_chain/spec/eth2_apis/rest_types.nim b/beacon_chain/spec/eth2_apis/rest_types.nim index fd883b167..55f5605bd 100644 --- a/beacon_chain/spec/eth2_apis/rest_types.nim +++ b/beacon_chain/spec/eth2_apis/rest_types.nim @@ -520,8 +520,7 @@ type signature*: ValidatorSig slot*: Slot - Web3SignerKeysResponse* = object - keys*: seq[ValidatorPubKey] + Web3SignerKeysResponse* = seq[ValidatorPubKey] Web3SignerStatusResponse* = object status*: string @@ -564,6 +563,10 @@ type timestamp*: uint64 pubkey*: ValidatorPubKey + Web3SignerMerkleProof* = object + index*: GeneralizedIndex + merkleProofs* {.serializedFieldName: "merkle_proofs".}: seq[Eth2Digest] + Web3SignerRequestKind* {.pure.} = enum AggregationSlot, AggregateAndProof, Attestation, Block, BlockV2, Deposit, RandaoReveal, VoluntaryExit, SyncCommitteeMessage, @@ -588,6 +591,7 @@ type of Web3SignerRequestKind.BlockV2: beaconBlock* {. serializedFieldName: "beacon_block".}: Web3SignerForkedBeaconBlock + proofs*: Opt[seq[Web3SignerMerkleProof]] of Web3SignerRequestKind.Deposit: deposit*: Web3SignerDepositData of Web3SignerRequestKind.RandaoReveal: @@ -759,7 +763,8 @@ func init*(t: typedesc[Web3SignerRequest], fork: Fork, ) func init*(t: typedesc[Web3SignerRequest], fork: Fork, - genesis_validators_root: Eth2Digest, data: Web3SignerForkedBeaconBlock, + genesis_validators_root: Eth2Digest, + data: Web3SignerForkedBeaconBlock, signingRoot: Option[Eth2Digest] = none[Eth2Digest]() ): Web3SignerRequest = Web3SignerRequest( @@ -771,6 +776,22 @@ func init*(t: typedesc[Web3SignerRequest], fork: Fork, beaconBlock: data ) +func init*(t: typedesc[Web3SignerRequest], fork: Fork, + genesis_validators_root: Eth2Digest, + data: Web3SignerForkedBeaconBlock, + proofs: openArray[Web3SignerMerkleProof], + signingRoot: Option[Eth2Digest] = none[Eth2Digest]() + ): Web3SignerRequest = + Web3SignerRequest( + kind: Web3SignerRequestKind.BlockV2, + forkInfo: some(Web3SignerForkInfo( + fork: fork, genesis_validators_root: genesis_validators_root + )), + signingRoot: signingRoot, + proofs: Opt.some(@proofs), + beaconBlock: data + ) + func init*(t: typedesc[Web3SignerRequest], genesisForkVersion: Version, data: DepositMessage, signingRoot: Option[Eth2Digest] = none[Eth2Digest]() diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 0819ee87c..fd3b2b6d6 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -157,6 +157,7 @@ type flags*: set[RemoteKeystoreFlag] remotes*: seq[RemoteSignerInfo] threshold*: uint32 + remoteType*: RemoteSignerType NetKeystore* = object crypto*: Crypto @@ -166,7 +167,7 @@ type version*: int RemoteSignerType* {.pure.} = enum - Web3Signer + Web3Signer, Web3SignerDiva RemoteKeystore* = object version*: uint64 @@ -598,6 +599,8 @@ proc writeValue*(writer: var JsonWriter, value: RemoteKeystore) case value.remoteType of RemoteSignerType.Web3Signer: writer.writeField("type", "web3signer") + of RemoteSignerType.Web3SignerDiva: + writer.writeField("type", "web3signer-diva") if value.description.isSome(): writer.writeField("description", value.description.get()) if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags: @@ -704,6 +707,8 @@ proc readValue*(reader: var JsonReader, value: var RemoteKeystore) case res.toLowerAscii() of "web3signer": RemoteSignerType.Web3Signer + of "web3signer-diva": + RemoteSignerType.Web3SignerDiva else: reader.raiseUnexpectedValue("Unsupported remote signer `type` value") else: diff --git a/beacon_chain/validators/keystore_management.nim b/beacon_chain/validators/keystore_management.nim index d5814959d..1ad3a15e6 100644 --- a/beacon_chain/validators/keystore_management.nim +++ b/beacon_chain/validators/keystore_management.nim @@ -140,7 +140,8 @@ func init*(T: type KeystoreData, keystore: RemoteKeystore, description: keystore.description, version: keystore.version, remotes: keystore.remotes, - threshold: keystore.threshold + threshold: keystore.threshold, + remoteType: keystore.remoteType )) func init*(T: type KeystoreData, cookedKey: CookedPubKey, diff --git a/beacon_chain/validators/validator_pool.nim b/beacon_chain/validators/validator_pool.nim index a3f12515e..6c43f40de 100644 --- a/beacon_chain/validators/validator_pool.nim +++ b/beacon_chain/validators/validator_pool.nim @@ -9,7 +9,7 @@ import std/[tables, json, streams, sequtils, uri], - chronos, chronicles, metrics, eth/async_utils, + chronos, chronicles, metrics, json_serialization/std/net, presto, presto/client, @@ -268,6 +268,7 @@ proc close*(pool: var ValidatorPool) = if res.isErr(): notice "Could not unlock validator's keystore file", pubkey = validator.pubkey, validator = shortLog(validator) + pool.validators.clear() iterator publicKeys*(pool: ValidatorPool): ValidatorPubKey = for item in pool.validators.keys(): @@ -388,10 +389,13 @@ proc signWithDistributedKey(v: AttachedValidator, doAssert v.data.threshold <= uint32(v.clients.len) let - signatureReqs = mapIt(v.clients, it[0].signData(it[1].pubkey, request)) deadline = sleepAsync(WEB3_SIGNER_DELAY_TOLERANCE) + signatureReqs = mapIt(v.clients, it[0].signData(it[1].pubkey, deadline, + 2, request)) - await allFutures(signatureReqs) or deadline + await allFutures(signatureReqs) + + if not(deadline.finished()): await cancelAndWait(deadline) var shares: seq[SignatureShare] var neededShares = v.data.threshold @@ -404,7 +408,9 @@ proc signWithDistributedKey(v: AttachedValidator, else: warn "Failed to obtain signature from remote signer", pubkey = shareInfo.pubkey, - signerUrl = $(v.clients[i][0].address) + signerUrl = $(v.clients[i][0].address), + reason = req.read.error.message, + kind = req.read.error.kind if neededShares == 0: let recovered = shares.recoverSignature() @@ -413,17 +419,19 @@ proc signWithDistributedKey(v: AttachedValidator, return SignatureResult.err "Not enough shares to recover the signature" proc signWithSingleKey(v: AttachedValidator, - request: Web3SignerRequest): Future[SignatureResult] - {.async.} = + request: Web3SignerRequest): Future[SignatureResult] {. + async.} = doAssert v.clients.len == 1 - let (client, info) = v.clients[0] - let res = awaitWithTimeout(client.signData(info.pubkey, request), - WEB3_SIGNER_DELAY_TOLERANCE): - return SignatureResult.err "Timeout" - if res.isErr: - return SignatureResult.err res.error + let + deadline = sleepAsync(WEB3_SIGNER_DELAY_TOLERANCE) + (client, info) = v.clients[0] + res = await client.signData(info.pubkey, deadline, 2, request) + + if not(deadline.finished()): await cancelAndWait(deadline) + if res.isErr(): + return SignatureResult.err(res.error.message) else: - return SignatureResult.ok res.get.toValidatorSig + return SignatureResult.ok(res.get().toValidatorSig()) proc signData(v: AttachedValidator, request: Web3SignerRequest): Future[SignatureResult] = @@ -435,6 +443,55 @@ proc signData(v: AttachedValidator, else: v.signWithDistributedKey(request) +proc getFeeRecipientProof(blck: ForkedBeaconBlock | ForkedBlindedBeaconBlock | + bellatrix_mev.BlindedBeaconBlock | + capella_mev.BlindedBeaconBlock + ): Result[Web3SignerMerkleProof, string] = + when blck is ForkedBlindedBeaconBlock: + case blck.kind + of ConsensusFork.Phase0: + err("Invalid block fork: phase0") + of ConsensusFork.Altair: + err("Invalid block fork: altair") + of ConsensusFork.Bellatrix: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.bellatrixData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + of ConsensusFork.Capella: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.capellaData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + of ConsensusFork.Deneb: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.denebData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + elif blck is bellatrix_mev.BlindedBeaconBlock: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + elif blck is capella_mev.BlindedBeaconBlock: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + else: + case blck.kind + of ConsensusFork.Phase0: + err("Invalid block fork: phase0") + of ConsensusFork.Altair: + err("Invalid block fork: altair") + of ConsensusFork.Bellatrix: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.bellatrixData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + of ConsensusFork.Capella: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.capellaData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + of ConsensusFork.Deneb: + const FeeRecipientIndex = GeneralizedIndex(401) + let res = ? build_proof(blck.denebData.body, FeeRecipientIndex) + ok(Web3SignerMerkleProof(index: FeeRecipientIndex, merkleProofs: @res)) + # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/phase0/validator.md#signature proc getBlockSignature*(v: AttachedValidator, fork: Fork, genesis_validators_root: Eth2Digest, slot: Slot, @@ -451,76 +508,151 @@ proc getBlockSignature*(v: AttachedValidator, fork: Fork, fork, genesis_validators_root, slot, block_root, v.data.privateKey).toValidatorSig()) of ValidatorKind.Remote: - when blck is ForkedBlindedBeaconBlock: - let - web3SignerBlock = - case blck.kind - of ConsensusFork.Phase0: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Phase0, - phase0Data: blck.phase0Data) - of ConsensusFork.Altair: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Altair, - altairData: blck.altairData) - of ConsensusFork.Bellatrix: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Bellatrix, - bellatrixData: blck.bellatrixData.toBeaconBlockHeader) - of ConsensusFork.Capella: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Capella, - capellaData: blck.capellaData.toBeaconBlockHeader) - of ConsensusFork.Deneb: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Deneb, - denebData: blck.denebData.toBeaconBlockHeader) - - request = Web3SignerRequest.init( - fork, genesis_validators_root, web3SignerBlock) - await v.signData(request) - elif blck is bellatrix_mev.BlindedBeaconBlock: - let request = Web3SignerRequest.init( - fork, genesis_validators_root, - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Bellatrix, - bellatrixData: blck.toBeaconBlockHeader)) - await v.signData(request) - elif blck is capella_mev.BlindedBeaconBlock: - let request = Web3SignerRequest.init( - fork, genesis_validators_root, - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Capella, - capellaData: blck.toBeaconBlockHeader)) - await v.signData(request) - else: - let - web3SignerBlock = - case blck.kind - of ConsensusFork.Phase0: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Phase0, - phase0Data: blck.phase0Data) - of ConsensusFork.Altair: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Altair, - altairData: blck.altairData) - of ConsensusFork.Bellatrix: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Bellatrix, - bellatrixData: blck.bellatrixData.toBeaconBlockHeader) - of ConsensusFork.Capella: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Capella, - capellaData: blck.capellaData.toBeaconBlockHeader) - of ConsensusFork.Deneb: - Web3SignerForkedBeaconBlock( - kind: ConsensusFork.Deneb, - denebData: blck.denebData.toBeaconBlockHeader) - - request = Web3SignerRequest.init( - fork, genesis_validators_root, web3SignerBlock) - await v.signData(request) + let web3SignerRequest = + when blck is ForkedBlindedBeaconBlock: + case blck.kind + of ConsensusFork.Phase0: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Phase0, + phase0Data: blck.phase0Data)) + of RemoteSignerType.Web3SignerDiva: + return SignatureResult.err("Invalid beacon block fork version") + of ConsensusFork.Altair: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Altair, + altairData: blck.altairData)) + of RemoteSignerType.Web3SignerDiva: + return SignatureResult.err("Invalid beacon block fork version") + of ConsensusFork.Bellatrix: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.bellatrixData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.bellatrixData.toBeaconBlockHeader), + [res.get()]) + of ConsensusFork.Capella: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.capellaData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.capellaData.toBeaconBlockHeader), + [res.get()]) + of ConsensusFork.Deneb: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, + denebData: blck.denebData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, + denebData: blck.denebData.toBeaconBlockHeader), + [res.get()]) + elif blck is bellatrix_mev.BlindedBeaconBlock: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.toBeaconBlockHeader) + ) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.toBeaconBlockHeader), + [res.get()]) + elif blck is capella_mev.BlindedBeaconBlock: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.toBeaconBlockHeader), + [res.get()]) + else: + case blck.kind + of ConsensusFork.Phase0: + # In case of `phase0` block we did not send merkle proof. + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Phase0, + phase0Data: blck.phase0Data)) + of RemoteSignerType.Web3SignerDiva: + return SignatureResult.err("Invalid beacon block fork version") + of ConsensusFork.Altair: + # In case of `altair` block we did not send merkle proof. + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Altair, + altairData: blck.altairData)) + of RemoteSignerType.Web3SignerDiva: + return SignatureResult.err("Invalid beacon block fork version") + of ConsensusFork.Bellatrix: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.bellatrixData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Bellatrix, + bellatrixData: blck.bellatrixData.toBeaconBlockHeader), + [res.get()]) + of ConsensusFork.Capella: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.capellaData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Capella, + capellaData: blck.capellaData.toBeaconBlockHeader), + [res.get()]) + of ConsensusFork.Deneb: + case v.data.remoteType + of RemoteSignerType.Web3Signer: + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, + denebData: blck.denebData.toBeaconBlockHeader)) + of RemoteSignerType.Web3SignerDiva: + let res = getFeeRecipientProof(blck) + if res.isErr(): return SignatureResult.err(res.error) + Web3SignerRequest.init(fork, genesis_validators_root, + Web3SignerForkedBeaconBlock(kind: ConsensusFork.Deneb, + denebData: blck.denebData.toBeaconBlockHeader), + [res.get()]) + await v.signData(web3SignerRequest) # https://github.com/ethereum/consensus-specs/blob/v1.3.0-rc.5/specs/phase0/validator.md#aggregate-signature proc getAttestationSignature*(v: AttachedValidator, fork: Fork, @@ -662,6 +794,34 @@ proc getSlotSignature*(v: AttachedValidator, fork: Fork, v.slotSignature = Opt.some((slot, signature.get)) return signature +proc getValidatorExitSignature*(v: AttachedValidator, fork: Fork, + genesis_validators_root: Eth2Digest, + voluntary_exit: VoluntaryExit + ): Future[SignatureResult] {.async.} = + return + case v.kind + of ValidatorKind.Local: + SignatureResult.ok(get_voluntary_exit_signature( + fork, genesis_validators_root, voluntary_exit, + v.data.privateKey).toValidatorSig()) + of ValidatorKind.Remote: + let request = Web3SignerRequest.init(fork, genesis_validators_root, + voluntary_exit) + await v.signData(request) + +proc getDepositMessageSignature*(v: AttachedValidator, version: Version, + deposit_message: DepositMessage + ): Future[SignatureResult] {.async.} = + return + case v.kind + of ValidatorKind.Local: + SignatureResult.ok(get_deposit_signature( + deposit_message, version, + v.data.privateKey).toValidatorSig()) + of ValidatorKind.Remote: + let request = Web3SignerRequest.init(version, deposit_message) + await v.signData(request) + # https://github.com/ethereum/builder-specs/blob/v0.3.0/specs/bellatrix/builder.md#signing proc getBuilderSignature*(v: AttachedValidator, fork: Fork, validatorRegistration: ValidatorRegistrationV1): diff --git a/tests/all_tests.nim b/tests/all_tests.nim index 23b1a4ca8..c30b9407f 100644 --- a/tests/all_tests.nim +++ b/tests/all_tests.nim @@ -45,6 +45,7 @@ import # Unit test ./test_sync_manager, ./test_validator_pool, ./test_zero_signature, + ./test_signing_node, ./fork_choice/tests_fork_choice, ./consensus_spec/all_tests as consensus_all_tests, ./slashing_protection/test_fixtures, diff --git a/tests/test_signing_node.nim b/tests/test_signing_node.nim new file mode 100644 index 000000000..6ef02d10d --- /dev/null +++ b/tests/test_signing_node.nim @@ -0,0 +1,1595 @@ +import + std/[osproc, algorithm], + presto, unittest2, chronicles, stew/[results, byteutils, io2], + chronos/unittest2/asynctests, + ../beacon_chain/spec/[signatures, crypto], + ../beacon_chain/spec/eth2_apis/rest_remote_signer_calls, + ../beacon_chain/filepath, + ../beacon_chain/validators/validator_pool + +{.used.} + +const + TestDirectoryName = "test-signing-node" + TestDirectoryNameDiva = "test-signing-node-diva" + ValidatorKeystore1 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"8b98b30b4e144dbbcc724e502ffecc67c33651aa49600e745e41f959e12abf37\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"04f91a7eb3d6430a598255ea83621e78\"},\"message\":\"5c652e6cdd1215eb9203281e2446abc4d3e1bd50cb822583ce5c74570e9cab18\"}},\"pubkey\":\"99a8df087e253a874c3ca31e0d1115500a671ed8714800d503e99c2c887331a968a7fa7f0290c3a0698675eee138b407\",\"path\":\"m/12381/3600/161/0/0\",\"uuid\":\"81bec933-d928-4e7e-83da-54bbe37a4715\",\"version\":4}" + ValidatorKeystore2 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"2ecda276340c04cb92ce003db9cface0727905f0ba1aa9c60b101f478fca9a5e\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"9d9d73af0031fd19e6833983557b2e30\"},\"message\":\"16d5f87e0675c95cb1e4fc209eea738d45c19b3c0f14088c9e140c573bce0253\"}},\"pubkey\":\"aa19751eb240a04a17b8720e2334acf1d78182ab496e77c51b3bb9e887d50295a478d499abcf6434efbc1aa4c4c4f352\",\"path\":\"m/12381/3600/232/0/0\",\"uuid\":\"291e837b-d8ff-494c-8c7b-7e6bab23b8bf\",\"version\":4}" + ValidatorKeystore3 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"a8c2333e787d65415a02d607c0ec774b654e5a67066e4bc379e2f3b7cf4c826a\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"161171cb21c1c6ec20b15798f545fffc\"},\"message\":\"8ecb326d14dece099d4ba4800a5326324ccf3a8df38fd4aa37af02e8f0617da0\"}},\"pubkey\":\"acf31f9b1ecf65dbb198e380599b6c81fc1a1f5db4457482cc697d81b1fdfb6e49cf8eff4980477f6e32749eef61dc4d\",\"path\":\"m/12381/3600/36/0/0\",\"uuid\":\"420578fd-6832-4e79-a3db-ac0662ace13c\",\"version\":4}" + ValidatorKeystore4 = "{\"crypto\":{\"kdf\":{\"function\":\"pbkdf2\",\"params\":{\"dklen\":32,\"c\":1,\"prf\":\"hmac-sha256\",\"salt\":\"040f3f4b9dfc4bdeb37de870cbaa83582f981f358e370f271c2945f2e6430aab\"},\"message\":\"\"},\"checksum\":{\"function\":\"sha256\",\"params\":{},\"message\":\"ca3ab990616d81e77e89b14eb6f613c1f13056ef2d062259259d54c7a85d63c9\"},\"cipher\":{\"function\":\"aes-128-ctr\",\"params\":{\"iv\":\"d0096f545dcdb366ef3f86e609fc008e\"},\"message\":\"be2f4f3edde8ade888eb4b0211a00b0528ddc4fa68bb0e67c992a05518cd9d96\"}},\"pubkey\":\"a73469094bf134f32a4e91fce07101290c85ffb259f277c97308310ffd0ef1aa3bd90eea1a8217d060b727b7a0154c34\",\"path\":\"m/12381/3600/119/0/0\",\"uuid\":\"2e07f033-c1b6-4d5f-b448-d18caab93adc\",\"version\":4}" + + KeystorePassword = + "1331CE70907C1F64745D47447CE378EEA6A95DB271CDA7E54D9D7AB52EE0E0A2" + ValidatorPrivateKey1 = + "0x151c2858787a50476b5107f64977bfaed5b925e9db38b2f5a6ed39c77159d7a6" + ValidatorPrivateKey2 = + "0x44e711335ab6981a92a8711cd68399b4d14da7105368fc26cd59520f69dd8e83" + ValidatorPrivateKey3 = + "0x47264627bb3d80ceab5d4de081418927837ce777434af2609c1106d0b5327cb5" + ValidatorPubKey1 = + "0x99a8df087e253a874c3ca31e0d1115500a671ed8714800d503e99c2c887331a968a7fa7f0290c3a0698675eee138b407" + ValidatorPubKey2 = + "0xaa19751eb240a04a17b8720e2334acf1d78182ab496e77c51b3bb9e887d50295a478d499abcf6434efbc1aa4c4c4f352" + ValidatorPubKey3 = + "0xacf31f9b1ecf65dbb198e380599b6c81fc1a1f5db4457482cc697d81b1fdfb6e49cf8eff4980477f6e32749eef61dc4d" + ValidatorPubKey4 = + "0xa73469094bf134f32a4e91fce07101290c85ffb259f277c97308310ffd0ef1aa3bd90eea1a8217d060b727b7a0154c34" + GenesisValidatorsRoot = Eth2Digest.fromHex( + "043db0d9a83813551ee2f33450d23797757d430911a9320530ad8a0eabc43efb") + GenesisForkVersion = Version(hexToByteArray[4]("00001020")) + SomeOtherRoot = Eth2Digest.fromHex( + "ccccccaaaaaaffffffeeeeee50d23797757d430911a9320530ad8a0eabc43efb") + SigningFork = Fork( + previous_version: Version(hexToByteArray[4]("00001020")), + current_version: Version(hexToByteArray[4]("00001020")), + epoch: Epoch(0'u64) + ) + SomeSignature = + "0xb3baa751d0a9132cfe93e4e3d5ff9075111100e3789dca219ade5a24d27e19d16b3353149da1833e9b691bb38634e8dc04469be7032132906c927d7e1a49b414730612877bc6b2810c8f202daf793d1ab0d6b5cb21d52f9e52e883859887a5d9" + + SigningExpectedFeeRecipient = "0x000095e79eac4d76aab57cb2c1f091d553b36ca0" + SigningOtherFeeRecipient = "0x000096e79eac4d76aab57cb2c1f091d553b36ca0" + + AgAttestation = "{\"data\":{\"aggregation_bits\":\"0x01\",\"signature\":\"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505\",\"data\":{\"slot\":\"1\",\"index\":\"1\",\"beacon_block_root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\",\"source\":{\"epoch\":\"1\",\"root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\"},\"target\":{\"epoch\":\"1\",\"root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\"}}}}" + + Phase0Block = "{\"version\":\"phase0\",\"data\":{\"slot\":\"1\",\"proposer_index\":\"1\",\"parent_root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\",\"state_root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\",\"body\":{\"randao_reveal\":\"0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505\",\"eth1_data\":{\"deposit_root\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\",\"deposit_count\":\"1\",\"block_hash\":\"0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2\"},\"graffiti\":\"\",\"proposer_slashings\":[],\"attester_slashings\":[],\"attestations\":[],\"deposits\":[],\"voluntary_exits\":[]}}}" + AltairBlock = "{\"version\":\"altair\",\"data\":{\"slot\":\"5297696\",\"proposer_index\":\"153094\",\"parent_root\":\"0xe6106533af9be918120ead7440a8006c7f123cc3cb7daf1f11d951864abea014\",\"state_root\":\"0xf86196d34500ca25d1f4e7431d4d52f6f85540bcaf97dd0d2ad9ecdb3eebcdf0\",\"body\":{\"randao_reveal\":\"0xa7efee3d5ddceb60810b23e3b5d39734696418f41dfd13a0851c7be7a72acbdceaa61e1db27513801917d72519d1c1040ccfed829faf06abe06d9964949554bf4369134b66de715ea49eb4fecf3e2b7e646f1764a1993e31e53dbc6557929c12\",\"eth1_data\":{\"deposit_root\":\"0x8ec87d7219a3c873fff3bfe206b4f923d1b471ce4ff9d6d6ecc162ef07825e14\",\"deposit_count\":\"259476\",\"block_hash\":\"0x877b6f8332c7397251ff3f0c5cecec105ff7d4cb78251b47f91fd15a86a565ab\"},\"graffiti\":\"\",\"proposer_slashings\":[],\"attester_slashings\":[],\"attestations\":[],\"deposits\":[],\"voluntary_exits\":[],\"sync_aggregate\":{\"sync_committee_bits\":\"0x733dfda7f5ffde5ade73367fcbf7fffeef7fe43777ffdffab9dbad6f7eed5fff9bfec4affdefbfaddf35bf5efbff9ffff9dfd7dbf97fbfcdfaddfeffbf95f75f\",\"sync_committee_signature\":\"0x81fdf76e797f81b0116a1c1ae5200b613c8041115223cd89e8bd5477aab13de6097a9ebf42b130c59527bbb4c96811b809353a17c717549f82d4bd336068ef0b99b1feebd4d2432a69fa77fac12b78f1fcc9d7b59edbeb381adf10b15bc4a520\"}}}}" + BellatrixBlock = "{\"version\":\"bellatrix\",\"data\":{\"slot\":\"5297696\",\"proposer_index\":\"153094\",\"parent_root\":\"0xe6106533af9be918120ead7440a8006c7f123cc3cb7daf1f11d951864abea014\",\"state_root\":\"0xf86196d34500ca25d1f4e7431d4d52f6f85540bcaf97dd0d2ad9ecdb3eebcdf0\",\"body\":{\"randao_reveal\":\"0xa7efee3d5ddceb60810b23e3b5d39734696418f41dfd13a0851c7be7a72acbdceaa61e1db27513801917d72519d1c1040ccfed829faf06abe06d9964949554bf4369134b66de715ea49eb4fecf3e2b7e646f1764a1993e31e53dbc6557929c12\",\"eth1_data\":{\"deposit_root\":\"0x8ec87d7219a3c873fff3bfe206b4f923d1b471ce4ff9d6d6ecc162ef07825e14\",\"deposit_count\":\"259476\",\"block_hash\":\"0x877b6f8332c7397251ff3f0c5cecec105ff7d4cb78251b47f91fd15a86a565ab\"},\"graffiti\":\"\",\"proposer_slashings\":[],\"attester_slashings\":[],\"attestations\":[],\"deposits\":[],\"voluntary_exits\":[],\"sync_aggregate\":{\"sync_committee_bits\":\"0x733dfda7f5ffde5ade73367fcbf7fffeef7fe43777ffdffab9dbad6f7eed5fff9bfec4affdefbfaddf35bf5efbff9ffff9dfd7dbf97fbfcdfaddfeffbf95f75f\",\"sync_committee_signature\":\"0x81fdf76e797f81b0116a1c1ae5200b613c8041115223cd89e8bd5477aab13de6097a9ebf42b130c59527bbb4c96811b809353a17c717549f82d4bd336068ef0b99b1feebd4d2432a69fa77fac12b78f1fcc9d7b59edbeb381adf10b15bc4a520\"},\"execution_payload\":{\"parent_hash\":\"0x14c2242a8cfbce559e84c391f5f16d10d7719751b8558873012dc88ae5a193e8\",\"fee_recipient\":\"$1\",\"state_root\":\"0xdf8d96b2c292736d39e72e25802c2744d34d3d3c616de5b362425cab01f72fa5\",\"receipts_root\":\"0x4938a2bf640846d213b156a1a853548b369cd02917fa63d8766ab665d7930bac\",\"logs_bloom\":\"0x298610600038408c201080013832408850a00bc8f801920121840030a015310010e2a0e0108628110552062811441c84802f43825c4fc82140b036c58025a28800054c80a44025c052090a0f2c209a0400058040019ea0008e589084078048050880930113a2894082e0112408b088382402a851621042212aa40018a408d07e178c68691486411aa9a2809043b000a04c040000065a030028018540b04b1820271d00821b00c29059095022322c10a530060223240416140190056608200063c82248274ba8f0098e402041cd9f451031481a1010b8220824833520490221071898802d206348449116812280014a10a2d1c210100a30010802490f0a221849\",\"prev_randao\":\"0xc061711e135cd40531ec3ee29d17d3824c0e5f80d07f721e792ab83240aa0ab5\",\"block_number\":\"8737497\",\"gas_limit\":\"30000000\",\"gas_used\":\"16367052\",\"timestamp\":\"1680080352\",\"extra_data\":\"0xd883010b05846765746888676f312e32302e32856c696e7578\",\"base_fee_per_gas\":\"231613172261\",\"block_hash\":\"0x5aa9fd22a9238925adb2b038fd6eafc77adabf554051db5bc16ae5168a52eff6\",\"transactions\":[],\"withdrawals\":[]}}}}" + CapellaBlock = "{\"version\":\"capella\",\"data\":{\"slot\":\"5297696\",\"proposer_index\":\"153094\",\"parent_root\":\"0xe6106533af9be918120ead7440a8006c7f123cc3cb7daf1f11d951864abea014\",\"state_root\":\"0xf86196d34500ca25d1f4e7431d4d52f6f85540bcaf97dd0d2ad9ecdb3eebcdf0\",\"body\":{\"randao_reveal\":\"0xa7efee3d5ddceb60810b23e3b5d39734696418f41dfd13a0851c7be7a72acbdceaa61e1db27513801917d72519d1c1040ccfed829faf06abe06d9964949554bf4369134b66de715ea49eb4fecf3e2b7e646f1764a1993e31e53dbc6557929c12\",\"eth1_data\":{\"deposit_root\":\"0x8ec87d7219a3c873fff3bfe206b4f923d1b471ce4ff9d6d6ecc162ef07825e14\",\"deposit_count\":\"259476\",\"block_hash\":\"0x877b6f8332c7397251ff3f0c5cecec105ff7d4cb78251b47f91fd15a86a565ab\"},\"graffiti\":\"\",\"proposer_slashings\":[],\"attester_slashings\":[],\"attestations\":[],\"deposits\":[],\"voluntary_exits\":[],\"sync_aggregate\":{\"sync_committee_bits\":\"0x733dfda7f5ffde5ade73367fcbf7fffeef7fe43777ffdffab9dbad6f7eed5fff9bfec4affdefbfaddf35bf5efbff9ffff9dfd7dbf97fbfcdfaddfeffbf95f75f\",\"sync_committee_signature\":\"0x81fdf76e797f81b0116a1c1ae5200b613c8041115223cd89e8bd5477aab13de6097a9ebf42b130c59527bbb4c96811b809353a17c717549f82d4bd336068ef0b99b1feebd4d2432a69fa77fac12b78f1fcc9d7b59edbeb381adf10b15bc4a520\"},\"execution_payload\":{\"parent_hash\":\"0x14c2242a8cfbce559e84c391f5f16d10d7719751b8558873012dc88ae5a193e8\",\"fee_recipient\":\"$1\",\"state_root\":\"0xdf8d96b2c292736d39e72e25802c2744d34d3d3c616de5b362425cab01f72fa5\",\"receipts_root\":\"0x4938a2bf640846d213b156a1a853548b369cd02917fa63d8766ab665d7930bac\",\"logs_bloom\":\"0x298610600038408c201080013832408850a00bc8f801920121840030a015310010e2a0e0108628110552062811441c84802f43825c4fc82140b036c58025a28800054c80a44025c052090a0f2c209a0400058040019ea0008e589084078048050880930113a2894082e0112408b088382402a851621042212aa40018a408d07e178c68691486411aa9a2809043b000a04c040000065a030028018540b04b1820271d00821b00c29059095022322c10a530060223240416140190056608200063c82248274ba8f0098e402041cd9f451031481a1010b8220824833520490221071898802d206348449116812280014a10a2d1c210100a30010802490f0a221849\",\"prev_randao\":\"0xc061711e135cd40531ec3ee29d17d3824c0e5f80d07f721e792ab83240aa0ab5\",\"block_number\":\"8737497\",\"gas_limit\":\"30000000\",\"gas_used\":\"16367052\",\"timestamp\":\"1680080352\",\"extra_data\":\"0xd883010b05846765746888676f312e32302e32856c696e7578\",\"base_fee_per_gas\":\"231613172261\",\"block_hash\":\"0x5aa9fd22a9238925adb2b038fd6eafc77adabf554051db5bc16ae5168a52eff6\",\"transactions\":[],\"withdrawals\":[]},\"bls_to_execution_changes\":[]}}}" + DenebBlock = "{\"version\":\"deneb\",\"data\":{\"slot\":\"5297696\",\"proposer_index\":\"153094\",\"parent_root\":\"0xe6106533af9be918120ead7440a8006c7f123cc3cb7daf1f11d951864abea014\",\"state_root\":\"0xf86196d34500ca25d1f4e7431d4d52f6f85540bcaf97dd0d2ad9ecdb3eebcdf0\",\"body\":{\"randao_reveal\":\"0xa7efee3d5ddceb60810b23e3b5d39734696418f41dfd13a0851c7be7a72acbdceaa61e1db27513801917d72519d1c1040ccfed829faf06abe06d9964949554bf4369134b66de715ea49eb4fecf3e2b7e646f1764a1993e31e53dbc6557929c12\",\"eth1_data\":{\"deposit_root\":\"0x8ec87d7219a3c873fff3bfe206b4f923d1b471ce4ff9d6d6ecc162ef07825e14\",\"deposit_count\":\"259476\",\"block_hash\":\"0x877b6f8332c7397251ff3f0c5cecec105ff7d4cb78251b47f91fd15a86a565ab\"},\"graffiti\":\"\",\"proposer_slashings\":[],\"attester_slashings\":[],\"attestations\":[],\"deposits\":[],\"voluntary_exits\":[],\"sync_aggregate\":{\"sync_committee_bits\":\"0x733dfda7f5ffde5ade73367fcbf7fffeef7fe43777ffdffab9dbad6f7eed5fff9bfec4affdefbfaddf35bf5efbff9ffff9dfd7dbf97fbfcdfaddfeffbf95f75f\",\"sync_committee_signature\":\"0x81fdf76e797f81b0116a1c1ae5200b613c8041115223cd89e8bd5477aab13de6097a9ebf42b130c59527bbb4c96811b809353a17c717549f82d4bd336068ef0b99b1feebd4d2432a69fa77fac12b78f1fcc9d7b59edbeb381adf10b15bc4a520\"},\"execution_payload\":{\"parent_hash\":\"0x14c2242a8cfbce559e84c391f5f16d10d7719751b8558873012dc88ae5a193e8\",\"fee_recipient\":\"$1\",\"state_root\":\"0xdf8d96b2c292736d39e72e25802c2744d34d3d3c616de5b362425cab01f72fa5\",\"receipts_root\":\"0x4938a2bf640846d213b156a1a853548b369cd02917fa63d8766ab665d7930bac\",\"logs_bloom\":\"0x298610600038408c201080013832408850a00bc8f801920121840030a015310010e2a0e0108628110552062811441c84802f43825c4fc82140b036c58025a28800054c80a44025c052090a0f2c209a0400058040019ea0008e589084078048050880930113a2894082e0112408b088382402a851621042212aa40018a408d07e178c68691486411aa9a2809043b000a04c040000065a030028018540b04b1820271d00821b00c29059095022322c10a530060223240416140190056608200063c82248274ba8f0098e402041cd9f451031481a1010b8220824833520490221071898802d206348449116812280014a10a2d1c210100a30010802490f0a221849\",\"prev_randao\":\"0xc061711e135cd40531ec3ee29d17d3824c0e5f80d07f721e792ab83240aa0ab5\",\"block_number\":\"8737497\",\"gas_limit\":\"30000000\",\"gas_used\":\"16367052\",\"timestamp\":\"1680080352\",\"extra_data\":\"0xd883010b05846765746888676f312e32302e32856c696e7578\",\"base_fee_per_gas\":\"231613172261\",\"block_hash\":\"0x5aa9fd22a9238925adb2b038fd6eafc77adabf554051db5bc16ae5168a52eff6\",\"transactions\":[],\"withdrawals\":[],\"excess_data_gas\":\"231613172261\"},\"bls_to_execution_changes\":[],\"blob_kzg_commitments\":[]}}}" + + SigningNodeAddress = "127.0.0.1" + SigningNodePort = 35333 + + SigningRequestTimeoutSeconds = 1 + +proc getNodePort(rt: RemoteSignerType): int = + case rt + of RemoteSignerType.Web3Signer: + SigningNodePort + of RemoteSignerType.Web3SignerDiva: + SigningNodePort + 1 + +proc getBlock(fork: ConsensusFork, + feeRecipient = SigningExpectedFeeRecipient): ForkedBeaconBlock = + let + blckData = + case fork + of ConsensusFork.Phase0: Phase0Block + of ConsensusFork.Altair: AltairBlock + of ConsensusFork.Bellatrix: BellatrixBlock % [feeRecipient] + of ConsensusFork.Capella: CapellaBlock % [feeRecipient] + of ConsensusFork.Deneb: DenebBlock % [feeRecipient] + contentType = ContentTypeData( + mediaType: MediaType.init("application/json")) + + decodeBytes(ProduceBlockResponseV2, + blckData.toOpenArrayByte(0, len(blckData) - 1), + Opt.some(contentType)).tryGet() + +proc init(t: typedesc[Web3SignerForkedBeaconBlock], + forked: ForkedBeaconBlock): Web3SignerForkedBeaconBlock = + case forked.kind + of ConsensusFork.Phase0: + Web3SignerForkedBeaconBlock( + kind: ConsensusFork.Phase0, + phase0Data: forked.phase0Data) + of ConsensusFork.Altair: + Web3SignerForkedBeaconBlock( + kind: ConsensusFork.Altair, + altairData: forked.altairData) + of ConsensusFork.Bellatrix: + Web3SignerForkedBeaconBlock( + kind: ConsensusFork.Bellatrix, + bellatrixData: forked.bellatrixData.toBeaconBlockHeader) + of ConsensusFork.Capella: + Web3SignerForkedBeaconBlock( + kind: ConsensusFork.Capella, + capellaData: forked.capellaData.toBeaconBlockHeader) + of ConsensusFork.Deneb: + Web3SignerForkedBeaconBlock( + kind: ConsensusFork.Deneb, + denebData: forked.denebData.toBeaconBlockHeader) + +proc createKeystore(dataDir, pubkey, + store, password: string): Result[void, string] = + let + validatorsDir = dataDir & DirSep & "validators" + keystoreDir = validatorsDir & DirSep & pubkey + keystoreFile = keystoreDir & DirSep & "keystore.json" + secretsDir = dataDir & DirSep & "secrets" + secretFile = secretsDir & DirSep & pubkey + + if not(isDir(dataDir)): + let res = secureCreatePath(dataDir) + if res.isErr(): return err(ioErrorMsg(res.error)) + if not(isDir(validatorsDir)): + let res = secureCreatePath(validatorsDir) + if res.isErr(): return err(ioErrorMsg(res.error)) + if not(isDir(secretsDir)): + let res = secureCreatePath(secretsDir) + if res.isErr(): return err(ioErrorMsg(res.error)) + if not(isDir(keystoreDir)): + let res = secureCreatePath(keystoreDir) + if res.isErr(): return err(ioErrorMsg(res.error)) + + block: + let res = secureWriteFile(keystoreFile, + store.toOpenArrayByte(0, len(store) - 1)) + if res.isErr(): return err(ioErrorMsg(res.error)) + block: + let res = secureWriteFile(secretFile, + password.toOpenArrayByte(0, len(password) - 1)) + if res.isErr(): return err(ioErrorMsg(res.error)) + + ok() + +proc removeKeystore(dataDir, pubkey: string) = + let + validatorsDir = dataDir & DirSep & "validators" + keystoreDir = validatorsDir & DirSep & pubkey + keystoreFile = keystoreDir & DirSep & "keystore.json" + secretsDir = dataDir & DirSep & "secrets" + secretFile = secretsDir & DirSep & pubkey + + discard removeFile(secretFile) + discard removeFile(keystoreFile) + discard removeDir(keystoreDir) + discard removeDir(validatorsDir) + discard removeDir(secretsDir) + +proc createDataDir(pathName: string): Result[void, string] = + ? createKeystore(pathName, ValidatorPubKey1, ValidatorKeystore1, + KeystorePassword) + ? createKeystore(pathName, ValidatorPubKey2, ValidatorKeystore2, + KeystorePassword) + ? createKeystore(pathName, ValidatorPubKey3, ValidatorKeystore3, + KeystorePassword) + ok() + +proc getTestDir(rt: RemoteSignerType): string = + case rt + of RemoteSignerType.Web3Signer: + TestDirectoryName + of RemoteSignerType.Web3SignerDiva: + TestDirectoryNameDiva + +proc createTestDir(rt: RemoteSignerType): Result[void, string] = + let + pathName = getTestDir(rt) + signingDir = pathName & DirSep & "signing-node" + if not(isDir(pathName)): + let res = secureCreatePath(pathName) + if res.isErr(): return err(ioErrorMsg(res.error)) + createDataDir(signingDir) + +proc createAdditionalKeystore(rt: RemoteSignerType): Result[void, string] = + let signingDir = getTestDir(rt) & DirSep & "signing-node" + createKeystore(signingDir, ValidatorPubKey4, ValidatorKeystore4, + KeystorePassword) + +proc removeTestDir(rt: RemoteSignerType) = + let + pathName = getTestDir(rt) + signingDir = pathName & DirSep & "signing-node" + # signing-node cleanup + removeKeystore(signingDir, ValidatorPubKey1) + removeKeystore(signingDir, ValidatorPubKey2) + removeKeystore(signingDir, ValidatorPubKey3) + removeKeystore(signingDir, ValidatorPubKey4) + discard removeDir(signingDir) + discard removeDir(pathName) + +proc getPrivateKey(data: string): Result[ValidatorPrivKey, string] = + var key: blscurve.SecretKey + if fromHex(key, data): + ok(ValidatorPrivKey(key)) + else: + err("Unable to initialize private key") + +proc getLocalKeystoreData(data: string): Result[KeystoreData, string] = + let privateKey = + block: + var key: blscurve.SecretKey + if not(fromHex(key, data)): + return err("Unable to initialize private key") + ValidatorPrivKey(key) + + ok(KeystoreData( + kind: KeystoreKind.Local, + privateKey: privateKey, + version: uint64(4), + pubkey: privateKey.toPubKey().toPubKey() + )) + +proc getRemoteKeystoreData(data: string, + rt: RemoteSignerType): Result[KeystoreData, string] = + let + publicKey = ValidatorPubKey.fromHex(data).valueOr: + return err("Invalid public key") + + info = RemoteSignerInfo( + url: HttpHostUri(parseUri("http://" & SigningNodeAddress & ":" & + $getNodePort(rt))), + pubkey: publicKey + ) + ok(KeystoreData( + kind: KeystoreKind.Remote, + remoteType: rt, + version: uint64(4), + pubkey: publicKey, + remotes: @[info] + )) + +proc spawnSigningNodeProcess(rt: RemoteSignerType): Result[Process, string] = + let process = + try: + let arguments = + case rt + of RemoteSignerType.Web3Signer: + @[ + "--non-interactive=true", + "--data-dir=" & getTestDir(rt) & DirSep & "signing-node", + "--bind-address=" & SigningNodeAddress, + "--bind-port=" & $getNodePort(rt), + "--request-timeout=" & $SigningRequestTimeoutSeconds + # we make so low `timeout` to test connection pool. + ] + of RemoteSignerType.Web3SignerDiva: + @[ + "--non-interactive=true", + "--data-dir=" & getTestDir(rt) & DirSep & "signing-node", + "--bind-address=" & SigningNodeAddress, + "--bind-port=" & $getNodePort(rt), + "--expected-fee-recipient=" & $SigningExpectedFeeRecipient, + "--request-timeout=" & $SigningRequestTimeoutSeconds + # we make so low `timeout` to test connection pool. + ] + osproc.startProcess("build/nimbus_signing_node", "", arguments) + except CatchableError as exc: + echo "Error while spawning `nimbus_signing_node` process [", $exc.name, + "] " & $exc.msg + return err($exc.msg) + ok(process) + +proc shutdownSigningNodeProcess(process: Process) = + if process.peekExitCode() == -1: + process.kill() + discard process.waitForExit() + +suite "Nimbus remote signer/signing test (web3signer)": + let res = createTestDir(RemoteSignerType.Web3Signer) + doAssert(res.isOk()) + + let pres = spawnSigningNodeProcess(RemoteSignerType.Web3Signer) + doAssert(pres.isOk()) + let process = pres.get() + + setup: + let pool1 = newClone(default(ValidatorPool)) + let + validator1 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey1).get(), + default(Eth1Address), 300_000_000'u64 + ) + validator2 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey2).get(), + default(Eth1Address), 300_000_000'u64 + ) + validator3 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey3).get(), + default(Eth1Address), 300_000_000'u64 + ) + + validator1.index = Opt.some(ValidatorIndex(100)) + validator2.index = Opt.some(ValidatorIndex(101)) + validator3.index = Opt.some(ValidatorIndex(102)) + + let pool2 = newClone(default(ValidatorPool)) + let validator4 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey1, + RemoteSignerType.Web3Signer).get(), + default(Eth1Address), 300_000_000'u64 + ) + let validator5 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey2, + RemoteSignerType.Web3Signer).get(), + default(Eth1Address), 300_000_000'u64 + ) + let validator6 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey3, + RemoteSignerType.Web3Signer).get(), + default(Eth1Address), 300_000_000'u64 + ) + + validator4.index = Opt.some(ValidatorIndex(100)) + validator5.index = Opt.some(ValidatorIndex(101)) + validator6.index = Opt.some(ValidatorIndex(102)) + + asyncTest "Waiting for signing node (/upcheck) test": + let + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3Signer) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + + check rclient.isOk() + let client = rclient.get() + + var attempts = 0 + while attempts < 3: + let loopBreak = + try: + let response = await client.getUpcheck() + check: + response.status == 200 + response.data.status == "OK" + true + except CatchableError: + inc(attempts) + false + if loopBreak: + break + await sleepAsync(500.milliseconds) + + await client.closeWait() + + asyncTest "Public keys enumeration (/api/v1/eth2/publicKeys) test": + let + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3Signer) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + + check rclient.isOk() + let client = rclient.get() + + try: + let response = await client.getKeys() + check: + response.status == 200 + len(response.data) == 3 + let + received = sorted([ + "0x" & response.data[0].toHex(), + "0x" & response.data[1].toHex(), + "0x" & response.data[2].toHex() + ]) + expected = sorted([ + ValidatorPubKey1, + ValidatorPubKey2, + ValidatorPubKey3 + ]) + check received == expected + finally: + await client.closeWait() + + asyncTest "Signing phase0 block": + let + forked = getBlock(ConsensusFork.Phase0) + blockRoot = withBlck(forked): hash_tree_root(blck) + request = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + forked.phase0Data) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3Signer) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + + check rclient.isOk() + let client = rclient.get() + let + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + + try: + let + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + response1 = await client.signData(publicKey1, request) + response2 = await client.signData(publicKey2, request) + response3 = await client.signData(publicKey3, request) + + check: + response1.isOk() + response2.isOk() + response3.isOk() + response1.get().toValidatorSig() == sres1.get() + response2.get().toValidatorSig() == sres2.get() + response3.get().toValidatorSig() == sres3.get() + + finally: + await client.closeWait() + + asyncTest "Signing aggregation slot (getSlotSignature())": + let + sres1 = + await validator1.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(10)) + sres2 = + await validator2.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(100)) + sres3 = + await validator3.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(1000)) + rres1 = + await validator4.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(10)) + rres2 = + await validator5.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(100)) + rres3 = + await validator6.getSlotSignature(SigningFork, + GenesisValidatorsRoot, Slot(1000)) + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing randao reveal (getEpochSignature())": + let + sres1 = + await validator1.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(10)) + sres2 = + await validator2.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(100)) + sres3 = + await validator3.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(1000)) + rres1 = + await validator4.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(10)) + rres2 = + await validator5.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(100)) + rres3 = + await validator6.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(1000)) + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing SC message (getSyncCommitteeMessage())": + let + sres1 = + await validator1.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(10), SomeOtherRoot) + sres2 = + await validator2.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(100), SomeOtherRoot) + sres3 = + await validator3.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(1000), SomeOtherRoot) + rres1 = + await validator4.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(10), SomeOtherRoot) + rres2 = + await validator5.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(100), SomeOtherRoot) + rres3 = + await validator6.getSyncCommitteeMessage(SigningFork, + GenesisValidatorsRoot, Slot(1000), SomeOtherRoot) + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing SC selection proof " & + "(getSyncCommitteeSelectionProof())": + let + sres1 = + await validator1.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(10), SyncSubcommitteeIndex(1)) + sres2 = + await validator2.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(100), SyncSubcommitteeIndex(2)) + sres3 = + await validator3.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(1000), SyncSubcommitteeIndex(3)) + rres1 = + await validator4.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(10), SyncSubcommitteeIndex(1)) + rres2 = + await validator5.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(100), SyncSubcommitteeIndex(2)) + rres3 = + await validator6.getSyncCommitteeSelectionProof(SigningFork, + GenesisValidatorsRoot, Slot(1000), SyncSubcommitteeIndex(3)) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing SC contribution and proof " & + "(getContributionAndProofSignature())": + let + conProof = default(ContributionAndProof) + sres1 = + await validator1.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + sres2 = + await validator2.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + sres3 = + await validator3.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + rres1 = + await validator4.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + rres2 = + await validator5.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + rres3 = + await validator6.getContributionAndProofSignature(SigningFork, + GenesisValidatorsRoot, conProof) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing attestation (getAttestationSignature())": + let + adata = default(AttestationData) + sres1 = + await validator1.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + sres2 = + await validator2.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + sres3 = + await validator3.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + rres1 = + await validator4.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + rres2 = + await validator5.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + rres3 = + await validator6.getAttestationSignature(SigningFork, + GenesisValidatorsRoot, adata) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing aggregate and proof (getAggregateAndProofSignature())": + let + contentType = ContentTypeData( + mediaType: MediaType.init("application/json")) + agAttestation = decodeBytes( + GetAggregatedAttestationResponse, + AgAttestation.toOpenArrayByte(0, len(AgAttestation) - 1), + Opt.some(contentType)).tryGet().data + agProof = AggregateAndProof( + aggregator_index: 1'u64, + aggregate: agAttestation, + selection_proof: ValidatorSig.fromHex(SomeSignature).get()) + sres1 = + await validator1.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + sres2 = + await validator2.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + sres3 = + await validator3.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + rres1 = + await validator4.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + rres2 = + await validator5.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + rres3 = + await validator6.getAggregateAndProofSignature(SigningFork, + GenesisValidatorsRoot, agProof) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing validator registration (getBuilderSignature())": + let + vdata = default(ValidatorRegistrationV1) + sres1 = await validator1.getBuilderSignature(SigningFork, vdata) + sres2 = await validator2.getBuilderSignature(SigningFork, vdata) + sres3 = await validator3.getBuilderSignature(SigningFork, vdata) + rres1 = await validator4.getBuilderSignature(SigningFork, vdata) + rres2 = await validator5.getBuilderSignature(SigningFork, vdata) + rres3 = await validator6.getBuilderSignature(SigningFork, vdata) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing voluntary exit (getValidatorExitSignature())": + let + voluntaryExit = default(VoluntaryExit) + sres1 = + await validator1.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + sres2 = + await validator2.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + sres3 = + await validator3.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + rres1 = + await validator4.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + rres2 = + await validator5.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + rres3 = + await validator6.getValidatorExitSignature(SigningFork, + GenesisValidatorsRoot, voluntaryExit) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing deposit message (getDepositMessageSignature())": + let + depositMessage = default(DepositMessage) + sres1 = + await validator1.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + sres2 = + await validator2.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + sres3 = + await validator3.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + rres1 = + await validator4.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + rres2 = + await validator5.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + rres3 = + await validator6.getDepositMessageSignature(GenesisForkVersion, + depositMessage) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing BeaconBlock (getBlockSignature(phase0))": + let + forked = getBlock(ConsensusFork.Phase0) + blockRoot = withBlck(forked): hash_tree_root(blck) + + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing BeaconBlock (getBlockSignature(altair))": + let + forked = getBlock(ConsensusFork.Altair) + blockRoot = withBlck(forked): hash_tree_root(blck) + + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing BeaconBlock (getBlockSignature(bellatrix))": + let + forked = getBlock(ConsensusFork.Bellatrix) + blockRoot = withBlck(forked): hash_tree_root(blck) + + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing BeaconBlock (getBlockSignature(capella))": + let + forked = getBlock(ConsensusFork.Capella) + blockRoot = withBlck(forked): hash_tree_root(blck) + + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Signing BeaconBlock (getBlockSignature(deneb))": + let + forked = getBlock(ConsensusFork.Deneb) + blockRoot = withBlck(forked): hash_tree_root(blck) + + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot, forked) + + check: + sres1.isOk() + sres2.isOk() + sres3.isOk() + rres1.isOk() + rres2.isOk() + rres3.isOk() + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + + asyncTest "Connections pool stress test": + const TestsCount = 100 + var + futures1: seq[Future[SignatureResult]] + futures2: seq[Future[SignatureResult]] + + for i in 0 ..< TestsCount: + futures1.add(validator4.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(i))) + await allFutures(futures1) + for fut in futures1: + check fut.done() + + await sleepAsync(seconds(SigningRequestTimeoutSeconds) + 100.milliseconds) + + for i in 0 ..< TestsCount: + futures2.add(validator4.getEpochSignature(SigningFork, + GenesisValidatorsRoot, Epoch(i))) + + await allFutures(futures2) + + for fut in futures2: + check fut.done() + + for i in 0 ..< TestsCount: + let + sres1 = futures1[i].read() + sres2 = futures2[i].read() + check: + sres1.isOk() + sres2.isOk() + sres1.get() == sres2.get() + + asyncTest "Idle connection test": + let + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3Signer) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = + RestClientRef.new(remoteUrl, prestoFlags, {}, + idleTimeout = 1.seconds, + idlePeriod = 250.milliseconds) + + check rclient.isOk() + let client = rclient.get() + + try: + block: + let response = await client.getKeys() + check: + response.status == 200 + len(response.data) == 3 + let + received = sorted([ + "0x" & response.data[0].toHex(), + "0x" & response.data[1].toHex(), + "0x" & response.data[2].toHex() + ]) + expected = sorted([ + ValidatorPubKey1, + ValidatorPubKey2, + ValidatorPubKey3 + ]) + check received == expected + + await sleepAsync(seconds(SigningRequestTimeoutSeconds) + 100.milliseconds) + + block: + let response = await client.getKeys() + check: + response.status == 200 + len(response.data) == 3 + let + received = sorted([ + "0x" & response.data[0].toHex(), + "0x" & response.data[1].toHex(), + "0x" & response.data[2].toHex() + ]) + expected = sorted([ + ValidatorPubKey1, + ValidatorPubKey2, + ValidatorPubKey3 + ]) + check received == expected + + finally: + await client.closeWait() + + asyncTest "Connection timeout test": + let + request = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Epoch(10)) + deadline = newFuture[void]() + (client, info) = validator4.clients[0] + + deadline.complete() + let res = await client.signData(info.pubkey, deadline, 1, request) + check: + res.isErr() + res.error.kind == Web3SignerErrorKind.TimeoutError + + asyncTest "Public keys reload (/reload) test": + let + res = createAdditionalKeystore(RemoteSignerType.Web3Signer) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3Signer) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + + check: + res.isOk() + rclient.isOk() + + let client = rclient.get() + check res.isOk() + try: + block: + let response = await client.reload() + check response.status == 200 + block: + let response = await client.getKeys() + check: + response.status == 200 + len(response.data) == 4 + let + received = sorted([ + "0x" & response.data[0].toHex(), + "0x" & response.data[1].toHex(), + "0x" & response.data[2].toHex(), + "0x" & response.data[3].toHex() + ]) + expected = sorted([ + ValidatorPubKey1, + ValidatorPubKey2, + ValidatorPubKey3, + ValidatorPubKey4 + ]) + check received == expected + finally: + await client.closeWait() + + shutdownSigningNodeProcess(process) + removeTestDir(RemoteSignerType.Web3Signer) + +suite "Nimbus remote signer/signing test (web3signer-diva)": + let res = createTestDir(RemoteSignerType.Web3SignerDiva) + doAssert(res.isOk()) + + let pres = spawnSigningNodeProcess(RemoteSignerType.Web3SignerDiva) + doAssert(pres.isOk()) + let process = pres.get() + + setup: + let pool1 = newClone(default(ValidatorPool)) + let + validator1 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey1).get(), + default(Eth1Address), 300_000_000'u64 + ) + validator2 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey2).get(), + default(Eth1Address), 300_000_000'u64 + ) + validator3 = pool1[].addValidator( + getLocalKeystoreData(ValidatorPrivateKey3).get(), + default(Eth1Address), 300_000_000'u64 + ) + + validator1.index = Opt.some(ValidatorIndex(100)) + validator2.index = Opt.some(ValidatorIndex(101)) + validator3.index = Opt.some(ValidatorIndex(102)) + + let pool2 = newClone(default(ValidatorPool)) + let validator4 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey1, + RemoteSignerType.Web3SignerDiva).get(), + default(Eth1Address), 300_000_000'u64 + ) + let validator5 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey2, + RemoteSignerType.Web3SignerDiva).get(), + default(Eth1Address), 300_000_000'u64 + ) + let validator6 = pool2[].addValidator( + getRemoteKeystoreData(ValidatorPubKey3, + RemoteSignerType.Web3SignerDiva).get(), + default(Eth1Address), 300_000_000'u64 + ) + + validator4.index = Opt.some(ValidatorIndex(100)) + validator5.index = Opt.some(ValidatorIndex(101)) + validator6.index = Opt.some(ValidatorIndex(102)) + + asyncTest "Waiting for signing node (/upcheck) test": + let + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + + check rclient.isOk() + let client = rclient.get() + + var attempts = 0 + while attempts < 3: + let loopBreak = + try: + let response = await client.getUpcheck() + check: + response.status == 200 + response.data.status == "OK" + true + except CatchableError: + inc(attempts) + false + if loopBreak: + break + await sleepAsync(500.milliseconds) + + await client.closeWait() + + asyncTest "Signing BeaconBlock (getBlockSignature(phase0))": + let + fork = ConsensusFork.Phase0 + forked1 = getBlock(fork) + blockRoot1 = withBlck(forked1): hash_tree_root(blck) + forked2 = getBlock(fork, SigningOtherFeeRecipient) + blockRoot2 = withBlck(forked2): hash_tree_root(blck) + request1 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1)) + request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1), @[]) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + + check rclient.isOk() + + let + client = rclient.get() + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + bres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + + check: + # Local requests + sres1.isOk() + sres2.isOk() + sres3.isOk() + # Phase0 blocks do not have FeeRecipient field, so it should not care + rres1.isErr() + rres2.isErr() + rres3.isErr() + # Phase0 blocks do not have FeeRecipient field, so it should not care + bres1.isErr() + bres2.isErr() + bres3.isErr() + + try: + let + # `proofs` array is not present. + response1 = await client.signDataPlain(publicKey1, request1) + response2 = await client.signDataPlain(publicKey2, request1) + response3 = await client.signDataPlain(publicKey3, request1) + # `proofs` array is empty. + response4 = await client.signDataPlain(publicKey1, request2) + response5 = await client.signDataPlain(publicKey2, request2) + response6 = await client.signDataPlain(publicKey3, request2) + check: + # When `Phase0` block specified remote signer should ignore `proof` + # field and its value + response1.status == 400 + response2.status == 400 + response3.status == 400 + response4.status == 400 + response5.status == 400 + response6.status == 400 + finally: + await client.closeWait() + + asyncTest "Signing BeaconBlock (getBlockSignature(altair))": + let + fork = ConsensusFork.Altair + forked1 = getBlock(fork) + blockRoot1 = withBlck(forked1): hash_tree_root(blck) + forked2 = getBlock(fork, SigningOtherFeeRecipient) + blockRoot2 = withBlck(forked2): hash_tree_root(blck) + request1 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1)) + request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1), @[]) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + + check rclient.isOk() + + let + client = rclient.get() + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + bres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + + check: + # Local requests + sres1.isOk() + sres2.isOk() + sres3.isOk() + # Altair block do not have FeeRecipient field, so it should not care + rres1.isErr() + rres2.isErr() + rres3.isErr() + # Altair block do not have FeeRecipient field, so it should not care + bres1.isErr() + bres2.isErr() + bres3.isErr() + + try: + let + # `proofs` array is not present. + response1 = await client.signDataPlain(publicKey1, request1) + response2 = await client.signDataPlain(publicKey2, request1) + response3 = await client.signDataPlain(publicKey3, request1) + # `proofs` array is empty. + response4 = await client.signDataPlain(publicKey1, request2) + response5 = await client.signDataPlain(publicKey2, request2) + response6 = await client.signDataPlain(publicKey3, request2) + check: + # When `Altair` block specified remote signer should ignore `proof` + # field and its value. + response1.status == 400 + response2.status == 400 + response3.status == 400 + response4.status == 400 + response5.status == 400 + response6.status == 400 + finally: + await client.closeWait() + + asyncTest "Signing BeaconBlock (getBlockSignature(bellatrix))": + let + fork = ConsensusFork.Bellatrix + forked1 = getBlock(fork) + blockRoot1 = withBlck(forked1): hash_tree_root(blck) + forked2 = getBlock(fork, SigningOtherFeeRecipient) + blockRoot2 = withBlck(forked2): hash_tree_root(blck) + request1 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1)) + request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1), @[]) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + + check rclient.isOk() + + let + client = rclient.get() + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + bres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + + check: + # Local requests + sres1.isOk() + sres2.isOk() + sres3.isOk() + # Remote requests with proper merkle proof of proper FeeRecipent field + rres1.isOk() + rres2.isOk() + rres3.isOk() + # Signature comparison + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + # Remote requests with changed FeeRecipient field + bres1.isErr() + bres2.isErr() + bres3.isErr() + + try: + let + # `proofs` array is not present. + response1 = await client.signDataPlain(publicKey1, request1) + response2 = await client.signDataPlain(publicKey2, request1) + response3 = await client.signDataPlain(publicKey3, request1) + # `proofs` array is empty. + response4 = await client.signDataPlain(publicKey1, request2) + response5 = await client.signDataPlain(publicKey2, request2) + response6 = await client.signDataPlain(publicKey3, request2) + check: + response1.status == 400 + response2.status == 400 + response3.status == 400 + response4.status == 400 + response5.status == 400 + response6.status == 400 + finally: + await client.closeWait() + + asyncTest "Signing BeaconBlock (getBlockSignature(capella))": + let + fork = ConsensusFork.Capella + forked1 = getBlock(fork) + blockRoot1 = withBlck(forked1): hash_tree_root(blck) + forked2 = getBlock(fork, SigningOtherFeeRecipient) + blockRoot2 = withBlck(forked2): hash_tree_root(blck) + request1 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1)) + request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1), @[]) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + + check rclient.isOk() + + let + client = rclient.get() + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + bres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + + check: + # Local requests + sres1.isOk() + sres2.isOk() + sres3.isOk() + # Remote requests with proper merkle proof of proper FeeRecipent field + rres1.isOk() + rres2.isOk() + rres3.isOk() + # Signature comparison + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + # Remote requests with changed FeeRecipient field + bres1.isErr() + bres2.isErr() + bres3.isErr() + + try: + let + # `proofs` array is not present. + response1 = await client.signDataPlain(publicKey1, request1) + response2 = await client.signDataPlain(publicKey2, request1) + response3 = await client.signDataPlain(publicKey3, request1) + # `proofs` array is empty. + response4 = await client.signDataPlain(publicKey1, request2) + response5 = await client.signDataPlain(publicKey2, request2) + response6 = await client.signDataPlain(publicKey3, request2) + check: + response1.status == 400 + response2.status == 400 + response3.status == 400 + response4.status == 400 + response5.status == 400 + response6.status == 400 + finally: + await client.closeWait() + + asyncTest "Signing BeaconBlock (getBlockSignature(deneb))": + let + fork = ConsensusFork.Deneb + forked1 = getBlock(fork) + blockRoot1 = withBlck(forked1): hash_tree_root(blck) + forked2 = getBlock(fork, SigningOtherFeeRecipient) + blockRoot2 = withBlck(forked2): hash_tree_root(blck) + request1 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1)) + request2 = Web3SignerRequest.init(SigningFork, GenesisValidatorsRoot, + Web3SignerForkedBeaconBlock.init(forked1), @[]) + remoteUrl = "http://" & SigningNodeAddress & ":" & + $getNodePort(RemoteSignerType.Web3SignerDiva) + prestoFlags = {RestClientFlag.CommaSeparatedArray} + rclient = RestClientRef.new(remoteUrl, prestoFlags, {}) + publicKey1 = ValidatorPubKey.fromHex(ValidatorPubKey1).get() + publicKey2 = ValidatorPubKey.fromHex(ValidatorPubKey2).get() + publicKey3 = ValidatorPubKey.fromHex(ValidatorPubKey3).get() + + check rclient.isOk() + + let + client = rclient.get() + sres1 = + await validator1.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres2 = + await validator2.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + sres3 = + await validator3.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + rres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot1, forked1) + bres1 = + await validator4.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres2 = + await validator5.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + bres3 = + await validator6.getBlockSignature(SigningFork, GenesisValidatorsRoot, + Slot(1), blockRoot2, forked2) + + check: + # Local requests + sres1.isOk() + sres2.isOk() + sres3.isOk() + # Remote requests with proper merkle proof of proper FeeRecipent field + rres1.isOk() + rres2.isOk() + rres3.isOk() + # Signature comparison + sres1.get() == rres1.get() + sres2.get() == rres2.get() + sres3.get() == rres3.get() + # Remote requests with changed FeeRecipient field + bres1.isErr() + bres2.isErr() + bres3.isErr() + + try: + let + # `proofs` array is not present. + response1 = await client.signDataPlain(publicKey1, request1) + response2 = await client.signDataPlain(publicKey2, request1) + response3 = await client.signDataPlain(publicKey3, request1) + # `proofs` array is empty. + response4 = await client.signDataPlain(publicKey1, request2) + response5 = await client.signDataPlain(publicKey2, request2) + response6 = await client.signDataPlain(publicKey3, request2) + check: + response1.status == 400 + response2.status == 400 + response3.status == 400 + response4.status == 400 + response5.status == 400 + response6.status == 400 + finally: + await client.closeWait() + + shutdownSigningNodeProcess(process) + removeTestDir(RemoteSignerType.Web3SignerDiva) diff --git a/vendor/nim-presto b/vendor/nim-presto index ba20cebf0..2b440a443 160000 --- a/vendor/nim-presto +++ b/vendor/nim-presto @@ -1 +1 @@ -Subproject commit ba20cebf0f729b52d90487f34efb5923dfc7b2c7 +Subproject commit 2b440a443f3fc29197f267879e16bb8057ccc0ed