/* * Copyright 2014 Google Inc. * Copyright 2016 Andreas Schildbach * * 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 org.bitcoinj.core; import org.bitcoinj.core.TransactionConfidence.*; import org.bitcoinj.crypto.TransactionSignature; import org.bitcoinj.params.*; import org.bitcoinj.script.*; import org.bitcoinj.testing.*; import org.easymock.*; import org.junit.*; import java.math.BigInteger; import java.util.*; import static org.bitcoinj.core.Utils.HEX; import static org.easymock.EasyMock.*; import static org.junit.Assert.*; /** * Just check the Transaction.verify() method. Most methods that have complicated logic in Transaction are tested * elsewhere, e.g. signing and hashing are well exercised by the wallet tests, the full block chain tests and so on. * The verify method is also exercised by the full block chain tests, but it can also be used by API users alone, * so we make sure to cover it here as well. */ public class TransactionTest { private static final NetworkParameters PARAMS = UnitTestParams.get(); private static final Address ADDRESS = new ECKey().toAddress(PARAMS); private Transaction tx; @Before public void setUp() throws Exception { Context context = new Context(PARAMS); tx = FakeTxBuilder.createFakeTx(PARAMS); } @Test(expected = VerificationException.EmptyInputsOrOutputs.class) public void emptyOutputs() throws Exception { tx.clearOutputs(); tx.verify(); } @Test(expected = VerificationException.EmptyInputsOrOutputs.class) public void emptyInputs() throws Exception { tx.clearInputs(); tx.verify(); } @Test(expected = VerificationException.LargerThanMaxBlockSize.class) public void tooHuge() throws Exception { tx.getInput(0).setScriptBytes(new byte[Block.MAX_BLOCK_SIZE]); tx.verify(); } @Test(expected = VerificationException.DuplicatedOutPoint.class) public void duplicateOutPoint() throws Exception { TransactionInput input = tx.getInput(0); input.setScriptBytes(new byte[1]); tx.addInput(input.duplicateDetached()); tx.verify(); } @Test(expected = VerificationException.NegativeValueOutput.class) public void negativeOutput() throws Exception { tx.getOutput(0).setValue(Coin.NEGATIVE_SATOSHI); tx.verify(); } @Test(expected = VerificationException.ExcessiveValue.class) public void exceedsMaxMoney2() throws Exception { Coin half = PARAMS.getMaxMoney().divide(2).add(Coin.SATOSHI); tx.getOutput(0).setValue(half); tx.addOutput(half, ADDRESS); tx.verify(); } @Test(expected = VerificationException.UnexpectedCoinbaseInput.class) public void coinbaseInputInNonCoinbaseTX() throws Exception { tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[10]).build()); tx.verify(); } @Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class) public void coinbaseScriptSigTooSmall() throws Exception { tx.clearInputs(); tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().build()); tx.verify(); } @Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class) public void coinbaseScriptSigTooLarge() throws Exception { tx.clearInputs(); TransactionInput input = tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[99]).build()); assertEquals(101, input.getScriptBytes().length); tx.verify(); } @Test public void testEstimatedLockTime_WhenParameterSignifiesBlockHeight() { int TEST_LOCK_TIME = 20; Date now = Calendar.getInstance().getTime(); BlockChain mockBlockChain = createMock(BlockChain.class); EasyMock.expect(mockBlockChain.estimateBlockTime(TEST_LOCK_TIME)).andReturn(now); Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); tx.setLockTime(TEST_LOCK_TIME); // less than five hundred million replay(mockBlockChain); assertEquals(tx.estimateLockTime(mockBlockChain), now); } @Test public void testOptimalEncodingMessageSize() { Transaction tx = new Transaction(PARAMS); int length = tx.length; // add basic transaction input, check the length tx.addOutput(new TransactionOutput(PARAMS, null, Coin.COIN, ADDRESS)); length += getCombinedLength(tx.getOutputs()); // add basic output, check the length length += getCombinedLength(tx.getInputs()); // optimal encoding size should equal the length we just calculated assertEquals(tx.getOptimalEncodingMessageSize(), length); } private int getCombinedLength(List<? extends Message> list) { int sumOfAllMsgSizes = 0; for (Message m: list) { sumOfAllMsgSizes += m.getMessageSize() + 1; } return sumOfAllMsgSizes; } @Test public void testIsMatureReturnsFalseIfTransactionIsCoinbaseAndConfidenceTypeIsNotEqualToBuilding() { Transaction tx = FakeTxBuilder.createFakeCoinbaseTx(PARAMS); tx.getConfidence().setConfidenceType(ConfidenceType.UNKNOWN); assertEquals(tx.isMature(), false); tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); assertEquals(tx.isMature(), false); tx.getConfidence().setConfidenceType(ConfidenceType.DEAD); assertEquals(tx.isMature(), false); } @Test public void testCLTVPaymentChannelTransactionSpending() { BigInteger time = BigInteger.valueOf(20); ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey(); Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to); Transaction tx = new Transaction(PARAMS); tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {})); tx.getInput(0).setSequenceNumber(0); tx.setLockTime(time.subtract(BigInteger.ONE).longValue()); TransactionSignature fromSig = tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false); TransactionSignature toSig = tx.calculateSignature(0, to, outputScript, Transaction.SigHash.SINGLE, false); TransactionSignature incorrectSig = tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false); Script scriptSig = ScriptBuilder.createCLTVPaymentChannelInput(fromSig, toSig); Script refundSig = ScriptBuilder.createCLTVPaymentChannelRefund(fromSig); Script invalidScriptSig1 = ScriptBuilder.createCLTVPaymentChannelInput(fromSig, incorrectSig); Script invalidScriptSig2 = ScriptBuilder.createCLTVPaymentChannelInput(incorrectSig, toSig); try { scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); } catch (ScriptException e) { e.printStackTrace(); fail("Settle transaction failed to correctly spend the payment channel"); } try { refundSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); fail("Refund passed before expiry"); } catch (ScriptException e) { } try { invalidScriptSig1.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); fail("Invalid sig 1 passed"); } catch (ScriptException e) { } try { invalidScriptSig2.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); fail("Invalid sig 2 passed"); } catch (ScriptException e) { } } @Test public void testCLTVPaymentChannelTransactionRefund() { BigInteger time = BigInteger.valueOf(20); ECKey from = new ECKey(), to = new ECKey(), incorrect = new ECKey(); Script outputScript = ScriptBuilder.createCLTVPaymentChannelOutput(time, from, to); Transaction tx = new Transaction(PARAMS); tx.addInput(new TransactionInput(PARAMS, tx, new byte[] {})); tx.getInput(0).setSequenceNumber(0); tx.setLockTime(time.add(BigInteger.ONE).longValue()); TransactionSignature fromSig = tx.calculateSignature(0, from, outputScript, Transaction.SigHash.SINGLE, false); TransactionSignature incorrectSig = tx.calculateSignature(0, incorrect, outputScript, Transaction.SigHash.SINGLE, false); Script scriptSig = ScriptBuilder.createCLTVPaymentChannelRefund(fromSig); Script invalidScriptSig = ScriptBuilder.createCLTVPaymentChannelRefund(incorrectSig); try { scriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); } catch (ScriptException e) { e.printStackTrace(); fail("Refund failed to correctly spend the payment channel"); } try { invalidScriptSig.correctlySpends(tx, 0, outputScript, Script.ALL_VERIFY_FLAGS); fail("Invalid sig passed"); } catch (ScriptException e) { } } @Test public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() { Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); TransactionInput input = tx.getInput(0); input.setSequenceNumber(42); int TEST_LOCK_TIME = 20; tx.setLockTime(TEST_LOCK_TIME); Calendar cal = Calendar.getInstance(); cal.set(2085, 10, 4, 17, 53, 21); cal.set(Calendar.MILLISECOND, 0); BlockChain mockBlockChain = createMock(BlockChain.class); EasyMock.expect(mockBlockChain.estimateBlockTime(TEST_LOCK_TIME)).andReturn(cal.getTime()); replay(mockBlockChain); String str = tx.toString(mockBlockChain); assertEquals(str.contains("block " + TEST_LOCK_TIME), true); assertEquals(str.contains("estimated to be reached at"), true); } @Test public void testToStringWhenIteratingOverAnInputCatchesAnException() { Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); TransactionInput ti = new TransactionInput(PARAMS, tx, new byte[0]) { @Override public Script getScriptSig() throws ScriptException { throw new ScriptException(""); } }; tx.addInput(ti); assertEquals(tx.toString().contains("[exception: "), true); } @Test public void testToStringWhenThereAreZeroInputs() { Transaction tx = new Transaction(PARAMS); assertEquals(tx.toString().contains("No inputs!"), true); } @Test public void testTheTXByHeightComparator() { Transaction tx1 = FakeTxBuilder.createFakeTx(PARAMS); tx1.getConfidence().setAppearedAtChainHeight(1); Transaction tx2 = FakeTxBuilder.createFakeTx(PARAMS); tx2.getConfidence().setAppearedAtChainHeight(2); Transaction tx3 = FakeTxBuilder.createFakeTx(PARAMS); tx3.getConfidence().setAppearedAtChainHeight(3); SortedSet<Transaction> set = new TreeSet<Transaction>(Transaction.SORT_TX_BY_HEIGHT); set.add(tx2); set.add(tx1); set.add(tx3); Iterator<Transaction> iterator = set.iterator(); assertEquals(tx1.equals(tx2), false); assertEquals(tx1.equals(tx3), false); assertEquals(tx1.equals(tx1), true); assertEquals(iterator.next().equals(tx3), true); assertEquals(iterator.next().equals(tx2), true); assertEquals(iterator.next().equals(tx1), true); assertEquals(iterator.hasNext(), false); } @Test(expected = ScriptException.class) public void testAddSignedInputThrowsExceptionWhenScriptIsNotToRawPubKeyAndIsNotToAddress() { ECKey key = new ECKey(); Address addr = key.toAddress(PARAMS); Transaction fakeTx = FakeTxBuilder.createFakeTx(PARAMS, Coin.COIN, addr); Transaction tx = new Transaction(PARAMS); tx.addOutput(fakeTx.getOutput(0)); Script script = ScriptBuilder.createOpReturnScript(new byte[0]); tx.addSignedInput(fakeTx.getOutput(0).getOutPointFor(), script, key); } @Test public void testPrioSizeCalc() throws Exception { Transaction tx1 = FakeTxBuilder.createFakeTx(PARAMS, Coin.COIN, ADDRESS); int size1 = tx1.getMessageSize(); int size2 = tx1.getMessageSizeForPriorityCalc(); assertEquals(113, size1 - size2); tx1.getInput(0).setScriptSig(new Script(new byte[109])); assertEquals(78, tx1.getMessageSizeForPriorityCalc()); tx1.getInput(0).setScriptSig(new Script(new byte[110])); assertEquals(78, tx1.getMessageSizeForPriorityCalc()); tx1.getInput(0).setScriptSig(new Script(new byte[111])); assertEquals(79, tx1.getMessageSizeForPriorityCalc()); } @Test public void testCoinbaseHeightCheck() throws VerificationException { // Coinbase transaction from block 300,000 final byte[] transactionBytes = HEX.decode("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4803e09304062f503253482f0403c86d53087ceca141295a00002e522cfabe6d6d7561cf262313da1144026c8f7a43e3899c44f6145f39a36507d36679a8b7006104000000000000000000000001c8704095000000001976a91480ad90d403581fa3bf46086a91b2d9d4125db6c188ac00000000"); final int height = 300000; final Transaction transaction = PARAMS.getDefaultSerializer().makeTransaction(transactionBytes); transaction.checkCoinBaseHeight(height); } /** * Test a coinbase transaction whose script has nonsense after the block height. * See https://github.com/bitcoinj/bitcoinj/issues/1097 */ @Test public void testCoinbaseHeightCheckWithDamagedScript() throws VerificationException { // Coinbase transaction from block 224,430 final byte[] transactionBytes = HEX.decode( "010000000100000000000000000000000000000000000000000000000000000000" + "00000000ffffffff3b03ae6c0300044bd7031a0400000000522cfabe6d6d0000" + "0000000000b7b8bf0100000068692066726f6d20706f6f6c7365727665726aac" + "1eeeed88ffffffff01e0587597000000001976a91421c0d001728b3feaf11551" + "5b7c135e779e9f442f88ac00000000"); final int height = 224430; final Transaction transaction = PARAMS.getDefaultSerializer().makeTransaction(transactionBytes); transaction.checkCoinBaseHeight(height); } @Test public void optInFullRBF() { // a standard transaction as wallets would create Transaction tx = FakeTxBuilder.createFakeTx(PARAMS); assertFalse(tx.isOptInFullRBF()); tx.getInputs().get(0).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2); assertTrue(tx.isOptInFullRBF()); } /** * Ensure that hashForSignature() doesn't modify a transaction's data, which could wreak multithreading havoc. */ @Test public void testHashForSignatureThreadSafety() { Block genesis = UnitTestParams.get().getGenesisBlock(); Block block1 = genesis.createNextBlock(new ECKey().toAddress(UnitTestParams.get()), genesis.getTransactions().get(0).getOutput(0).getOutPointFor()); final Transaction tx = block1.getTransactions().get(1); final String txHash = tx.getHashAsString(); final String txNormalizedHash = tx.hashForSignature(0, new byte[0], Transaction.SigHash.ALL.byteValue()).toString(); for (int i = 0; i < 100; i++) { // ensure the transaction object itself was not modified; if it was, the hash will change assertEquals(txHash, tx.getHashAsString()); new Thread(){ public void run() { assertEquals(txNormalizedHash, tx.hashForSignature(0, new byte[0], Transaction.SigHash.ALL.byteValue()).toString()); } }; } } }