/* * Copyright 2012 Google Inc. * Copyright 2012 Matt Corallo. * * 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 com.google.common.collect.Lists; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.params.UnitTestParams; import org.bitcoinj.script.Script; import org.bitcoinj.store.BlockStoreException; import org.bitcoinj.store.FullPrunedBlockStore; import org.bitcoinj.utils.BlockFileLoader; import org.bitcoinj.utils.BriefLogFormatter; import org.bitcoinj.wallet.SendRequest; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.WalletTransaction; import org.junit.Before; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.List; import static org.bitcoinj.core.Coin.FIFTY_COINS; import static org.junit.Assert.*; import org.junit.rules.ExpectedException; /** * We don't do any wallet tests here, we leave that to {@link ChainSplitTest} */ public abstract class AbstractFullPrunedBlockChainTest { @org.junit.Rule public ExpectedException thrown = ExpectedException.none(); private static final Logger log = LoggerFactory.getLogger(AbstractFullPrunedBlockChainTest.class); protected static final NetworkParameters PARAMS = new UnitTestParams() { @Override public int getInterval() { return 10000; } }; protected FullPrunedBlockChain chain; protected FullPrunedBlockStore store; @Before public void setUp() throws Exception { BriefLogFormatter.init(); Context.propagate(new Context(PARAMS, 100, Coin.ZERO, false)); } public abstract FullPrunedBlockStore createStore(NetworkParameters params, int blockCount) throws BlockStoreException; public abstract void resetStore(FullPrunedBlockStore store) throws BlockStoreException; @Test public void testGeneratedChain() throws Exception { // Tests various test cases from FullBlockTestGenerator FullBlockTestGenerator generator = new FullBlockTestGenerator(PARAMS); RuleList blockList = generator.getBlocksToTest(false, false, null); store = createStore(PARAMS, blockList.maximumReorgBlockCount); chain = new FullPrunedBlockChain(PARAMS, store); for (Rule rule : blockList.list) { if (!(rule instanceof FullBlockTestGenerator.BlockAndValidity)) continue; FullBlockTestGenerator.BlockAndValidity block = (FullBlockTestGenerator.BlockAndValidity) rule; log.info("Testing rule " + block.ruleName + " with block hash " + block.block.getHash()); boolean threw = false; try { if (chain.add(block.block) != block.connects) { log.error("Block didn't match connects flag on block " + block.ruleName); fail(); } } catch (VerificationException e) { threw = true; if (!block.throwsException) { log.error("Block didn't match throws flag on block " + block.ruleName); throw e; } if (block.connects) { log.error("Block didn't match connects flag on block " + block.ruleName); fail(); } } if (!threw && block.throwsException) { log.error("Block didn't match throws flag on block " + block.ruleName); fail(); } if (!chain.getChainHead().getHeader().getHash().equals(block.hashChainTipAfterBlock)) { log.error("New block head didn't match the correct value after block " + block.ruleName); fail(); } if (chain.getChainHead().getHeight() != block.heightAfterBlock) { log.error("New block head didn't match the correct height after block " + block.ruleName); fail(); } } try { store.close(); } catch (Exception e) {} } @Test public void skipScripts() throws Exception { store = createStore(PARAMS, 10); chain = new FullPrunedBlockChain(PARAMS, store); // Check that we aren't accidentally leaving any references // to the full StoredUndoableBlock's lying around (ie memory leaks) ECKey outKey = new ECKey(); int height = 1; // Build some blocks on genesis block to create a spendable output Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); TransactionOutput spendableOutput = rollingBlock.getTransactions().get(0).getOutput(0); for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) { rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); } rollingBlock = rollingBlock.createNextBlock(null); Transaction t = new Transaction(PARAMS); t.addOutput(new TransactionOutput(PARAMS, t, FIFTY_COINS, new byte[] {})); TransactionInput input = t.addInput(spendableOutput); // Invalid script. input.clearScriptBytes(); rollingBlock.addTransaction(t); rollingBlock.solve(); chain.setRunScripts(false); try { chain.add(rollingBlock); } catch (VerificationException e) { fail(); } try { store.close(); } catch (Exception e) {} } @Test public void testFinalizedBlocks() throws Exception { final int UNDOABLE_BLOCKS_STORED = 10; store = createStore(PARAMS, UNDOABLE_BLOCKS_STORED); chain = new FullPrunedBlockChain(PARAMS, store); // Check that we aren't accidentally leaving any references // to the full StoredUndoableBlock's lying around (ie memory leaks) ECKey outKey = new ECKey(); int height = 1; // Build some blocks on genesis block to create a spendable output Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, rollingBlock.getTransactions().get(0).getHash()); byte[] spendableOutputScriptPubKey = rollingBlock.getTransactions().get(0).getOutputs().get(0).getScriptBytes(); for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) { rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); } WeakReference<UTXO> out = new WeakReference<UTXO> (store.getTransactionOutput(spendableOutput.getHash(), spendableOutput.getIndex())); rollingBlock = rollingBlock.createNextBlock(null); Transaction t = new Transaction(PARAMS); // Entirely invalid scriptPubKey t.addOutput(new TransactionOutput(PARAMS, t, FIFTY_COINS, new byte[]{})); t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); rollingBlock.addTransaction(t); rollingBlock.solve(); chain.add(rollingBlock); WeakReference<StoredUndoableBlock> undoBlock = new WeakReference<StoredUndoableBlock>(store.getUndoBlock(rollingBlock.getHash())); StoredUndoableBlock storedUndoableBlock = undoBlock.get(); assertNotNull(storedUndoableBlock); assertNull(storedUndoableBlock.getTransactions()); WeakReference<TransactionOutputChanges> changes = new WeakReference<TransactionOutputChanges>(storedUndoableBlock.getTxOutChanges()); assertNotNull(changes.get()); storedUndoableBlock = null; // Blank the reference so it can be GCd. // Create a chain longer than UNDOABLE_BLOCKS_STORED for (int i = 0; i < UNDOABLE_BLOCKS_STORED; i++) { rollingBlock = rollingBlock.createNextBlock(null); chain.add(rollingBlock); } // Try to get the garbage collector to run System.gc(); assertNull(undoBlock.get()); assertNull(changes.get()); assertNull(out.get()); try { store.close(); } catch (Exception e) {} } @Test public void testFirst100KBlocks() throws Exception { NetworkParameters params = MainNetParams.get(); Context context = new Context(params); File blockFile = new File(getClass().getResource("first-100k-blocks.dat").getFile()); BlockFileLoader loader = new BlockFileLoader(params, Arrays.asList(blockFile)); store = createStore(params, 10); resetStore(store); chain = new FullPrunedBlockChain(context, store); for (Block block : loader) chain.add(block); try { store.close(); } catch (Exception e) {} } @Test public void testGetOpenTransactionOutputs() throws Exception { final int UNDOABLE_BLOCKS_STORED = 10; store = createStore(PARAMS, UNDOABLE_BLOCKS_STORED); chain = new FullPrunedBlockChain(PARAMS, store); // Check that we aren't accidentally leaving any references // to the full StoredUndoableBlock's lying around (ie memory leaks) ECKey outKey = new ECKey(); int height = 1; // Build some blocks on genesis block to create a spendable output Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); Transaction transaction = rollingBlock.getTransactions().get(0); TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, transaction.getHash()); byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes(); for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) { rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); } rollingBlock = rollingBlock.createNextBlock(null); // Create bitcoin spend of 1 BTC. ECKey toKey = new ECKey(); Coin amount = Coin.valueOf(100000000); Address address = new Address(PARAMS, toKey.getPubKeyHash()); Coin totalAmount = Coin.ZERO; Transaction t = new Transaction(PARAMS); t.addOutput(new TransactionOutput(PARAMS, t, amount, toKey)); t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); rollingBlock.addTransaction(t); rollingBlock.solve(); chain.add(rollingBlock); totalAmount = totalAmount.add(amount); List<UTXO> outputs = store.getOpenTransactionOutputs(Lists.newArrayList(address)); assertNotNull(outputs); assertEquals("Wrong Number of Outputs", 1, outputs.size()); UTXO output = outputs.get(0); assertEquals("The address is not equal", address.toString(), output.getAddress()); assertEquals("The amount is not equal", totalAmount, output.getValue()); outputs = null; output = null; try { store.close(); } catch (Exception e) {} } @Test public void testUTXOProviderWithWallet() throws Exception { final int UNDOABLE_BLOCKS_STORED = 10; store = createStore(PARAMS, UNDOABLE_BLOCKS_STORED); chain = new FullPrunedBlockChain(PARAMS, store); // Check that we aren't accidentally leaving any references // to the full StoredUndoableBlock's lying around (ie memory leaks) ECKey outKey = new ECKey(); int height = 1; // Build some blocks on genesis block to create a spendable output. Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); Transaction transaction = rollingBlock.getTransactions().get(0); TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, transaction.getHash()); byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes(); for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) { rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++); chain.add(rollingBlock); } rollingBlock = rollingBlock.createNextBlock(null); // Create 1 BTC spend to a key in this wallet (to ourselves). Wallet wallet = new Wallet(PARAMS); assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); assertEquals("Estimated balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.ESTIMATED)); wallet.setUTXOProvider(store); ECKey toKey = wallet.freshReceiveKey(); Coin amount = Coin.valueOf(100000000); Transaction t = new Transaction(PARAMS); t.addOutput(new TransactionOutput(PARAMS, t, amount, toKey)); t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey); rollingBlock.addTransaction(t); rollingBlock.solve(); chain.add(rollingBlock); // Create another spend of 1/2 the value of BTC we have available using the wallet (store coin selector). ECKey toKey2 = new ECKey(); Coin amount2 = amount.divide(2); Address address2 = new Address(PARAMS, toKey2.getPubKeyHash()); SendRequest req = SendRequest.to(address2, amount2); wallet.completeTx(req); wallet.commitTx(req.tx); Coin fee = Coin.ZERO; // There should be one pending tx (our spend). assertEquals("Wrong number of PENDING.4", 1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); Coin totalPendingTxAmount = Coin.ZERO; for (Transaction tx : wallet.getPendingTransactions()) { totalPendingTxAmount = totalPendingTxAmount.add(tx.getValueSentToMe(wallet)); } // The availbale balance should be the 0 (as we spent the 1 BTC that's pending) and estimated should be 1/2 - fee BTC assertEquals("Available balance is incorrect", Coin.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE)); assertEquals("Estimated balance is incorrect", amount2.subtract(fee), wallet.getBalance(Wallet.BalanceType.ESTIMATED)); assertEquals("Pending tx amount is incorrect", amount2.subtract(fee), totalPendingTxAmount); try { store.close(); } catch (Exception e) {} } /** * Test that if the block height is missing from coinbase of a version 2 * block, it's rejected. */ @Test public void missingHeightFromCoinbase() throws Exception { final int UNDOABLE_BLOCKS_STORED = PARAMS.getMajorityEnforceBlockUpgrade() + 1; store = createStore(PARAMS, UNDOABLE_BLOCKS_STORED); try { chain = new FullPrunedBlockChain(PARAMS, store); ECKey outKey = new ECKey(); int height = 1; Block chainHead = PARAMS.getGenesisBlock(); // Build some blocks on genesis block to create a spendable output. // Put in just enough v1 blocks to stop the v2 blocks from forming a majority for (height = 1; height <= (PARAMS.getMajorityWindow() - PARAMS.getMajorityEnforceBlockUpgrade()); height++) { chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height); chain.add(chainHead); } // Fill the rest of the window in with v2 blocks for (; height < PARAMS.getMajorityWindow(); height++) { chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_BIP34, outKey.getPubKey(), height); chain.add(chainHead); } // Throw a broken v2 block in before we have a supermajority to enable // enforcement, which should validate as-is chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_BIP34, outKey.getPubKey(), height * 2); chain.add(chainHead); height++; // Trying to add a broken v2 block should now result in rejection as // we have a v2 supermajority thrown.expect(VerificationException.CoinbaseHeightMismatch.class); chainHead = chainHead.createNextBlockWithCoinbase(Block.BLOCK_VERSION_BIP34, outKey.getPubKey(), height * 2); chain.add(chainHead); } catch(final VerificationException ex) { throw (Exception) ex.getCause(); } finally { try { store.close(); } catch(Exception e) { // Catch and drop any exception so a break mid-test doesn't result // in a new exception being thrown and the original lost } } } }