Handle receiving a Pong with changed NodeID #5452
Changes from 4 commits
258c891
5373edf
1c03c6f
85952c9
68a3b59
80d9071
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -322,7 +322,7 @@ void NodeTable::ping(Node const& _node, shared_ptr<NodeEntry> _replacementNodeEn | |
return; | ||
|
||
// Don't sent Ping if one is already sent | ||
if (contains(m_sentPings, _node.id)) | ||
if (m_sentPings.find(_node.endpoint) != m_sentPings.end()) | ||
{ | ||
LOG(m_logger) << "Ignoring request to ping " << _node << ", because it's already pinged"; | ||
return; | ||
|
@@ -334,8 +334,9 @@ void NodeTable::ping(Node const& _node, shared_ptr<NodeEntry> _replacementNodeEn | |
LOG(m_logger) << p.typeName() << " to " << _node; | ||
m_socket->send(p); | ||
|
||
m_sentPings[_node.id] = { | ||
_node.endpoint, chrono::steady_clock::now(), pingHash, move(_replacementNodeEntry)}; | ||
NodeValidation const validation(_node.id, _node.endpoint.tcpPort(), chrono::steady_clock::now(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use braces for initialization |
||
pingHash, _replacementNodeEntry); | ||
m_sentPings.insert({_node.endpoint, validation}); | ||
} | ||
|
||
void NodeTable::schedulePing(Node const& _node) | ||
|
@@ -470,32 +471,42 @@ void NodeTable::onPacketReceived( | |
{ | ||
case Pong::type: | ||
{ | ||
auto const& pong = dynamic_cast<Pong const&>(*packet); | ||
auto const& sourceId = pong.sourceid; | ||
|
||
// validate pong | ||
auto const sentPing = m_sentPings.find(sourceId); | ||
auto const sentPing = m_sentPings.find(_from); | ||
if (sentPing == m_sentPings.end()) | ||
{ | ||
LOG(m_logger) << "Unexpected PONG from " << _from.address().to_string() << ":" | ||
<< _from.port(); | ||
return; | ||
} | ||
|
||
if (pong.echo != sentPing->second.pingHash) | ||
auto const& pong = dynamic_cast<Pong const&>(*packet); | ||
auto const& nodeValidation = sentPing->second; | ||
if (pong.echo != nodeValidation.pingHash) | ||
{ | ||
LOG(m_logger) << "Invalid PONG from " << _from.address().to_string() << ":" | ||
<< _from.port(); | ||
return; | ||
} | ||
|
||
// in case the node answers with new NodeID, drop the record with the old NodeID | ||
auto const& sourceId = pong.sourceid; | ||
if (sourceId != nodeValidation.nodeID) | ||
{ | ||
LOG(m_logger) << "Node " << _from << " changed public key from " | ||
<< nodeValidation.nodeID << " to " << sourceId; | ||
if (auto node = nodeEntry(nodeValidation.nodeID)) | ||
dropNode(move(node)); | ||
} | ||
|
||
// create or update nodeEntry with new Pong received time | ||
DEV_GUARDED(x_nodes) | ||
{ | ||
auto it = m_allNodes.find(sourceId); | ||
if (it == m_allNodes.end()) | ||
sourceNodeEntry = make_shared<NodeEntry>(m_hostNodeID, sourceId, | ||
sentPing->second.endpoint, RLPXDatagramFace::secondsSinceEpoch(), 0); | ||
NodeIPEndpoint{_from.address(), _from.port(), nodeValidation.tcpPort}, | ||
RLPXDatagramFace::secondsSinceEpoch(), 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Nit) can you add an inline comment stating what the purpose of the 0 literal e.g. 0 /* last pong sent time */ |
||
else | ||
{ | ||
sourceNodeEntry = it->second; | ||
|
@@ -504,7 +515,7 @@ void NodeTable::onPacketReceived( | |
} | ||
} | ||
|
||
m_sentPings.erase(sentPing); | ||
m_sentPings.erase(_from); | ||
|
||
// update our endpoint address and UDP port | ||
DEV_GUARDED(x_nodes) | ||
|
@@ -682,7 +693,7 @@ void NodeTable::doHandleTimeouts() | |
{ | ||
if (chrono::steady_clock::now() > it->second.pingSendTime + m_requestTimeToLive) | ||
{ | ||
if (auto node = nodeEntry(it->first)) | ||
if (auto node = nodeEntry(it->second.nodeID)) | ||
{ | ||
dropNode(move(node)); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -177,15 +177,25 @@ class NodeTable : UDPSocketEvents | |
// protected only for derived classes in tests | ||
protected: | ||
/** | ||
* NodeValidation is used to record Pinged node's endpoint, the timepoint of sent PING, | ||
* NodeValidation is used to record Pinged node's ID, the timepoint of sent PING, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "the timepoint of sent PING" and "time of sending" is redundant |
||
* time of sending and the new node ID to replace unresponsive node. | ||
*/ | ||
struct NodeValidation | ||
{ | ||
NodeIPEndpoint endpoint; | ||
NodeID nodeID; | ||
uint16_t tcpPort = 0; | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
TimePoint pingSendTime; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (Nit) I think this should be called pingSentTime (since it's referring to a past event) |
||
h256 pingHash; | ||
std::shared_ptr<NodeEntry> replacementNodeEntry; | ||
|
||
NodeValidation(NodeID const& _nodeID, uint16_t _tcpPort, TimePoint _pingSendTime, | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
h256 const& _pingHash, std::shared_ptr<NodeEntry> _replacementNodeEntry) | ||
: nodeID(_nodeID), | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
tcpPort(_tcpPort), | ||
pingSendTime(_pingSendTime), | ||
pingHash(_pingHash), | ||
replacementNodeEntry(std::move(_replacementNodeEntry)) | ||
{} | ||
}; | ||
|
||
/// Constants for Kademlia, derived from address space. | ||
|
@@ -308,7 +318,7 @@ class NodeTable : UDPSocketEvents | |
std::shared_ptr<NodeSocket> m_socket; ///< Shared pointer for our UDPSocket; ASIO requires shared_ptr. | ||
|
||
// The info about PING packets we've sent to other nodes and haven't received PONG yet | ||
std::unordered_map<NodeID, NodeValidation> m_sentPings; | ||
std::map<bi::udp::endpoint, NodeValidation> m_sentPings; | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Expiration time of sent discovery packets. | ||
std::chrono::seconds m_requestTimeToLive; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -185,11 +185,11 @@ struct TestNodeTable: public NodeTable | |
return m_buckets[_bucket].nodes.back().lock(); | ||
} | ||
|
||
boost::optional<NodeValidation> nodeValidation(NodeID const& _id) | ||
boost::optional<NodeValidation> nodeValidation(bi::udp::endpoint const& _endpoint) | ||
{ | ||
promise<boost::optional<NodeValidation>> promise; | ||
m_io.post([this, &promise, _id] { | ||
auto validation = m_sentPings.find(_id); | ||
m_io.post([this, &promise, _endpoint] { | ||
auto validation = m_sentPings.find(_endpoint); | ||
if (validation != m_sentPings.end()) | ||
promise.set_value(validation->second); | ||
else | ||
|
@@ -544,7 +544,7 @@ BOOST_AUTO_TEST_CASE(noteActiveNodeEvictsTheNodeWhenBucketIsFull) | |
// least recently seen node not removed yet | ||
BOOST_CHECK_EQUAL(nodeTable->bucketFirstNode(bucketIndex), leastRecentlySeenNode); | ||
// but added to evictions | ||
auto evicted = nodeTable->nodeValidation(leastRecentlySeenNode->id); | ||
auto evicted = nodeTable->nodeValidation(leastRecentlySeenNode->endpoint); | ||
BOOST_REQUIRE(evicted.is_initialized()); | ||
BOOST_REQUIRE(evicted->replacementNodeEntry); | ||
BOOST_CHECK_EQUAL(evicted->replacementNodeEntry->id, newNodeId); | ||
|
@@ -632,7 +632,7 @@ BOOST_AUTO_TEST_CASE(invalidPong) | |
nodeTable->packetsReceived.pop(); | ||
|
||
// pending node validation should still be not deleted | ||
BOOST_REQUIRE(nodeTable->nodeValidation(nodePubKey)); | ||
BOOST_REQUIRE(nodeTable->nodeValidation(nodeEndpoint)); | ||
// node is not in the node table | ||
BOOST_REQUIRE(!nodeTable->nodeExists(nodePubKey)); | ||
} | ||
|
@@ -675,6 +675,54 @@ BOOST_AUTO_TEST_CASE(validPong) | |
BOOST_CHECK(addedNode->lastPongReceivedTime > 0); | ||
} | ||
|
||
BOOST_AUTO_TEST_CASE(pongWithChangedNodeID) | ||
{ | ||
// NodeTable sending PING | ||
TestNodeTableHost nodeTableHost(0); | ||
nodeTableHost.start(); | ||
auto& nodeTable = nodeTableHost.nodeTable; | ||
nodeTable->setRequestTimeToLive(std::chrono::seconds(1)); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// socket receiving PING | ||
TestUDPSocketHost nodeSocketHost{randomPortNumber()}; | ||
nodeSocketHost.start(); | ||
uint16_t nodePort = nodeSocketHost.port; | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// add a node to node table, initiating PING | ||
auto nodeEndpoint = NodeIPEndpoint{bi::address::from_string(c_localhostIp), nodePort, nodePort}; | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
auto nodeKeyPair = KeyPair::create(); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
auto nodePubKey = nodeKeyPair.pub(); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
nodeTable->addNode(Node{nodePubKey, nodeEndpoint}); | ||
|
||
// handle received PING | ||
auto pingDataReceived = nodeSocketHost.packetsReceived.pop(); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
auto pingDatagram = | ||
DiscoveryDatagram::interpretUDP(bi::udp::endpoint{}, dev::ref(pingDataReceived)); | ||
auto ping = dynamic_cast<PingNode const&>(*pingDatagram); | ||
|
||
// send PONG with different NodeID | ||
Pong pong(nodeTable->m_hostNodeEndpoint); | ||
pong.echo = ping.echo; | ||
auto newNodeKeyPair = KeyPair::create(); | ||
auto newNodePubKey = newNodeKeyPair.pub(); | ||
pong.sign(newNodeKeyPair.secret()); | ||
nodeSocketHost.socket->send(pong); | ||
|
||
// wait for PONG to be received and handled | ||
nodeTable->packetsReceived.pop(); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
BOOST_REQUIRE(nodeTable->nodeExists(newNodeKeyPair.pub())); | ||
auto addedNode = nodeTable->nodeEntry(newNodePubKey); | ||
BOOST_CHECK(addedNode->lastPongReceivedTime > 0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this (and the rest of the BOOST_CHECKs in this test case) be BOOST_REQUIRE? Or is the reasoning that if the checks failed other tests would fail (in which case is there value in having the checks at all)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reasoning is that updating the node in the node table, updating There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, but I think the checks will only provide value when you do a local run (let me know if I'm wrong here)? The problem is that failed boost_checks don't trigger test failures and boost_require triggers a test failure and ceases test execution and the boost_checks are all after a boost_require, meaning that if the boost_require fails the subsequent boost_checks won't be executed, and if the boost_require passes the boost_checks might fail but won't trigger a test failure (so we won't know to look at the logs)? Also, do boost_check failures show up in CircleCI logs even if the tests themselves passed? Can we place the boost_checks before the boost_require? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe BOOST_CHECK failures always show up in logs. |
||
|
||
// wait for old node to be kicked out after timeout | ||
this_thread::sleep_for(std::chrono::seconds(6)); | ||
halfalicious marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
BOOST_CHECK(!nodeTable->nodeExists(nodePubKey)); | ||
auto sentPing = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_CHECK(!sentPing.is_initialized()); | ||
} | ||
|
||
BOOST_AUTO_TEST_CASE(pingTimeout) | ||
{ | ||
// NodeTable sending PING | ||
|
@@ -698,7 +746,7 @@ BOOST_AUTO_TEST_CASE(pingTimeout) | |
this_thread::sleep_for(chrono::seconds(6)); | ||
|
||
BOOST_CHECK(!nodeTable->nodeExists(nodePubKey)); | ||
auto sentPing = nodeTable->nodeValidation(nodePubKey); | ||
auto sentPing = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_CHECK(!sentPing.is_initialized()); | ||
|
||
// handle received PING | ||
|
@@ -717,7 +765,7 @@ BOOST_AUTO_TEST_CASE(pingTimeout) | |
nodeTable->packetsReceived.pop(); | ||
|
||
BOOST_CHECK(!nodeTable->nodeExists(nodePubKey)); | ||
sentPing = nodeTable->nodeValidation(nodePubKey); | ||
sentPing = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_CHECK(!sentPing.is_initialized()); | ||
} | ||
|
||
|
@@ -849,7 +897,7 @@ BOOST_AUTO_TEST_CASE(evictionWithOldNodeAnswering) | |
// wait for eviction | ||
evictEvents.pop(chrono::seconds(5)); | ||
|
||
auto evicted = nodeTable->nodeValidation(nodeId); | ||
auto evicted = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_REQUIRE(evicted.is_initialized()); | ||
|
||
// handle received PING | ||
|
@@ -871,7 +919,7 @@ BOOST_AUTO_TEST_CASE(evictionWithOldNodeAnswering) | |
BOOST_REQUIRE(nodeTable->nodeExists(nodeId)); | ||
auto addedNode = nodeTable->nodeEntry(nodeId); | ||
BOOST_CHECK(addedNode->lastPongReceivedTime); | ||
auto sentPing = nodeTable->nodeValidation(nodeId); | ||
auto sentPing = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_CHECK(!sentPing.is_initialized()); | ||
// check that old node is most recently seen in the bucket | ||
BOOST_CHECK(nodeTable->bucketLastNode(bucketIndex)->id == nodeId); | ||
|
@@ -890,7 +938,7 @@ BOOST_AUTO_TEST_CASE(evictionWithOldNodeDropped) | |
|
||
nodeTableHost.start(); | ||
|
||
auto oldNodeId = nodeTable->bucketFirstNode(bucketIndex)->id; | ||
auto oldNode = nodeTable->bucketFirstNode(bucketIndex); | ||
|
||
// generate new address for the same bucket | ||
NodeID newNodeId; | ||
|
@@ -910,8 +958,8 @@ BOOST_AUTO_TEST_CASE(evictionWithOldNodeDropped) | |
this_thread::sleep_for(chrono::seconds(6)); | ||
|
||
// check that old node is evicted | ||
BOOST_CHECK(!nodeTable->nodeExists(oldNodeId)); | ||
BOOST_CHECK(!nodeTable->nodeValidation(oldNodeId).is_initialized()); | ||
BOOST_CHECK(!nodeTable->nodeExists(oldNode->id)); | ||
BOOST_CHECK(!nodeTable->nodeValidation(oldNode->endpoint).is_initialized()); | ||
// check that replacement node is active | ||
BOOST_CHECK(nodeTable->nodeExists(newNodeId)); | ||
auto newNode = nodeTable->nodeEntry(newNodeId); | ||
|
@@ -971,12 +1019,12 @@ BOOST_AUTO_TEST_CASE(addSelf) | |
auto nodePubKey = KeyPair::create().pub(); | ||
Node node(nodePubKey, nodeEndpoint); | ||
nodeTable->addNode(node); | ||
BOOST_CHECK(nodeTable->nodeValidation(nodePubKey)); | ||
BOOST_CHECK(nodeTable->nodeValidation(nodeEndpoint)); | ||
|
||
// Create self node and verify it isn't pinged | ||
Node self(nodeTableHost.m_alias.pub(), nodeEndpoint); | ||
Node self(nodeTableHost.m_alias.pub(), nodeTable->m_hostNodeEndpoint); | ||
nodeTable->addNode(self); | ||
BOOST_CHECK(!nodeTable->nodeValidation(nodeTableHost.m_alias.pub())); | ||
BOOST_CHECK(!nodeTable->nodeValidation(nodeTable->m_hostNodeEndpoint)); | ||
} | ||
|
||
BOOST_AUTO_TEST_CASE(findNodeIsSentAfterPong) | ||
|
@@ -1108,15 +1156,15 @@ BOOST_AUTO_TEST_CASE(addNodePingsNodeOnlyOnce) | |
auto nodePubKey = KeyPair::create().pub(); | ||
nodeTable->addNode(Node{nodePubKey, nodeEndpoint}); | ||
|
||
auto sentPing = nodeTable->nodeValidation(nodePubKey); | ||
auto sentPing = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_REQUIRE(sentPing.is_initialized()); | ||
|
||
this_thread::sleep_for(chrono::milliseconds(2000)); | ||
|
||
// add it for the second time | ||
nodeTable->addNode(Node{nodePubKey, nodeEndpoint}); | ||
|
||
auto sentPing2 = nodeTable->nodeValidation(nodePubKey); | ||
auto sentPing2 = nodeTable->nodeValidation(nodeEndpoint); | ||
BOOST_REQUIRE(sentPing2.is_initialized()); | ||
|
||
// check that Ping was sent only once, so Ping hash didn't change | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not your changes, but minor grammar issue (1st sent should be send)