Another rewrite of the re-org handling:

- Split the unit tests for this into a separate file
- Add more tests for double spends, reversal of external spends and more edge cases
- Handle the case where transactions should be resurrected after a re-org
- Handle the case where transactions are overridden by double spends

Should address [some of] the points Miron raised during his review. There are likely still bugs but it's a lot closer to correct than before.
This commit is contained in:
Mike Hearn 2011-05-17 15:22:28 +00:00
parent eee6e03416
commit 32436ddc7f
11 changed files with 772 additions and 275 deletions

View file

@ -457,7 +457,7 @@ public class Block extends Message {
// Real coinbase transactions use <pubkey> OP_CHECKSIG rather than a send to an address though there's
// nothing in the system that enforces that and both are just as valid.
coinbase.inputs.add(new TransactionInput(params, new byte[] { (byte) coinbaseCounter++ } ));
coinbase.outputs.add(new TransactionOutput(params, Utils.toNanoCoins(50, 0), to, coinbase));
coinbase.outputs.add(new TransactionOutput(params, coinbase, Utils.toNanoCoins(50, 0), to));
transactions.add(coinbase);
}

View file

@ -195,8 +195,8 @@ public class BlockChain {
log.info("New chain head: {}", newChainHead.getHeader().getHashAsString());
log.info("Split at block: {}", splitPoint.getHeader().getHashAsString());
// Then build a list of all blocks in the old part of the chain and the new part.
Set<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
Set<StoredBlock> newBlocks = getPartialChain(newChainHead, splitPoint);
List<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
List<StoredBlock> newBlocks = getPartialChain(newChainHead, splitPoint);
// Now inform the wallet. This is necessary so the set of currently active transactions (that we can spend)
// can be updated to take into account the re-organize. We might also have received new coins we didn't have
// before and our previous spends might have been undone.
@ -208,9 +208,9 @@ public class BlockChain {
/**
* Returns the set of contiguous blocks between 'higher' and 'lower'. Higher is included, lower is not.
*/
private Set<StoredBlock> getPartialChain(StoredBlock higher, StoredBlock lower) throws BlockStoreException {
private List<StoredBlock> getPartialChain(StoredBlock higher, StoredBlock lower) throws BlockStoreException {
assert higher.getHeight() > lower.getHeight();
Set<StoredBlock> results = new HashSet<StoredBlock>();
LinkedList<StoredBlock> results = new LinkedList<StoredBlock>();
StoredBlock cursor = higher;
while (true) {
results.add(cursor);

View file

@ -117,7 +117,7 @@ public class Transaction extends Message implements Serializable {
BigInteger v = BigInteger.ZERO;
for (TransactionOutput o : outputs) {
if (!o.isMine(wallet)) continue;
if (!includeSpent && o.isSpent) continue;
if (!includeSpent && !o.isAvailableForSpending()) continue;
v = v.add(o.getValue());
}
return v;
@ -161,17 +161,44 @@ public class Transaction extends Message implements Serializable {
// This is tested in WalletTest.
BigInteger v = BigInteger.ZERO;
for (TransactionInput input : inputs) {
boolean connected = input.outpoint.connect(wallet.unspent.values()) ||
input.outpoint.connect(wallet.spent.values());
if (connected) {
// This input is taking value from an transaction in our wallet. To discover the value,
// we must find the connected transaction.
v = v.add(input.outpoint.getConnectedOutput().getValue());
}
TransactionOutput connected = input.getConnectedOutput(wallet.unspent);
if (connected == null)
connected = input.getConnectedOutput(wallet.spent);
if (connected == null)
connected = input.getConnectedOutput(wallet.pending);
if (connected == null)
continue;
v = v.add(connected.getValue());
}
return v;
}
boolean disconnectInputs() {
boolean disconnected = false;
for (TransactionInput input : inputs) {
disconnected |= input.disconnect();
}
return disconnected;
}
/**
* Connects all inputs using the provided transactions. If any input cannot be connected returns that input or
* null on success.
*/
TransactionInput connectInputs(Map<Sha256Hash, Transaction> transactions, boolean disconnect) {
for (TransactionInput input : inputs) {
// Coinbase transactions, by definition, do not have connectable inputs.
if (input.isCoinBase()) continue;
if (input.connect(transactions, disconnect) != TransactionInput.ConnectionResult.SUCCESS) {
// Could not connect this input, so return it and abort.
return input;
}
}
return null;
}
/**
* These constants are a part of a scriptSig signature on the inputs. They define the details of how a
* transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.
@ -225,6 +252,9 @@ public class Transaction extends Message implements Serializable {
*/
public String toString() {
StringBuffer s = new StringBuffer();
s.append(" ");
s.append(getHashAsString());
s.append("\n");
if (isCoinBase()) {
String script = "???";
String script2 = "???";
@ -323,7 +353,6 @@ public class Transaction extends Message implements Serializable {
// The anyoneCanPay feature isn't used at the moment.
boolean anyoneCanPay = false;
byte[] hash = hashTransactionForSignature(hashType, anyoneCanPay);
log.info(" signInputs hash={}", Utils.bytesToHexString(hash));
// Set the script to empty again for the next input.
input.scriptBytes = TransactionInput.EMPTY_ARRAY;

View file

@ -19,6 +19,7 @@ package com.google.bitcoin.core;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Map;
/**
* A transfer of coins from one address to another creates a transaction in which the outputs
@ -27,11 +28,14 @@ import java.io.Serializable;
* to the outputs of another. The exceptions are coinbase transactions, which create new coins.
*/
public class TransactionInput extends Message implements Serializable {
private static final long serialVersionUID = -7687665228438202968L;
// An apparently unused field intended for altering transactions after they were broadcast.
long sequence;
// The output of the transaction we're gathering coins from.
private static final long serialVersionUID = 2;
public static final byte[] EMPTY_ARRAY = new byte[0];
// Allows for altering transactions after they were broadcast. Tx replacement is currently disabled in the C++
// client so this is always the UINT_MAX.
// TODO: Document this in more detail and build features that use it.
long sequence;
// Data needed to connect to the output of the transaction we're gathering coins from.
TransactionOutPoint outpoint;
// The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there
// is no input transaction, so instead the scriptBytes contains some extra stuff (like a rollover nonce) that we
@ -41,8 +45,6 @@ public class TransactionInput extends Message implements Serializable {
// coinbase.
transient private Script scriptSig;
static public final byte[] EMPTY_ARRAY = new byte[0];
/** Used only in creation of the genesis block. */
TransactionInput(NetworkParameters params, byte[] scriptBytes) {
super(params);
@ -124,4 +126,62 @@ public class TransactionInput extends Message implements Serializable {
throw new RuntimeException(e);
}
}
enum ConnectionResult {
NO_SUCH_TX,
ALREADY_SPENT,
SUCCESS
}
// TODO: Clean all this up once TransactionOutPoint disappears.
/**
* Locates the referenced output from the given pool of transactions.
* @return The TransactionOutput or null if the transactions map doesn't contain the referenced tx.
*/
TransactionOutput getConnectedOutput(Map<Sha256Hash, Transaction> transactions) {
Sha256Hash h = new Sha256Hash(outpoint.hash);
Transaction tx = transactions.get(h);
if (tx == null)
return null;
TransactionOutput out = tx.outputs.get((int)outpoint.index);
return out;
}
/**
* Connects this input to the relevant output of the referenced transaction if it's in the given map.
* Connecting means updating the internal pointers and spent flags.
*
* @param transactions Map of txhash->transaction.
* @param disconnect Whether to abort if there's a pre-existing connection or not.
* @return true if connection took place, false if the referenced transaction was not in the list.
*/
ConnectionResult connect(Map<Sha256Hash, Transaction> transactions, boolean disconnect) {
Sha256Hash h = new Sha256Hash(outpoint.hash);
Transaction tx = transactions.get(h);
if (tx == null)
return TransactionInput.ConnectionResult.NO_SUCH_TX;
TransactionOutput out = tx.outputs.get((int)outpoint.index);
if (!out.isAvailableForSpending()) {
if (disconnect)
out.markAsUnspent();
else
return TransactionInput.ConnectionResult.ALREADY_SPENT;
}
outpoint.fromTx = tx;
out.markAsSpent(this);
return TransactionInput.ConnectionResult.SUCCESS;
}
/**
* Release the connected output, making it spendable once again.
*
* @return true if the disconnection took place, false if it was not connected.
*/
boolean disconnect() {
if (outpoint.fromTx == null) return false;
outpoint.fromTx.outputs.get((int)outpoint.index).markAsUnspent();
outpoint.fromTx = null;
return true;
}
}

View file

@ -19,8 +19,8 @@ package com.google.bitcoin.core;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
// TODO: Fold this class into the TransactionInput class. It's not necessary.
/**
* This message is a reference or pointer to an output of a different transaction.
@ -33,7 +33,8 @@ public class TransactionOutPoint extends Message implements Serializable {
/** Which output of that transaction we are talking about. */
long index;
// This is not part of bitcoin serialization.
// This is not part of Bitcoin serialization. It's included in Java serialization.
// It points to the connected transaction.
Transaction fromTx;
TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) {
@ -66,27 +67,12 @@ public class TransactionOutPoint extends Message implements Serializable {
Utils.uint32ToByteStreamLE(index, stream);
}
/**
* Scans the list for the transaction this outpoint refers to, and sets up the internal reference used by
* getConnectedOutput().
* @return true if connection took place, false if the referenced transaction was not in the list.
*/
boolean connect(Collection<Transaction> transactions) {
for (Transaction tx : transactions) {
if (Arrays.equals(tx.getHash().hash, hash)) {
fromTx = tx;
return true;
}
}
return false;
}
/**
* If this transaction was created using the explicit constructor rather than deserialized,
* retrieves the connected output transaction. Asserts if there is no connected transaction.
*/
TransactionOutput getConnectedOutput() {
assert fromTx != null;
if (fromTx == null) return null;
return fromTx.outputs.get((int)index);
}

View file

@ -40,10 +40,12 @@ public class TransactionOutput extends Message implements Serializable {
// The script bytes are parsed and turned into a Script on demand.
private transient Script scriptPubKey;
// This field is Java serialized but not BitCoin serialized. It's used for tracking purposes in our wallet only.
// If this flag is set to true, it means we have spent this outputs value and it shouldn't be used again or
// counted towards our balance.
boolean isSpent;
// These fields are Java serialized but not BitCoin serialized. They are used for tracking purposes in our wallet
// only. If set to true, this output is counted towards our balance. If false and spentBy is null the tx output
// was owned by us and was sent to somebody else. If false and spentBy is true it means this output was owned by
// us and used in one of our own transactions (eg, because it is a change output).
private boolean availableForSpending;
private TransactionInput spentBy;
// A reference to the transaction which holds this output.
Transaction parentTransaction;
@ -53,13 +55,15 @@ public class TransactionOutput extends Message implements Serializable {
int offset) throws ProtocolException {
super(params, payload, offset);
parentTransaction = parent;
availableForSpending = true;
}
TransactionOutput(NetworkParameters params, BigInteger value, Address to, Transaction parent) {
TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, Address to) {
super(params);
this.value = value;
this.scriptBytes = Script.createOutputScript(to);
parentTransaction = parent;
availableForSpending = true;
}
/** Used only in creation of the genesis blocks and in unit tests. */
@ -67,6 +71,7 @@ public class TransactionOutput extends Message implements Serializable {
super(params);
this.scriptBytes = scriptBytes;
this.value = Utils.toNanoCoins(50, 0);
availableForSpending = true;
}
public Script getScriptPubKey() throws ScriptException {
@ -108,6 +113,25 @@ public class TransactionOutput extends Message implements Serializable {
throw new RuntimeException("Output linked to wrong parent transaction?");
}
/**
* Sets this objects availableToSpend flag to false and the spentBy pointer to the given input.
* If the input is null, it means this output was signed over to somebody else rather than one of our own keys.
*/
void markAsSpent(TransactionInput input) {
assert availableForSpending;
availableForSpending = false;
spentBy = input;
}
void markAsUnspent() {
availableForSpending = true;
spentBy = null;
}
boolean isAvailableForSpending() {
return availableForSpending;
}
public byte[] getScriptBytes() {
return scriptBytes;
}

View file

@ -27,12 +27,11 @@ import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
/**
* A Wallet stores keys and a record of transactions that have not yet been spent. Thus, it is capable of
* providing transactions on demand that meet a given combined value. Once a transaction
* output is used, it is removed from the wallet as it is no longer available for spending.<p>
* providing transactions on demand that meet a given combined value.<p>
*
* The Wallet is read and written from disk, so be sure to follow the Java serialization
* versioning rules here. We use the built in Java serialization to avoid the need to
* pull in a potentially large (code-size) third party serialization library.<p>
* The Wallet is read and written from disk, so be sure to follow the Java serialization versioning rules here. We
* use the built in Java serialization to avoid the need to pull in a potentially large (code-size) third party
* serialization library.<p>
*/
public class Wallet implements Serializable {
private static final Logger log = LoggerFactory.getLogger(Wallet.class);
@ -54,15 +53,15 @@ public class Wallet implements Serializable {
// 5. Inbound tx is accepted into a side chain:
// ->inactive
//
// Whilst it's also 'pending' in some sense, in that miners will probably try and incorporate it into the
// best chain, we don't mark it as such here. It'll eventually show up after a re-org.
//
// Re-orgs:
// 1. Tx is present in old chain and not present in new chain
// <-unspent/spent ->inactive
// <-unspent/spent ->pending
//
// These newly inactive transactions will (if they are relevant to us) eventually come back via receive()
// as miners resurrect them and re-include into the new best chain. Until then we do NOT consider them
// pending as it's possible some of the transactions have become invalid (eg because the new chain contains
// a double spend). This could cause some confusing UI changes for the user but these events should be very
// rare.
// as miners resurrect them and re-include into the new best chain.
//
// 2. Tx is not present in old chain and is present in new chain
// <-inactive and ->unspent/spent
@ -74,7 +73,8 @@ public class Wallet implements Serializable {
// change outputs would not be considered spendable.
/**
* Map of txhash->Transactions that have not made it into the best chain yet. These transactions inputs count as
* Map of txhash->Transactions that have not made it into the best chain yet. They are eligible to move there but
* are waiting for a miner to send a block on the best chain including them. These transactions inputs count as
* spent for the purposes of calculating our balance but their outputs are not available for spending yet. This
* means after a spend, our balance can actually go down temporarily before going up again!
*/
@ -114,6 +114,14 @@ public class Wallet implements Serializable {
*/
private Map<Sha256Hash, Transaction> inactive;
/**
* A dead transaction is one that's been overridden by a double spend. Such a transaction is pending except it
* will never confirm and so should be presented to the user in some unique way - flashing red for example. This
* 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;
/** A list of public/private EC keys owned by this user. */
public final ArrayList<ECKey> keychain;
@ -132,6 +140,7 @@ public class Wallet implements Serializable {
spent = new HashMap<Sha256Hash, Transaction>();
inactive = new HashMap<Sha256Hash, Transaction>();
pending = new HashMap<Sha256Hash, Transaction>();
dead = new HashMap<Sha256Hash, Transaction>();
eventListeners = new ArrayList<WalletEventListener>();
}
@ -194,6 +203,11 @@ public class Wallet implements Serializable {
* block might change which chain is best causing a reorganize. A re-org can totally change our balance!
*/
synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException {
receive(tx, block, blockType, false);
}
private synchronized void receive(Transaction tx, StoredBlock block,
BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException, ScriptException {
// Runs in a peer thread.
BigInteger prevBalance = getBalance();
@ -206,8 +220,10 @@ public class Wallet implements Serializable {
BigInteger valueSentToMe = tx.getValueSentToMe(this);
BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe);
log.info("Wallet: Received tx" + (sideChain ? " on a side chain" :"") + " for " +
bitcoinValueToFriendlyString(valueDifference) + " BTC");
if (!reorg) {
log.info("Received tx{} for {} BTC: {}", new Object[] { sideChain ? " on a side chain" : "",
bitcoinValueToFriendlyString(valueDifference), tx.getHashAsString()});
}
// If this transaction is already in the wallet we may need to move it into a different pool. At the very
// least we need to ensure we're manipulating the canonical object rather than a duplicate.
@ -248,8 +264,10 @@ public class Wallet implements Serializable {
pending.put(wtx.getHash(), wtx);
}
} else {
if (!reorg) {
// Mark the tx as appearing in this block so we can find it later after a re-org.
tx.addBlockAppearance(block);
}
// This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys
// are being shared between different wallets.
if (sideChain) {
@ -265,7 +283,7 @@ public class Wallet implements Serializable {
// Inform anyone interested that we have new coins. Note: we may be re-entered by the event listener,
// so we must not make assumptions about our state after this loop returns! For example,
// the balance we just received might already be spent!
if (bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) {
if (!reorg && bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) {
for (WalletEventListener l : eventListeners) {
synchronized (l) {
l.onCoinsReceived(this, tx, prevBalance, getBalance());
@ -284,37 +302,58 @@ public class Wallet implements Serializable {
updateForSpends(tx);
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
// It's sending us coins.
log.info(" ->unspent");
log.info(" new tx ->unspent");
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "TX was received twice";
} else {
// It spent some of our coins and did not send us any.
log.info(" ->spent");
log.info(" new tx ->spent");
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "TX was received twice";
}
}
/**
* Updates the wallet by checking if this TX spends any of our unspent outputs. This is not used normally because
* Updates the wallet by checking if this TX spends any of our outputs. This is not used normally because
* when we receive our own spends, we've already marked the outputs as spent previously (during tx creation) so
* there's no need to go through and do it again.
*/
private void updateForSpends(Transaction tx) throws VerificationException {
for (TransactionInput input : tx.inputs) {
if (input.outpoint.connect(unspent.values())) {
TransactionOutput output = input.outpoint.getConnectedOutput();
assert !output.isSpent : "Double spend accepted by the network?";
log.info(" Saw some of my unspent outputs be spent by someone else who has my keys.");
log.info(" Total spent value is " + bitcoinValueToFriendlyString(output.getValue()));
output.isSpent = true;
Transaction connectedTx = input.outpoint.fromTx;
if (connectedTx.getValueSentToMe(this, false).equals(BigInteger.ZERO)) {
TransactionInput.ConnectionResult result = input.connect(unspent, false);
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
// Doesn't spend any of our outputs or is coinbase.
continue;
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
// Double spend! This must have overridden a pending tx, or the block is bad (contains transactions
// that illegally double spend: should never occur if we are connected to an honest node).
Transaction connected = input.outpoint.fromTx;
if (pending.containsKey(connected.getHash())) {
log.info("Saw double spend from chain override pending tx {}", connected.getHashAsString());
log.info(" <-pending ->dead");
pending.remove(connected.getHash());
dead.put(connected.getHash(), connected);
// Now forcibly change the connection.
input.connect(unspent, true);
// Inform the event listeners of the newly dead tx.
for (WalletEventListener listener : eventListeners) {
synchronized (listener) {
listener.onDeadTransaction(connected, tx);
}
}
}
} else if (result == TransactionInput.ConnectionResult.SUCCESS) {
// Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet.
// The outputs are already marked as spent by the connect call above, so check if there are any more for
// us to use. Move if not.
Transaction connected = input.outpoint.fromTx;
if (connected.getValueSentToMe(this, false).equals(BigInteger.ZERO)) {
// There's nothing left I can spend in this transaction.
if (unspent.remove(connectedTx.getHash()) != null);
if (unspent.remove(connected.getHash()) != null) {
log.info(" prevtx <-unspent");
spent.put(connectedTx.getHash(), connectedTx);
log.info(" prevtx ->spent");
spent.put(connected.getHash(), connected);
}
}
}
}
@ -337,11 +376,11 @@ public class Wallet implements Serializable {
*/
synchronized void confirmSend(Transaction tx) {
assert !pending.containsKey(tx) : "confirmSend called on the same transaction twice";
// Mark each connected output of the tx as spent, so we don't try and spend it again.
log.info("confirmSend of {}", tx.getHashAsString());
// Mark the outputs of the used transcations as spent, so we don't try and spend it again.
for (TransactionInput input : tx.inputs) {
TransactionOutput connectedOutput = input.outpoint.getConnectedOutput();
assert !connectedOutput.isSpent : "createSend called before corresponding confirmSend";
connectedOutput.isSpent = true;
connectedOutput.markAsSpent(input);
}
// Some of the outputs probably send coins back to us, eg for change or because this transaction is just
// consolidating the wallet. Mark any output that is NOT back to us as spent. Then add this TX to the
@ -349,8 +388,7 @@ public class Wallet implements Serializable {
for (TransactionOutput output : tx.outputs) {
if (!output.isMine(this)) {
// This output didn't go to us, so by definition it is now spent.
assert !output.isSpent;
output.isSpent = true;
output.markAsSpent(null);
}
}
pending.put(tx.getHash(), tx);
@ -412,7 +450,7 @@ public class Wallet implements Serializable {
List<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
for (Transaction tx : unspent.values()) {
for (TransactionOutput output : tx.outputs) {
if (output.isSpent) continue;
if (!output.isAvailableForSpending()) continue;
if (!output.isMine(this)) continue;
gathered.add(output);
valueGathered = valueGathered.add(output.getValue());
@ -428,14 +466,14 @@ public class Wallet implements Serializable {
}
assert gathered.size() > 0;
Transaction sendTx = new Transaction(params);
sendTx.addOutput(new TransactionOutput(params, nanocoins, address, sendTx));
sendTx.addOutput(new TransactionOutput(params, sendTx, nanocoins, address));
BigInteger change = valueGathered.subtract(nanocoins);
if (change.compareTo(BigInteger.ZERO) > 0) {
// The value of the inputs is greater than what we want to send. Just like in real life then,
// we need to take back some coins ... this is called "change". Add another output that sends the change
// back to us.
log.info(" with " + bitcoinValueToFriendlyString(change) + " coins change");
sendTx.addOutput(new TransactionOutput(params, change, changeAddress, sendTx));
sendTx.addOutput(new TransactionOutput(params, sendTx, change, changeAddress));
}
for (TransactionOutput output : gathered) {
sendTx.addInput(output);
@ -449,6 +487,7 @@ public class Wallet implements Serializable {
// happen, if it does it means the wallet has got into an inconsistent state.
throw new RuntimeException(e);
}
log.info(" created {}", sendTx.getHashAsString());
return sendTx;
}
@ -494,18 +533,64 @@ public class Wallet implements Serializable {
}
/**
* Returns the balance of this wallet by summing up all unspent outputs that were sent to us.
* It's possible to calculate a wallets balance from multiple points of view. This enum selects which
* getBalance() should use.<p>
*
* Consider a real-world example: you buy a snack costing $5 but you only have a $10 bill. At the start you have
* $10 viewed from every possible angle. After you order the snack you hand over your $10 bill. From the
* perspective of your wallet you have zero dollars (AVAILABLE). But you know in a few seconds the shopkeeper
* will give you back $5 change so most people in practice would say they have $5 (ESTIMATED).<p>
*/
public enum BalanceType {
/**
* Balance calculated assuming all pending transactions are in fact included into the best chain by miners.
* This is the right balance to show in user interfaces.
*/
ESTIMATED,
/**
* Balance that can be safely used to create new spends. This is all confirmed unspent outputs minus the ones
* spent by pending transactions, but not including the outputs of those pending transactions.
*/
AVAILABLE
};
/**
* Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this
* means.<p>
*
* Note: the estimated balance is usually the one you want to show to the end user - however attempting to
* actually spend these coins may result in temporary failure. This method returns how much you can safely
* provide to {@link Wallet#createSend(Address, java.math.BigInteger)}.
*/
public synchronized BigInteger getBalance() {
BigInteger balance = BigInteger.ZERO;
return getBalance(BalanceType.AVAILABLE);
}
/**
* Returns the balance of this wallet as calculated by the provided balanceType.
*/
public synchronized BigInteger getBalance(BalanceType balanceType) {
BigInteger available = BigInteger.ZERO;
for (Transaction tx : unspent.values()) {
for (TransactionOutput output : tx.outputs) {
if (output.isSpent) continue;
if (!output.isMine(this)) continue;
balance = balance.add(output.getValue());
if (!output.isAvailableForSpending()) continue;
available = available.add(output.getValue());
}
}
return balance;
if (balanceType == BalanceType.AVAILABLE)
return available;
assert balanceType == BalanceType.ESTIMATED;
// Now add back all the pending outputs to assume the transaction goes through.
BigInteger estimated = available;
for (Transaction tx : pending.values()) {
for (TransactionOutput output : tx.outputs) {
if (!output.isMine(this)) continue;
estimated = estimated.add(output.getValue());
}
}
return estimated;
}
@Override
@ -534,67 +619,162 @@ public class Wallet implements Serializable {
* Called by the {@link BlockChain} when the best chain (representing total work done) has changed. In this case,
* we need to go through our transactions and find out if any have become invalid. It's possible for our balance
* to go down in this case: money we thought we had can suddenly vanish if the rest of the network agrees it
* should be so.
* should be so.<p>
*
* The oldBlocks/newBlocks lists are ordered height-wise from top first to bottom last.
*/
synchronized void reorganize(Set<StoredBlock> oldBlocks, Set<StoredBlock> newBlocks) throws VerificationException {
synchronized void reorganize(List<StoredBlock> oldBlocks, List<StoredBlock> newBlocks) throws VerificationException {
// This runs on any peer thread with the block chain synchronized.
//
// The reorganize functionality of the wallet is tested in the BlockChainTest.testForking* methods.
// The reorganize functionality of the wallet is tested in ChainSplitTests.
//
// For each transaction we track which blocks they appeared in. Once a re-org takes place we have to find all
// transactions in the old branch, all transactions in the new branch and find the difference of those sets.
//
// receive() has been called on the block that is triggering the re-org before this is called.
Set<Transaction> oldChainTransactions = new HashSet<Transaction>();
Set<Transaction> newChainTransactions = new HashSet<Transaction>();
Set<Transaction> all = new HashSet<Transaction>();
all.addAll(unspent.values());
all.addAll(spent.values());
all.addAll(inactive.values());
for (Transaction tx : all) {
log.info(" Old part of chain (top to bottom):");
for (StoredBlock b : oldBlocks) log.info(" {}", b.getHeader().getHashAsString());
log.info(" New part of chain (top to bottom):");
for (StoredBlock b : newBlocks) log.info(" {}", b.getHeader().getHashAsString());
// Transactions that appear in the old chain segment.
Map<Sha256Hash, Transaction> oldChainTransactions = new HashMap<Sha256Hash, Transaction>();
// Transactions that appear in the old chain segment and NOT the new chain segment.
Map<Sha256Hash, Transaction> onlyOldChainTransactions = new HashMap<Sha256Hash, Transaction>();
// Transactions that appear in the new chain segment.
Map<Sha256Hash, Transaction> newChainTransactions = new HashMap<Sha256Hash, Transaction>();
// Transactions that don't appear in either the new or the old section, ie, the shared trunk.
Map<Sha256Hash, Transaction> commonChainTransactions = new HashMap<Sha256Hash, Transaction>();
Map<Sha256Hash, Transaction> all = new HashMap<Sha256Hash, Transaction>();
all.putAll(unspent);
all.putAll(spent);
all.putAll(inactive);
for (Transaction tx : all.values()) {
Set<StoredBlock> appearsIn = tx.getAppearsIn();
assert appearsIn != null;
// If the set of blocks this transaction appears in is disjoint with one of the chain segments it means
// the transaction was never incorporated by a miner into that side of the chain.
if (!Collections.disjoint(appearsIn, oldBlocks)) {
boolean alreadyPresent = !oldChainTransactions.add(tx);
assert !alreadyPresent : "Transaction appears twice in chain segment";
boolean inOldSection = !Collections.disjoint(appearsIn, oldBlocks);
boolean inNewSection = !Collections.disjoint(appearsIn, newBlocks);
boolean inCommonSection = !inNewSection && !inOldSection;
if (inCommonSection) {
boolean alreadyPresent = commonChainTransactions.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "Transaction appears twice in common chain segment";
} else {
if (inOldSection) {
boolean alreadyPresent = oldChainTransactions.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "Transaction appears twice in old chain segment";
if (!inNewSection) {
alreadyPresent = onlyOldChainTransactions.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "Transaction appears twice in only-old map";
}
}
if (inNewSection) {
boolean alreadyPresent = newChainTransactions.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "Transaction appears twice in new chain segment";
}
if (!Collections.disjoint(appearsIn, newBlocks)) {
boolean alreadyPresent = !newChainTransactions.add(tx);
assert !alreadyPresent : "Transaction appears twice in chain segment";
}
}
// If there is no difference it means we the user doesn't really care about this re-org but we still need to
// update the transaction block pointers for next time.
// If there is no difference it means we have nothing we need to do and the user does not care.
boolean affectedUs = !oldChainTransactions.equals(newChainTransactions);
log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions");
if (!affectedUs) return;
// Transactions that were in the old chain but aren't in the new chain. These will become inactive.
Set<Transaction> gone = new HashSet<Transaction>(oldChainTransactions);
gone.removeAll(newChainTransactions);
// Transactions that are in the new chain but aren't in the old chain. These will be re-processed.
Set<Transaction> fresh = new HashSet<Transaction>(newChainTransactions);
fresh.removeAll(oldChainTransactions);
assert !(gone.isEmpty() && fresh.isEmpty()) : "There must have been some changes to get here";
// For simplicity we will reprocess every transaction to ensure it's in the right bucket and has the right
// connections. Attempting to update each one with minimal work is possible but complex and was leading to
// edge cases that were hard to fix. As re-orgs are rare the amount of work this implies should be manageable
// unless the user has an enormous wallet. As an optimization fully spent transactions buried deeper than
// 1000 blocks could be put into yet another bucket which we never touch and assume re-orgs cannot affect.
for (Transaction tx : gone) {
log.info("tx not in new chain: <-unspent/spent ->inactive\n" + tx.toString());
unspent.remove(tx.getHash());
spent.remove(tx.getHash());
inactive.put(tx.getHash(), tx);
// We do not put it into the pending pool. Pending is for transactions we know are valid. After a re-org
// some transactions may become permanently invalid if the new chain contains a double spend. We don't
// want transactions sitting in the pending pool forever. This means shortly after a re-org the balance
// might change rapidly as newly transactions are resurrected and included into the new chain by miners.
for (Transaction tx : onlyOldChainTransactions.values()) log.info(" Only Old: {}", tx.getHashAsString());
for (Transaction tx : oldChainTransactions.values()) log.info(" Old: {}", tx.getHashAsString());
for (Transaction tx : newChainTransactions.values()) log.info(" New: {}", tx.getHashAsString());
// Break all the existing connections.
for (Transaction tx : all.values())
tx.disconnectInputs();
for (Transaction tx : pending.values())
tx.disconnectInputs();
// Reconnect the transactions in the common part of the chain.
for (Transaction tx : commonChainTransactions.values()) {
TransactionInput badInput = tx.connectInputs(all, false);
assert badInput == null : "Failed to connect " + tx.getHashAsString() + ", " + badInput.toString();
}
for (Transaction tx : fresh) {
inactive.remove(tx.getHash());
processTxFromBestChain(tx);
// Recalculate the unspent/spent buckets for the transactions the re-org did not affect.
unspent.clear();
spent.clear();
inactive.clear();
for (Transaction tx : commonChainTransactions.values()) {
int unspentOutputs = 0;
for (TransactionOutput output : tx.outputs) {
if (output.isAvailableForSpending()) unspentOutputs++;
}
if (unspentOutputs > 0) {
log.info(" TX {}: ->unspent", tx.getHashAsString());
unspent.put(tx.getHash(), tx);
} else {
log.info(" TX {}: ->spent", tx.getHashAsString());
spent.put(tx.getHash(), 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.
Collections.reverse(newBlocks); // Need bottom-to-top but we get top-to-bottom.
for (StoredBlock b : newBlocks) {
log.info("Replaying block {}", b.getHeader().getHashAsString());
Set<Transaction> txns = new HashSet<Transaction>();
for (Transaction tx : newChainTransactions.values()) {
if (tx.appearsIn.contains(b)) {
txns.add(tx);
log.info(" containing tx {}", tx.getHashAsString());
}
}
for (Transaction t : txns) {
try {
receive(t, b, BlockChain.NewBlockType.BEST_CHAIN, true);
} catch (ScriptException e) {
throw new RuntimeException(e); // Cannot happen as these blocks were already verified.
}
}
}
// Find the transactions that didn't make it into the new chain yet. For each input, try to connect it to the
// transactions that are in {spent,unspent,pending}. Check the status of each input. For inactive
// transactions that only send us money, we put them into the inactive pool where they sit around waiting for
// another re-org or re-inclusion into the main chain. For inactive transactions where we spent money we must
// put them back into the pending pool if we can reconnect them, so we don't create a double spend whilst the
// network heals itself.
Map<Sha256Hash, Transaction> pool = new HashMap<Sha256Hash, Transaction>();
pool.putAll(unspent);
pool.putAll(spent);
pool.putAll(pending);
Map<Sha256Hash, Transaction> toReprocess = new HashMap<Sha256Hash, Transaction>();
toReprocess.putAll(onlyOldChainTransactions);
toReprocess.putAll(pending);
log.info("Reprocessing:");
// Note, we must reprocess dead transactions first. The reason is that if there is a double spend across
// chains from our own coins we get a complicated situation:
//
// 1) We switch to a new chain (B) that contains a double spend overriding a pending transaction. It goes dead.
// 2) We switch BACK to the first chain (A). The dead transaction must go pending again.
// 3) We resurrect the transactions that were in chain (B) and assume the miners will start work on putting them
// in to the chain, but it's not possible because it's a double spend. So now that transaction must become
// dead instead of pending.
//
// This only occurs when we are double spending our own coins.
for (Transaction tx : dead.values()) {
reprocessTxAfterReorg(pool, tx);
}
for (Transaction tx : toReprocess.values()) {
reprocessTxAfterReorg(pool, tx);
}
log.info("post-reorg balance is {}", Utils.bitcoinValueToFriendlyString(getBalance()));
// Inform event listeners that a re-org took place.
for (WalletEventListener l : eventListeners) {
@ -606,6 +786,51 @@ public class Wallet implements Serializable {
}
}
private void reprocessTxAfterReorg(Map<Sha256Hash, Transaction> pool, Transaction tx) {
log.info(" TX {}", tx.getHashAsString());
int numInputs = tx.inputs.size();
int noSuchTx = 0;
int success = 0;
boolean isDead = false;
for (TransactionInput input : tx.inputs) {
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, false);
if (result == TransactionInput.ConnectionResult.SUCCESS) {
success++;
} else if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
noSuchTx++;
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
isDead = true;
// This transaction was replaced by a double spend on the new chain. Did you just reverse
// your own transaction? I hope not!!
log.info(" ->dead, will not confirm now unless there's another re-org",
tx.getHashAsString());
dead.put(tx.getHash(), tx);
// Inform the event listeners of the newly dead tx.
for (WalletEventListener listener : eventListeners) {
synchronized (listener) {
listener.onDeadTransaction(input.outpoint.fromTx, tx);
}
}
break;
}
}
if (isDead) return;
if (noSuchTx == numInputs) {
log.info(" ->inactive", tx.getHashAsString());
inactive.put(tx.getHash(), tx);
} 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());
pending.put(tx.getHash(), tx);
}
}
/**
* Returns an immutable view of the transactions currently waiting for network confirmations.
*/

View file

@ -1,7 +1,25 @@
/**
* Copyright 2011 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 java.math.BigInteger;
// TODO: Make this be an interface with a convenience abstract impl.
/**
* Implementing a subclass WalletEventListener allows you to learn when the contents of the wallet changes due to
* receiving money or a block chain re-organize. Methods are called with the event listener object locked so your
@ -36,4 +54,20 @@ public abstract class WalletEventListener {
*/
public void onReorganize() {
}
/**
* This is called on a Peer thread when a transaction becomes <i>dead</i>. A dead transaction is one that has
* been overridden by a double spend from the network and so will never confirm no matter how long you wait.<p>
*
* A dead transaction can occur if somebody is attacking the network, or by accident if keys are being shared.
* You can use this event handler to inform the user of the situation. A dead spend will show up in the BitCoin
* C++ client of the recipient as 0/unconfirmed forever, so if it was used to purchase something,
* the user needs to know their goods will never arrive.
*
* @param deadTx The transaction that is newly dead.
* @param replacementTx The transaction that killed it.
*/
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
}
}

View file

@ -24,10 +24,8 @@ import java.math.BigInteger;
import static org.junit.Assert.*;
// Tests still to write:
// - Fragmented chains can be joined together.
// - Longest testNetChain is selected based on total difficulty not length.
// - Many more ...
// NOTE: Handling of chain splits/reorgs are in ChainSplitTests.
public class BlockChainTest {
private static final NetworkParameters testNet = NetworkParameters.testNet();
private BlockChain testNetChain;
@ -88,140 +86,6 @@ public class BlockChainTest {
assertEquals(chain.getChainHead().getHeader(), b3.cloneAsHeader());
}
@Test
public void testForking1() throws Exception {
// Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions
// (receiving coins). Checking that we understand reversed spends is in testForking2.
// TODO: Change this test to not use coinbase transactions as they are special (maturity rules).
final boolean[] reorgHappened = new boolean[1];
reorgHappened[0] = false;
wallet.addEventListener(new WalletEventListener() {
@Override
public void onReorganize() {
reorgHappened[0] = true;
}
});
// Start by building a couple of blocks on top of the genesis block.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b2 = b1.createNextBlock(coinbaseTo);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
assertFalse(reorgHappened[0]);
// We got two blocks which generated 50 coins each, to us.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// We now have the following chain:
// genesis -> b1 -> b2
//
// so fork like this:
//
// genesis -> b1 -> b2
// \-> b3
//
// Nothing should happen at this point. We saw b2 first so it takes priority.
Block b3 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertFalse(reorgHappened[0]); // No re-org took place.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// Now we add another block to make the alternative chain longer.
assertTrue(chain.add(b3.createNextBlock(someOtherGuy)));
assertTrue(reorgHappened[0]); // Re-org took place.
reorgHappened[0] = false;
//
// genesis -> b1 -> b2
// \-> b3 -> b4
//
// We lost some coins! b2 is no longer a part of the best chain so our balance should drop to 50 again.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first chain.
Block b5 = b2.createNextBlock(coinbaseTo);
Block b6 = b5.createNextBlock(coinbaseTo);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
assertTrue(reorgHappened[0]);
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking2() throws Exception {
// Check that if the chain forks and new coins are received in the alternate chain our balance goes up.
Block b1 = unitTestParams.genesisBlock.createNextBlock(someOtherGuy);
Block b2 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
// genesis -> b1 -> b2
// \-> b3 -> b4
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b3 = b1.createNextBlock(coinbaseTo);
Block b4 = b3.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertTrue(chain.add(b4));
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking3() throws Exception {
// Check that we can handle our own spends being rolled back by a fork.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0));
wallet.confirmSend(spend);
// Waiting for confirmation ...
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b2 = b1.createNextBlock(someOtherGuy);
b2.addTransaction(spend);
b2.solve();
chain.add(b2);
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance());
// genesis -> b1 (receive coins) -> b2 (spend coins)
// \-> b3 -> b4
Block b3 = b1.createNextBlock(someOtherGuy);
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b3);
chain.add(b4);
// b4 causes a re-org that should make our spend go inactive. Because the inputs are already spent our balance
// drops to zero again.
assertEquals(BigInteger.ZERO, wallet.getBalance());
// Not pending .... we don't know if our spend will EVER become active again (if there's an attack it may not).
assertEquals(0, wallet.getPendingTransactions().size());
}
@Test
public void testForking4() throws Exception {
// Check that we can handle external spends on an inactive chain becoming active. An external spend is where
// we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when
// keys are being shared between wallets.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(50, 0));
// We do NOT confirm the spend here. That means it's not considered to be pending because createSend is
// stateless. For our purposes it is as if some other program with our keys created the tx.
//
// genesis -> b1 (receive 50) --> b2
// \-> b3 (external spend) -> b4
Block b2 = b1.createNextBlock(someOtherGuy);
chain.add(b2);
Block b3 = b1.createNextBlock(someOtherGuy);
b3.addTransaction(spend);
b3.solve();
chain.add(b3);
// The external spend is not active yet.
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b4);
// The external spend is now active.
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
}
@Test
public void testDifficultyTransitions() throws Exception {
// Add a bunch of blocks in a loop until we reach a difficulty transition point. The unit test params have an

View file

@ -0,0 +1,271 @@
/**
* Copyright 2011 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 org.junit.Before;
import org.junit.Test;
import java.math.BigInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class ChainSplitTests {
private NetworkParameters unitTestParams;
private Wallet wallet;
private BlockChain chain;
private Address coinbaseTo;
private Address someOtherGuy;
@Before
public void setUp() {
unitTestParams = NetworkParameters.unitTests();
wallet = new Wallet(unitTestParams);
wallet.addKey(new ECKey());
chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams));
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
someOtherGuy = new ECKey().toAddress(unitTestParams);
}
@Test
public void testForking1() throws Exception {
// Check that if the block chain forks, we end up using the right chain. Only tests inbound transactions
// (receiving coins). Checking that we understand reversed spends is in testForking2.
// TODO: Change this test to not use coinbase transactions as they are special (maturity rules).
final boolean[] reorgHappened = new boolean[1];
reorgHappened[0] = false;
wallet.addEventListener(new WalletEventListener() {
@Override
public void onReorganize() {
reorgHappened[0] = true;
}
});
// Start by building a couple of blocks on top of the genesis block.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
Block b2 = b1.createNextBlock(coinbaseTo);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
assertFalse(reorgHappened[0]);
// We got two blocks which generated 50 coins each, to us.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// We now have the following chain:
// genesis -> b1 -> b2
//
// so fork like this:
//
// genesis -> b1 -> b2
// \-> b3
//
// Nothing should happen at this point. We saw b2 first so it takes priority.
Block b3 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertFalse(reorgHappened[0]); // No re-org took place.
assertEquals("100.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// Now we add another block to make the alternative chain longer.
assertTrue(chain.add(b3.createNextBlock(someOtherGuy)));
assertTrue(reorgHappened[0]); // Re-org took place.
reorgHappened[0] = false;
//
// genesis -> b1 -> b2
// \-> b3 -> b4
//
// We lost some coins! b2 is no longer a part of the best chain so our available balance should drop to 50.
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
// ... and back to the first chain.
Block b5 = b2.createNextBlock(coinbaseTo);
Block b6 = b5.createNextBlock(coinbaseTo);
assertTrue(chain.add(b5));
assertTrue(chain.add(b6));
//
// genesis -> b1 -> b2 -> b5 -> b6
// \-> b3 -> b4
//
assertTrue(reorgHappened[0]);
assertEquals("200.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking2() throws Exception {
// Check that if the chain forks and new coins are received in the alternate chain our balance goes up
// after the re-org takes place.
Block b1 = unitTestParams.genesisBlock.createNextBlock(someOtherGuy);
Block b2 = b1.createNextBlock(someOtherGuy);
assertTrue(chain.add(b1));
assertTrue(chain.add(b2));
// genesis -> b1 -> b2
// \-> b3 -> b4
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b3 = b1.createNextBlock(coinbaseTo);
Block b4 = b3.createNextBlock(someOtherGuy);
assertTrue(chain.add(b3));
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertTrue(chain.add(b4));
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
}
@Test
public void testForking3() throws Exception {
// Check that we can handle our own spends being rolled back by a fork.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0));
wallet.confirmSend(spend);
// Waiting for confirmation ...
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b2 = b1.createNextBlock(someOtherGuy);
b2.addTransaction(spend);
b2.solve();
chain.add(b2);
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance());
// genesis -> b1 (receive coins) -> b2 (spend coins)
// \-> b3 -> b4
Block b3 = b1.createNextBlock(someOtherGuy);
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b3);
chain.add(b4);
// b4 causes a re-org that should make our spend go inactive. Because the inputs are already spent our
// available balance drops to zero again.
assertEquals(BigInteger.ZERO, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
// We estimate that it'll make it back into the block chain (we know we won't double spend).
// assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void testForking4() throws Exception {
// Check that we can handle external spends on an inactive chain becoming active. An external spend is where
// we see a transaction that spends our own coins but we did not broadcast it ourselves. This happens when
// keys are being shared between wallets.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
Address dest = new ECKey().toAddress(unitTestParams);
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(50, 0));
// We do NOT confirm the spend here. That means it's not considered to be pending because createSend is
// stateless. For our purposes it is as if some other program with our keys created the tx.
//
// genesis -> b1 (receive 50) --> b2
// \-> b3 (external spend) -> b4
Block b2 = b1.createNextBlock(someOtherGuy);
chain.add(b2);
Block b3 = b1.createNextBlock(someOtherGuy);
b3.addTransaction(spend);
b3.solve();
chain.add(b3);
// The external spend is not active yet.
assertEquals(Utils.toNanoCoins(50, 0), wallet.getBalance());
Block b4 = b3.createNextBlock(someOtherGuy);
chain.add(b4);
// The external spend is now active.
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
}
@Test
public void testDoubleSpendOnFork() throws Exception {
// Check what happens when a re-org happens and one of our confirmed transactions becomes invalidated by a
// double spend on the new best chain.
final boolean[] eventCalled = new boolean[1];
wallet.addEventListener(new WalletEventListener() {
@Override
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
eventCalled[0] = true;
}
});
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
wallet.confirmSend(t1);
// Receive t1 as confirmed by the network.
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b2.addTransaction(t1);
b2.solve();
chain.add(b2);
// Now we make a double spend become active after a re-org.
Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b3.addTransaction(t2);
b3.solve();
chain.add(b3); // Side chain.
Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b4); // New best chain.
// Should have seen a double spend.
assertTrue(eventCalled[0]);
assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance());
}
@Test
public void testDoubleSpendOnForkPending() throws Exception {
// Check what happens when a re-org happens and one of our UNconfirmed transactions becomes invalidated by a
// double spend on the new best chain.
final boolean[] eventCalled = new boolean[1];
wallet.addEventListener(new WalletEventListener() {
@Override
public void onDeadTransaction(Transaction deadTx, Transaction replacementTx) {
eventCalled[0] = true;
}
});
// Start with 50 coins.
Block b1 = unitTestParams.genesisBlock.createNextBlock(coinbaseTo);
chain.add(b1);
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
wallet.confirmSend(t1);
// t1 is still pending ...
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b2);
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now we make a double spend become active after a re-org.
// genesis -> b1 -> b2 [t1 pending]
// \-> b3 (t2) -> b4
Block b3 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b3.addTransaction(t2);
b3.solve();
chain.add(b3); // Side chain.
Block b4 = b3.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b4); // New best chain.
// Should have seen a double spend against the pending pool.
assertTrue(eventCalled[0]);
assertEquals(Utils.toNanoCoins(30, 0), wallet.getBalance());
// ... and back to our own parallel universe.
Block b5 = b2.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b5);
Block b6 = b5.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b6);
// genesis -> b1 -> b2 -> b5 -> b6 [t1 pending]
// \-> b3 [t2 inactive] -> b4
assertEquals(Utils.toNanoCoins(0, 0), wallet.getBalance());
assertEquals(Utils.toNanoCoins(40, 0), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
}

View file

@ -23,6 +23,7 @@ import java.math.BigInteger;
import static com.google.bitcoin.core.Utils.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public class WalletTest {
@ -41,16 +42,17 @@ public class WalletTest {
blockStore = new MemoryBlockStore(params);
}
private static byte fakeHashCounter = 0;
private Transaction createFakeTx(BigInteger nanocoins, Address to) {
Transaction t = new Transaction(params);
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t);
TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to);
t.addOutput(o1);
// t1 is not a valid transaction - it has no inputs. Nonetheless, if we set it up with a fake hash it'll be
// valid enough for these tests.
byte[] hash = new byte[32];
hash[0] = fakeHashCounter++;
t.setFakeHashForTesting(new Sha256Hash(hash));
// Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't
// matter for our purposes.
Transaction prevTx = new Transaction(params);
TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to);
prevTx.addOutput(prevOut);
// Connect it.
t.addInput(prevOut);
return t;
}
@ -152,8 +154,11 @@ public class WalletTest {
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
wallet.confirmSend(spend);
// Balance should be 0.50 because the change output is pending confirmation by the network.
assertEquals(toNanoCoins(0, 50), wallet.getBalance());
// Available and estimated balances should not be the same. We don't check the exact available balance here
// because it depends on the coin selection algorithm.
assertEquals(toNanoCoins(4, 50), wallet.getBalance(Wallet.BalanceType.ESTIMATED));
assertFalse(wallet.getBalance(Wallet.BalanceType.AVAILABLE).equals(
wallet.getBalance(Wallet.BalanceType.ESTIMATED)));
// Now confirm the transaction by including it into a block.
StoredBlock b3 = createFakeBlock(spend).storedBlock;
@ -161,8 +166,7 @@ public class WalletTest {
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
BigInteger v4 = toNanoCoins(4, 50);
assertEquals(bitcoinValueToFriendlyString(v4),
bitcoinValueToFriendlyString(wallet.getBalance()));
assertEquals(v4, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
}
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an