mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-01-19 05:33:44 +01:00
coinbase phase 3 - coinbase death and resurrection now working (updated with Mike's comments)
This commit is contained in:
parent
6a2139f681
commit
4d1e6259a7
@ -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
|
||||
*
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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);
|
||||
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
BIN
core/src/test/resources/com/google/bitcoin/core/block169482.dat
Normal file
BIN
core/src/test/resources/com/google/bitcoin/core/block169482.dat
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user