/**
* 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.bitcoin.core;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.Date;
import static com.google.bitcoin.core.Utils.*;
/**
* A NetworkConnection handles talking to a remote BitCoin peer at a low level. It understands how to read and write
* messages off the network, but doesn't asynchronously communicate with the peer or handle the higher level details
* of the protocol. After constructing a NetworkConnection, use a {@link Peer} to hand off communication to a
* background thread.
*
* Construction is blocking whilst the protocol version is negotiated.
*/
public class NetworkConnection {
static final int COMMAND_LEN = 12;
// Message strings.
static final String MSG_VERSION = "version";
static final String MSG_INVENTORY = "inv";
static final String MSG_BLOCK = "block";
static final String MSG_GETBLOCKS = "getblocks";
static final String MSG_GETDATA = "getdata";
static final String MSG_TX = "tx";
static final String MSG_ADDR = "addr";
static final String MSG_VERACK = "verack";
private final Socket socket;
private final OutputStream out;
private final InputStream in;
// The IP address to which we are connecting.
private final InetAddress remoteIp;
private boolean usesChecksumming;
private final NetworkParameters params;
private final VersionMessage versionMessage;
private static final boolean PROTOCOL_LOG = false;
/**
* Connect to the given IP address using the port specified as part of the network parameters. Once construction
* is complete a functioning network channel is set up and running.
*
* @param remoteIp IP address to connect to. IPv6 is not currently supported by BitCoin.
* @param params Defines which network to connect to and details of the protocol.
* @param bestHeight How many blocks are in our best chain
* @throws IOException if there is a network related failure.
* @throws ProtocolException if the version negotiation failed.
*/
public NetworkConnection(InetAddress remoteIp, NetworkParameters params, int bestHeight)
throws IOException, ProtocolException {
this.params = params;
this.remoteIp = remoteIp;
socket = new Socket(remoteIp, params.port);
out = socket.getOutputStream();
in = socket.getInputStream();
// 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.
writeMessage(MSG_VERSION, new VersionMessage(params, bestHeight));
// 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.
versionMessage = (VersionMessage) readMessage();
// Now it's our turn ...
// Send an ACK message stating we accept the peers protocol version.
writeMessage(MSG_VERACK, new byte[] {});
// And get one back ...
readMessage();
// Switch to the new protocol version.
int peerVersion = versionMessage.clientVersion;
LOG(String.format("Connected to peer: version=%d, subVer='%s', services=0x%x, time=%s, blocks=%d",
peerVersion, versionMessage.subVer,
versionMessage.localServices, new Date(versionMessage.time * 1000).toString(), versionMessage.bestHeight));
// 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.
if (!versionMessage.hasBlockChain())
throw new ProtocolException("Peer does not have a copy of the block chain.");
usesChecksumming = peerVersion >= 209;
// Handshake is done!
}
/**
* Sends a "ping" message to the remote node. The protocol doesn't presently use this feature much.
* @throws IOException
*/
public void ping() throws IOException {
writeMessage("ping", new byte[] {});
}
/**
* Shuts down the network socket. Note that there's no way to wait for a socket to be fully flushed out to the
* wire, so if you call this immediately after sending a message it might not get sent.
*/
public void shutdown() throws IOException {
socket.shutdownOutput();
socket.shutdownInput();
socket.close();
}
@Override
public String toString() {
return "[" + remoteIp.getHostAddress() + "]:" + params.port + " (" + (socket.isConnected() ? "connected" :
"disconnected") + ")";
}
private void seekPastMagicBytes() throws IOException {
int magicCursor = 3; // Which byte of the magic we're looking for currently.
while (true) {
int b = in.read(); // Read a byte.
if (b == -1) {
// There's no more data to read.
throw new IOException("Socket is disconnected");
}
// We're looking for a run of bytes that is the same as the packet magic but we want to ignore partial
// magics that aren't complete. So we keep track of where we're up to with magicCursor.
int expectedByte = 0xFF & (int)(params.packetMagic >>> (magicCursor * 8));
if (b == expectedByte) {
magicCursor--;
if (magicCursor < 0) {
// We found the magic sequence.
return;
} else {
// We still have further to go to find the next message.
}
} else {
magicCursor = 3;
}
}
}
/**
* Reads a network message from the wire, blocking until the message is fully received.
*
* @return An instance of a Message subclass
* @throws ProtocolException if the message is badly formatted, failed checksum or there was a TCP failure.
*/
public Message readMessage() throws IOException, ProtocolException {
// A BitCoin protocol message has the following format.
//
// - 4 byte magic number: 0xfabfb5da for the testnet or
// 0xf9beb4d9 for production
// - 12 byte command in ASCII
// - 4 byte payload size
// - 4 byte checksum
// - Payload data
//
// The checksum is the first 4 bytes of a SHA256 hash of the message payload. It isn't
// present for all messages, notably, the first one on a connection.
//
// Satoshis implementation ignores garbage before the magic header bytes. We have to do the same because
// sometimes it sends us stuff that isn't part of any message.
seekPastMagicBytes();
// Now read in the header.
byte[] header = new byte[COMMAND_LEN + 4 + (usesChecksumming ? 4 : 0)];
int readCursor = 0;
while (readCursor < header.length) {
int bytesRead = in.read(header, readCursor, header.length - readCursor);
if (bytesRead == -1) {
// There's no more data to read.
throw new IOException("Socket is disconnected");
}
readCursor += bytesRead;
}
int cursor = 0;
// The command is a NULL terminated string, unless the command fills all twelve bytes
// in which case the termination is implicit.
String command;
int mark = cursor;
for (; header[cursor] != 0 && cursor - mark < COMMAND_LEN; cursor++);
byte[] commandBytes = new byte[cursor - mark];
System.arraycopy(header, mark, commandBytes, 0, cursor - mark);
try {
command = new String(commandBytes, "US-ASCII");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e); // Cannot happen.
}
cursor = mark + COMMAND_LEN;
int size = (int) readUint32(header, cursor);
cursor += 4;
if (size > Message.MAX_SIZE)
throw new ProtocolException("Message size too large: " + size);
// Old clients don't send the checksum.
byte[] checksum = new byte[4];
if (usesChecksumming) {
// Note that the size read above includes the checksum bytes.
System.arraycopy(header, cursor, checksum, 0, 4);
cursor += 4;
}
// Now try to read the whole message.
readCursor = 0;
byte[] payloadBytes = new byte[size];
while (readCursor < payloadBytes.length - 1) {
int bytesRead = in.read(payloadBytes, readCursor, size - readCursor);
if (bytesRead == -1) {
throw new IOException("Socket is disconnected");
}
readCursor += bytesRead;
}
// Verify the checksum.
if (usesChecksumming) {
byte[] hash = doubleDigest(payloadBytes);
if (checksum[0] != hash[0] || checksum[1] != hash[1] ||
checksum[2] != hash[2] || checksum[3] != hash[3]) {
throw new ProtocolException("Checksum failed to verify, actual " +
bytesToHexString(hash) +
" vs " + bytesToHexString(checksum));
}
}
if (PROTOCOL_LOG)
LOG("Received " + size + " byte '" + command + "' message: " + Utils.bytesToHexString(payloadBytes));
try {
Message message;
if (command.equals(MSG_VERSION))
message = new VersionMessage(params, payloadBytes);
else if (command.equals(MSG_INVENTORY))
message = new InventoryMessage(params, payloadBytes);
else if (command.equals(MSG_BLOCK))
message = new Block(params, payloadBytes);
else if (command.equals(MSG_GETDATA))
message = new GetDataMessage(params, payloadBytes);
else if (command.equals(MSG_TX))
message = new Transaction(params, payloadBytes);
else if (command.equals(MSG_ADDR))
message = new AddressMessage(params, payloadBytes);
else
message = new UnknownMessage(params, command, payloadBytes);
return message;
} catch (Exception e) {
throw new ProtocolException("Error deserializing message " + Utils.bytesToHexString(payloadBytes) + "\n", e);
}
}
private void writeMessage(String name, byte[] payload) throws IOException {
byte[] header = new byte[4 + COMMAND_LEN + 4 + (usesChecksumming ? 4 : 0)];
uint32ToByteArrayBE(params.packetMagic, header, 0);
// The header array is initialized to zero by Java so we don't have to worry about
// NULL terminating the string here.
for (int i = 0; i < name.length() && i < COMMAND_LEN; i++) {
header[4 + i] = (byte) (name.codePointAt(i) & 0xFF);
}
Utils.uint32ToByteArrayLE(payload.length, header, 4 + COMMAND_LEN);
if (usesChecksumming) {
byte[] hash = doubleDigest(payload);
System.arraycopy(hash, 0, header, 4 + COMMAND_LEN + 4, 4);
}
if (PROTOCOL_LOG)
LOG("Sending " + name + " message: " + bytesToHexString(payload));
// Another writeMessage call may be running concurrently.
synchronized (out) {
out.write(header);
out.write(payload);
}
}
/**
* Writes the given message out over the network using the protocol tag. For a Transaction
* this should be "tx" for example. It's safe to call this from multiple threads simultaneously,
* the actual writing will be serialized.
*
* @throws IOException
*/
public void writeMessage(String tag, Message message) throws IOException {
// TODO: Requiring "tag" here is redundant, the message object should know its own protocol tag.
writeMessage(tag, message.bitcoinSerialize());
}
/** Returns the version message received from the other end of the connection during the handshake. */
public VersionMessage getVersionMessage() {
return versionMessage;
}
}