/* * 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.p2p; import org.ethereum.config.SystemProperties; import org.ethereum.core.Block; import org.ethereum.core.Transaction; import org.ethereum.listener.EthereumListener; import org.ethereum.manager.WorldManager; import org.ethereum.net.MessageQueue; import org.ethereum.net.client.Capability; import org.ethereum.net.client.ConfigCapabilities; import org.ethereum.net.eth.message.NewBlockMessage; import org.ethereum.net.eth.message.TransactionsMessage; import org.ethereum.net.message.ReasonCode; import org.ethereum.net.message.StaticMessages; import org.ethereum.net.server.Channel; import org.ethereum.net.shh.ShhHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; import org.ethereum.net.swarm.Util; import org.ethereum.net.swarm.bzz.BzzHandler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.*; import static org.ethereum.net.eth.EthVersion.*; import static org.ethereum.net.message.StaticMessages.*; /** * Process the basic protocol messages between every peer on the network. * * Peers can send/receive * <ul> * <li>HELLO : Announce themselves to the network</li> * <li>DISCONNECT : Disconnect themselves from the network</li> * <li>GET_PEERS : Request a list of other knows peers</li> * <li>PEERS : Send a list of known peers</li> * <li>PING : Check if another peer is still alive</li> * <li>PONG : Confirm that they themselves are still alive</li> * </ul> */ @Component @Scope("prototype") public class P2pHandler extends SimpleChannelInboundHandler<P2pMessage> { public final static byte VERSION = 4; public final static byte[] SUPPORTED_VERSIONS = {4, 5}; private final static Logger logger = LoggerFactory.getLogger("net"); private static ScheduledExecutorService pingTimer = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { public Thread newThread(Runnable r) { return new Thread(r, "P2pPingTimer"); } }); private MessageQueue msgQueue; private boolean peerDiscoveryMode = false; private HelloMessage handshakeHelloMessage = null; private int ethInbound; private int ethOutbound; @Autowired EthereumListener ethereumListener; @Autowired ConfigCapabilities configCapabilities; @Autowired SystemProperties config; private Channel channel; private ScheduledFuture<?> pingTask; public P2pHandler() { this.peerDiscoveryMode = false; } public P2pHandler(MessageQueue msgQueue, boolean peerDiscoveryMode) { this.msgQueue = msgQueue; this.peerDiscoveryMode = peerDiscoveryMode; } public void setPeerDiscoveryMode(boolean peerDiscoveryMode) { this.peerDiscoveryMode = peerDiscoveryMode; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { logger.debug("P2P protocol activated"); msgQueue.activate(ctx); ethereumListener.trace("P2P protocol activated"); startTimers(); } @Override public void channelRead0(final ChannelHandlerContext ctx, P2pMessage msg) throws InterruptedException { if (P2pMessageCodes.inRange(msg.getCommand().asByte())) logger.trace("P2PHandler invoke: [{}]", msg.getCommand()); ethereumListener.trace(String.format("P2PHandler invoke: [%s]", msg.getCommand())); switch (msg.getCommand()) { case HELLO: msgQueue.receivedMessage(msg); setHandshake((HelloMessage) msg, ctx); // sendGetPeers(); break; case DISCONNECT: msgQueue.receivedMessage(msg); channel.getNodeStatistics().nodeDisconnectedRemote(((DisconnectMessage) msg).getReason()); processDisconnect(ctx, (DisconnectMessage) msg); break; case PING: msgQueue.receivedMessage(msg); ctx.writeAndFlush(PONG_MESSAGE); break; case PONG: msgQueue.receivedMessage(msg); channel.getNodeStatistics().lastPongReplyTime.set(Util.curTime()); break; case PEERS: msgQueue.receivedMessage(msg); if (peerDiscoveryMode || !handshakeHelloMessage.getCapabilities().contains(Capability.ETH)) { disconnect(ReasonCode.REQUESTED); killTimers(); ctx.close().sync(); ctx.disconnect().sync(); } break; default: ctx.fireChannelRead(msg); break; } } private void disconnect(ReasonCode reasonCode) { msgQueue.sendMessage(new DisconnectMessage(reasonCode)); channel.getNodeStatistics().nodeDisconnectedLocal(reasonCode); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { logger.debug("channel inactive: ", ctx.toString()); this.killTimers(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.warn("P2p handling failed", cause); ctx.close(); killTimers(); } private void processDisconnect(ChannelHandlerContext ctx, DisconnectMessage msg) { if (logger.isInfoEnabled() && msg.getReason() == ReasonCode.USELESS_PEER) { if (channel.getNodeStatistics().ethInbound.get() - ethInbound > 1 || channel.getNodeStatistics().ethOutbound.get() - ethOutbound > 1) { // it means that we've been disconnected // after some incorrect action from our peer // need to log this moment logger.debug("From: \t{}\t [DISCONNECT reason=BAD_PEER_ACTION]", channel); } } ctx.close(); killTimers(); } private void sendGetPeers() { msgQueue.sendMessage(StaticMessages.GET_PEERS_MESSAGE); } public void setHandshake(HelloMessage msg, ChannelHandlerContext ctx) { channel.getNodeStatistics().setClientId(msg.getClientId()); channel.getNodeStatistics().capabilities.clear(); channel.getNodeStatistics().capabilities.addAll(msg.getCapabilities()); this.ethInbound = (int) channel.getNodeStatistics().ethInbound.get(); this.ethOutbound = (int) channel.getNodeStatistics().ethOutbound.get(); this.handshakeHelloMessage = msg; if (!isProtocolVersionSupported(msg.getP2PVersion())) { disconnect(ReasonCode.INCOMPATIBLE_PROTOCOL); } else { List<Capability> capInCommon = getSupportedCapabilities(msg); channel.initMessageCodes(capInCommon); for (Capability capability : capInCommon) { if (capability.getName().equals(Capability.ETH)) { // Activate EthHandler for this peer channel.activateEth(ctx, fromCode(capability.getVersion())); } else if (capability.getName().equals(Capability.SHH) && capability.getVersion() == ShhHandler.VERSION) { // Activate ShhHandler for this peer channel.activateShh(ctx); } else if (capability.getName().equals(Capability.BZZ) && capability.getVersion() == BzzHandler.VERSION) { // Activate ShhHandler for this peer channel.activateBzz(ctx); } } //todo calculate the Offsets ethereumListener.onHandShakePeer(channel, msg); } } /** * submit transaction to the network * * @param tx - fresh transaction object */ public void sendTransaction(Transaction tx) { TransactionsMessage msg = new TransactionsMessage(tx); msgQueue.sendMessage(msg); } public void sendNewBlock(Block block) { NewBlockMessage msg = new NewBlockMessage(block, block.getDifficulty()); msgQueue.sendMessage(msg); } public void sendDisconnect() { msgQueue.disconnect(); } public HelloMessage getHandshakeHelloMessage() { return handshakeHelloMessage; } private void startTimers() { // sample for pinging in background pingTask = pingTimer.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { msgQueue.sendMessage(PING_MESSAGE); } catch (Throwable t) { logger.error("Unhandled exception", t); } } }, 2, config.getProperty("peer.p2p.pingInterval", 5L), TimeUnit.SECONDS); } public void killTimers() { pingTask.cancel(false); msgQueue.close(); } public void setMsgQueue(MessageQueue msgQueue) { this.msgQueue = msgQueue; } public void setChannel(Channel channel) { this.channel = channel; } public static boolean isProtocolVersionSupported(byte ver) { for (byte v : SUPPORTED_VERSIONS) { if (v == ver) return true; } return false; } public List<Capability> getSupportedCapabilities(HelloMessage hello) { List<Capability> configCaps = configCapabilities.getConfigCapabilities(); List<Capability> supported = new ArrayList<>(); List<Capability> eths = new ArrayList<>(); for (Capability cap : hello.getCapabilities()) { if (configCaps.contains(cap)) { if (cap.isEth()) { eths.add(cap); } else { supported.add(cap); } } } if (eths.isEmpty()) { return supported; } // we need to pick up // the most recent Eth version Capability highest = null; for (Capability eth : eths) { if (highest == null || highest.getVersion() < eth.getVersion()) { highest = eth; } } supported.add(highest); return supported; } }