From 0208b426f5ec537cd019378d4261868af9cb9b1e Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Sun, 30 Dec 2012 13:53:48 -0500 Subject: [PATCH] Add a few more tests to FullBlockTestGenerator. ...including one which (somewhat) tests the optimally encoded size stuff. --- .../java/com/google/bitcoin/core/Block.java | 3 +- .../bitcoin/core/FullBlockTestGenerator.java | 175 +++++++++++++++++- 2 files changed, 173 insertions(+), 5 deletions(-) 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 cf2a2465..020bdeac 100644 --- a/core/src/main/java/com/google/bitcoin/core/Block.java +++ b/core/src/main/java/com/google/bitcoin/core/Block.java @@ -333,7 +333,8 @@ public class Block extends Message { } } - private void writeHeader(OutputStream stream) throws IOException { + // default for testing + void writeHeader(OutputStream stream) throws IOException { // try for cached write first if (headerBytesValid && bytes != null && bytes.length >= offset + HEADER_SIZE) { stream.write(bytes, offset, HEADER_SIZE); diff --git a/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java b/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java index 5e1a76f9..0096bedb 100644 --- a/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java +++ b/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java @@ -49,7 +49,7 @@ public class FullBlockTestGenerator { Utils.rollMockClock(0); // Set a mock clock for timestamp tests } - public List getBlocksToTest(boolean addExpensiveBlocks) throws ScriptException { + public List getBlocksToTest(boolean addExpensiveBlocks) throws ScriptException, ProtocolException, IOException { List blocks = new LinkedList(); Queue spendableOutputs = new LinkedList(); @@ -701,7 +701,6 @@ public class FullBlockTestGenerator { } b40.solve(); blocks.add(new BlockAndValidity(b40, false, true, b39.getHash(), "b40")); - Block b41 = null; if (addExpensiveBlocks) { @@ -1054,7 +1053,7 @@ public class FullBlockTestGenerator { // Test tx.isFinal is properly rejected (not an exhaustive tx.isFinal test, that should be in data-driven transaction tests) // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) // \-> b62 (18) - // + // Block b62 = createNextBlock(b60, chainHeadHeight + 17, null, null); { Transaction tx = new Transaction(params); @@ -1062,10 +1061,178 @@ public class FullBlockTestGenerator { tx.addOutput(new TransactionOutput(params, tx, BigInteger.ZERO, new byte[] { Script.OP_TRUE })); addOnlyInputToTransaction(tx, out18, 0); b62.addTransaction(tx); + Preconditions.checkState(!tx.isFinal(chainHeadHeight + 17, b62.getTimeSeconds())); } b62.solve(); blocks.add(new BlockAndValidity(b62, false, true, b60.getHash(), "b62")); + // Test a non-final coinbase is also rejected + // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) + // \-> b63 (-) + // + Block b63 = createNextBlock(b60, chainHeadHeight + 17, null, null); + { + b63.getTransactions().get(0).setLockTime(0xffffffffL); + b63.getTransactions().get(0).getInputs().get(0).setSequenceNumber(0xDEADBEEF); + Preconditions.checkState(!b63.getTransactions().get(0).isFinal(chainHeadHeight + 17, b63.getTimeSeconds())); + } + b63.solve(); + blocks.add(new BlockAndValidity(b63, false, true, b60.getHash(), "b63")); + + // Check that a block which is (when properly encoded) <= MAX_BLOCK_SIZE is accepted + // Even when it is encoded with varints that make its encoded size actually > MAX_BLOCK_SIZE + // -> b39 (11) -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) + // + Block b64; + { + Block b64Created = createNextBlock(b60, chainHeadHeight + 17, out18, null); + Transaction tx = new Transaction(params); + // Signature size is non-deterministic, so it may take several runs before finding any off-by-one errors + byte[] outputScript = new byte[Block.MAX_BLOCK_SIZE - b64Created.getMessageSize() - 138]; + Arrays.fill(outputScript, (byte)Script.OP_FALSE); + tx.addOutput(new TransactionOutput(params, tx, BigInteger.valueOf(1), outputScript)); + addOnlyInputToTransaction(tx, new TransactionOutPointWithValue( + new TransactionOutPoint(params, 1, b64Created.getTransactions().get(1).getHash()), + BigInteger.valueOf(1), b64Created.getTransactions().get(1).getOutputs().get(1).getScriptPubKey())); + b64Created.addTransaction(tx); + b64Created.solve(); + + UnsafeByteArrayOutputStream stream = new UnsafeByteArrayOutputStream(b64Created.getMessageSize() + 8); + b64Created.writeHeader(stream); + + byte[] varIntBytes = new byte[9]; + varIntBytes[0] = (byte) 255; + Utils.uint32ToByteArrayLE((long)b64Created.getTransactions().size(), varIntBytes, 1); + Utils.uint32ToByteArrayLE(((long)b64Created.getTransactions().size()) >>> 32, varIntBytes, 5); + stream.write(varIntBytes); + Preconditions.checkState(new VarInt(varIntBytes, 0).value == b64Created.getTransactions().size()); + + for (Transaction transaction : b64Created.getTransactions()) + transaction.bitcoinSerialize(stream); + b64 = new Block(params, stream.toByteArray(), false, true, stream.size()); + + // The following checks are checking to ensure block serialization functions in the way needed for this test + // If they fail, it is likely not an indication of error, but an indication that this test needs rewritten + Preconditions.checkState(stream.size() == b64Created.getMessageSize() + 8); + Preconditions.checkState(stream.size() == b64.getMessageSize()); + Preconditions.checkState(Arrays.equals(stream.toByteArray(), b64.bitcoinSerialize())); + Preconditions.checkState(b64.getOptimalEncodingMessageSize() == b64Created.getMessageSize()); + } + blocks.add(new BlockAndValidity(b64, true, false, b64.getHash(), "b64")); + spendableOutputs.offer(new TransactionOutPointWithValue( + new TransactionOutPoint(params, 0, b64.getTransactions().get(0).getHash()), + b64.getTransactions().get(0).getOutputs().get(0).getValue(), + b64.getTransactions().get(0).getOutputs().get(0).getScriptPubKey())); + + // Spend an output created in the block itself + // -> b42 (12) -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) + // + TransactionOutPointWithValue out19 = spendableOutputs.poll(); + + Block b65 = createNextBlock(b64, chainHeadHeight + 18, null, null); + { + Transaction tx1 = new Transaction(params); + tx1.addOutput(new TransactionOutput(params, tx1, out19.value, new byte[]{ Script.OP_TRUE })); + addOnlyInputToTransaction(tx1, out19, 0); + b65.addTransaction(tx1); + Transaction tx2 = new Transaction(params); + tx2.addOutput(new TransactionOutput(params, tx2, BigInteger.ZERO, new byte[]{ Script.OP_TRUE })); + tx2.addInput(new TransactionInput(params, tx2, new byte[]{ Script.OP_TRUE }, + new TransactionOutPoint(params, 0, tx1.getHash()))); + b65.addTransaction(tx2); + } + b65.solve(); + blocks.add(new BlockAndValidity(b65, true, false, b65.getHash(), "b65")); + spendableOutputs.offer(new TransactionOutPointWithValue( + new TransactionOutPoint(params, 0, b65.getTransactions().get(0).getHash()), + b65.getTransactions().get(0).getOutputs().get(0).getValue(), + b65.getTransactions().get(0).getOutputs().get(0).getScriptPubKey())); + + // Attempt to spend an output created later in the same block + // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) + // \-> b66 (20) + // + TransactionOutPointWithValue out20 = spendableOutputs.poll(); + + Block b66 = createNextBlock(b65, chainHeadHeight + 19, null, null); + { + Transaction tx1 = new Transaction(params); + tx1.addOutput(new TransactionOutput(params, tx1, out20.value, new byte[]{ Script.OP_TRUE })); + addOnlyInputToTransaction(tx1, out20, 0); + Transaction tx2 = new Transaction(params); + tx2.addOutput(new TransactionOutput(params, tx2, BigInteger.ZERO, new byte[]{ Script.OP_TRUE })); + tx2.addInput(new TransactionInput(params, tx2, new byte[]{ Script.OP_TRUE }, + new TransactionOutPoint(params, 0, tx1.getHash()))); + b66.addTransaction(tx2); + b66.addTransaction(tx1); + } + b66.solve(); + blocks.add(new BlockAndValidity(b66, false, true, b65.getHash(), "b66")); + + // Attempt to double-spend a transaction created in a block + // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) + // \-> b67 (20) + // + Block b67 = createNextBlock(b65, chainHeadHeight + 19, null, null); + { + Transaction tx1 = new Transaction(params); + tx1.addOutput(new TransactionOutput(params, tx1, out20.value, new byte[]{ Script.OP_TRUE })); + addOnlyInputToTransaction(tx1, out20, 0); + b67.addTransaction(tx1); + Transaction tx2 = new Transaction(params); + tx2.addOutput(new TransactionOutput(params, tx2, BigInteger.ZERO, new byte[]{ Script.OP_TRUE })); + tx2.addInput(new TransactionInput(params, tx2, new byte[]{ Script.OP_TRUE }, + new TransactionOutPoint(params, 0, tx1.getHash()))); + b67.addTransaction(tx2); + Transaction tx3 = new Transaction(params); + tx3.addOutput(new TransactionOutput(params, tx3, out20.value, new byte[]{ Script.OP_TRUE })); + tx3.addInput(new TransactionInput(params, tx3, new byte[]{ Script.OP_TRUE }, + new TransactionOutPoint(params, 0, tx1.getHash()))); + b67.addTransaction(tx3); + } + b67.solve(); + blocks.add(new BlockAndValidity(b67, false, true, b65.getHash(), "b67")); + + // A few more tests of block subsidy + // -> b43 (13) -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) + // \-> b68 (20) + // + Block b68 = createNextBlock(b65, chainHeadHeight + 19, null, BigInteger.TEN); + { + Transaction tx = new Transaction(params); + tx.addOutput(new TransactionOutput(params, tx, out20.value.subtract(BigInteger.valueOf(9)), new byte[]{ Script.OP_TRUE })); + addOnlyInputToTransaction(tx, out20, 0); + b68.addTransaction(tx); + } + b68.solve(); + blocks.add(new BlockAndValidity(b68, false, true, b65.getHash(), "b68")); + + Block b69 = createNextBlock(b65, chainHeadHeight + 19, null, BigInteger.TEN); + { + Transaction tx = new Transaction(params); + tx.addOutput(new TransactionOutput(params, tx, out20.value.subtract(BigInteger.TEN), new byte[]{ Script.OP_TRUE })); + addOnlyInputToTransaction(tx, out20, 0); + b69.addTransaction(tx); + } + b69.solve(); + blocks.add(new BlockAndValidity(b69, true, false, b69.getHash(), "b69")); + + // Test spending the outpoint of a non-existent transaction + // -> b53 (14) -> b55 (15) -> b57 (16) -> b60 (17) -> b64 (18) -> b65 (19) -> b69 (20) + // \-> b70 (21) + // + TransactionOutPointWithValue out21 = spendableOutputs.poll(); + Block b70 = createNextBlock(b69, chainHeadHeight + 20, out21, null); + { + Transaction tx = new Transaction(params); + tx.addOutput(new TransactionOutput(params, tx, BigInteger.ZERO, new byte[]{ Script.OP_TRUE })); + tx.addInput(new TransactionInput(params, tx, new byte[]{ Script.OP_TRUE }, + new TransactionOutPoint(params, 0, new Sha256Hash("23c70ed7c0506e9178fc1a987f40a33946d4ad4c962b5ae3a52546da53af0c5c")))); + b70.addTransaction(tx); + } + b70.solve(); + blocks.add(new BlockAndValidity(b70, false, true, b69.getHash(), "b70")); + //TODO: Explicitly address MoneyRange() checks // (finally) return the created chain @@ -1075,7 +1242,7 @@ public class FullBlockTestGenerator { private Block createNextBlock(Block baseBlock, int nextBlockHeight, TransactionOutPointWithValue prevOut, BigInteger additionalCoinbaseValue) throws ScriptException { BigInteger coinbaseValue = Utils.toNanoCoins(50, 0).shiftRight(nextBlockHeight / params.getSubsidyDecreaseBlockCount()) - .add((prevOut != null ? prevOut.value : BigInteger.valueOf(0))).subtract(BigInteger.valueOf(1)) + .add((prevOut != null ? prevOut.value.subtract(BigInteger.ONE) : BigInteger.valueOf(0))) .add(additionalCoinbaseValue == null ? BigInteger.valueOf(0) : additionalCoinbaseValue); Block block = baseBlock.createNextBlockWithCoinbase(coinbaseOutKeyPubKey, coinbaseValue); if (prevOut != null) {