2019-12-17 23:16:28 +00:00
|
|
|
import
|
2020-03-05 00:25:21 +00:00
|
|
|
std/[tables, sets, options, math, random],
|
|
|
|
json_serialization/std/net,
|
|
|
|
stew/[byteutils, endians2], chronicles, chronos, stint,
|
2020-03-02 13:10:19 +00:00
|
|
|
eth/[rlp, keys], ../enode, types, encoding, node, routing_table, enr
|
2019-12-17 23:16:28 +00:00
|
|
|
|
2019-12-16 19:38:45 +00:00
|
|
|
import nimcrypto except toHex
|
|
|
|
|
2020-02-19 12:11:19 +00:00
|
|
|
logScope:
|
|
|
|
topics = "discv5"
|
|
|
|
|
2020-02-27 12:45:12 +00:00
|
|
|
const
|
|
|
|
alpha = 3 ## Kademlia concurrency factor
|
|
|
|
lookupRequestLimit = 3
|
|
|
|
findNodeResultLimit = 15 # applies in FINDNODE handler
|
|
|
|
maxNodesPerPacket = 3
|
|
|
|
lookupInterval = 60.seconds ## Interval of launching a random lookup to
|
|
|
|
## populate the routing table. go-ethereum seems to do 3 runs every 30
|
|
|
|
## minutes. Trinity starts one every minute.
|
|
|
|
handshakeTimeout* = 2.seconds ## timeout for the reply on the
|
|
|
|
## whoareyou message
|
|
|
|
responseTimeout* = 2.seconds ## timeout for the response of a request-response
|
|
|
|
## call
|
|
|
|
magicSize = 32 ## size of the magic which is the start of the whoareyou
|
|
|
|
## message
|
|
|
|
|
2019-12-16 19:38:45 +00:00
|
|
|
type
|
|
|
|
Protocol* = ref object
|
|
|
|
transp: DatagramTransport
|
2020-02-17 15:36:04 +00:00
|
|
|
localNode*: Node
|
2019-12-16 19:38:45 +00:00
|
|
|
privateKey: PrivateKey
|
2020-02-27 12:45:12 +00:00
|
|
|
whoareyouMagic: array[magicSize, byte]
|
2019-12-16 19:38:45 +00:00
|
|
|
idHash: array[32, byte]
|
2020-02-27 12:45:12 +00:00
|
|
|
pendingRequests: Table[AuthTag, PendingRequest]
|
2019-12-16 19:38:45 +00:00
|
|
|
db: Database
|
|
|
|
routingTable: RoutingTable
|
2020-02-26 22:15:14 +00:00
|
|
|
codec*: Codec
|
2019-12-16 19:38:45 +00:00
|
|
|
awaitedPackets: Table[(Node, RequestId), Future[Option[Packet]]]
|
2020-02-24 14:45:30 +00:00
|
|
|
lookupLoop: Future[void]
|
|
|
|
revalidateLoop: Future[void]
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
PendingRequest = object
|
|
|
|
node: Node
|
|
|
|
packet: seq[byte]
|
|
|
|
|
2020-02-27 12:45:12 +00:00
|
|
|
proc whoareyouMagic(toNode: NodeId): array[magicSize, byte] =
|
2019-12-23 17:21:11 +00:00
|
|
|
const prefix = "WHOAREYOU"
|
|
|
|
var data: array[prefix.len + sizeof(toNode), byte]
|
|
|
|
data[0 .. sizeof(toNode) - 1] = toNode.toByteArrayBE()
|
|
|
|
for i, c in prefix: data[sizeof(toNode) + i] = byte(c)
|
2019-12-16 19:38:45 +00:00
|
|
|
sha256.digest(data).data
|
|
|
|
|
2020-02-21 23:55:37 +00:00
|
|
|
proc newProtocol*(privKey: PrivateKey, db: Database,
|
|
|
|
ip: IpAddress, tcpPort, udpPort: Port): Protocol =
|
|
|
|
let
|
|
|
|
a = Address(ip: ip, tcpPort: tcpPort, udpPort: udpPort)
|
|
|
|
enode = initENode(privKey.getPublicKey(), a)
|
|
|
|
enrRec = enr.Record.init(12, privKey, a)
|
|
|
|
node = newNode(enode, enrRec)
|
|
|
|
|
|
|
|
result = Protocol(
|
|
|
|
privateKey: privKey,
|
|
|
|
db: db,
|
|
|
|
localNode: node,
|
|
|
|
whoareyouMagic: whoareyouMagic(node.id),
|
|
|
|
idHash: sha256.digest(node.id.toByteArrayBE).data,
|
|
|
|
codec: Codec(localNode: node, privKey: privKey, db: db))
|
|
|
|
|
|
|
|
result.routingTable.init(node)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
proc send(d: Protocol, a: Address, data: seq[byte]) =
|
2020-02-17 16:44:56 +00:00
|
|
|
# debug "Sending bytes", amount = data.len, to = a
|
2019-12-16 19:38:45 +00:00
|
|
|
let ta = initTAddress(a.ip, a.udpPort)
|
|
|
|
let f = d.transp.sendTo(ta, data)
|
|
|
|
f.callback = proc(data: pointer) {.gcsafe.} =
|
|
|
|
if f.failed:
|
|
|
|
debug "Discovery send failed", msg = f.readError.msg
|
|
|
|
|
|
|
|
proc send(d: Protocol, n: Node, data: seq[byte]) =
|
|
|
|
d.send(n.node.address, data)
|
|
|
|
|
|
|
|
proc `xor`[N: static[int], T](a, b: array[N, T]): array[N, T] =
|
|
|
|
for i in 0 .. a.high:
|
|
|
|
result[i] = a[i] xor b[i]
|
|
|
|
|
|
|
|
proc isWhoAreYou(d: Protocol, msg: Bytes): bool =
|
|
|
|
if msg.len > d.whoareyouMagic.len:
|
2020-02-27 12:45:12 +00:00
|
|
|
result = d.whoareyouMagic == msg.toOpenArray(0, magicSize - 1)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
proc decodeWhoAreYou(d: Protocol, msg: Bytes): Whoareyou =
|
|
|
|
result = Whoareyou()
|
2020-02-27 12:45:12 +00:00
|
|
|
result[] = rlp.decode(msg.toRange[magicSize .. ^1], WhoareyouObj)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-27 12:45:12 +00:00
|
|
|
proc sendWhoareyou(d: Protocol, address: Address, toNode: NodeId, authTag: AuthTag) =
|
2020-02-22 18:49:14 +00:00
|
|
|
trace "sending who are you", to = $toNode, toAddress = $address
|
2020-03-10 21:28:11 +00:00
|
|
|
let challenge = Whoareyou(authTag: authTag, recordSeq: 0)
|
2020-02-26 22:15:14 +00:00
|
|
|
encoding.randomBytes(challenge.idNonce)
|
|
|
|
# If there is already a handshake going on for this nodeid then we drop this
|
|
|
|
# new one. Handshake will get cleaned up after `handshakeTimeout`.
|
|
|
|
# If instead overwriting the handshake would be allowed, the handshake timeout
|
|
|
|
# will need to be canceled each time.
|
|
|
|
# TODO: could also clean up handshakes in a seperate call, e.g. triggered in
|
|
|
|
# a loop.
|
2020-02-27 12:59:36 +00:00
|
|
|
# Use toNode + address to make it more difficult for an attacker to occupy
|
|
|
|
# the handshake of another node.
|
2020-02-27 21:36:42 +00:00
|
|
|
|
|
|
|
let key = HandShakeKey(nodeId: toNode, address: $address)
|
|
|
|
if not d.codec.handshakes.hasKeyOrPut(key, challenge):
|
2020-02-26 22:15:14 +00:00
|
|
|
sleepAsync(handshakeTimeout).addCallback() do(data: pointer):
|
|
|
|
# TODO: should we still provide cancellation in case handshake completes
|
|
|
|
# correctly?
|
2020-02-27 21:36:42 +00:00
|
|
|
d.codec.handshakes.del(key)
|
2020-02-26 22:15:14 +00:00
|
|
|
|
|
|
|
var data = @(whoareyouMagic(toNode))
|
|
|
|
data.add(rlp.encode(challenge[]))
|
|
|
|
d.send(address, data)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
proc sendNodes(d: Protocol, toNode: Node, reqId: RequestId, nodes: openarray[Node]) =
|
|
|
|
proc sendNodes(d: Protocol, toNode: Node, packet: NodesPacket, reqId: RequestId) {.nimcall.} =
|
2019-12-18 10:36:11 +00:00
|
|
|
let (data, _) = d.codec.encodeEncrypted(toNode, encodePacket(packet, reqId), challenge = nil)
|
2019-12-16 19:38:45 +00:00
|
|
|
d.send(toNode, data)
|
|
|
|
|
|
|
|
var packet: NodesPacket
|
|
|
|
packet.total = ceil(nodes.len / maxNodesPerPacket).uint32
|
|
|
|
|
|
|
|
for i in 0 ..< nodes.len:
|
|
|
|
packet.enrs.add(nodes[i].record)
|
|
|
|
if packet.enrs.len == 3:
|
|
|
|
d.sendNodes(toNode, packet, reqId)
|
|
|
|
packet.enrs.setLen(0)
|
|
|
|
|
|
|
|
if packet.enrs.len != 0:
|
|
|
|
d.sendNodes(toNode, packet, reqId)
|
|
|
|
|
2019-12-18 10:36:11 +00:00
|
|
|
proc handlePing(d: Protocol, fromNode: Node, ping: PingPacket, reqId: RequestId) =
|
|
|
|
let a = fromNode.address
|
2019-12-16 19:38:45 +00:00
|
|
|
var pong: PongPacket
|
|
|
|
pong.enrSeq = ping.enrSeq
|
|
|
|
pong.ip = case a.ip.family
|
|
|
|
of IpAddressFamily.IPv4: @(a.ip.address_v4)
|
|
|
|
of IpAddressFamily.IPv6: @(a.ip.address_v6)
|
|
|
|
pong.port = a.udpPort.uint16
|
|
|
|
|
2019-12-18 10:36:11 +00:00
|
|
|
let (data, _) = d.codec.encodeEncrypted(fromNode, encodePacket(pong, reqId), challenge = nil)
|
2019-12-16 19:38:45 +00:00
|
|
|
d.send(fromNode, data)
|
|
|
|
|
2019-12-18 10:36:11 +00:00
|
|
|
proc handleFindNode(d: Protocol, fromNode: Node, fn: FindNodePacket, reqId: RequestId) =
|
2019-12-16 19:38:45 +00:00
|
|
|
if fn.distance == 0:
|
|
|
|
d.sendNodes(fromNode, reqId, [d.localNode])
|
|
|
|
else:
|
|
|
|
let distance = min(fn.distance, 256)
|
|
|
|
d.sendNodes(fromNode, reqId, d.routingTable.neighboursAtDistance(distance))
|
|
|
|
|
2020-02-26 22:15:14 +00:00
|
|
|
proc receive*(d: Protocol, a: Address, msg: Bytes) {.gcsafe,
|
2020-02-17 22:47:13 +00:00
|
|
|
raises: [
|
|
|
|
Defect,
|
|
|
|
# TODO This is now coming from Chronos's callSoon
|
|
|
|
Exception,
|
|
|
|
# TODO All of these should probably be handled here
|
|
|
|
RlpError,
|
|
|
|
IOError,
|
|
|
|
TransportAddressError,
|
|
|
|
EthKeysException,
|
|
|
|
Secp256k1Exception,
|
|
|
|
].} =
|
2020-02-27 12:45:12 +00:00
|
|
|
if msg.len < tagSize: # or magicSize, can be either
|
2019-12-16 19:38:45 +00:00
|
|
|
return # Invalid msg
|
|
|
|
|
2020-02-17 16:44:56 +00:00
|
|
|
# debug "Packet received: ", length = msg.len
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-17 16:44:56 +00:00
|
|
|
if d.isWhoAreYou(msg):
|
2020-03-02 13:10:19 +00:00
|
|
|
trace "Received whoareyou", localNode = $d.localNode, address = a
|
2020-02-17 16:44:56 +00:00
|
|
|
let whoareyou = d.decodeWhoAreYou(msg)
|
|
|
|
var pr: PendingRequest
|
|
|
|
if d.pendingRequests.take(whoareyou.authTag, pr):
|
|
|
|
let toNode = pr.node
|
2020-02-17 22:47:13 +00:00
|
|
|
try:
|
|
|
|
let (data, _) = d.codec.encodeEncrypted(toNode, pr.packet, challenge = whoareyou)
|
|
|
|
d.send(toNode, data)
|
|
|
|
except RandomSourceDepleted as err:
|
|
|
|
debug "Failed to respond to a who-you-are msg " &
|
|
|
|
"due to randomness source depletion."
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-17 16:44:56 +00:00
|
|
|
else:
|
2020-02-27 12:45:12 +00:00
|
|
|
var tag: array[tagSize, byte]
|
|
|
|
tag[0 .. ^1] = msg.toOpenArray(0, tagSize - 1)
|
2020-02-17 16:44:56 +00:00
|
|
|
let senderData = tag xor d.idHash
|
|
|
|
let sender = readUintBE[256](senderData)
|
|
|
|
|
2020-02-27 12:45:12 +00:00
|
|
|
var authTag: AuthTag
|
2020-02-17 16:44:56 +00:00
|
|
|
var node: Node
|
|
|
|
var packet: Packet
|
2020-02-25 13:49:31 +00:00
|
|
|
let decoded = d.codec.decodeEncrypted(sender, a, msg, authTag, node, packet)
|
|
|
|
if decoded == DecodeStatus.Success:
|
2020-02-17 16:44:56 +00:00
|
|
|
if node.isNil:
|
|
|
|
node = d.routingTable.getNode(sender)
|
|
|
|
else:
|
2020-03-02 13:10:19 +00:00
|
|
|
debug "Adding new node to routing table", node = $node, localNode = $d.localNode
|
2020-02-17 16:44:56 +00:00
|
|
|
discard d.routingTable.addNode(node)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-17 16:44:56 +00:00
|
|
|
doAssert(not node.isNil, "No node in the routing table (internal error?)")
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-17 16:44:56 +00:00
|
|
|
case packet.kind
|
|
|
|
of ping:
|
|
|
|
d.handlePing(node, packet.ping, packet.reqId)
|
|
|
|
of findNode:
|
|
|
|
d.handleFindNode(node, packet.findNode, packet.reqId)
|
2019-12-16 19:38:45 +00:00
|
|
|
else:
|
2020-02-17 16:44:56 +00:00
|
|
|
var waiter: Future[Option[Packet]]
|
|
|
|
if d.awaitedPackets.take((node, packet.reqId), waiter):
|
|
|
|
waiter.complete(packet.some)
|
|
|
|
else:
|
2020-02-17 22:47:13 +00:00
|
|
|
debug "TODO: handle packet: ", packet = packet.kind, origin = $node
|
2020-03-10 15:01:04 +00:00
|
|
|
elif decoded == DecodeStatus.DecryptError:
|
|
|
|
debug "Could not decrypt packet, respond with whoareyou",
|
2020-03-02 13:10:19 +00:00
|
|
|
localNode = $d.localNode, address = a
|
2020-03-10 15:01:04 +00:00
|
|
|
# only sendingWhoareyou in case it is a decryption failure
|
2020-02-17 16:44:56 +00:00
|
|
|
d.sendWhoareyou(a, sender, authTag)
|
2020-03-10 15:01:04 +00:00
|
|
|
elif decoded == DecodeStatus.PacketError:
|
|
|
|
# Still adding the node in case there is a packet error (could be
|
|
|
|
# unsupported packet)
|
|
|
|
if not node.isNil:
|
|
|
|
debug "Adding new node to routing table", node = $node, localNode = $d.localNode
|
|
|
|
discard d.routingTable.addNode(node)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
proc waitPacket(d: Protocol, fromNode: Node, reqId: RequestId): Future[Option[Packet]] =
|
|
|
|
result = newFuture[Option[Packet]]("waitPacket")
|
|
|
|
let res = result
|
|
|
|
let key = (fromNode, reqId)
|
2020-02-26 22:15:14 +00:00
|
|
|
sleepAsync(responseTimeout).addCallback() do(data: pointer):
|
2019-12-16 19:38:45 +00:00
|
|
|
d.awaitedPackets.del(key)
|
|
|
|
if not res.finished:
|
|
|
|
res.complete(none(Packet))
|
|
|
|
d.awaitedPackets[key] = result
|
|
|
|
|
|
|
|
proc addNodesFromENRs(result: var seq[Node], enrs: openarray[Record]) =
|
|
|
|
for r in enrs: result.add(newNode(r))
|
|
|
|
|
|
|
|
proc waitNodes(d: Protocol, fromNode: Node, reqId: RequestId): Future[seq[Node]] {.async.} =
|
|
|
|
var op = await d.waitPacket(fromNode, reqId)
|
|
|
|
if op.isSome and op.get.kind == nodes:
|
|
|
|
result.addNodesFromENRs(op.get.nodes.enrs)
|
|
|
|
let total = op.get.nodes.total
|
|
|
|
for i in 1 ..< total:
|
|
|
|
op = await d.waitPacket(fromNode, reqId)
|
|
|
|
if op.isSome and op.get.kind == nodes:
|
|
|
|
result.addNodesFromENRs(op.get.nodes.enrs)
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
proc findNode(d: Protocol, toNode: Node, distance: uint32): Future[seq[Node]] {.async.} =
|
|
|
|
let reqId = newRequestId()
|
|
|
|
let packet = encodePacket(FindNodePacket(distance: distance), reqId)
|
2019-12-18 10:36:11 +00:00
|
|
|
let (data, nonce) = d.codec.encodeEncrypted(toNode, packet, challenge = nil)
|
2019-12-16 19:38:45 +00:00
|
|
|
d.pendingRequests[nonce] = PendingRequest(node: toNode, packet: packet)
|
|
|
|
d.send(toNode, data)
|
|
|
|
result = await d.waitNodes(toNode, reqId)
|
|
|
|
|
|
|
|
proc lookupDistances(target, dest: NodeId): seq[uint32] =
|
|
|
|
let td = logDist(target, dest)
|
|
|
|
result.add(td)
|
|
|
|
var i = 1'u32
|
|
|
|
while result.len < lookupRequestLimit:
|
|
|
|
if td + i < 256:
|
|
|
|
result.add(td + i)
|
|
|
|
if td - i > 0'u32:
|
|
|
|
result.add(td - i)
|
|
|
|
inc i
|
|
|
|
|
|
|
|
proc lookupWorker(p: Protocol, destNode: Node, target: NodeId): Future[seq[Node]] {.async.} =
|
|
|
|
let dists = lookupDistances(target, destNode.id)
|
|
|
|
var i = 0
|
2020-02-24 14:45:30 +00:00
|
|
|
while i < lookupRequestLimit and result.len < findNodeResultLimit:
|
2020-02-17 16:44:56 +00:00
|
|
|
# TODO: Handle failures
|
2020-02-24 14:45:30 +00:00
|
|
|
let r = await p.findNode(destNode, dists[i])
|
|
|
|
# TODO: I guess it makes sense to limit here also to `findNodeResultLimit`?
|
2019-12-16 19:38:45 +00:00
|
|
|
result.add(r)
|
|
|
|
inc i
|
|
|
|
|
|
|
|
for n in result:
|
|
|
|
discard p.routingTable.addNode(n)
|
|
|
|
|
2020-02-24 14:45:30 +00:00
|
|
|
proc lookup*(p: Protocol, target: NodeId): Future[seq[Node]] {.async.} =
|
2020-02-19 12:11:19 +00:00
|
|
|
## Perform a lookup for the given target, return the closest n nodes to the
|
|
|
|
## target. Maximum value for n is `BUCKET_SIZE`.
|
|
|
|
# TODO: Sort the returned nodes on distance
|
|
|
|
result = p.routingTable.neighbours(target, BUCKET_SIZE)
|
2019-12-16 19:38:45 +00:00
|
|
|
var asked = initHashSet[NodeId]()
|
|
|
|
asked.incl(p.localNode.id)
|
|
|
|
var seen = asked
|
2020-02-19 12:11:19 +00:00
|
|
|
for node in result:
|
|
|
|
seen.incl(node.id)
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
var pendingQueries = newSeqOfCap[Future[seq[Node]]](alpha)
|
|
|
|
|
|
|
|
while true:
|
|
|
|
var i = 0
|
|
|
|
while i < result.len and pendingQueries.len < alpha:
|
|
|
|
let n = result[i]
|
|
|
|
if not asked.containsOrIncl(n.id):
|
|
|
|
pendingQueries.add(p.lookupWorker(n, target))
|
|
|
|
inc i
|
|
|
|
|
2020-02-25 13:49:31 +00:00
|
|
|
trace "discv5 pending queries", total = pendingQueries.len
|
2020-02-05 13:47:43 +00:00
|
|
|
|
2019-12-16 19:38:45 +00:00
|
|
|
if pendingQueries.len == 0:
|
|
|
|
break
|
|
|
|
|
|
|
|
let idx = await oneIndex(pendingQueries)
|
2020-02-25 13:49:31 +00:00
|
|
|
trace "Got discv5 lookup response", idx
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
let nodes = pendingQueries[idx].read
|
|
|
|
pendingQueries.del(idx)
|
|
|
|
for n in nodes:
|
|
|
|
if not seen.containsOrIncl(n.id):
|
|
|
|
if result.len < BUCKET_SIZE:
|
|
|
|
result.add(n)
|
|
|
|
|
2020-02-26 22:15:14 +00:00
|
|
|
proc lookupRandom*(p: Protocol): Future[seq[Node]]
|
|
|
|
{.raises:[RandomSourceDepleted, Defect, Exception].} =
|
2019-12-16 19:38:45 +00:00
|
|
|
var id: NodeId
|
2020-02-26 22:15:14 +00:00
|
|
|
if randomBytes(addr id, sizeof(id)) != sizeof(id):
|
|
|
|
raise newException(RandomSourceDepleted, "Could not randomize bytes")
|
2019-12-16 19:38:45 +00:00
|
|
|
p.lookup(id)
|
|
|
|
|
|
|
|
proc processClient(transp: DatagramTransport,
|
|
|
|
raddr: TransportAddress): Future[void] {.async, gcsafe.} =
|
|
|
|
var proto = getUserData[Protocol](transp)
|
|
|
|
try:
|
|
|
|
# TODO: Maybe here better to use `peekMessage()` to avoid allocation,
|
|
|
|
# but `Bytes` object is just a simple seq[byte], and `ByteRange` object
|
|
|
|
# do not support custom length.
|
|
|
|
var buf = transp.getMessage()
|
|
|
|
let a = Address(ip: raddr.address, udpPort: raddr.port, tcpPort: raddr.port)
|
|
|
|
proto.receive(a, buf)
|
2020-02-17 16:44:56 +00:00
|
|
|
except RlpError as e:
|
|
|
|
debug "Receive failed", exception = e.name, msg = e.msg
|
|
|
|
# TODO: what else can be raised? Figure this out and be more restrictive?
|
|
|
|
except CatchableError as e:
|
|
|
|
debug "Receive failed", exception = e.name, msg = e.msg,
|
|
|
|
stacktrace = e.getStackTrace()
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-03-06 16:01:45 +00:00
|
|
|
proc ping(p: Protocol, toNode: Node): RequestId =
|
|
|
|
let
|
|
|
|
reqId = newRequestId()
|
|
|
|
ping = PingPacket(enrSeq: p.localNode.record.seqNum)
|
|
|
|
packet = encodePacket(ping, reqId)
|
|
|
|
(data, nonce) = p.codec.encodeEncrypted(toNode, packet, challenge = nil)
|
|
|
|
p.pendingRequests[nonce] = PendingRequest(node: toNode, packet: packet)
|
|
|
|
p.send(toNode, data)
|
|
|
|
return reqId
|
|
|
|
|
2020-02-24 14:45:30 +00:00
|
|
|
proc revalidateNode(p: Protocol, n: Node)
|
|
|
|
{.async, raises:[Defect, Exception].} = # TODO: Exception
|
2020-03-06 16:01:45 +00:00
|
|
|
let reqId = p.ping(n)
|
2019-12-23 17:21:11 +00:00
|
|
|
|
|
|
|
let resp = await p.waitPacket(n, reqId)
|
|
|
|
if resp.isSome and resp.get.kind == pong:
|
|
|
|
let pong = resp.get.pong
|
2020-02-11 16:25:31 +00:00
|
|
|
if pong.enrSeq > n.record.seqNum:
|
2019-12-23 17:21:11 +00:00
|
|
|
# TODO: Request new ENR
|
|
|
|
discard
|
|
|
|
|
|
|
|
p.routingTable.setJustSeen(n)
|
2020-03-10 21:28:11 +00:00
|
|
|
trace "Revalidated node", node = $n
|
2019-12-23 17:21:11 +00:00
|
|
|
else:
|
|
|
|
if false: # TODO: if not bootnode:
|
|
|
|
p.routingTable.removeNode(n)
|
|
|
|
|
|
|
|
proc revalidateLoop(p: Protocol) {.async.} =
|
2020-02-24 14:45:30 +00:00
|
|
|
try:
|
|
|
|
# TODO: We need to handle actual errors still, which might just allow to
|
|
|
|
# continue the loop. However, currently `revalidateNode` raises a general
|
|
|
|
# `Exception` making this rather hard.
|
|
|
|
while true:
|
|
|
|
await sleepAsync(rand(10 * 1000).milliseconds)
|
|
|
|
let n = p.routingTable.nodeToRevalidate()
|
|
|
|
if not n.isNil:
|
2020-03-01 10:50:26 +00:00
|
|
|
# TODO: Should we do these in parallel and/or async to be certain of how
|
|
|
|
# often nodes are revalidated?
|
2020-02-24 14:45:30 +00:00
|
|
|
await p.revalidateNode(n)
|
|
|
|
except CancelledError:
|
|
|
|
trace "revalidateLoop canceled"
|
|
|
|
|
|
|
|
proc lookupLoop(d: Protocol) {.async.} =
|
|
|
|
## TODO: Same story as for `revalidateLoop`
|
|
|
|
try:
|
|
|
|
while true:
|
|
|
|
let nodes = await d.lookupRandom()
|
2020-03-02 13:10:19 +00:00
|
|
|
trace "Discovered nodes", nodes = $nodes
|
2020-02-24 14:45:30 +00:00
|
|
|
await sleepAsync(lookupInterval)
|
|
|
|
except CancelledError:
|
|
|
|
trace "lookupLoop canceled"
|
2019-12-23 17:21:11 +00:00
|
|
|
|
2019-12-16 19:38:45 +00:00
|
|
|
proc open*(d: Protocol) =
|
2020-03-10 21:28:11 +00:00
|
|
|
debug "Starting discovery node", node = $d.localNode,
|
|
|
|
uri = toURI(d.localNode.record)
|
2019-12-16 19:38:45 +00:00
|
|
|
# TODO allow binding to specific IP / IPv6 / etc
|
|
|
|
let ta = initTAddress(IPv4_any(), d.localNode.node.address.udpPort)
|
|
|
|
d.transp = newDatagramTransport(processClient, udata = d, local = ta)
|
2020-02-24 14:45:30 +00:00
|
|
|
# Might want to move these to a separate proc if this turns out to be needed.
|
|
|
|
d.lookupLoop = lookupLoop(d)
|
|
|
|
d.revalidateLoop = revalidateLoop(d)
|
|
|
|
|
|
|
|
proc close*(d: Protocol) =
|
|
|
|
doAssert(not d.lookupLoop.isNil() or not d.revalidateLoop.isNil())
|
|
|
|
doAssert(not d.transp.closed)
|
|
|
|
|
2020-03-02 13:10:19 +00:00
|
|
|
debug "Closing discovery node", node = $d.localNode
|
2020-02-24 14:45:30 +00:00
|
|
|
d.revalidateLoop.cancel()
|
|
|
|
d.lookupLoop.cancel()
|
|
|
|
# TODO: unsure if close can't create issues in the not awaited cancellations
|
|
|
|
# above
|
|
|
|
d.transp.close()
|
|
|
|
|
|
|
|
proc closeWait*(d: Protocol) {.async.} =
|
|
|
|
doAssert(not d.lookupLoop.isNil() or not d.revalidateLoop.isNil())
|
|
|
|
doAssert(not d.transp.closed)
|
|
|
|
|
2020-03-02 13:10:19 +00:00
|
|
|
debug "Closing discovery node", node = $d.localNode
|
2020-02-24 14:45:30 +00:00
|
|
|
await allFutures([d.revalidateLoop.cancelAndWait(),
|
|
|
|
d.lookupLoop.cancelAndWait()])
|
|
|
|
await d.transp.closeWait()
|
2019-12-16 19:38:45 +00:00
|
|
|
|
2020-02-05 12:28:23 +00:00
|
|
|
proc addNode*(d: Protocol, node: Node) =
|
|
|
|
discard d.routingTable.addNode(node)
|
|
|
|
|
|
|
|
template addNode*(d: Protocol, enode: ENode) =
|
|
|
|
addNode d, newNode(enode)
|
|
|
|
|
|
|
|
template addNode*(d: Protocol, r: Record) =
|
|
|
|
addNode d, newNode(r)
|
2019-12-17 21:20:26 +00:00
|
|
|
|
|
|
|
proc addNode*(d: Protocol, enr: EnrUri) =
|
|
|
|
var r: Record
|
|
|
|
let res = r.fromUri(enr)
|
|
|
|
doAssert(res)
|
2020-02-05 13:47:43 +00:00
|
|
|
d.addNode newNode(r)
|
2019-12-17 21:20:26 +00:00
|
|
|
|
2019-12-23 17:21:11 +00:00
|
|
|
proc randomNodes*(k: Protocol, count: int): seq[Node] =
|
|
|
|
k.routingTable.randomNodes(count)
|
|
|
|
|
|
|
|
proc nodesDiscovered*(k: Protocol): int {.inline.} = k.routingTable.len
|
|
|
|
|
2019-12-16 19:38:45 +00:00
|
|
|
when isMainModule:
|
|
|
|
import discovery_db
|
|
|
|
import eth/trie/db
|
|
|
|
|
|
|
|
proc genDiscoveries(n: int): seq[Protocol] =
|
|
|
|
var pks = ["98b3d4d4fe348ac5192d16b46aa36c41f847b9f265ba4d56f6326669449a968b", "88d125288fbb19ecd7b6a355faf3e842e3c6158d38af14bb97ac8d957ec9cb58", "c9a24471d2f84efa103b9abbdedd4c0fea8402f94e5ceb3ca4d9cff951fc407f"]
|
|
|
|
for i in 0 ..< n:
|
|
|
|
var pk: PrivateKey
|
|
|
|
if i < pks.len:
|
|
|
|
pk = initPrivateKey(pks[i])
|
|
|
|
else:
|
|
|
|
pk = newPrivateKey()
|
|
|
|
|
2020-02-17 23:07:23 +00:00
|
|
|
let d = newProtocol(pk, DiscoveryDB.init(newMemoryDB()),
|
2020-02-21 23:55:37 +00:00
|
|
|
parseIpAddress("127.0.0.1"), Port(12001 + i), Port(12001 + i))
|
2019-12-16 19:38:45 +00:00
|
|
|
d.open()
|
|
|
|
result.add(d)
|
|
|
|
|
|
|
|
proc addNode(d: openarray[Protocol], enr: string) =
|
2019-12-18 10:36:11 +00:00
|
|
|
for dd in d: dd.addNode(EnrUri(enr))
|
2019-12-16 19:38:45 +00:00
|
|
|
|
|
|
|
proc test() {.async.} =
|
|
|
|
block:
|
|
|
|
let d = genDiscoveries(3)
|
|
|
|
d.addNode("enr:-IS4QPvi3TdAUd2Jdrx-8ScRbCzrV1kVsTTM02mfz8Fx7CtrAfYN7AjxTx3MWbY2efRmAhS-Yyv4nhyzKu_YS6jSh08BgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQJeWTAJhJYN2q3BvcQwsyo7pIi8KnfwDIrhNdflCFvqr4N1ZHCCD6A")
|
|
|
|
|
|
|
|
for i, dd in d:
|
|
|
|
let nodes = await dd.lookupRandom()
|
|
|
|
echo "NODES ", i, ": ", nodes
|
|
|
|
|
|
|
|
# block:
|
|
|
|
# var d = genDiscoveries(4)
|
|
|
|
# let rootD = d[0]
|
|
|
|
# d.del(0)
|
|
|
|
|
|
|
|
|
|
|
|
# d.addNode(rootD.localNode.record.toUri)
|
|
|
|
|
|
|
|
# for i, dd in d:
|
|
|
|
# let nodes = await dd.lookupRandom()
|
|
|
|
# echo "NODES ", i, ": ", nodes
|
|
|
|
|
|
|
|
waitFor test()
|
2019-12-17 21:20:26 +00:00
|
|
|
runForever()
|