coinbase phase 3 - coinbase death and resurrection now working (updated with Mike's comments)

This commit is contained in:
Mike Hearn 2012-05-30 13:58:11 +02:00 committed by Jim Burton
parent 6a2139f681
commit 4d1e6259a7
10 changed files with 440 additions and 61 deletions

View File

@ -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
*

View File

@ -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<Wallet, List<Transaction>> 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<Transaction> txList = walletToTxMap.get(wallet);
if (txList == null) {
txList = new LinkedList<Transaction>();
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.
*/

View File

@ -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;
}
}

View File

@ -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.
*/

View File

@ -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<Sha256Hash, Transaction> inactive;
Map<Sha256Hash, Transaction> 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<Sha256Hash, Transaction> dead;
Map<Sha256Hash, Transaction> 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<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
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<Sha256Hash> 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<Sha256Hash, Transaction> 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<Transaction> connectedTransactions = new TreeSet<Transaction>();
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.
*/

View File

@ -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);

View File

@ -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<Transaction> txns = new ArrayList<Transaction>(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()));
}
}

View File

@ -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<Transaction> 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();
}
}

View File

@ -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
}