fix(discovery): prevent premature node eviction from routing table

The findNode and findNodeFast operations were using the default aggressive
removal threshold (1.0) when timing out, while other timeout operations
(ping, talkReq, getProviders) correctly used NoreplyRemoveThreshold (0.5).

This inconsistency caused nodes with excellent reliability (1.0) to be removed
during heavy load scenarios when findNode/findNodeFast operations timed out,
even though the nodes were still healthy and simply slow to respond.

Changed findNode and findNodeFast timeout paths to use NoreplyRemoveThreshold,
ensuring consistent and more tolerant behavior across all timeout scenarios.
This aligns with Kademlia's recommendation to be conservative about removing
nodes, especially during temporary network congestion.

Evidence from logs showing the issue:

DBG - Node added to routing table           topics="discv5 routingtable" tid=1 n=1ff*7a561e:10.244.0.208:6890
DBG - bucket                                topics="discv5" tid=1 depth=0 len=2 standby=0
DBG - node                                  topics="discv5" tid=1 n=130*db8a1b:10.244.2.207:6890 rttMin=1 rttAvg=2 reliability=1.0
DBG - node                                  topics="discv5" tid=1 n=1ff*7a561e:10.244.0.208:6890 rttMin=1 rttAvg=14 reliability=1.0
DBG - Node removed from routing table       topics="discv5 routingtable" tid=1 n=1ff*7a561e:10.244.0.208:6890
DBG - Total nodes in discv5 routing table   topics="discv5" tid=1 total=1
DBG - bucket                                topics="discv5" tid=1 depth=0 len=1 standby=0
DBG - node                                  topics="discv5" tid=1 n=130*db8a1b:10.244.2.207:6890 rttMin=1 rttAvg=165 reliability=0.957
DBG - Node removed from routing table       topics="discv5 routingtable" tid=1 n=130*db8a1b:10.244.2.207:6890
DBG - Total nodes in discv5 routing table   topics="discv5" tid=1 total=0

First entry shows a node with perfect reliability (1.0) and 14ms RTT being
removed. Second shows a node with 95.7% reliability also being evicted.

Signed-off-by: Chrysostomos Nanakos <chris@include.gr>
This commit is contained in:
Chrysostomos Nanakos 2025-10-09 22:16:32 +03:00
parent 99884b5971
commit 71bd679365
No known key found for this signature in database
2 changed files with 294 additions and 158 deletions

View File

@ -136,7 +136,7 @@ const
FindnodeSeenThreshold = 1.0 ## threshold used as findnode response filter FindnodeSeenThreshold = 1.0 ## threshold used as findnode response filter
LookupSeenThreshold = 0.0 ## threshold used for lookup nodeset selection LookupSeenThreshold = 0.0 ## threshold used for lookup nodeset selection
QuerySeenThreshold = 0.0 ## threshold used for query nodeset selection QuerySeenThreshold = 0.0 ## threshold used for query nodeset selection
NoreplyRemoveThreshold = 0.5 ## remove node on no reply if 'seen' is below this value NoreplyRemoveThreshold* = 0.5 ## remove node on no reply if 'seen' is below this value
func shortLog*(record: SignedPeerRecord): string = func shortLog*(record: SignedPeerRecord): string =
## Returns compact string representation of ``SignedPeerRecord``. ## Returns compact string representation of ``SignedPeerRecord``.
@ -587,7 +587,7 @@ proc findNode*(d: Protocol, toNode: Node, distances: seq[uint16]):
return ok(res) return ok(res)
else: else:
trace "findNode nodes not OK." trace "findNode nodes not OK."
d.replaceNode(toNode) d.replaceNode(toNode, NoreplyRemoveThreshold)
return err(nodes.error) return err(nodes.error)
proc findNodeFast*(d: Protocol, toNode: Node, target: NodeId): proc findNodeFast*(d: Protocol, toNode: Node, target: NodeId):
@ -605,7 +605,7 @@ proc findNodeFast*(d: Protocol, toNode: Node, target: NodeId):
let res = verifyNodesRecords(nodes.get(), toNode, FindNodeFastResultLimit) let res = verifyNodesRecords(nodes.get(), toNode, FindNodeFastResultLimit)
return ok(res) return ok(res)
else: else:
d.replaceNode(toNode) d.replaceNode(toNode, NoreplyRemoveThreshold)
return err(nodes.error) return err(nodes.error)

View File

