From 4d1e6259a7d845c40374f265e89db50d223fb962 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Wed, 30 May 2012 13:58:11 +0200 Subject: [PATCH] coinbase phase 3 - coinbase death and resurrection now working (updated with Mike's comments) --- .../java/com/google/bitcoin/core/Block.java | 44 +++++-- .../com/google/bitcoin/core/BlockChain.java | 28 ----- .../bitcoin/core/NetworkParameters.java | 16 ++- .../com/google/bitcoin/core/Transaction.java | 13 ++ .../java/com/google/bitcoin/core/Wallet.java | 92 +++++++++++--- .../google/bitcoin/core/BlockChainTest.java | 87 +++++++++++++ .../google/bitcoin/core/ChainSplitTest.java | 116 ++++++++++++++++++ .../bitcoin/core/CoinbaseBlockTest.java | 102 +++++++++++++++ .../com/google/bitcoin/core/WalletTest.java | 3 + .../com/google/bitcoin/core/block169482.dat | Bin 0 -> 18678 bytes 10 files changed, 440 insertions(+), 61 deletions(-) create mode 100644 core/src/test/java/com/google/bitcoin/core/CoinbaseBlockTest.java create mode 100644 core/src/test/resources/com/google/bitcoin/core/block169482.dat 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 2c2c07790..d34771c07 100644 --- a/core/src/main/java/com/google/bitcoin/core/Block.java +++ b/core/src/main/java/com/google/bitcoin/core/Block.java @@ -844,22 +844,32 @@ public class Block extends Message { * Returns a solved block that builds on top of this one. This exists for unit tests. */ Block createNextBlock(Address to, long time) { + return createNextBlock(to, time, EMPTY_BYTES); + } + + /** + * Returns a solved block that builds on top of this one. This exists for unit tests. + * In this variant you can specify a public key (pubkey) for use in generating coinbase blocks. + */ + Block createNextBlock(Address to, long time, byte[] pubKey) { Block b = new Block(params); b.setDifficultyTarget(difficultyTarget); - b.addCoinbaseTransaction(EMPTY_BYTES); + b.addCoinbaseTransaction(pubKey); - // Add a transaction paying 50 coins to the "to" address. - Transaction t = new Transaction(params); - t.addOutput(new TransactionOutput(params, t, Utils.toNanoCoins(50, 0), to)); - // The input does not really need to be a valid signature, as long as it has the right general form. - TransactionInput input = new TransactionInput(params, t, Script.createInputScript(EMPTY_BYTES, EMPTY_BYTES)); - // Importantly the outpoint hash cannot be zero as that's how we detect a coinbase transaction in isolation - // but it must be unique to avoid 'different' transactions looking the same. - byte[] counter = new byte[32]; - counter[0] = (byte) txCounter++; - input.getOutpoint().setHash(new Sha256Hash(counter)); - t.addInput(input); - b.addTransaction(t); + if (to != null) { + // Add a transaction paying 50 coins to the "to" address. + Transaction t = new Transaction(params); + t.addOutput(new TransactionOutput(params, t, Utils.toNanoCoins(50, 0), to)); + // The input does not really need to be a valid signature, as long as it has the right general form. + TransactionInput input = new TransactionInput(params, t, Script.createInputScript(EMPTY_BYTES, EMPTY_BYTES)); + // Importantly the outpoint hash cannot be zero as that's how we detect a coinbase transaction in isolation + // but it must be unique to avoid 'different' transactions looking the same. + byte[] counter = new byte[32]; + counter[0] = (byte) txCounter++; + input.getOutpoint().setHash(new Sha256Hash(counter)); + t.addInput(input); + b.addTransaction(t); + } b.setPrevBlockHash(getHash()); b.setTime(time); @@ -877,6 +887,14 @@ public class Block extends Message { return createNextBlock(to, Utils.now().getTime() / 1000); } + /** + * Create a block sending 50BTC as a coinbase transaction to the public key specified. + * This method is intended for test use only. + */ + Block createNextBlockWithCoinbase(byte[] pubKey) { + return createNextBlock(null, Utils.now().getTime() / 1000, pubKey); + } + /** * Used for unit test * diff --git a/core/src/main/java/com/google/bitcoin/core/BlockChain.java b/core/src/main/java/com/google/bitcoin/core/BlockChain.java index fb4762b1e..c25d28998 100644 --- a/core/src/main/java/com/google/bitcoin/core/BlockChain.java +++ b/core/src/main/java/com/google/bitcoin/core/BlockChain.java @@ -510,34 +510,6 @@ public class BlockChain { } } - /** - * For the transactions in the given block, update the txToWalletMap such that each wallet maps to a list of - * transactions for which it is relevant. - */ - private void scanTransactions(Block block, HashMap> walletToTxMap) - throws VerificationException { - for (Transaction tx : block.transactions) { - try { - for (Wallet wallet : wallets) { - if (tx.isCoinBase()) - continue; - boolean shouldReceive = wallet.isTransactionRelevant(tx, true); - if (!shouldReceive) continue; - List txList = walletToTxMap.get(wallet); - if (txList == null) { - txList = new LinkedList(); - walletToTxMap.put(wallet, txList); - } - txList.add(tx); - } - } catch (ScriptException e) { - // We don't want scripts we don't understand to break the block chain so just note that this tx was - // not scanned here and continue. - log.warn("Failed to parse a script: " + e.toString()); - } - } - } - /** * Returns true if any connected wallet considers any transaction in the block to be relevant. */ diff --git a/core/src/main/java/com/google/bitcoin/core/NetworkParameters.java b/core/src/main/java/com/google/bitcoin/core/NetworkParameters.java index c1d71b625..b0f25d904 100644 --- a/core/src/main/java/com/google/bitcoin/core/NetworkParameters.java +++ b/core/src/main/java/com/google/bitcoin/core/NetworkParameters.java @@ -54,7 +54,6 @@ public class NetworkParameters implements Serializable { */ public static final String ID_TESTNET = "org.bitcoin.test"; - // TODO: Seed nodes and checkpoint values should be here as well. /** @@ -103,6 +102,11 @@ public class NetworkParameters implements Serializable { */ private String id; + /** + * The depth of blocks required for a coinbase transaction to be spendable. + */ + private int spendableCoinbaseDepth; + /** * The version codes that prefix addresses which are acceptable on this network. Although Satoshi intended these to * be used for "versioning", in fact they are today used to discriminate what kind of data is contained in the @@ -153,6 +157,7 @@ public class NetworkParameters implements Serializable { n.genesisBlock.setTime(1296688602L); n.genesisBlock.setDifficultyTarget(0x1d07fff8L); n.genesisBlock.setNonce(384568319); + n.setSpendableCoinbaseDepth(5); n.id = ID_TESTNET; String genesisHash = n.genesisBlock.getHashAsString(); checkState(genesisHash.equals("00000007199508e34a9ff81e6ec0c477a4cccff2a4767a8eee39c11db367b008"), @@ -182,6 +187,7 @@ public class NetworkParameters implements Serializable { n.genesisBlock.setDifficultyTarget(0x1d00ffffL); n.genesisBlock.setTime(1231006505L); n.genesisBlock.setNonce(2083236893); + n.setSpendableCoinbaseDepth(120); n.id = ID_PRODNET; String genesisHash = n.genesisBlock.getHashAsString(); checkState(genesisHash.equals("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"), @@ -233,4 +239,12 @@ public class NetworkParameters implements Serializable { return null; } } + + public int getSpendableCoinbaseDepth() { + return spendableCoinbaseDepth; + } + + public void setSpendableCoinbaseDepth(int coinbaseDepth) { + this.spendableCoinbaseDepth = coinbaseDepth; + } } 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 b5ff61122..3c4aa2d12 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -521,6 +521,19 @@ public class Transaction extends ChildMessage implements Serializable { return inputs.get(0).isCoinBase(); } + /** + * A transaction is mature if it is either a building coinbase tx that is as deep or deeper than the required coinbase depth, or a non-coinbase tx. + */ + public boolean isMature() { + if (!isCoinBase()) + return true; + + if (getConfidence().getConfidenceType() != ConfidenceType.BUILDING) + return false; + + return getConfidence().getDepthInBlocks() >= params.getSpendableCoinbaseDepth(); + } + /** * A human readable version of the transaction useful for debugging. The format is not guaranteed to be stable. */ diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 785d6e373..43b56c998 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -123,7 +123,7 @@ public class Wallet implements Serializable { * Note that in the case where a transaction appears in both the best chain and a side chain as well, it is not * placed in this map. It's an error for a transaction to be in both the inactive pool and unspent/spent. */ - private Map inactive; + Map inactive; /** * A dead transaction is one that's been overridden by a double spend. Such a transaction is pending except it @@ -131,7 +131,7 @@ public class Wallet implements Serializable { * should nearly never happen in normal usage. Dead transactions can be "resurrected" by re-orgs just like any * other. Dead transactions are not in the pending pool. */ - private Map dead; + Map dead; /** * A list of public/private EC keys owned by this user. @@ -461,10 +461,7 @@ public class Wallet implements Serializable { */ public synchronized boolean isTransactionRelevant(Transaction tx, boolean includeDoubleSpending) throws ScriptException { - // For now we never consider coinbase transactions relevant, because we have not implemented the rules for - // tracking when to spend them, so we must avoid them entering the wallet. - return !tx.isCoinBase() && - tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 || + return tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 || tx.getValueSentToMe(this).compareTo(BigInteger.ZERO) > 0 || (includeDoubleSpending && (findDoubleSpendAgainstPending(tx) != null)); } @@ -507,7 +504,7 @@ public class Wallet implements Serializable { BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe); if (!reorg) { - log.info("Received tx{} for {} BTC: {}", new Object[]{sideChain ? " on a side chain" : "", + log.info("Received tx {} for {} BTC: {}", new Object[]{sideChain ? " on a side chain" : "", bitcoinValueToFriendlyString(valueDifference), tx.getHashAsString()}); } @@ -632,16 +629,30 @@ public class Wallet implements Serializable { */ private void processTxFromBestChain(Transaction tx) throws VerificationException, ScriptException { // This TX may spend our existing outputs even though it was not pending. This can happen in unit - // tests, if keys are moved between wallets, and if we're catching up to the chain given only a set of keys. + // tests, if keys are moved between wallets, if we're catching up to the chain given only a set of keys, + // or if a dead coinbase transaction has moved back onto the main chain. + boolean isDeadCoinbase = tx.isCoinBase() && dead.containsKey(tx.getHash()); + if (isDeadCoinbase) { + // There is a dead coinbase tx being received on the best chain. + // A coinbase tx is made dead when it moves to a side chain but it can be switched back on a reorg and + // 'resurrected' back to spent or unspent. + // Take it out of the dead pool. + // The receive method will then treat it as a new transaction and put it in spent or unspent as appropriate. + // TODO - Theorectically it could also be in the dead pool if it was double spent - this needs testing. + log.info(" coinbase tx {} <-dead", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); + dead.remove(tx.getHash()); + } + updateForSpends(tx, true); + if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) { // It's sending us coins. - log.info(" new tx ->unspent"); + log.info(" new tx {} ->unspent"); boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "TX was received twice"); } else if (!tx.getValueSentFromMe(this).equals(BigInteger.ZERO)) { // It spent some of our coins and did not send us any. - log.info(" new tx ->spent"); + log.info(" new tx {} ->spent"); boolean alreadyPresent = spent.put(tx.getHash(), tx) != null; checkState(!alreadyPresent, "TX was received twice"); } else { @@ -1153,6 +1164,10 @@ public class Wallet implements Serializable { BigInteger valueGathered = BigInteger.ZERO; List gathered = new LinkedList(); for (Transaction tx : unspent.values()) { + // Do not try and spend coinbases that were mined too recently, the protocol forbids it. + if (!tx.isMature()) { + continue; + } for (TransactionOutput output : tx.getOutputs()) { if (!output.isAvailableForSpending()) continue; if (!output.isMine(this)) continue; @@ -1306,6 +1321,11 @@ public class Wallet implements Serializable { public synchronized BigInteger getBalance(BalanceType balanceType) { BigInteger available = BigInteger.ZERO; for (Transaction tx : unspent.values()) { + // For an 'available to spend' balance exclude coinbase transactions that have not yet matured. + if (balanceType == BalanceType.AVAILABLE && !tx.isMature()) { + continue; + } + for (TransactionOutput output : tx.getOutputs()) { if (!output.isMine(this)) continue; if (!output.isAvailableForSpending()) continue; @@ -1430,6 +1450,14 @@ public class Wallet implements Serializable { all.putAll(unspent); all.putAll(spent); all.putAll(inactive); + + // Dead coinbase transactions are potentially resurrected so added to the list of tx to process. + for (Transaction tx : dead.values()) { + if (tx.isCoinBase()) { + all.put(tx.getHash(), tx); + } + } + for (Transaction tx : all.values()) { Collection appearsIn = tx.getAppearsInHashes(); checkNotNull(appearsIn); @@ -1502,11 +1530,32 @@ public class Wallet implements Serializable { spent.put(tx.getHash(), tx); } } + // Inform all transactions that exist only in the old chain that they have moved, so they can update confidence // and timestamps. Transactions will be told they're on the new best chain when the blocks are replayed. for (Transaction tx : onlyOldChainTransactions.values()) { tx.notifyNotOnBestChain(); + + // Kill any coinbase transactions that are only in the old chain. + // These transactions are no longer valid. + if (tx.isCoinBase()) { + // Move the transaction to the dead pool. + if (unspent.containsKey(tx.getHash())) { + log.info(" coinbase tx {} unspent->dead", tx.getHashAsString()); + unspent.remove(tx.getHash()); + } else if (spent.containsKey(tx.getHash())) { + log.info(" coinbase tx {} spent->dead", tx.getHashAsString()); + // TODO Remove any dependent child transactions of the just removed coinbase transaction. + spent.remove(tx.getHash()); + } + dead.put(tx.getHash(), tx); + + // Set transaction confidence to dead and notify listeners. + tx.getConfidence().setConfidenceType(ConfidenceType.DEAD); + invokeOnTransactionConfidenceChanged(tx); + } } + // Now replay the act of receiving the blocks that were previously in a side chain. This will: // - Move any transactions that were pending and are now accepted into the right bucket. // - Connect the newly active transactions. @@ -1545,6 +1594,7 @@ public class Wallet implements Serializable { log.info(" containing tx {}", tx.getHashAsString()); } } + if (!txns.isEmpty()) { // Add the transactions to the new blocks. for (Transaction t : txns) { @@ -1615,7 +1665,15 @@ public class Wallet implements Serializable { } private void reprocessUnincludedTxAfterReorg(Map pool, Transaction tx) { - log.info("TX {}", tx.getHashAsString()); + log.info("TX {}", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); + + boolean isDeadCoinbase = tx.isCoinBase() && ConfidenceType.DEAD == tx.getConfidence().getConfidenceType(); + + // Dead coinbase transactions on a side chain stay dead. + if (isDeadCoinbase) { + return; + } + int numInputs = tx.getInputs().size(); int noSuchTx = 0; int success = 0; @@ -1624,11 +1682,6 @@ public class Wallet implements Serializable { // bucket if all their outputs got spent. Set connectedTransactions = new TreeSet(); for (TransactionInput input : tx.getInputs()) { - if (input.isCoinBase()) { - // Input is not in our wallet so there is "no such input tx", bit of an abuse. - noSuchTx++; - continue; - } TransactionInput.ConnectionResult result = input.connect(pool, TransactionInput.ConnectMode.ABORT_ON_CONFLICT); if (result == TransactionInput.ConnectionResult.SUCCESS) { success++; @@ -1653,12 +1706,14 @@ public class Wallet implements Serializable { } if (isDead) return; + // If all inputs do not appear in this wallet move to inactive. if (noSuchTx == numInputs) { - log.info(" ->inactive", tx.getHashAsString()); + log.info(" ->inactive", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); inactive.put(tx.getHash(), tx); + dead.remove(tx.getHash()); } else if (success == numInputs - noSuchTx) { // All inputs are either valid for spending or don't come from us. Miners are trying to reinclude it. - log.info(" ->pending", tx.getHashAsString()); + log.info(" ->pending", tx.getHashAsString() + ", confidence = " + tx.getConfidence().getConfidenceType().name()); pending.put(tx.getHash(), tx); dead.remove(tx.getHash()); } @@ -1679,7 +1734,6 @@ public class Wallet implements Serializable { }); } - /** * Returns an immutable view of the transactions currently waiting for network confirmations. */ diff --git a/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java b/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java index 02eef2949..afe38492b 100644 --- a/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java +++ b/core/src/test/java/com/google/bitcoin/core/BlockChainTest.java @@ -16,6 +16,7 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.Wallet.BalanceType; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.MemoryBlockStore; import com.google.bitcoin.utils.BriefLogFormatter; @@ -40,6 +41,7 @@ public class BlockChainTest { private Address coinbaseTo; private NetworkParameters unitTestParams; private final StoredBlock[] block = new StoredBlock[1]; + private Transaction coinbaseTransaction; private void resetBlockStore() { blockStore = new MemoryBlockStore(unitTestParams); @@ -55,6 +57,9 @@ public class BlockChainTest { public void receiveFromBlock(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException { super.receiveFromBlock(tx, block, blockType); BlockChainTest.this.block[0] = block; + if (tx.isCoinBase()) { + BlockChainTest.this.coinbaseTransaction = tx; + } } }; wallet.addKey(new ECKey()); @@ -252,6 +257,88 @@ public class BlockChainTest { assertEquals(BigInteger.ZERO, wallet.getBalance()); } + @Test + public void coinbaseTransactionAvailability() throws Exception { + // Check that a coinbase transaction is only available to spend after NetworkParameters.getSpendableCoinbaseDepth() blocks. + + // Create a second wallet to receive the coinbase spend. + Wallet wallet2 = new Wallet(unitTestParams); + ECKey receiveKey = new ECKey(); + wallet2.addKey(receiveKey); + chain.addWallet(wallet2); + + Address addressToSendTo = receiveKey.toAddress(unitTestParams); + + // Create a block, sending the coinbase to the coinbaseTo address (which is in the wallet). + Block b1 = unitTestParams.genesisBlock.createNextBlockWithCoinbase(wallet.keychain.get(0).getPubKey()); + chain.add(b1); + + // Check a transaction has been received. + assertNotNull(coinbaseTransaction); + + // The coinbase tx is not yet available to spend. + assertTrue(wallet.getBalance().equals(BigInteger.ZERO)); + assertTrue(wallet.getBalance(BalanceType.ESTIMATED).equals(Utils.toNanoCoins(50, 0))); + assertTrue(!coinbaseTransaction.isMature()); + + // Attempt to spend the coinbase - this should fail as the coinbase is not mature yet. + Transaction coinbaseSpend = wallet.createSend(addressToSendTo, Utils.toNanoCoins(49, 0)); + assertNull(coinbaseSpend); + + // Check that the coinbase is unavailable to spend for the next spendableCoinbaseDepth - 2 blocks. + for (int i = 0; i < unitTestParams.getSpendableCoinbaseDepth() - 2; i++) { + // Non relevant tx - just for fake block creation. + Transaction tx2 = createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), + new ECKey().toAddress(unitTestParams)); + + Block b2 = createFakeBlock(unitTestParams, blockStore, tx2).block; + chain.add(b2); + + // Wallet still does not have the coinbase transaction available for spend. + assertTrue(wallet.getBalance().equals(BigInteger.ZERO)); + assertTrue(wallet.getBalance(BalanceType.ESTIMATED).equals(Utils.toNanoCoins(50, 0))); + + // The coinbase transaction is still not mature. + assertTrue(!coinbaseTransaction.isMature()); + + // Attempt to spend the coinbase - this should fail. + coinbaseSpend = wallet.createSend(addressToSendTo, Utils.toNanoCoins(49, 0)); + assertNull(coinbaseSpend); + } + + // Give it one more block - should now be able to spend coinbase transaction. + // Non relevant tx. + Transaction tx3 = createFakeTx(unitTestParams, Utils.toNanoCoins(1, 0), + new ECKey().toAddress(unitTestParams)); + + Block b3 = createFakeBlock(unitTestParams, blockStore, tx3).block; + chain.add(b3); + + // Wallet now has the coinbase transaction available for spend. + assertTrue(wallet.getBalance().equals( Utils.toNanoCoins(50, 0))); + assertTrue(wallet.getBalance(BalanceType.ESTIMATED).equals(Utils.toNanoCoins(50, 0))); + assertTrue(coinbaseTransaction.isMature()); + + // Create a spend with the coinbase BTC to the address in the second wallet - this should now succeed. + coinbaseSpend = wallet.createSend(addressToSendTo, Utils.toNanoCoins(49, 0)); + assertNotNull(coinbaseSpend); + + // Commit the coinbaseSpend to the first wallet and check the balances decrement. + wallet.commitTx(coinbaseSpend); + assertTrue(wallet.getBalance(BalanceType.ESTIMATED).equals( Utils.toNanoCoins(1, 0))); + // Available balance is zero as change has not been received from a block yet. + assertTrue(wallet.getBalance(BalanceType.AVAILABLE).equals( Utils.toNanoCoins(0, 0))); + + // Give it one more block - change from coinbaseSpend should now be available in the first wallet. + Block b4 = createFakeBlock(unitTestParams, blockStore, coinbaseSpend).block; + chain.add(b4); + assertTrue(wallet.getBalance(BalanceType.AVAILABLE).equals(Utils.toNanoCoins(1, 0))); + + // Check the balances in the second wallet. + assertTrue(wallet2.getBalance(BalanceType.ESTIMATED).equals( Utils.toNanoCoins(49, 0))); + assertTrue(wallet2.getBalance(BalanceType.AVAILABLE).equals( Utils.toNanoCoins(49, 0))); + } + // Some blocks from the test net. private Block getBlock2() throws Exception { Block b2 = new Block(testNet); diff --git a/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java b/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java index 6d22b6fec..b08e8525f 100644 --- a/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java +++ b/core/src/test/java/com/google/bitcoin/core/ChainSplitTest.java @@ -16,10 +16,13 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.TransactionConfidence.ConfidenceType; import com.google.bitcoin.store.MemoryBlockStore; import com.google.bitcoin.utils.BriefLogFormatter; import org.junit.Before; import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.math.BigInteger; import java.util.ArrayList; @@ -27,6 +30,8 @@ import java.util.ArrayList; import static org.junit.Assert.*; public class ChainSplitTest { + private static final Logger log = LoggerFactory.getLogger(ChainSplitTest.class); + private NetworkParameters unitTestParams; private Wallet wallet; private BlockChain chain; @@ -428,4 +433,115 @@ public class ChainSplitTest { assertEquals(newWork2.add(extraWork), txns.get(1).getConfidence().getWorkDone()); assertEquals(newWork3.add(extraWork), txns.get(2).getConfidence().getWorkDone()); } + + @Test + public void coinbaseDeath() throws Exception { + // Check that a coinbase tx is marked as dead after a reorg rather than inactive as normal non-double-spent transactions would be. + // Also check that a dead coinbase on a sidechain is resurrected if the sidechain becomes the best chain once more. + final ArrayList txns = new ArrayList(3); + wallet.addEventListener(new AbstractWalletEventListener() { + @Override + public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) { + txns.add(tx); + } + }); + + // Start by building three blocks on top of the genesis block. + // The first block contains a normal transaction that spends to coinTo. + // The second block contains a coinbase transaction that spends to coinTo2. + // The third block contains a normal transaction that spends to coinTo. + Block b1 = unitTestParams.genesisBlock.createNextBlock(coinsTo); + Block b2 = b1.createNextBlockWithCoinbase(wallet.keychain.get(1).getPubKey()); + Block b3 = b2.createNextBlock(coinsTo); + + log.debug("Adding block b1"); + assertTrue(chain.add(b1)); + log.debug("Adding block b2"); + assertTrue(chain.add(b2)); + log.debug("Adding block b3"); + assertTrue(chain.add(b3)); + + // We now have the following chain: + // genesis -> b1 -> b2 -> b3 + // + + // Check we have seen the three transactions. + assertEquals(3, txns.size()); + + // Check the coinbase transaction is building and in the unspent pool only. + assertEquals(ConfidenceType.BUILDING, txns.get(1).getConfidence().getConfidenceType()); + assertTrue(!wallet.pending.containsKey(txns.get(1).getHash())); + assertTrue(wallet.unspent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.spent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.dead.containsKey(txns.get(1).getHash())); + + // Fork like this: + // + // genesis -> b1 -> b2 -> b3 + // \-> b4 -> b5 -> b6 + // + // The b4/ b5/ b6 is now the best chain + Block b4 = b1.createNextBlock(someOtherGuy); + Block b5 = b4.createNextBlock(someOtherGuy); + Block b6 = b5.createNextBlock(someOtherGuy); + + log.debug("Adding block b4"); + assertTrue(chain.add(b4)); + log.debug("Adding block b5"); + assertTrue(chain.add(b5)); + log.debug("Adding block b6"); + assertTrue(chain.add(b6)); + + // Transaction 1 (in block b2) is now on a side chain and should have confidence type of dead and be in the dead pool only + assertEquals(TransactionConfidence.ConfidenceType.DEAD, txns.get(1).getConfidence().getConfidenceType()); + assertTrue(!wallet.pending.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.spent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash())); + assertTrue(wallet.dead.containsKey(txns.get(1).getHash())); + + // ... and back to the first chain. + Block b7 = b3.createNextBlock(coinsTo); + Block b8 = b7.createNextBlock(coinsTo); + + log.debug("Adding block b7"); + assertTrue(chain.add(b7)); + log.debug("Adding block b8"); + assertTrue(chain.add(b8)); + + // + // genesis -> b1 -> b2 -> b3 -> b7 -> b8 + // \-> b4 -> b5 -> b6 + // + + // The coinbase transaction should now have confidence type of building once more and in the unspent pool only. + assertEquals(TransactionConfidence.ConfidenceType.BUILDING, txns.get(1).getConfidence().getConfidenceType()); + assertTrue(!wallet.pending.containsKey(txns.get(1).getHash())); + assertTrue(wallet.unspent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.spent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.dead.containsKey(txns.get(1).getHash())); + + // ... make the side chain dominant again. + Block b9 = b6.createNextBlock(coinsTo); + Block b10 = b9.createNextBlock(coinsTo); + + log.debug("Adding block b9"); + assertTrue(chain.add(b9)); + log.debug("Adding block b10"); + assertTrue(chain.add(b10)); + // + // genesis -> b1 -> b2 -> b3 -> b7 -> b8 + // \-> b4 -> b5 -> b6 -> b9 -> b10 + // + + // The coinbase transaction should now have the confidence type of dead and be in the dead pool only. + assertEquals(TransactionConfidence.ConfidenceType.DEAD, txns.get(1).getConfidence().getConfidenceType()); + assertTrue(!wallet.pending.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.unspent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.spent.containsKey(txns.get(1).getHash())); + assertTrue(!wallet.inactive.containsKey(txns.get(1).getHash())); + assertTrue(wallet.dead.containsKey(txns.get(1).getHash())); + } } diff --git a/core/src/test/java/com/google/bitcoin/core/CoinbaseBlockTest.java b/core/src/test/java/com/google/bitcoin/core/CoinbaseBlockTest.java new file mode 100644 index 000000000..3432f6512 --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/core/CoinbaseBlockTest.java @@ -0,0 +1,102 @@ +/** + * Copyright 2012 Google Inc. + * + * 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 com.google.bitcoin.core; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.util.List; + +import org.junit.Test; + +import com.google.bitcoin.core.BlockChain.NewBlockType; +import com.google.bitcoin.core.Wallet.BalanceType; + +/** + * Test that an example production coinbase transactions can be added to a wallet ok. + */ +public class CoinbaseBlockTest { + static final NetworkParameters params = NetworkParameters.prodNet(); + + // The address for this private key is 1GqtGtn4fctXuKxsVzRPSLmYWN1YioLi9y. + private static final String MINING_PRIVATE_KEY = "5JDxPrBRghF1EvSBjDigywqfmAjpHPmTJxYtQTYJxJRHLLQA4mG"; + + private static final int BLOCK_OF_INTEREST = 169482; + private static final int BLOCK_LENGTH_AS_HEX = 37357; + private static final long BLOCK_NONCE = 3973947400L; + private static final BigInteger BALANCE_AFTER_BLOCK = BigInteger.valueOf(22223642); + + @Test + public void testReceiveCoinbaseTransaction() throws Exception { + // Block 169482 (hash 0000000000000756935f1ee9d5987857b604046f846d3df56d024cdb5f368665) + // contains coinbase transactions that are mining pool shares. + // The private key MINERS_KEY is used to check transactions are received by a wallet correctly. + + byte[] blockAsBytes = getBytes(getClass().getResourceAsStream("block169482.dat")); + + // Create block 169482. + Block block = new Block(params, blockAsBytes); + + // Check block. + assertNotNull(block); + block.verify(); + assertEquals(BLOCK_NONCE, block.getNonce()); + + StoredBlock storedBlock = new StoredBlock(block, BigInteger.ONE, BLOCK_OF_INTEREST); // Nonsense work - not used in test. + + // Create a wallet contain the miner's key that receives a spend from a coinbase. + ECKey miningKey = (new DumpedPrivateKey(params, MINING_PRIVATE_KEY)).getKey(); + assertNotNull(miningKey); + + Wallet wallet = new Wallet(params); + wallet.addKey(miningKey); + + // Initial balance should be zero by construction. + assertEquals(BigInteger.ZERO, wallet.getBalance()); + + // Give the wallet the first transaction in the block - this is the coinbase tx. + List transactions = block.getTransactions(); + assertNotNull(transactions); + wallet.receiveFromBlock(transactions.get(0), storedBlock, NewBlockType.BEST_CHAIN); + + // Coinbase transaction should have been received successfully but be unavailable to spend (too young). + assertEquals(BALANCE_AFTER_BLOCK, wallet.getBalance(BalanceType.ESTIMATED)); + assertEquals(BigInteger.ZERO, wallet.getBalance(BalanceType.AVAILABLE)); + } + + /** + * Returns the contents of the InputStream as a byte array. + */ + private byte[] getBytes(InputStream inputStream) throws IOException { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int numberRead; + byte[] data = new byte[BLOCK_LENGTH_AS_HEX]; + + while ((numberRead = inputStream.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, numberRead); + } + + buffer.flush(); + + return buffer.toByteArray(); + } +} diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index c8771045b..4aed520c0 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -16,6 +16,7 @@ package com.google.bitcoin.core; +import com.google.bitcoin.core.Wallet.BalanceType; import com.google.bitcoin.core.WalletTransaction.Pool; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.MemoryBlockStore; @@ -734,5 +735,7 @@ public class WalletTest { System.out.println(t2); } + // There is a test for spending a coinbase transaction as it matures in BlockChainTest#coinbaseTransactionAvailability + // Support for offline spending is tested in PeerGroupTest } diff --git a/core/src/test/resources/com/google/bitcoin/core/block169482.dat b/core/src/test/resources/com/google/bitcoin/core/block169482.dat new file mode 100644 index 0000000000000000000000000000000000000000..9821206ea71d0d5957524ecae7640c309dd53587 GIT binary patch literal 18678 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