Skip to content

Commit

Permalink
Initial implementation of a JSON-RPC service
Browse files Browse the repository at this point in the history
  • Loading branch information
zah committed Mar 22, 2020
1 parent 53e4b0a commit adcec61
Show file tree
Hide file tree
Showing 10 changed files with 242 additions and 45 deletions.
142 changes: 124 additions & 18 deletions beacon_chain/beacon_node.nim
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import
# Standard library
os, net, tables, random, strutils, times, sequtils,
os, tables, random, strutils, times, sequtils,

# Nimble packages
stew/[objects, bitseqs, byteutils],
chronos, chronicles, confutils, metrics,
json_serialization/std/[options, sets], serialization/errors,
chronos, chronicles, confutils, metrics, json_rpc/[rpcserver, jsonmarshal],
json_serialization/std/[options, sets, net], serialization/errors,
kvstore, kvstore_sqlite3,
eth/p2p/enode, eth/[keys, async_utils], eth/p2p/discoveryv5/enr,
eth/p2p/enode, eth/[keys, async_utils], eth/p2p/discoveryv5/[protocol, enr],

# Local modules
spec/[datatypes, digest, crypto, beaconstate, helpers, validator, network,
Expand All @@ -24,6 +24,10 @@ const

type
KeyPair = eth2_network.KeyPair
RpcServer = RpcHttpServer

template init(T: type RpcHttpServer, ip: IpAddress, port: Port): T =
newRpcHttpServer([initTAddress(ip, port)])

# https://github.com/ethereum/eth2.0-metrics/blob/master/metrics.md#interop-metrics
declareGauge beacon_slot,
Expand Down Expand Up @@ -61,6 +65,7 @@ type
attestationPool: AttestationPool
mainchainMonitor: MainchainMonitor
beaconClock: BeaconClock
rpcServer: RpcServer

proc onBeaconBlock*(node: BeaconNode, signedBlock: SignedBeaconBlock) {.gcsafe.}
proc updateHead(node: BeaconNode): BlockRef
Expand Down Expand Up @@ -203,6 +208,11 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
addressFile = string(conf.dataDir) / "beacon_node.address"
network.saveConnectionAddressFile(addressFile)

let rpcServer = if conf.rpcEnabled:
RpcServer.init(conf.rpcAddress, conf.rpcPort)
else:
nil

var res = BeaconNode(
nickname: nickname,
network: network,
Expand All @@ -218,6 +228,7 @@ proc init*(T: type BeaconNode, conf: BeaconNodeConf): Future[BeaconNode] {.async
attestationPool: AttestationPool.init(blockPool),
mainchainMonitor: mainchainMonitor,
beaconClock: BeaconClock.init(blockPool.headState.data.data),
rpcServer: rpcServer,
)

# TODO sync is called when a remote peer is connected - is that the right
Expand Down Expand Up @@ -845,7 +856,107 @@ proc onSecond(node: BeaconNode, moment: Moment) {.async.} =
addTimer(nextSecond) do (p: pointer):
asyncCheck node.onSecond(nextSecond)

# TODO: Should we move these to other modules?
# This would require moving around other type definitions
proc installValidatorApiHandlers(rpcServer: RpcServer, node: BeaconNode) =
discard

func slotOrZero(time: BeaconTime): Slot =
let exSlot = time.toSlot
if exSlot.afterGenesis: exSlot.slot
else: Slot(0)

func currentSlot(node: BeaconNode): Slot =
node.beaconClock.now.slotOrZero

proc connectedPeersCount(node: BeaconNode): int =
libp2p_peers.value.int

proc fromJson(n: JsonNode; argName: string; result: var Slot) =
var i: int
fromJson(n, argName, i)
result = Slot(i)

proc installBeaconApiHandlers(rpcServer: RpcServer, node: BeaconNode) =
rpcServer.rpc("getBeaconHead") do () -> Slot:
return node.currentSlot

template requireOneOf(x, y: distinct Option) =
if x.isNone xor y.isNone:
raise newException(CatchableError,
"Please specify one of " & astToStr(x) & " or " & astToStr(y))

template jsonResult(x: auto): auto =
# TODO, yes this is silly, but teching json-rpc about
# all beacon node types will require quite a lot of work.
# A minor refactoring in json-rpc can solve this. We need
# to allow the handlers to return raw/literal json strings.
parseJson(Json.encode(x))

rpcServer.rpc("getBeaconBlock") do (slot: Option[Slot],
root: Option[Eth2Digest]) -> JsonNode:
requireOneOf(slot, root)
var blockHash: Eth2Digest
if root.isSome:
blockHash = root.get
else:
let foundRef = node.blockPool.getBlockByPreciseSlot(slot.get)
if foundRef.isSome:
blockHash = foundRef.get.root
else:
return newJNull()

let dbBlock = node.db.getBlock(blockHash)
if dbBlock.isSome:
return jsonResult(dbBlock.get)
else:
return newJNull()

rpcServer.rpc("getBeaconState") do (slot: Option[Slot],
root: Option[Eth2Digest]) -> JsonNode:
requireOneOf(slot, root)
if slot.isSome:
let blk = node.blockPool.head.blck.atSlot(slot.get)
var tmpState: StateData
node.blockPool.withState(tmpState, blk):
return jsonResult(state)
else:
let state = node.db.getState(root.get)
if state.isSome:
return jsonResult(state.get)
else:
return newJNull()

rpcServer.rpc("getNetworkPeerId") do () -> string:
when networkBackend != libp2p:
raise newException(CatchableError, "Unsupported operation")
else:
return $publicKey(node.network)

rpcServer.rpc("getNetworkPeers") do () -> seq[string]:
when networkBackend != libp2p:
if true:
raise newException(CatchableError, "Unsupported operation")

for peerId, peer in node.network.peerPool:
result.add $peerId

rpcServer.rpc("getNetworkEnr") do () -> string:
return $node.network.discovery.localNode.record

proc installDebugApiHandlers(rpcServer: RpcServer, node: BeaconNode) =
discard

proc installRpcHandlers(rpcServer: RpcServer, node: BeaconNode) =
rpcServer.installValidatorApiHandlers(node)
rpcServer.installBeaconApiHandlers(node)
rpcServer.installDebugApiHandlers(node)

proc run*(node: BeaconNode) =
if node.rpcServer != nil:
node.rpcServer.installRpcHandlers(node)
node.rpcServer.start()

waitFor node.network.subscribe(topicBeaconBlocks) do (signedBlock: SignedBeaconBlock):
onBeaconBlock(node, signedBlock)

Expand Down Expand Up @@ -955,11 +1066,6 @@ when hasPrompt:
else:
p[].writeLine("Unknown command: " & cmd)

proc slotOrZero(time: BeaconTime): Slot =
let exSlot = time.toSlot
if exSlot.afterGenesis: exSlot.slot
else: Slot(0)

proc initPrompt(node: BeaconNode) =
if isatty(stdout) and node.config.statusBarEnabled:
enableTrueColors()
Expand All @@ -982,7 +1088,7 @@ when hasPrompt:
# arbitrary expression that is resolvable through this API.
case expr.toLowerAscii
of "connected_peers":
$(libp2p_peers.value.int)
$(node.connectedPeersCount)

of "last_finalized_epoch":
var head = node.blockPool.finalizedHead
Expand All @@ -999,7 +1105,7 @@ when hasPrompt:
$SLOTS_PER_EPOCH

of "slot":
$node.beaconClock.now.slotOrZero
$node.currentSlot

of "slot_trailing_digits":
var slotStr = $node.beaconClock.now.slotOrZero
Expand Down Expand Up @@ -1115,9 +1221,9 @@ when isMainModule:
let
networkKeys = getPersistentNetKeys(config)
bootstrapAddress = enode.Address(
ip: parseIpAddress(config.bootstrapAddress),
tcpPort: Port config.bootstrapPort,
udpPort: Port config.bootstrapPort)
ip: config.bootstrapAddress,
tcpPort: config.bootstrapPort,
udpPort: config.bootstrapPort)

bootstrapEnr = enr.Record.init(
1, # sequence number
Expand Down Expand Up @@ -1151,11 +1257,11 @@ when isMainModule:
initPrompt(node)

when useInsecureFeatures:
if config.metricsServer:
let metricsAddress = config.metricsServerAddress
if config.metricsEnabled:
let metricsAddress = config.metricsAddress
info "Starting metrics HTTP server",
address = metricsAddress, port = config.metricsServerPort
metrics.startHttpServer(metricsAddress, Port(config.metricsServerPort))
address = metricsAddress, port = config.metricsPort
metrics.startHttpServer($metricsAddress, config.metricsPort)

if node.nickname != "":
dynamicLogScope(node = node.nickname): node.start()
Expand Down
11 changes: 11 additions & 0 deletions beacon_chain/block_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,17 @@ proc getBlockRange*(pool: BlockPool, headBlock: Eth2Digest,
trace "getBlockRange result", position = result, blockSlot = b.slot
skip skipStep

func getBlockBySlot*(pool: BlockPool, slot: Slot): BlockRef =
## Retrieves the first block in the current canonical chain
## with slot number less or equal to `slot`.
pool.head.blck.findAncestorBySlot(slot).blck

func getBlockByPreciseSlot*(pool: BlockPool, slot: Slot): Option[BlockRef] =
## Retrieves a block from the canonical chain with a slot
## number equal to `slot`.
let found = pool.getBlockBySlot(slot)
if found.slot != slot: some(found) else: none(BlockRef)

proc get*(pool: BlockPool, blck: BlockRef): BlockData =
## Retrieve the associated block body of a block reference
doAssert (not blck.isNil), "Trying to get nil BlockRef"
Expand Down
61 changes: 38 additions & 23 deletions beacon_chain/conf.nim
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import
os, options, strformat, strutils,
chronicles, confutils, json_serialization,
confutils/defs, chronicles/options as chroniclesOptions,
confutils/defs, confutils/std/net,
chronicles/options as chroniclesOptions,
spec/[crypto]

export
defs, enabledLogLevel

const
DEFAULT_NETWORK* {.strdefine.} = "testnet0"
defs, enabledLogLevel, parseCmdArg, completeCmdArg

type
ValidatorKeyPath* = TypedInputFile[ValidatorPrivKey, Txt, "privkey"]
Expand Down Expand Up @@ -75,6 +73,21 @@ type
desc: "Textual template for the contents of the status bar."
name: "status-bar-contents" }: string

rpcEnabled* {.
defaultValue: false
desc: "Enable the JSON-RPC server"
name: "rpc" }: bool

rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port for the JSON-RPC service."
name: "rpc-port" }: Port

rpcAddress* {.
defaultValue: defaultListenAddress(config)
desc: "Listening address of the RPC server"
name: "rpc-address" }: IpAddress

case cmd* {.
command
defaultValue: noCommand }: StartUpCmd
Expand All @@ -91,14 +104,14 @@ type
name: "bootstrap-file" }: InputFile

