From 10b40cbb487bf51eecf0d3fd284fc4749945d2be Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 15 Sep 2011 15:03:15 +0000 Subject: [PATCH] Split out parsing of header and payload. This is useful for high-performance programs that don't always need to parse the payload. Patch from shadders (CLA agreement pending). --- .../bitcoin/core/BitcoinSerializer.java | 207 +++++++++++------- .../google/bitcoin/core/GetAddrMessage.java | 28 +++ .../bitcoin/core/NetworkConnection.java | 2 +- 3 files changed, 160 insertions(+), 77 deletions(-) create mode 100644 src/com/google/bitcoin/core/GetAddrMessage.java diff --git a/src/com/google/bitcoin/core/BitcoinSerializer.java b/src/com/google/bitcoin/core/BitcoinSerializer.java index 8ab4505c..a94139b4 100644 --- a/src/com/google/bitcoin/core/BitcoinSerializer.java +++ b/src/com/google/bitcoin/core/BitcoinSerializer.java @@ -24,7 +24,6 @@ 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; @@ -42,11 +41,10 @@ import static com.google.bitcoin.core.Utils.*; * * */ -public class BitcoinSerializer -{ +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; @@ -62,6 +60,7 @@ public class BitcoinSerializer names.put(Ping.class, "ping"); names.put(VersionAck.class, "verack"); names.put(GetBlocksMessage.class, "getblocks"); + names.put(GetAddrMessage.class, "getaddr"); } /** @@ -75,10 +74,21 @@ public class BitcoinSerializer this.usesChecksumming = usesChecksumming; } - public void useChecksumming(boolean usesChecksumming) { + public void setUseChecksumming(boolean usesChecksumming) { this.usesChecksumming = usesChecksumming; } + public boolean getUseChecksumming() { + return usesChecksumming; + } + + /** + * Provides the expected header length, which varies depending on whether checksumming is used. + * Header length includes 4 byte magic number. + */ + public int getHeaderLength() { + return 4 + COMMAND_LEN + 4 + (usesChecksumming ? 4 : 0); + } /** * Writes message to to the output stream. @@ -115,9 +125,7 @@ public class BitcoinSerializer log.debug("Sending {} message: {}", name, bytesToHexString(header) + bytesToHexString(payload)); } - /** - * Reads a message from the given InputStream and returns it. - */ + /** 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. // @@ -134,53 +142,33 @@ public class BitcoinSerializer // 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; - } - + BitcoinPacketHeader header = new BitcoinPacketHeader(usesChecksumming, in); // Now try to read the whole message. - readCursor = 0; - byte[] payloadBytes = new byte[size]; + return deserializePayload(header, in); + + } + + /** + * Deserializes only the header in case packet meta data is needed before decoding + * the payload. This method assumes you have already called seekPastMagicBytes() + */ + public BitcoinPacketHeader deserializeHeader(InputStream in) throws ProtocolException, IOException { + return new BitcoinPacketHeader(usesChecksumming, in); + } + + /** + * Deserialize payload only. You must provide a header typically obtained by calling deserializeHeader. + * @param header + * @param in + * @return + * @throws ProtocolException + * @throws IOException + */ + public Message deserializePayload(BitcoinPacketHeader header, InputStream in) throws ProtocolException, IOException { + int readCursor = 0; + byte[] payloadBytes = new byte[header.size]; while (readCursor < payloadBytes.length - 1) { - int bytesRead = in.read(payloadBytes, readCursor, size - readCursor); + int bytesRead = in.read(payloadBytes, readCursor, header.size - readCursor); if (bytesRead == -1) { throw new IOException("Socket is disconnected"); } @@ -190,28 +178,27 @@ public class BitcoinSerializer // 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]) { + if (header.checksum[0] != hash[0] || header.checksum[1] != hash[1] || + header.checksum[2] != hash[2] || header.checksum[3] != hash[3]) { throw new ProtocolException("Checksum failed to verify, actual " + bytesToHexString(hash) + - " vs " + bytesToHexString(checksum)); + " vs " + bytesToHexString(header.checksum)); } } if (log.isDebugEnabled()) { log.debug("Received {} byte '{}' message: {}", new Object[]{ - size, - command, - Utils.bytesToHexString(payloadBytes) + header.size, + header.command, + Utils.bytesToHexString(payloadBytes) }); } try { - return makeMessage(command, payloadBytes); + return makeMessage(header.command, payloadBytes); } catch (Exception e) { throw new ProtocolException("Error deserializing message " + Utils.bytesToHexString(payloadBytes) + "\n", e); } - } private Message makeMessage(String command, byte[] payloadBytes) throws ProtocolException { @@ -237,21 +224,7 @@ public class BitcoinSerializer } } - 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 { + public 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. @@ -275,4 +248,86 @@ public class BitcoinSerializer } } } + + public class BitcoinPacketHeader { + final byte[] header; + final String command; + final int size; + final byte[] checksum; + + BitcoinPacketHeader(boolean usesCheckSumminng, InputStream in) throws ProtocolException, IOException { + 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("Incomplete packet in underlying stream"); + } + 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. + 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; + + 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. + 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; + } + } + + public boolean hasCheckSum() { + return checksum != null; + } + + /** + * @return the header + */ + public byte[] getHeader() { + return header; + } + + /** + * @return the command + */ + public String getCommand() { + return command; + } + + /** + * @return the size + */ + public int getPayloadSize() { + return size; + } + + /** + * @return the checksum + */ + public byte[] getChecksum() { + return checksum; + } + + } } diff --git a/src/com/google/bitcoin/core/GetAddrMessage.java b/src/com/google/bitcoin/core/GetAddrMessage.java new file mode 100644 index 00000000..11d92a8a --- /dev/null +++ b/src/com/google/bitcoin/core/GetAddrMessage.java @@ -0,0 +1,28 @@ +/** + * 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; + +public class GetAddrMessage extends Message { + public GetAddrMessage(NetworkParameters params) { + super(params); + } + + @Override + void parse() throws ProtocolException { + // TODO: Implement this. + } +} diff --git a/src/com/google/bitcoin/core/NetworkConnection.java b/src/com/google/bitcoin/core/NetworkConnection.java index 04e8fa66..25d56b75 100644 --- a/src/com/google/bitcoin/core/NetworkConnection.java +++ b/src/com/google/bitcoin/core/NetworkConnection.java @@ -110,7 +110,7 @@ public class NetworkConnection { throw new ProtocolException("Peer does not have a copy of the block chain."); } // newer clients use checksumming - serializer.useChecksumming(peerVersion >= 209); + serializer.setUseChecksumming(peerVersion >= 209); // Handshake is done! }