/* * Copyright 2011 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.devcoin.core; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import org.jboss.netty.bootstrap.ClientBootstrap; import org.jboss.netty.buffer.ChannelBuffer; import org.jboss.netty.buffer.ChannelBufferInputStream; import org.jboss.netty.buffer.ChannelBufferOutputStream; import org.jboss.netty.buffer.ChannelBuffers; import org.jboss.netty.channel.*; import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory; import org.jboss.netty.handler.codec.replay.ReplayingDecoder; import org.jboss.netty.handler.codec.replay.VoidEnum; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.SocketAddress; import java.util.Date; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static org.jboss.netty.channel.Channels.write; // TODO: Remove this class and refactor the way we build Netty pipelines. /** * <p>A {@code TCPNetworkConnection} is used for connecting to a Bitcoin node over the standard TCP/IP protocol.<p> * * <p>{@link TCPNetworkConnection#getHandler()} is part of a Netty Pipeline, downstream of other pipeline stages.</p> * */ public class TCPNetworkConnection implements NetworkConnection { private static final Logger log = LoggerFactory.getLogger(TCPNetworkConnection.class); // The IP address to which we are connecting. private InetAddress remoteIp; private final NetworkParameters params; private VersionMessage versionMessage; private BitcoinSerializer serializer = null; private VersionMessage myVersionMessage; private Channel channel; private NetworkHandler handler; // For ping nonces. private Random random = new Random(); /** * Construct a network connection with the given params and version. If you use this constructor you need to set * up the Netty pipelines and infrastructure yourself. If all you have is an IP address and port, use the static * connectTo method. * * @param params Defines which network to connect to and details of the protocol. * @param ver The VersionMessage to announce to the other side of the connection. */ public TCPNetworkConnection(NetworkParameters params, VersionMessage ver) { this.params = params; this.myVersionMessage = ver; this.serializer = new BitcoinSerializer(this.params); this.handler = new NetworkHandler(); } // Some members that are used for convenience APIs. If the app only uses PeerGroup then these won't be used. private static NioClientSocketChannelFactory channelFactory; private SettableFuture<TCPNetworkConnection> handshakeFuture; /** * Returns a future for a TCPNetworkConnection that is connected and version negotiated to the given remote address. * Behind the scenes this method sets up a thread pool and a Netty pipeline that uses it. The equivalent Netty code * is quite complex so use this method if you aren't writing a complex app. The future completes once version * handshaking is done, use .get() on the response to wait for it. * * @param params The network parameters to use (production or testnet) * @param address IP address and port to use * @param connectTimeoutMsec How long to wait before giving up and setting the future to failure. * @param peer If not null, this peer will be added to the pipeline. */ public static ListenableFuture<TCPNetworkConnection> connectTo(NetworkParameters params, InetSocketAddress address, int connectTimeoutMsec, @Nullable Peer peer) { synchronized (TCPNetworkConnection.class) { if (channelFactory == null) { ExecutorService bossExecutor = Executors.newCachedThreadPool(); ExecutorService workerExecutor = Executors.newCachedThreadPool(); channelFactory = new NioClientSocketChannelFactory(bossExecutor, workerExecutor); } } // Run the connection in the thread pool and wait for it to complete. ClientBootstrap clientBootstrap = new ClientBootstrap(channelFactory); ChannelPipeline pipeline = Channels.pipeline(); final TCPNetworkConnection conn = new TCPNetworkConnection(params, new VersionMessage(params, 0)); conn.handshakeFuture = SettableFuture.create(); conn.setRemoteAddress(address); pipeline.addLast("codec", conn.getHandler()); if (peer != null) pipeline.addLast("peer", peer.getHandler()); clientBootstrap.setPipeline(pipeline); clientBootstrap.setOption("connectTimeoutMillis", connectTimeoutMsec); ChannelFuture socketFuture = clientBootstrap.connect(address); // Once the socket is either connected on the TCP level, or failed ... socketFuture.addListener(new ChannelFutureListener() { public void operationComplete(ChannelFuture channelFuture) throws Exception { // Check if it failed ... if (channelFuture.isDone() && !channelFuture.isSuccess()) { // And complete the returned future with an exception. conn.handshakeFuture.setException(channelFuture.getCause()); } // Otherwise the handshakeFuture will be marked as completed once we did ver/verack exchange. } }); return conn.handshakeFuture; } public void writeMessage(Message message) throws IOException { write(channel, message); } private void onVersionMessage(Message m) throws IOException, ProtocolException { if (!(m instanceof VersionMessage)) { // Bad peers might not follow the protocol. This has been seen in the wild (issue 81). log.info("First message received was not a version message but rather " + m); return; } versionMessage = (VersionMessage) m; // Switch to the new protocol version. int peerVersion = versionMessage.clientVersion; log.info("Connected to {}: version={}, subVer='{}', services=0x{}, time={}, blocks={}", new Object[] { getPeerAddress().getAddr().getHostAddress(), peerVersion, versionMessage.subVer, versionMessage.localServices, new Date(versionMessage.time * 1000), versionMessage.bestHeight }); // Now it's our turn ... // Send an ACK message stating we accept the peers protocol version. write(channel, new VersionAck()); // bitcoinj is a client mode implementation. That means there's not much point in us talking to other client // mode nodes because we can't download the data from them we need to find/verify transactions. Some bogus // implementations claim to have a block chain in their services field but then report a height of zero, filter // them out here. if (!versionMessage.hasBlockChain() || (!params.allowEmptyPeerChain() && versionMessage.bestHeight <= 0)) { // Shut down the channel throw new ProtocolException("Peer does not have a copy of the block chain."); } // Handshake is done! if (handshakeFuture != null) handshakeFuture.set(this); } public void ping() throws IOException { // pong/nonce messages were added to any protocol version greater than 60000 if (versionMessage.clientVersion > 60000) { write(channel, new Ping(random.nextLong())); } else write(channel, new Ping()); } @Override public String toString() { return "[" + remoteIp.getHostAddress() + "]:" + params.getPort(); } public class NetworkHandler extends ReplayingDecoder<VoidEnum> implements ChannelDownstreamHandler { @Override public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception { super.channelConnected(ctx, e); channel = e.getChannel(); // The version message does not use checksumming, until Feb 2012 when it magically does. // Announce ourselves. This has to come first to connect to clients beyond v0.30.20.2 which wait to hear // from us until they send their version message back. log.info("Announcing to {} as: {}", channel.getRemoteAddress(), myVersionMessage.subVer); write(channel, myVersionMessage); // When connecting, the remote peer sends us a version message with various bits of // useful data in it. We need to know the peer protocol version before we can talk to it. } // Attempt to decode a Bitcoin message passing upstream in the channel. // // By extending ReplayingDecoder, reading past the end of buffer will throw a special Error // causing the channel to read more and retry. // // On VMs/systems where exception handling is slow, this will impact performance. On the // other hand, implementing a FrameDecoder will increase code complexity due to having // to implement retries ourselves. // // TODO: consider using a decoder state and checkpoint() if performance is an issue. @Override protected Object decode(ChannelHandlerContext ctx, Channel chan, ChannelBuffer buffer, VoidEnum state) throws Exception { Message message = serializer.deserialize(new ChannelBufferInputStream(buffer)); if (message instanceof VersionMessage) onVersionMessage(message); return message; } /** Serialize outgoing Bitcoin messages passing downstream in the channel. */ public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent evt) throws Exception { if (!(evt instanceof MessageEvent)) { ctx.sendDownstream(evt); return; } MessageEvent e = (MessageEvent) evt; Message message = (Message)e.getMessage(); ChannelBuffer buffer = ChannelBuffers.dynamicBuffer(); serializer.serialize(message, new ChannelBufferOutputStream(buffer)); write(ctx, e.getFuture(), buffer, e.getRemoteAddress()); } public TCPNetworkConnection getOwnerObject() { return TCPNetworkConnection.this; } } /** Returns the Netty Pipeline stage handling Bitcoin serialization for this connection. */ public NetworkHandler getHandler() { return handler; } public VersionMessage getVersionMessage() { return versionMessage; } public PeerAddress getPeerAddress() { return new PeerAddress(remoteIp, params.getPort()); } public void close() { channel.close(); } public void setRemoteAddress(SocketAddress address) { if (address instanceof InetSocketAddress) remoteIp = ((InetSocketAddress)address).getAddress(); } }