-
Notifications
You must be signed in to change notification settings - Fork 108
/
portalcli.nim
288 lines (240 loc) · 9.22 KB
/
portalcli.nim
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# Nimbus - Portal Network
# Copyright (c) 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/[options, strutils, tables],
confutils, confutils/std/net, chronicles, chronicles/topics_registry,
chronos, metrics, metrics/chronos_httpserver, stew/[byteutils, results],
nimcrypto/[hash, sha2],
eth/[keys, net/nat],
eth/p2p/discoveryv5/[enr, node],
eth/p2p/discoveryv5/protocol as discv5_protocol,
../common/common_utils,
../content_db,
../network/wire/[portal_protocol, portal_stream, portal_protocol_config],
../network/history/[history_content, history_network]
const
defaultListenAddress* = (static ValidIpAddress.init("0.0.0.0"))
defaultAdminListenAddress* = (static ValidIpAddress.init("127.0.0.1"))
defaultListenAddressDesc = $defaultListenAddress
defaultAdminListenAddressDesc = $defaultAdminListenAddress
# 100mb seems a bit smallish we may consider increasing defaults after some
# network measurements
defaultStorageSize* = uint32(1000 * 1000 * 100)
type
PortalCmd* = enum
noCommand
ping
findNodes
findContent
PortalCliConf* = object
logLevel* {.
defaultValue: LogLevel.DEBUG
defaultValueDesc: $LogLevel.DEBUG
desc: "Sets the log level"
name: "log-level" .}: LogLevel
udpPort* {.
defaultValue: 9009
desc: "UDP listening port"
name: "udp-port" .}: uint16
listenAddress* {.
defaultValue: defaultListenAddress
defaultValueDesc: $defaultListenAddressDesc
desc: "Listening address for the Discovery v5 traffic"
name: "listen-address" }: ValidIpAddress
# Note: This will add bootstrap nodes for both Discovery v5 network and each
# enabled Portal network. No distinction is made on bootstrap nodes per
# specific network.
bootstrapNodes* {.
desc: "ENR URI of node to bootstrap Discovery v5 and the Portal networks from. Argument may be repeated"
name: "bootstrap-node" .}: seq[Record]
bootstrapNodesFile* {.
desc: "Specifies a line-delimited file of ENR URIs to bootstrap Discovery v5 and Portal networks from"
defaultValue: ""
name: "bootstrap-file" }: InputFile
nat* {.
desc: "Specify method to use for determining public address. " &
"Must be one of: any, none, upnp, pmp, extip:<IP>"
defaultValue: NatConfig(hasExtIp: false, nat: NatAny)
defaultValueDesc: "any"
name: "nat" .}: NatConfig
enrAutoUpdate* {.
defaultValue: false
desc: "Discovery can automatically update its ENR with the IP address " &
"and UDP port as seen by other nodes it communicates with. " &
"This option allows to enable/disable this functionality"
name: "enr-auto-update" .}: bool
networkKey* {.
desc: "Private key (secp256k1) for the p2p network, hex encoded.",
defaultValue: PrivateKey.random(keys.newRng()[])
defaultValueDesc: "random"
name: "network-key" .}: PrivateKey
metricsEnabled* {.
defaultValue: false
desc: "Enable the metrics server"
name: "metrics" .}: bool
metricsAddress* {.
defaultValue: defaultAdminListenAddress
defaultValueDesc: $defaultAdminListenAddressDesc
desc: "Listening address of the metrics server"
name: "metrics-address" .}: ValidIpAddress
metricsPort* {.
defaultValue: 8008
desc: "Listening HTTP port of the metrics server"
name: "metrics-port" .}: Port
protocolId* {.
defaultValue: historyProtocolId
desc: "Portal wire protocol id for the network to connect to"
name: "protocol-id" .}: PortalProtocolId
# TODO maybe it is worth defining minimal storage size and throw error if
# value provided is smaller than minimum
storageSize* {.
desc: "Maximum amount (in bytes) of content which will be stored " &
"in local database."
defaultValue: defaultStorageSize
name: "storage-size" .}: uint32
case cmd* {.
command
defaultValue: noCommand }: PortalCmd
of noCommand:
discard
of ping:
pingTarget* {.
argument
desc: "ENR URI of the node to a send ping message"
name: "node" .}: Node
of findNodes:
distance* {.
defaultValue: 255
desc: "Distance parameter for the findNodes message"
name: "distance" .}: uint16
# TODO: Order here matters as else the help message does not show all the
# information, see: https://github.com/status-im/nim-confutils/issues/15
findNodesTarget* {.
argument
desc: "ENR URI of the node to send a findNodes message"
name: "node" .}: Node
of findContent:
findContentTarget* {.
argument
desc: "ENR URI of the node to send a findContent message"
name: "node" .}: Node
proc parseCmdArg*(T: type enr.Record, p: string): T =
if not fromURI(result, p):
raise newException(ValueError, "Invalid ENR")
proc completeCmdArg*(T: type enr.Record, val: string): seq[string] =
return @[]
proc parseCmdArg*(T: type Node, p: string): T =
var record: enr.Record
if not fromURI(record, p):
raise newException(ValueError, "Invalid ENR")
let n = newNode(record)
if n.isErr:
raise newException(ValueError, $n.error)
if n[].address.isNone():
raise newException(ValueError, "ENR without address")
n[]
proc completeCmdArg*(T: type Node, val: string): seq[string] =
return @[]
proc parseCmdArg*(T: type PrivateKey, p: string): T =
try:
result = PrivateKey.fromHex(p).tryGet()
except CatchableError:
raise newException(ValueError, "Invalid private key")
proc completeCmdArg*(T: type PrivateKey, val: string): seq[string] =
return @[]
proc parseCmdArg*(T: type PortalProtocolId, p: string): T =
try:
result = byteutils.hexToByteArray(p, 2)
except ValueError:
raise newException(ValueError,
"Invalid protocol id, not a valid hex value")
proc completeCmdArg*(T: type PortalProtocolId, val: string): seq[string] =
return @[]
proc discover(d: discv5_protocol.Protocol) {.async.} =
while true:
let discovered = await d.queryRandom()
info "Lookup finished", nodes = discovered.len
await sleepAsync(30.seconds)
proc testContentIdHandler(contentKey: ByteList): results.Opt[ContentId] =
# Note: Returning a static content id here, as in practice this depends
# on the content key to content id derivation, which is different for the
# different content networks. And we want these tests to be independent from
# that.
let idHash = sha256.digest("test")
ok(readUintBE[256](idHash.data))
proc run(config: PortalCliConf) =
let
rng = newRng()
bindIp = config.listenAddress
udpPort = Port(config.udpPort)
# TODO: allow for no TCP port mapping!
(extIp, _, extUdpPort) = setupAddress(config.nat,
config.listenAddress, udpPort, udpPort, "portalcli")
var bootstrapRecords: seq[Record]
loadBootstrapFile(string config.bootstrapNodesFile, bootstrapRecords)
bootstrapRecords.add(config.bootstrapNodes)
let d = newProtocol(
config.networkKey,
extIp, none(Port), extUdpPort,
bootstrapRecords = bootstrapRecords,
bindIp = bindIp, bindPort = udpPort,
enrAutoUpdate = config.enrAutoUpdate,
rng = rng)
d.open()
let
db = ContentDB.new("", config.storageSize, inMemory = true)
sm = StreamManager.new(d)
cq = newAsyncQueue[(ContentKeysList, seq[seq[byte]])](50)
stream = sm.registerNewStream(cq)
portal = PortalProtocol.new(d, config.protocolId,
testContentIdHandler, createGetHandler(db), stream,
bootstrapRecords = bootstrapRecords)
portal.dbPut = createStoreHandler(db, defaultRadiusConfig, portal)
if config.metricsEnabled:
let
address = config.metricsAddress
port = config.metricsPort
notice "Starting metrics HTTP server",
url = "http:https://" & $address & ":" & $port & "/metrics"
try:
chronos_httpserver.startMetricsHttpServer($address, port)
except CatchableError as exc: raise exc
except Exception as exc: raiseAssert exc.msg # TODO fix metrics
case config.cmd
of ping:
let pong = waitFor portal.ping(config.pingTarget)
if pong.isOk():
echo pong.get()
else:
echo pong.error
of findNodes:
let distances = @[config.distance]
let nodes = waitFor portal.findNodes(config.findNodesTarget, distances)
if nodes.isOk():
for node in nodes.get():
echo $node.record & " - " & shortLog(node)
else:
echo nodes.error
of findContent:
proc random(T: type UInt256, rng: var HmacDrbgContext): T =
rng.generate(T)
# For now just some random bytes
let contentKey = ByteList.init(@[1'u8])
let foundContent = waitFor portal.findContent(config.findContentTarget,
contentKey)
if foundContent.isOk():
echo foundContent.get()
else:
echo foundContent.error
of noCommand:
d.start()
portal.start()
waitFor(discover(d))
when isMainModule:
let config = PortalCliConf.load()
setLogLevel(config.logLevel)
run(config)