From 34fea867082bfb8210a40404ffef925298628d9f Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 11 Oct 2011 13:08:54 +0000 Subject: [PATCH] First part of Steves changes in preparation for a high performance multiplexing proxy: 1) Introduce partial support for caching the underlying byte arrays during message deserialization, so re-serialization can be skipped in the case where a message is not modified. 2) Add c'tors that allow a message to be configured to lazily parse on-demand. Note that the getters/setters that make lazy parsing transparent are coming in future commits. --- TODO | 12 ++ .../google/bitcoin/core/AddressMessage.java | 14 +- src/com/google/bitcoin/core/Block.java | 10 +- src/com/google/bitcoin/core/ChildMessage.java | 63 +++++++++ src/com/google/bitcoin/core/EmptyMessage.java | 9 +- .../google/bitcoin/core/GetAddrMessage.java | 1 + .../google/bitcoin/core/GetBlocksMessage.java | 2 +- .../google/bitcoin/core/GetDataMessage.java | 7 +- .../google/bitcoin/core/InventoryMessage.java | 7 +- src/com/google/bitcoin/core/ListMessage.java | 10 +- src/com/google/bitcoin/core/Message.java | 131 +++++++++++++++++- src/com/google/bitcoin/core/PeerAddress.java | 17 ++- src/com/google/bitcoin/core/Transaction.java | 44 ++++-- .../google/bitcoin/core/TransactionInput.java | 28 +++- .../bitcoin/core/TransactionOutPoint.java | 20 ++- .../bitcoin/core/TransactionOutput.java | 31 ++++- .../google/bitcoin/core/VersionMessage.java | 26 +++- 17 files changed, 384 insertions(+), 48 deletions(-) create mode 100644 src/com/google/bitcoin/core/ChildMessage.java diff --git a/TODO b/TODO index 9acb01d0..b5fd2275 100644 --- a/TODO +++ b/TODO @@ -23,3 +23,15 @@ Cleanup: - Find a way to avoid some horrid hacks when shutting down the network connection. - Implement a BitCoin class that encapsulates a BigInteger and formatting. - Make NetworkParameters use subclasses instead of static methods to construct. + + + +========== + +Impacts from Steves changes: +- Reformat affected files +- JavaDocs for the c'tors that take parseLazy/parseRetain +- Copyright header/correct author for EmptyMessage/ChildMessage +- Remove superfluous empty lines in ListMessage.java +- Remove VersionMessage check in Message serialization roundtrip checks +- Delete dead code in Message.checkParsed diff --git a/src/com/google/bitcoin/core/AddressMessage.java b/src/com/google/bitcoin/core/AddressMessage.java index 770fc467..c01b2266 100644 --- a/src/com/google/bitcoin/core/AddressMessage.java +++ b/src/com/google/bitcoin/core/AddressMessage.java @@ -8,12 +8,20 @@ public class AddressMessage extends Message { private static final long MAX_ADDRESSES = 1024; List addresses; + AddressMessage(NetworkParameters params, byte[] payload, int offset, boolean parseLazy, boolean parseRetain) throws ProtocolException { + super(params, payload, offset, parseLazy, parseRetain); + } + + AddressMessage(NetworkParameters params, byte[] payload, boolean parseLazy, boolean parseRetain) throws ProtocolException { + super(params, payload, 0, parseLazy, parseRetain); + } + AddressMessage(NetworkParameters params, byte[] payload, int offset) throws ProtocolException { - super(params, payload, offset); + super(params, payload, offset, false, false); } AddressMessage(NetworkParameters params, byte[] payload) throws ProtocolException { - super(params, payload, 0); + super(params, payload, 0, false, false); } @Override @@ -24,7 +32,7 @@ public class AddressMessage extends Message { throw new ProtocolException("Address message too large."); addresses = new ArrayList((int)numAddresses); for (int i = 0; i < numAddresses; i++) { - PeerAddress addr = new PeerAddress(params, bytes, cursor, protocolVersion); + PeerAddress addr = new PeerAddress(params, bytes, cursor, protocolVersion, this, parseLazy, parseRetain); addresses.add(addr); cursor += addr.getMessageSize(); } diff --git a/src/com/google/bitcoin/core/Block.java b/src/com/google/bitcoin/core/Block.java index 329336a9..5918332f 100644 --- a/src/com/google/bitcoin/core/Block.java +++ b/src/com/google/bitcoin/core/Block.java @@ -78,6 +78,10 @@ public class Block extends Message { public Block(NetworkParameters params, byte[] payloadBytes) throws ProtocolException { super(params, payloadBytes, 0); } + + public Block(NetworkParameters params, byte[] payloadBytes, boolean parseLazy, boolean parseRetain) throws ProtocolException { + super(params, payloadBytes, 0, parseLazy, parseRetain); + } private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); @@ -104,7 +108,7 @@ public class Block extends Message { int numTransactions = (int) readVarInt(); transactions = new ArrayList(numTransactions); for (int i = 0; i < numTransactions; i++) { - Transaction tx = new Transaction(params, bytes, cursor); + Transaction tx = new Transaction(params, bytes, cursor, this, parseLazy, parseRetain); transactions.add(tx); cursor += tx.getMessageSize(); } @@ -120,13 +124,13 @@ public class Block extends Message { } @Override - void bitcoinSerializeToStream(OutputStream stream) throws IOException { + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { writeHeader(stream); // We may only have enough data to write the header. if (transactions == null) return; stream.write(new VarInt(transactions.size()).encode()); for (Transaction tx : transactions) { - tx.bitcoinSerializeToStream(stream); + tx.bitcoinSerialize(stream); } } diff --git a/src/com/google/bitcoin/core/ChildMessage.java b/src/com/google/bitcoin/core/ChildMessage.java new file mode 100644 index 00000000..c0877ba9 --- /dev/null +++ b/src/com/google/bitcoin/core/ChildMessage.java @@ -0,0 +1,63 @@ +package com.google.bitcoin.core; + +/** + * Represents a Message type that can be contained within another Message. ChildMessages that have a cached + * backing byte array need to invalidate their parent's caches as well as their own if they are modified. + * + * @author git + * + */ +public abstract class ChildMessage extends Message { + + private Message parent; + + protected ChildMessage() { + } + + public ChildMessage(NetworkParameters params) { + super(params); + } + + public ChildMessage(NetworkParameters params, byte[] msg, int offset, int protocolVersion) throws ProtocolException { + super(params, msg, offset, protocolVersion); + } + + public ChildMessage(NetworkParameters params, byte[] msg, int offset, int protocolVersion, Message parent, boolean parseLazy, + boolean parseRetain) throws ProtocolException { + super(params, msg, offset, protocolVersion, parseLazy, parseRetain); + this.parent = parent; + } + + public ChildMessage(NetworkParameters params, byte[] msg, int offset) throws ProtocolException { + super(params, msg, offset); + } + + public ChildMessage(NetworkParameters params, byte[] msg, int offset, Message parent, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, offset, parseLazy, parseRetain); + this.parent = parent; + } + + public void setParent(Message parent) { + if (this.parent != null && this.parent != parent && parent !=null) { + //after old parent is unlinked it won't be able to receive notice if this ChildMessage + //changes internally. To be safe we invalidate the parent cache to ensure it rebuilds + //manually on serialization. + this.parent.unCache(); + } + this.parent = parent; + } + + /* (non-Javadoc) + * @see com.google.bitcoin.core.Message#unCache() + */ + @Override + protected void unCache() { + super.unCache(); + if (parent != null) + parent.unCache(); + } + + + +} diff --git a/src/com/google/bitcoin/core/EmptyMessage.java b/src/com/google/bitcoin/core/EmptyMessage.java index b2fa40d4..4d474ee8 100644 --- a/src/com/google/bitcoin/core/EmptyMessage.java +++ b/src/com/google/bitcoin/core/EmptyMessage.java @@ -4,7 +4,8 @@ import java.io.IOException; import java.io.OutputStream; /** - * Parent class for header only message that don't have a payload + * Parent class for header only messages that don't have a payload. + * Currently this includes getaddr, ping, verack as well as the special bitcoinj class UnknownMessage * @author git * */ @@ -17,16 +18,12 @@ public abstract class EmptyMessage extends Message { super(params); } - public EmptyMessage(NetworkParameters params, byte[] msg, int offset, int protocolVersion) throws ProtocolException { - super(params, msg, offset, protocolVersion); - } - public EmptyMessage(NetworkParameters params, byte[] msg, int offset) throws ProtocolException { super(params, msg, offset); } @Override - final void bitcoinSerializeToStream(OutputStream stream) throws IOException { + final protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { } } diff --git a/src/com/google/bitcoin/core/GetAddrMessage.java b/src/com/google/bitcoin/core/GetAddrMessage.java index 50922c6e..8eff455e 100644 --- a/src/com/google/bitcoin/core/GetAddrMessage.java +++ b/src/com/google/bitcoin/core/GetAddrMessage.java @@ -17,6 +17,7 @@ package com.google.bitcoin.core; public class GetAddrMessage extends EmptyMessage { + public GetAddrMessage(NetworkParameters params) { super(params); } diff --git a/src/com/google/bitcoin/core/GetBlocksMessage.java b/src/com/google/bitcoin/core/GetBlocksMessage.java index e97191b2..13eb73a5 100644 --- a/src/com/google/bitcoin/core/GetBlocksMessage.java +++ b/src/com/google/bitcoin/core/GetBlocksMessage.java @@ -52,7 +52,7 @@ public class GetBlocksMessage extends Message { return b.toString(); } - void bitcoinSerializeToStream(OutputStream stream) throws IOException { + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { // Version, for some reason. Utils.uint32ToByteStreamLE(NetworkParameters.PROTOCOL_VERSION, stream); // Then a vector of block hashes. This is actually a "block locator", a set of block diff --git a/src/com/google/bitcoin/core/GetDataMessage.java b/src/com/google/bitcoin/core/GetDataMessage.java index eff6b0c5..db94279f 100644 --- a/src/com/google/bitcoin/core/GetDataMessage.java +++ b/src/com/google/bitcoin/core/GetDataMessage.java @@ -22,8 +22,13 @@ public class GetDataMessage extends ListMessage { public GetDataMessage(NetworkParameters params, byte[] payloadBytes) throws ProtocolException { super(params, payloadBytes); } + + public GetDataMessage(NetworkParameters params, byte[] msg, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, parseLazy, parseRetain); + } - public GetDataMessage(NetworkParameters params) { + 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 78d02bb4..70c62be7 100644 --- a/src/com/google/bitcoin/core/InventoryMessage.java +++ b/src/com/google/bitcoin/core/InventoryMessage.java @@ -23,7 +23,12 @@ public class InventoryMessage extends ListMessage { super(params, bytes); } - public InventoryMessage(NetworkParameters params) { + public InventoryMessage(NetworkParameters params, byte[] msg, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, parseLazy, parseRetain); + } + + public InventoryMessage(NetworkParameters params) { super(params); } diff --git a/src/com/google/bitcoin/core/ListMessage.java b/src/com/google/bitcoin/core/ListMessage.java index ac02ef58..6e932014 100644 --- a/src/com/google/bitcoin/core/ListMessage.java +++ b/src/com/google/bitcoin/core/ListMessage.java @@ -36,8 +36,16 @@ public abstract class ListMessage extends Message super(params, bytes, 0); } + - public ListMessage(NetworkParameters params) { + public ListMessage(NetworkParameters params, byte[] msg, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, 0, parseLazy, parseRetain); + } + + + + public ListMessage(NetworkParameters params) { super(params); items = new ArrayList(); } diff --git a/src/com/google/bitcoin/core/Message.java b/src/com/google/bitcoin/core/Message.java index 01e91c73..9db7b7d5 100644 --- a/src/com/google/bitcoin/core/Message.java +++ b/src/com/google/bitcoin/core/Message.java @@ -47,6 +47,10 @@ public abstract class Message implements Serializable { // The raw message bytes themselves. protected transient byte[] bytes; + + private transient boolean parsed = false; + protected transient final boolean parseLazy; + protected transient final boolean parseRetain; protected transient int protocolVersion; @@ -55,21 +59,37 @@ public abstract class Message implements Serializable { /** This exists for the Java serialization framework to use only. */ protected Message() { + parsed = true; + parseLazy = false; + parseRetain = false; } Message(NetworkParameters params) { this.params = params; + parsed = true; + parseLazy = false; + parseRetain = false; } - @SuppressWarnings("unused") Message(NetworkParameters params, byte[] msg, int offset, int protocolVersion) throws ProtocolException { - this.protocolVersion = protocolVersion; + this(params, msg, offset, protocolVersion, false, false); + } + + @SuppressWarnings("unused") + Message(NetworkParameters params, byte[] msg, int offset, int protocolVersion, final boolean parseLazy, final boolean parseRetain) throws ProtocolException { + this.parseLazy = parseLazy; + this.parseRetain = parseRetain; + this.protocolVersion = protocolVersion; this.params = params; this.bytes = msg; this.cursor = this.offset = offset; - parse(); + if (!parseLazy) { + parse(); + parsed = true; + } if (SELF_CHECK && !this.getClass().getSimpleName().equals("VersionMessage")) { - byte[] msgbytes = new byte[cursor - offset]; + checkParse(); + byte[] msgbytes = new byte[cursor - offset]; System.arraycopy(msg, offset, msgbytes, 0, cursor - offset); byte[] reserialized = bitcoinSerialize(); if (!Arrays.equals(reserialized, msgbytes)) @@ -77,11 +97,17 @@ public abstract class Message implements Serializable { Utils.bytesToHexString(reserialized) + " vs \n" + Utils.bytesToHexString(msgbytes)); } + if (parseRetain || !parsed) + return; this.bytes = null; } Message(NetworkParameters params, byte[] msg, int offset) throws ProtocolException { - this(params, msg, offset, NetworkParameters.PROTOCOL_VERSION); + this(params, msg, offset, NetworkParameters.PROTOCOL_VERSION, false, false); + } + + Message(NetworkParameters params, byte[] msg, int offset, final boolean parseLazy, final boolean parseRetain) throws ProtocolException { + this(params, msg, offset, NetworkParameters.PROTOCOL_VERSION, parseLazy, parseRetain); } // These methods handle the serialization/deserialization using the custom BitCoin protocol. @@ -90,8 +116,87 @@ public abstract class Message implements Serializable { // are serialized to the wallet. abstract void parse() throws ProtocolException; + /** + * Ensure the object is parsed if needed. This should be called in every getter before returning a value. + * If the lazy parse flag is not set this is a method returns immediately. + */ + protected synchronized void checkParse() { + if (parsed || bytes == null) + return; + try { + parse(); + parsed = true; + //if (!parseRetain) + //bytes = null; + } catch (ProtocolException e) { + throw new RuntimeException("Lazy parsing of message failed", e); + } + } + + /** + * To be called before any change of internal values including any setters. This ensures any cached byte array is removed after performing + * a lazy parse if necessary to ensure the object is fully populated. + * + * Child messages of this object(e.g. Transactions belonging to a Block) will not have their internal byte caches invalidated unless + * they are also modified internally. + */ + protected void unCache() { + + /* + * This is a NOP at the moment. Will complete lazy parsing as a separate patch first. + * safe retention of backing byte array is tricky in cases where a parent Message object + * may have child message objects (e.g. block - tx). There has to be a way to either + * mark the cursor at the end of the parent portion of the array or a way the child can + * invalidate the parent array. This might require a ByteArrayView class which implements List + * and retains a reference to it's parent ByteArrayView so it can invalidate it. + * Alternately the child message can hold a reference to + * it's parent and propagate a call to unCache up the chain to the parent. This way only those children on the + * invalidated branch lose their caching. On the other hand this might introduce more overhead than it's worth + * since this call has to made in every setter. + * Perhaps a simpler approach where in the special cases where a cached array is wanted it is the callers responsibility + * to keep track of whether the cache is valid or not. + */ + + + //if (!parseRetain) + // return; + //checkParse(); + //bytes = null; + } + + /** + * Serialize this message to a byte array that conforms to the bitcoin wire protocol. + *
+ * This method may return the original byte array used to construct this message if the + * following conditions are met: + *
    + *
  1. 1) The message was parsed from a byte array with parseRetain = true
  2. + *
  3. 2) The message has not been modified
  4. + *
  5. 3) The array had an offset of 0 and no surplus bytes
  6. + *
+ * + * If condition 3 is not met then an copy of the relevant portion of the array will be returned. + * Otherwise a full serialize will occur. + * + * @return + */ final public byte[] bitcoinSerialize() { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + //1st attempt to use a cached array + if (bytes != null) { + if (offset == 0 && cursor == bytes.length) { + //cached byte array is the entire message with no extras + //so we can return as is and avoid an array copy. + return bytes; + } + int len = cursor - offset; + byte[] buf = new byte[len]; + System.arraycopy(bytes, offset, buf, 0, len); + return buf; + } + + //no cached array available so serialize parts by stream. + ByteArrayOutputStream stream = new ByteArrayOutputStream(); try { bitcoinSerializeToStream(stream); } catch (IOException e) { @@ -100,6 +205,20 @@ public abstract class Message implements Serializable { } return stream.toByteArray(); } + + /** + * Serialize this message to the provided OutputStream using the bitcoin wire format. + * @param stream + * @throws IOException + */ + final public void bitcoinSerialize(OutputStream stream) throws IOException { + //1st check for cached bytes + if (bytes != null) { + stream.write(bytes, offset, cursor - offset); + return; + } + bitcoinSerializeToStream(stream); + } /** * Serializes this message to the provided stream. If you just want the raw bytes use bitcoinSerialize(). diff --git a/src/com/google/bitcoin/core/PeerAddress.java b/src/com/google/bitcoin/core/PeerAddress.java index 4d0e85a1..86c1afbd 100644 --- a/src/com/google/bitcoin/core/PeerAddress.java +++ b/src/com/google/bitcoin/core/PeerAddress.java @@ -31,7 +31,7 @@ import static com.google.bitcoin.core.Utils.uint64ToByteStreamLE; * A PeerAddress holds an IP address and port number representing the network location of * a peer in the BitCoin P2P network. It exists primarily for serialization purposes. */ -public class PeerAddress extends Message { +public class PeerAddress extends ChildMessage { private static final long serialVersionUID = 7501293709324197411L; InetAddress addr; @@ -46,7 +46,18 @@ public class PeerAddress extends Message { super(params, payload, offset, protocolVersion); } + /** + * Construct a peer address from a serialized payload. + */ + public PeerAddress(NetworkParameters params, byte[] msg, int offset, int protocolVersion, Message parent, boolean parseLazy, + boolean parseRetain) throws ProtocolException { + super(params, msg, offset, protocolVersion, parent, parseLazy, parseRetain); + } + + + + /** * Construct a peer address from a memorized or hardcoded address. */ public PeerAddress(InetAddress addr, int port, int protocolVersion) { @@ -68,8 +79,8 @@ public class PeerAddress extends Message { this(addr.getAddress(), addr.getPort()); } - @Override - public void bitcoinSerializeToStream(OutputStream stream) throws IOException { + @Override + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { if (protocolVersion >= 31402) { int secs = (int)(Utils.now().getTime() / 1000); uint32ToByteStreamLE(secs, stream); diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 5b08157d..478447d6 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -18,6 +18,7 @@ package com.google.bitcoin.core; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.math.BigInteger; @@ -37,7 +38,7 @@ import static com.google.bitcoin.core.Utils.*; * serialization which is used for the wallet. This allows us to easily add extra fields used for our own accounting * or UI purposes. */ -public class Transaction extends Message implements Serializable { +public class Transaction extends ChildMessage implements Serializable { private static final Logger log = LoggerFactory.getLogger(Transaction.class); private static final long serialVersionUID = -8567546957352643140L; @@ -80,7 +81,7 @@ public class Transaction extends Message implements Serializable { public Transaction(NetworkParameters params, byte[] payloadBytes) throws ProtocolException { super(params, payloadBytes, 0); } - + /** * Creates a transaction by reading payload starting from offset bytes in. Length of a transaction is fixed. */ @@ -88,8 +89,24 @@ public class Transaction extends Message implements Serializable { super(params, payload, offset); // inputs/outputs will be created in parse() } - + /** + * Creates a transaction by reading payload starting from offset bytes in. Length of a transaction is fixed. + */ + public Transaction(NetworkParameters params, byte[] msg, int offset, Message parent, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, offset, parent, parseLazy, parseRetain); + } + + /** + * Creates a transaction by reading payload starting from offset bytes in. Length of a transaction is fixed. + */ + public Transaction(NetworkParameters params, byte[] msg, Message parent, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, 0, parent, parseLazy, parseRetain); + } + + /** * Returns a read-only list of the inputs of this transaction. */ public List getInputs() { @@ -272,7 +289,7 @@ public class Transaction extends Message implements Serializable { long numInputs = readVarInt(); inputs = new ArrayList((int)numInputs); for (long i = 0; i < numInputs; i++) { - TransactionInput input = new TransactionInput(params, this, bytes, cursor); + TransactionInput input = new TransactionInput(params, this, bytes, cursor, parseLazy, parseRetain); inputs.add(input); cursor += input.getMessageSize(); } @@ -280,7 +297,7 @@ public class Transaction extends Message implements Serializable { long numOutputs = readVarInt(); outputs = new ArrayList((int)numOutputs); for (long i = 0; i < numOutputs; i++) { - TransactionOutput output = new TransactionOutput(params, this, bytes, cursor); + TransactionOutput output = new TransactionOutput(params, this, bytes, cursor, parseLazy, parseRetain); outputs.add(output); cursor += output.getMessageSize(); } @@ -457,14 +474,14 @@ public class Transaction extends Message implements Serializable { } @Override - public void bitcoinSerializeToStream(OutputStream stream) throws IOException { + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { uint32ToByteStreamLE(version, stream); stream.write(new VarInt(inputs.size()).encode()); for (TransactionInput in : inputs) - in.bitcoinSerializeToStream(stream); + in.bitcoinSerialize(stream); stream.write(new VarInt(outputs.size()).encode()); for (TransactionOutput out : outputs) - out.bitcoinSerializeToStream(stream); + out.bitcoinSerialize(stream); uint32ToByteStreamLE(lockTime, stream); } @@ -480,4 +497,15 @@ public class Transaction extends Message implements Serializable { public int hashCode() { return getHash().hashCode(); } + + /** + * Ensure object is fully parsed before invoking java serialization. The backing byte array + * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet + * then data will be lost during serialization. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + checkParse(); + out.defaultWriteObject(); + } + } diff --git a/src/com/google/bitcoin/core/TransactionInput.java b/src/com/google/bitcoin/core/TransactionInput.java index 34410484..71863b7f 100644 --- a/src/com/google/bitcoin/core/TransactionInput.java +++ b/src/com/google/bitcoin/core/TransactionInput.java @@ -17,6 +17,7 @@ package com.google.bitcoin.core; import java.io.IOException; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.util.Map; @@ -27,7 +28,7 @@ import java.util.Map; * transaction as being a module which is wired up to others, the inputs of one have to be wired * to the outputs of another. The exceptions are coinbase transactions, which create new coins. */ -public class TransactionInput extends Message implements Serializable { +public class TransactionInput extends ChildMessage implements Serializable { private static final long serialVersionUID = 2; public static final byte[] EMPTY_ARRAY = new byte[0]; @@ -73,8 +74,15 @@ public class TransactionInput extends Message implements Serializable { this.parentTransaction = parentTransaction; } - void parse() throws ProtocolException { - outpoint = new TransactionOutPoint(params, bytes, cursor); + /** Deserializes an input message. This is usually part of a transaction message. */ + public TransactionInput(NetworkParameters params, Transaction parentTransaction, byte[] msg, int offset, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, offset, parentTransaction, parseLazy, parseRetain); + this.parentTransaction = parentTransaction; + } + + void parse() throws ProtocolException { + outpoint = new TransactionOutPoint(params, bytes, cursor, this, parseLazy, parseRetain); cursor += outpoint.getMessageSize(); int scriptLen = (int) readVarInt(); scriptBytes = readBytes(scriptLen); @@ -82,8 +90,8 @@ public class TransactionInput extends Message implements Serializable { } @Override - public void bitcoinSerializeToStream(OutputStream stream) throws IOException { - outpoint.bitcoinSerializeToStream(stream); + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { + outpoint.bitcoinSerialize(stream); stream.write(new VarInt(scriptBytes.length).encode()); stream.write(scriptBytes); Utils.uint32ToByteStreamLE(sequence, stream); @@ -187,4 +195,14 @@ public class TransactionInput extends Message implements Serializable { outpoint.fromTx = null; return true; } + + /** + * Ensure object is fully parsed before invoking java serialization. The backing byte array + * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet + * then data will be lost during serialization. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + checkParse(); + out.defaultWriteObject(); + } } diff --git a/src/com/google/bitcoin/core/TransactionOutPoint.java b/src/com/google/bitcoin/core/TransactionOutPoint.java index 36f26a8e..4f38f0f1 100644 --- a/src/com/google/bitcoin/core/TransactionOutPoint.java +++ b/src/com/google/bitcoin/core/TransactionOutPoint.java @@ -17,6 +17,7 @@ package com.google.bitcoin.core; import java.io.IOException; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; @@ -25,7 +26,7 @@ import java.io.Serializable; /** * This message is a reference or pointer to an output of a different transaction. */ -public class TransactionOutPoint extends Message implements Serializable { +public class TransactionOutPoint extends ChildMessage implements Serializable { private static final long serialVersionUID = -6320880638344662579L; /** Hash of the transaction to which we refer. */ @@ -54,6 +55,11 @@ public class TransactionOutPoint extends Message implements Serializable { super(params, payload, offset); } + /** Deserializes the message. This is usually part of a transaction message. */ + public TransactionOutPoint(NetworkParameters params, byte[] payload, int offset, Message parent, boolean parseLazy, boolean parseRetain) throws ProtocolException { + super(params, payload, offset, parent, parseLazy, parseRetain); + } + @Override void parse() throws ProtocolException { hash = readHash(); @@ -61,7 +67,7 @@ public class TransactionOutPoint extends Message implements Serializable { } @Override - public void bitcoinSerializeToStream(OutputStream stream) throws IOException { + protected void bitcoinSerializeToStream(OutputStream stream) throws IOException { stream.write(Utils.reverseBytes(hash.getBytes())); Utils.uint32ToByteStreamLE(index, stream); } @@ -96,4 +102,14 @@ public class TransactionOutPoint extends Message implements Serializable { public String toString() { return "outpoint " + index + ":" + hash.toString(); } + + /** + * Ensure object is fully parsed before invoking java serialization. The backing byte array + * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet + * then data will be lost during serialization. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + checkParse(); + out.defaultWriteObject(); + } } diff --git a/src/com/google/bitcoin/core/TransactionOutput.java b/src/com/google/bitcoin/core/TransactionOutput.java index 5938a842..34cde548 100644 --- a/src/com/google/bitcoin/core/TransactionOutput.java +++ b/src/com/google/bitcoin/core/TransactionOutput.java @@ -20,6 +20,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.ObjectOutputStream; import java.io.OutputStream; import java.io.Serializable; import java.math.BigInteger; @@ -28,7 +29,7 @@ import java.math.BigInteger; * A TransactionOutput message contains a scriptPubKey that controls who is able to spend its value. It is a sub-part * of the Transaction message. */ -public class TransactionOutput extends Message implements Serializable { +public class TransactionOutput extends ChildMessage implements Serializable { private static final Logger log = LoggerFactory.getLogger(TransactionOutput.class); private static final long serialVersionUID = -590332479859256824L; @@ -53,12 +54,20 @@ public class TransactionOutput extends Message implements Serializable { /** Deserializes a transaction output message. This is usually part of a transaction message. */ public TransactionOutput(NetworkParameters params, Transaction parent, byte[] payload, int offset) throws ProtocolException { - super(params, payload, offset); - parentTransaction = parent; + super(params, payload, offset); + parentTransaction = parent; availableForSpending = true; } + + /** Deserializes a transaction output message. This is usually part of a transaction message. */ + public TransactionOutput(NetworkParameters params, Transaction parent, byte[] msg, int offset, boolean parseLazy, boolean parseRetain) + throws ProtocolException { + super(params, msg, offset, parent, parseLazy, parseRetain); + parentTransaction = parent; + availableForSpending = true; + } - TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, Address to) { + TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, Address to) { super(params); this.value = value; this.scriptBytes = Script.createOutputScript(to); @@ -75,7 +84,7 @@ public class TransactionOutput extends Message implements Serializable { availableForSpending = true; } - public Script getScriptPubKey() throws ScriptException { + public Script getScriptPubKey() throws ScriptException { if (scriptPubKey == null) scriptPubKey = new Script(params, scriptBytes, 0, scriptBytes.length); return scriptPubKey; @@ -88,7 +97,7 @@ public class TransactionOutput extends Message implements Serializable { } @Override - public void bitcoinSerializeToStream( OutputStream stream) throws IOException { + protected void bitcoinSerializeToStream( OutputStream stream) throws IOException { assert scriptBytes != null; Utils.uint64ToByteStreamLE(getValue(), stream); // TODO: Move script serialization into the Script class, where it belongs. @@ -162,4 +171,14 @@ public class TransactionOutput extends Message implements Serializable { TransactionInput getSpentBy() { return spentBy; } + + /** + * Ensure object is fully parsed before invoking java serialization. The backing byte array + * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet + * then data will be lost during serialization. + */ + private void writeObject(ObjectOutputStream out) throws IOException { + checkParse(); + out.defaultWriteObject(); + } } \ No newline at end of file diff --git a/src/com/google/bitcoin/core/VersionMessage.java b/src/com/google/bitcoin/core/VersionMessage.java index ede9b389..dedf4081 100644 --- a/src/com/google/bitcoin/core/VersionMessage.java +++ b/src/com/google/bitcoin/core/VersionMessage.java @@ -50,6 +50,15 @@ public class VersionMessage extends Message { public VersionMessage(NetworkParameters params, byte[] msg) throws ProtocolException { super(params, msg, 0); } + + /** + * It doesn't really make sense to ever lazily parse a version message or to retain the backing bytes. + * If you're receiving this on the wire you need to check the protocol version and it will never need to be sent + * back down the wire. + */ +// public VersionMessage(NetworkParameters params, byte[] msg, boolean parseLazy, boolean parseRetain) throws ProtocolException { +// super(params, msg, 0, parseLazy, parseRetain); +// } public VersionMessage(NetworkParameters params, int newBestHeight) { super(params); @@ -96,9 +105,9 @@ public class VersionMessage extends Message { Utils.uint32ToByteStreamLE(time >> 32, buf); try { // My address. - myAddr.bitcoinSerializeToStream(buf); + myAddr.bitcoinSerialize(buf); // Their address. - theirAddr.bitcoinSerializeToStream(buf); + theirAddr.bitcoinSerialize(buf); } catch (UnknownHostException e) { throw new RuntimeException(e); // Can't happen. } catch (IOException e) { @@ -143,4 +152,17 @@ public class VersionMessage extends Message { return (int)bestHeight ^ clientVersion ^ (int)localServices ^ (int)time ^ subVer.hashCode() ^ myAddr.hashCode() ^ theirAddr.hashCode(); } + + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + sb.append("client version: ").append(clientVersion).append("\n"); + sb.append("local services: ").append(localServices).append("\n"); + sb.append("time: ").append(time).append("\n"); + sb.append("my addr: ").append(myAddr).append("\n"); + sb.append("their addr: ").append(theirAddr).append("\n"); + sb.append("sub version: ").append(subVer).append("\n"); + sb.append("best height: ").append(bestHeight).append("\n"); + return sb.toString(); + } }