2021-03-17 18:46:45 +00:00
|
|
|
import
|
|
|
|
stew/results,
|
|
|
|
presto,
|
|
|
|
chronicles,
|
|
|
|
eth/p2p/discoveryv5/enr,
|
|
|
|
libp2p/[multiaddress, multicodec],
|
|
|
|
nimcrypto/utils as ncrutils,
|
2021-03-17 20:42:55 +00:00
|
|
|
../version, ../beacon_node_common, ../sync/sync_manager,
|
|
|
|
../networking/[eth2_network, peer_pool],
|
2021-03-17 18:46:45 +00:00
|
|
|
../spec/[datatypes, digest, presets],
|
|
|
|
../spec/eth2_apis/callsigs_types,
|
|
|
|
./rest_utils
|
|
|
|
|
|
|
|
logScope: topics = "rest_node"
|
|
|
|
|
|
|
|
type
|
|
|
|
ConnectionStateSet* = set[ConnectionState]
|
|
|
|
PeerTypeSet* = set[PeerType]
|
|
|
|
|
|
|
|
proc validateState(states: seq[PeerStateKind]): Result[ConnectionStateSet,
|
|
|
|
cstring] =
|
|
|
|
var res: set[ConnectionState]
|
|
|
|
for item in states:
|
|
|
|
case item
|
|
|
|
of PeerStateKind.Disconnected:
|
|
|
|
if ConnectionState.Disconnected in res:
|
|
|
|
return err("Peer connection states must be unique")
|
|
|
|
res.incl(ConnectionState.Disconnected)
|
|
|
|
of PeerStateKind.Connecting:
|
|
|
|
if ConnectionState.Connecting in res:
|
|
|
|
return err("Peer connection states must be unique")
|
|
|
|
res.incl(ConnectionState.Connecting)
|
|
|
|
of PeerStateKind.Connected:
|
|
|
|
if ConnectionState.Connected in res:
|
|
|
|
return err("Peer connection states must be unique")
|
|
|
|
res.incl(ConnectionState.Connected)
|
|
|
|
of PeerStateKind.Disconnecting:
|
|
|
|
if ConnectionState.Disconnecting in res:
|
|
|
|
return err("Peer connection states must be unique")
|
|
|
|
res.incl(ConnectionState.Disconnecting)
|
|
|
|
if res == {}:
|
|
|
|
res = {ConnectionState.Connecting, ConnectionState.Connected,
|
|
|
|
ConnectionState.Disconnecting, ConnectionState.Disconnected}
|
|
|
|
ok(res)
|
|
|
|
|
|
|
|
proc validateDirection(directions: seq[PeerDirectKind]): Result[PeerTypeSet,
|
|
|
|
cstring] =
|
|
|
|
var res: set[PeerType]
|
|
|
|
for item in directions:
|
|
|
|
case item
|
|
|
|
of PeerDirectKind.Inbound:
|
|
|
|
if PeerType.Incoming in res:
|
|
|
|
return err("Peer direction states must be unique")
|
|
|
|
res.incl(PeerType.Incoming)
|
|
|
|
of PeerDirectKind.Outbound:
|
|
|
|
if PeerType.Outgoing in res:
|
|
|
|
return err("Peer direction states must be unique")
|
|
|
|
res.incl(PeerType.Outgoing)
|
|
|
|
if res == {}:
|
|
|
|
res = {PeerType.Incoming, PeerType.Outgoing}
|
|
|
|
ok(res)
|
|
|
|
|
|
|
|
proc toString(state: ConnectionState): string =
|
|
|
|
case state
|
|
|
|
of ConnectionState.Disconnected:
|
|
|
|
"disconnected"
|
|
|
|
of ConnectionState.Connecting:
|
|
|
|
"connecting"
|
|
|
|
of ConnectionState.Connected:
|
|
|
|
"connected"
|
|
|
|
of ConnectionState.Disconnecting:
|
|
|
|
"disconnecting"
|
|
|
|
else:
|
|
|
|
""
|
|
|
|
|
|
|
|
proc toString(direction: PeerType): string =
|
|
|
|
case direction:
|
|
|
|
of PeerType.Incoming:
|
|
|
|
"inbound"
|
|
|
|
of PeerType.Outgoing:
|
|
|
|
"outbound"
|
|
|
|
|
|
|
|
proc getLastSeenAddress(info: PeerInfo): string =
|
|
|
|
# TODO (cheatfate): We need to provide filter here, which will be able to
|
|
|
|
# filter such multiaddresses like `/ip4/0.0.0.0` or local addresses or
|
|
|
|
# addresses with peer ids.
|
|
|
|
if len(info.addrs) > 0:
|
|
|
|
$info.addrs[len(info.addrs) - 1]
|
|
|
|
else:
|
|
|
|
""
|
|
|
|
|
|
|
|
proc getDiscoveryAddresses(node: BeaconNode): Option[seq[string]] =
|
|
|
|
let restr = node.network.enrRecord().toTypedRecord()
|
|
|
|
if restr.isErr():
|
|
|
|
return none[seq[string]]()
|
|
|
|
let respa = restr.get().toPeerAddr(udpProtocol)
|
|
|
|
if respa.isErr():
|
|
|
|
return none[seq[string]]()
|
|
|
|
let pa = respa.get()
|
|
|
|
let mpa = MultiAddress.init(multicodec("p2p"), pa.peerId)
|
|
|
|
if mpa.isErr():
|
|
|
|
return none[seq[string]]()
|
|
|
|
var addresses = newSeqOfCap[string](len(pa.addrs))
|
|
|
|
for item in pa.addrs:
|
|
|
|
let resa = concat(item, mpa.get())
|
|
|
|
if resa.isOk():
|
|
|
|
addresses.add($(resa.get()))
|
|
|
|
return some(addresses)
|
|
|
|
|
|
|
|
proc getP2PAddresses(node: BeaconNode): Option[seq[string]] =
|
|
|
|
let pinfo = node.network.switch.peerInfo
|
|
|
|
let mpa = MultiAddress.init(multicodec("p2p"), pinfo.peerId)
|
|
|
|
if mpa.isErr():
|
|
|
|
return none[seq[string]]()
|
|
|
|
var addresses = newSeqOfCap[string](len(pinfo.addrs))
|
|
|
|
for item in pinfo.addrs:
|
|
|
|
let resa = concat(item, mpa.get())
|
|
|
|
if resa.isOk():
|
|
|
|
addresses.add($(resa.get()))
|
|
|
|
return some(addresses)
|
|
|
|
|
|
|
|
proc installNodeApiHandlers*(router: var RestRouter, node: BeaconNode) =
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/identity") do () -> RestApiResponse:
|
|
|
|
let discoveryAddresses =
|
|
|
|
block:
|
|
|
|
let res = node.getDiscoveryAddresses()
|
|
|
|
if res.isSome():
|
|
|
|
res.get()
|
|
|
|
else:
|
|
|
|
newSeq[string](0)
|
|
|
|
|
|
|
|
let p2pAddresses =
|
|
|
|
block:
|
|
|
|
let res = node.getP2PAddresses()
|
|
|
|
if res.isSome():
|
|
|
|
res.get()
|
|
|
|
else:
|
|
|
|
newSeq[string]()
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponse(
|
|
|
|
%(
|
|
|
|
peer_id: $node.network.peerId(),
|
|
|
|
enr: node.network.enrRecord().toUri(),
|
|
|
|
p2p_addresses: p2pAddresses,
|
|
|
|
discovery_addresses: discoveryAddresses,
|
|
|
|
metadata: (node.network.metadata.seq_number,
|
|
|
|
"0x" & ncrutils.toHex(node.network.metadata.attnets.bytes))
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/peers") do (
|
|
|
|
states: seq[PeerStateKind],
|
|
|
|
directions: seq[PeerDirectKind]) -> RestApiResponse:
|
|
|
|
if states.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400,
|
|
|
|
"Invalid state value(s)",
|
|
|
|
$states.error())
|
|
|
|
if directions.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400,
|
|
|
|
"Invalid direction value(s)",
|
|
|
|
$directions.error())
|
|
|
|
let sres = validateState(states.get())
|
|
|
|
if sres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400,
|
|
|
|
"Invalid state value(s)",
|
|
|
|
$sres.error())
|
|
|
|
let dres = validateDirection(directions.get())
|
|
|
|
if dres.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400,
|
|
|
|
"Invalid direction value(s)",
|
|
|
|
$dres.error())
|
|
|
|
|
|
|
|
var res: seq[NodePeerTuple]
|
|
|
|
let connectionMask = sres.get()
|
|
|
|
let directionMask = dres.get()
|
|
|
|
for item in node.network.peers.values():
|
|
|
|
if (item.connectionState in connectionMask) and
|
|
|
|
(item.direction in directionMask):
|
|
|
|
let peer = (
|
|
|
|
peer_id: $item.info.peerId,
|
|
|
|
enr: if item.enr.isSome(): item.enr.get().toUri() else: "",
|
|
|
|
last_seen_p2p_address: item.info.getLastSeenAddress(),
|
|
|
|
state: item.connectionState.toString(),
|
|
|
|
direction: item.direction.toString(),
|
|
|
|
agent: item.info.agentVersion, # Fields `agent` and `proto` are not
|
|
|
|
proto: item.info.protoVersion # part of specification.
|
|
|
|
)
|
|
|
|
res.add(peer)
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponse(%res)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/peer_count") do () -> RestApiResponse:
|
|
|
|
var res: NodePeerCountTuple
|
|
|
|
for item in node.network.peers.values():
|
|
|
|
case item.connectionState
|
|
|
|
of Connecting:
|
|
|
|
inc(res.connecting)
|
|
|
|
of Connected:
|
|
|
|
inc(res.connected)
|
|
|
|
of Disconnecting:
|
|
|
|
inc(res.disconnecting)
|
|
|
|
of Disconnected:
|
|
|
|
inc(res.disconnected)
|
|
|
|
of ConnectionState.None:
|
|
|
|
discard
|
|
|
|
return RestApiResponse.jsonResponse(%res)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/peers/{peer_id}") do (
|
|
|
|
peer_id: PeerID) -> RestApiResponse:
|
|
|
|
if peer_id.isErr():
|
|
|
|
return RestApiResponse.jsonError(Http400, "PeerID could not be parsed",
|
|
|
|
$peer_id.error())
|
|
|
|
let peer = node.network.peers.getOrDefault(peer_id.get())
|
|
|
|
if isNil(peer):
|
|
|
|
return RestApiResponse.jsonError(Http404, "Peer not found")
|
|
|
|
|
|
|
|
return RestApiResponse.jsonResponse(
|
|
|
|
%(
|
|
|
|
peer_id: $peer.info.peerId,
|
|
|
|
enr: if peer.enr.isSome(): peer.enr.get().toUri() else: "",
|
|
|
|
last_seen_p2p_address: peer.info.getLastSeenAddress(),
|
|
|
|
state: peer.connectionState.toString(),
|
|
|
|
direction: peer.direction.toString(),
|
|
|
|
agent: peer.info.agentVersion, # Fields `agent` and `proto` are not
|
|
|
|
proto: peer.info.protoVersion # part of specification
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/version") do () -> RestApiResponse:
|
|
|
|
return RestApiResponse.jsonResponse(
|
|
|
|
%(version: "Nimbus/" & fullVersionStr)
|
|
|
|
)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/syncing") do () -> RestApiResponse:
|
|
|
|
return RestApiResponse.jsonResponse(
|
|
|
|
%node.syncManager.getInfo()
|
|
|
|
)
|
|
|
|
|
|
|
|
router.api(MethodGet, "/api/eth/v1/node/health") do () -> RestApiResponse:
|
|
|
|
# TODO: Add ability to detect node's issues and return 503 error according
|
|
|
|
# to specification.
|
|
|
|
let res =
|
|
|
|
if node.syncManager.inProgress:
|
|
|
|
(health: 206)
|
|
|
|
else:
|
|
|
|
(health: 200)
|
|
|
|
return RestApiResponse.jsonResponse(%res)
|