mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-07-30 19:41:24 +00:00
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:
@@ -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
|
// 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.
|
// nothing in the system that enforces that and both are just as valid.
|
||||||
coinbase.inputs.add(new TransactionInput(params, new byte[] { (byte) coinbaseCounter++ } ));
|
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);
|
transactions.add(coinbase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -195,8 +195,8 @@ public class BlockChain {
|
|||||||
log.info("New chain head: {}", newChainHead.getHeader().getHashAsString());
|
log.info("New chain head: {}", newChainHead.getHeader().getHashAsString());
|
||||||
log.info("Split at block: {}", splitPoint.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.
|
// Then build a list of all blocks in the old part of the chain and the new part.
|
||||||
Set<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
|
List<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
|
||||||
Set<StoredBlock> newBlocks = getPartialChain(newChainHead, 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)
|
// 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
|
// 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.
|
// 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.
|
* 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();
|
assert higher.getHeight() > lower.getHeight();
|
||||||
Set<StoredBlock> results = new HashSet<StoredBlock>();
|
LinkedList<StoredBlock> results = new LinkedList<StoredBlock>();
|
||||||
StoredBlock cursor = higher;
|
StoredBlock cursor = higher;
|
||||||
while (true) {
|
while (true) {
|
||||||
results.add(cursor);
|
results.add(cursor);
|
||||||
|
@@ -117,7 +117,7 @@ public class Transaction extends Message implements Serializable {
|
|||||||
BigInteger v = BigInteger.ZERO;
|
BigInteger v = BigInteger.ZERO;
|
||||||
for (TransactionOutput o : outputs) {
|
for (TransactionOutput o : outputs) {
|
||||||
if (!o.isMine(wallet)) continue;
|
if (!o.isMine(wallet)) continue;
|
||||||
if (!includeSpent && o.isSpent) continue;
|
if (!includeSpent && !o.isAvailableForSpending()) continue;
|
||||||
v = v.add(o.getValue());
|
v = v.add(o.getValue());
|
||||||
}
|
}
|
||||||
return v;
|
return v;
|
||||||
@@ -161,17 +161,44 @@ public class Transaction extends Message implements Serializable {
|
|||||||
// This is tested in WalletTest.
|
// This is tested in WalletTest.
|
||||||
BigInteger v = BigInteger.ZERO;
|
BigInteger v = BigInteger.ZERO;
|
||||||
for (TransactionInput input : inputs) {
|
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,
|
// This input is taking value from an transaction in our wallet. To discover the value,
|
||||||
// we must find the connected transaction.
|
// 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;
|
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
|
* 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.
|
* 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() {
|
public String toString() {
|
||||||
StringBuffer s = new StringBuffer();
|
StringBuffer s = new StringBuffer();
|
||||||
|
s.append(" ");
|
||||||
|
s.append(getHashAsString());
|
||||||
|
s.append("\n");
|
||||||
if (isCoinBase()) {
|
if (isCoinBase()) {
|
||||||
String script = "???";
|
String script = "???";
|
||||||
String script2 = "???";
|
String script2 = "???";
|
||||||
@@ -323,7 +353,6 @@ public class Transaction extends Message implements Serializable {
|
|||||||
// The anyoneCanPay feature isn't used at the moment.
|
// The anyoneCanPay feature isn't used at the moment.
|
||||||
boolean anyoneCanPay = false;
|
boolean anyoneCanPay = false;
|
||||||
byte[] hash = hashTransactionForSignature(hashType, anyoneCanPay);
|
byte[] hash = hashTransactionForSignature(hashType, anyoneCanPay);
|
||||||
log.info(" signInputs hash={}", Utils.bytesToHexString(hash));
|
|
||||||
// Set the script to empty again for the next input.
|
// Set the script to empty again for the next input.
|
||||||
input.scriptBytes = TransactionInput.EMPTY_ARRAY;
|
input.scriptBytes = TransactionInput.EMPTY_ARRAY;
|
||||||
|
|
||||||
|
@@ -19,6 +19,7 @@ package com.google.bitcoin.core;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A transfer of coins from one address to another creates a transaction in which the outputs
|
* 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.
|
* to the outputs of another. The exceptions are coinbase transactions, which create new coins.
|
||||||
*/
|
*/
|
||||||
public class TransactionInput extends Message implements Serializable {
|
public class TransactionInput extends Message implements Serializable {
|
||||||
private static final long serialVersionUID = -7687665228438202968L;
|
private static final long serialVersionUID = 2;
|
||||||
// An apparently unused field intended for altering transactions after they were broadcast.
|
public static final byte[] EMPTY_ARRAY = new byte[0];
|
||||||
long sequence;
|
|
||||||
// The output of the transaction we're gathering coins from.
|
|
||||||
|
|
||||||
|
// 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;
|
TransactionOutPoint outpoint;
|
||||||
// The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there
|
// 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
|
// 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.
|
// coinbase.
|
||||||
transient private Script scriptSig;
|
transient private Script scriptSig;
|
||||||
|
|
||||||
static public final byte[] EMPTY_ARRAY = new byte[0];
|
|
||||||
|
|
||||||
/** Used only in creation of the genesis block. */
|
/** Used only in creation of the genesis block. */
|
||||||
TransactionInput(NetworkParameters params, byte[] scriptBytes) {
|
TransactionInput(NetworkParameters params, byte[] scriptBytes) {
|
||||||
super(params);
|
super(params);
|
||||||
@@ -124,4 +126,62 @@ public class TransactionInput extends Message implements Serializable {
|
|||||||
throw new RuntimeException(e);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,8 +19,8 @@ package com.google.bitcoin.core;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.Serializable;
|
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.
|
* 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. */
|
/** Which output of that transaction we are talking about. */
|
||||||
long index;
|
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;
|
Transaction fromTx;
|
||||||
|
|
||||||
TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) {
|
TransactionOutPoint(NetworkParameters params, long index, Transaction fromTx) {
|
||||||
@@ -66,27 +67,12 @@ public class TransactionOutPoint extends Message implements Serializable {
|
|||||||
Utils.uint32ToByteStreamLE(index, stream);
|
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,
|
* If this transaction was created using the explicit constructor rather than deserialized,
|
||||||
* retrieves the connected output transaction. Asserts if there is no connected transaction.
|
* retrieves the connected output transaction. Asserts if there is no connected transaction.
|
||||||
*/
|
*/
|
||||||
TransactionOutput getConnectedOutput() {
|
TransactionOutput getConnectedOutput() {
|
||||||
assert fromTx != null;
|
if (fromTx == null) return null;
|
||||||
return fromTx.outputs.get((int)index);
|
return fromTx.outputs.get((int)index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -40,10 +40,12 @@ public class TransactionOutput extends Message implements Serializable {
|
|||||||
// The script bytes are parsed and turned into a Script on demand.
|
// The script bytes are parsed and turned into a Script on demand.
|
||||||
private transient Script scriptPubKey;
|
private transient Script scriptPubKey;
|
||||||
|
|
||||||
// This field is Java serialized but not BitCoin serialized. It's used for tracking purposes in our wallet only.
|
// These fields are Java serialized but not BitCoin serialized. They are used for tracking purposes in our wallet
|
||||||
// If this flag is set to true, it means we have spent this outputs value and it shouldn't be used again or
|
// only. If set to true, this output is counted towards our balance. If false and spentBy is null the tx output
|
||||||
// counted towards our balance.
|
// was owned by us and was sent to somebody else. If false and spentBy is true it means this output was owned by
|
||||||
boolean isSpent;
|
// 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.
|
// A reference to the transaction which holds this output.
|
||||||
Transaction parentTransaction;
|
Transaction parentTransaction;
|
||||||
@@ -53,13 +55,15 @@ public class TransactionOutput extends Message implements Serializable {
|
|||||||
int offset) throws ProtocolException {
|
int offset) throws ProtocolException {
|
||||||
super(params, payload, offset);
|
super(params, payload, offset);
|
||||||
parentTransaction = parent;
|
parentTransaction = parent;
|
||||||
|
availableForSpending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionOutput(NetworkParameters params, BigInteger value, Address to, Transaction parent) {
|
TransactionOutput(NetworkParameters params, Transaction parent, BigInteger value, Address to) {
|
||||||
super(params);
|
super(params);
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.scriptBytes = Script.createOutputScript(to);
|
this.scriptBytes = Script.createOutputScript(to);
|
||||||
parentTransaction = parent;
|
parentTransaction = parent;
|
||||||
|
availableForSpending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Used only in creation of the genesis blocks and in unit tests. */
|
/** 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);
|
super(params);
|
||||||
this.scriptBytes = scriptBytes;
|
this.scriptBytes = scriptBytes;
|
||||||
this.value = Utils.toNanoCoins(50, 0);
|
this.value = Utils.toNanoCoins(50, 0);
|
||||||
|
availableForSpending = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Script getScriptPubKey() throws ScriptException {
|
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?");
|
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() {
|
public byte[] getScriptBytes() {
|
||||||
return scriptBytes;
|
return scriptBytes;
|
||||||
}
|
}
|
||||||
|
@@ -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
|
* 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
|
* providing transactions on demand that meet a given combined value.<p>
|
||||||
* output is used, it is removed from the wallet as it is no longer available for spending.<p>
|
|
||||||
*
|
*
|
||||||
* The Wallet is read and written from disk, so be sure to follow the Java serialization
|
* The Wallet is read and written from disk, so be sure to follow the Java serialization versioning rules here. We
|
||||||
* versioning rules here. We use the built in Java serialization to avoid the need to
|
* use the built in Java serialization to avoid the need to pull in a potentially large (code-size) third party
|
||||||
* pull in a potentially large (code-size) third party serialization library.<p>
|
* serialization library.<p>
|
||||||
*/
|
*/
|
||||||
public class Wallet implements Serializable {
|
public class Wallet implements Serializable {
|
||||||
private static final Logger log = LoggerFactory.getLogger(Wallet.class);
|
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:
|
// 5. Inbound tx is accepted into a side chain:
|
||||||
// ->inactive
|
// ->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:
|
// Re-orgs:
|
||||||
// 1. Tx is present in old chain and not present in new chain
|
// 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()
|
// 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
|
// as miners resurrect them and re-include into the new best chain.
|
||||||
// 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.
|
|
||||||
//
|
//
|
||||||
// 2. Tx is not present in old chain and is present in new chain
|
// 2. Tx is not present in old chain and is present in new chain
|
||||||
// <-inactive and ->unspent/spent
|
// <-inactive and ->unspent/spent
|
||||||
@@ -74,7 +73,8 @@ public class Wallet implements Serializable {
|
|||||||
// change outputs would not be considered spendable.
|
// 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
|
* 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!
|
* 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;
|
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. */
|
/** A list of public/private EC keys owned by this user. */
|
||||||
public final ArrayList<ECKey> keychain;
|
public final ArrayList<ECKey> keychain;
|
||||||
|
|
||||||
@@ -132,6 +140,7 @@ public class Wallet implements Serializable {
|
|||||||
spent = new HashMap<Sha256Hash, Transaction>();
|
spent = new HashMap<Sha256Hash, Transaction>();
|
||||||
inactive = new HashMap<Sha256Hash, Transaction>();
|
inactive = new HashMap<Sha256Hash, Transaction>();
|
||||||
pending = new HashMap<Sha256Hash, Transaction>();
|
pending = new HashMap<Sha256Hash, Transaction>();
|
||||||
|
dead = new HashMap<Sha256Hash, Transaction>();
|
||||||
eventListeners = new ArrayList<WalletEventListener>();
|
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!
|
* 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 {
|
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.
|
// Runs in a peer thread.
|
||||||
BigInteger prevBalance = getBalance();
|
BigInteger prevBalance = getBalance();
|
||||||
|
|
||||||
@@ -206,8 +220,10 @@ public class Wallet implements Serializable {
|
|||||||
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
||||||
BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe);
|
BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe);
|
||||||
|
|
||||||
log.info("Wallet: Received tx" + (sideChain ? " on a side chain" :"") + " for " +
|
if (!reorg) {
|
||||||
bitcoinValueToFriendlyString(valueDifference) + " BTC");
|
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
|
// 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.
|
// 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);
|
pending.put(wtx.getHash(), wtx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (!reorg) {
|
||||||
// Mark the tx as appearing in this block so we can find it later after a re-org.
|
// Mark the tx as appearing in this block so we can find it later after a re-org.
|
||||||
tx.addBlockAppearance(block);
|
tx.addBlockAppearance(block);
|
||||||
|
}
|
||||||
// This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys
|
// 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.
|
// are being shared between different wallets.
|
||||||
if (sideChain) {
|
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,
|
// 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,
|
// so we must not make assumptions about our state after this loop returns! For example,
|
||||||
// the balance we just received might already be spent!
|
// 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) {
|
for (WalletEventListener l : eventListeners) {
|
||||||
synchronized (l) {
|
synchronized (l) {
|
||||||
l.onCoinsReceived(this, tx, prevBalance, getBalance());
|
l.onCoinsReceived(this, tx, prevBalance, getBalance());
|
||||||
@@ -284,37 +302,58 @@ public class Wallet implements Serializable {
|
|||||||
updateForSpends(tx);
|
updateForSpends(tx);
|
||||||
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
|
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
|
||||||
// It's sending us coins.
|
// It's sending us coins.
|
||||||
log.info(" ->unspent");
|
log.info(" new tx ->unspent");
|
||||||
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
|
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
|
||||||
assert !alreadyPresent : "TX was received twice";
|
assert !alreadyPresent : "TX was received twice";
|
||||||
} else {
|
} else {
|
||||||
// It spent some of our coins and did not send us any.
|
// 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;
|
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
|
||||||
assert !alreadyPresent : "TX was received twice";
|
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
|
* 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.
|
* there's no need to go through and do it again.
|
||||||
*/
|
*/
|
||||||
private void updateForSpends(Transaction tx) throws VerificationException {
|
private void updateForSpends(Transaction tx) throws VerificationException {
|
||||||
for (TransactionInput input : tx.inputs) {
|
for (TransactionInput input : tx.inputs) {
|
||||||
if (input.outpoint.connect(unspent.values())) {
|
TransactionInput.ConnectionResult result = input.connect(unspent, false);
|
||||||
TransactionOutput output = input.outpoint.getConnectedOutput();
|
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
|
||||||
assert !output.isSpent : "Double spend accepted by the network?";
|
// Doesn't spend any of our outputs or is coinbase.
|
||||||
log.info(" Saw some of my unspent outputs be spent by someone else who has my keys.");
|
continue;
|
||||||
log.info(" Total spent value is " + bitcoinValueToFriendlyString(output.getValue()));
|
} else if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
|
||||||
output.isSpent = true;
|
// Double spend! This must have overridden a pending tx, or the block is bad (contains transactions
|
||||||
Transaction connectedTx = input.outpoint.fromTx;
|
// that illegally double spend: should never occur if we are connected to an honest node).
|
||||||
if (connectedTx.getValueSentToMe(this, false).equals(BigInteger.ZERO)) {
|
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.
|
// 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");
|
log.info(" prevtx <-unspent");
|
||||||
spent.put(connectedTx.getHash(), connectedTx);
|
|
||||||
log.info(" prevtx ->spent");
|
log.info(" prevtx ->spent");
|
||||||
|
spent.put(connected.getHash(), connected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -337,11 +376,11 @@ public class Wallet implements Serializable {
|
|||||||
*/
|
*/
|
||||||
synchronized void confirmSend(Transaction tx) {
|
synchronized void confirmSend(Transaction tx) {
|
||||||
assert !pending.containsKey(tx) : "confirmSend called on the same transaction twice";
|
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) {
|
for (TransactionInput input : tx.inputs) {
|
||||||
TransactionOutput connectedOutput = input.outpoint.getConnectedOutput();
|
TransactionOutput connectedOutput = input.outpoint.getConnectedOutput();
|
||||||
assert !connectedOutput.isSpent : "createSend called before corresponding confirmSend";
|
connectedOutput.markAsSpent(input);
|
||||||
connectedOutput.isSpent = true;
|
|
||||||
}
|
}
|
||||||
// Some of the outputs probably send coins back to us, eg for change or because this transaction is just
|
// 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
|
// 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) {
|
for (TransactionOutput output : tx.outputs) {
|
||||||
if (!output.isMine(this)) {
|
if (!output.isMine(this)) {
|
||||||
// This output didn't go to us, so by definition it is now spent.
|
// This output didn't go to us, so by definition it is now spent.
|
||||||
assert !output.isSpent;
|
output.markAsSpent(null);
|
||||||
output.isSpent = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pending.put(tx.getHash(), tx);
|
pending.put(tx.getHash(), tx);
|
||||||
@@ -412,7 +450,7 @@ public class Wallet implements Serializable {
|
|||||||
List<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
|
List<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
|
||||||
for (Transaction tx : unspent.values()) {
|
for (Transaction tx : unspent.values()) {
|
||||||
for (TransactionOutput output : tx.outputs) {
|
for (TransactionOutput output : tx.outputs) {
|
||||||
if (output.isSpent) continue;
|
if (!output.isAvailableForSpending()) continue;
|
||||||
if (!output.isMine(this)) continue;
|
if (!output.isMine(this)) continue;
|
||||||
gathered.add(output);
|
gathered.add(output);
|
||||||
valueGathered = valueGathered.add(output.getValue());
|
valueGathered = valueGathered.add(output.getValue());
|
||||||
@@ -428,14 +466,14 @@ public class Wallet implements Serializable {
|
|||||||
}
|
}
|
||||||
assert gathered.size() > 0;
|
assert gathered.size() > 0;
|
||||||
Transaction sendTx = new Transaction(params);
|
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);
|
BigInteger change = valueGathered.subtract(nanocoins);
|
||||||
if (change.compareTo(BigInteger.ZERO) > 0) {
|
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,
|
// 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
|
// we need to take back some coins ... this is called "change". Add another output that sends the change
|
||||||
// back to us.
|
// back to us.
|
||||||
log.info(" with " + bitcoinValueToFriendlyString(change) + " coins change");
|
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) {
|
for (TransactionOutput output : gathered) {
|
||||||
sendTx.addInput(output);
|
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.
|
// happen, if it does it means the wallet has got into an inconsistent state.
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
log.info(" created {}", sendTx.getHashAsString());
|
||||||
return sendTx;
|
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() {
|
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 (Transaction tx : unspent.values()) {
|
||||||
for (TransactionOutput output : tx.outputs) {
|
for (TransactionOutput output : tx.outputs) {
|
||||||
if (output.isSpent) continue;
|
|
||||||
if (!output.isMine(this)) 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
|
@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,
|
* 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
|
* 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
|
* 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.
|
// 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
|
// 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.
|
// 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.
|
// 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>();
|
log.info(" Old part of chain (top to bottom):");
|
||||||
all.addAll(unspent.values());
|
for (StoredBlock b : oldBlocks) log.info(" {}", b.getHeader().getHashAsString());
|
||||||
all.addAll(spent.values());
|
log.info(" New part of chain (top to bottom):");
|
||||||
all.addAll(inactive.values());
|
for (StoredBlock b : newBlocks) log.info(" {}", b.getHeader().getHashAsString());
|
||||||
for (Transaction tx : all) {
|
|
||||||
|
// 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();
|
Set<StoredBlock> appearsIn = tx.getAppearsIn();
|
||||||
assert appearsIn != null;
|
assert appearsIn != null;
|
||||||
// If the set of blocks this transaction appears in is disjoint with one of the chain segments it means
|
// 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.
|
// the transaction was never incorporated by a miner into that side of the chain.
|
||||||
if (!Collections.disjoint(appearsIn, oldBlocks)) {
|
boolean inOldSection = !Collections.disjoint(appearsIn, oldBlocks);
|
||||||
boolean alreadyPresent = !oldChainTransactions.add(tx);
|
boolean inNewSection = !Collections.disjoint(appearsIn, newBlocks);
|
||||||
assert !alreadyPresent : "Transaction appears twice in chain segment";
|
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
|
// If there is no difference it means we have nothing we need to do and the user does not care.
|
||||||
// update the transaction block pointers for next time.
|
|
||||||
boolean affectedUs = !oldChainTransactions.equals(newChainTransactions);
|
boolean affectedUs = !oldChainTransactions.equals(newChainTransactions);
|
||||||
log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions");
|
log.info(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions");
|
||||||
if (!affectedUs) return;
|
if (!affectedUs) return;
|
||||||
|
|
||||||
// Transactions that were in the old chain but aren't in the new chain. These will become inactive.
|
// For simplicity we will reprocess every transaction to ensure it's in the right bucket and has the right
|
||||||
Set<Transaction> gone = new HashSet<Transaction>(oldChainTransactions);
|
// connections. Attempting to update each one with minimal work is possible but complex and was leading to
|
||||||
gone.removeAll(newChainTransactions);
|
// edge cases that were hard to fix. As re-orgs are rare the amount of work this implies should be manageable
|
||||||
// Transactions that are in the new chain but aren't in the old chain. These will be re-processed.
|
// unless the user has an enormous wallet. As an optimization fully spent transactions buried deeper than
|
||||||
Set<Transaction> fresh = new HashSet<Transaction>(newChainTransactions);
|
// 1000 blocks could be put into yet another bucket which we never touch and assume re-orgs cannot affect.
|
||||||
fresh.removeAll(oldChainTransactions);
|
|
||||||
assert !(gone.isEmpty() && fresh.isEmpty()) : "There must have been some changes to get here";
|
|
||||||
|
|
||||||
for (Transaction tx : gone) {
|
for (Transaction tx : onlyOldChainTransactions.values()) log.info(" Only Old: {}", tx.getHashAsString());
|
||||||
log.info("tx not in new chain: <-unspent/spent ->inactive\n" + tx.toString());
|
for (Transaction tx : oldChainTransactions.values()) log.info(" Old: {}", tx.getHashAsString());
|
||||||
unspent.remove(tx.getHash());
|
for (Transaction tx : newChainTransactions.values()) log.info(" New: {}", tx.getHashAsString());
|
||||||
spent.remove(tx.getHash());
|
|
||||||
inactive.put(tx.getHash(), tx);
|
// Break all the existing connections.
|
||||||
// We do not put it into the pending pool. Pending is for transactions we know are valid. After a re-org
|
for (Transaction tx : all.values())
|
||||||
// some transactions may become permanently invalid if the new chain contains a double spend. We don't
|
tx.disconnectInputs();
|
||||||
// want transactions sitting in the pending pool forever. This means shortly after a re-org the balance
|
for (Transaction tx : pending.values())
|
||||||
// might change rapidly as newly transactions are resurrected and included into the new chain by miners.
|
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) {
|
// Recalculate the unspent/spent buckets for the transactions the re-org did not affect.
|
||||||
inactive.remove(tx.getHash());
|
unspent.clear();
|
||||||
processTxFromBestChain(tx);
|
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.
|
// Inform event listeners that a re-org took place.
|
||||||
for (WalletEventListener l : eventListeners) {
|
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.
|
* Returns an immutable view of the transactions currently waiting for network confirmations.
|
||||||
*/
|
*/
|
||||||
|
@@ -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;
|
package com.google.bitcoin.core;
|
||||||
|
|
||||||
import java.math.BigInteger;
|
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
|
* 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
|
* 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() {
|
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) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,10 +24,8 @@ import java.math.BigInteger;
|
|||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
// Tests still to write:
|
// NOTE: Handling of chain splits/reorgs are in ChainSplitTests.
|
||||||
// - Fragmented chains can be joined together.
|
|
||||||
// - Longest testNetChain is selected based on total difficulty not length.
|
|
||||||
// - Many more ...
|
|
||||||
public class BlockChainTest {
|
public class BlockChainTest {
|
||||||
private static final NetworkParameters testNet = NetworkParameters.testNet();
|
private static final NetworkParameters testNet = NetworkParameters.testNet();
|
||||||
private BlockChain testNetChain;
|
private BlockChain testNetChain;
|
||||||
@@ -88,140 +86,6 @@ public class BlockChainTest {
|
|||||||
assertEquals(chain.getChainHead().getHeader(), b3.cloneAsHeader());
|
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
|
@Test
|
||||||
public void testDifficultyTransitions() throws Exception {
|
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
|
// Add a bunch of blocks in a loop until we reach a difficulty transition point. The unit test params have an
|
||||||
|
271
tests/com/google/bitcoin/core/ChainSplitTests.java
Normal file
271
tests/com/google/bitcoin/core/ChainSplitTests.java
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
@@ -23,6 +23,7 @@ import java.math.BigInteger;
|
|||||||
|
|
||||||
import static com.google.bitcoin.core.Utils.*;
|
import static com.google.bitcoin.core.Utils.*;
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class WalletTest {
|
public class WalletTest {
|
||||||
@@ -41,16 +42,17 @@ public class WalletTest {
|
|||||||
blockStore = new MemoryBlockStore(params);
|
blockStore = new MemoryBlockStore(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte fakeHashCounter = 0;
|
|
||||||
private Transaction createFakeTx(BigInteger nanocoins, Address to) {
|
private Transaction createFakeTx(BigInteger nanocoins, Address to) {
|
||||||
Transaction t = new Transaction(params);
|
Transaction t = new Transaction(params);
|
||||||
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t);
|
TransactionOutput o1 = new TransactionOutput(params, t, nanocoins, to);
|
||||||
t.addOutput(o1);
|
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
|
// Make a previous tx simply to send us sufficient coins. This prev tx is not really valid but it doesn't
|
||||||
// valid enough for these tests.
|
// matter for our purposes.
|
||||||
byte[] hash = new byte[32];
|
Transaction prevTx = new Transaction(params);
|
||||||
hash[0] = fakeHashCounter++;
|
TransactionOutput prevOut = new TransactionOutput(params, prevTx, nanocoins, to);
|
||||||
t.setFakeHashForTesting(new Sha256Hash(hash));
|
prevTx.addOutput(prevOut);
|
||||||
|
// Connect it.
|
||||||
|
t.addInput(prevOut);
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +154,11 @@ public class WalletTest {
|
|||||||
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
|
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
|
||||||
wallet.confirmSend(spend);
|
wallet.confirmSend(spend);
|
||||||
|
|
||||||
// Balance should be 0.50 because the change output is pending confirmation by the network.
|
// Available and estimated balances should not be the same. We don't check the exact available balance here
|
||||||
assertEquals(toNanoCoins(0, 50), wallet.getBalance());
|
// 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.
|
// Now confirm the transaction by including it into a block.
|
||||||
StoredBlock b3 = createFakeBlock(spend).storedBlock;
|
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.
|
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
|
||||||
BigInteger v4 = toNanoCoins(4, 50);
|
BigInteger v4 = toNanoCoins(4, 50);
|
||||||
assertEquals(bitcoinValueToFriendlyString(v4),
|
assertEquals(v4, wallet.getBalance(Wallet.BalanceType.AVAILABLE));
|
||||||
bitcoinValueToFriendlyString(wallet.getBalance()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an
|
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an
|
||||||
|
Reference in New Issue
Block a user