From ac1b02698a878178125faaae952accfd1f596256 Mon Sep 17 00:00:00 2001 From: Eugene Kabanov Date: Thu, 6 Jul 2023 12:14:22 +0300 Subject: [PATCH] VC: Use scoring function to select best attestation data when using multiple BNs. (#5101) * Initial commit. * Move score selection log statement to debug level. * Fix proper float64 log format. * Cleanup imports and legacy code. * Address review comments. * Address review comments. * Fix scoring function. * Address review comments. * Address review comments 2. Fix registerBlock post-rebase issues. * Simplify innerLoop decision making. * Make getAttestationDataScore() more testable. Add tests for getAttestationDataScore(). * Add modified AllTests copy. --- AllTests-mainnet.md | 5 +- beacon_chain/validator_client/api.nim | 378 +++++++++++++++------- beacon_chain/validator_client/common.nim | 14 +- beacon_chain/validator_client/scoring.nim | 49 +++ tests/test_validator_client.nim | 103 +++++- 5 files changed, 420 insertions(+), 129 deletions(-) create mode 100644 beacon_chain/validator_client/scoring.nim diff --git a/AllTests-mainnet.md b/AllTests-mainnet.md index 8739be906..f031c30d7 100644 --- a/AllTests-mainnet.md +++ b/AllTests-mainnet.md @@ -575,9 +575,10 @@ OK: 24/24 Fail: 0/24 Skip: 0/24 OK: 1/1 Fail: 0/1 Skip: 0/1 ## Validator Client test suite ```diff ++ getAttestationDataScore() test vectors OK + normalizeUri() test vectors OK ``` -OK: 1/1 Fail: 0/1 Skip: 0/1 +OK: 2/2 Fail: 0/2 Skip: 0/2 ## Validator change pool testing suite ```diff + addValidatorChangeMessage/getAttesterSlashingMessage OK @@ -688,4 +689,4 @@ OK: 2/2 Fail: 0/2 Skip: 0/2 OK: 9/9 Fail: 0/9 Skip: 0/9 ---TOTAL--- -OK: 389/394 Fail: 0/394 Skip: 5/394 +OK: 390/395 Fail: 0/395 Skip: 5/395 diff --git a/beacon_chain/validator_client/api.nim b/beacon_chain/validator_client/api.nim index f56c1398f..1923eb7df 100644 --- a/beacon_chain/validator_client/api.nim +++ b/beacon_chain/validator_client/api.nim @@ -5,10 +5,11 @@ # * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except according to those terms. -import chronicles -import ../spec/eth2_apis/eth2_rest_serialization, - ../spec/datatypes/[phase0, altair] -import common, fallback_service +import std/strutils +import chronicles, stew/base10 +import ".."/spec/eth2_apis/eth2_rest_serialization, + ".."/spec/datatypes/[phase0, altair] +import "."/[common, fallback_service, scoring] export eth2_rest_serialization, common @@ -35,12 +36,47 @@ type status*: ApiOperation data*: seq[ApiNodeResponse[T]] + ApiScore* = object + index*: int + score*: Opt[float64] + + BestNodeResponse*[T] = object + node*: BeaconNodeServerRef + data*: ApiResponse[T] + score*: float64 + const ViableNodeStatus = {RestBeaconNodeStatus.Compatible, RestBeaconNodeStatus.NotSynced, RestBeaconNodeStatus.OptSynced, RestBeaconNodeStatus.Synced} +proc `$`*(s: ApiScore): string = + var res = Base10.toString(uint64(s.index)) + res.add(": ") + if s.score.isSome(): + res.add(shortScore(s.score.get())) + else: + res.add("") + res + +proc `$`*(ss: openArray[ApiScore]): string = + "[" & ss.mapIt($it).join(",") & "]" + +chronicles.formatIt(seq[ApiScore]): + $it + +func init*(t: typedesc[ApiScore], node: BeaconNodeServerRef, + score: float64): ApiScore = + ApiScore(index: node.index, score: Opt.some(score)) + +func init*(t: typedesc[ApiScore], node: BeaconNodeServerRef): ApiScore = + ApiScore(index: node.index, score: Opt.none(float64)) + +func init*[T](t: typedesc[BestNodeResponse], node: BeaconNodeServerRef, + data: ApiResponse[T], score: float64): BestNodeResponse[T] = + BestNodeResponse[T](node: node, data: data, score: score) + proc lazyWaiter(node: BeaconNodeServerRef, request: FutureBase, requestName: string, strategy: ApiStrategyKind) {.async.} = try: @@ -78,6 +114,18 @@ proc lazyWait(nodes: seq[BeaconNodeServerRef], requests: seq[FutureBase], else: await allFutures(futures) +proc apiResponseOr[T](future: FutureBase, timerFut: Future[void], + message: string): ApiResponse[T] = + if future.finished(): + doAssert(not(future.cancelled())) + if future.failed(): + ApiResponse[T].err($future.error.msg) + else: + ApiResponse[T].ok(Future[T](future).read()) + else: + doAssert(timerFut.finished()) + ApiResponse[T].err(message) + template firstSuccessParallel*( vc: ValidatorClientRef, responseType: typedesc, @@ -118,8 +166,7 @@ template firstSuccessParallel*( # This case could not be happened. error "Unexpected exception while waiting for beacon nodes", err_name = $exc.name, err_msg = $exc.msg - var default: seq[BeaconNodeServerRef] - default + default(seq[BeaconNodeServerRef]) if len(onlineNodes) == 0: retRes = ApiResponse[handlerType].err("No online beacon node(s)") @@ -183,15 +230,8 @@ template firstSuccessParallel*( let node {.inject.} = beaconNode apiResponse {.inject.} = - if timerFut.finished(): - ApiResponse[responseType].err( - "Timeout exceeded while awaiting for the response") - else: - if requestFut.failed(): - ApiResponse[responseType].err($requestFut.error.msg) - else: - ApiResponse[responseType].ok( - Future[responseType](requestFut).read()) + apiResponseOr[responseType](requestFut, timerFut, + "Timeout exceeded while awaiting for the response") handlerResponse = try: body2 @@ -200,7 +240,7 @@ template firstSuccessParallel*( except CatchableError: raiseAssert("Response handler must not raise exceptions") - if apiResponse.isOk() and handlerResponse.isOk(): + if handlerResponse.isOk(): retRes = handlerResponse resultReady = true asyncSpawn lazyWait(pendingNodes, pendingRequests, timerFut, @@ -213,7 +253,7 @@ template firstSuccessParallel*( pendingCancel.add(raceFut.cancelAndWait()) if not(isNil(timerFut)) and not(timerFut.finished()): pendingCancel.add(timerFut.cancelAndWait()) - for index, future in pendingRequests.pairs(): + for future in pendingRequests.items(): if not(future.finished()): pendingCancel.add(future.cancelAndWait()) await allFutures(pendingCancel) @@ -236,13 +276,16 @@ template firstSuccessParallel*( template bestSuccess*( vc: ValidatorClientRef, responseType: typedesc, + handlerType: typedesc, timeout: Duration, statuses: set[RestBeaconNodeStatus], roles: set[BeaconNodeRole], bodyRequest, - bodyScore: untyped): ApiResponse[responseType] = - var it {.inject.}: RestClientRef - type BodyType = typeof(bodyRequest) + bodyScore, + bodyHandler: untyped): ApiResponse[handlerType] = + var + it {.inject.}: RestClientRef + iterations = 0 var timerFut = if timeout != InfiniteDuration: @@ -250,114 +293,166 @@ template bestSuccess*( else: nil - let onlineNodes = - try: - await vc.waitNodes(timerFut, statuses, roles, false) - vc.filterNodes(statuses, roles) - except CancelledError as exc: - if not(isNil(timerFut)) and not(timerFut.finished()): - await timerFut.cancelAndWait() - raise exc - except CatchableError as exc: - # This case could not be happened. - error "Unexpected exception while waiting for beacon nodes", - err_name = $exc.name, err_msg = $exc.msg - var default: seq[BeaconNodeServerRef] - default + var + retRes: ApiResponse[handlerType] + scores: seq[ApiScore] + bestResponse: Opt[BestNodeResponse[handlerType]] - if len(onlineNodes) == 0: - ApiResponse[responseType].err("No online beacon node(s)") - else: - let - (pendingRequests, pendingNodes) = - block: - var requests: seq[BodyType] - var nodes: seq[BeaconNodeServerRef] - for node {.inject.} in onlineNodes: - it = node.client - let fut = bodyRequest - requests.add(fut) - nodes.add(node) - (requests, nodes) - - status = + block mainLoop: + while true: + let onlineNodes = try: - if isNil(timerFut): - await allFutures(pendingRequests) - ApiOperation.Success + if iterations == 0: + # We are not going to wait for BNs if there some available. + await vc.waitNodes(timerFut, statuses, roles, false) else: - let waitFut = allFutures(pendingRequests) - discard await race(waitFut, timerFut) - if not(waitFut.finished()): - await waitFut.cancelAndWait() - ApiOperation.Timeout - else: - if not(timerFut.finished()): - await timerFut.cancelAndWait() - ApiOperation.Success + # We get here only, if all the requests are failed. To avoid requests + # spam we going to wait for changes in BNs statuses. + await vc.waitNodes(timerFut, statuses, roles, true) + vc.filterNodes(statuses, roles) except CancelledError as exc: - # We should cancel all the pending requests and timer before we return - # result. - var pendingCancel: seq[Future[void]] - for future in pendingRequests: - if not(fut.finished()): - pendingCancel.add(fut.cancelAndWait()) if not(isNil(timerFut)) and not(timerFut.finished()): - pendingCancel.add(timerFut.cancelAndWait()) - await allFutures(pendingCancel) + await timerFut.cancelAndWait() raise exc - except CatchableError: - # This should not be happened, because allFutures() and race() did not - # raise any exceptions. - ApiOperation.Failure + except CatchableError as exc: + # This case could not be happened. + error "Unexpected exception while waiting for beacon nodes", + err_name = $exc.name, err_msg = $exc.msg + default(seq[BeaconNodeServerRef]) - apiResponses {.inject.} = - block: - var res: seq[ApiNodeResponse[responseType]] - for requestFut, pnode in pendingRequests.pairs(): - let beaconNode = pendingNodes[index] - if requestFut.finished(): - if requestFut.failed(): - let exc = requestFut.readError() - debug "One of operation requests has been failed", - node = beaconNode, err_name = $exc.name, - err_msg = $exc.msg - beaconNode.status.updateStatus(RestBeaconNodeStatus.Offline) - elif future.cancelled(): - debug "One of operation requests has been interrupted", - node = beaconNode - else: - res.add( - ApiNodeResponse( - node: beaconNode, - data: ApiResponse[responseType].ok(future.read()) - ) - ) - else: - case status - of ApiOperation.Timeout: - debug "One of operation requests has been timed out", - node = beaconNode - pendingNodes[index].status = RestBeaconNodeStatus.Offline - of ApiOperation.Success, ApiOperation.Failure, - ApiOperation.Interrupt: - # This should not be happened, because all Futures should be - # finished. - debug "One of operation requests failed unexpectedly", - node = beaconNode - pendingNodes[index].status = RestBeaconNodeStatus.Offline - res - - if len(apiResponses) == 0: - ApiResponse[responseType].err("No successful responses available") - else: - let index = bestScore - if index >= 0: - debug "Operation request result was selected", - node = apiResponses[index].node - apiResponses[index].data + if len(onlineNodes) == 0: + retRes = ApiResponse[handlerType].err("No online beacon node(s)") + break mainLoop else: - ApiResponse[responseType].err("Unable to get best response") + var + (pendingRequests, pendingNodes) = + block: + var requests: seq[FutureBase] + var nodes: seq[BeaconNodeServerRef] + for node {.inject.} in onlineNodes: + it = node.client + let fut = FutureBase(bodyRequest) + requests.add(fut) + nodes.add(node) + (requests, nodes) + perfectScoreFound = false + + block innerLoop: + while len(pendingRequests) > 0: + var + finishedRequests: seq[FutureBase] + finishedNodes: seq[BeaconNodeServerRef] + raceFut: Future[FutureBase] + try: + raceFut = race(pendingRequests) + + if isNil(timerFut): + await raceFut or timerFut + else: + await allFutures(raceFut) + + for index, future in pendingRequests.pairs(): + if future.finished() or + (not(isNil(timerFut)) and timerFut.finished()): + finishedRequests.add(future) + finishedNodes.add(pendingNodes[index]) + let + node {.inject.} = pendingNodes[index] + apiResponse {.inject.} = + apiResponseOr[responseType](future, timerFut, + "Timeout exceeded while awaiting for the response") + handlerResponse = + try: + bodyHandler + except CancelledError as exc: + raise exc + except CatchableError: + raiseAssert( + "Response handler must not raise exceptions") + + if handlerResponse.isOk(): + let + itresponse {.inject.} = handlerResponse.get() + score = + try: + bodyScore + except CancelledError as exc: + raise exc + except CatchableError: + raiseAssert("Score handler must not raise exceptions") + scores.add(ApiScore.init(node, score)) + if bestResponse.isNone() or + (score > bestResponse.get().score): + bestResponse = Opt.some( + BestNodeResponse.init(node, handlerResponse, score)) + if perfectScore(score): + perfectScoreFound = true + break + else: + scores.add(ApiScore.init(node)) + + if perfectScoreFound: + # lazyWait will cancel `pendingRequests` on timeout. + asyncSpawn lazyWait(pendingNodes, pendingRequests, timerFut, + RequestName, strategy) + break innerLoop + + if not(isNil(timerFut)) and timerFut.finished(): + # If timeout is exceeded we need to cancel all the tasks which + # are still running. + var pendingCancel: seq[Future[void]] + for future in pendingRequests.items(): + if not(future.finished()): + pendingCancel.add(future.cancelAndWait()) + await allFutures(pendingCancel) + break innerLoop + + pendingRequests.keepItIf(it notin finishedRequests) + pendingNodes.keepItIf(it notin finishedNodes) + + except CancelledError as exc: + var pendingCancel: seq[Future[void]] + # `or` operation does not cancelling Futures passed as arguments. + if not(isNil(raceFut)) and not(raceFut.finished()): + pendingCancel.add(raceFut.cancelAndWait()) + if not(isNil(timerFut)) and not(timerFut.finished()): + pendingCancel.add(timerFut.cancelAndWait()) + # We should cancel all the requests which are still pending. + for future in pendingRequests.items(): + if not(future.finished()): + pendingCancel.add(future.cancelAndWait()) + # Awaiting cancellations. + await allFutures(pendingCancel) + raise exc + except CatchableError as exc: + # This should not be happened, because allFutures() and race() + # did not raise any exceptions. + error "Unexpected exception while processing request", + err_name = $exc.name, err_msg = $exc.msg + retRes = ApiResponse[handlerType].err("Unexpected error") + break mainLoop + + if bestResponse.isSome(): + retRes = bestResponse.get().data + break mainLoop + else: + if timerFut.finished(): + retRes = ApiResponse[handlerType].err( + "Timeout exceeded while awaiting for responses") + break mainLoop + else: + # When all requests failed + discard + + inc(iterations) + + if retRes.isOk(): + debug "Best score result selected", + request = RequestName, available_scores = scores, + best_score = shortScore(bestResponse.get().score), + best_node = bestResponse.get().node + + retRes template onceToAll*( vc: ValidatorClientRef, @@ -1225,7 +1320,7 @@ proc produceAttestationData*( var failures: seq[ApiNodeFailure] case strategy - of ApiStrategyKind.First, ApiStrategyKind.Best: + of ApiStrategyKind.First: let res = vc.firstSuccessParallel( RestPlainResponse, ProduceAttestationDataResponse, @@ -1266,6 +1361,47 @@ proc produceAttestationData*( raise (ref ValidatorApiError)(msg: res.error, data: failures) return res.get().data + of ApiStrategyKind.Best: + let res = vc.bestSuccess( + RestPlainResponse, + ProduceAttestationDataResponse, + OneThirdDuration, + ViableNodeStatus, + {BeaconNodeRole.AttestationData}, + produceAttestationDataPlain(it, slot, committee_index), + getAttestationDataScore(vc, itresponse)): + if apiResponse.isErr(): + handleCommunicationError() + ApiResponse[ProduceAttestationDataResponse].err(apiResponse.error) + else: + let response = apiResponse.get() + case response.status + of 200: + let res = decodeBytes(ProduceAttestationDataResponse, response.data, + response.contentType) + if res.isErr(): + handleUnexpectedData() + ApiResponse[ProduceAttestationDataResponse].err($res.error) + else: + ApiResponse[ProduceAttestationDataResponse].ok(res.get()) + of 400: + handle400() + ApiResponse[ProduceAttestationDataResponse].err(ResponseInvalidError) + of 500: + handle500() + ApiResponse[ProduceAttestationDataResponse].err(ResponseInternalError) + of 503: + handle503() + ApiResponse[ProduceAttestationDataResponse].err( + ResponseNoSyncError) + else: + handleUnexpectedCode() + ApiResponse[ProduceAttestationDataResponse].err( + ResponseUnexpectedError) + if res.isErr(): + raise (ref ValidatorApiError)(msg: res.error, data: failures) + return res.get().data + of ApiStrategyKind.Priority: vc.firstSuccessSequential( RestPlainResponse, diff --git a/beacon_chain/validator_client/common.nim b/beacon_chain/validator_client/common.nim index b2ac5314b..404975054 100644 --- a/beacon_chain/validator_client/common.nim +++ b/beacon_chain/validator_client/common.nim @@ -208,6 +208,7 @@ type dynamicFeeRecipientsStore*: ref DynamicFeeRecipientsStore validatorsRegCache*: Table[ValidatorPubKey, SignedValidatorRegistrationV1] blocksSeen*: Table[Slot, BlockDataItem] + rootsSeen*: Table[Eth2Digest, Slot] processingDelay*: Opt[Duration] rng*: ref HmacDrbgContext @@ -1207,23 +1208,25 @@ proc expectBlock*(vc: ValidatorClientRef, slot: Slot, if not(retFuture.finished()): retFuture.cancelCallback = cancellation retFuture -proc registerBlock*(vc: ValidatorClientRef, data: EventBeaconBlockObject, +proc registerBlock*(vc: ValidatorClientRef, eblck: EventBeaconBlockObject, node: BeaconNodeServerRef) = let wallTime = vc.beaconClock.now() - delay = wallTime - data.slot.start_beacon_time() + delay = wallTime - eblck.slot.start_beacon_time() - debug "Block received", slot = data.slot, - block_root = shortLog(data.block_root), optimistic = data.optimistic, + debug "Block received", slot = eblck.slot, + block_root = shortLog(eblck.block_root), optimistic = eblck.optimistic, node = node, delay = delay proc scheduleCallbacks(data: var BlockDataItem, blck: EventBeaconBlockObject) = + vc.rootsSeen[blck.block_root] = blck.slot data.blocks.add(blck.block_root) for mitem in data.waiters.mitems(): if mitem.count >= len(data.blocks): if not(mitem.future.finished()): mitem.future.complete(data.blocks) - vc.blocksSeen.mgetOrPut(data.slot, BlockDataItem()).scheduleCallbacks(data) + + vc.blocksSeen.mgetOrPut(eblck.slot, BlockDataItem()).scheduleCallbacks(eblck) proc pruneBlocksSeen*(vc: ValidatorClientRef, epoch: Epoch) = var blocksSeen: Table[Slot, BlockDataItem] @@ -1231,6 +1234,7 @@ proc pruneBlocksSeen*(vc: ValidatorClientRef, epoch: Epoch) = if (slot.epoch() + HISTORICAL_DUTIES_EPOCHS) >= epoch: blocksSeen[slot] = item else: + for root in item.blocks: vc.rootsSeen.del(root) let blockRoot = if len(item.blocks) == 0: "" diff --git a/beacon_chain/validator_client/scoring.nim b/beacon_chain/validator_client/scoring.nim new file mode 100644 index 000000000..cb9c0e139 --- /dev/null +++ b/beacon_chain/validator_client/scoring.nim @@ -0,0 +1,49 @@ +# beacon_chain +# Copyright (c) 2023 Status Research & Development GmbH +# Licensed and distributed under either of +# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT). +# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0). +# at your option. This file may not be copied, modified, or distributed except according to those terms. + +import std/strutils +import "."/common + +{.push raises: [].} + +func perfectScore*(score: float64): bool = + score == Inf + +proc shortScore*(score: float64): string = + if score == Inf: "" else: formatFloat(score, ffDecimal, 4) + +proc getAttestationDataScore*(rootsSeen: Table[Eth2Digest, Slot], + adata: ProduceAttestationDataResponse): float64 = + let + slot = rootsSeen.getOrDefault( + adata.data.beacon_block_root, FAR_FUTURE_SLOT) + + let res = + if (slot == adata.data.slot) and + (adata.data.source.epoch + 1 == adata.data.target.epoch): + # Perfect score + Inf + else: + let score = float64(adata.data.source.epoch) + + float64(adata.data.target.epoch) + if slot == FAR_FUTURE_SLOT: + score + else: + if adata.data.slot + 1 == slot: + # To avoid `DivizionByZero` defect. + score + else: + score + float64(1) / (float64(adata.data.slot) + float64(1) - + float64(slot)) + + debug "Attestation score", attestation_data = shortLog(adata.data), + block_slot = slot, score = shortScore(res) + res + +proc getAttestationDataScore*(vc: ValidatorClientRef, + adata: ProduceAttestationDataResponse): float64 = + getAttestationDataScore(vc.rootsSeen, adata) diff --git a/tests/test_validator_client.nim b/tests/test_validator_client.nim index e798dc896..1ba828713 100644 --- a/tests/test_validator_client.nim +++ b/tests/test_validator_client.nim @@ -10,7 +10,7 @@ import std/strutils import unittest2 -import ../beacon_chain/validator_client/common +import ../beacon_chain/validator_client/[common, scoring] const HostNames = [ @@ -143,9 +143,110 @@ const ("", "err(Missing hostname)") ] +type + AttestationDataTuple* = tuple[ + slot: uint64, + index: uint64, + beacon_block_root: string, + source: uint64, + target: uint64 + ] + +const + AttestationDataVectors = [ + # Attestation score with block monitoring enabled (perfect). + ((6002798'u64, 10'u64, "22242212", 187586'u64, 187587'u64), + ("22242212", 6002798'u64), ""), + ((6002811'u64, 24'u64, "26ec78d6", 187586'u64, 187587'u64), + ("26ec78d6", 6002811'u64), ""), + ((6002821'u64, 11'u64, "10c6d1a2", 187587'u64, 187588'u64), + ("10c6d1a2", 6002821'u64), ""), + ((6002836'u64, 15'u64, "42354ded", 187587'u64, 187588'u64), + ("42354ded", 6002836'u64), ""), + ((6002859'u64, 10'u64, "97d8ac69", 187588'u64, 187589'u64), + ("97d8ac69", 6002859'u64), ""), + # Attestation score with block monitoring enabled #1 (not perfect). + ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), + ("524a9e2b", 6002870'u64), "375177.5000"), + ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), + ("524a9e2b", 6002869'u64), "375177.3333"), + ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), + ("524a9e2b", 6002868'u64), "375177.2500"), + ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), + ("524a9e2b", 6002867'u64), "375177.2000"), + ((6002871'u64, 25'u64, "524a9e2b", 187588'u64, 187589'u64), + ("524a9e2b", 6002866'u64), "375177.1667"), + # Attestation score with block monitoring enabled #2 (not perfect). + ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), + ("22a19d87", 6002961'u64), "375183.5000"), + ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), + ("22a19d87", 6002960'u64), "375183.3333"), + ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), + ("22a19d87", 6002959'u64), "375183.2500"), + ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), + ("22a19d87", 6002958'u64), "375183.2000"), + ((6002962'u64, 14'u64, "22a19d87", 187591'u64, 187592'u64), + ("22a19d87", 6002957'u64), "375183.1667"), + # Attestation score with block monitoring disabled #1. + ((6003217'u64, 52'u64, "5e945218", 187599'u64, 187600'u64), + ("00000000", 0'u64), "375199.0000"), + ((6003217'u64, 52'u64, "5e945218", 187598'u64, 187600'u64), + ("00000000", 0'u64), "375198.0000"), + ((6003217'u64, 52'u64, "5e945218", 187597'u64, 187600'u64), + ("00000000", 0'u64), "375197.0000"), + ((6003217'u64, 52'u64, "5e945218", 187596'u64, 187600'u64), + ("00000000", 0'u64), "375196.0000"), + ((6003217'u64, 52'u64, "5e945218", 187595'u64, 187600'u64), + ("00000000", 0'u64), "375195.0000"), + # Attestation score with block monitoring disabled #2. + ((6003257'u64, 9'u64, "7bfa464e", 187600'u64, 187601'u64), + ("00000000", 0'u64), "375201.0000"), + ((6003257'u64, 9'u64, "7bfa464e", 187599'u64, 187601'u64), + ("00000000", 0'u64), "375200.0000"), + ((6003257'u64, 9'u64, "7bfa464e", 187598'u64, 187601'u64), + ("00000000", 0'u64), "375199.0000"), + ((6003257'u64, 9'u64, "7bfa464e", 187597'u64, 187601'u64), + ("00000000", 0'u64), "375198.0000"), + ((6003257'u64, 9'u64, "7bfa464e", 187596'u64, 187601'u64), + ("00000000", 0'u64), "375197.0000"), + ] + +proc init(t: typedesc[Eth2Digest], data: string): Eth2Digest = + let length = len(data) + var dst = Eth2Digest() + try: + hexToByteArray(data.toOpenArray(0, len(data) - 1), + dst.data.toOpenArray(0, (length div 2) - 1)) + except ValueError: + discard + dst + +proc init*(t: typedesc[ProduceAttestationDataResponse], + ad: AttestationDataTuple): ProduceAttestationDataResponse = + ProduceAttestationDataResponse(data: AttestationData( + slot: Slot(ad.slot), index: ad.index, + beacon_block_root: Eth2Digest.init(ad.beacon_block_root), + source: Checkpoint(epoch: Epoch(ad.source)), + target: Checkpoint(epoch: Epoch(ad.target)) + )) + +proc createRootsSeen( + root: tuple[root: string, slot: uint64]): Table[Eth2Digest, Slot] = + var res: Table[Eth2Digest, Slot] + res[Eth2Digest.init(root.root)] = Slot(root.slot) + res + suite "Validator Client test suite": test "normalizeUri() test vectors": for hostname in HostNames: for vector in GoodTestVectors: let expect = vector[1] % (hostname) check $normalizeUri(parseUri(vector[0] % (hostname))) == expect + + test "getAttestationDataScore() test vectors": + for vector in AttestationDataVectors: + let + adata = ProduceAttestationDataResponse.init(vector[0]) + roots = createRootsSeen(vector[1]) + score = shortScore(roots.getAttestationDataScore(adata)) + check score == vector[2]