tcpPort* {.
defaultValue: defaultPort(config)
defaultValue: defaultEth2TcpPort
desc: "TCP listening port."
name: "tcp-port" }: int
name: "tcp-port" }: Port

udpPort* {.
defaultValue: defaultPort(config)
defaultValue: defaultEth2TcpPort
desc: "UDP listening port."
name: "udp-port" }: int
name: "udp-port" }: Port

maxPeers* {.
defaultValue: 10
Expand Down Expand Up @@ -137,20 +150,20 @@ type
desc: "A positive epoch selects the epoch at which to stop."
name: "stop-at-epoch" }: uint64

metricsServer* {.
metricsEnabled* {.
defaultValue: false
desc: "Enable the metrics server."
name: "metrics-server" }: bool
name: "metrics" }: bool

metricsServerAddress* {.
defaultValue: "0.0.0.0"
metricsAddress* {.
defaultValue: defaultListenAddress(config)
desc: "Listening address of the metrics server."
name: "metrics-server-address" }: string # TODO: use a validated type here
name: "metrics-address" }: IpAddress

metricsServerPort* {.
metricsPort* {.
defaultValue: 8008
desc: "Listening HTTP port of the metrics server."
name: "metrics-server-port" }: uint16
name: "metrics-port" }: Port

dump* {.
defaultValue: false
Expand Down Expand Up @@ -178,14 +191,14 @@ type
name: "last-user-validator" }: uint64

