mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-11 14:54:12 +00:00
8771e91d53
* Support for driving multiple EL nodes from a single Nimbus BN Full list of changes: * Eth1Monitor has been renamed to ELManager to match its current responsibilities better. * The ELManager is no longer optional in the code (it won't have a nil value under any circumstances). * The support for subscribing for headers was removed as it only worked with WebSockets and contributed significant complexity while bringing only a very minor advantage. * The `--web3-url` parameter has been deprecated in favor of a new `--el` parameter. The new parameter has a reasonable default value and supports specifying a different JWT for each connection. Each connection can also be configured with a different set of responsibilities (e.g. download deposits, validate blocks and/or produce blocks). On the command-line, these properties can be configured through URL properties stored in the #anchor part of the URL. In TOML files, they come with a very natural syntax (althrough the URL scheme is also supported). * The previously scattered EL-related state and logic is now moved to `eth1_monitor.nim` (this module will be renamed to `el_manager.nim` in a follow-up commit). State is assigned properly either to the `ELManager` or the to individual `ELConnection` objects where appropriate. The ELManager executes all Engine API requests against all attached EL nodes, in parallel. It compares their results and if there is a disagreement regarding the validity of a certain payload, this is detected and the beacon node is protected from publishing a block with a potential execution layer consensus bug in it. The BN provides metrics per EL node for the number of successful or failed requests for each type Engine API requests. If an EL node goes offline and connectivity is resoted later, we report the problem and the remedy in edge-triggered fashion. * More progress towards implementing Deneb block production in the VC and comparing the value of blocks produced by the EL and the builder API. * Adds a Makefile target for the zhejiang testnet
432 lines
15 KiB
Nim
432 lines
15 KiB
Nim
# Copyright (c) 2018-2021 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,
|
|
chronicles,
|
|
libp2p/[multiaddress, multicodec, peerstore],
|
|
libp2p/protocols/pubsub/pubsubpeer,
|
|
./rest_utils,
|
|
../eth1/eth1_monitor,
|
|
../validators/validator_duties,
|
|
../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_connections*: uint64
|
|
http_body_readers*: uint64
|
|
http_body_writers*: uint64
|
|
|
|
RestPubSubPeer* = object
|
|
peerId*: PeerId
|
|
score*: float64
|
|
iWantBudget*: int
|
|
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,
|
|
iWantBudget: v.iWantBudget,
|
|
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 tres.optimistic:
|
|
return RestApiResponse.jsonError(Http503, BeaconNodeInSyncError)
|
|
tres.head
|
|
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(ttype: untyped, name: string): uint64 =
|
|
let res = ttype(getTracker(name))
|
|
if res.isNil(): 0'u64 else: uint64(res.opened - res.closed)
|
|
|
|
let res = RestChronosMetricsInfo(
|
|
tcp_transports: getCount(StreamTransportTracker, "stream.transport"),
|
|
udp_transports: getCount(DgramTransportTracker, "datagram.transport"),
|
|
tcp_servers: getCount(StreamServerTracker, "stream.server"),
|
|
stream_readers: getCount(AsyncStreamTracker,
|
|
AsyncStreamReaderTrackerName),
|
|
stream_writers: getCount(AsyncStreamTracker,
|
|
AsyncStreamWriterTrackerName),
|
|
http_client_connections: getCount(HttpClientTracker,
|
|
HttpClientConnectionTrackerName),
|
|
http_client_requests: getCount(HttpClientTracker,
|
|
HttpClientRequestTrackerName),
|
|
http_client_responses: getCount(HttpClientTracker,
|
|
HttpClientResponseTrackerName),
|
|
http_server_connections: lenu64(node.restServer.server.connections),
|
|
http_body_readers: getCount(HttpBodyTracker, HttpBodyReaderTrackerName),
|
|
http_body_writers: getCount(HttpBodyTracker, HttpBodyWriterTrackerName)
|
|
)
|
|
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
|
|
)
|
|
)
|