From a955187e04f4b2cc0f1288f9ad333754de36594c Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Mon, 13 Jun 2011 10:27:15 +0000 Subject: [PATCH] Refactor the serialization logic: - Extract serialization code out of NetworkConnection into a new class - Created classes for Ping and VerAck messages - Use reflection to create new message classes - Add a few test cases to exercise the BitcoinSerializer class Patch by Noa Resare. --- .../bitcoin/core/BitcoinSerializer.java | 268 ++++++++++++++++++ .../google/bitcoin/core/GetDataMessage.java | 14 +- .../google/bitcoin/core/InventoryMessage.java | 57 +--- src/com/google/bitcoin/core/ListMessage.java | 94 ++++++ .../bitcoin/core/NetworkConnection.java | 206 ++------------ src/com/google/bitcoin/core/Peer.java | 31 +- src/com/google/bitcoin/core/Ping.java | 27 ++ src/com/google/bitcoin/core/VersionAck.java | 41 +++ .../bitcoin/core/BitcoinSerializerTest.java | 64 +++++ 9 files changed, 539 insertions(+), 263 deletions(-) create mode 100644 src/com/google/bitcoin/core/BitcoinSerializer.java create mode 100644 src/com/google/bitcoin/core/ListMessage.java create mode 100644 src/com/google/bitcoin/core/Ping.java create mode 100644 src/com/google/bitcoin/core/VersionAck.java create mode 100644 tests/com/google/bitcoin/core/BitcoinSerializerTest.java diff --git a/src/com/google/bitcoin/core/BitcoinSerializer.java b/src/com/google/bitcoin/core/BitcoinSerializer.java new file mode 100644 index 000000000..ea7115ed3 --- /dev/null +++ b/src/com/google/bitcoin/core/BitcoinSerializer.java @@ -0,0 +1,268 @@ +/** + * 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.util.HashMap; +import java.util.Map; + +import static com.google.bitcoin.core.Utils.*; + +/** + * Methods to serialize and de-serialize messages to the bitcoin network format as defined in the bitcoin protocol + * specification at https://en.bitcoin.it/wiki/Protocol_specification + * + * To be able to serialize and deserialize new Message subclasses the following criteria needs to be met. + * + * + */ +public class BitcoinSerializer +{ + private static final Logger log = LoggerFactory.getLogger(BitcoinSerializer.class); + private static final int COMMAND_LEN = 12; + + private NetworkParameters params; + private boolean usesChecksumming; + + private static Map, String> names = new HashMap,String>(); + private static Map> + messageConstructors = new HashMap>(); + + static { + names.put(VersionMessage.class, "version"); + names.put(InventoryMessage.class, "inv"); + names.put(Block.class, "block"); + names.put(GetDataMessage.class, "getdata"); + names.put(Transaction.class, "tx"); + names.put(AddressMessage.class, "addr"); + names.put(Ping.class, "ping"); + names.put(VersionAck.class, "verack"); + names.put(GetBlocksMessage.class, "getblocks"); + } + + /** + * Constructs a BitcoinSerializer with the given behavior. + * + * @param params networkParams used to create Messages instances and termining packetMagic + * @param usesChecksumming set to true if checkums should be included and expected in headers + */ + public BitcoinSerializer(NetworkParameters params, boolean usesChecksumming) { + this.params = params; + this.usesChecksumming = usesChecksumming; + + // some Message subclasses can only be sent for now, ignore missing constructors + for (Class c : names.keySet()) { + Constructor ct = makeConstructor(c); + if (ct != null) { + messageConstructors.put(names.get(c),ct); + } + } + } + + public void useChecksumming(boolean usesChecksumming) { + this.usesChecksumming = usesChecksumming; + } + + + /** + * Writes message to to the output stream. + */ + public void serialize(Message message, OutputStream out) throws IOException { + String name = names.get(message.getClass()); + if (name == null) { + throw new Error("BitcoinSerializer doesn't currently know how to serialize "+ message.getClass()); + } + + 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); + } + + byte[] payload = message.bitcoinSerialize(); + + Utils.uint32ToByteArrayLE(payload.length, header, 4 + COMMAND_LEN); + + if (usesChecksumming) { + byte[] hash = doubleDigest(payload); + System.arraycopy(hash, 0, header, 4 + COMMAND_LEN + 4, 4); + } + + out.write(header); + out.write(payload); + + log.debug("Sending {} message: {}", name, bytesToHexString(header) + bytesToHexString(payload)); + } + + /** + * Reads a message from the given InputStream and returns it. + */ + public Message deserialize(InputStream in) throws ProtocolException, IOException { + // 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. + // + // Satoshi's 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(in); + // 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 (log.isDebugEnabled()) { + log.debug("Received {} byte '{}' message: {}", new Object[]{ + size, + command, + Utils.bytesToHexString(payloadBytes) + }); + } + + try { + Constructor c = messageConstructors.get(command); + if (c == null) { + throw new ProtocolException("No support for deserializing message with name " + command); + } + return c.newInstance(params, payloadBytes); + } catch (Exception e) { + throw new ProtocolException("Error deserializing message " + Utils.bytesToHexString(payloadBytes) + "\n", e); + } + + } + + private Constructor makeConstructor(Class c) { + Class parTypes[] = new Class[2]; + parTypes[0] = NetworkParameters.class; + parTypes[1] = byte[].class; + + try { + return c.getDeclaredConstructor(parTypes); + } catch (NoSuchMethodException e) { + return null; + } + + } + + + private void seekPastMagicBytes(InputStream in) 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; + } + } + } +} diff --git a/src/com/google/bitcoin/core/GetDataMessage.java b/src/com/google/bitcoin/core/GetDataMessage.java index 0e15c88fe..eff6b0c5f 100644 --- a/src/com/google/bitcoin/core/GetDataMessage.java +++ b/src/com/google/bitcoin/core/GetDataMessage.java @@ -16,20 +16,14 @@ package com.google.bitcoin.core; -public class GetDataMessage extends Message { +public class GetDataMessage extends ListMessage { private static final long serialVersionUID = 2754681589501709887L; public GetDataMessage(NetworkParameters params, byte[] payloadBytes) throws ProtocolException { - super(params, payloadBytes, 0); + super(params, payloadBytes); } - - public byte[] bitcoinSerialize() { - return new byte[] {}; - } - - @Override - public void parse() throws ProtocolException { - // TODO Auto-generated method stub + public GetDataMessage(NetworkParameters params) { + super(params); } } diff --git a/src/com/google/bitcoin/core/InventoryMessage.java b/src/com/google/bitcoin/core/InventoryMessage.java index 016f73317..78d02bb4b 100644 --- a/src/com/google/bitcoin/core/InventoryMessage.java +++ b/src/com/google/bitcoin/core/InventoryMessage.java @@ -16,64 +16,15 @@ package com.google.bitcoin.core; -import com.google.bitcoin.core.InventoryItem.Type; - -import java.io.IOException; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -public class InventoryMessage extends Message { +public class InventoryMessage extends ListMessage { private static final long serialVersionUID = -7050246551646107066L; - private static final long MAX_INVENTORY_ITEMS = 50000; - - // For some reason the compiler complains if this is inside InventoryItem - public List items; public InventoryMessage(NetworkParameters params, byte[] bytes) throws ProtocolException { - super(params, bytes, 0); + super(params, bytes); } - - @Override - public void parse() throws ProtocolException { - // An inv is vector where CInv is int+hash. The int is either 1 or 2 for tx or block. - long arrayLen = readVarInt(); - if (arrayLen > MAX_INVENTORY_ITEMS) - throw new ProtocolException("Too many items in INV message: " + arrayLen); - items = new ArrayList((int)arrayLen); - for (int i = 0; i < arrayLen; i++) { - if (cursor + 4 + 32 > bytes.length) { - throw new ProtocolException("Ran off the end of the INV"); - } - int typeCode = (int) readUint32(); - Type type; - // See ppszTypeName in net.h - switch (typeCode) { - case 0: type = InventoryItem.Type.Error; break; - case 1: type = InventoryItem.Type.Transaction; break; - case 2: type = InventoryItem.Type.Block; break; - default: - throw new ProtocolException("Unknown CInv type: " + typeCode); - } - InventoryItem item = new InventoryItem(type, readHash()); - items.add(item); - } - bytes = null; - } - + public InventoryMessage(NetworkParameters params) { super(params); - items = new ArrayList(); - } - - @Override - public void bitcoinSerializeToStream( OutputStream stream) throws IOException { - stream.write(new VarInt(items.size()).encode()); - for (InventoryItem i : items) { - // Write out the type code. - Utils.uint32ToByteStreamLE(i.type.ordinal(), stream); - // And now the hash. - stream.write(Utils.reverseBytes(i.hash)); - } } + } diff --git a/src/com/google/bitcoin/core/ListMessage.java b/src/com/google/bitcoin/core/ListMessage.java new file mode 100644 index 000000000..1e0295af4 --- /dev/null +++ b/src/com/google/bitcoin/core/ListMessage.java @@ -0,0 +1,94 @@ +/** + * 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.OutputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * Abstract superclass of classes with list based payload, i.e. InventoryMessage and GetDataMessage. + */ +public abstract class ListMessage extends Message +{ + // For some reason the compiler complains if this is inside InventoryItem + private List items; + + private static final long MAX_INVENTORY_ITEMS = 50000; + + + public ListMessage(NetworkParameters params, byte[] bytes) throws ProtocolException { + super(params, bytes, 0); + } + + + public ListMessage(NetworkParameters params) { + super(params); + items = new ArrayList(); + } + + public List getItems() + { + return items; + } + + public void addItem(InventoryItem item) + { + items.add(item); + } + + @Override + public void parse() throws ProtocolException { + // An inv is vector where CInv is int+hash. The int is either 1 or 2 for tx or block. + long arrayLen = readVarInt(); + if (arrayLen > MAX_INVENTORY_ITEMS) + throw new ProtocolException("Too many items in INV message: " + arrayLen); + items = new ArrayList((int)arrayLen); + for (int i = 0; i < arrayLen; i++) { + if (cursor + 4 + 32 > bytes.length) { + throw new ProtocolException("Ran off the end of the INV"); + } + int typeCode = (int) readUint32(); + InventoryItem.Type type; + // See ppszTypeName in net.h + switch (typeCode) { + case 0: type = InventoryItem.Type.Error; break; + case 1: type = InventoryItem.Type.Transaction; break; + case 2: type = InventoryItem.Type.Block; break; + default: + throw new ProtocolException("Unknown CInv type: " + typeCode); + } + InventoryItem item = new InventoryItem(type, readHash()); + items.add(item); + } + bytes = null; + } + + + @Override + public void bitcoinSerializeToStream( OutputStream stream) throws IOException + { + stream.write(new VarInt(items.size()).encode()); + for (InventoryItem i : items) { + // Write out the type code. + Utils.uint32ToByteStreamLE(i.type.ordinal(), stream); + // And now the hash. + stream.write(Utils.reverseBytes(i.hash)); + } + } +} diff --git a/src/com/google/bitcoin/core/NetworkConnection.java b/src/com/google/bitcoin/core/NetworkConnection.java index 5fbf6b556..54f649baf 100644 --- a/src/com/google/bitcoin/core/NetworkConnection.java +++ b/src/com/google/bitcoin/core/NetworkConnection.java @@ -16,20 +16,17 @@ package com.google.bitcoin.core; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.util.Date; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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 @@ -41,28 +38,17 @@ import static com.google.bitcoin.core.Utils.*; public class NetworkConnection { private static final Logger log = LoggerFactory.getLogger(NetworkConnection.class); - 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; + private BitcoinSerializer serializer = null; + /** * 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. @@ -78,7 +64,8 @@ public class NetworkConnection { throws IOException, ProtocolException { this.params = params; this.remoteIp = remoteIp; - + + InetSocketAddress address = new InetSocketAddress(remoteIp, params.port); socket = new Socket(); socket.connect(address, connectTimeout); @@ -86,15 +73,18 @@ public class NetworkConnection { out = socket.getOutputStream(); in = socket.getInputStream(); + // the version message never uses checksumming. Update checkumming property after version is read. + this.serializer = new BitcoinSerializer(params, false); + // 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)); + writeMessage(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[] {}); + writeMessage(new VersionAck()); // And get one back ... readMessage(); // Switch to the new protocol version. @@ -110,7 +100,8 @@ public class NetworkConnection { // 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; + // newer clients use checksumming + serializer.useChecksumming(peerVersion >= 209); // Handshake is done! } @@ -119,7 +110,7 @@ public class NetworkConnection { * @throws IOException */ public void ping() throws IOException { - writeMessage("ping", new byte[] {}); + writeMessage(new Ping()); } /** @@ -138,31 +129,6 @@ public class NetworkConnection { "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. * @@ -170,140 +136,7 @@ public class NetworkConnection { * @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 (log.isDebugEnabled()) { - log.debug("Received {} byte '{}' message: {}", new Object[]{ - size, - command, - 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); - } - - log.debug("Sending {} message: {}", name, bytesToHexString(payload)); - - // Another writeMessage call may be running concurrently. - synchronized (out) { - out.write(header); - out.write(payload); - } + return serializer.deserialize(in); } /** @@ -313,9 +146,10 @@ public class NetworkConnection { * * @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()); + public void writeMessage(Message message) throws IOException { + synchronized (out) { + serializer.serialize(message, out); + } } /** Returns the version message received from the other end of the connection during the handshake. */ diff --git a/src/com/google/bitcoin/core/Peer.java b/src/com/google/bitcoin/core/Peer.java index 83a1ba587..faa57c036 100644 --- a/src/com/google/bitcoin/core/Peer.java +++ b/src/com/google/bitcoin/core/Peer.java @@ -16,6 +16,9 @@ package com.google.bitcoin.core; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -23,9 +26,6 @@ import java.util.LinkedList; import java.util.List; import java.util.concurrent.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - /** * A Peer handles the high level communication with a BitCoin node. It requires a NetworkConnection to be set up for * it. After that it takes ownership of the connection, creates and manages its own thread used for communication @@ -162,8 +162,9 @@ public class Peer { // enough to be a problem. Block topBlock = blockChain.getUnconnectedBlock(); byte[] topHash = (topBlock != null ? topBlock.getHash() : null); - if (inv.items.size() == 1 && inv.items.get(0).type == InventoryItem.Type.Block && topHash != null && - Arrays.equals(inv.items.get(0).hash, topHash)) { + List items = inv.getItems(); + if (items.size() == 1 && items.get(0).type == InventoryItem.Type.Block && topHash != null && + Arrays.equals(items.get(0).hash, topHash)) { // An inv with a single hash containing our most recent unconnected block is a special inv, // it's kind of like a tickle from the peer telling us that it's time to download more blocks to catch up to // the block chain. We could just ignore this and treat it as a regular inv but then we'd download the head @@ -171,17 +172,19 @@ public class Peer { blockChainDownload(topHash); return; } - InventoryMessage getdata = new InventoryMessage(params); - for (InventoryItem item : inv.items) { + GetDataMessage getdata = new GetDataMessage(params); + boolean dirty = false; + for (InventoryItem item : items) { if (item.type != InventoryItem.Type.Block) continue; - getdata.items.add(item); + getdata.addItem(item); + dirty = true; } // No blocks to download. This probably contained transactions instead, but right now we can't prove they are // valid so we don't bother downloading transactions that aren't in blocks yet. - if (getdata.items.size() == 0) + if (!dirty) return; // This will cause us to receive a bunch of block messages. - conn.writeMessage(NetworkConnection.MSG_GETDATA, getdata); + conn.writeMessage(getdata); } /** @@ -196,14 +199,14 @@ public class Peer { public Future getBlock(byte[] blockHash) throws IOException { InventoryMessage getdata = new InventoryMessage(params); InventoryItem inventoryItem = new InventoryItem(InventoryItem.Type.Block, blockHash); - getdata.items.add(inventoryItem); + getdata.addItem(inventoryItem); GetDataFuture future = new GetDataFuture(inventoryItem); // Add to the list of things we're waiting for. It's important this come before the network send to avoid // race conditions. synchronized (pendingGetBlockFutures) { pendingGetBlockFutures.add(future); } - conn.writeMessage(NetworkConnection.MSG_GETDATA, getdata); + conn.writeMessage(getdata); return future; } @@ -267,7 +270,7 @@ public class Peer { * @throws IOException */ void broadcastTransaction(Transaction tx) throws IOException { - conn.writeMessage(NetworkConnection.MSG_TX, tx); + conn.writeMessage(tx); } private void blockChainDownload(byte[] toHash) throws IOException { @@ -309,7 +312,7 @@ public class Peer { if (!topBlock.equals(params.genesisBlock)) blockLocator.add(0, topBlock.getHash()); GetBlocksMessage message = new GetBlocksMessage(params, blockLocator, toHash); - conn.writeMessage(NetworkConnection.MSG_GETBLOCKS, message); + conn.writeMessage(message); } /** diff --git a/src/com/google/bitcoin/core/Ping.java b/src/com/google/bitcoin/core/Ping.java new file mode 100644 index 000000000..5fe5a4e49 --- /dev/null +++ b/src/com/google/bitcoin/core/Ping.java @@ -0,0 +1,27 @@ +/** + * Copyright 2011 Noa Resare + * + * 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; + +public class Ping + extends Message +{ + @Override + void parse() throws ProtocolException + { + // nothing to parse + } +} diff --git a/src/com/google/bitcoin/core/VersionAck.java b/src/com/google/bitcoin/core/VersionAck.java new file mode 100644 index 000000000..818696e8c --- /dev/null +++ b/src/com/google/bitcoin/core/VersionAck.java @@ -0,0 +1,41 @@ +/** + * Copyright 2011 Noa Resare. + * + * 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; + +/** + * The verack message, sent by a client accepting the version message they received from their peer. + */ +public class VersionAck + extends Message +{ + public VersionAck() + { + + } + + // this is needed by the BitcoinSerializer + public VersionAck(NetworkParameters params, byte[] payload) { + + } + + @Override + void parse() throws ProtocolException + { + // nothing to parse for now + } +} diff --git a/tests/com/google/bitcoin/core/BitcoinSerializerTest.java b/tests/com/google/bitcoin/core/BitcoinSerializerTest.java new file mode 100644 index 000000000..2df4916df --- /dev/null +++ b/tests/com/google/bitcoin/core/BitcoinSerializerTest.java @@ -0,0 +1,64 @@ +/** + * Copyright 2011 Noa Resare + * + * 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 com.google.bitcoin.bouncycastle.util.encoders.Hex; +import org.junit.Test; + +import java.io.ByteArrayInputStream; + +import static org.junit.Assert.assertEquals; + +public class BitcoinSerializerTest +{ + @Test + public void testVersion() throws Exception { + BitcoinSerializer bs = new BitcoinSerializer(NetworkParameters.prodNet(), false); + // the actual data from https://en.bitcoin.it/wiki/Protocol_specification#version + ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d976657273696f6e0000000000550000009" + + "c7c00000100000000000000e615104d00000000010000000000000000000000000000000000ffff0a000001daf6010000" + + "000000000000000000000000000000ffff0a000002208ddd9d202c3ab457130055810100")); + VersionMessage vm = (VersionMessage)bs.deserialize(bais); + assertEquals(31900, vm.clientVersion); + assertEquals(1292899814L, vm.time); + assertEquals(98645L, vm.bestHeight); + } + + + @Test + public void testVerack() throws Exception { + BitcoinSerializer bs = new BitcoinSerializer(NetworkParameters.prodNet(), false); + // the actual data from https://en.bitcoin.it/wiki/Protocol_specification#verack + ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d976657261636b00000000000000000000")); + VersionAck va = (VersionAck)bs.deserialize(bais); + + } + + @Test + public void testAddr() throws Exception { + BitcoinSerializer bs = new BitcoinSerializer(NetworkParameters.prodNet(), true); + // the actual data from https://en.bitcoin.it/wiki/Protocol_specification#addr + ByteArrayInputStream bais = new ByteArrayInputStream(Hex.decode("f9beb4d96164647200000000000000001f000000" + + "ed52399b01e215104d010000000000000000000000000000000000ffff0a000001208d")); + AddressMessage a = (AddressMessage)bs.deserialize(bais); + assertEquals(1, a.addresses.size()); + PeerAddress pa = a.addresses.get(0); + assertEquals(8333, pa.port); + assertEquals("10.0.0.1", pa.addr.getHostAddress()); + } +}