/* * Copyright (c) [2016] [ <ether.camp> ] * This file is part of the ethereumJ library. * * The ethereumJ library is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * The ethereumJ library 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>. */ package org.ethereum.net.rlpx.discover; import org.ethereum.net.rlpx.*; import org.ethereum.net.rlpx.discover.table.KademliaOptions; import org.ethereum.net.swarm.Util; import org.slf4j.LoggerFactory; import org.spongycastle.util.encoders.Hex; import java.net.InetSocketAddress; import java.util.List; import java.util.concurrent.TimeUnit; /** * The instance of this class responsible for discovery messages exchange with the specified Node * It also manages itself regarding inclusion/eviction from Kademlia table * * Created by Anton Nashatyrev on 14.07.2015. */ public class NodeHandler { static final org.slf4j.Logger logger = LoggerFactory.getLogger("discover"); static long PingTimeout = 15000; //KademliaOptions.REQ_TIMEOUT; static final int WARN_PACKET_SIZE = 1400; private static volatile int msgInCount = 0, msgOutCount = 0; private static boolean initialLogging = true; // gradually reducing log level for dumping discover messages // they are not so informative when everything is already up and running // but could be interesting when discovery just starts private void logMessage(Message msg, boolean inbound) { String s = String.format("%s[%s (%s)] %s", inbound ? " ===> " : "<=== ", msg.getClass().getSimpleName(), msg.getPacket().length, this); if (msgInCount > 1024) { logger.trace(s); } else { logger.debug(s); } if (!inbound && msg.getPacket().length > WARN_PACKET_SIZE) { logger.warn("Sending UDP packet exceeding safe size of {} bytes, actual: {} bytes", WARN_PACKET_SIZE, msg.getPacket().length); logger.warn(s); } if (initialLogging) { if (msgOutCount == 0) { logger.info("Pinging discovery nodes..."); } if (msgInCount == 0 && inbound) { logger.info("Received response."); } if (inbound && msg instanceof NeighborsMessage) { logger.info("New peers discovered."); initialLogging = false; } } if (inbound) msgInCount++; else msgOutCount++; } public enum State { /** * The new node was just discovered either by receiving it with Neighbours * message or by receiving Ping from a new node * In either case we are sending Ping and waiting for Pong * If the Pong is received the node becomes {@link #Alive} * If the Pong was timed out the node becomes {@link #Dead} */ Discovered, /** * The node didn't send the Pong message back withing acceptable timeout * This is the final state */ Dead, /** * The node responded with Pong and is now the candidate for inclusion to the table * If the table has bucket space for this node it is added to table and becomes {@link #Active} * If the table bucket is full this node is challenging with the old node from the bucket * if it wins then old node is dropped, and this node is added and becomes {@link #Active} * else this node becomes {@link #NonActive} */ Alive, /** * The node is included in the table. It may become {@link #EvictCandidate} if a new node * wants to become Active but the table bucket is full. */ Active, /** * This node is in the table but is currently challenging with a new Node candidate * to survive in the table bucket * If it wins then returns back to {@link #Active} state, else is evicted from the table * and becomes {@link #NonActive} */ EvictCandidate, /** * Veteran. It was Alive and even Active but is now retired due to loosing the challenge * with another Node. * For no this is the final state * It's an option for future to return veterans back to the table */ NonActive } Node node; NodeManager nodeManager; private NodeStatistics nodeStatistics; State state; boolean waitForPong = false; long pingSent; int pingTrials = 3; NodeHandler replaceCandidate; public NodeHandler(Node node, NodeManager nodeManager) { this.node = node; this.nodeManager = nodeManager; changeState(State.Discovered); } public InetSocketAddress getInetSocketAddress() { return new InetSocketAddress(node.getHost(), node.getPort()); } public Node getNode() { return node; } public State getState() { return state; } public NodeStatistics getNodeStatistics() { if (nodeStatistics == null) { nodeStatistics = new NodeStatistics(node); } return nodeStatistics; } private void challengeWith(NodeHandler replaceCandidate) { this.replaceCandidate = replaceCandidate; changeState(State.EvictCandidate); } // Manages state transfers private void changeState(State newState) { State oldState = state; if (newState == State.Discovered) { // will wait for Pong to assume this alive sendPing(); } if (!node.isDiscoveryNode()) { if (newState == State.Alive) { Node evictCandidate = nodeManager.table.addNode(this.node); if (evictCandidate == null) { newState = State.Active; } else { NodeHandler evictHandler = nodeManager.getNodeHandler(evictCandidate); if (evictHandler.state != State.EvictCandidate) { evictHandler.challengeWith(this); } } } if (newState == State.Active) { if (oldState == State.Alive) { // new node won the challenge nodeManager.table.addNode(node); } else if (oldState == State.EvictCandidate) { // nothing to do here the node is already in the table } else { // wrong state transition } } if (newState == State.NonActive) { if (oldState == State.EvictCandidate) { // lost the challenge // Removing ourselves from the table nodeManager.table.dropNode(node); // Congratulate the winner replaceCandidate.changeState(State.Active); } else if (oldState == State.Alive) { // ok the old node was better, nothing to do here } else { // wrong state transition } } } if (newState == State.EvictCandidate) { // trying to survive, sending ping and waiting for pong sendPing(); } state = newState; stateChanged(oldState, newState); } protected void stateChanged(State oldState, State newState) { logger.trace("State change " + oldState + " -> " + newState + ": " + this); nodeManager.stateChanged(this, oldState, newState); } void handlePing(PingMessage msg) { logMessage(msg, true); // logMessage(" ===> [PING] " + this); getNodeStatistics().discoverInPing.add(); if (!nodeManager.table.getNode().equals(node)) { sendPong(msg.getMdc()); } } void handlePong(PongMessage msg) { logMessage(msg, true); // logMessage(" ===> [PONG] " + this); if (waitForPong) { waitForPong = false; getNodeStatistics().discoverInPong.add(); getNodeStatistics().discoverMessageLatency.add(Util.curTime() - pingSent); getNodeStatistics().lastPongReplyTime.set(Util.curTime()); changeState(State.Alive); } } void handleNeighbours(NeighborsMessage msg) { logMessage(msg, true); // logMessage(" ===> [NEIGHBOURS] " + this + ", Count: " + msg.getNodes().size()); getNodeStatistics().discoverInNeighbours.add(); for (Node n : msg.getNodes()) { nodeManager.getNodeHandler(n); } } void handleFindNode(FindNodeMessage msg) { logMessage(msg, true); // logMessage(" ===> [FIND_NODE] " + this); getNodeStatistics().discoverInFind.add(); List<Node> closest = nodeManager.table.getClosestNodes(msg.getTarget()); Node publicHomeNode = nodeManager.getPublicHomeNode(); if (publicHomeNode != null) { if (closest.size() == KademliaOptions.BUCKET_SIZE) closest.remove(closest.size() - 1); closest.add(publicHomeNode); } sendNeighbours(closest); } void handleTimedOut() { waitForPong = false; if (--pingTrials > 0) { sendPing(); } else { if (state == State.Discovered) { changeState(State.Dead); } else if (state == State.EvictCandidate) { changeState(State.NonActive); } else { // TODO just influence to reputation } } } void sendPing() { if (waitForPong) { logger.trace("<=/= [PING] (Waiting for pong) " + this); } // logMessage("<=== [PING] " + this); Message ping = PingMessage.create(nodeManager.table.getNode(), getNode(), nodeManager.key); logMessage(ping, false); waitForPong = true; pingSent = Util.curTime(); sendMessage(ping); getNodeStatistics().discoverOutPing.add(); if (nodeManager.getPongTimer().isShutdown()) return; nodeManager.getPongTimer().schedule(new Runnable() { public void run() { try { if (waitForPong) { waitForPong = false; handleTimedOut(); } } catch (Throwable t) { logger.error("Unhandled exception", t); } } }, PingTimeout, TimeUnit.MILLISECONDS); } void sendPong(byte[] mdc) { // logMessage("<=== [PONG] " + this); Message pong = PongMessage.create(mdc, node, nodeManager.key); logMessage(pong, false); sendMessage(pong); getNodeStatistics().discoverOutPong.add(); } void sendNeighbours(List<Node> neighbours) { // logMessage("<=== [NEIGHBOURS] " + this); NeighborsMessage neighbors = NeighborsMessage.create(neighbours, nodeManager.key); logMessage(neighbors, false); sendMessage(neighbors); getNodeStatistics().discoverOutNeighbours.add(); } void sendFindNode(byte[] target) { // logMessage("<=== [FIND_NODE] " + this); Message findNode = FindNodeMessage.create(target, nodeManager.key); logMessage(findNode, false); sendMessage(findNode); getNodeStatistics().discoverOutFind.add(); } private void sendMessage(Message msg) { nodeManager.sendOutbound(new DiscoveryEvent(msg, getInetSocketAddress())); } @Override public String toString() { return "NodeHandler[state: " + state + ", node: " + node.getHost() + ":" + node.getPort() + ", id=" + (node.getId().length > 0 ? Hex.toHexString(node.getId(), 0, 4) : "empty") + "]"; } }