@ -2,10 +2,15 @@
import import
std/tables, std/tables,
chronos, chronicles, stint, asynctest/chronos/unittest, chronos,
stew/byteutils, bearssl/rand, chronicles,
stint,
asynctest/chronos/unittest,
stew/byteutils,
bearssl/rand,
libp2p/crypto/crypto, libp2p/crypto/crypto,
codexdht/discv5/[transport, spr, node, routing_table, encoding, sessions, nodes_verification], codexdht/discv5/
[transport, spr, node, routing_table, encoding, sessions, nodes_verification],
codexdht/discv5/crypto as dhtcrypto, codexdht/discv5/crypto as dhtcrypto,
codexdht/discv5/protocol as discv5_protocol, codexdht/discv5/protocol as discv5_protocol,
../dht/test_helper ../dht/test_helper
@ -22,13 +27,13 @@ suite "Discovery v5 Tests":
pk = PrivateKey.example(rng) pk = PrivateKey.example(rng)
targetPk = PrivateKey.example(rng) targetPk = PrivateKey.example(rng)
node = initDiscoveryNode(rng, pk, localAddress(20302)) node = initDiscoveryNode(rng, pk, localAddress(20302))
targetNode = targetPk.generateNode(port=26302) targetNode = targetPk.generateNode(port = 26302)
check node.addNode(targetNode) check node.addNode(targetNode)
for i in 0..<1000: for i in 0 ..< 1000:
let pk = PrivateKey.example(rng) let pk = PrivateKey.example(rng)
discard node.addNode(pk.generateNode(port=27302+i)) discard node.addNode(pk.generateNode(port = 27302 + i))
let n = node.getNode(targetNode.id) let n = node.getNode(targetNode.id)
check n.isSome() check n.isSome()
@ -39,14 +44,13 @@ suite "Discovery v5 Tests":
test "Node deletion": test "Node deletion":
let let
pkBootnode = PrivateKey.example(rng) pkBootnode = PrivateKey.example(rng)
bootnode = initDiscoveryNode( bootnode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
rng, PrivateKey.example(rng), localAddress(20301))
node1 = initDiscoveryNode( node1 = initDiscoveryNode(
rng, PrivateKey.example(rng), localAddress(20302), rng, PrivateKey.example(rng), localAddress(20302), @[bootnode.localNode.record]
@[bootnode.localNode.record]) )
node2 = initDiscoveryNode( node2 = initDiscoveryNode(
rng, PrivateKey.example(rng), localAddress(20303), rng, PrivateKey.example(rng), localAddress(20303), @[bootnode.localNode.record]
@[bootnode.localNode.record]) )
pong1 = await discv5_protocol.ping(node1, bootnode.localNode) pong1 = await discv5_protocol.ping(node1, bootnode.localNode)
pong2 = await discv5_protocol.ping(node1, node2.localNode) pong2 = await discv5_protocol.ping(node1, node2.localNode)
@ -83,7 +87,7 @@ suite "Discovery v5 Tests":
("0x0100", 9'u16), ("0x0100", 9'u16),
("0x01ff", 9'u16), ("0x01ff", 9'u16),
("0x8000", 16'u16), ("0x8000", 16'u16),
("0xffff", 16'u16) ("0xffff", 16'u16),
] ]
for (id, d) in testValues: for (id, d) in testValues:
@ -93,20 +97,17 @@ suite "Discovery v5 Tests":
# Values for this test are taken from # Values for this test are taken from
# https://github.com/ethereum/go-ethereum/blob/d8ff53dfb8a516f47db37dbc7fd7ad18a1e8a125/p2p/discover/v4_lookup_test.go#L176 # https://github.com/ethereum/go-ethereum/blob/d8ff53dfb8a516f47db37dbc7fd7ad18a1e8a125/p2p/discover/v4_lookup_test.go#L176
const const
targetKey = "045d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125" targetKey =
"045d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125"
testValues = [ testValues = [
("14a98db9b46a831d67eff29f3b85b1b485bb12ae9796aea98d91be3dc78d8a91", 248'u16), ("14a98db9b46a831d67eff29f3b85b1b485bb12ae9796aea98d91be3dc78d8a91", 248'u16),
("29738ba0c1a4397d6a65f292eee07f02df8e58d41594ba2be3cf84ce0fc58169", 252'u16), ("29738ba0c1a4397d6a65f292eee07f02df8e58d41594ba2be3cf84ce0fc58169", 252'u16),
("dec742079ec00ff4ec1284d7905bc3de2366f67a0769431fd16f80fd68c58a7c", 252'u16), ("dec742079ec00ff4ec1284d7905bc3de2366f67a0769431fd16f80fd68c58a7c", 252'u16),
("ce1435a956a98ffec484cd11489c4f165cf1606819ab6b521cee440f0c677e9e", 253'u16), ("ce1435a956a98ffec484cd11489c4f165cf1606819ab6b521cee440f0c677e9e", 253'u16),
("120260dce739b6f71f171da6f65bc361b5fad51db74cf02d3e973347819a6518", 253'u16), ("120260dce739b6f71f171da6f65bc361b5fad51db74cf02d3e973347819a6518", 253'u16),
("a30599b12827b69120633f15b98a7f6bc9fc2e9a0fd6ae2ebb767c0e64d743ab", 254'u16), ("a30599b12827b69120633f15b98a7f6bc9fc2e9a0fd6ae2ebb767c0e64d743ab", 254'u16),
("8c5b422155d33ea8e9d46f71d1ad3e7b24cb40051413ffa1a81cff613d243ba9", 254'u16), ("8c5b422155d33ea8e9d46f71d1ad3e7b24cb40051413ffa1a81cff613d243ba9", 254'u16),
("996e7f8d1638be92d7328b4770f47e5420fc4bafecb4324fd33b1f5d9f403a75", 254'u16), ("996e7f8d1638be92d7328b4770f47e5420fc4bafecb4324fd33b1f5d9f403a75", 254'u16),
("d09e5eaeec0fd596236faed210e55ef45112409a5aa7f3276d26646080dcfaeb", 255'u16), ("d09e5eaeec0fd596236faed210e55ef45112409a5aa7f3276d26646080dcfaeb", 255'u16),
("6cfbd7b8503073fc3dbdb746a7c672571648d3bd15197ccf7f7fef3d904f53a2", 255'u16), ("6cfbd7b8503073fc3dbdb746a7c672571648d3bd15197ccf7f7fef3d904f53a2", 255'u16),
("9ae91101d6b5048607f41ec0f690ef5d09507928aded2410aabd9237aa2727d7", 255'u16), ("9ae91101d6b5048607f41ec0f690ef5d09507928aded2410aabd9237aa2727d7", 255'u16),
@ -116,7 +117,6 @@ suite "Discovery v5 Tests":
("1fa56cf25d4b46c2bf94e82355aa631717b63190785ac6bae545a88aadc304a9", 255'u16), ("1fa56cf25d4b46c2bf94e82355aa631717b63190785ac6bae545a88aadc304a9", 255'u16),
("3c38c503c0376f9b4adcbe935d5f4b890391741c764f61b03cd4d0d42deae002", 255'u16), ("3c38c503c0376f9b4adcbe935d5f4b890391741c764f61b03cd4d0d42deae002", 255'u16),
("3a54af3e9fa162bc8623cdf3e5d9b70bf30ade1d54cc3abea8659aba6cff471f", 255'u16), ("3a54af3e9fa162bc8623cdf3e5d9b70bf30ade1d54cc3abea8659aba6cff471f", 255'u16),
("511b1686e4e58a917f7f848e9bf5539d206a68f5ad6b54b552c2399fe7d174ae", 256'u16), ("511b1686e4e58a917f7f848e9bf5539d206a68f5ad6b54b552c2399fe7d174ae", 256'u16),
("c1e20dbbf0d530e50573bd0a260b32ec15eb9190032b4633d44834afc8afe578", 256'u16), ("c1e20dbbf0d530e50573bd0a260b32ec15eb9190032b4633d44834afc8afe578", 256'u16),
("ed5f38f5702d92d306143e5d9154fb21819777da39af325ea359f453d179e80b", 256'u16), ("ed5f38f5702d92d306143e5d9154fb21819777da39af325ea359f453d179e80b", 256'u16),
@ -138,7 +138,7 @@ suite "Discovery v5 Tests":
("d269609743ef29d6446e3355ec647e38d919c82a4eb5837e442efd7f4218944f", 256'u16), ("d269609743ef29d6446e3355ec647e38d919c82a4eb5837e442efd7f4218944f", 256'u16),
("937b1af801def4e8f5a3a8bd225a8bcff1db764e41d3e177f2e9376e8dd87233", 256'u16), ("937b1af801def4e8f5a3a8bd225a8bcff1db764e41d3e177f2e9376e8dd87233", 256'u16),
("6799a02ea1999aefdcbcc4d3ff9544478be7365a328d0d0f37c26bd95ade0cda", 256'u16), ("6799a02ea1999aefdcbcc4d3ff9544478be7365a328d0d0f37c26bd95ade0cda", 256'u16),
("e24a7bc9051058f918646b0f6e3d16884b2a55a15553b89bab910d55ebc36116", 256'u16) ("e24a7bc9051058f918646b0f6e3d16884b2a55a15553b89bab910d55ebc36116", 256'u16),
] ]
let let
@ -163,7 +163,7 @@ suite "Discovery v5 Tests":
("0x0008", 4'u16), ("0x0008", 4'u16),
("0x0080", 8'u16), ("0x0080", 8'u16),
("0x0100", 9'u16), ("0x0100", 9'u16),
("0x8000", 16'u16) ("0x8000", 16'u16),
] ]
for (id, d) in testValues: for (id, d) in testValues:
@ -171,14 +171,15 @@ suite "Discovery v5 Tests":
test "Distance to id check with keys": test "Distance to id check with keys":
const const
targetKey = "045d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125" targetKey =
"045d485bdcbe9bc89314a10ae9231e429d33853e3a8fa2af39f5f827370a2e4185e344ace5d16237491dad41f278f1d3785210d29ace76cd627b9147ee340b1125"
testValues = [ # possible id in that distance range testValues = [ # possible id in that distance range
("cd2c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 251'u16), ("cd2c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 251'u16),
("c12c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 252'u16), ("c12c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 252'u16),
("d92c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 253'u16), ("d92c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 253'u16),
("e92c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 254'u16), ("e92c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 254'u16),
("892c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 255'u16), ("892c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 255'u16),
("492c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 256'u16) ("492c707bdcbdf5109c66de68e8f26adec1527d075e0f93df0ad21c72e98b7a4d", 256'u16),
] ]
let let
@ -191,11 +192,11 @@ suite "Discovery v5 Tests":
test "FindNode Test": test "FindNode Test":
const dist = 253'u16 const dist = 253'u16
let let
mainNodeKey = PrivateKey.fromHex( mainNodeKey = PrivateKey
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617") .fromHex("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a617")
.expect("Valid private key hex") .expect("Valid private key hex")
testNodeKey = PrivateKey.fromHex( testNodeKey = PrivateKey
"a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618") .fromHex("a2b50376a79b1a8c8a3296485572bdfbf54708bb46d3c25d73d2723aaaf6a618")
.expect("Valid private key hex") .expect("Valid private key hex")
mainNode = initDiscoveryNode(rng, mainNodeKey, localAddress(20301)) mainNode = initDiscoveryNode(rng, mainNodeKey, localAddress(20301))
testNode = initDiscoveryNode(rng, testNodeKey, localAddress(20302)) testNode = initDiscoveryNode(rng, testNodeKey, localAddress(20302))
@ -210,38 +211,33 @@ suite "Discovery v5 Tests":
check (await mainNode.ping(testNode.localNode)).isOk() check (await mainNode.ping(testNode.localNode)).isOk()
# Get SPR of the node itself # Get SPR of the node itself
var discovered = var discovered = await findNode(testNode, mainNode.localNode, @[0'u16])
await findNode(testNode, mainNode.localNode, @[0'u16])
check: check:
discovered.isOk discovered.isOk
discovered[].len == 1 discovered[].len == 1
discovered[][0] == mainNode.localNode discovered[][0] == mainNode.localNode
# Get SPRs of nodes added at provided logarithmic distance # Get SPRs of nodes added at provided logarithmic distance
discovered = discovered = await findNode(testNode, mainNode.localNode, @[dist])
await findNode(testNode, mainNode.localNode, @[dist])
check discovered.isOk check discovered.isOk
check discovered[].len == 10 check discovered[].len == 10
for n in nodes: for n in nodes:
check discovered[].contains(n) check discovered[].contains(n)
# Too high logarithmic distance, should return no nodes. # Too high logarithmic distance, should return no nodes.
discovered = discovered = await findNode(testNode, mainNode.localNode, @[high(uint16)])
await findNode(testNode, mainNode.localNode, @[high(uint16)])
check: check:
discovered.isOk discovered.isOk
discovered[].len == 0 discovered[].len == 0
# Logarithmic distance of 256 should only return the testNode # Logarithmic distance of 256 should only return the testNode
discovered = discovered = await findNode(testNode, mainNode.localNode, @[256'u16])
await findNode(testNode, mainNode.localNode, @[256'u16])
check: check:
discovered.isOk discovered.isOk
discovered[].len == 1 discovered[].len == 1
discovered[][0] == testNode.localNode discovered[][0] == testNode.localNode
# Empty bucket # Empty bucket
discovered = discovered = await findNode(testNode, mainNode.localNode, @[254'u16])
await findNode(testNode, mainNode.localNode, @[254'u16])
check discovered.isOk check discovered.isOk
check discovered[].len == 0 check discovered[].len == 0
@ -250,8 +246,7 @@ suite "Discovery v5 Tests":
discard mainNode.addSeenNode(n) # for testing only! discard mainNode.addSeenNode(n) # for testing only!
# Full bucket # Full bucket
discovered = discovered = await findNode(testNode, mainNode.localNode, @[dist])
await findNode(testNode, mainNode.localNode, @[dist])
check discovered.isOk check discovered.isOk
check discovered[].len == 16 check discovered[].len == 16
@ -259,27 +254,26 @@ suite "Discovery v5 Tests":
await testNode.closeWait() await testNode.closeWait()
test "FindNode with test table": test "FindNode with test table":
let mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
let mainNode =
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
# Generate 1000 random nodes and add to our main node's routing table # Generate 1000 random nodes and add to our main node's routing table
for i in 0..<1000: for i in 0 ..< 1000:
discard mainNode.addSeenNode(generateNode(PrivateKey.example(rng), port=28302+i)) # for testing only! discard mainNode.addSeenNode(
generateNode(PrivateKey.example(rng), port = 28302 + i)
) # for testing only!
let let
neighbours = mainNode.neighbours(mainNode.localNode.id) neighbours = mainNode.neighbours(mainNode.localNode.id)
closest = neighbours[0] closest = neighbours[0]
closestDistance = logDistance(closest.id, mainNode.localNode.id) closestDistance = logDistance(closest.id, mainNode.localNode.id)
debug "Closest neighbour", closestDistance, id=closest.id.toHex() debug "Closest neighbour", closestDistance, id = closest.id.toHex()
let let
testNode = initDiscoveryNode( testNode = initDiscoveryNode(
rng, PrivateKey.example(rng), localAddress(20302), rng, PrivateKey.example(rng), localAddress(20302), @[mainNode.localNode.record]
@[mainNode.localNode.record]) )
discovered = await findNode(testNode, mainNode.localNode, discovered = await findNode(testNode, mainNode.localNode, @[closestDistance])
@[closestDistance])
check discovered.isOk check discovered.isOk
check closest in discovered[] check closest in discovered[]
@ -288,18 +282,22 @@ suite "Discovery v5 Tests":
await testNode.closeWait() await testNode.closeWait()
proc testLookupTargets(fast: bool = false): Future[bool] {.async.} = proc testLookupTargets(fast: bool = false): Future[bool] {.async.} =
const const nodeCount = 17
nodeCount = 17
let bootNode = let bootNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
await bootNode.start() await bootNode.start()
var nodes = newSeqOfCap[discv5_protocol.Protocol](nodeCount) var nodes = newSeqOfCap[discv5_protocol.Protocol](nodeCount)
nodes.add(bootNode) nodes.add(bootNode)
for i in 1 ..< nodeCount: for i in 1 ..< nodeCount:
nodes.add(initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301 + i), nodes.add(
@[bootNode.localNode.record])) initDiscoveryNode(
rng,
PrivateKey.example(rng),
localAddress(20301 + i),
@[bootNode.localNode.record],
)
)
# Make sure all nodes have "seen" each other by forcing pings # Make sure all nodes have "seen" each other by forcing pings
for n in nodes: for n in nodes:
@ -314,9 +312,10 @@ suite "Discovery v5 Tests":
for i in 1 ..< nodeCount: for i in 1 ..< nodeCount:
await nodes[i].start() await nodes[i].start()
for i in 0..<nodeCount-1: for i in 0 ..< nodeCount - 1:
let target = nodes[i] let target = nodes[i]
let discovered = await nodes[nodeCount-1].lookup(target.localNode.id, fast = fast) let discovered =
await nodes[nodeCount - 1].lookup(target.localNode.id, fast = fast)
debug "Lookup result", target = target.localNode, discovered debug "Lookup result", target = target.localNode, discovered
if discovered[0] != target.localNode: if discovered[0] != target.localNode:
return false return false
@ -334,10 +333,8 @@ suite "Discovery v5 Tests":
test "Resolve target": test "Resolve target":
let let
mainNode = mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301)) lookupNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
lookupNode =
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
targetKey = PrivateKey.example(rng) targetKey = PrivateKey.example(rng)
targetAddress = localAddress(20303) targetAddress = localAddress(20303)
targetNode = initDiscoveryNode(rng, targetKey, targetAddress) targetNode = initDiscoveryNode(rng, targetKey, targetAddress)
@ -356,20 +353,27 @@ suite "Discovery v5 Tests":
n.isSome() n.isSome()
n.get().id == targetId n.get().id == targetId
n.get().record.seqNum == targetSeqNum n.get().record.seqNum == targetSeqNum
# Node will be removed because of failed findNode request.
# Verify node is still in routing table after timeout (high reliability preserved)
let nodeInTable = mainNode.getNode(targetId)
check:
nodeInTable.isSome()
nodeInTable.get().id == targetId
nodeInTable.get().record.seqNum == targetSeqNum
# Bring target back online, update seqNum in SPR, check if we get the # Bring target back online, update seqNum in SPR, check if we get the
# updated SPR. # updated SPR.
block: block:
targetNode.open() targetNode.open()
# Request the target SPR and manually add it to the routing table. # Request the target SPR and manually add/update it in the routing table.
# Ping for handshake based SPR passing will not work as our previous # Ping for handshake based SPR passing will not work as our previous
# session will still be in the LRU cache. # session will still be in the LRU cache.
let nodes = await mainNode.findNode(targetNode.localNode, @[0'u16]) let nodes = await mainNode.findNode(targetNode.localNode, @[0'u16])
check: check:
nodes.isOk() nodes.isOk()
nodes[].len == 1 nodes[].len == 1
mainNode.addNode(nodes[][0]) # Add/update from findNode result - false because node already exists
mainNode.addNode(nodes[][0]) == false
targetSeqNum.inc() targetSeqNum.inc()
# need to add something to get the spr sequence number incremented # need to add something to get the spr sequence number incremented
@ -387,9 +391,8 @@ suite "Discovery v5 Tests":
n.isSome() n.isSome()
n.get().id == targetId n.get().id == targetId
n.get().record.seqNum == targetSeqNum n.get().record.seqNum == targetSeqNum
# Add the updated version - node already exsists, just updating
# Add the updated version mainNode.addNode(n.get()) == false
discard mainNode.addNode(n.get())
# Update seqNum in SPR again, ping lookupNode to be added in routing table, # Update seqNum in SPR again, ping lookupNode to be added in routing table,
# close targetNode, resolve should lookup, check if we get updated SPR. # close targetNode, resolve should lookup, check if we get updated SPR.
@ -403,6 +406,12 @@ suite "Discovery v5 Tests":
# ping node so that it becomes "seen" and thus will be forwarded on a # ping node so that it becomes "seen" and thus will be forwarded on a
# findNode request # findNode request
check (await lookupNode.ping(targetNode.localNode)).isOk() check (await lookupNode.ping(targetNode.localNode)).isOk()
# lower reliability so timeout will trigger removal with 0.5 threshold
let existingNode = mainNode.getNode(targetId)
if existingNode.isSome():
existingNode.get().seen = 0.3 # below NoreplyRemoveThreshold (0.5)
await targetNode.closeWait() await targetNode.closeWait()
check mainNode.addNode(lookupNode.localNode.record) check mainNode.addNode(lookupNode.localNode.record)
@ -412,9 +421,79 @@ suite "Discovery v5 Tests":
n.get().id == targetId n.get().id == targetId
n.get().record.seqNum == targetSeqNum n.get().record.seqNum == targetSeqNum
# Demonstrate seen degradation via repeated findNode failures
# Node was re-added by lookup with seen=1.0, now repeatedly timeout to degrade
block:
var attempt = 0
while true:
attempt.inc()
let nodeBeforeTimeout = mainNode.getNode(targetId)
if nodeBeforeTimeout.isNone():
break
discard await mainNode.findNode(nodeBeforeTimeout.get(), @[0'u16])
if attempt > 10:
break
# Verify node was eventually removed due to degraded reliability
let finalCheck = mainNode.getNode(targetId)
check finalCheck.isNone()
await mainNode.closeWait() await mainNode.closeWait()
await lookupNode.closeWait() await lookupNode.closeWait()
test "FindNode timeout preserves high reliability nodes":
# Nodes with reliability > NoreplyRemoveThreshold are kept when no replacements available
let
mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20401))
targetNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20402))
targetId = targetNode.localNode.id
# add node to routing table via ping
check (await mainNode.ping(targetNode.localNode)).isOk()
check mainNode.addNode(targetNode.localNode.record)
var n = mainNode.getNode(targetId)
check n.isSome()
n.get().seen = 1.8 * discv5_protocol.NoreplyRemoveThreshold
# close node to trigger timeout
await targetNode.closeWait()
discard await mainNode.findNode(n.get(), @[0'u16])
# verify node still in routing table
n = mainNode.getNode(targetId)
check n.isSome()
await mainNode.closeWait()
test "FindNode timeout removes low reliability nodes":
# Nodes with reliability <= NoreplyRemoveThreshold are removed on timeout
let
mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20403))
targetNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20404))
targetId = targetNode.localNode.id
check (await mainNode.ping(targetNode.localNode)).isOk()
check mainNode.addNode(targetNode.localNode.record)
var n = mainNode.getNode(targetId)
check n.isSome()
n.get().seen = 0.01 * discv5_protocol.NoreplyRemoveThreshold
await targetNode.closeWait()
discard await mainNode.findNode(n.get(), @[0'u16])
# verify node removed from routing table
n = mainNode.getNode(targetId)
check n.isNone()
await mainNode.closeWait()
test "Random nodes, also with filter": test "Random nodes, also with filter":
let let
lookupNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301)) lookupNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
@ -429,11 +508,18 @@ suite "Discovery v5 Tests":
let discovered = lookupNode.randomNodes(10) let discovered = lookupNode.randomNodes(10)
check discovered.len == 3 check discovered.len == 3
let discoveredFiltered = lookupNode.randomNodes(10, let discoveredFiltered = lookupNode.randomNodes(
proc(n: Node) : bool = n.address.get.port == Port(20302)) 10,
check discoveredFiltered.len == 1 and discoveredFiltered.contains(targetNode.localNode) proc(n: Node): bool =
let discoveredEmpty = lookupNode.randomNodes(10, n.address.get.port == Port(20302),
proc(n: Node) : bool = n.address.get.port == Port(20305)) )
check discoveredFiltered.len == 1 and
discoveredFiltered.contains(targetNode.localNode)
let discoveredEmpty = lookupNode.randomNodes(
10,
proc(n: Node): bool =
n.address.get.port == Port(20305),
)
check discoveredEmpty.len == 0 check discoveredEmpty.len == 0
await lookupNode.closeWait() await lookupNode.closeWait()
@ -441,22 +527,41 @@ suite "Discovery v5 Tests":
await otherNode.closeWait() await otherNode.closeWait()
await anotherNode.closeWait() await anotherNode.closeWait()
test "New protocol with spr": test "New protocol with spr":
let let
privKey = PrivateKey.example(rng) privKey = PrivateKey.example(rng)
ip = some(parseIpAddress("127.0.0.1")) ip = some(parseIpAddress("127.0.0.1"))
port = Port(20301) port = Port(20301)
node = newProtocol(privKey, ip, some(port), some(port), bindPort = port, node =
rng = rng) newProtocol(privKey, ip, some(port), some(port), bindPort = port, rng = rng)
noUpdatesNode = newProtocol(privKey, ip, some(port), some(port), noUpdatesNode = newProtocol(
bindPort = port, rng = rng, previousRecord = some(node.getRecord())) privKey,
updatesNode = newProtocol(privKey, ip, some(port), some(Port(20302)), ip,
bindPort = port, rng = rng, some(port),
previousRecord = some(noUpdatesNode.getRecord())) some(port),
moreUpdatesNode = newProtocol(privKey, ip, some(port), some(port), bindPort = port,
bindPort = port, rng = rng, localEnrFields = {"addfield": @[byte 0]}, rng = rng,
previousRecord = some(updatesNode.getRecord())) previousRecord = some(node.getRecord()),
)
updatesNode = newProtocol(
privKey,
ip,
some(port),
some(Port(20302)),
bindPort = port,
rng = rng,
previousRecord = some(noUpdatesNode.getRecord()),
)
moreUpdatesNode = newProtocol(
privKey,
ip,
some(port),
some(port),
bindPort = port,
rng = rng,
localEnrFields = {"addfield": @[byte 0]},
previousRecord = some(updatesNode.getRecord()),
)
check: check:
node.getRecord().seqNum == 1 node.getRecord().seqNum == 1
noUpdatesNode.getRecord().seqNum == 1 noUpdatesNode.getRecord().seqNum == 1
@ -465,16 +570,20 @@ suite "Discovery v5 Tests":
# Defect (for now?) on incorrect key use # Defect (for now?) on incorrect key use
expect ResultDefect: expect ResultDefect:
let incorrectKeyUpdates = newProtocol(PrivateKey.example(rng), let incorrectKeyUpdates = newProtocol(
ip, some(port), some(port), bindPort = port, rng = rng, PrivateKey.example(rng),
previousRecord = some(updatesNode.getRecord())) ip,
some(port),
some(port),
bindPort = port,
rng = rng,
previousRecord = some(updatesNode.getRecord()),
)
test "Update node record with revalidate": test "Update node record with revalidate":
let let
mainNode = mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301)) testNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
testNode =
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
testNodeId = testNode.localNode.id testNodeId = testNode.localNode.id
check: check:
@ -505,14 +614,13 @@ suite "Discovery v5 Tests":
test "Update node record with handshake": test "Update node record with handshake":
let let
mainNode = mainNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301))
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20301)) testNode = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
testNode =
initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
testNodeId = testNode.localNode.id testNodeId = testNode.localNode.id
# Add the node (from the record, so new node!) so no handshake is done yet. # Add the node (from the record, so new node!) so no handshake is done yet.
check: mainNode.addNode(testNode.localNode.record) check:
mainNode.addNode(testNode.localNode.record)
check: check:
testNode.updateRecord().isOk() testNode.updateRecord().isOk()
@ -539,9 +647,13 @@ suite "Discovery v5 Tests":
test "Verify records of nodes message": test "Verify records of nodes message":
let let
port = Port(9000) port = Port(9000)
fromNoderecord = SignedPeerRecord.init(1, PrivateKey.example(rng), fromNoderecord = SignedPeerRecord.init(
1,
PrivateKey.example(rng),
some(parseIpAddress("11.12.13.14")), some(parseIpAddress("11.12.13.14")),
some(port), some(port))[] some(port),
some(port),
)[]
fromNode = newNode(fromNoderecord)[] fromNode = newNode(fromNoderecord)[]
privKey = PrivateKey.example(rng) privKey = PrivateKey.example(rng)
pubKey = privKey.getPublicKey.expect("Valid private key for public key") pubKey = privKey.getPublicKey.expect("Valid private key for public key")
@ -550,10 +662,9 @@ suite "Discovery v5 Tests":
limit = 16 limit = 16
block: # Duplicates block: # Duplicates
let let record = SignedPeerRecord.init(
record = SignedPeerRecord.init( 1, privKey, some(parseIpAddress("12.13.14.15")), some(port), some(port)
1, privKey, some(parseIpAddress("12.13.14.15")), )[]
some(port), some(port))[]
# Exact duplicates # Exact duplicates
var records = @[record, record] var records = @[record, record]
@ -562,16 +673,16 @@ suite "Discovery v5 Tests":
# Node id duplicates # Node id duplicates
let recordSameId = SignedPeerRecord.init( let recordSameId = SignedPeerRecord.init(
1, privKey, some(parseIpAddress("212.13.14.15")), 1, privKey, some(parseIpAddress("212.13.14.15")), some(port), some(port)
some(port), some(port))[] )[]
records.add(recordSameId) records.add(recordSameId)
nodes = verifyNodesRecords(records, fromNode, limit, targetDistance) nodes = verifyNodesRecords(records, fromNode, limit, targetDistance)
check nodes.len == 1 check nodes.len == 1
block: # No address block: # No address
let let
recordNoAddress = SignedPeerRecord.init( recordNoAddress =
1, privKey, none(IpAddress), some(port), some(port))[] SignedPeerRecord.init(1, privKey, none(IpAddress), some(port), some(port))[]
records = [recordNoAddress] records = [recordNoAddress]
test = verifyNodesRecords(records, fromNode, limit, targetDistance) test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0 check test.len == 0
@ -579,8 +690,8 @@ suite "Discovery v5 Tests":
block: # Invalid address - site local block: # Invalid address - site local
let let
recordInvalidAddress = SignedPeerRecord.init( recordInvalidAddress = SignedPeerRecord.init(
1, privKey, some(parseIpAddress("10.1.2.3")), 1, privKey, some(parseIpAddress("10.1.2.3")), some(port), some(port)
some(port), some(port))[] )[]
records = [recordInvalidAddress] records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, limit, targetDistance) test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0 check test.len == 0
@ -588,8 +699,8 @@ suite "Discovery v5 Tests":
block: # Invalid address - loopback block: # Invalid address - loopback
let let
recordInvalidAddress = SignedPeerRecord.init( recordInvalidAddress = SignedPeerRecord.init(
1, privKey, some(parseIpAddress("127.0.0.1")), 1, privKey, some(parseIpAddress("127.0.0.1")), some(port), some(port)
some(port), some(port))[] )[]
records = [recordInvalidAddress] records = [recordInvalidAddress]
test = verifyNodesRecords(records, fromNode, limit, targetDistance) test = verifyNodesRecords(records, fromNode, limit, targetDistance)
check test.len == 0 check test.len == 0
@ -597,8 +708,8 @@ suite "Discovery v5 Tests":
block: # Invalid distance block: # Invalid distance
let let
recordInvalidDistance = SignedPeerRecord.init( recordInvalidDistance = SignedPeerRecord.init(
1, privKey, some(parseIpAddress("12.13.14.15")), 1, privKey, some(parseIpAddress("12.13.14.15")), some(port), some(port)
some(port), some(port))[] )[]
records = [recordInvalidDistance] records = [recordInvalidDistance]
test = verifyNodesRecords(records, fromNode, limit, @[0'u16]) test = verifyNodesRecords(records, fromNode, limit, @[0'u16])
check test.len == 0 check test.len == 0
@ -606,8 +717,8 @@ suite "Discovery v5 Tests":
block: # Invalid distance but distance validation is disabled block: # Invalid distance but distance validation is disabled
let let
recordInvalidDistance = SignedPeerRecord.init( recordInvalidDistance = SignedPeerRecord.init(
1, privKey, some(parseIpAddress("12.13.14.15")), 1, privKey, some(parseIpAddress("12.13.14.15")), some(port), some(port)
some(port), some(port))[] )[]
records = [recordInvalidDistance] records = [recordInvalidDistance]
test = verifyNodesRecords(records, fromNode, limit) test = verifyNodesRecords(records, fromNode, limit)
check test.len == 1 check test.len == 1
@ -623,8 +734,8 @@ suite "Discovery v5 Tests":
test "Handshake cleanup: different ids": test "Handshake cleanup: different ids":
# Node to test the handshakes on. # Node to test the handshakes on.
let receiveNode = initDiscoveryNode( let receiveNode =
rng, PrivateKey.example(rng), localAddress(20302)) initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
# Create random packets with same ip but different node ids # Create random packets with same ip but different node ids
# and "receive" them on receiveNode # and "receive" them on receiveNode
@ -632,14 +743,22 @@ suite "Discovery v5 Tests":
for i in 0 ..< 5: for i in 0 ..< 5:
let let
privKey = PrivateKey.example(rng) privKey = PrivateKey.example(rng)
enrRec = SignedPeerRecord.init(1, privKey, enrRec = SignedPeerRecord
some(parseIpAddress("127.0.0.1")), some(Port(9000)), .init(
some(Port(9000))).expect("Properly intialized private key") 1,
privKey,
some(parseIpAddress("127.0.0.1")),
some(Port(9000)),
some(Port(9000)),
)
.expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record") sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5)) var codec =
Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))
let (packet, _, _) = encodeMessagePacket(rng[], codec, let (packet, _, _) = encodeMessagePacket(
receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]) rng[], codec, receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]
)
receiveNode.transport.receive(a, packet) receiveNode.transport.receive(a, packet)
# Checking different nodeIds but same address # Checking different nodeIds but same address
@ -654,22 +773,29 @@ suite "Discovery v5 Tests":
test "Handshake cleanup: different ips": test "Handshake cleanup: different ips":
# Node to test the handshakes on. # Node to test the handshakes on.
let receiveNode = initDiscoveryNode( let receiveNode =
rng, PrivateKey.example(rng), localAddress(20302)) initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
# Create random packets with same node ids but different ips # Create random packets with same node ids but different ips
# and "receive" them on receiveNode # and "receive" them on receiveNode
let let
privKey = PrivateKey.example(rng) privKey = PrivateKey.example(rng)
enrRec = SignedPeerRecord.init(1, privKey, enrRec = SignedPeerRecord
some(parseIpAddress("127.0.0.1")), some(Port(9000)), .init(
some(Port(9000))).expect("Properly intialized private key") 1,
privKey,
some(parseIpAddress("127.0.0.1")),
some(Port(9000)),
some(Port(9000)),
)
.expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record") sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5)) var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))
for i in 0 ..< 5: for i in 0 ..< 5:
let a = localAddress(20303 + i) let a = localAddress(20303 + i)
let (packet, _, _) = encodeMessagePacket(rng[], codec, let (packet, _, _) = encodeMessagePacket(
receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]) rng[], codec, receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]
)
receiveNode.transport.receive(a, packet) receiveNode.transport.receive(a, packet)
# Checking different nodeIds but same address # Checking different nodeIds but same address
@ -684,24 +810,31 @@ suite "Discovery v5 Tests":
test "Handshake duplicates": test "Handshake duplicates":
# Node to test the handshakes on. # Node to test the handshakes on.
let receiveNode = initDiscoveryNode( let receiveNode =
rng, PrivateKey.example(rng), localAddress(20302)) initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
# Create random packets with same node ids and same ips # Create random packets with same node ids and same ips
# and "receive" them on receiveNode # and "receive" them on receiveNode
let let
a = localAddress(20303) a = localAddress(20303)
privKey = PrivateKey.example(rng) privKey = PrivateKey.example(rng)
enrRec = SignedPeerRecord.init(1, privKey, enrRec = SignedPeerRecord
some(parseIpAddress("127.0.0.1")), some(Port(9000)), .init(
some(Port(9000))).expect("Properly intialized private key") 1,
privKey,
some(parseIpAddress("127.0.0.1")),
some(Port(9000)),
some(Port(9000)),
)
.expect("Properly intialized private key")
sendNode = newNode(enrRec).expect("Properly initialized record") sendNode = newNode(enrRec).expect("Properly initialized record")
var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5)) var codec = Codec(localNode: sendNode, privKey: privKey, sessions: Sessions.init(5))
var firstRequestNonce: AESGCMNonce var firstRequestNonce: AESGCMNonce
for i in 0 ..< 5: for i in 0 ..< 5:
let (packet, requestNonce, _) = encodeMessagePacket(rng[], codec, let (packet, requestNonce, _) = encodeMessagePacket(
receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]) rng[], codec, receiveNode.localNode.id, receiveNode.localNode.address.get(), @[]
)
receiveNode.transport.receive(a, packet) receiveNode.transport.receive(a, packet)
if i == 0: if i == 0:
firstRequestNonce = requestNonce firstRequestNonce = requestNonce
@ -717,12 +850,10 @@ suite "Discovery v5 Tests":
test "Talkreq no protocol": test "Talkreq no protocol":
let let
node1 = initDiscoveryNode( node1 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
rng, PrivateKey.example(rng), localAddress(20302)) node2 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20303))
node2 = initDiscoveryNode( talkresp =
rng, PrivateKey.example(rng), localAddress(20303)) await discv5_protocol.talkReq(node1, node2.localNode, @[byte 0x01], @[])
talkresp = await discv5_protocol.talkReq(node1, node2.localNode,
@[byte 0x01], @[])
check: check:
talkresp.isOk() talkresp.isOk()
@ -733,21 +864,24 @@ suite "Discovery v5 Tests":
test "Talkreq echo protocol": test "Talkreq echo protocol":
let let
node1 = initDiscoveryNode( node1 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
rng, PrivateKey.example(rng), localAddress(20302)) node2 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20303))
node2 = initDiscoveryNode(
rng, PrivateKey.example(rng), localAddress(20303))
talkProtocol = "echo".toBytes() talkProtocol = "echo".toBytes()
proc handler(protocol: TalkProtocol, request: seq[byte], fromId: NodeId, fromUdpAddress: Address): seq[byte] proc handler(
{.gcsafe, raises: [Defect].} = protocol: TalkProtocol,
request: seq[byte],
fromId: NodeId,
fromUdpAddress: Address,
): seq[byte] {.gcsafe, raises: [Defect].} =
request request
let echoProtocol = TalkProtocol(protocolHandler: handler) let echoProtocol = TalkProtocol(protocolHandler: handler)
check node2.registerTalkProtocol(talkProtocol, echoProtocol).isOk() check node2.registerTalkProtocol(talkProtocol, echoProtocol).isOk()
let talkresp = await discv5_protocol.talkReq(node1, node2.localNode, let talkresp = await discv5_protocol.talkReq(
talkProtocol, "hello".toBytes()) node1, node2.localNode, talkProtocol, "hello".toBytes()
)
check: check:
talkresp.isOk() talkresp.isOk()
@ -758,14 +892,16 @@ suite "Discovery v5 Tests":
test "Talkreq register protocols": test "Talkreq register protocols":
let let
node1 = initDiscoveryNode( node1 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20302))
rng, PrivateKey.example(rng), localAddress(20302)) node2 = initDiscoveryNode(rng, PrivateKey.example(rng), localAddress(20303))
node2 = initDiscoveryNode(
rng, PrivateKey.example(rng), localAddress(20303))
talkProtocol = "echo".toBytes() talkProtocol = "echo".toBytes()
proc handler(protocol: TalkProtocol, request: seq[byte], fromId: NodeId, fromUdpAddress: Address): seq[byte] proc handler(
{.gcsafe, raises: [Defect].} = protocol: TalkProtocol,
request: seq[byte],
fromId: NodeId,
fromUdpAddress: Address,
): seq[byte] {.gcsafe, raises: [Defect].} =
request request
let echoProtocol = TalkProtocol(protocolHandler: handler) let echoProtocol = TalkProtocol(protocolHandler: handler)