nimbus-eth2/beacon_chain/rpc/rest_nimbus_api.nim

532 lines
18 KiB
Nim

# beacon_chain
# Copyright (c) 2018-2024 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
RestJson.useDefaultSerializationFor(
BlockProposalEth1Data,
Eth1BlockObj,
RestChronosMetricsInfo,
RestConnectionInfo,
RestFutureInfo,
RestPeerInfo,
RestPeerInfoTuple,
RestPeerStats,
RestPeerStatus,
RestPubSubPeer,
RestSimplePeer,
)
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.api2(MethodGet, "/nimbus/v1/beacon/head") do () -> RestApiResponse:
RestApiResponse.jsonResponse(node.dag.head.slot)
router.api2(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)
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.api2(MethodGet, "/nimbus/v1/syncmanager/status") do (
) -> RestApiResponse:
RestApiResponse.jsonResponse(node.syncManager.inProgress)
router.api2(MethodGet, "/nimbus/v1/node/peerid") do (
) -> RestApiResponse:
RestApiResponse.jsonResponse((peerid: $node.network.peerId()))
router.api2(MethodGet, "/nimbus/v1/node/version") do (
) -> RestApiResponse:
RestApiResponse.jsonResponse((version: "Nimbus/" & fullVersionStr))
router.api2(MethodGet, "/nimbus/v1/network/ids") do (
) -> RestApiResponse:
var res: seq[PeerId]
for peerId, peer in node.network.peerPool:
res.add(peerId)
RestApiResponse.jsonResponse((peerids: res))
router.api2(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
)
)
RestApiResponse.jsonResponse((peers: res))
router.api2(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()
RestApiResponse.jsonResponse((result: true))
elif body.contentType == TextPlainMediaType:
node.setGraffitiAux body.strData()
elif body.contentType == UrlEncodedMediaType:
node.setGraffitiAux decodeUrl(body.strData())
else:
RestApiResponse.jsonError(
Http400, "Unsupported content type: " & $body.contentType)
router.api2(MethodGet, "/nimbus/v1/graffiti") do () -> RestApiResponse:
RestApiResponse.jsonResponse(node.graffitiBytes)
router.api2(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.}:
try:
updateLogLevel(level)
except ValueError:
return RestApiResponse.jsonResponse((result: false))
RestApiResponse.jsonResponse((result: true))
router.api2(MethodGet, "/nimbus/v1/eth1/chain") do () -> RestApiResponse:
let res = mapIt(node.elManager.eth1ChainBlocks, it)
RestApiResponse.jsonResponse(res)
router.api2(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.api2(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
)
)
RestApiResponse.jsonResponse(res)
else:
RestApiResponse.jsonError(Http503,
"Compile with '-d:chronosFutureTracking' to get this request working")
router.api2(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)
)
RestApiResponse.jsonResponse(res)
router.api2(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
)
)
RestApiResponse.jsonResponse(res)
router.api2(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)
)
)
RestApiResponse.jsonResponse(response)
router.api2(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
RestApiResponse.jsonResponse(
(
gossip_peers: gossipPeers,
mesh_peers: meshPeers,
colocation_peers: colocationPeers,
peer_stats: peerStats,
all_peers: allPeers
)
)
router.api2(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)
)
RestApiResponse.jsonResponsePlain(response)