bootstrapAddress* {.
defaultValue: "127.0.0.1"
defaultValue: parseIpAddress("127.0.0.1")
desc: "The public IP address that will be advertised as a bootstrap node for the testnet."
name: "bootstrap-address" }: string
name: "bootstrap-address" }: IpAddress

bootstrapPort* {.
defaultValue: defaultPort(config)
defaultValue: defaultEth2TcpPort
desc: "The TCP/UDP port that will be used by the bootstrap node."
name: "bootstrap-port" }: int
name: "bootstrap-port" }: Port

genesisOffset* {.
defaultValue: 5
Expand Down Expand Up @@ -248,9 +261,6 @@ type
argument
desc: "REST API path to evaluate" }: string

proc defaultPort*(config: BeaconNodeConf): int =
9000

proc defaultDataDir*(conf: BeaconNodeConf): string =
let dataDir = when defined(windows):
"AppData" / "Roaming" / "Nimbus"
Expand All @@ -274,6 +284,11 @@ func localValidatorsDir*(conf: BeaconNodeConf): string =
func databaseDir*(conf: BeaconNodeConf): string =
conf.dataDir / "db"

func defaultListenAddress*(conf: BeaconNodeConf): IpAddress =
# TODO: How should we select between IPv4 and IPv6
# Maybe there should be a config option for this.
parseIpAddress("0.0.0.0")

iterator validatorKeys*(conf: BeaconNodeConf): ValidatorPrivKey =
for validatorKeyFile in conf.validators:
try:
Expand Down
3 changes: 3 additions & 0 deletions beacon_chain/libp2p_backend.nim
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ proc init*(T: type Eth2Node, conf: BeaconNodeConf,
if msg.protocolMounter != nil:
msg.protocolMounter result

template publicKey*(node: Eth2Node): keys.PublicKey =
node.discovery.privKey.getPublicKey

template addKnownPeer*(node: Eth2Node, peer: ENode|enr.Record) =
node.discovery.addNode peer

Expand Down
4 changes: 4 additions & 0 deletions beacon_chain/peer_pool.nim
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ proc fireNotFullEvent[A, B](pool: PeerPool[A, B],
elif item.peerType == PeerType.Outgoing:
pool.outNotFullEvent.fire()

iterator pairs*[A, B](pool: PeerPool[A, B]): (B, A) =
for peerId, peerIdx in pool.registry:
yield (peerId, pool.storage[peerIdx.data].data)

proc waitNotEmptyEvent[A, B](pool: PeerPool[A, B],
filter: set[PeerType]) {.async.} =
if filter == {PeerType.Incoming, PeerType.Outgoing} or filter == {}:
Expand Down
Loading

0 comments on commit adcec61

Please sign in to comment.