mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-10 22:36:01 +00:00
a5151bc546
This PR renames the existing `validator_duties` to `beacon_validators` and in doing so, names validators running inside the beacon node process "beacon validators" while those running the VC can be referred to as "client validators" to disambiguate the two. The existing `validator_duties` instead takes on a new responsibility: as a home for logic shared between beacon and client validators - ie code that provides consistency in implementation and behavior between the two modes of operation. Not only does this simplify reasoning about where to put code -it also reduces the number of dependencies the validator client has from ~5000 to ~3000 modules (!) according to `nim genDepend` significantly reducing compile times.
518 lines
18 KiB
Nim
518 lines
18 KiB
Nim
# beacon_chain
|
|
# Copyright (c) 2018-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/[sequtils],
|
|
stew/[results, base10],
|
|
chronicles,
|
|
chronos/apps/http/httpdebug,
|
|
libp2p/[multiaddress, multicodec, peerstore],
|
|
libp2p/protocols/pubsub/pubsubpeer,
|
|
./rest_utils,
|
|
../el/el_manager,
|
|
../validators/beacon_validators,
|
|
../spec/[forks, beacon_time],
|
|
../beacon_node, ../nimbus_binary_common
|
|
|
|
export rest_utils
|
|
|
|
when defined(chronosFutureTracking):
|
|
import stew/base10
|
|
|
|
logScope: topics = "rest_nimbusapi"
|
|
|
|
type
|
|
RestPeerInfo* = object
|
|
peerId*: string
|
|
addrs*: seq[string]
|
|
protocols*: seq[string]
|
|
protoVersion*: string
|
|
agentVersion*: string
|
|
|
|
RestPeerInfoTuple* = tuple
|
|
peerId: string
|
|
addrs: seq[string]
|
|
protocols: seq[string]
|
|
protoVersion: string
|
|
agentVersion: string
|
|
|
|
RestSimplePeer* = object
|
|
info*: RestPeerInfo
|
|
connectionState*: string
|
|
score*: int
|
|
|
|
RestFutureInfo* = object
|
|
id*: string
|
|
child_id*: string
|
|
procname*: string
|
|
filename*: string
|
|
line*: int
|
|
state*: string
|
|
|
|
RestChronosMetricsInfo* = object
|
|
tcp_transports*: uint64
|
|
udp_transports*: uint64
|
|
tcp_servers*: uint64
|
|
stream_readers*: uint64
|
|
stream_writers*: uint64
|
|
http_client_connections*: uint64
|
|
http_client_requests*: uint64
|
|
http_client_responses*: uint64
|
|
http_server_secure_connections*: uint64
|
|
http_server_unsecure_connections*: uint64
|
|
http_server_requests*: uint64
|
|
http_server_responses*: uint64
|
|
http_body_readers*: uint64
|
|
http_body_writers*: uint64
|
|
|
|
RestConnectionInfo* = object
|
|
handle*: string
|
|
query*: string
|
|
connection_type*: string
|
|
connection_state*: string
|
|
remote_address*: string
|
|
local_address*: string
|
|
since_accept*: string
|
|
since_create*: string
|
|
|
|
RestPubSubPeer* = object
|
|
peerId*: PeerId
|
|
score*: float64
|
|
iHaveBudget*: int
|
|
outbound*: bool
|
|
appScore*: float64
|
|
behaviourPenalty*: float64
|
|
sendConnAvail*: bool
|
|
closed*: bool
|
|
atEof*: bool
|
|
address*: string
|
|
backoff*: string
|
|
agent*: string
|
|
|
|
RestPeerStats* = object
|
|
peerId*: PeerId
|
|
null*: bool
|
|
connected*: bool
|
|
expire*: string
|
|
score*: float64
|
|
|
|
RestPeerStatus* = object
|
|
peerId*: PeerId
|
|
connected*: bool
|
|
|
|
proc toInfo(node: BeaconNode, peerId: PeerId): RestPeerInfo =
|
|
RestPeerInfo(
|
|
peerId: $peerId,
|
|
addrs: node.network.switch.peerStore[AddressBook][peerId].mapIt($it),
|
|
protocols: node.network.switch.peerStore[ProtoBook][peerId],
|
|
protoVersion: node.network.switch.peerStore[ProtoVersionBook][peerId],
|
|
agentVersion: node.network.switch.peerStore[AgentBook][peerId]
|
|
)
|
|
|
|
proc toNode(v: PubSubPeer, backoff: Moment): RestPubSubPeer =
|
|
RestPubSubPeer(
|
|
peerId: v.peerId,
|
|
score: v.score,
|
|
iHaveBudget: v.iHaveBudget,
|
|
outbound: v.outbound,
|
|
appScore: v.appScore,
|
|
behaviourPenalty: v.behaviourPenalty,
|
|
sendConnAvail: v.sendConn != nil,
|
|
closed: v.sendConn != nil and v.sendConn.closed,
|
|
atEof: v.sendConn != nil and v.sendConn.atEof,
|
|
address:
|
|
if v.address.isSome():
|
|
$v.address.get()
|
|
else:
|
|
"<no address>",
|
|
backoff: $(backoff - Moment.now()),
|
|
agent:
|
|
when defined(libp2p_agents_metrics):
|
|
v.shortAgent
|
|
else:
|
|
"unknown"
|
|
)
|
|
|
|
proc installNimbusApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|
router.api(MethodGet, "/nimbus/v1/beacon/head") do () -> RestApiResponse:
|
|
return RestApiResponse.jsonResponse(node.dag.head.slot)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/chain/head") do() -> RestApiResponse:
|
|
let
|
|
head = node.dag.head
|
|
finalized = getStateField(node.dag.headState, finalized_checkpoint)
|
|
justified =
|
|
getStateField(node.dag.headState, current_justified_checkpoint)
|
|
return RestApiResponse.jsonResponse(
|
|
(
|
|
head_slot: head.slot,
|
|
head_block_root: head.root.data.toHex(),
|
|
finalized_slot: finalized.epoch * SLOTS_PER_EPOCH,
|
|
finalized_block_root: finalized.root.data.toHex(),
|
|
justified_slot: justified.epoch * SLOTS_PER_EPOCH,
|
|
justified_block_root: justified.root.data.toHex()
|
|
)
|
|
)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/syncmanager/status") do (
|
|
) -> RestApiResponse:
|
|
return RestApiResponse.jsonResponse(node.syncManager.inProgress)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/node/peerid") do (
|
|
) -> RestApiResponse:
|
|
return RestApiResponse.jsonResponse((peerid: $node.network.peerId()))
|
|
|
|
router.api(MethodGet, "/nimbus/v1/node/version") do (
|
|
) -> RestApiResponse:
|
|
return RestApiResponse.jsonResponse((version: "Nimbus/" & fullVersionStr))
|
|
|
|
router.api(MethodGet, "/nimbus/v1/network/ids") do (
|
|
) -> RestApiResponse:
|
|
var res: seq[PeerId]
|
|
for peerId, peer in node.network.peerPool:
|
|
res.add(peerId)
|
|
return RestApiResponse.jsonResponse((peerids: res))
|
|
|
|
router.api(MethodGet, "/nimbus/v1/network/peers") do (
|
|
) -> RestApiResponse:
|
|
var res: seq[RestSimplePeer]
|
|
for id, peer in node.network.peerPool:
|
|
res.add(
|
|
RestSimplePeer(
|
|
info: toInfo(node, id),
|
|
connectionState: $peer.connectionState,
|
|
score: peer.score
|
|
)
|
|
)
|
|
return RestApiResponse.jsonResponse((peers: res))
|
|
|
|
router.api(MethodPost, "/nimbus/v1/graffiti") do (
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
if contentBody.isNone:
|
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
|
|
|
template setGraffitiAux(node: BeaconNode,
|
|
graffitiStr: string): RestApiResponse =
|
|
node.graffitiBytes = try:
|
|
GraffitiBytes.init(graffitiStr)
|
|
except CatchableError as err:
|
|
return RestApiResponse.jsonError(Http400, InvalidGraffitiBytesValue,
|
|
err.msg)
|
|
RestApiResponse.jsonResponse((result: true))
|
|
|
|
let body = contentBody.get()
|
|
if body.contentType == ApplicationJsonMediaType:
|
|
let graffitiBytes = decodeBody(GraffitiBytes, body)
|
|
if graffitiBytes.isErr():
|
|
return RestApiResponse.jsonError(Http400, InvalidGraffitiBytesValue,
|
|
$graffitiBytes.error())
|
|
node.graffitiBytes = graffitiBytes.get()
|
|
return RestApiResponse.jsonResponse((result: true))
|
|
elif body.contentType == TextPlainMediaType:
|
|
return node.setGraffitiAux body.strData()
|
|
elif body.contentType == UrlEncodedMediaType:
|
|
return node.setGraffitiAux decodeUrl(body.strData())
|
|
else:
|
|
return RestApiResponse.jsonError(Http400, "Unsupported content type: " &
|
|
$body.contentType)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/graffiti") do (
|
|
) -> RestApiResponse:
|
|
return RestApiResponse.jsonResponse(node.graffitiBytes)
|
|
|
|
router.api(MethodPost, "/nimbus/v1/chronicles/settings") do (
|
|
log_level: Option[string]) -> RestApiResponse:
|
|
if log_level.isSome():
|
|
let level =
|
|
block:
|
|
let res = log_level.get()
|
|
if res.isErr():
|
|
return RestApiResponse.jsonError(Http400, InvalidLogLevelValueError,
|
|
$res.error())
|
|
res.get()
|
|
{.gcsafe.}:
|
|
updateLogLevel(level)
|
|
return RestApiResponse.jsonResponse((result: true))
|
|
|
|
router.api(MethodGet, "/nimbus/v1/eth1/chain") do (
|
|
) -> RestApiResponse:
|
|
let res = mapIt(node.elManager.eth1ChainBlocks, it)
|
|
return RestApiResponse.jsonResponse(res)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/eth1/proposal_data") do (
|
|
) -> RestApiResponse:
|
|
let wallSlot = node.beaconClock.now.slotOrZero
|
|
let head =
|
|
block:
|
|
let res = node.getSyncedHead(wallSlot)
|
|
if res.isErr():
|
|
return RestApiResponse.jsonError(Http503, BeaconNodeInSyncError,
|
|
$res.error())
|
|
let tres = res.get()
|
|
if not tres.executionValid:
|
|
return RestApiResponse.jsonError(Http503, BeaconNodeInSyncError)
|
|
tres
|
|
let proposalState = assignClone(node.dag.headState)
|
|
node.dag.withUpdatedState(
|
|
proposalState[],
|
|
head.atSlot(wallSlot).toBlockSlotId().expect("not nil")):
|
|
return RestApiResponse.jsonResponse(
|
|
node.getBlockProposalEth1Data(updatedState))
|
|
do:
|
|
return RestApiResponse.jsonError(Http400, PrunedStateError)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/debug/chronos/futures") do (
|
|
) -> RestApiResponse:
|
|
when defined(chronosFutureTracking):
|
|
var res: seq[RestFutureInfo]
|
|
for item in pendingFutures():
|
|
let loc = item.location[LocCreateIndex][]
|
|
let futureId = Base10.toString(item.id)
|
|
let childId =
|
|
if isNil(item.child): ""
|
|
else: Base10.toString(item.child.id)
|
|
res.add(
|
|
RestFutureInfo(
|
|
id: futureId,
|
|
child_id: childId,
|
|
procname: $loc.procedure,
|
|
filename: $loc.file,
|
|
line: loc.line,
|
|
state: $item.state
|
|
)
|
|
)
|
|
return RestApiResponse.jsonResponse(res)
|
|
else:
|
|
return RestApiResponse.jsonError(Http503,
|
|
"Compile with '-d:chronosFutureTracking' to get this request working")
|
|
|
|
router.api(MethodGet, "/nimbus/v1/debug/chronos/metrics") do (
|
|
) -> RestApiResponse:
|
|
|
|
template getCount(name: string): uint64 =
|
|
let res = getTrackerCounter(name)
|
|
uint64(res.opened - res.closed)
|
|
|
|
let res = RestChronosMetricsInfo(
|
|
tcp_transports: getCount(StreamTransportTrackerName),
|
|
udp_transports: getCount(DgramTransportTrackerName),
|
|
tcp_servers: getCount(StreamServerTrackerName),
|
|
stream_readers: getCount(AsyncStreamReaderTrackerName),
|
|
stream_writers: getCount(AsyncStreamWriterTrackerName),
|
|
http_client_connections: getCount(HttpClientConnectionTrackerName),
|
|
http_client_requests: getCount(HttpClientRequestTrackerName),
|
|
http_client_responses: getCount(HttpClientResponseTrackerName),
|
|
http_server_secure_connections:
|
|
getCount(HttpServerSecureConnectionTrackerName),
|
|
http_server_unsecure_connections:
|
|
getCount(HttpServerUnsecureConnectionTrackerName),
|
|
http_server_requests: getCount(HttpServerRequestTrackerName),
|
|
http_server_responses: getCount(HttpServerResponseTrackerName),
|
|
http_body_readers: getCount(HttpBodyReaderTrackerName),
|
|
http_body_writers: getCount(HttpBodyWriterTrackerName)
|
|
)
|
|
return RestApiResponse.jsonResponse(res)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/debug/chronos/restserver/connections") do (
|
|
) -> RestApiResponse:
|
|
var res: seq[RestConnectionInfo]
|
|
for connection in node.restServer.server.getConnections():
|
|
let
|
|
connectionState =
|
|
case connection.connectionState
|
|
of httpdebug.ConnectionState.Accepted: "accepted"
|
|
of httpdebug.ConnectionState.Alive: "alive"
|
|
of httpdebug.ConnectionState.Closing: "closing"
|
|
of httpdebug.ConnectionState.Closed: "closed"
|
|
connectionType =
|
|
case connection.connectionType
|
|
of ConnectionType.Secure: "secure"
|
|
of ConnectionType.NonSecure: "non-secure"
|
|
localAddress =
|
|
if connection.localAddress.isNone():
|
|
"not available"
|
|
else:
|
|
$connection.localAddress.get()
|
|
remoteAddress =
|
|
if connection.remoteAddress.isNone():
|
|
"not available"
|
|
else:
|
|
$connection.remoteAddress.get()
|
|
query = connection.query.get("not available")
|
|
handle = Base10.toString(uint64(connection.handle))
|
|
moment = Moment.now()
|
|
sinceAccept = $(moment - connection.acceptMoment)
|
|
sinceCreate =
|
|
if connection.createMoment.isSome():
|
|
$(moment - connection.createMoment.get())
|
|
else:
|
|
"not available"
|
|
|
|
res.add(
|
|
RestConnectionInfo(
|
|
handle: handle,
|
|
query: query,
|
|
connection_state: connectionState,
|
|
connection_type: connectionType,
|
|
local_address: localAddress,
|
|
remote_address: remoteAddress,
|
|
since_accept: sinceAccept,
|
|
since_create: sinceCreate
|
|
)
|
|
)
|
|
return RestApiResponse.jsonResponse(res)
|
|
|
|
router.api(MethodPost, "/nimbus/v1/validator/activity/{epoch}") do (
|
|
epoch: Epoch, contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
let indexList =
|
|
block:
|
|
if contentBody.isNone():
|
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
|
let dres = decodeBody(seq[RestValidatorIndex], contentBody.get())
|
|
if dres.isErr():
|
|
return RestApiResponse.jsonError(Http400,
|
|
InvalidValidatorIndexValueError,
|
|
$dres.error())
|
|
var
|
|
res: seq[ValidatorIndex]
|
|
dupset: HashSet[ValidatorIndex]
|
|
|
|
let items = dres.get()
|
|
for item in items:
|
|
let vres = item.toValidatorIndex()
|
|
if vres.isErr():
|
|
case vres.error()
|
|
of ValidatorIndexError.TooHighValue:
|
|
return RestApiResponse.jsonError(Http400,
|
|
TooHighValidatorIndexValueError)
|
|
of ValidatorIndexError.UnsupportedValue:
|
|
return RestApiResponse.jsonError(Http500,
|
|
UnsupportedValidatorIndexValueError)
|
|
let index = vres.get()
|
|
if index in dupset:
|
|
return RestApiResponse.jsonError(Http400,
|
|
DuplicateValidatorIndexArrayError)
|
|
dupset.incl(index)
|
|
res.add(index)
|
|
if len(res) == 0:
|
|
return RestApiResponse.jsonError(Http400,
|
|
EmptyValidatorIndexArrayError)
|
|
res
|
|
let qepoch =
|
|
block:
|
|
if epoch.isErr():
|
|
return RestApiResponse.jsonError(Http400, InvalidEpochValueError,
|
|
$epoch.error())
|
|
let
|
|
res = epoch.get()
|
|
wallEpoch = node.currentSlot().epoch()
|
|
nextEpoch =
|
|
if wallEpoch == FAR_FUTURE_EPOCH:
|
|
wallEpoch
|
|
else:
|
|
wallEpoch + 1
|
|
prevEpoch = get_previous_epoch(wallEpoch)
|
|
if (res < prevEpoch) or (res > nextEpoch):
|
|
return RestApiResponse.jsonError(Http400, InvalidEpochValueError,
|
|
"Requested epoch is more than one epoch from current epoch")
|
|
res
|
|
let response = indexList.mapIt(
|
|
RestActivityItem(
|
|
index: it,
|
|
epoch: qepoch,
|
|
active: node.attestationPool[].validatorSeenAtEpoch(qepoch, it)
|
|
)
|
|
)
|
|
return RestApiResponse.jsonResponse(response)
|
|
|
|
router.api(MethodGet, "/nimbus/v1/debug/gossip/peers") do (
|
|
) -> RestApiResponse:
|
|
|
|
let gossipPeers =
|
|
block:
|
|
var res: seq[tuple[topic: string, peers: seq[RestPubSubPeer]]]
|
|
for topic, v in node.network.pubsub.gossipsub:
|
|
var peers: seq[RestPubSubPeer]
|
|
let backoff = node.network.pubsub.backingOff.getOrDefault(topic)
|
|
for peer in v:
|
|
peers.add(peer.toNode(backoff.getOrDefault(peer.peerId)))
|
|
res.add((topic: topic, peers: peers))
|
|
res
|
|
let meshPeers =
|
|
block:
|
|
var res: seq[tuple[topic: string, peers: seq[RestPubSubPeer]]]
|
|
for topic, v in node.network.pubsub.mesh:
|
|
var peers: seq[RestPubSubPeer]
|
|
let backoff = node.network.pubsub.backingOff.getOrDefault(topic)
|
|
for peer in v:
|
|
peers.add(peer.toNode(backoff.getOrDefault(peer.peerId)))
|
|
res.add((topic: topic, peers: peers))
|
|
res
|
|
let colocationPeers =
|
|
block:
|
|
var res: seq[tuple[address: string, peerids: seq[PeerId]]]
|
|
for k, v in node.network.pubsub.peersInIP:
|
|
var peerids: seq[PeerId]
|
|
for id in v:
|
|
peerids.add(id)
|
|
res.add(($k, peerids))
|
|
res
|
|
let peerStats =
|
|
block:
|
|
var stats: seq[RestPeerStats]
|
|
for peerId, pstats in node.network.pubsub.peerStats:
|
|
let peer = node.network.pubsub.peers.getOrDefault(peerId)
|
|
stats.add(
|
|
RestPeerStats(
|
|
peerId: peerId,
|
|
null: isNil(peer),
|
|
connected: if isNil(peer): false else: peer.connected(),
|
|
expire: $(pstats.expire - Moment.now()),
|
|
score: pstats.score
|
|
)
|
|
)
|
|
stats
|
|
let allPeers =
|
|
block:
|
|
var peers: seq[RestPeerStatus]
|
|
for peerId, peer in node.network.pubsub.peers:
|
|
peers.add(RestPeerStatus(peerId: peerId, connected: peer.connected))
|
|
peers
|
|
return RestApiResponse.jsonResponse(
|
|
(
|
|
gossip_peers: gossipPeers,
|
|
mesh_peers: meshPeers,
|
|
colocation_peers: colocationPeers,
|
|
peer_stats: peerStats,
|
|
all_peers: allPeers
|
|
)
|
|
)
|
|
|
|
router.api(MethodPost, "/nimbus/v1/timesync") do (
|
|
contentBody: Option[ContentBody]) -> RestApiResponse:
|
|
let
|
|
timestamp2 = getTimestamp()
|
|
timestamp1 =
|
|
block:
|
|
if contentBody.isNone():
|
|
return RestApiResponse.jsonError(Http400, EmptyRequestBodyError)
|
|
let dres = decodeBody(RestNimbusTimestamp1, contentBody.get())
|
|
if dres.isErr():
|
|
return RestApiResponse.jsonError(Http400,
|
|
InvalidTimestampValue,
|
|
$dres.error())
|
|
dres.get().timestamp1
|
|
let
|
|
delay = node.processingDelay.valueOr: ZeroDuration
|
|
response = RestNimbusTimestamp2(
|
|
timestamp1: timestamp1,
|
|
timestamp2: timestamp2,
|
|
timestamp3: getTimestamp(),
|
|
delay: uint64(delay.nanoseconds)
|
|
)
|
|
return RestApiResponse.jsonResponsePlain(response)
|