/* This file is part of cpp-ethereum. cpp-ethereum is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. cpp-ethereum is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with cpp-ethereum. If not, see . */ /** @file NodeTable.cpp * @author Alex Leverington * @date 2014 */ #include "NodeTable.h" using namespace std; using namespace dev; using namespace dev::p2p; NodeTable::NodeTable(ba::io_service& _io, KeyPair _alias, uint16_t _listenPort): m_node(Node(_alias.pub(), bi::udp::endpoint())), m_secret(_alias.sec()), m_socket(new NodeSocket(_io, *this, _listenPort)), m_socketPtr(m_socket.get()), m_io(_io), m_bucketRefreshTimer(m_io), m_evictionCheckTimer(m_io) { for (unsigned i = 0; i < s_bins; i++) { m_state[i].distance = i; m_state[i].modified = chrono::steady_clock::now() - chrono::seconds(1); } m_socketPtr->connect(); doRefreshBuckets(boost::system::error_code()); } NodeTable::~NodeTable() { m_evictionCheckTimer.cancel(); m_bucketRefreshTimer.cancel(); m_socketPtr->disconnect(); } void NodeTable::join() { doFindNode(m_node.id); } list NodeTable::nodes() const { list nodes; Guard l(x_nodes); for (auto& i: m_nodes) nodes.push_back(i.second->id); return move(nodes); } list NodeTable::state() const { list ret; Guard l(x_state); for (auto s: m_state) for (auto n: s.nodes) ret.push_back(*n.lock()); return move(ret); } NodeTable::NodeEntry NodeTable::operator[](NodeId _id) { Guard l(x_nodes); return *m_nodes[_id]; } void NodeTable::requestNeighbours(NodeEntry const& _node, NodeId _target) const { FindNode p(_node.endpoint.udp, _target); p.sign(m_secret); m_socketPtr->send(p); } void NodeTable::doFindNode(NodeId _node, unsigned _round, shared_ptr>> _tried) { if (!m_socketPtr->isOpen() || _round == s_maxSteps) return; if (_round == s_maxSteps) { clog(NodeTableNote) << "Terminating doFindNode after " << _round << " rounds."; return; } else if(!_round && !_tried) // initialized _tried on first round _tried.reset(new set>()); auto nearest = findNearest(_node); list> tried; for (unsigned i = 0; i < nearest.size() && tried.size() < s_alpha; i++) if (!_tried->count(nearest[i])) { auto r = nearest[i]; tried.push_back(r); FindNode p(r->endpoint.udp, _node); p.sign(m_secret); m_socketPtr->send(p); } if (tried.empty()) { clog(NodeTableNote) << "Terminating doFindNode after " << _round << " rounds."; return; } while (!tried.empty()) { _tried->insert(tried.front()); tried.pop_front(); } auto self(shared_from_this()); m_evictionCheckTimer.expires_from_now(boost::posix_time::milliseconds(c_reqTimeout.count())); m_evictionCheckTimer.async_wait([this, self, _node, _round, _tried](boost::system::error_code const& _ec) { if (_ec) return; doFindNode(_node, _round + 1, _tried); }); } vector> NodeTable::findNearest(NodeId _target) { // send s_alpha FindNode packets to nodes we know, closest to target static unsigned lastBin = s_bins - 1; unsigned head = dist(m_node.id, _target); unsigned tail = head == 0 ? lastBin : (head - 1) % s_bins; map>> found; unsigned count = 0; // if d is 0, then we roll look forward, if last, we reverse, else, spread from d if (head > 1 && tail != lastBin) while (head != tail && head < s_bins && count < s_bucketSize) { Guard l(x_state); for (auto n: m_state[head].nodes) if (auto p = n.lock()) { if (count < s_bucketSize) found[dist(_target, p->id)].push_back(p); else break; } if (count < s_bucketSize && tail) for (auto n: m_state[tail].nodes) if (auto p = n.lock()) { if (count < s_bucketSize) found[dist(_target, p->id)].push_back(p); else break; } head++; if (tail) tail--; } else if (head < 2) while (head < s_bins && count < s_bucketSize) { Guard l(x_state); for (auto n: m_state[head].nodes) if (auto p = n.lock()) { if (count < s_bucketSize) found[dist(_target, p->id)].push_back(p); else break; } head++; } else while (tail > 0 && count < s_bucketSize) { Guard l(x_state); for (auto n: m_state[tail].nodes) if (auto p = n.lock()) { if (count < s_bucketSize) found[dist(_target, p->id)].push_back(p); else break; } tail--; } vector> ret; for (auto& nodes: found) for (auto n: nodes.second) ret.push_back(n); return move(ret); } void NodeTable::ping(bi::udp::endpoint _to) const { PingNode p(_to, m_node.endpoint.udp.address().to_string(), m_node.endpoint.udp.port()); p.sign(m_secret); m_socketPtr->send(p); } void NodeTable::ping(NodeEntry* _n) const { if (_n) ping(_n->endpoint.udp); } void NodeTable::evict(shared_ptr _leastSeen, shared_ptr _new) { if (!m_socketPtr->isOpen()) return; Guard l(x_evictions); m_evictions.push_back(EvictionTimeout(make_pair(_leastSeen->id,chrono::steady_clock::now()), _new->id)); if (m_evictions.size() == 1) doCheckEvictions(boost::system::error_code()); m_evictions.push_back(EvictionTimeout(make_pair(_leastSeen->id,chrono::steady_clock::now()), _new->id)); ping(_leastSeen.get()); } void NodeTable::noteNode(Public const& _pubk, bi::udp::endpoint const& _endpoint) { // Don't add ourself if (_pubk == m_node.address()) return; shared_ptr node; { Guard l(x_nodes); auto n = m_nodes.find(_pubk); if (n == m_nodes.end()) { node.reset(new NodeEntry(m_node, _pubk, _endpoint)); m_nodes[_pubk] = node; // clog(NodeTableMessageSummary) << "Adding node to cache: " << _pubk; } else { node = n->second; // clog(NodeTableMessageSummary) << "Found node in cache: " << _pubk; } } // todo: why is this necessary? if (!!node) noteNode(node); } void NodeTable::noteNode(shared_ptr _n) { shared_ptr contested; { NodeBucket& s = bucket(_n.get()); Guard l(x_state); s.nodes.remove_if([&_n](weak_ptr n) { if (n.lock() == _n) return true; return false; }); if (s.nodes.size() >= s_bucketSize) { contested = s.nodes.front().lock(); if (!contested) { s.nodes.pop_front(); s.nodes.push_back(_n); } } else s.nodes.push_back(_n); } if (contested) evict(contested, _n); } void NodeTable::dropNode(shared_ptr _n) { NodeBucket &s = bucket(_n.get()); { Guard l(x_state); s.nodes.remove_if([&_n](weak_ptr n) { return n.lock() == _n; }); } Guard l(x_nodes); m_nodes.erase(_n->id); } NodeTable::NodeBucket& NodeTable::bucket(NodeEntry const* _n) { return m_state[_n->distance - 1]; } void NodeTable::onReceived(UDPSocketFace*, bi::udp::endpoint const& _from, bytesConstRef _packet) { // h256 + Signature + RLP (smallest possible packet is empty neighbours packet which is 3 bytes) if (_packet.size() < h256::size + Signature::size + 3) { clog(NodeTableMessageSummary) << "Invalid Message size from " << _from.address().to_string() << ":" << _from.port(); return; } bytesConstRef signedBytes(_packet.cropped(h256::size, _packet.size() - h256::size)); h256 hashSigned(sha3(signedBytes)); if (!_packet.cropped(0, h256::size).contentsEqual(hashSigned.asBytes())) { clog(NodeTableMessageSummary) << "Invalid Message hash from " << _from.address().to_string() << ":" << _from.port(); return; } bytesConstRef rlpBytes(signedBytes.cropped(Signature::size, signedBytes.size() - Signature::size)); RLP rlp(rlpBytes); unsigned itemCount = rlp.itemCount(); bytesConstRef sigBytes(_packet.cropped(h256::size, Signature::size)); Public nodeid(dev::recover(*(Signature const*)sigBytes.data(), sha3(rlpBytes))); if (!nodeid) { clog(NodeTableMessageSummary) << "Invalid Message signature from " << _from.address().to_string() << ":" << _from.port(); return; } noteNode(nodeid, _from); try { switch (itemCount) { case 1: { // clog(NodeTableMessageSummary) << "Received Pong from " << _from.address().to_string() << ":" << _from.port(); Pong in = Pong::fromBytesConstRef(_from, rlpBytes); // whenever a pong is received, first check if it's in m_evictions break; } case 2: if (rlp[0].isList()) { Neighbours in = Neighbours::fromBytesConstRef(_from, rlpBytes); // clog(NodeTableMessageSummary) << "Received " << in.nodes.size() << " Neighbours from " << _from.address().to_string() << ":" << _from.port(); for (auto n: in.nodes) noteNode(n.node, bi::udp::endpoint(bi::address::from_string(n.ipAddress), n.port)); } else { // clog(NodeTableMessageSummary) << "Received FindNode from " << _from.address().to_string() << ":" << _from.port(); FindNode in = FindNode::fromBytesConstRef(_from, rlpBytes); vector> nearest = findNearest(in.target); static unsigned const nlimit = (m_socketPtr->maxDatagramSize - 11) / 86; for (unsigned offset = 0; offset < nearest.size(); offset += nlimit) { Neighbours out(_from, nearest, offset, nlimit); out.sign(m_secret); m_socketPtr->send(out); } } break; case 3: { // clog(NodeTableMessageSummary) << "Received PingNode from " << _from.address().to_string() << ":" << _from.port(); PingNode in = PingNode::fromBytesConstRef(_from, rlpBytes); Pong p(_from); p.replyTo = sha3(rlpBytes); p.sign(m_secret); m_socketPtr->send(p); break; } default: clog(NodeTableMessageSummary) << "Invalid Message received from " << _from.address().to_string() << ":" << _from.port(); return; } } catch (...) { clog(NodeTableWarn) << "Exception processing message from " << _from.address().to_string() << ":" << _from.port(); } } void NodeTable::doCheckEvictions(boost::system::error_code const& _ec) { if (_ec || !m_socketPtr->isOpen()) return; auto self(shared_from_this()); m_evictionCheckTimer.expires_from_now(c_evictionCheckInterval); m_evictionCheckTimer.async_wait([this, self](boost::system::error_code const& _ec) { if (_ec) return; bool evictionsRemain = false; list> drop; { Guard le(x_evictions); Guard ln(x_nodes); for (auto& e: m_evictions) if (chrono::steady_clock::now() - e.first.second > c_reqTimeout) if (auto n = m_nodes[e.second]) drop.push_back(n); evictionsRemain = m_evictions.size() - drop.size() > 0; } drop.unique(); for (auto n: drop) dropNode(n); if (evictionsRemain) doCheckEvictions(boost::system::error_code()); }); } void NodeTable::doRefreshBuckets(boost::system::error_code const& _ec) { if (_ec) return; clog(NodeTableNote) << "refreshing buckets"; bool connected = m_socketPtr->isOpen(); bool refreshed = false; if (connected) { Guard l(x_state); for (auto& d: m_state) if (chrono::steady_clock::now() - d.modified > c_bucketRefresh) while (!d.nodes.empty()) { auto n = d.nodes.front(); if (auto p = n.lock()) { refreshed = true; ping(p.get()); break; } d.nodes.pop_front(); } } unsigned nextRefresh = connected ? (refreshed ? 200 : c_bucketRefresh.count()*1000) : 10000; auto runcb = [this](boost::system::error_code const& error) -> void { doRefreshBuckets(error); }; m_bucketRefreshTimer.expires_from_now(boost::posix_time::milliseconds(nextRefresh)); m_bucketRefreshTimer.async_wait(runcb); }