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.
This commit is contained in:
parent
1e227b2704
commit
ac1b02698a
|
@ -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
|
||||
|
|
|
@ -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("<n/a>")
|
||||
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,
|
||||
|
|
|
@ -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:
|
||||
"<missing>"
|
||||
|
|
|
@ -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: "<perfect>" 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)
|
|
@ -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), "<perfect>"),
|
||||
((6002811'u64, 24'u64, "26ec78d6", 187586'u64, 187587'u64),
|
||||
("26ec78d6", 6002811'u64), "<perfect>"),
|
||||
((6002821'u64, 11'u64, "10c6d1a2", 187587'u64, 187588'u64),
|
||||
("10c6d1a2", 6002821'u64), "<perfect>"),
|
||||
((6002836'u64, 15'u64, "42354ded", 187587'u64, 187588'u64),
|
||||
("42354ded", 6002836'u64), "<perfect>"),
|
||||
((6002859'u64, 10'u64, "97d8ac69", 187588'u64, 187589'u64),
|
||||
("97d8ac69", 6002859'u64), "<perfect>"),
|
||||
# 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]
|
||||
|
|
Loading…
Reference in New Issue