mirror of https://github.com/status-im/nim-eth.git
Merge pull request #347 from status-im/discv4-strict
Discv4 with chronos strict usage
This commit is contained in:
commit
04f641c923
|
@ -47,15 +47,11 @@ task test_discv4, "Run discovery v4 tests":
|
||||||
runTest("tests/p2p/test_discovery", chronosStrict = false)
|
runTest("tests/p2p/test_discovery", chronosStrict = false)
|
||||||
|
|
||||||
task test_p2p, "Run p2p tests":
|
task test_p2p, "Run p2p tests":
|
||||||
test_discv5_task()
|
runTest("tests/p2p/all_tests")
|
||||||
|
|
||||||
|
# Code that still requires chronosStrict = false
|
||||||
for filename in [
|
for filename in [
|
||||||
"les/test_flow_control",
|
"les/test_flow_control",
|
||||||
"test_auth",
|
|
||||||
"test_crypt",
|
|
||||||
"test_discovery",
|
|
||||||
"test_ecies",
|
|
||||||
"test_enode",
|
|
||||||
"test_rlpx_thunk",
|
"test_rlpx_thunk",
|
||||||
"test_shh",
|
"test_shh",
|
||||||
"test_shh_config",
|
"test_shh_config",
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
|
# nim-eth
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[tables, hashes],
|
std/[tables, hashes],
|
||||||
stew/results, stew/shims/net as stewNet, chronos, chronicles
|
stew/results, stew/shims/net as stewNet, chronos, chronicles
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
type
|
type
|
||||||
IpLimits* = object
|
IpLimits* = object
|
||||||
limit*: uint
|
limit*: uint
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
#
|
# nim-eth
|
||||||
# Ethereum P2P
|
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
||||||
# (c) Copyright 2018
|
# Licensed and distributed under either of
|
||||||
# Status Research & Development GmbH
|
# * 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).
|
||||||
# Licensed under either of
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
||||||
# MIT license (LICENSE-MIT)
|
{.push raises: [Defect].}
|
||||||
#
|
|
||||||
|
|
||||||
import
|
import
|
||||||
std/times,
|
std/times,
|
||||||
|
@ -57,9 +56,9 @@ proc append*(w: var RlpWriter, a: IpAddress) =
|
||||||
of IpAddressFamily.IPv4:
|
of IpAddressFamily.IPv4:
|
||||||
w.append(a.address_v4)
|
w.append(a.address_v4)
|
||||||
|
|
||||||
proc append(w: var RlpWriter, p: Port) {.inline.} = w.append(p.int)
|
proc append(w: var RlpWriter, p: Port) = w.append(p.int)
|
||||||
proc append(w: var RlpWriter, pk: PublicKey) {.inline.} = w.append(pk.toRaw())
|
proc append(w: var RlpWriter, pk: PublicKey) = w.append(pk.toRaw())
|
||||||
proc append(w: var RlpWriter, h: MDigest[256]) {.inline.} = w.append(h.data)
|
proc append(w: var RlpWriter, h: MDigest[256]) = w.append(h.data)
|
||||||
|
|
||||||
proc pack(cmdId: CommandId, payload: openArray[byte], pk: PrivateKey): seq[byte] =
|
proc pack(cmdId: CommandId, payload: openArray[byte], pk: PrivateKey): seq[byte] =
|
||||||
## Create and sign a UDP message to be sent to a remote node.
|
## Create and sign a UDP message to be sent to a remote node.
|
||||||
|
@ -90,7 +89,8 @@ proc recoverMsgPublicKey(msg: openArray[byte]): DiscResult[PublicKey] =
|
||||||
let sig = ? Signature.fromRaw(msg.toOpenArray(MAC_SIZE, HEAD_SIZE))
|
let sig = ? Signature.fromRaw(msg.toOpenArray(MAC_SIZE, HEAD_SIZE))
|
||||||
recover(sig, msg.toOpenArray(HEAD_SIZE, msg.high))
|
recover(sig, msg.toOpenArray(HEAD_SIZE, msg.high))
|
||||||
|
|
||||||
proc unpack(msg: openArray[byte]): tuple[cmdId: CommandId, payload: seq[byte]] =
|
proc unpack(msg: openArray[byte]): tuple[cmdId: CommandId, payload: seq[byte]]
|
||||||
|
{.raises: [DiscProtocolError, Defect].} =
|
||||||
# Check against possible RangeError
|
# Check against possible RangeError
|
||||||
if msg[HEAD_SIZE].int < CommandId.low.ord or
|
if msg[HEAD_SIZE].int < CommandId.low.ord or
|
||||||
msg[HEAD_SIZE].int > CommandId.high.ord:
|
msg[HEAD_SIZE].int > CommandId.high.ord:
|
||||||
|
@ -103,14 +103,14 @@ proc expiration(): uint32 =
|
||||||
|
|
||||||
# Wire protocol
|
# Wire protocol
|
||||||
|
|
||||||
proc send(d: DiscoveryProtocol, n: Node, data: seq[byte]) =
|
proc send(d: DiscoveryProtocol, n: Node, data: seq[byte]) {.raises: [Defect].} =
|
||||||
let ta = initTAddress(n.node.address.ip, n.node.address.udpPort)
|
let ta = initTAddress(n.node.address.ip, n.node.address.udpPort)
|
||||||
let f = d.transp.sendTo(ta, data)
|
let f = d.transp.sendTo(ta, data)
|
||||||
f.callback = proc(data: pointer) {.gcsafe.} =
|
f.callback = proc(data: pointer) {.gcsafe.} =
|
||||||
if f.failed:
|
if f.failed:
|
||||||
debug "Discovery send failed", msg = f.readError.msg
|
debug "Discovery send failed", msg = f.readError.msg
|
||||||
|
|
||||||
proc sendPing*(d: DiscoveryProtocol, n: Node): seq[byte] =
|
proc sendPing*(d: DiscoveryProtocol, n: Node): seq[byte] {.raises: [Defect].} =
|
||||||
let payload = rlp.encode((PROTO_VERSION, d.address, n.node.address,
|
let payload = rlp.encode((PROTO_VERSION, d.address, n.node.address,
|
||||||
expiration()))
|
expiration()))
|
||||||
let msg = pack(cmdPing, payload, d.privKey)
|
let msg = pack(cmdPing, payload, d.privKey)
|
||||||
|
@ -165,17 +165,18 @@ proc newDiscoveryProtocol*(privKey: PrivateKey, address: Address,
|
||||||
result.thisNode = newNode(privKey.toPublicKey(), address)
|
result.thisNode = newNode(privKey.toPublicKey(), address)
|
||||||
result.kademlia = newKademliaProtocol(result.thisNode, result, rng = rng)
|
result.kademlia = newKademliaProtocol(result.thisNode, result, rng = rng)
|
||||||
|
|
||||||
proc recvPing(d: DiscoveryProtocol, node: Node,
|
proc recvPing(d: DiscoveryProtocol, node: Node, msgHash: MDigest[256])
|
||||||
msgHash: MDigest[256]) {.inline.} =
|
{.raises: [ValueError, Defect].} =
|
||||||
d.kademlia.recvPing(node, msgHash)
|
d.kademlia.recvPing(node, msgHash)
|
||||||
|
|
||||||
proc recvPong(d: DiscoveryProtocol, node: Node, payload: seq[byte]) {.inline.} =
|
proc recvPong(d: DiscoveryProtocol, node: Node, payload: seq[byte])
|
||||||
|
{.raises: [RlpError, Defect].} =
|
||||||
let rlp = rlpFromBytes(payload)
|
let rlp = rlpFromBytes(payload)
|
||||||
let tok = rlp.listElem(1).toBytes()
|
let tok = rlp.listElem(1).toBytes()
|
||||||
d.kademlia.recvPong(node, tok)
|
d.kademlia.recvPong(node, tok)
|
||||||
|
|
||||||
proc recvNeighbours(d: DiscoveryProtocol, node: Node,
|
proc recvNeighbours(d: DiscoveryProtocol, node: Node, payload: seq[byte])
|
||||||
payload: seq[byte]) {.inline.} =
|
{.raises: [RlpError, Defect].} =
|
||||||
let rlp = rlpFromBytes(payload)
|
let rlp = rlpFromBytes(payload)
|
||||||
let neighboursList = rlp.listElem(0)
|
let neighboursList = rlp.listElem(0)
|
||||||
let sz = neighboursList.listLen()
|
let sz = neighboursList.listLen()
|
||||||
|
@ -206,7 +207,8 @@ proc recvNeighbours(d: DiscoveryProtocol, node: Node,
|
||||||
neighbours.add(newNode(pk[], Address(ip: ip, udpPort: udpPort, tcpPort: tcpPort)))
|
neighbours.add(newNode(pk[], Address(ip: ip, udpPort: udpPort, tcpPort: tcpPort)))
|
||||||
d.kademlia.recvNeighbours(node, neighbours)
|
d.kademlia.recvNeighbours(node, neighbours)
|
||||||
|
|
||||||
proc recvFindNode(d: DiscoveryProtocol, node: Node, payload: openArray[byte]) {.inline, gcsafe.} =
|
proc recvFindNode(d: DiscoveryProtocol, node: Node, payload: openArray[byte])
|
||||||
|
{.raises: [RlpError, ValueError, Defect].} =
|
||||||
let rlp = rlpFromBytes(payload)
|
let rlp = rlpFromBytes(payload)
|
||||||
trace "<<< find_node from ", node
|
trace "<<< find_node from ", node
|
||||||
let rng = rlp.listElem(0).toBytes
|
let rng = rlp.listElem(0).toBytes
|
||||||
|
@ -218,7 +220,7 @@ proc recvFindNode(d: DiscoveryProtocol, node: Node, payload: openArray[byte]) {.
|
||||||
trace "Invalid target public key received"
|
trace "Invalid target public key received"
|
||||||
|
|
||||||
proc expirationValid(cmdId: CommandId, rlpEncodedPayload: openArray[byte]):
|
proc expirationValid(cmdId: CommandId, rlpEncodedPayload: openArray[byte]):
|
||||||
bool {.inline, raises:[DiscProtocolError, RlpError].} =
|
bool {.raises: [DiscProtocolError, RlpError].} =
|
||||||
## Can only raise `DiscProtocolError` and all of `RlpError`
|
## Can only raise `DiscProtocolError` and all of `RlpError`
|
||||||
# Check if there is a payload
|
# Check if there is a payload
|
||||||
if rlpEncodedPayload.len <= 0:
|
if rlpEncodedPayload.len <= 0:
|
||||||
|
@ -233,8 +235,8 @@ proc expirationValid(cmdId: CommandId, rlpEncodedPayload: openArray[byte]):
|
||||||
else:
|
else:
|
||||||
raise newException(DiscProtocolError, "Invalid RLP list for this packet id")
|
raise newException(DiscProtocolError, "Invalid RLP list for this packet id")
|
||||||
|
|
||||||
proc receive*(d: DiscoveryProtocol, a: Address, msg: openArray[byte]) {.gcsafe.} =
|
proc receive*(d: DiscoveryProtocol, a: Address, msg: openArray[byte])
|
||||||
## Can raise `DiscProtocolError` and all of `RlpError`
|
{.raises: [DiscProtocolError, RlpError, ValueError, Defect].} =
|
||||||
# Note: export only needed for testing
|
# Note: export only needed for testing
|
||||||
let msgHash = validateMsgHash(msg)
|
let msgHash = validateMsgHash(msg)
|
||||||
if msgHash.isOk():
|
if msgHash.isOk():
|
||||||
|
@ -260,36 +262,36 @@ proc receive*(d: DiscoveryProtocol, a: Address, msg: openArray[byte]) {.gcsafe.}
|
||||||
else:
|
else:
|
||||||
notice "Wrong msg mac from ", a
|
notice "Wrong msg mac from ", a
|
||||||
|
|
||||||
proc processClient(transp: DatagramTransport,
|
proc processClient(transp: DatagramTransport, raddr: TransportAddress):
|
||||||
raddr: TransportAddress): Future[void] {.async, gcsafe.} =
|
Future[void] {.async, raises: [Defect].} =
|
||||||
var proto = getUserData[DiscoveryProtocol](transp)
|
var proto = getUserData[DiscoveryProtocol](transp)
|
||||||
try:
|
let buf = try: transp.getMessage()
|
||||||
# TODO: Maybe here better to use `peekMessage()` to avoid allocation,
|
except TransportOsError as e:
|
||||||
# but `Bytes` object is just a simple seq[byte], and `ByteRange` object
|
# This is likely to be local network connection issues.
|
||||||
# do not support custom length.
|
warn "Transport getMessage", exception = e.name, msg = e.msg
|
||||||
var buf = transp.getMessage()
|
return
|
||||||
let a = Address(ip: raddr.address, udpPort: raddr.port, tcpPort: raddr.port)
|
let a = Address(ip: raddr.address, udpPort: raddr.port, tcpPort: raddr.port)
|
||||||
|
try:
|
||||||
proto.receive(a, buf)
|
proto.receive(a, buf)
|
||||||
except RlpError as e:
|
except RlpError as e:
|
||||||
debug "Receive failed", exc = e.name, err = e.msg
|
debug "Receive failed", exc = e.name, err = e.msg
|
||||||
except DiscProtocolError as e:
|
except DiscProtocolError as e:
|
||||||
debug "Receive failed", exc = e.name, err = e.msg
|
debug "Receive failed", exc = e.name, err = e.msg
|
||||||
except Exception as e:
|
except ValueError as e:
|
||||||
debug "Receive failed", exc = e.name, err = e.msg
|
debug "Receive failed", exc = e.name, err = e.msg
|
||||||
raise e
|
|
||||||
|
|
||||||
proc open*(d: DiscoveryProtocol) =
|
proc open*(d: DiscoveryProtocol) {.raises: [Defect, CatchableError].} =
|
||||||
# TODO allow binding to specific IP / IPv6 / etc
|
# TODO allow binding to specific IP / IPv6 / etc
|
||||||
let ta = initTAddress(IPv4_any(), d.address.udpPort)
|
let ta = initTAddress(IPv4_any(), d.address.udpPort)
|
||||||
d.transp = newDatagramTransport(processClient, udata = d, local = ta)
|
d.transp = newDatagramTransport(processClient, udata = d, local = ta)
|
||||||
|
|
||||||
proc lookupRandom*(d: DiscoveryProtocol): Future[seq[Node]] {.inline.} =
|
proc lookupRandom*(d: DiscoveryProtocol): Future[seq[Node]] =
|
||||||
d.kademlia.lookupRandom()
|
d.kademlia.lookupRandom()
|
||||||
|
|
||||||
proc run(d: DiscoveryProtocol) {.async.} =
|
proc run(d: DiscoveryProtocol) {.async.} =
|
||||||
while true:
|
while true:
|
||||||
discard await d.lookupRandom()
|
discard await d.lookupRandom()
|
||||||
await sleepAsync(3000)
|
await sleepAsync(chronos.seconds(3))
|
||||||
trace "Discovered nodes", nodes = d.kademlia.nodesDiscovered
|
trace "Discovered nodes", nodes = d.kademlia.nodesDiscovered
|
||||||
|
|
||||||
proc bootstrap*(d: DiscoveryProtocol) {.async.} =
|
proc bootstrap*(d: DiscoveryProtocol) {.async.} =
|
||||||
|
@ -299,7 +301,7 @@ proc bootstrap*(d: DiscoveryProtocol) {.async.} =
|
||||||
proc resolve*(d: DiscoveryProtocol, n: NodeId): Future[Node] =
|
proc resolve*(d: DiscoveryProtocol, n: NodeId): Future[Node] =
|
||||||
d.kademlia.resolve(n)
|
d.kademlia.resolve(n)
|
||||||
|
|
||||||
proc randomNodes*(d: DiscoveryProtocol, count: int): seq[Node] {.inline.} =
|
proc randomNodes*(d: DiscoveryProtocol, count: int): seq[Node] =
|
||||||
d.kademlia.randomNodes(count)
|
d.kademlia.randomNodes(count)
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
|
|
|
@ -1,8 +1,18 @@
|
||||||
|
# nim-eth - Node Discovery Protocol v5
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
#
|
||||||
## Discovery v5 packet encoding as specified at
|
## Discovery v5 packet encoding as specified at
|
||||||
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#packet-encoding
|
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#packet-encoding
|
||||||
## And handshake/sessions as specified at
|
## And handshake/sessions as specified at
|
||||||
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#sessions
|
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#sessions
|
||||||
##
|
##
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[tables, options, hashes, net],
|
std/[tables, options, hashes, net],
|
||||||
nimcrypto, stint, chronicles, bearssl, stew/[results, byteutils],
|
nimcrypto, stint, chronicles, bearssl, stew/[results, byteutils],
|
||||||
|
@ -13,8 +23,6 @@ from stew/objects import checkedEnumAssign
|
||||||
|
|
||||||
export keys
|
export keys
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
logScope:
|
logScope:
|
||||||
topics = "discv5"
|
topics = "discv5"
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
# ENR implementation according to specification in EIP-778:
|
# nim-eth - Node Discovery Protocol v5
|
||||||
# https://github.com/ethereum/EIPs/blob/master/EIPS/eip-778.md
|
# Copyright (c) 2020-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.
|
||||||
|
#
|
||||||
|
## ENR implementation according to specification in EIP-778:
|
||||||
|
## https://github.com/ethereum/EIPs/blob/master/EIPS/eip-778.md
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[strutils, macros, algorithm, options],
|
std/[strutils, macros, algorithm, options],
|
||||||
|
@ -8,8 +17,6 @@ import
|
||||||
|
|
||||||
export options
|
export options
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
const
|
const
|
||||||
maxEnrSize = 300 ## Maximum size of an encoded node record, in bytes.
|
maxEnrSize = 300 ## Maximum size of an encoded node record, in bytes.
|
||||||
minRlpListLen = 4 ## Minimum node record RLP list has: signature, seqId,
|
minRlpListLen = 4 ## Minimum node record RLP list has: signature, seqId,
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
# nim-eth - Node Discovery Protocol v5
|
# nim-eth - Node Discovery Protocol v5
|
||||||
# Copyright (c) 2021 Status Research & Development GmbH
|
# Copyright (c) 2021 Status Research & Development GmbH
|
||||||
# Licensed under either of
|
# Licensed and distributed under either of
|
||||||
# * Apache License, version 2.0, (LICENSE-APACHEv2)
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
||||||
# * MIT license (LICENSE-MIT)
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
||||||
# at your option.
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
# This file may not be copied, modified, or distributed except
|
#
|
||||||
# according to those terms.
|
|
||||||
|
|
||||||
## IP:port address votes implemented similarly as in
|
## IP:port address votes implemented similarly as in
|
||||||
## https://github.com/sigp/discv5
|
## https://github.com/sigp/discv5
|
||||||
##
|
##
|
||||||
|
@ -17,6 +15,8 @@
|
||||||
## To select the right address, a majority count is done. This is done over a
|
## To select the right address, a majority count is done. This is done over a
|
||||||
## sort of moving window as votes expire after `IpVoteTimeout`.
|
## sort of moving window as votes expire after `IpVoteTimeout`.
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[tables, options],
|
std/[tables, options],
|
||||||
chronos,
|
chronos,
|
||||||
|
@ -24,8 +24,6 @@ import
|
||||||
|
|
||||||
export options
|
export options
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
const IpVoteTimeout = 5.minutes ## Duration until a vote expires
|
const IpVoteTimeout = 5.minutes ## Duration until a vote expires
|
||||||
|
|
||||||
type
|
type
|
||||||
|
|
|
@ -1,14 +1,22 @@
|
||||||
|
# nim-eth - Node Discovery Protocol v5
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
#
|
||||||
## Discovery v5 Protocol Messages as specified at
|
## Discovery v5 Protocol Messages as specified at
|
||||||
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#protocol-messages
|
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-wire.md#protocol-messages
|
||||||
## These messages get RLP encoded.
|
## These messages get RLP encoded.
|
||||||
##
|
##
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[hashes, net],
|
std/[hashes, net],
|
||||||
stew/arrayops,
|
stew/arrayops,
|
||||||
../../rlp, ./enr
|
../../rlp, ./enr
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
type
|
type
|
||||||
MessageKind* = enum
|
MessageKind* = enum
|
||||||
# TODO This is needed only to make Nim 1.2.6 happy
|
# TODO This is needed only to make Nim 1.2.6 happy
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
# nim-eth - Node Discovery Protocol v5
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/hashes,
|
std/hashes,
|
||||||
nimcrypto, stint, chronos, stew/shims/net, chronicles,
|
nimcrypto, stint, chronos, stew/shims/net, chronicles,
|
||||||
|
@ -5,8 +14,6 @@ import
|
||||||
|
|
||||||
export stint
|
export stint
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
type
|
type
|
||||||
NodeId* = UInt256
|
NodeId* = UInt256
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
# nim-eth - Node Discovery Protocol v5
|
# nim-eth - Node Discovery Protocol v5
|
||||||
# Copyright (c) 2020-2021 Status Research & Development GmbH
|
# Copyright (c) 2020-2021 Status Research & Development GmbH
|
||||||
# Licensed under either of
|
# Licensed and distributed under either of
|
||||||
# * Apache License, version 2.0, (LICENSE-APACHEv2)
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
||||||
# * MIT license (LICENSE-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
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
# according to those terms.
|
|
||||||
|
|
||||||
## Node Discovery Protocol v5
|
## Node Discovery Protocol v5
|
||||||
##
|
##
|
||||||
|
@ -72,6 +71,8 @@
|
||||||
## more requests will be needed for a lookup (adding bandwidth and latency).
|
## more requests will be needed for a lookup (adding bandwidth and latency).
|
||||||
## This might be a concern for mobile devices.
|
## This might be a concern for mobile devices.
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[tables, sets, options, math, sequtils, algorithm],
|
std/[tables, sets, options, math, sequtils, algorithm],
|
||||||
stew/shims/net as stewNet, json_serialization/std/net,
|
stew/shims/net as stewNet, json_serialization/std/net,
|
||||||
|
@ -83,8 +84,6 @@ import nimcrypto except toHex
|
||||||
|
|
||||||
export options
|
export options
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
declareCounter discovery_message_requests_outgoing,
|
declareCounter discovery_message_requests_outgoing,
|
||||||
"Discovery protocol outgoing message requests", labels = ["response"]
|
"Discovery protocol outgoing message requests", labels = ["response"]
|
||||||
declareCounter discovery_message_requests_incoming,
|
declareCounter discovery_message_requests_incoming,
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
# nim-eth - Node Discovery Protocol v5
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[algorithm, times, sequtils, bitops, sets, options],
|
std/[algorithm, times, sequtils, bitops, sets, options],
|
||||||
stint, chronicles, metrics, bearssl, chronos, stew/shims/net as stewNet,
|
stint, chronicles, metrics, bearssl, chronos, stew/shims/net as stewNet,
|
||||||
|
@ -6,8 +15,6 @@ import
|
||||||
|
|
||||||
export options
|
export options
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
declarePublicGauge routing_table_nodes,
|
declarePublicGauge routing_table_nodes,
|
||||||
"Discovery routing table nodes", labels = ["state"]
|
"Discovery routing table nodes", labels = ["state"]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,16 @@
|
||||||
|
# nim-eth - Node Discovery Protocol v5
|
||||||
|
# Copyright (c) 2020-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.
|
||||||
|
#
|
||||||
## Session cache as mentioned at
|
## Session cache as mentioned at
|
||||||
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#session-cache
|
## https://github.com/ethereum/devp2p/blob/master/discv5/discv5-theory.md#session-cache
|
||||||
##
|
##
|
||||||
|
|
||||||
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/options,
|
std/options,
|
||||||
stint, stew/endians2, stew/shims/net,
|
stint, stew/endians2, stew/shims/net,
|
||||||
|
@ -8,8 +18,6 @@ import
|
||||||
|
|
||||||
export lru
|
export lru
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
|
||||||
|
|
||||||
const
|
const
|
||||||
aesKeySize* = 128 div 8
|
aesKeySize* = 128 div 8
|
||||||
keySize = sizeof(NodeId) +
|
keySize = sizeof(NodeId) +
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
#
|
# nim-eth
|
||||||
# Ethereum P2P
|
# Copyright (c) 2018-2021 Status Research & Development GmbH
|
||||||
# (c) Copyright 2018
|
# Licensed and distributed under either of
|
||||||
# Status Research & Development GmbH
|
# * 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).
|
||||||
# Licensed under either of
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
||||||
# MIT license (LICENSE-MIT)
|
{.push raises: [Defect].}
|
||||||
#
|
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[tables, hashes, times, algorithm, sets, sequtils, random],
|
std/[tables, hashes, times, algorithm, sets, sequtils, random],
|
||||||
|
@ -26,7 +25,7 @@ type
|
||||||
routing: RoutingTable
|
routing: RoutingTable
|
||||||
pongFutures: Table[seq[byte], Future[bool]]
|
pongFutures: Table[seq[byte], Future[bool]]
|
||||||
pingFutures: Table[Node, Future[bool]]
|
pingFutures: Table[Node, Future[bool]]
|
||||||
neighboursCallbacks: Table[Node, proc(n: seq[Node]) {.gcsafe.}]
|
neighboursCallbacks: Table[Node, proc(n: seq[Node]) {.gcsafe, raises: [Defect].}]
|
||||||
rng: ref BrHmacDrbgContext
|
rng: ref BrHmacDrbgContext
|
||||||
|
|
||||||
NodeId* = UInt256
|
NodeId* = UInt256
|
||||||
|
@ -79,7 +78,8 @@ proc `$`*(n: Node): string =
|
||||||
"Node[" & $n.node.address.ip & ":" & $n.node.address.udpPort & "]"
|
"Node[" & $n.node.address.ip & ":" & $n.node.address.udpPort & "]"
|
||||||
|
|
||||||
proc hash*(n: Node): hashes.Hash = hash(n.node.pubkey.toRaw)
|
proc hash*(n: Node): hashes.Hash = hash(n.node.pubkey.toRaw)
|
||||||
proc `==`*(a, b: Node): bool = (a.isNil and b.isNil) or (not a.isNil and not b.isNil and a.node.pubkey == b.node.pubkey)
|
proc `==`*(a, b: Node): bool = (a.isNil and b.isNil) or
|
||||||
|
(not a.isNil and not b.isNil and a.node.pubkey == b.node.pubkey)
|
||||||
|
|
||||||
proc newKBucket(istart, iend: NodeId): KBucket =
|
proc newKBucket(istart, iend: NodeId): KBucket =
|
||||||
result.new()
|
result.new()
|
||||||
|
@ -95,8 +95,8 @@ proc distanceTo(k: KBucket, id: NodeId): UInt256 = k.midpoint xor id
|
||||||
proc nodesByDistanceTo(k: KBucket, id: NodeId): seq[Node] =
|
proc nodesByDistanceTo(k: KBucket, id: NodeId): seq[Node] =
|
||||||
sortedByIt(k.nodes, it.distanceTo(id))
|
sortedByIt(k.nodes, it.distanceTo(id))
|
||||||
|
|
||||||
proc len(k: KBucket): int {.inline.} = k.nodes.len
|
proc len(k: KBucket): int = k.nodes.len
|
||||||
proc head(k: KBucket): Node {.inline.} = k.nodes[0]
|
proc head(k: KBucket): Node = k.nodes[0]
|
||||||
|
|
||||||
proc add(k: KBucket, n: Node): Node =
|
proc add(k: KBucket, n: Node): Node =
|
||||||
## Try to add the given node to this bucket.
|
## Try to add the given node to this bucket.
|
||||||
|
@ -137,15 +137,15 @@ proc split(k: KBucket): tuple[lower, upper: KBucket] =
|
||||||
let bucket = if node.id <= splitid: result.lower else: result.upper
|
let bucket = if node.id <= splitid: result.lower else: result.upper
|
||||||
bucket.replacementCache.add(node)
|
bucket.replacementCache.add(node)
|
||||||
|
|
||||||
proc inRange(k: KBucket, n: Node): bool {.inline.} =
|
proc inRange(k: KBucket, n: Node): bool =
|
||||||
k.istart <= n.id and n.id <= k.iend
|
k.istart <= n.id and n.id <= k.iend
|
||||||
|
|
||||||
proc isFull(k: KBucket): bool = k.len == BUCKET_SIZE
|
proc isFull(k: KBucket): bool = k.len == BUCKET_SIZE
|
||||||
|
|
||||||
proc contains(k: KBucket, n: Node): bool = n in k.nodes
|
proc contains(k: KBucket, n: Node): bool = n in k.nodes
|
||||||
|
|
||||||
proc binaryGetBucketForNode(buckets: openarray[KBucket],
|
proc binaryGetBucketForNode(buckets: openarray[KBucket], n: Node):
|
||||||
n: Node): KBucket {.inline.} =
|
KBucket {.raises: [ValueError, Defect].} =
|
||||||
## Given a list of ordered buckets, returns the bucket for a given node.
|
## Given a list of ordered buckets, returns the bucket for a given node.
|
||||||
let bucketPos = lowerBound(buckets, n.id) do(a: KBucket, b: NodeId) -> int:
|
let bucketPos = lowerBound(buckets, n.id) do(a: KBucket, b: NodeId) -> int:
|
||||||
cmp(a.iend, b)
|
cmp(a.iend, b)
|
||||||
|
@ -174,7 +174,7 @@ proc computeSharedPrefixBits(nodes: openarray[Node]): int =
|
||||||
|
|
||||||
doAssert(false, "Unable to calculate number of shared prefix bits")
|
doAssert(false, "Unable to calculate number of shared prefix bits")
|
||||||
|
|
||||||
proc init(r: var RoutingTable, thisNode: Node) {.inline.} =
|
proc init(r: var RoutingTable, thisNode: Node) =
|
||||||
r.thisNode = thisNode
|
r.thisNode = thisNode
|
||||||
r.buckets = @[newKBucket(0.u256, high(Uint256))]
|
r.buckets = @[newKBucket(0.u256, high(Uint256))]
|
||||||
randomize() # for later `randomNodes` selection
|
randomize() # for later `randomNodes` selection
|
||||||
|
@ -185,13 +185,15 @@ proc splitBucket(r: var RoutingTable, index: int) =
|
||||||
r.buckets[index] = a
|
r.buckets[index] = a
|
||||||
r.buckets.insert(b, index + 1)
|
r.buckets.insert(b, index + 1)
|
||||||
|
|
||||||
proc bucketForNode(r: RoutingTable, n: Node): KBucket =
|
proc bucketForNode(r: RoutingTable, n: Node): KBucket
|
||||||
|
{.raises: [ValueError, Defect].} =
|
||||||
binaryGetBucketForNode(r.buckets, n)
|
binaryGetBucketForNode(r.buckets, n)
|
||||||
|
|
||||||
proc removeNode(r: var RoutingTable, n: Node) =
|
proc removeNode(r: var RoutingTable, n: Node) {.raises: [ValueError, Defect].} =
|
||||||
r.bucketForNode(n).removeNode(n)
|
r.bucketForNode(n).removeNode(n)
|
||||||
|
|
||||||
proc addNode(r: var RoutingTable, n: Node): Node =
|
proc addNode(r: var RoutingTable, n: Node): Node
|
||||||
|
{.raises: [ValueError, Defect].} =
|
||||||
if n == r.thisNode:
|
if n == r.thisNode:
|
||||||
warn "Trying to add ourselves to the routing table", node = n
|
warn "Trying to add ourselves to the routing table", node = n
|
||||||
return
|
return
|
||||||
|
@ -209,7 +211,8 @@ proc addNode(r: var RoutingTable, n: Node): Node =
|
||||||
# Nothing added, ping evictionCandidate
|
# Nothing added, ping evictionCandidate
|
||||||
return evictionCandidate
|
return evictionCandidate
|
||||||
|
|
||||||
proc contains(r: RoutingTable, n: Node): bool = n in r.bucketForNode(n)
|
proc contains(r: RoutingTable, n: Node): bool {.raises: [ValueError, Defect].} =
|
||||||
|
n in r.bucketForNode(n)
|
||||||
|
|
||||||
proc bucketsByDistanceTo(r: RoutingTable, id: NodeId): seq[KBucket] =
|
proc bucketsByDistanceTo(r: RoutingTable, id: NodeId): seq[KBucket] =
|
||||||
sortedByIt(r.buckets, it.distanceTo(id))
|
sortedByIt(r.buckets, it.distanceTo(id))
|
||||||
|
@ -243,9 +246,11 @@ proc newKademliaProtocol*[Wire](
|
||||||
result.routing.init(thisNode)
|
result.routing.init(thisNode)
|
||||||
result.rng = rng
|
result.rng = rng
|
||||||
|
|
||||||
proc bond(k: KademliaProtocol, n: Node): Future[bool] {.async, gcsafe.}
|
proc bond(k: KademliaProtocol, n: Node): Future[bool] {.async.}
|
||||||
|
proc bondDiscard(k: KademliaProtocol, n: Node) {.async.}
|
||||||
|
|
||||||
proc updateRoutingTable(k: KademliaProtocol, n: Node) {.gcsafe.} =
|
proc updateRoutingTable(k: KademliaProtocol, n: Node)
|
||||||
|
{.raises: [ValueError, Defect].} =
|
||||||
## Update the routing table entry for the given node.
|
## Update the routing table entry for the given node.
|
||||||
let evictionCandidate = k.routing.addNode(n)
|
let evictionCandidate = k.routing.addNode(n)
|
||||||
if not evictionCandidate.isNil:
|
if not evictionCandidate.isNil:
|
||||||
|
@ -253,17 +258,17 @@ proc updateRoutingTable(k: KademliaProtocol, n: Node) {.gcsafe.} =
|
||||||
# with the least recently seen node on that bucket. If the bonding fails the node will
|
# with the least recently seen node on that bucket. If the bonding fails the node will
|
||||||
# be removed from the bucket and a new one will be picked from the bucket's
|
# be removed from the bucket and a new one will be picked from the bucket's
|
||||||
# replacement cache.
|
# replacement cache.
|
||||||
asyncCheck k.bond(evictionCandidate)
|
asyncSpawn k.bondDiscard(evictionCandidate)
|
||||||
|
|
||||||
proc doSleep(p: proc() {.gcsafe.}) {.async, gcsafe.} =
|
proc doSleep(p: proc() {.gcsafe, raises: [Defect].}) {.async.} =
|
||||||
await sleepAsync(REQUEST_TIMEOUT)
|
await sleepAsync(REQUEST_TIMEOUT)
|
||||||
p()
|
p()
|
||||||
|
|
||||||
template onTimeout(b: untyped) =
|
template onTimeout(b: untyped) =
|
||||||
asyncCheck doSleep() do():
|
asyncSpawn doSleep() do():
|
||||||
b
|
b
|
||||||
|
|
||||||
proc pingId(n: Node, token: seq[byte]): seq[byte] {.inline.} =
|
proc pingId(n: Node, token: seq[byte]): seq[byte] =
|
||||||
result = token & @(n.node.pubkey.toRaw)
|
result = token & @(n.node.pubkey.toRaw)
|
||||||
|
|
||||||
proc waitPong(k: KademliaProtocol, n: Node, pingid: seq[byte]): Future[bool] =
|
proc waitPong(k: KademliaProtocol, n: Node, pingid: seq[byte]): Future[bool] =
|
||||||
|
@ -280,7 +285,7 @@ proc ping(k: KademliaProtocol, n: Node): seq[byte] =
|
||||||
doAssert(n != k.thisNode)
|
doAssert(n != k.thisNode)
|
||||||
result = k.wire.sendPing(n)
|
result = k.wire.sendPing(n)
|
||||||
|
|
||||||
proc waitPing(k: KademliaProtocol, n: Node): Future[bool] {.gcsafe.} =
|
proc waitPing(k: KademliaProtocol, n: Node): Future[bool] =
|
||||||
result = newFuture[bool]("waitPing")
|
result = newFuture[bool]("waitPing")
|
||||||
doAssert(n notin k.pingFutures)
|
doAssert(n notin k.pingFutures)
|
||||||
k.pingFutures[n] = result
|
k.pingFutures[n] = result
|
||||||
|
@ -290,12 +295,13 @@ proc waitPing(k: KademliaProtocol, n: Node): Future[bool] {.gcsafe.} =
|
||||||
k.pingFutures.del(n)
|
k.pingFutures.del(n)
|
||||||
fut.complete(false)
|
fut.complete(false)
|
||||||
|
|
||||||
proc waitNeighbours(k: KademliaProtocol, remote: Node): Future[seq[Node]] =
|
proc waitNeighbours(k: KademliaProtocol, remote: Node):
|
||||||
|
Future[seq[Node]] {.raises: [Defect].} =
|
||||||
doAssert(remote notin k.neighboursCallbacks)
|
doAssert(remote notin k.neighboursCallbacks)
|
||||||
result = newFuture[seq[Node]]("waitNeighbours")
|
result = newFuture[seq[Node]]("waitNeighbours")
|
||||||
let fut = result
|
let fut = result
|
||||||
var neighbours = newSeqOfCap[Node](BUCKET_SIZE)
|
var neighbours = newSeqOfCap[Node](BUCKET_SIZE)
|
||||||
k.neighboursCallbacks[remote] = proc(n: seq[Node]) =
|
k.neighboursCallbacks[remote] = proc(n: seq[Node]) {.gcsafe, raises: [Defect].} =
|
||||||
# This callback is expected to be called multiple times because nodes usually
|
# This callback is expected to be called multiple times because nodes usually
|
||||||
# split the neighbours replies into multiple packets, so we only complete the
|
# split the neighbours replies into multiple packets, so we only complete the
|
||||||
# future event.set() we've received enough neighbours.
|
# future event.set() we've received enough neighbours.
|
||||||
|
@ -336,9 +342,21 @@ proc findNode*(k: KademliaProtocol, nodesSeen: ref HashSet[Node],
|
||||||
# 3. Deduplicates candidates
|
# 3. Deduplicates candidates
|
||||||
candidates.keepItIf(not nodesSeen[].containsOrIncl(it))
|
candidates.keepItIf(not nodesSeen[].containsOrIncl(it))
|
||||||
trace "Got new candidates", count = candidates.len
|
trace "Got new candidates", count = candidates.len
|
||||||
let bonded = await all(candidates.mapIt(k.bond(it)))
|
|
||||||
for i in 0 ..< bonded.len:
|
var bondedNodes: seq[Future[bool]] = @[]
|
||||||
if not bonded[i]: candidates[i] = nil
|
for node in candidates:
|
||||||
|
bondedNodes.add(k.bond(node))
|
||||||
|
|
||||||
|
await allFutures(bondedNodes)
|
||||||
|
|
||||||
|
for i in 0..<bondedNodes.len:
|
||||||
|
let b = bondedNodes[i]
|
||||||
|
# `bond` will not raise so there should be no failures,
|
||||||
|
# and for cancellation this should be fine to raise for now.
|
||||||
|
doAssert(b.finished() and not(b.failed()))
|
||||||
|
let bonded = b.read()
|
||||||
|
if not bonded: candidates[i] = nil
|
||||||
|
|
||||||
candidates.keepItIf(not it.isNil)
|
candidates.keepItIf(not it.isNil)
|
||||||
trace "Bonded with candidates", count = candidates.len
|
trace "Bonded with candidates", count = candidates.len
|
||||||
result = candidates
|
result = candidates
|
||||||
|
@ -350,9 +368,9 @@ proc populateNotFullBuckets(k: KademliaProtocol) =
|
||||||
## When the bonding succeeds the node is automatically added to the bucket.
|
## When the bonding succeeds the node is automatically added to the bucket.
|
||||||
for bucket in k.routing.notFullBuckets:
|
for bucket in k.routing.notFullBuckets:
|
||||||
for node in bucket.replacementCache:
|
for node in bucket.replacementCache:
|
||||||
asyncCheck k.bond(node)
|
asyncSpawn k.bondDiscard(node)
|
||||||
|
|
||||||
proc bond(k: KademliaProtocol, n: Node): Future[bool] {.async, gcsafe.} =
|
proc bond(k: KademliaProtocol, n: Node): Future[bool] {.async.} =
|
||||||
## Bond with the given node.
|
## Bond with the given node.
|
||||||
##
|
##
|
||||||
## Bonding consists of pinging the node, waiting for a pong and maybe a ping as well.
|
## Bonding consists of pinging the node, waiting for a pong and maybe a ping as well.
|
||||||
|
@ -388,6 +406,9 @@ proc bond(k: KademliaProtocol, n: Node): Future[bool] {.async, gcsafe.} =
|
||||||
k.updateRoutingTable(n)
|
k.updateRoutingTable(n)
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
proc bondDiscard(k: KademliaProtocol, n: Node) {.async.} =
|
||||||
|
discard (await k.bond(n))
|
||||||
|
|
||||||
proc sortByDistance(nodes: var seq[Node], nodeId: NodeId, maxResults = 0) =
|
proc sortByDistance(nodes: var seq[Node], nodeId: NodeId, maxResults = 0) =
|
||||||
nodes = nodes.sortedByIt(it.distanceTo(nodeId))
|
nodes = nodes.sortedByIt(it.distanceTo(nodeId))
|
||||||
if maxResults != 0 and nodes.len > maxResults:
|
if maxResults != 0 and nodes.len > maxResults:
|
||||||
|
@ -411,9 +432,19 @@ proc lookup*(k: KademliaProtocol, nodeId: NodeId): Future[seq[Node]] {.async.} =
|
||||||
while nodesToAsk.len != 0:
|
while nodesToAsk.len != 0:
|
||||||
trace "Node lookup; querying ", nodesToAsk
|
trace "Node lookup; querying ", nodesToAsk
|
||||||
nodesAsked.incl(nodesToAsk.toHashSet())
|
nodesAsked.incl(nodesToAsk.toHashSet())
|
||||||
let results = await all(nodesToAsk.mapIt(k.findNode(nodesSeen, nodeId, it)))
|
|
||||||
for candidates in results:
|
var findNodeRequests: seq[Future[seq[Node]]] = @[]
|
||||||
closest.add(candidates)
|
for node in nodesToAsk:
|
||||||
|
findNodeRequests.add(k.findNode(nodesSeen, nodeId, node))
|
||||||
|
|
||||||
|
await allFutures(findNodeRequests)
|
||||||
|
|
||||||
|
for candidates in findNodeRequests:
|
||||||
|
# `findNode` will not raise so there should be no failures,
|
||||||
|
# and for cancellation this should be fine to raise for now.
|
||||||
|
doAssert(candidates.finished() and not(candidates.failed()))
|
||||||
|
closest.add(candidates.read())
|
||||||
|
|
||||||
sortByDistance(closest, nodeId, BUCKET_SIZE)
|
sortByDistance(closest, nodeId, BUCKET_SIZE)
|
||||||
nodesToAsk = excludeIfAsked(closest)
|
nodesToAsk = excludeIfAsked(closest)
|
||||||
|
|
||||||
|
@ -440,7 +471,15 @@ proc bootstrap*(k: KademliaProtocol, bootstrapNodes: seq[Node], retries = 0) {.a
|
||||||
var numTries = 0
|
var numTries = 0
|
||||||
if bootstrapNodes.len != 0:
|
if bootstrapNodes.len != 0:
|
||||||
while true:
|
while true:
|
||||||
let bonded = await all(bootstrapNodes.mapIt(k.bond(it)))
|
var bondedNodes: seq[Future[bool]] = @[]
|
||||||
|
for node in bootstrapNodes:
|
||||||
|
bondedNodes.add(k.bond(node))
|
||||||
|
await allFutures(bondedNodes)
|
||||||
|
|
||||||
|
# `bond` will not raise so there should be no failures,
|
||||||
|
# and for cancellation this should be fine to raise for now.
|
||||||
|
let bonded = bondedNodes.mapIt(it.read())
|
||||||
|
|
||||||
if true notin bonded:
|
if true notin bonded:
|
||||||
inc numTries
|
inc numTries
|
||||||
if retries == 0 or numTries < retries:
|
if retries == 0 or numTries < retries:
|
||||||
|
@ -463,7 +502,8 @@ proc recvPong*(k: KademliaProtocol, n: Node, token: seq[byte]) =
|
||||||
if k.pongFutures.take(pingid, future):
|
if k.pongFutures.take(pingid, future):
|
||||||
future.complete(true)
|
future.complete(true)
|
||||||
|
|
||||||
proc recvPing*(k: KademliaProtocol, n: Node, msgHash: any) =
|
proc recvPing*(k: KademliaProtocol, n: Node, msgHash: any)
|
||||||
|
{.raises: [ValueError, Defect].} =
|
||||||
trace "<<< ping from ", n
|
trace "<<< ping from ", n
|
||||||
k.updateRoutingTable(n)
|
k.updateRoutingTable(n)
|
||||||
k.wire.sendPong(n, msgHash)
|
k.wire.sendPong(n, msgHash)
|
||||||
|
@ -486,7 +526,8 @@ proc recvNeighbours*(k: KademliaProtocol, remote: Node, neighbours: seq[Node]) =
|
||||||
else:
|
else:
|
||||||
trace "Unexpected neighbours, probably came too late", remote
|
trace "Unexpected neighbours, probably came too late", remote
|
||||||
|
|
||||||
proc recvFindNode*(k: KademliaProtocol, remote: Node, nodeId: NodeId) {.gcsafe.} =
|
proc recvFindNode*(k: KademliaProtocol, remote: Node, nodeId: NodeId)
|
||||||
|
{.raises: [ValueError, Defect].} =
|
||||||
if remote notin k.routing:
|
if remote notin k.routing:
|
||||||
# FIXME: This is not correct; a node we've bonded before may have become unavailable
|
# FIXME: This is not correct; a node we've bonded before may have become unavailable
|
||||||
# and thus removed from self.routing, but once it's back online we should accept
|
# and thus removed from self.routing, but once it's back online we should accept
|
||||||
|
@ -520,7 +561,7 @@ proc randomNodes*(k: KademliaProtocol, count: int): seq[Node] =
|
||||||
result.add(node)
|
result.add(node)
|
||||||
seen.incl(node)
|
seen.incl(node)
|
||||||
|
|
||||||
proc nodesDiscovered*(k: KademliaProtocol): int {.inline.} = k.routing.len
|
proc nodesDiscovered*(k: KademliaProtocol): int = k.routing.len
|
||||||
|
|
||||||
when isMainModule:
|
when isMainModule:
|
||||||
proc randomNode(): Node =
|
proc randomNode(): Node =
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
./test_enr,
|
./test_enr,
|
||||||
./test_hkdf,
|
./test_hkdf,
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import
|
||||||
|
./all_discv5_tests,
|
||||||
|
./test_auth,
|
||||||
|
./test_crypt,
|
||||||
|
./test_discovery,
|
||||||
|
./test_ecies,
|
||||||
|
./test_enode
|
|
@ -10,12 +10,6 @@ proc localAddress*(port: int): Address =
|
||||||
result = Address(udpPort: port, tcpPort: port,
|
result = Address(udpPort: port, tcpPort: port,
|
||||||
ip: parseIpAddress("127.0.0.1"))
|
ip: parseIpAddress("127.0.0.1"))
|
||||||
|
|
||||||
proc startDiscoveryNode*(privKey: PrivateKey, address: Address,
|
|
||||||
bootnodes: seq[ENode]): Future[DiscoveryProtocol] {.async.} =
|
|
||||||
result = newDiscoveryProtocol(privKey, address, bootnodes)
|
|
||||||
result.open()
|
|
||||||
await result.bootstrap()
|
|
||||||
|
|
||||||
proc setupTestNode*(
|
proc setupTestNode*(
|
||||||
rng: ref BrHmacDrbgContext,
|
rng: ref BrHmacDrbgContext,
|
||||||
capabilities: varargs[ProtocolInfo, `protocolInfo`]): EthereumNode {.gcsafe.} =
|
capabilities: varargs[ProtocolInfo, `protocolInfo`]): EthereumNode {.gcsafe.} =
|
||||||
|
@ -27,13 +21,6 @@ proc setupTestNode*(
|
||||||
for capability in capabilities:
|
for capability in capabilities:
|
||||||
result.addCapability capability
|
result.addCapability capability
|
||||||
|
|
||||||
proc packData*(payload: openArray[byte], pk: PrivateKey): seq[byte] =
|
|
||||||
let
|
|
||||||
payloadSeq = @payload
|
|
||||||
signature = @(pk.sign(payload).toRaw())
|
|
||||||
msgHash = keccak256.digest(signature & payloadSeq)
|
|
||||||
result = @(msgHash.data) & signature & payloadSeq
|
|
||||||
|
|
||||||
template sourceDir*: string = currentSourcePath.rsplit(DirSep, 1)[0]
|
template sourceDir*: string = currentSourcePath.rsplit(DirSep, 1)[0]
|
||||||
|
|
||||||
proc recvMsgMock*(msg: openArray[byte]): tuple[msgId: int, msgData: Rlp] =
|
proc recvMsgMock*(msg: openArray[byte]): tuple[msgId: int, msgData: Rlp] =
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
# distribution, for details about the copyright.
|
# distribution, for details about the copyright.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/unittest,
|
std/unittest,
|
||||||
nimcrypto/[utils, keccak],
|
nimcrypto/[utils, keccak],
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
# distribution, for details about the copyright.
|
# distribution, for details about the copyright.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/unittest,
|
std/unittest,
|
||||||
nimcrypto/[utils, sysrand, keccak],
|
nimcrypto/[utils, sysrand, keccak],
|
||||||
|
|
|
@ -7,26 +7,46 @@
|
||||||
# distribution, for details about the copyright.
|
# distribution, for details about the copyright.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[sequtils, unittest],
|
std/sequtils,
|
||||||
chronos, stew/byteutils,
|
chronos, stew/byteutils, nimcrypto, testutils/unittests,
|
||||||
../../eth/[keys, rlp], ../../eth/p2p/[discovery, kademlia, enode],
|
../../eth/keys, ../../eth/p2p/[discovery, kademlia, enode]
|
||||||
./p2p_test_helper
|
|
||||||
|
proc localAddress(port: int): Address =
|
||||||
|
let port = Port(port)
|
||||||
|
result = Address(udpPort: port, tcpPort: port,
|
||||||
|
ip: parseIpAddress("127.0.0.1"))
|
||||||
|
|
||||||
|
proc initDiscoveryNode(privKey: PrivateKey, address: Address,
|
||||||
|
bootnodes: seq[ENode]): DiscoveryProtocol =
|
||||||
|
let node = newDiscoveryProtocol(privKey, address, bootnodes)
|
||||||
|
node.open()
|
||||||
|
|
||||||
|
return node
|
||||||
|
|
||||||
|
proc packData(payload: openArray[byte], pk: PrivateKey): seq[byte] =
|
||||||
|
let
|
||||||
|
payloadSeq = @payload
|
||||||
|
signature = @(pk.sign(payload).toRaw())
|
||||||
|
msgHash = keccak256.digest(signature & payloadSeq)
|
||||||
|
result = @(msgHash.data) & signature & payloadSeq
|
||||||
|
|
||||||
proc nodeIdInNodes(id: NodeId, nodes: openarray[Node]): bool =
|
proc nodeIdInNodes(id: NodeId, nodes: openarray[Node]): bool =
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
if id == n.id: return true
|
if id == n.id: return true
|
||||||
|
|
||||||
proc test() {.async.} =
|
procSuite "Discovery Tests":
|
||||||
suite "Discovery Tests":
|
|
||||||
let
|
let
|
||||||
bootNodeKey = PrivateKey.fromHex(
|
bootNodeKey = PrivateKey.fromHex(
|
||||||
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")[]
|
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")[]
|
||||||
bootNodeAddr = localAddress(20301)
|
bootNodeAddr = localAddress(20301)
|
||||||
bootENode = ENode(pubkey: bootNodeKey.toPublicKey(), address: bootNodeAddr)
|
bootENode = ENode(pubkey: bootNodeKey.toPublicKey(), address: bootNodeAddr)
|
||||||
bootNode = await startDiscoveryNode(bootNodeKey, bootNodeAddr, @[])
|
bootNode = initDiscoveryNode(bootNodeKey, bootNodeAddr, @[])
|
||||||
|
waitFor bootNode.bootstrap()
|
||||||
|
|
||||||
test "Discover nodes":
|
asyncTest "Discover nodes":
|
||||||
let nodeKeys = [
|
let nodeKeys = [
|
||||||
PrivateKey.fromHex(
|
PrivateKey.fromHex(
|
||||||
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")[],
|
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")[],
|
||||||
|
@ -35,12 +55,14 @@ proc test() {.async.} =
|
||||||
PrivateKey.fromHex(
|
PrivateKey.fromHex(
|
||||||
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a620")[]
|
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a620")[]
|
||||||
]
|
]
|
||||||
var nodeAddrs = newSeqOfCap[Address](nodeKeys.len)
|
|
||||||
for i in 0 ..< nodeKeys.len: nodeAddrs.add(localAddress(20302 + i))
|
|
||||||
|
|
||||||
var nodes = await all(zip(nodeKeys, nodeAddrs).mapIt(
|
var nodes: seq[DiscoveryProtocol]
|
||||||
startDiscoveryNode(it[0], it[1], @[bootENode]))
|
for i in 0..<nodeKeys.len:
|
||||||
)
|
let node = initDiscoveryNode(nodeKeys[i], localAddress(20302 + i),
|
||||||
|
@[bootENode])
|
||||||
|
nodes.add(node)
|
||||||
|
|
||||||
|
await allFutures(nodes.mapIt(it.bootstrap()))
|
||||||
nodes.add(bootNode)
|
nodes.add(bootNode)
|
||||||
|
|
||||||
for i in nodes:
|
for i in nodes:
|
||||||
|
@ -97,7 +119,7 @@ proc test() {.async.} =
|
||||||
# msg mac
|
# msg mac
|
||||||
bootNode.receive(address, packData(@[], nodeKey))
|
bootNode.receive(address, packData(@[], nodeKey))
|
||||||
|
|
||||||
test "Two findNode calls for the same peer in rapid succession":
|
asyncTest "Two findNode calls for the same peer in rapid succession":
|
||||||
let targetKey = PrivateKey.fromHex(
|
let targetKey = PrivateKey.fromHex(
|
||||||
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")[]
|
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")[]
|
||||||
let peerKey = PrivateKey.fromHex(
|
let peerKey = PrivateKey.fromHex(
|
||||||
|
@ -121,5 +143,3 @@ proc test() {.async.} =
|
||||||
# Just for completeness, wait for the first result out of order.
|
# Just for completeness, wait for the first result out of order.
|
||||||
# Max delay 5 seconds.
|
# Max delay 5 seconds.
|
||||||
discard await neighbours1Future
|
discard await neighbours1Future
|
||||||
|
|
||||||
waitFor test()
|
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
# distribution, for details about the copyright.
|
# distribution, for details about the copyright.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/unittest,
|
std/unittest,
|
||||||
nimcrypto/[utils, sha2, hmac, rijndael],
|
nimcrypto/[utils, sha2, hmac, rijndael],
|
||||||
|
|
|
@ -7,6 +7,8 @@
|
||||||
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||||||
# MIT license (LICENSE-MIT)
|
# MIT license (LICENSE-MIT)
|
||||||
|
|
||||||
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
std/[unittest, net, options],
|
std/[unittest, net, options],
|
||||||
../../eth/p2p/enode
|
../../eth/p2p/enode
|
||||||
|
|
Loading…
Reference in New Issue