diff --git a/core/src/main/java/com/google/bitcoin/core/Block.java b/core/src/main/java/com/google/bitcoin/core/Block.java index 84bb0325..cf2a2465 100644 --- a/core/src/main/java/com/google/bitcoin/core/Block.java +++ b/core/src/main/java/com/google/bitcoin/core/Block.java @@ -90,6 +90,11 @@ public class Block extends Message { private transient boolean headerBytesValid; private transient boolean transactionBytesValid; + + // Blocks can be encoded in a way that will use more bytes than is optimal (due to VarInts having multiple encodings) + // MAX_BLOCK_SIZE must be compared to the optimal encoding, not the actual encoding, so when parsing, we keep track + // of the size of the ideal encoding in addition to the actual message size (which Message needs) + private transient int optimalEncodingMessageSize; /** Special case constructor, used for the genesis node, cloneAsHeader and unit tests. */ Block(NetworkParameters params) { @@ -167,6 +172,7 @@ public class Block extends Message { return; cursor = offset + HEADER_SIZE; + optimalEncodingMessageSize = HEADER_SIZE; if (bytes.length == cursor) { // This message is just a header, it has no transactions. transactionsParsed = true; @@ -175,11 +181,13 @@ public class Block extends Message { } int numTransactions = (int) readVarInt(); + optimalEncodingMessageSize += VarInt.sizeOf(numTransactions); transactions = new ArrayList(numTransactions); for (int i = 0; i < numTransactions; i++) { Transaction tx = new Transaction(params, bytes, cursor, this, parseLazy, parseRetain, UNKNOWN_LENGTH); transactions.add(tx); cursor += tx.getMessageSize(); + optimalEncodingMessageSize += tx.getOptimalEncodingMessageSize(); } // No need to set length here. If length was not provided then it should be set at the end of parseLight(). // If this is a genuine lazy parse then length must have been provided to the constructor. @@ -192,6 +200,16 @@ public class Block extends Message { parseTransactions(); length = cursor - offset; } + + public int getOptimalEncodingMessageSize() { + if (optimalEncodingMessageSize != 0) + return optimalEncodingMessageSize; + maybeParseTransactions(); + if (optimalEncodingMessageSize != 0) + return optimalEncodingMessageSize; + optimalEncodingMessageSize = getMessageSize(); + return optimalEncodingMessageSize; + } protected void parseLite() throws ProtocolException { // Ignore the header since it has fixed length. If length is not provided we will have to @@ -716,7 +734,7 @@ public class Block extends Message { if (transactions.isEmpty()) throw new VerificationException("Block had no transactions"); maybeParseTransactions(); - if (this.getMessageSize() > MAX_BLOCK_SIZE) + if (this.getOptimalEncodingMessageSize() > MAX_BLOCK_SIZE) throw new VerificationException("Block larger than MAX_BLOCK_SIZE"); checkTransactions(); checkMerkleRoot(); diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java index a7878da9..5662b300 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -79,6 +79,13 @@ public class Transaction extends ChildMessage implements Serializable { // // If this transaction is not stored in the wallet, appearsInHashes is null. private Set appearsInHashes; + + // Transactions can be encoded in a way that will use more bytes than is optimal + // (due to VarInts having multiple encodings) + // MAX_BLOCK_SIZE must be compared to the optimal encoding, not the actual encoding, so when parsing, we keep track + // of the size of the ideal encoding in addition to the actual message size (which Message needs) so that Blocks + // can properly keep track of optimal encoded size + private transient int optimalEncodingMessageSize; public Transaction(NetworkParameters params) { super(params); @@ -478,26 +485,44 @@ public class Transaction extends ChildMessage implements Serializable { cursor = offset; version = readUint32(); + optimalEncodingMessageSize = 4; // First come the inputs. long numInputs = readVarInt(); + optimalEncodingMessageSize += VarInt.sizeOf(numInputs); inputs = new ArrayList((int) numInputs); for (long i = 0; i < numInputs; i++) { TransactionInput input = new TransactionInput(params, this, bytes, cursor, parseLazy, parseRetain); inputs.add(input); - cursor += input.getMessageSize(); + long scriptLen = readVarInt(TransactionOutPoint.MESSAGE_LENGTH); + optimalEncodingMessageSize += TransactionOutPoint.MESSAGE_LENGTH + VarInt.sizeOf(scriptLen) + scriptLen + 4; + cursor += scriptLen + 4; } // Now the outputs long numOutputs = readVarInt(); + optimalEncodingMessageSize += VarInt.sizeOf(numOutputs); outputs = new ArrayList((int) numOutputs); for (long i = 0; i < numOutputs; i++) { TransactionOutput output = new TransactionOutput(params, this, bytes, cursor, parseLazy, parseRetain); outputs.add(output); - cursor += output.getMessageSize(); + long scriptLen = readVarInt(8); + optimalEncodingMessageSize += 8 + VarInt.sizeOf(scriptLen) + scriptLen; + cursor += scriptLen; } lockTime = readUint32(); + optimalEncodingMessageSize += 4; length = cursor - offset; } + + public int getOptimalEncodingMessageSize() { + if (optimalEncodingMessageSize != 0) + return optimalEncodingMessageSize; + maybeParse(); + if (optimalEncodingMessageSize != 0) + return optimalEncodingMessageSize; + optimalEncodingMessageSize = getMessageSize(); + return optimalEncodingMessageSize; + } /** * A coinbase transaction is one that creates a new coin. They are the first transaction in each block and their