First cut at a pending transactions patch. This isn't the final API, which will

involve some changes to the wallet event listener/tx to have a concept of
confidence levels.
This commit is contained in:
Mike Hearn
2011-11-29 15:11:15 +01:00
committed by Miron Cuperman
parent e6acc153ad
commit 7aa485110a
17 changed files with 559 additions and 157 deletions

View File

@@ -35,4 +35,9 @@ public class AbstractPeerEventListener extends Object implements PeerEventListen
public void onPeerDisconnected(Peer peer, int peerCount) {
}
public Message onPreMessageReceived(Peer peer, Message m) {
// Just pass the message right through for further processing.
return m;
}
}

View File

@@ -312,7 +312,7 @@ public class BlockChain {
try {
List<Transaction> txns = entry.getValue();
for (Transaction tx : txns) {
entry.getKey().receive(tx, block, blockType);
entry.getKey().receiveFromBlock(tx, block, blockType);
}
} catch (ScriptException e) {
// We don't want scripts we don't understand to break the block chain so just note that this tx was
@@ -427,28 +427,9 @@ public class BlockChain {
for (Transaction tx : block.transactions) {
try {
for (Wallet wallet : wallets) {
boolean shouldReceive = false;
for (TransactionOutput output : tx.getOutputs()) {
// TODO: Handle more types of outputs, not just regular to address outputs.
if (output.getScriptPubKey().isSentToIP()) continue;
// This is not thread safe as a key could be removed between the call to isMine and receive.
if (output.isMine(wallet)) {
shouldReceive = true;
break;
}
}
// Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air).
if (!shouldReceive && !tx.isCoinBase()) {
for (TransactionInput i : tx.getInputs()) {
byte[] pubkey = i.getScriptSig().getPubKey();
// This is not thread safe as a key could be removed between the call to isPubKeyMine and receive.
if (wallet.isPubKeyMine(pubkey)) {
shouldReceive = true;
}
}
}
if (tx.isCoinBase())
continue;
boolean shouldReceive = wallet.isTransactionRelevant(tx, true);
if (!shouldReceive) continue;
List<Transaction> txList = walletToTxMap.get(wallet);
if (txList == null) {

View File

@@ -41,6 +41,7 @@ public class Peer {
private final BlockChain blockChain;
// When an API user explicitly requests a block or transaction from a peer, the InventoryItem is put here
// whilst waiting for the response. Synchronized on itself. Is not used for downloads Peer generates itself.
// TODO: Make this work for transactions as well.
private final List<GetDataFuture<Block>> pendingGetBlockFutures;
// Height of the chain advertised in the peers version message.
private int bestHeight;
@@ -64,6 +65,9 @@ public class Peer {
// received at which point it gets set to true again. This isn't relevant unless downloadData is true.
private boolean downloadBlockBodies = true;
// Wallets that will be notified of pending transactions.
private ArrayList<Wallet> wallets = new ArrayList<Wallet>();
/**
* Construct a peer that reads/writes from the given block chain. Note that communication won't occur until
* you call connect(), which will set up a new NetworkConnection.
@@ -151,10 +155,23 @@ public class Peer {
try {
while (true) {
Message m = conn.readMessage();
// Allow event listeners to filter the message stream. Listeners are allowed to drop messages by
// returning null.
for (PeerEventListener listener : eventListeners) {
synchronized (listener) {
m = listener.onPreMessageReceived(this, m);
if (m == null) break;
}
}
if (m == null) continue;
if (m instanceof InventoryMessage) {
processInv((InventoryMessage) m);
} else if (m instanceof Block) {
processBlock((Block) m);
} else if (m instanceof Transaction) {
processTransaction((Transaction) m);
} else if (m instanceof AddressMessage) {
// We don't care about addresses of the network right now. But in future,
// we should save them in the wallet so we don't put too much load on the seed nodes and can
@@ -225,8 +242,21 @@ public class Peer {
}
}
private void processTransaction(Transaction m) {
log.info("Received broadcast tx {}", m.getHashAsString());
for (Wallet wallet : wallets) {
try {
wallet.receivePending(m);
} catch (VerificationException e) {
log.warn("Received invalid transaction, ignoring", e);
} catch (ScriptException e) {
log.warn("Received invalid transaction, ignoring", e);
}
}
}
private void processBlock(Block m) throws IOException {
// This should called in the network loop thread for this peer
log.info("Received broadcast block {}", m.getHashAsString());
try {
// Was this block requested by getBlock()?
synchronized (pendingGetBlockFutures) {
@@ -293,18 +323,13 @@ public class Peer {
blockChainDownload(topHash);
return;
}
// Just copy the message contents across - request whatever we're told about.
// TODO: Don't re-request items that were already fetched.
GetDataMessage getdata = new GetDataMessage(params);
boolean dirty = false;
for (InventoryItem item : items) {
if (item.type != InventoryItem.Type.Block) continue;
getdata.addItem(item);
dirty = true;
}
// No blocks to download. This probably contained transactions instead, but right now we can't prove they are
// valid so we don't bother downloading transactions that aren't in blocks yet.
if (!dirty)
return;
// This will cause us to receive a bunch of block messages.
// This will cause us to receive a bunch of block or tx messages.
conn.writeMessage(getdata);
}
@@ -361,6 +386,21 @@ public class Peer {
}
}
}
/**
* Add a wallet that will receive notifications of broadcast transactions. You need to connect the peer to each
* wallet this way if you care about handling un-confirmed transactions. If you don't,
* it's unnecessary as the wallet will be informed of transactions in new blocks via the block chain object.
*/
public void addWallet(Wallet wallet) {
if (wallet == null)
throw new IllegalArgumentException("Wallet must not be null");
wallets.add(wallet);
}
public void removeWallet(Wallet wallet) {
wallets.remove(wallet);
}
// A GetDataFuture wraps the result of a getBlock or (in future) getTransaction so the owner of the object can
// decide whether to wait forever, wait for a short while or check later after doing other work.
@@ -418,7 +458,7 @@ public class Peer {
/**
* Send the given Transaction, ie, make a payment with BitCoins. To create a transaction you can broadcast, use
* a {@link Wallet}. After the broadcast completes, confirm the send using the wallet confirmSend() method.
* a {@link Wallet}. After the broadcast completes, confirm the send using the wallet commitTx() method.
* @throws IOException
*/
void broadcastTransaction(Transaction tx) throws IOException {

View File

@@ -60,4 +60,12 @@ public interface PeerEventListener {
* @param peerCount the total number of connected peers
*/
public void onPeerDisconnected(Peer peer, int peerCount);
/**
* Called when a message is received by a peer, before the message is processed. The returned message is
* processed instead. Returning null will cause the message to be ignored by the Peer returning the same message
* object allows you to see the messages received but not change them. The result from one event listeners
* callback is passed as "m" to the next, forming a chain.
*/
public Message onPreMessageReceived(Peer peer, Message m);
}

View File

@@ -19,8 +19,6 @@ package com.google.bitcoin.core;
import com.google.bitcoin.discovery.PeerDiscovery;
import com.google.bitcoin.discovery.PeerDiscoveryException;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.BlockStoreException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -75,28 +73,33 @@ public class PeerGroup {
private Set<PeerDiscovery> peerDiscoverers;
private NetworkParameters params;
private BlockStore blockStore;
private BlockChain chain;
private int connectionDelayMillis;
private long fastCatchupTimeSecs;
private Set<Wallet> wallets;
/**
* Creates a PeerGroup with the given parameters and a default 5 second connection timeout.
* Creates a PeerGroup with the given parameters and a default 5 second connection timeout. If you don't care
* about blocks or pending transactions, you can just provide a MemoryBlockStore and a newly created Wallet.
*
* @param params Network parameters
* @param chain a BlockChain object that will receive and handle block messages.
* @param wallet a Wallet object that will receive pending transactions.
*/
public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain) {
this(blockStore, params, chain, DEFAULT_CONNECTION_DELAY_MILLIS);
public PeerGroup(NetworkParameters params, BlockChain chain) {
this(params, chain, DEFAULT_CONNECTION_DELAY_MILLIS);
}
/**
* Creates a PeerGroup with the given parameters. The connectionDelayMillis parameter controls how long the
* PeerGroup will wait between attempts to connect to nodes or read from any added peer discovery sources.
*/
public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain, int connectionDelayMillis) {
this.blockStore = blockStore;
public PeerGroup(NetworkParameters params, BlockChain chain, int connectionDelayMillis) {
this.params = params;
this.chain = chain;
this.connectionDelayMillis = connectionDelayMillis;
this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds();
this.wallets = new HashSet<Wallet>();
inactives = new LinkedBlockingQueue<PeerAddress>();
peers = Collections.synchronizedSet(new HashSet<Peer>());
@@ -122,6 +125,20 @@ public class PeerGroup {
return peerEventListeners.remove(listener);
}
/**
* The given wallet will receive broadcast/pending transactions that did not appear in a block yet.
* @return Whether the wallet was added successfully.
*/
public boolean addWallet(Wallet wallet) {
if (wallet == null)
throw new IllegalArgumentException("Wallet must not be null");
return wallets.add(wallet);
}
public boolean removeWallet(Wallet wallet) {
return wallets.remove(wallet);
}
/**
* Use this to directly add an already initialized and connected {@link Peer} object. Normally, you would prefer
* to use {@link PeerGroup#addAddress(PeerAddress)} and let this object handle construction of the peer for you.
@@ -291,7 +308,7 @@ public class PeerGroup {
final PeerAddress address = inactives.take();
while (true) {
try {
Peer peer = new Peer(params, address, blockStore.getChainHead().getHeight(), chain);
Peer peer = new Peer(params, address, chain.getChainHead().getHeight(), chain);
executePeer(address, peer, true, ExecuteBlockMode.RETURN_IMMEDIATELY);
break;
} catch (RejectedExecutionException e) {
@@ -301,11 +318,6 @@ public class PeerGroup {
// if we reached maxConnections or if peer queue is empty. Also consider
// exponential backoff on peers and adjusting the sleep time according to the
// lowest backoff value in queue.
} catch (BlockStoreException e) {
// Fatal error
log.error("Block store corrupt?", e);
running = false;
throw new RuntimeException(e);
}
// If we got here, we should retry this address because an error unrelated
@@ -442,10 +454,15 @@ public class PeerGroup {
private synchronized void setDownloadPeer(Peer peer) {
if (downloadPeer != null) {
log.info("Unsetting download peer: {}", downloadPeer);
downloadPeer.setDownloadData(false);
for (Wallet w : wallets) {
downloadPeer.removeWallet(w);
}
}
downloadPeer = peer;
if (downloadPeer != null) {
log.info("Setting download peer: {}", downloadPeer);
downloadPeer.setDownloadData(true);
downloadPeer.setFastCatchupTime(fastCatchupTimeSecs);
}
@@ -459,6 +476,9 @@ public class PeerGroup {
fastCatchupTimeSecs = secondsSinceEpoch;
if (downloadPeer != null) {
downloadPeer.setFastCatchupTime(secondsSinceEpoch);
for (Wallet w : wallets) {
downloadPeer.addWallet(w);
}
}
}

View File

@@ -75,7 +75,7 @@ public class Transaction extends ChildMessage implements Serializable {
inputs = new ArrayList<TransactionInput>();
outputs = new ArrayList<TransactionOutput>();
// We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet.
length = 10; //8 for std fields + 1 for each 0 varint
length = 10; // 8 for std fields + 1 for each 0 varint
}
/**
@@ -176,6 +176,15 @@ public class Transaction extends ChildMessage implements Serializable {
return appearsIn;
}
/** Returns true if this transaction hasn't been seen in any block yet. */
public boolean isPending() {
if (appearsIn == null)
return true;
if (appearsIn.size() == 0)
return true;
return false;
}
/**
* Adds the given block to the internal serializable set of blocks in which this transaction appears. This is
* used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
@@ -451,7 +460,6 @@ public class Transaction extends ChildMessage implements Serializable {
s.append(in.getScriptSig().getFromAddress().toString());
} catch (Exception e) {
s.append("[exception: ").append(e.getMessage()).append("]");
throw new RuntimeException(e);
}
s.append("\n");
}

View File

@@ -21,8 +21,6 @@ import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.Serializable;
// 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.
*/
@@ -103,8 +101,9 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
}
/**
* If this transaction was created using the explicit constructor rather than deserialized,
* retrieves the connected output transaction. Asserts if there is no connected transaction.
* An outpoint is a part of a transaction input that points to the output of another transaction. If we have both
* sides in memory, and they have been linked together, this returns a pointer to the connected output, or null
* if there is no such connection.
*/
TransactionOutput getConnectedOutput() {
if (fromTx == null) return null;
@@ -157,14 +156,6 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
return index;
}
// /**
// * @param index the index to set
// */
// public void setIndex(long index) {
// unCache();
// this.index = index;
// }
/**
* Ensure object is fully parsed before invoking java serialization. The backing byte array
* is transient so if the object has parseLazy = true and hasn't invoked checkParse yet
@@ -174,4 +165,16 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
maybeParse();
out.defaultWriteObject();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof TransactionOutPoint)) return false;
TransactionOutPoint o = (TransactionOutPoint) other;
return o.getIndex() == getIndex() && o.getHash().equals(getHash());
}
@Override
public int hashCode() {
return getHash().hashCode();
}
}

View File

@@ -178,7 +178,7 @@ public class Wallet implements Serializable {
}
/**
* Returns a wallet deserialied from the given file input stream.
* Returns a wallet deserialized from the given file input stream.
*/
public static Wallet loadFromFileStream(FileInputStream f) throws IOException {
ObjectInputStream ois = null;
@@ -200,25 +200,113 @@ public class Wallet implements Serializable {
/**
* Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses or
* spends coins from one of our addresses (note that a single transaction can do both).<p>
* <p/>
*
* This is necessary for the internal book-keeping Wallet does. When a transaction is received that sends us
* coins it is added to a pool so we can use it later to create spends. When a transaction is received that
* consumes outputs they are marked as spent so they won't be used in future.<p>
* <p/>
*
* A transaction that spends our own coins can be received either because a spend we created was accepted by the
* network and thus made it into a block, or because our keys are being shared between multiple instances and
* some other node spent the coins instead. We still have to know about that to avoid accidentally trying to
* double spend.<p>
* <p/>
*
* A transaction may be received multiple times if is included into blocks in parallel chains. The blockType
* parameter describes whether the containing block is on the main/best chain or whether it's on a presently
* inactive side chain. We must still record these transactions and the blocks they appear in because a future
* 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 receiveFromBlock(Transaction tx, StoredBlock block,
BlockChain.NewBlockType blockType) throws VerificationException, ScriptException {
receive(tx, block, blockType, false);
}
/**
* Called when we have found a tranasction (via network broadcast or otherwise) that is relevant to this wallet
* and want to record it. Note that we <b>cannot verify these transactions at all</b>, they may spend fictional
* coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon,
* and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not
* be double spent as an optimization.
*
* @param tx
* @throws VerificationException
* @throws ScriptException
*/
synchronized void receivePending(Transaction tx) throws VerificationException, ScriptException {
// TODO: Add a notion of confidence levels to this API.
// Runs in a peer thread.
log.info(tx.getHashAsString());
// Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
// between pools.
EnumSet<Pool> containingPools = getContainingPools(tx);
if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
log.info("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
return;
}
// We only care about transactions that:
// - Send us coins
// - Spend our coins
if (!isTransactionRelevant(tx, true)) {
log.info("Received tx that isn't relevant to this wallet, discarding.");
return;
}
BigInteger value = tx.getValueSentToMe(this);
log.info("Received a pending transaction {} that sends us {} BTC", tx.getHashAsString(),
Utils.bitcoinValueToFriendlyString(value));
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
// timestamp on the transaction.
commitTx(tx);
// Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes.
BigInteger balance = getBalance();
BigInteger newBalance = balance.add(value);
for (WalletEventListener l : eventListeners) {
synchronized (l) {
l.onCoinsReceived(this, tx, balance, newBalance);
}
}
}
/**
* Returns true if the given transaction sends coins to any of our keys, or has inputs spending any of our outputs,
* and if includeDoubleSpending is true, also returns true if tx has inputs that are spending outputs which are
* not ours but which are spent by pending transactions.<p>
*
* Note that if the tx has inputs containing one of our keys, but the connected transaction is not in the wallet,
* it will not be considered relevant.
*/
public boolean isTransactionRelevant(Transaction tx, boolean includeDoubleSpending) throws ScriptException {
return tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 ||
tx.getValueSentToMe(this).compareTo(BigInteger.ZERO) > 0 ||
(includeDoubleSpending && (findDoubleSpendAgainstPending(tx) != null));
}
/**
* Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if
* the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found.
*/
private Transaction findDoubleSpendAgainstPending(Transaction tx) {
// Compile a set of outpoints that are spent by tx.
HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>();
for (TransactionInput input : tx.getInputs()) {
outpoints.add(input.getOutpoint());
}
// Now for each pending transaction, see if it shares any outpoints with this tx.
for (Transaction p : pending.values()) {
for (TransactionInput input : p.getInputs()) {
if (outpoints.contains(input.getOutpoint())) {
// It does, it's a double spend against the pending pool, which makes it relevant.
return p;
}
}
}
return null;
}
private synchronized void receive(Transaction tx, StoredBlock block,
BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException, ScriptException {
// Runs in a peer thread.
@@ -311,31 +399,61 @@ public class Wallet implements Serializable {
* Handle when a transaction becomes newly active on the best chain, either due to receiving a new block or a
* re-org making inactive transactions active.
*/
private void processTxFromBestChain(Transaction tx) throws VerificationException {
private void processTxFromBestChain(Transaction tx) throws VerificationException, ScriptException {
// This TX may spend our existing outputs even though it was not pending. This can happen in unit
// tests and if keys are moved between wallets.
updateForSpends(tx);
// tests, if keys are moved between wallets, and if we're catching up to the chain given only a set of keys.
updateForSpends(tx, true);
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
// It's sending us coins.
log.info(" new tx ->unspent");
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "TX was received twice";
} else {
} else if (!tx.getValueSentFromMe(this).equals(BigInteger.ZERO)) {
// It spent some of our coins and did not send us any.
log.info(" new tx ->spent");
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
assert !alreadyPresent : "TX was received twice";
} else {
// It didn't send us coins nor spend any of our coins. If we're processing it, that must be because it
// spends outpoints that are also spent by some pending transactions - maybe a double spend of somebody
// elses coins that were originally sent to us? ie, this might be a Finney attack where we think we
// received some money and then the sender co-operated with a miner to take back the coins, using a tx
// that isn't involving our keys at all.
Transaction doubleSpend = findDoubleSpendAgainstPending(tx);
if (doubleSpend != null) {
// This is mostly the same as the codepath in updateForSpends, but that one is only triggered when
// the transaction being double spent is actually in our wallet (ie, maybe we're double spending).
log.warn("Saw double spend from chain override pending tx {}", doubleSpend.getHashAsString());
log.warn(" <-pending ->dead");
pending.remove(doubleSpend.getHash());
dead.put(doubleSpend.getHash(), doubleSpend);
// Inform the event listeners of the newly dead tx.
for (WalletEventListener listener : eventListeners) {
synchronized (listener) {
listener.onDeadTransaction(this, doubleSpend, tx);
}
}
}
}
}
/**
* 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.
* Updates the wallet by checking if this TX spends any of our outputs, and marking them as spent if so. It can
* be called in two contexts. One is when we receive a transaction on the best chain but it wasn't pending, this
* most commonly happens when we have a set of keys but the wallet transactions were wiped and we are catching up
* with the block chain. It can also happen if a block includes a transaction we never saw at broadcast time.
* If this tx double spends, it takes precedence over our pending transactions and the pending tx goes dead.
*
* The other context it can be called is from {@link Wallet#receivePending(Transaction)} ie we saw a tx be
* broadcast or one was submitted directly that spends our own coins. If this tx double spends it does NOT take
* precedence because the winner will be resolved by the miners - we assume that our version will win,
* if we are wrong then when a block appears the tx will go dead.
*/
private void updateForSpends(Transaction tx) throws VerificationException {
private void updateForSpends(Transaction tx, boolean fromChain) throws VerificationException {
// tx is on the best chain by this point.
for (TransactionInput input : tx.getInputs()) {
List<TransactionInput> inputs = tx.getInputs();
for (int i = 0; i < inputs.size(); i++) {
TransactionInput input = inputs.get(i);
TransactionInput.ConnectionResult result = input.connect(unspent, false);
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
// Not found in the unspent map. Try again with the spent map.
@@ -347,10 +465,7 @@ public class Wallet implements Serializable {
}
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).
//
// Work backwards like so:
// Double spend! Work backwards like so:
//
// A -> spent by B [pending]
// \-> spent by C [chain]
@@ -362,19 +477,31 @@ public class Wallet implements Serializable {
assert spentBy != null;
Transaction connected = spentBy.getParentTransaction();
assert connected != null;
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(this, connected, tx);
if (fromChain) {
// 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).
if (pending.containsKey(connected.getHash())) {
log.warn("Saw double spend from chain override pending tx {}", connected.getHashAsString());
log.warn(" <-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(this, connected, tx);
}
}
}
} else {
// A pending transaction that tried to double spend our coins - we log and ignore it, because either
// 1) The double-spent tx is confirmed and thus this tx has no effect .... or
// 2) Both txns are pending, neither has priority. Miners will decide in a few minutes which won.
log.warn("Saw double spend from another pending transaction, ignoring tx {}",
tx.getHashAsString());
log.warn(" offending input is input {}", i);
return;
}
} else if (result == TransactionInput.ConnectionResult.SUCCESS) {
// Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet.
@@ -423,19 +550,23 @@ public class Wallet implements Serializable {
}
/**
* Call this when we have successfully transmitted the send tx to the network, to update the wallet.
* Updates the wallet with the given transaction: puts it into the pending pool and sets the spent flags. Used in
* two situations:<p>
*
* <ol>
* <li>When we have just successfully transmitted the tx we created to the network.</li>
* <li>When we receive a pending transaction that didn't appear in the chain yet,
* and we did not create it, and it spends some of our outputs.</li>
* </ol>
*/
synchronized void confirmSend(Transaction tx) {
assert !pending.containsKey(tx.getHash()) : "confirmSend called on the same transaction twice";
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.getInputs()) {
TransactionOutput connectedOutput = input.getOutpoint().getConnectedOutput();
Transaction connectedTx = connectedOutput.parentTransaction;
connectedOutput.markAsSpent(input);
maybeMoveTxToSpent(connectedTx, "spent tx");
}
synchronized void commitTx(Transaction tx) throws VerificationException {
assert !pending.containsKey(tx.getHash()) : "commitTx called on the same transaction twice";
log.info("commitTx of {}", tx.getHashAsString());
tx.updatedAt = Utils.now();
// Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also
// move any transactions that are now fully spent to the spent map so we can skip them when creating future
// spends.
updateForSpends(tx, false);
// Add to the pending pool. It'll be moved out once we receive this transaction on the best chain.
pending.put(tx.getHash(), tx);
}
@@ -505,6 +636,27 @@ public class Wallet implements Serializable {
ALL,
}
EnumSet<Pool> getContainingPools(Transaction tx) {
EnumSet<Pool> result = EnumSet.noneOf(Pool.class);
Sha256Hash txHash = tx.getHash();
if (unspent.containsKey(txHash)) {
result.add(Pool.UNSPENT);
}
if (spent.containsKey(txHash)) {
result.add(Pool.SPENT);
}
if (pending.containsKey(txHash)) {
result.add(Pool.PENDING);
}
if (inactive.containsKey(txHash)) {
result.add(Pool.INACTIVE);
}
if (dead.containsKey(txHash)) {
result.add(Pool.DEAD);
}
return result;
}
int getPoolSize(Pool pool) {
switch (pool) {
case UNSPENT:
@@ -529,7 +681,7 @@ public class Wallet implements Serializable {
* <p/>
* This method is stateless in the sense that calling it twice with the same inputs will result in two
* Transaction objects which are equal. The wallet is not updated to track its pending status or to mark the
* coins as spent until confirmSend is called on the result.
* coins as spent until commitTx is called on the result.
*/
synchronized Transaction createSend(Address address, BigInteger nanocoins) {
// For now let's just pick the first key in our keychain. In future we might want to do something else to
@@ -557,7 +709,11 @@ public class Wallet implements Serializable {
}
// TODO - retry logic
confirmSend(tx);
try {
commitTx(tx);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
}
return tx;
}
@@ -575,7 +731,11 @@ public class Wallet implements Serializable {
if (tx == null) // Not enough money! :-(
return null;
peer.broadcastTransaction(tx);
confirmSend(tx);
try {
commitTx(tx);
} catch (VerificationException e) {
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
}
return tx;
}
@@ -583,7 +743,7 @@ public class Wallet implements Serializable {
* Creates a transaction that sends $coins.$cents BTC to the given address.<p>
* <p/>
* IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions
* that spend the same coins. You have to call confirmSend on the created transaction to prevent this,
* that spend the same coins. You have to call commitTx on the created transaction to prevent this,
* but that should only occur once the transaction has been accepted by the network. This implies you cannot have
* more than one outstanding sending tx at once.
*
@@ -714,8 +874,6 @@ public class Wallet implements Serializable {
AVAILABLE
}
;
/**
* Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this
* means.<p>

View File

@@ -74,8 +74,9 @@ public class DerbyPingService {
// Connect to the localhost node. One minute timeout since we won't try any other peers
System.out.println("Connecting ...");
BlockChain chain = new BlockChain(params, wallet, blockStore);
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
final PeerGroup peerGroup = new PeerGroup(params, chain);
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
peerGroup.addWallet(wallet);
peerGroup.start();
// We want to know when the balance changes.

View File

@@ -81,10 +81,11 @@ public class PingService {
System.out.println("Connecting ...");
BlockChain chain = new BlockChain(params, wallet, blockStore);
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
final PeerGroup peerGroup = new PeerGroup(params, chain);
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
// Download headers only until a day ago.
peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24));
peerGroup.addWallet(wallet);
peerGroup.start();
// We want to know when the balance changes.
@@ -93,13 +94,22 @@ public class PingService {
public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
// Running on a peer thread.
assert !newBalance.equals(BigInteger.ZERO);
BigInteger value = tx.getValueSentToMe(w);
if (tx.isPending()) {
System.out.println("Received pending tx for " + Utils.bitcoinValueToFriendlyString(value));
// Ignore for now, as we won't be allowed to spend until the tx is no longer pending. We'll get
// another callback here when the tx is confirmed.
return;
}
// It's impossible to pick one specific identity that you receive coins from in BitCoin as there
// could be inputs from many addresses. So instead we just pick the first and assume they were all
// owned by the same person.
try {
TransactionInput input = tx.getInputs().get(0);
Address from = input.getFromAddress();
BigInteger value = tx.getValueSentToMe(w);
System.out.println("Received " + Utils.bitcoinValueToFriendlyString(value) + " from " + from.toString());
// Now send the coins back!
Transaction sendTx = w.sendCoins(peerGroup, from, value);

View File

@@ -58,7 +58,7 @@ public class PrivateKeys {
final MemoryBlockStore blockStore = new MemoryBlockStore(params);
BlockChain chain = new BlockChain(params, wallet, blockStore);
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
final PeerGroup peerGroup = new PeerGroup(params, chain);
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
peerGroup.start();
peerGroup.downloadBlockChain();

View File

@@ -38,7 +38,7 @@ public class RefreshWallet {
BlockStore blockStore = new MemoryBlockStore(params);
BlockChain chain = new BlockChain(params, wallet, blockStore);
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
final PeerGroup peerGroup = new PeerGroup(params, chain);
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
peerGroup.start();

View File

@@ -129,7 +129,7 @@ public class ChainSplitTests {
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);
wallet.commitTx(spend);
// Waiting for confirmation ...
assertEquals(BigInteger.ZERO, wallet.getBalance());
Block b2 = b1.createNextBlock(someOtherGuy);
@@ -198,7 +198,7 @@ public class ChainSplitTests {
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);
wallet.commitTx(t1);
// Receive t1 as confirmed by the network.
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
b2.addTransaction(t1);
@@ -240,7 +240,7 @@ public class ChainSplitTests {
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);
wallet.commitTx(t1);
// t1 is still pending ...
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
chain.add(b2);

View File

@@ -23,6 +23,7 @@ import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
@@ -33,7 +34,6 @@ import static org.junit.Assert.*;
public class PeerGroupTest extends TestWithNetworkConnections {
static final NetworkParameters params = NetworkParameters.unitTests();
private Wallet wallet;
private PeerGroup peerGroup;
private final BlockingQueue<Peer> disconnectedPeers = new LinkedBlockingQueue<Peer>();
@@ -41,10 +41,10 @@ public class PeerGroupTest extends TestWithNetworkConnections {
@Before
public void setUp() throws Exception {
super.setUp();
wallet = new Wallet(params);
blockStore = new MemoryBlockStore(params);
BlockChain chain = new BlockChain(params, wallet, blockStore);
peerGroup = new PeerGroup(blockStore, params, chain, 1000);
peerGroup = new PeerGroup(params, chain, 1000);
// Support for testing disconnect events in a non-racy manner.
peerGroup.addEventListener(new AbstractPeerEventListener() {
@@ -95,6 +95,33 @@ public class PeerGroupTest extends TestWithNetworkConnections {
peerGroup.stop();
}
@Test
public void receiveTxBroadcast() throws Exception {
// Check that when we receive transactions on all our peers, we do the right thing.
// Create a couple of peers.
peerGroup.addWallet(wallet);
MockNetworkConnection n1 = createMockNetworkConnection();
Peer p1 = new Peer(params, blockChain, n1);
MockNetworkConnection n2 = createMockNetworkConnection();
Peer p2 = new Peer(params, blockChain, n2);
peerGroup.start();
peerGroup.addPeer(p1);
peerGroup.addPeer(p2);
BigInteger value = Utils.toNanoCoins(1, 0);
Transaction t1 = TestUtils.createFakeTx(unitTestParams, value, address);
InventoryMessage inv = new InventoryMessage(unitTestParams);
inv.addItem(new InventoryItem(InventoryItem.Type.Transaction, t1.getHash()));
n1.inbound(inv);
n2.inbound(inv);
GetDataMessage getdata = (GetDataMessage) n1.outbound();
assertNull(n2.outbound()); // Only one peer is used to download.
n1.inbound(t1);
n1.outbound(); // Wait for processing to complete.
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
@Test
public void singleDownloadPeer1() throws Exception {
// Check that we don't attempt to retrieve blocks on multiple peers.

View File

@@ -20,6 +20,7 @@ import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
@@ -38,6 +39,7 @@ public class PeerTest extends TestWithNetworkConnections {
conn = createMockNetworkConnection();
peer = new Peer(unitTestParams, blockChain, conn);
peer.addWallet(wallet);
}
@Test
@@ -139,6 +141,26 @@ public class PeerTest extends TestWithNetworkConnections {
assertNull(message != null ? message.toString() : "", message);
}
@Test
public void invDownloadTx() throws Exception {
peer.setDownloadData(true);
// Make a transaction and tell the peer we have it.;
BigInteger value = Utils.toNanoCoins(1, 0);
Transaction tx = TestUtils.createFakeTx(unitTestParams, value, address);
InventoryMessage inv = new InventoryMessage(unitTestParams);
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
inv.addItem(item);
conn.inbound(inv);
// Peer hasn't seen it before, so will ask for it.
runPeer(peer, conn);
GetDataMessage message = (GetDataMessage) conn.popOutbound();
assertEquals(1, message.getItems().size());
assertEquals(tx.getHash(), message.getItems().get(0).hash);
conn.inbound(tx);
runPeer(peer, conn);
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
}
// Check that inventory message containing blocks we want is processed correctly.
@Test
public void newBlock() throws Exception {
@@ -148,7 +170,6 @@ public class PeerTest extends TestWithNetworkConnections {
Block b1 = TestUtils.createFakeBlock(unitTestParams, blockStore).block;
blockChain.add(b1);
Block b2 = TestUtils.makeSolvedTestBlock(unitTestParams, b1);
Block b3 = TestUtils.makeSolvedTestBlock(unitTestParams, b2);
conn.setVersionMessageForHeight(unitTestParams, 100);
// Receive notification of a new block.
InventoryMessage inv = new InventoryMessage(unitTestParams);
@@ -158,6 +179,8 @@ public class PeerTest extends TestWithNetworkConnections {
// Response to the getdata message.
conn.inbound(b2);
expect(listener.onPreMessageReceived(eq(peer), eq(inv))).andReturn(inv);
expect(listener.onPreMessageReceived(eq(peer), eq(b2))).andReturn(b2);
listener.onBlocksDownloaded(eq(peer), anyObject(Block.class), eq(98));
expectLastCall();

View File

@@ -17,6 +17,7 @@
package com.google.bitcoin.core;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.BriefLogFormatter;
import org.easymock.IMocksControl;
import java.io.IOException;
@@ -31,14 +32,23 @@ public class TestWithNetworkConnections {
protected NetworkParameters unitTestParams;
protected MemoryBlockStore blockStore;
protected BlockChain blockChain;
protected Wallet wallet;
protected ECKey key;
protected Address address;
public void setUp() throws Exception {
BriefLogFormatter.init();
control = createStrictControl();
control.checkOrder(true);
unitTestParams = NetworkParameters.unitTests();
blockStore = new MemoryBlockStore(unitTestParams);
blockChain = new BlockChain(unitTestParams, new Wallet(unitTestParams), blockStore);
wallet = new Wallet(unitTestParams);
key = new ECKey();
address = key.toAddress(unitTestParams);
wallet.addKey(key);
blockChain = new BlockChain(unitTestParams, wallet, blockStore);
}
protected MockNetworkConnection createMockNetworkConnection() {

View File

@@ -16,19 +16,23 @@
package com.google.bitcoin.core;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.MemoryBlockStore;
import org.junit.Before;
import org.junit.Test;
import java.math.BigInteger;
import java.util.List;
import static com.google.bitcoin.core.TestUtils.createFakeBlock;
import static com.google.bitcoin.core.TestUtils.createFakeTx;
import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
import static com.google.bitcoin.core.Utils.toNanoCoins;
import static org.junit.Assert.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.math.BigInteger;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.MemoryBlockStore;
import com.google.bitcoin.utils.BriefLogFormatter;
public class WalletTest {
static final NetworkParameters params = NetworkParameters.unitTests();
@@ -45,6 +49,8 @@ public class WalletTest {
wallet = new Wallet(params);
wallet.addKey(myKey);
blockStore = new MemoryBlockStore(params);
BriefLogFormatter.init();
}
@Test
@@ -53,7 +59,7 @@ public class WalletTest {
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(params, v1, myAddress);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
@@ -70,7 +76,7 @@ public class WalletTest {
// We have NOT proven that the signature is correct!
wallet.confirmSend(t2);
wallet.commitTx(t2);
assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING));
assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT));
assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL));
@@ -82,14 +88,14 @@ public class WalletTest {
BigInteger v1 = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(params, v1, myAddress);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(v1, wallet.getBalance());
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
BigInteger v2 = toNanoCoins(0, 50);
Transaction t2 = createFakeTx(params, v2, myAddress);
wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
wallet.receiveFromBlock(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
assertEquals(1, wallet.getPoolSize(Wallet.Pool.INACTIVE));
assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL));
@@ -110,7 +116,7 @@ public class WalletTest {
}
};
wallet.addEventListener(listener);
wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
assertTrue(didRun[0]);
}
@@ -125,19 +131,16 @@ public class WalletTest {
StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock;
BigInteger expected = toNanoCoins(5, 50);
assertEquals(0, wallet.getPoolSize(Wallet.Pool.ALL));
wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(2, wallet.getPoolSize(Wallet.Pool.UNSPENT));
assertEquals(expected, wallet.getBalance());
// Now spend one coin.
BigInteger v3 = toNanoCoins(1, 0);
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
wallet.confirmSend(spend);
// TODO: This test depends on the coin selection which is non-deterministic. FIX!
// assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
// assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT));
wallet.commitTx(spend);
assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING));
// Available and estimated balances should not be the same. We don't check the exact available balance here
@@ -148,7 +151,7 @@ public class WalletTest {
// Now confirm the transaction by including it into a block.
StoredBlock b3 = createFakeBlock(params, blockStore, spend).storedBlock;
wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(spend, b3, BlockChain.NewBlockType.BEST_CHAIN);
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
BigInteger v4 = toNanoCoins(4, 50);
@@ -164,21 +167,21 @@ public class WalletTest {
public void blockChainCatchup() throws Exception {
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
// Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we
// want to get back to our previous state. We can do this by just not confirming the transaction as
// createSend is stateless.
StoredBlock b2 = createFakeBlock(params, blockStore, send1).storedBlock;
wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90");
// And we do it again after the catchup.
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
// What we'd really like to do is prove the official client would accept it .... no such luck unfortunately.
wallet.confirmSend(send2);
wallet.commitTx(send2);
StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock;
wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80");
}
@@ -186,7 +189,7 @@ public class WalletTest {
public void balances() throws Exception {
BigInteger nanos = Utils.toNanoCoins(1, 0);
Transaction tx1 = createFakeTx(params, nanos, myAddress);
wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(nanos, tx1.getValueSentToMe(wallet, true));
// Send 0.10 to somebody else.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
@@ -206,7 +209,7 @@ public class WalletTest {
// Note that tx is no longer valid: it spends more than it imports. However checking transactions balance
// correctly isn't possible in SPV mode because value is a property of outputs not inputs. Without all
// transactions you can't check they add up.
wallet.receive(tx, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN);
// Now the other guy creates a transaction which spends that change.
Transaction tx2 = new Transaction(params);
tx2.addInput(output);
@@ -223,20 +226,20 @@ public class WalletTest {
BigInteger coinHalf = Utils.toNanoCoins(0, 50);
// Start by giving us 1 coin.
Transaction inbound1 = createFakeTx(params, coin1, myAddress);
wallet.receive(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
// Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is
// in the unspent pool, not pending or spent.
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
Address someOtherGuy = new ECKey().toAddress(params);
Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf);
wallet.confirmSend(outbound1);
wallet.receive(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.commitTx(outbound1);
wallet.receiveFromBlock(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
// That other guy gives us the coins right back.
Transaction inbound2 = new Transaction(params);
inbound2.addOutput(new TransactionOutput(params, inbound2, coinHalf, myAddress));
inbound2.addInput(outbound1.getOutputs().get(0));
wallet.receive(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(coin1, wallet.getBalance());
}
@@ -266,19 +269,124 @@ public class WalletTest {
// Receive 1 BTC.
BigInteger nanos = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(params, nanos, myAddress);
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
// Create a send to a merchant.
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
// Create a double spend.
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
// Broadcast send1.
wallet.confirmSend(send1);
wallet.commitTx(send1);
// Receive a block that overrides it.
wallet.receive(send2, null, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(send2, null, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]);
}
@Test
public void pending1() throws Exception {
// Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.
final BigInteger nanos = Utils.toNanoCoins(1, 0);
final Transaction t1 = createFakeTx(params, nanos, myAddress);
// First one is "called" second is "pending".
final boolean[] flags = new boolean[2];
wallet.addEventListener(new AbstractWalletEventListener() {
@Override
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
// Check we got the expected transaction.
assertEquals(tx, t1);
// Check that it's considered to be pending inclusion in the block chain.
assertEquals(prevBalance, BigInteger.ZERO);
assertEquals(newBalance, nanos);
flags[0] = true;
flags[1] = tx.isPending();
}
});
wallet.receivePending(t1);
assertTrue(flags[0]);
assertTrue(flags[1]); // is pending
flags[0] = false;
// Check we don't get notified if we receive it again.
wallet.receivePending(t1);
assertFalse(flags[0]);
// Now check again when we receive it via a block.
flags[1] = true;
wallet.receiveFromBlock(t1, createFakeBlock(params, blockStore, t1).storedBlock,
BlockChain.NewBlockType.BEST_CHAIN);
assertTrue(flags[0]);
assertFalse(flags[1]); // is not pending
// Check we don't get notified about an irrelevant transaction.
flags[0] = false;
flags[1] = false;
Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params));
wallet.receivePending(irrelevant);
assertFalse(flags[0]);
}
@Test
public void pending2() throws Exception {
// Check that if we receive a pending tx we did not send, it updates our spent flags correctly.
// Receive some coins.
BigInteger nanos = Utils.toNanoCoins(1, 0);
Transaction t1 = createFakeTx(params, nanos, myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock;
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(nanos, wallet.getBalance());
// Create a spend with them, but don't commit it (ie it's from somewhere else but using our keys).
Transaction t2 = wallet.createSend(new ECKey().toAddress(params), nanos);
// Now receive it as pending.
wallet.receivePending(t2);
// Our balance is now zero.
assertEquals(BigInteger.ZERO, wallet.getBalance());
}
@Test
public void pending3() throws Exception {
// Check that if we receive a pending tx, and it's overridden by a double spend from the main chain, we
// are notified that it's dead. This should work even if the pending tx inputs are NOT ours, ie, they don't
// connect to anything.
BigInteger nanos = Utils.toNanoCoins(1, 0);
// Create two transactions that share the same input tx.
Address badGuy = new ECKey().toAddress(params);
Transaction doubleSpentTx = new Transaction(params);
TransactionOutput doubleSpentOut = new TransactionOutput(params, doubleSpentTx, nanos, badGuy);
doubleSpentTx.addOutput(doubleSpentOut);
Transaction t1 = new Transaction(params);
TransactionOutput o1 = new TransactionOutput(params, t1, nanos, myAddress);
t1.addOutput(o1);
t1.addInput(doubleSpentOut);
Transaction t2 = new Transaction(params);
TransactionOutput o2 = new TransactionOutput(params, t2, nanos, badGuy);
t2.addOutput(o2);
t2.addInput(doubleSpentOut);
final Transaction[] called = new Transaction[2];
wallet.addEventListener(new AbstractWalletEventListener() {
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
called[0] = tx;
}
public void onDeadTransaction(Wallet wallet, Transaction deadTx, Transaction replacementTx) {
called[0] = deadTx;
called[1] = replacementTx;
}
});
assertEquals(BigInteger.ZERO, wallet.getBalance());
wallet.receivePending(t1);
assertEquals(t1, called[0]);
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
// Now receive a double spend on the main chain.
called[0] = called[1] = null;
wallet.receiveFromBlock(t2, createFakeBlock(params, blockStore, t2).storedBlock, BlockChain.NewBlockType.BEST_CHAIN);
assertEquals(BigInteger.ZERO, wallet.getBalance());
assertEquals(t1, called[0]); // dead
assertEquals(t2, called[1]); // replacement
}
@Test
public void transactionsList() throws Exception {
// Check the wallet can give us an ordered list of all received transactions.
@@ -286,12 +394,12 @@ public class WalletTest {
// Receive a coin.
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
// Receive half a coin 10 minutes later.
Utils.rollMockClock(60 * 10);
Transaction tx2 = createFakeTx(params, Utils.toNanoCoins(0, 5), myAddress);
StoredBlock b2 = createFakeBlock(params, blockStore, tx1).storedBlock;
wallet.receive(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN);
wallet.receiveFromBlock(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN);
// Check we got them back in order.
List<Transaction> transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
@@ -307,7 +415,7 @@ public class WalletTest {
Transaction tx3 = wallet.createSend(new ECKey().toAddress(params), Utils.toNanoCoins(0, 5));
// Does not appear in list yet.
assertEquals(2, wallet.getTransactionsByTime().size());
wallet.confirmSend(tx3);
wallet.commitTx(tx3);
// Now it does.
transactions = wallet.getTransactionsByTime();
assertEquals(3, transactions.size());