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 }