Coverage Summary for Class: PeerExplorer (co.rsk.net.discovery)

Class Class, % Method, % Line, %
PeerExplorer 0% (0/1) 0% (0/38) 0% (0/160)


1 /* 2  * This file is part of RskJ 3  * Copyright (C) 2017 RSK Labs Ltd. 4  * 5  * This program is free software: you can redistribute it and/or modify 6  * it under the terms of the GNU Lesser General Public License as published by 7  * the Free Software Foundation, either version 3 of the License, or 8  * (at your option) any later version. 9  * 10  * This program is distributed in the hope that it will be useful, 11  * but WITHOUT ANY WARRANTY; without even the implied warranty of 12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13  * GNU Lesser General Public License for more details. 14  * 15  * You should have received a copy of the GNU Lesser General Public License 16  * along with this program. If not, see <http://www.gnu.org/licenses/>. 17  */ 18  19 package co.rsk.net.discovery; 20  21 import co.rsk.net.NodeID; 22 import co.rsk.net.discovery.message.*; 23 import co.rsk.net.discovery.table.NodeDistanceTable; 24 import co.rsk.net.discovery.table.OperationResult; 25 import co.rsk.net.discovery.table.PeerDiscoveryRequestBuilder; 26 import co.rsk.util.IpUtils; 27 import com.google.common.annotations.VisibleForTesting; 28 import org.apache.commons.lang3.StringUtils; 29 import org.ethereum.crypto.ECKey; 30 import org.ethereum.net.rlpx.Node; 31 import org.slf4j.Logger; 32 import org.slf4j.LoggerFactory; 33  34 import java.net.InetSocketAddress; 35 import java.security.SecureRandom; 36 import java.util.*; 37 import java.util.concurrent.ConcurrentHashMap; 38 import java.util.concurrent.locks.Lock; 39 import java.util.concurrent.locks.ReentrantLock; 40 import java.util.stream.Collectors; 41  42 /** 43  * Created by mario on 10/02/17. 44  */ 45 public class PeerExplorer { 46  private static final Logger logger = LoggerFactory.getLogger(PeerExplorer.class); 47  private static final int MAX_NODES_PER_MSG = 20; 48  private static final int MAX_NODES_TO_ASK = 24; 49  private static final int MAX_NODES_TO_CHECK = 16; 50  private static final int RETRIES_COUNT = 3; 51  52  private final Set<InetSocketAddress> bootNodes = ConcurrentHashMap.newKeySet(); 53  private final Map<String, PeerDiscoveryRequest> pendingPingRequests = new ConcurrentHashMap<>(); 54  private final Map<String, PeerDiscoveryRequest> pendingFindNodeRequests = new ConcurrentHashMap<>(); 55  56  private final Map<NodeID, Node> establishedConnections = new ConcurrentHashMap<>(); 57  private final Integer networkId; 58  59  private UDPChannel udpChannel; 60  61  private final ECKey key; 62  63  private final Node localNode; 64  65  private final NodeDistanceTable distanceTable; 66  67  private final Lock updateEntryLock; 68  69  private final PeerExplorerCleaner cleaner; 70  71  private final NodeChallengeManager challengeManager; 72  73  private long requestTimeout; 74  75  public PeerExplorer(List<String> initialBootNodes, Node localNode, NodeDistanceTable distanceTable, ECKey key, long reqTimeOut, long updatePeriod, long cleanPeriod, Integer networkId) { 76  this.localNode = localNode; 77  this.key = key; 78  this.distanceTable = distanceTable; 79  this.updateEntryLock = new ReentrantLock(); 80  this.networkId = networkId; 81  loadInitialBootNodes(initialBootNodes); 82  83  this.cleaner = new PeerExplorerCleaner(this, updatePeriod, cleanPeriod); 84  this.challengeManager = new NodeChallengeManager(); 85  this.requestTimeout = reqTimeOut; 86  } 87  88  public void start() { 89  this.cleaner.run(); 90  this.startConversationWithNewNodes(); 91  } 92  93  public Set<String> startConversationWithNewNodes() { 94  Set<String> sentAddresses = new HashSet<>(); 95  96  for (InetSocketAddress nodeAddress : this.bootNodes) { 97  sendPing(nodeAddress, 1); 98  sentAddresses.add(nodeAddress.toString()); 99  } 100  101  this.bootNodes.removeAll(pendingPingRequests.values().stream() 102  .map(PeerDiscoveryRequest::getAddress).collect(Collectors.toList())); 103  104  return sentAddresses; 105  } 106  107  public void setUDPChannel(UDPChannel udpChannel) { 108  this.udpChannel = udpChannel; 109  } 110  111  112  public void handleMessage(DiscoveryEvent event) { 113  DiscoveryMessageType type = event.getMessage().getMessageType(); 114  //If this is not from my network ignore it. But if the messages do not 115  //have a networkId in the message yet, then just let them through, for now. 116  if (event.getMessage().getNetworkId().isPresent() && 117  event.getMessage().getNetworkId().getAsInt() != this.networkId) { 118  return; 119  } 120  if (type == DiscoveryMessageType.PING) { 121  this.handlePingMessage(event.getAddress(), (PingPeerMessage) event.getMessage()); 122  } 123  124  if (type == DiscoveryMessageType.PONG) { 125  this.handlePong(event.getAddress(), (PongPeerMessage) event.getMessage()); 126  } 127  128  if (type == DiscoveryMessageType.FIND_NODE) { 129  this.handleFindNode((FindNodePeerMessage) event.getMessage()); 130  } 131  132  if (type == DiscoveryMessageType.NEIGHBORS) { 133  this.handleNeighborsMessage(event.getAddress(), (NeighborsPeerMessage) event.getMessage()); 134  } 135  } 136  137  public void handlePingMessage(InetSocketAddress address, PingPeerMessage message) { 138  this.sendPong(address, message); 139  140  Node connectedNode = this.establishedConnections.get(message.getNodeId()); 141  142  if (connectedNode == null) { 143  this.sendPing(address, 1); 144  } else { 145  updateEntry(connectedNode); 146  } 147  } 148  149  public void handlePong(InetSocketAddress pongAddress, PongPeerMessage message) { 150  PeerDiscoveryRequest request = this.pendingPingRequests.get(message.getMessageId()); 151  152  if (request != null && request.validateMessageResponse(pongAddress, message)) { 153  this.pendingPingRequests.remove(message.getMessageId()); 154  NodeChallenge challenge = this.challengeManager.removeChallenge(message.getMessageId()); 155  if (challenge == null) { 156  this.addConnection(message, request.getAddress().getHostString(), request.getAddress().getPort()); 157  } 158  } 159  } 160  161  public void handleFindNode(FindNodePeerMessage message) { 162  NodeID nodeId = message.getNodeId(); 163  Node connectedNode = this.establishedConnections.get(nodeId); 164  165  if (connectedNode != null) { 166  List<Node> nodesToSend = this.distanceTable.getClosestNodes(nodeId); 167  logger.debug("About to send [{}] neighbors to ip[{}] port[{}] nodeId[{}]", nodesToSend.size(), connectedNode.getHost(), connectedNode.getPort(), connectedNode.getHexId()); 168  this.sendNeighbors(connectedNode.getAddress(), nodesToSend, message.getMessageId()); 169  updateEntry(connectedNode); 170  } 171  } 172  173  public void handleNeighborsMessage(InetSocketAddress neighborsResponseAddress, NeighborsPeerMessage message) { 174  Node connectedNode = this.establishedConnections.get(message.getNodeId()); 175  176  if (connectedNode != null) { 177  logger.debug("Neighbors received from [{}]", connectedNode.getHexId()); 178  PeerDiscoveryRequest request = this.pendingFindNodeRequests.remove(message.getMessageId()); 179  180  if (request != null && request.validateMessageResponse(neighborsResponseAddress, message)) { 181  List<Node> nodes = (message.countNodes() > MAX_NODES_PER_MSG) ? message.getNodes().subList(0, MAX_NODES_PER_MSG -1) : message.getNodes(); 182  nodes.stream().filter(n -> !StringUtils.equals(n.getHexId(), this.localNode.getHexId())) 183  .forEach(node -> this.bootNodes.add(node.getAddress())); 184  this.startConversationWithNewNodes(); 185  } 186  updateEntry(connectedNode); 187  } 188  } 189  190  public List<Node> getNodes() { 191  return new ArrayList<>(this.establishedConnections.values()); 192  } 193  194  public PingPeerMessage sendPing(InetSocketAddress nodeAddress, int attempt) { 195  return sendPing(nodeAddress, attempt, null); 196  } 197  198  public PingPeerMessage sendPing(InetSocketAddress nodeAddress, int attempt, Node node) { 199  PingPeerMessage nodeMessage = checkPendingPeerToAddress(nodeAddress); 200  201  if (nodeMessage != null) { 202  return nodeMessage; 203  } 204  205  InetSocketAddress localAddress = this.localNode.getAddress(); 206  String id = UUID.randomUUID().toString(); 207  nodeMessage = PingPeerMessage.create( 208  localAddress.getAddress().getHostAddress(), 209  localAddress.getPort(), 210  id, this.key, this.networkId); 211  udpChannel.write(new DiscoveryEvent(nodeMessage, nodeAddress)); 212  213  PeerDiscoveryRequest request = PeerDiscoveryRequestBuilder.builder().messageId(id) 214  .message(nodeMessage).address(nodeAddress).expectedResponse(DiscoveryMessageType.PONG).relatedNode(node) 215  .expirationPeriod(requestTimeout).attemptNumber(attempt).build(); 216  217  pendingPingRequests.put(nodeMessage.getMessageId(), request); 218  219  return nodeMessage; 220  } 221  222  private void updateEntry(Node connectedNode) { 223  try{ 224  updateEntryLock.lock(); 225  this.distanceTable.updateEntry(connectedNode); 226  } finally { 227  updateEntryLock.unlock(); 228  } 229  } 230  231  private PingPeerMessage checkPendingPeerToAddress(InetSocketAddress address) { 232  for (PeerDiscoveryRequest req : this.pendingPingRequests.values()) { 233  if (req.getAddress().equals(address)) { 234  return (PingPeerMessage) req.getMessage(); 235  } 236  } 237  238  return null; 239  } 240  241  public PongPeerMessage sendPong(InetSocketAddress nodeAddress, PingPeerMessage message) { 242  InetSocketAddress localAddress = this.localNode.getAddress(); 243  PongPeerMessage pongPeerMessage = PongPeerMessage.create(localAddress.getHostName(), localAddress.getPort(), message.getMessageId(), this.key, this.networkId); 244  udpChannel.write(new DiscoveryEvent(pongPeerMessage, nodeAddress)); 245  246  return pongPeerMessage; 247  } 248  249  public FindNodePeerMessage sendFindNode(Node node) { 250  InetSocketAddress nodeAddress = node.getAddress(); 251  String id = UUID.randomUUID().toString(); 252  FindNodePeerMessage findNodePeerMessage = FindNodePeerMessage.create(this.key.getNodeId(), id, this.key, this.networkId); 253  udpChannel.write(new DiscoveryEvent(findNodePeerMessage, nodeAddress)); 254  PeerDiscoveryRequest request = PeerDiscoveryRequestBuilder.builder().messageId(id).relatedNode(node) 255  .message(findNodePeerMessage).address(nodeAddress).expectedResponse(DiscoveryMessageType.NEIGHBORS) 256  .expirationPeriod(requestTimeout).build(); 257  pendingFindNodeRequests.put(findNodePeerMessage.getMessageId(), request); 258  259  return findNodePeerMessage; 260  } 261  262  public NeighborsPeerMessage sendNeighbors(InetSocketAddress nodeAddress, List<Node> nodes, String id) { 263  List<Node> nodesToSend = getRandomizeLimitedList(nodes, MAX_NODES_PER_MSG, 5); 264  NeighborsPeerMessage sendNodesMessage = NeighborsPeerMessage.create(nodesToSend, id, this.key, networkId); 265  udpChannel.write(new DiscoveryEvent(sendNodesMessage, nodeAddress)); 266  logger.debug(" [{}] Neighbors Sent to ip:[{}] port:[{}]", nodesToSend.size(), nodeAddress.getAddress().getHostAddress(), nodeAddress.getPort()); 267  268  return sendNodesMessage; 269  } 270  271  public void purgeRequests() { 272  List<PeerDiscoveryRequest> oldPingRequests = removeExpiredRequests(this.pendingPingRequests); 273  removeExpiredChallenges(oldPingRequests); 274  resendExpiredPing(oldPingRequests); 275  removeConnections(oldPingRequests.stream(). 276  filter(r -> r.getAttemptNumber() >= RETRIES_COUNT).collect(Collectors.toList())); 277  278  removeExpiredRequests(this.pendingFindNodeRequests); 279  } 280  281  public void clean() { 282  this.purgeRequests(); 283  } 284  285  public void update() { 286  List<Node> closestNodes = this.distanceTable.getClosestNodes(this.localNode.getId()); 287  this.askForMoreNodes(closestNodes); 288  this.checkPeersPulse(closestNodes); 289  } 290  291  private void checkPeersPulse(List<Node> closestNodes) { 292  List<Node> nodesToCheck = this.getRandomizeLimitedList(closestNodes, MAX_NODES_TO_CHECK, 10); 293  nodesToCheck.forEach(node -> sendPing(node.getAddress(), 1, node)); 294  } 295  296  private void askForMoreNodes(List<Node> closestNodes) { 297  List<Node> nodesToAsk = getRandomizeLimitedList(closestNodes, MAX_NODES_TO_ASK, 5); 298  nodesToAsk.forEach(this::sendFindNode); 299  } 300  301  private List<PeerDiscoveryRequest> removeExpiredRequests(Map<String, PeerDiscoveryRequest> pendingRequests) { 302  List<PeerDiscoveryRequest> requests = pendingRequests.values().stream() 303  .filter(PeerDiscoveryRequest::hasExpired).collect(Collectors.toList()); 304  requests.forEach(r -> pendingRequests.remove(r.getMessageId())); 305  306  return requests; 307  } 308  309  private void removeExpiredChallenges(List<PeerDiscoveryRequest> peerDiscoveryRequests) { 310  peerDiscoveryRequests.stream().forEach(r -> challengeManager.removeChallenge(r.getMessageId())); 311  } 312  313  private void resendExpiredPing(List<PeerDiscoveryRequest> peerDiscoveryRequests) { 314  peerDiscoveryRequests.stream().filter(r -> r.getAttemptNumber() < RETRIES_COUNT) 315  .forEach(r -> sendPing(r.getAddress(), r.getAttemptNumber() + 1)); 316  } 317  318  private void removeConnections(List<PeerDiscoveryRequest> expiredRequests) { 319  for (PeerDiscoveryRequest req : expiredRequests) { 320  Node node = req.getRelatedNode(); 321  322  if (node != null) { 323  this.establishedConnections.remove(node.getId()); 324  this.distanceTable.removeNode(node); 325  } 326  } 327  } 328  329  private void addConnection(PongPeerMessage message, String ip, int port) { 330  Node senderNode = new Node(message.getNodeId().getID(), ip, port); 331  if (!StringUtils.equals(senderNode.getHexId(), this.localNode.getHexId())) { 332  OperationResult result = this.distanceTable.addNode(senderNode); 333  334  if (result.isSuccess()) { 335  NodeID senderId = senderNode.getId(); 336  this.establishedConnections.put(senderId, senderNode); 337  logger.debug("New Peer found ip:[{}] port[{}]", ip, port); 338  } else { 339  this.challengeManager.startChallenge(result.getAffectedEntry().getNode(), senderNode, this); 340  } 341  } 342  } 343  344  private void loadInitialBootNodes(List<String> nodes) { 345  bootNodes.addAll(IpUtils.parseAddresses(nodes)); 346  } 347  348  private List<Node> getRandomizeLimitedList(List<Node> nodes, int maxNumber, int randomElements) { 349  if (nodes.size() <= maxNumber) { 350  return nodes; 351  } else { 352  List<Node> ret = new ArrayList<>(); 353  int limit = maxNumber - randomElements; 354  ret.addAll(nodes.subList(0, limit - 1)); 355  ret.addAll(collectRandomNodes(nodes.subList(limit, nodes.size()), randomElements)); 356  357  return ret; 358  } 359  } 360  361  private Set<Node> collectRandomNodes(List<Node> originalList, int elementsNbr) { 362  Set<Node> ret = new HashSet<>(); 363  SecureRandom rnd = new SecureRandom(); 364  365  while (ret.size() < elementsNbr) { 366  int i = rnd.nextInt(originalList.size()); 367  ret.add(originalList.get(i)); 368  } 369  370  return ret; 371  } 372  373  @VisibleForTesting 374  public NodeChallengeManager getChallengeManager() { 375  return challengeManager; 376  } 377 }