mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-01-31 07:12:17 +00:00
Second part of supporting block chain re-orgs. Wallet now organizes transactions into four pools: unspent, spent, pending and inactive. Transactions track which blocks they have appeared in. BlockChain provides the Wallet with both segments of chain and it then moves transactions between the pools as necessary.
Activated the disabled tests in testForking, added a bunch more tests for this functionality. Added various utility methods and made small fixes as required. This changes the wallet format and invalidates existing wallets.
This commit is contained in:
parent
86d7b15f8c
commit
c20118d871
@ -294,7 +294,7 @@ public class Block extends Message {
|
||||
ArrayList<byte[]> tree = new ArrayList<byte[]>();
|
||||
// Start by adding all the hashes of the transactions as leaves of the tree.
|
||||
for (Transaction t : transactions) {
|
||||
tree.add(t.getHash());
|
||||
tree.add(t.getHash().hash);
|
||||
}
|
||||
int j = 0;
|
||||
// Now step through each level ...
|
||||
@ -453,7 +453,7 @@ public class Block extends Message {
|
||||
// Real coinbase transactions use <pubkey> OP_CHECKSIG rather than a send to an address though there's
|
||||
// nothing in the system that enforces that and both are just as valid.
|
||||
coinbase.inputs.add(new TransactionInput(params, new byte[] { (byte) coinbaseCounter++ } ));
|
||||
coinbase.outputs.add(new TransactionOutput(params, Utils.toNanoCoins(50, 0), to));
|
||||
coinbase.outputs.add(new TransactionOutput(params, Utils.toNanoCoins(50, 0), to, coinbase));
|
||||
transactions.add(coinbase);
|
||||
}
|
||||
|
||||
|
@ -17,9 +17,7 @@
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.bitcoin.core.Utils.LOG;
|
||||
|
||||
@ -105,23 +103,24 @@ public class BlockChain {
|
||||
|
||||
private synchronized boolean add(Block block, boolean tryConnecting)
|
||||
throws BlockStoreException, VerificationException, ScriptException {
|
||||
try {
|
||||
LOG("Adding block " + block.getHashAsString() + " to the chain");
|
||||
if (blockStore.get(block.getHash()) != null) {
|
||||
LOG("Already have block");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Prove the block is internally valid: hash is lower than target, merkle root is correct and so on.
|
||||
try {
|
||||
block.verify();
|
||||
} catch (VerificationException e) {
|
||||
LOG("Failed to verify block: " + e.toString());
|
||||
LOG(block.toString());
|
||||
throw e;
|
||||
}
|
||||
// Inform the wallet about transactions relevant to our keys, then throw away the transaction data.
|
||||
extractRelevantTransactions(block);
|
||||
assert block.transactions == null;
|
||||
|
||||
if (blockStore.get(block.getHash()) != null) {
|
||||
LOG("Already have block");
|
||||
return true;
|
||||
}
|
||||
// Try linking it to a place in the currently known blocks.
|
||||
StoredBlock storedPrev = blockStore.get(block.getPrevBlockHash());
|
||||
|
||||
if (storedPrev == null) {
|
||||
// We can't find the previous block. Probably we are still in the process of downloading the chain and a
|
||||
// block was solved whilst we were doing it. We put it to one side and try to connect it later when we
|
||||
@ -131,8 +130,15 @@ public class BlockChain {
|
||||
return false;
|
||||
} else {
|
||||
// It connects to somewhere on the chain. Not necessarily the top of the best known chain.
|
||||
//
|
||||
// Create a new StoredBlock from this block. It will throw away the transaction data so when block goes
|
||||
// out of scope we will reclaim the used memory.
|
||||
checkDifficultyTransitions(storedPrev, block);
|
||||
connectAndStoreBlock(block, storedPrev);
|
||||
StoredBlock newStoredBlock = storedPrev.build(block);
|
||||
blockStore.put(newStoredBlock);
|
||||
// block.transactions may be null here if we received only a header and not a full block. This does not
|
||||
// happen currently but might in future if getheaders is implemented.
|
||||
connectBlock(newStoredBlock, storedPrev, block.transactions);
|
||||
}
|
||||
|
||||
if (tryConnecting)
|
||||
@ -141,35 +147,123 @@ public class BlockChain {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void connectAndStoreBlock(Block block, StoredBlock storedPrev) throws BlockStoreException, VerificationException {
|
||||
StoredBlock newStoredBlock = storedPrev.build(block);
|
||||
blockStore.put(newStoredBlock);
|
||||
private void connectBlock(StoredBlock newStoredBlock, StoredBlock storedPrev, List<Transaction> newTransactions)
|
||||
throws BlockStoreException, VerificationException {
|
||||
if (storedPrev.equals(chainHead)) {
|
||||
// This block connects to the best known block, it is a normal continuation of the system.
|
||||
setChainHead(newStoredBlock);
|
||||
LOG("Received block " + block.getHashAsString() + ", chain is now " + chainHead.getHeight() +
|
||||
" blocks high");
|
||||
LOG("Chain is now " + chainHead.getHeight() + " blocks high");
|
||||
if (newTransactions != null)
|
||||
sendTransactionsToWallet(newStoredBlock, NewBlockType.BEST_CHAIN, newTransactions);
|
||||
} else {
|
||||
// This block connects to somewhere other than the top of the chain.
|
||||
if (newStoredBlock.moreWorkThan(chainHead)) {
|
||||
// This chain has overtaken the one we currently believe is best. Reorganize is required.
|
||||
wallet.reorganize(chainHead, newStoredBlock);
|
||||
// Update the pointer to the best known block.
|
||||
setChainHead(newStoredBlock);
|
||||
// This block connects to somewhere other than the top of the best known chain. We treat these differently.
|
||||
//
|
||||
// Note that we send the transactions to the wallet FIRST, even if we're about to re-organize this block
|
||||
// to become the new best chain head. This simplifies handling of the re-org in the Wallet class.
|
||||
boolean causedSplit = newStoredBlock.moreWorkThan(chainHead);
|
||||
if (causedSplit) {
|
||||
LOG("Block is causing a re-organize");
|
||||
} else {
|
||||
LOG("Received a block which forks the chain, but it did not cause a reorganize.");
|
||||
LOG("Block forks the chain, but it did not cause a reorganize.");
|
||||
}
|
||||
|
||||
// We may not have any transactions if we received only a header. That never happens today but will in
|
||||
// future when getheaders is used as an optimization.
|
||||
if (newTransactions != null) {
|
||||
sendTransactionsToWallet(newStoredBlock, NewBlockType.SIDE_CHAIN, newTransactions);
|
||||
}
|
||||
|
||||
if (causedSplit)
|
||||
handleChainSplit(newStoredBlock);
|
||||
}
|
||||
}
|
||||
|
||||
private void extractRelevantTransactions(Block block) throws VerificationException {
|
||||
// If this block is a full block, scan, otherwise it's just headers (eg from getheaders or a unit test).
|
||||
if (block.transactions != null) {
|
||||
// Scan the transactions to find out if any sent money to us. We don't care about the rest.
|
||||
// TODO: We should also scan to see if any of our own keys sent money to somebody else and became spent.
|
||||
for (Transaction tx : block.transactions) {
|
||||
/**
|
||||
* Called as part of connecting a block when the new block results in a different chain having higher total work.
|
||||
*/
|
||||
private void handleChainSplit(StoredBlock newChainHead) throws BlockStoreException, VerificationException {
|
||||
// This chain has overtaken the one we currently believe is best. Reorganize is required.
|
||||
//
|
||||
// Firstly, calculate the block at which the chain diverged. We only need to examine the
|
||||
// chain from beyond this block to find differences.
|
||||
StoredBlock splitPoint = findSplit(newChainHead, chainHead);
|
||||
LOG("Re-organize after split at height " + splitPoint.getHeight());
|
||||
LOG("Old chain head: " + chainHead.getHeader().getHashAsString());
|
||||
LOG("New chain head: " + newChainHead.getHeader().getHashAsString());
|
||||
LOG("Split at block: " + splitPoint.getHeader().getHashAsString());
|
||||
// Then build a list of all blocks in the old part of the chain and the new part.
|
||||
Set<StoredBlock> oldBlocks = getPartialChain(chainHead, splitPoint);
|
||||
Set<StoredBlock> newBlocks = getPartialChain(newChainHead, splitPoint);
|
||||
// Now inform the wallet. This is necessary so the set of currently active transactions (that we can spend)
|
||||
// can be updated to take into account the re-organize. We might also have received new coins we didn't have
|
||||
// before and our previous spends might have been undone.
|
||||
wallet.reorganize(oldBlocks, newBlocks);
|
||||
// Update the pointer to the best known block.
|
||||
setChainHead(newChainHead);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
assert higher.getHeight() > lower.getHeight();
|
||||
Set<StoredBlock> results = new HashSet<StoredBlock>();
|
||||
StoredBlock cursor = higher;
|
||||
while (true) {
|
||||
results.add(cursor);
|
||||
cursor = cursor.getPrev(blockStore);
|
||||
assert cursor != null : "Ran off the end of the chain";
|
||||
if (cursor.equals(lower)) break;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locates the point in the chain at which newStoredBlock and chainHead diverge. Returns null if no split point was
|
||||
* found (ie they are part of the same chain).
|
||||
*/
|
||||
private StoredBlock findSplit(StoredBlock newChainHead, StoredBlock chainHead) throws BlockStoreException {
|
||||
StoredBlock currentChainCursor = chainHead;
|
||||
StoredBlock newChainCursor = newChainHead;
|
||||
// Loop until we find the block both chains have in common. Example:
|
||||
//
|
||||
// A -> B -> C -> D
|
||||
// \--> E -> F -> G
|
||||
//
|
||||
// findSplit will return block B. chainHead = D and newChainHead = G.
|
||||
while (!currentChainCursor.equals(newChainCursor)) {
|
||||
// Move the new chain cursor backwards until it is at the same height as the current chain head.
|
||||
while (newChainCursor.getHeight() > currentChainCursor.getHeight()) {
|
||||
newChainCursor = blockStore.get(newChainCursor.getHeader().getPrevBlockHash());
|
||||
// Stores contain the genesis block which has a height of zero. Thus we should always be able to find
|
||||
// a block of equal height in this loop. If we fall off the end of the chain it means there is a bug
|
||||
// in the library.
|
||||
assert newChainCursor != null : "Attempt to follow an orphan chain";
|
||||
}
|
||||
// We found a place where the chains have equal height. In the first iteration with the above example
|
||||
// newChainCursor will be F. Did we find the fork yet?
|
||||
if (newChainCursor.equals(currentChainCursor))
|
||||
break;
|
||||
// No, we did not. First iteration D != F so move currentChainCursor backwards one and try again.
|
||||
currentChainCursor = blockStore.get(currentChainCursor.getHeader().getPrevBlockHash());
|
||||
assert currentChainCursor != null : "Attempt to follow an orphan chain";
|
||||
// Eventually currentChainCursor will move from C to B, the inner while loop will move newChainCursor
|
||||
// from E to B and we will exit.
|
||||
}
|
||||
return currentChainCursor;
|
||||
}
|
||||
|
||||
enum NewBlockType {
|
||||
BEST_CHAIN,
|
||||
SIDE_CHAIN
|
||||
}
|
||||
|
||||
private void sendTransactionsToWallet(StoredBlock block, NewBlockType blockType,
|
||||
List<Transaction> newTransactions) throws VerificationException {
|
||||
// Scan the transactions to find out if any mention addresses we own.
|
||||
for (Transaction tx : newTransactions) {
|
||||
try {
|
||||
scanTransaction(tx);
|
||||
scanTransaction(block, tx, 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 not scanned here and continue.
|
||||
@ -177,11 +271,6 @@ public class BlockChain {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Throw away the transactions. We have to do this because we can't hold all the transaction data for the
|
||||
// production chain in memory at once. Because BitCoinJ implements client mode/simplified payment
|
||||
// verification we don't store the transactions to disk or use them later anyway.
|
||||
block.transactions = null;
|
||||
}
|
||||
|
||||
private void setChainHead(StoredBlock chainHead) {
|
||||
this.chainHead = chainHead;
|
||||
@ -280,41 +369,31 @@ public class BlockChain {
|
||||
receivedDifficulty.toString(16) + " vs " + newDifficulty.toString(16));
|
||||
}
|
||||
|
||||
private void scanTransaction(Transaction tx) throws ScriptException, VerificationException {
|
||||
for (TransactionOutput i : tx.outputs) {
|
||||
private void scanTransaction(StoredBlock block, Transaction tx, NewBlockType blockType)
|
||||
throws ScriptException, VerificationException {
|
||||
boolean shouldReceive = false;
|
||||
for (TransactionOutput output : tx.outputs) {
|
||||
// TODO: Handle more types of outputs, not just regular to address outputs.
|
||||
if (i.getScriptPubKey().isSentToIP()) return;
|
||||
byte[] pubKeyHash;
|
||||
pubKeyHash = i.getScriptPubKey().getPubKeyHash();
|
||||
synchronized (wallet) {
|
||||
for (ECKey key : wallet.keychain) {
|
||||
if (Arrays.equals(pubKeyHash, key.getPubKeyHash())) {
|
||||
// We found a transaction that sends us money.
|
||||
if (!wallet.isTransactionPresent(tx)) {
|
||||
wallet.receive(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (output.getScriptPubKey().isSentToIP()) return;
|
||||
// This is not thread safe as a key could be removed between the call to isMine and receive.
|
||||
if (output.isMine(wallet)) {
|
||||
shouldReceive = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air),
|
||||
// so we can stop scanning at this point.
|
||||
if (tx.isCoinBase()) return;
|
||||
|
||||
// Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air).
|
||||
if (!tx.isCoinBase()) {
|
||||
for (TransactionInput i : tx.inputs) {
|
||||
byte[] pubkey = i.getScriptSig().getPubKey();
|
||||
synchronized (wallet) {
|
||||
for (ECKey key : wallet.keychain) {
|
||||
if (Arrays.equals(pubkey, key.getPubKey())) {
|
||||
// We found a transaction where we spent money.
|
||||
if (wallet.isTransactionPresent(tx)) {
|
||||
// TODO: Implement catching up with a set of pre-generated keys using the blockchain.
|
||||
}
|
||||
}
|
||||
// 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 (shouldReceive)
|
||||
wallet.receive(tx, block, blockType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,7 +36,6 @@ public class StoredBlock implements Serializable {
|
||||
private int height;
|
||||
|
||||
public StoredBlock(Block header, BigInteger chainWork, int height) {
|
||||
assert header.transactions == null : "Should not have transactions in a block header object";
|
||||
this.header = header;
|
||||
this.chainWork = chainWork;
|
||||
this.height = height;
|
||||
@ -93,6 +92,21 @@ public class StoredBlock implements Serializable {
|
||||
// the largest amount of work done not the tallest.
|
||||
BigInteger chainWork = this.chainWork.add(block.getWork());
|
||||
int height = this.height + 1;
|
||||
return new StoredBlock(block, chainWork, height);
|
||||
return new StoredBlock(block.cloneAsHeader(), chainWork, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a block store, looks up the previous block in this chain. Convenience method for doing
|
||||
* <tt>store.get(this.getHeader().getPrevBlockHash())</tt>.
|
||||
*
|
||||
* @return the previous block in the chain or null if it was not found in the store.
|
||||
*/
|
||||
public StoredBlock getPrev(BlockStore store) throws BlockStoreException {
|
||||
return store.get(getHeader().getPrevBlockHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Block at height " + getHeight() + ": " + getHeader().toString();
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +21,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.bitcoin.core.Utils.*;
|
||||
|
||||
@ -46,14 +43,24 @@ public class Transaction extends Message implements Serializable {
|
||||
ArrayList<TransactionOutput> outputs;
|
||||
long lockTime;
|
||||
|
||||
// This is only stored in Java serialization. It records which blocks (and their height + work) the transaction
|
||||
// has been included in. For most transactions this set will have a single member. In the case of a chain split a
|
||||
// transaction may appear in multiple blocks but only one of them is part of the best chain. It's not valid to
|
||||
// have an identical transaction appear in two blocks in the same chain but this invariant is expensive to check,
|
||||
// so it's not directly enforced anywhere.
|
||||
//
|
||||
// If this transaction is not stored in the wallet, appearsIn is null.
|
||||
Set<StoredBlock> appearsIn;
|
||||
|
||||
// This is an in memory helper only.
|
||||
transient byte[] hash;
|
||||
transient Sha256Hash hash;
|
||||
|
||||
Transaction(NetworkParameters params) {
|
||||
super(params);
|
||||
version = 1;
|
||||
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.
|
||||
}
|
||||
|
||||
/**
|
||||
@ -81,37 +88,64 @@ public class Transaction extends Message implements Serializable {
|
||||
/**
|
||||
* Returns the transaction hash as you see them in the block explorer.
|
||||
*/
|
||||
public byte[] getHash() {
|
||||
public Sha256Hash getHash() {
|
||||
if (hash == null) {
|
||||
byte[] bits = bitcoinSerialize();
|
||||
hash = reverseBytes(doubleDigest(bits));
|
||||
hash = new Sha256Hash(reverseBytes(doubleDigest(bits)));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
public String getHashAsString() {
|
||||
return Utils.bytesToHexString(getHash());
|
||||
return getHash().toString();
|
||||
}
|
||||
|
||||
void setFakeHashForTesting(byte[] hash) {
|
||||
void setFakeHashForTesting(Sha256Hash hash) {
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of the outputs that are sending coins to a key in the wallet.
|
||||
* @return sum in nanocoins
|
||||
* Calculates the sum of the outputs that are sending coins to a key in the wallet. The flag controls whether to
|
||||
* include spent outputs or not.
|
||||
*/
|
||||
public BigInteger getValueSentToMe(Wallet wallet) {
|
||||
BigInteger getValueSentToMe(Wallet wallet, boolean includeSpent) {
|
||||
// This is tested in WalletTest.
|
||||
BigInteger v = BigInteger.ZERO;
|
||||
for (TransactionOutput o : outputs) {
|
||||
if (o.isMine(wallet)) {
|
||||
if (!o.isMine(wallet)) continue;
|
||||
if (!includeSpent && o.isSpent) continue;
|
||||
v = v.add(o.getValue());
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of the outputs that are sending coins to a key in the wallet.
|
||||
*/
|
||||
public BigInteger getValueSentToMe(Wallet wallet) {
|
||||
return getValueSentToMe(wallet, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a set of blocks which contain the transaction, or null if this transaction doesn't have that data
|
||||
* because it's not stored in the wallet or because it has never appeared in a block.
|
||||
*/
|
||||
Set<StoredBlock> getAppearsIn() {
|
||||
return appearsIn;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* block stores do not save the transaction data at all.
|
||||
*/
|
||||
void addBlockAppearance(StoredBlock block) {
|
||||
if (appearsIn == null) {
|
||||
appearsIn = new HashSet<StoredBlock>();
|
||||
}
|
||||
appearsIn.add(block);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the sum of the inputs that are spending coins with keys in the wallet. This requires the
|
||||
* transactions sending coins to those keys to be in the wallet. This method will not attempt to download the
|
||||
@ -123,8 +157,8 @@ public class Transaction extends Message implements Serializable {
|
||||
// This is tested in WalletTest.
|
||||
BigInteger v = BigInteger.ZERO;
|
||||
for (TransactionInput input : inputs) {
|
||||
boolean connected = input.outpoint.connect(wallet.unspent) ||
|
||||
input.outpoint.connect(wallet.fullySpent);
|
||||
boolean connected = input.outpoint.connect(wallet.unspent.values()) ||
|
||||
input.outpoint.connect(wallet.spent.values());
|
||||
if (connected) {
|
||||
// This input is taking value from an transaction in our wallet. To discover the value,
|
||||
// we must find the connected transaction.
|
||||
@ -138,10 +172,9 @@ public class Transaction extends Message implements Serializable {
|
||||
* 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.
|
||||
*
|
||||
* Note: in the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation,
|
||||
* that's kept separate.
|
||||
*
|
||||
* Also note: only SIGHASH_ALL is actually used in the official client today.
|
||||
* In the official client, this enum also has another flag, SIGHASH_ANYONECANPAY. In this implementation,
|
||||
* that's kept separate. Only SIGHASH_ALL is actually used in the official client today. The other flags
|
||||
* exist to allow for distributed contracts.
|
||||
*/
|
||||
public enum SigHash {
|
||||
ALL, // 1
|
||||
@ -170,7 +203,7 @@ public class Transaction extends Message implements Serializable {
|
||||
lockTime = readUint32();
|
||||
|
||||
// Store a hash, it may come in useful later (want to avoid reserialization costs).
|
||||
hash = reverseBytes(doubleDigest(bytes, offset, cursor - offset));
|
||||
hash = new Sha256Hash(reverseBytes(doubleDigest(bytes, offset, cursor - offset)));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -315,7 +348,7 @@ public class Transaction extends Message implements Serializable {
|
||||
// Every input is now complete.
|
||||
}
|
||||
|
||||
private byte[] hashTransactionForSignature( SigHash type, boolean anyoneCanPay) {
|
||||
private byte[] hashTransactionForSignature(SigHash type, boolean anyoneCanPay) {
|
||||
try {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
bitcoinSerializeToStream(bos);
|
||||
@ -377,13 +410,11 @@ public class Transaction extends Message implements Serializable {
|
||||
if (!(other instanceof Transaction)) return false;
|
||||
Transaction t = (Transaction) other;
|
||||
|
||||
byte[] hash1 = t.getHash();
|
||||
byte[] hash2 = getHash();
|
||||
return Arrays.equals(hash2, hash1);
|
||||
return t.getHash().equals(getHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Arrays.hashCode(hash);
|
||||
return Arrays.hashCode(getHash().hash);
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* This message is a reference or pointer to an output of a different transaction.
|
||||
@ -40,7 +40,7 @@ public class TransactionOutPoint extends Message implements Serializable {
|
||||
super(params);
|
||||
this.index = index;
|
||||
if (fromTx != null) {
|
||||
this.hash = fromTx.getHash();
|
||||
this.hash = fromTx.getHash().hash;
|
||||
this.fromTx = fromTx;
|
||||
} else {
|
||||
// This happens when constructing the genesis block.
|
||||
@ -71,9 +71,9 @@ public class TransactionOutPoint extends Message implements Serializable {
|
||||
* getConnectedOutput().
|
||||
* @return true if connection took place, false if the referenced transaction was not in the list.
|
||||
*/
|
||||
boolean connect(List<Transaction> transactions) {
|
||||
boolean connect(Collection<Transaction> transactions) {
|
||||
for (Transaction tx : transactions) {
|
||||
if (Arrays.equals(tx.getHash(), hash)) {
|
||||
if (Arrays.equals(tx.getHash().hash, hash)) {
|
||||
fromTx = tx;
|
||||
return true;
|
||||
}
|
||||
@ -84,7 +84,6 @@ public class TransactionOutPoint extends Message 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.
|
||||
* @return
|
||||
*/
|
||||
TransactionOutput getConnectedOutput() {
|
||||
assert fromTx != null;
|
||||
|
@ -20,7 +20,6 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* A TransactionOutput message contains a scriptPubKey that controls who is able to spend its value. It is a sub-part
|
||||
@ -52,10 +51,11 @@ public class TransactionOutput extends Message implements Serializable {
|
||||
parentTransaction = parent;
|
||||
}
|
||||
|
||||
TransactionOutput(NetworkParameters params, BigInteger value, Address to) {
|
||||
TransactionOutput(NetworkParameters params, BigInteger value, Address to, Transaction parent) {
|
||||
super(params);
|
||||
this.value = value;
|
||||
this.scriptBytes = Script.createOutputScript(to);
|
||||
parentTransaction = parent;
|
||||
}
|
||||
|
||||
/** Used only in creation of the genesis blocks and in unit tests. */
|
||||
@ -112,11 +112,7 @@ public class TransactionOutput extends Message implements Serializable {
|
||||
public boolean isMine(Wallet wallet) {
|
||||
try {
|
||||
byte[] pubkeyHash = getScriptPubKey().getPubKeyHash();
|
||||
for (ECKey key : wallet.keychain) {
|
||||
if (Arrays.equals(key.getPubKeyHash(), pubkeyHash))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return wallet.isPubKeyHashMine(pubkeyHash);
|
||||
} catch (ScriptException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -18,12 +18,10 @@ package com.google.bitcoin.core;
|
||||
|
||||
import java.io.*;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.bitcoin.core.Utils.LOG;
|
||||
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
|
||||
@ -35,21 +33,83 @@ import static com.google.bitcoin.core.Utils.LOG;
|
||||
* pull in a potentially large (code-size) third party serialization library.<p>
|
||||
*/
|
||||
public class Wallet implements Serializable {
|
||||
private static final long serialVersionUID = -4501424466753895784L;
|
||||
private static final long serialVersionUID = 2L;
|
||||
|
||||
// Algorithm for movement of transactions between pools. Outbound tx = us spending coins. Inbound tx = us
|
||||
// receiving coins. If a tx is both inbound and outbound (spend with change) it is considered outbound for the
|
||||
// purposes of the explanation below.
|
||||
//
|
||||
// 1. Outbound tx is created by us: ->pending
|
||||
// 2. Outbound tx that was broadcast is accepted into the main chain:
|
||||
// <-pending and
|
||||
// If there is a change output ->unspent
|
||||
// If there is no change output ->spent
|
||||
// 3. Outbound tx that was broadcast is accepted into a side chain:
|
||||
// ->inactive (remains in pending).
|
||||
// 4. Inbound tx is accepted into the best chain:
|
||||
// ->unspent/spent
|
||||
// 5. Inbound tx is accepted into a side chain:
|
||||
// ->inactive
|
||||
//
|
||||
// Re-orgs:
|
||||
// 1. Tx is present in old chain and not present in new chain
|
||||
// <-unspent/spent ->inactive
|
||||
//
|
||||
// These newly inactive transactions will (if they are relevant to us) eventually come back via receive()
|
||||
// as miners resurrect them and re-include into the new best chain. Until then we do NOT consider them
|
||||
// pending as it's possible some of the transactions have become invalid (eg because the new chain contains
|
||||
// a double spend). This could cause some confusing UI changes for the user but these events should be very
|
||||
// rare.
|
||||
//
|
||||
// 2. Tx is not present in old chain and is present in new chain
|
||||
// <-inactive and ->unspent/spent
|
||||
//
|
||||
// Balance:
|
||||
// 1. Sum up all unspent outputs of the transactions in unspent.
|
||||
// 2. Subtract the inputs of transactions in pending.
|
||||
// 3. In future: re-add the outputs of pending transactions that are mine. Don't do this today because those
|
||||
// change outputs would not be considered spendable.
|
||||
|
||||
/**
|
||||
* A list of transactions with outputs we can spend. Note that some of these transactions may be partially spent,
|
||||
* that is, they have outputs some of which are redeemed and others which aren't already. The spentness of each
|
||||
* output is tracked in the TransactionOutput object. The value of all unspent outputs is the balance of the
|
||||
* wallet.
|
||||
* Map of txhash->Transactions that have not made it into the best chain yet. These transactions inputs count as
|
||||
* spent for the purposes of calculating our balance but their outputs are not available for spending yet. This
|
||||
* means after a spend, our balance can actually go down temporarily before going up again!
|
||||
*/
|
||||
public final ArrayList<Transaction> unspent;
|
||||
final Map<Sha256Hash, Transaction> pending;
|
||||
|
||||
/**
|
||||
* When all the outputs of a transaction are spent, it gets put here. These transactions aren't useful for
|
||||
* anything except record keeping and presentation to the user.
|
||||
* Map of txhash->Transactions where the Transaction has unspent outputs. These are transactions we can use
|
||||
* to pay other people and so count towards our balance. Transactions only appear in this map if they are part
|
||||
* of the best chain. Transactions we have broacast that are not confirmed yet appear in pending even though they
|
||||
* may have unspent "change" outputs.<p>
|
||||
*
|
||||
* Note: for now we will not allow spends of transactions that did not make it into the block chain. The code
|
||||
* that handles this in BitCoin C++ is complicated. Satoshis code will not allow you to spend unconfirmed coins,
|
||||
* however, it does seem to support dependency resolution entirely within the context of the memory pool so
|
||||
* theoretically you could spend zero-conf coins and all of them would be included together. To simplify we'll
|
||||
* make people wait but it would be a good improvement to resolve this in future.
|
||||
*/
|
||||
final LinkedList<Transaction> fullySpent;
|
||||
final Map<Sha256Hash, Transaction> unspent;
|
||||
|
||||
/**
|
||||
* Map of txhash->Transactions where the Transactions outputs are all fully spent. They are kept separately so
|
||||
* the time to create a spend does not grow infinitely as wallets become more used. Some of these transactions
|
||||
* may not have appeared in a block yet if they were created by us to spend coins and that spend is still being
|
||||
* worked on by miners.<p>
|
||||
*
|
||||
* Transactions only appear in this map if they are part of the best chain.
|
||||
*/
|
||||
final Map<Sha256Hash, Transaction> spent;
|
||||
|
||||
/**
|
||||
* An inactive transaction is one that is seen only in a block that is not a part of the best chain. We keep it
|
||||
* around in case a re-org promotes a different chain to be the best. In this case some (not necessarily all)
|
||||
* inactive transactions will be moved out to unspent and spent, and some might be moved in.<p>
|
||||
*
|
||||
* Note that in the case where a transaction appears in both the best chain and a side chain as well, it is not
|
||||
* placed in this map. It's an error for a transaction to be in both the inactive pool and unspent/spent.
|
||||
*/
|
||||
private Map<Sha256Hash, Transaction> inactive;
|
||||
|
||||
/** A list of public/private EC keys owned by this user. */
|
||||
public final ArrayList<ECKey> keychain;
|
||||
@ -65,8 +125,10 @@ public class Wallet implements Serializable {
|
||||
public Wallet(NetworkParameters params) {
|
||||
this.params = params;
|
||||
keychain = new ArrayList<ECKey>();
|
||||
unspent = new ArrayList<Transaction>();
|
||||
fullySpent = new LinkedList<Transaction>();
|
||||
unspent = new HashMap<Sha256Hash, Transaction>();
|
||||
spent = new HashMap<Sha256Hash, Transaction>();
|
||||
inactive = new HashMap<Sha256Hash, Transaction>();
|
||||
pending = new HashMap<Sha256Hash, Transaction>();
|
||||
eventListeners = new ArrayList<WalletEventListener>();
|
||||
}
|
||||
|
||||
@ -105,73 +167,155 @@ public class Wallet implements Serializable {
|
||||
* transaction is logically equal.
|
||||
*/
|
||||
public synchronized boolean isTransactionPresent(Transaction transaction) {
|
||||
for (Transaction tx : unspent) {
|
||||
if (Arrays.equals(tx.getHash(), transaction.getHash())) return true;
|
||||
}
|
||||
for (Transaction tx : fullySpent) {
|
||||
if (Arrays.equals(tx.getHash(), transaction.getHash())) return true;
|
||||
}
|
||||
return false;
|
||||
// TODO: Redefine or delete this method.
|
||||
Sha256Hash hash = transaction.getHash();
|
||||
return unspent.containsKey(hash) || spent.containsKey(hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses,
|
||||
* stores the transaction in the wallet so we can spend it in future. Don't call this on transactions we already
|
||||
* have, for instance because we created them ourselves!
|
||||
* 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>
|
||||
*
|
||||
* 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>
|
||||
*
|
||||
* 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>
|
||||
*
|
||||
* 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) throws VerificationException {
|
||||
synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException {
|
||||
// Runs in a peer thread.
|
||||
BigInteger prevBalance = getBalance();
|
||||
|
||||
// We need to check if this transaction is spending one of our own previous transactions. This allows us to
|
||||
// build up a record of our balance by reading the block chain from scratch. Other than making testing easier
|
||||
// this will be useful if one day we want to support importing keypairs from a wallet.db
|
||||
for (TransactionInput input : tx.inputs) {
|
||||
for (int i = 0; i < unspent.size(); i++) {
|
||||
Transaction t = unspent.get(i);
|
||||
if (!Arrays.equals(input.outpoint.hash, t.getHash())) continue;
|
||||
if (input.outpoint.index > t.outputs.size()) {
|
||||
throw new VerificationException("Invalid tx connection for " +
|
||||
Utils.bytesToHexString(tx.getHash()));
|
||||
Sha256Hash txHash = tx.getHash();
|
||||
|
||||
boolean bestChain = blockType == BlockChain.NewBlockType.BEST_CHAIN;
|
||||
boolean sideChain = blockType == BlockChain.NewBlockType.SIDE_CHAIN;
|
||||
|
||||
BigInteger valueSentFromMe = tx.getValueSentFromMe(this);
|
||||
BigInteger valueSentToMe = tx.getValueSentToMe(this);
|
||||
BigInteger valueDifference = valueSentToMe.subtract(valueSentFromMe);
|
||||
|
||||
LOG("Wallet: Received tx" + (sideChain ? " on a side chain" :"") + " for " +
|
||||
bitcoinValueToFriendlyString(valueDifference) + " BTC");
|
||||
|
||||
// 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.
|
||||
Transaction wtx = null;
|
||||
if ((wtx = pending.remove(txHash)) != null) {
|
||||
LOG(" <-pending");
|
||||
// A transaction we created appeared in a block. Probably this is a spend we broadcast that has been
|
||||
// accepted by the network.
|
||||
//
|
||||
// Mark the tx as appearing in this block so we can find it later after a re-org.
|
||||
wtx.addBlockAppearance(block);
|
||||
if (bestChain) {
|
||||
if (valueSentToMe.equals(BigInteger.ZERO)) {
|
||||
// There were no change transactions so this tx is fully spent.
|
||||
LOG(" ->spent");
|
||||
boolean alreadyPresent = spent.put(wtx.getHash(), wtx) != null;
|
||||
assert !alreadyPresent : "TX in both pending and spent pools";
|
||||
} else {
|
||||
// There was change back to us, or this tx was purely a spend back to ourselves (perhaps for
|
||||
// anonymization purposes).
|
||||
LOG(" ->unspent");
|
||||
boolean alreadyPresent = unspent.put(wtx.getHash(), wtx) != null;
|
||||
assert !alreadyPresent : "TX in both pending and unspent pools";
|
||||
}
|
||||
TransactionOutput linkedOutput = t.outputs.get((int) input.outpoint.index);
|
||||
assert !linkedOutput.isSpent : "Double spend was accepted by network?";
|
||||
LOG("Saw a record of me spending " + Utils.bitcoinValueToFriendlyString(linkedOutput.getValue())
|
||||
+ " BTC");
|
||||
linkedOutput.isSpent = true;
|
||||
// Are all the outputs on this TX that are mine now spent? Note that some of the outputs may not
|
||||
// be mine and thus we don't care about them.
|
||||
int myOutputs = 0;
|
||||
int mySpentOutputs = 0;
|
||||
for (TransactionOutput output : t.outputs) {
|
||||
if (!output.isMine(this)) continue;
|
||||
myOutputs++;
|
||||
if (output.isSpent)
|
||||
mySpentOutputs++;
|
||||
} else if (sideChain) {
|
||||
// The transaction was accepted on an inactive side chain, but not yet by the best chain.
|
||||
LOG(" ->inactive");
|
||||
// It's OK for this to already be in the inactive pool because there can be multiple independent side
|
||||
// chains in which it appears:
|
||||
//
|
||||
// b1 --> b2
|
||||
// \-> b3
|
||||
// \-> b4 (at this point it's already present in 'inactive'
|
||||
boolean alreadyPresent = inactive.put(wtx.getHash(), wtx) != null;
|
||||
if (alreadyPresent)
|
||||
LOG("Saw a transaction be incorporated into multiple independent side chains");
|
||||
// Put it back into the pending pool, because 'pending' means 'waiting to be included in best chain'.
|
||||
pending.put(wtx.getHash(), wtx);
|
||||
}
|
||||
if (myOutputs == mySpentOutputs) {
|
||||
// All the outputs we can claim on this TX are gone now. So remove it from the unspent list
|
||||
// so future transaction processing is faster.
|
||||
unspent.remove(i);
|
||||
i--; // Adjust the counter so we are still in the right place after removal.
|
||||
// Keep around a record of the now useless TX in case we need it in future.
|
||||
fullySpent.add(t);
|
||||
} else {
|
||||
// Mark the tx as appearing in this block so we can find it later after a re-org.
|
||||
tx.addBlockAppearance(block);
|
||||
// This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys
|
||||
// are being shared between different wallets.
|
||||
if (sideChain) {
|
||||
LOG(" ->inactive");
|
||||
inactive.put(tx.getHash(), tx);
|
||||
} else if (bestChain) {
|
||||
processTxFromBestChain(tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
LOG("Received " + Utils.bitcoinValueToFriendlyString(tx.getValueSentToMe(this)));
|
||||
unspent.add(tx);
|
||||
LOG("Balance is now: " + Utils.bitcoinValueToFriendlyString(getBalance()));
|
||||
|
||||
LOG("Balance is now: " + bitcoinValueToFriendlyString(getBalance()));
|
||||
|
||||
// Inform anyone interested that we have new coins. Note: we may be re-entered by the event listener,
|
||||
// so we must not make assumptions about our state after this loop returns! For example,
|
||||
// the balance we just received might already be spent!
|
||||
if (bestChain && valueDifference.compareTo(BigInteger.ZERO) > 0) {
|
||||
for (WalletEventListener l : eventListeners) {
|
||||
synchronized (l) {
|
||||
l.onCoinsReceived(this, tx, prevBalance, getBalance());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// 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);
|
||||
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
|
||||
// It's sending us coins.
|
||||
LOG(" ->unspent");
|
||||
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
|
||||
assert !alreadyPresent : "TX was received twice";
|
||||
} else {
|
||||
// It spent some of our coins and did not send us any.
|
||||
LOG(" ->spent");
|
||||
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
|
||||
assert !alreadyPresent : "TX was received twice";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the wallet by checking if this TX spends any of our unspent outputs. This is not used normally because
|
||||
* when we receive our own spends, we've already marked the outputs as spent previously (during tx creation) so
|
||||
* there's no need to go through and do it again.
|
||||
*/
|
||||
private void updateForSpends(Transaction tx) throws VerificationException {
|
||||
for (TransactionInput input : tx.inputs) {
|
||||
if (input.outpoint.connect(unspent.values())) {
|
||||
TransactionOutput output = input.outpoint.getConnectedOutput();
|
||||
assert !output.isSpent : "Double spend accepted by the network?";
|
||||
LOG(" Saw some of my unspent outputs be spent by someone else who has my keys.");
|
||||
LOG(" Total spent value is " + bitcoinValueToFriendlyString(output.getValue()));
|
||||
output.isSpent = true;
|
||||
Transaction connectedTx = input.outpoint.fromTx;
|
||||
if (connectedTx.getValueSentToMe(this, false).equals(BigInteger.ZERO)) {
|
||||
// There's nothing left I can spend in this transaction.
|
||||
if (unspent.remove(connectedTx.getHash()) != null);
|
||||
LOG(" prevtx <-unspent");
|
||||
spent.put(connectedTx.getHash(), connectedTx);
|
||||
LOG(" prevtx ->spent");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an event listener object. Methods on this object are called when something interesting happens,
|
||||
@ -189,9 +333,7 @@ public class Wallet implements Serializable {
|
||||
* Call this when we have successfully transmitted the send tx to the network, to update the wallet.
|
||||
*/
|
||||
synchronized void confirmSend(Transaction tx) {
|
||||
// This tx is supposed to be fresh, it's an error to confirmSend on a transaction that was already sent.
|
||||
assert !unspent.contains(tx);
|
||||
assert !fullySpent.contains(tx);
|
||||
assert !pending.containsKey(tx) : "confirmSend called on the same transaction twice";
|
||||
// Mark each connected output of the tx as spent, so we don't try and spend it again.
|
||||
for (TransactionInput input : tx.inputs) {
|
||||
TransactionOutput connectedOutput = input.outpoint.getConnectedOutput();
|
||||
@ -199,35 +341,25 @@ public class Wallet implements Serializable {
|
||||
connectedOutput.isSpent = true;
|
||||
}
|
||||
// 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 wallet so we can show it in the UI later and use it for further spending.
|
||||
try {
|
||||
int numSpentOutputs = 0;
|
||||
// consolidating the wallet. Mark any output that is NOT back to us as spent. Then add this TX to the
|
||||
// pending pool.
|
||||
for (TransactionOutput output : tx.outputs) {
|
||||
if (findKeyFromPubHash(output.getScriptPubKey().getToAddress().getHash160()) == null) {
|
||||
if (!output.isMine(this)) {
|
||||
// This output didn't go to us, so by definition it is now spent.
|
||||
assert !output.isSpent;
|
||||
output.isSpent = true;
|
||||
numSpentOutputs++;
|
||||
}
|
||||
}
|
||||
if (numSpentOutputs == tx.outputs.size()) {
|
||||
// All of the outputs are to other people, so this transaction isn't useful anymore for further
|
||||
// spending. Stick it in a different section of the wallet so it doesn't slow down creating future
|
||||
// spend transactions.
|
||||
fullySpent.add(tx);
|
||||
} else {
|
||||
unspent.add(tx);
|
||||
}
|
||||
} catch (ScriptException e) {
|
||||
// This cannot happen - we made this script so we should be able to parse it.
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
pending.put(tx.getHash(), tx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transaction that sends the given number of nanocoins to address. The change is sent to the first
|
||||
* address in the wallet, so you must have added at least one key.
|
||||
* Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to
|
||||
* the first address in the wallet, so you must have added at least one key.<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.
|
||||
*/
|
||||
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
|
||||
@ -269,13 +401,13 @@ public class Wallet implements Serializable {
|
||||
*/
|
||||
synchronized Transaction createSend(Address address, BigInteger nanocoins, Address changeAddress) {
|
||||
LOG("Creating send tx to " + address.toString() + " for " +
|
||||
Utils.bitcoinValueToFriendlyString(nanocoins));
|
||||
// To send money to somebody else, we need to do the following:
|
||||
// - Gather up transactions with unspent outputs until we have sufficient value.
|
||||
// TODO: Sort coins so we use the smallest first, to combat wallet fragmentation.
|
||||
bitcoinValueToFriendlyString(nanocoins));
|
||||
// To send money to somebody else, we need to do gather up transactions with unspent outputs until we have
|
||||
// sufficient value. Many coin selection algorithms are possible, we use a simple but suboptimal one.
|
||||
// TODO: Sort coins so we use the smallest first, to combat wallet fragmentation and reduce fees.
|
||||
BigInteger valueGathered = BigInteger.ZERO;
|
||||
List<TransactionOutput> gathered = new LinkedList<TransactionOutput>();
|
||||
for (Transaction tx : unspent) {
|
||||
for (Transaction tx : unspent.values()) {
|
||||
for (TransactionOutput output : tx.outputs) {
|
||||
if (output.isSpent) continue;
|
||||
if (!output.isMine(this)) continue;
|
||||
@ -287,19 +419,20 @@ public class Wallet implements Serializable {
|
||||
// Can we afford this?
|
||||
if (valueGathered.compareTo(nanocoins) < 0) {
|
||||
LOG("Insufficient value in wallet for send, missing " +
|
||||
Utils.bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered)));
|
||||
bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered)));
|
||||
// TODO: Should throw an exception here.
|
||||
return null;
|
||||
}
|
||||
assert gathered.size() > 0;
|
||||
Transaction sendTx = new Transaction(params);
|
||||
sendTx.addOutput(new TransactionOutput(params, nanocoins, address));
|
||||
sendTx.addOutput(new TransactionOutput(params, nanocoins, address, sendTx));
|
||||
BigInteger change = valueGathered.subtract(nanocoins);
|
||||
if (change.compareTo(BigInteger.ZERO) > 0) {
|
||||
// The value of the inputs is greater than what we want to send. Just like in real life then,
|
||||
// we need to take back some coins ... this is called "change". Add another output that sends the change
|
||||
// back to us.
|
||||
LOG(" with " + Utils.bitcoinValueToFriendlyString(change) + " coins change");
|
||||
sendTx.addOutput(new TransactionOutput(params, change, changeAddress));
|
||||
LOG(" with " + bitcoinValueToFriendlyString(change) + " coins change");
|
||||
sendTx.addOutput(new TransactionOutput(params, change, changeAddress, sendTx));
|
||||
}
|
||||
for (TransactionOutput output : gathered) {
|
||||
sendTx.addInput(output);
|
||||
@ -336,12 +469,33 @@ public class Wallet implements Serializable {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns true if this wallet contains a public key which hashes to the given hash. */
|
||||
public synchronized boolean isPubKeyHashMine(byte[] pubkeyHash) {
|
||||
return findKeyFromPubHash(pubkeyHash) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the balance of this wallet in nanocoins by summing up all unspent outputs that were sent to us.
|
||||
* Locates a keypair from the keychain given the raw public key bytes.
|
||||
* @return ECKey or null if no such key was found.
|
||||
*/
|
||||
public synchronized ECKey findKeyFromPubKey(byte[] pubkey) {
|
||||
for (ECKey key : keychain) {
|
||||
if (Arrays.equals(key.getPubKey(), pubkey)) return key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Returns true if this wallet contains a keypair with the given public key. */
|
||||
public synchronized boolean isPubKeyMine(byte[] pubkey) {
|
||||
return findKeyFromPubKey(pubkey) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the balance of this wallet by summing up all unspent outputs that were sent to us.
|
||||
*/
|
||||
public synchronized BigInteger getBalance() {
|
||||
BigInteger balance = BigInteger.ZERO;
|
||||
for (Transaction tx : unspent) {
|
||||
for (Transaction tx : unspent.values()) {
|
||||
for (TransactionOutput output : tx.outputs) {
|
||||
if (output.isSpent) continue;
|
||||
if (!output.isMine(this)) continue;
|
||||
@ -355,11 +509,11 @@ public class Wallet implements Serializable {
|
||||
public synchronized String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
builder.append("Wallet containing ");
|
||||
builder.append(Utils.bitcoinValueToFriendlyString(getBalance()));
|
||||
builder.append(bitcoinValueToFriendlyString(getBalance()));
|
||||
builder.append("BTC in ");
|
||||
builder.append(unspent.size());
|
||||
builder.append(" unspent transactions/");
|
||||
builder.append(fullySpent.size());
|
||||
builder.append(spent.size());
|
||||
builder.append(" spent transactions");
|
||||
// Do the keys.
|
||||
builder.append("\nKeys:\n");
|
||||
@ -379,29 +533,80 @@ public class Wallet implements Serializable {
|
||||
* 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.
|
||||
*/
|
||||
synchronized void reorganize(StoredBlock chainHead, StoredBlock newStoredBlock) {
|
||||
// This runs on any peer thread with the block chain synchronized. Thus we do not have to worry about it
|
||||
// being called simultaneously or repeatedly.
|
||||
LOG("Re-organize!");
|
||||
LOG("Old chain head: " + chainHead.getHeader().toString());
|
||||
LOG("New chain head: " + newStoredBlock.getHeader().toString());
|
||||
synchronized void reorganize(Set<StoredBlock> oldBlocks, Set<StoredBlock> newBlocks) throws VerificationException {
|
||||
// This runs on any peer thread with the block chain synchronized.
|
||||
//
|
||||
// The reorganize functionality of the wallet is tested in the BlockChainTest.testForking* methods.
|
||||
//
|
||||
// For each transaction we track which blocks they appeared in. Once a re-org takes place we have to find all
|
||||
// transactions in the old branch, all transactions in the new branch and find the difference of those sets.
|
||||
//
|
||||
// receive() has been called on the block that is triggering the re-org before this is called.
|
||||
Set<Transaction> oldChainTransactions = new HashSet<Transaction>();
|
||||
Set<Transaction> newChainTransactions = new HashSet<Transaction>();
|
||||
|
||||
// TODO: Implement me!
|
||||
// For each transaction we have to track which blocks they appeared in. Once a re-org takes place,
|
||||
// we will have to find all transactions in the old branch, all transactions in the new branch and find the
|
||||
// difference of those sets. If there is no difference it means we the user doesn't really care about this
|
||||
// re-org but we still need to update the transaction block pointers.
|
||||
boolean affectedUs = true;
|
||||
Set<Transaction> all = new HashSet<Transaction>();
|
||||
all.addAll(unspent.values());
|
||||
all.addAll(spent.values());
|
||||
all.addAll(inactive.values());
|
||||
for (Transaction tx : all) {
|
||||
Set<StoredBlock> appearsIn = tx.getAppearsIn();
|
||||
assert appearsIn != null;
|
||||
// If the set of blocks this transaction appears in is disjoint with one of the chain segments it means
|
||||
// the transaction was never incorporated by a miner into that side of the chain.
|
||||
if (!Collections.disjoint(appearsIn, oldBlocks)) {
|
||||
boolean alreadyPresent = !oldChainTransactions.add(tx);
|
||||
assert !alreadyPresent : "Transaction appears twice in chain segment";
|
||||
}
|
||||
if (!Collections.disjoint(appearsIn, newBlocks)) {
|
||||
boolean alreadyPresent = !newChainTransactions.add(tx);
|
||||
assert !alreadyPresent : "Transaction appears twice in chain segment";
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no difference it means we the user doesn't really care about this re-org but we still need to
|
||||
// update the transaction block pointers for next time.
|
||||
boolean affectedUs = !oldChainTransactions.equals(newChainTransactions);
|
||||
LOG(affectedUs ? "Re-org affected our transactions" : "Re-org had no effect on our transactions");
|
||||
if (!affectedUs) return;
|
||||
|
||||
// Transactions that were in the old chain but aren't in the new chain. These will become inactive.
|
||||
Set<Transaction> gone = new HashSet<Transaction>(oldChainTransactions);
|
||||
gone.removeAll(newChainTransactions);
|
||||
// Transactions that are in the new chain but aren't in the old chain. These will be re-processed.
|
||||
Set<Transaction> fresh = new HashSet<Transaction>(newChainTransactions);
|
||||
fresh.removeAll(oldChainTransactions);
|
||||
assert !(gone.isEmpty() && fresh.isEmpty()) : "There must have been some changes to get here";
|
||||
|
||||
for (Transaction tx : gone) {
|
||||
LOG("tx not in new chain: <-unspent/spent ->inactive\n" + tx.toString());
|
||||
unspent.remove(tx.getHash());
|
||||
spent.remove(tx.getHash());
|
||||
inactive.put(tx.getHash(), tx);
|
||||
// We do not put it into the pending pool. Pending is for transactions we know are valid. After a re-org
|
||||
// some transactions may become permanently invalid if the new chain contains a double spend. We don't
|
||||
// want transactions sitting in the pending pool forever. This means shortly after a re-org the balance
|
||||
// might change rapidly as newly transactions are resurrected and included into the new chain by miners.
|
||||
}
|
||||
for (Transaction tx : fresh) {
|
||||
inactive.remove(tx.getHash());
|
||||
processTxFromBestChain(tx);
|
||||
}
|
||||
|
||||
// We should only trigger this event if the re-org actually impacted our wallet. Otherwise the user is
|
||||
// unlikely to care.
|
||||
if (affectedUs) {
|
||||
// Inform event listeners that a re-org took place.
|
||||
for (WalletEventListener l : eventListeners) {
|
||||
// Synchronize on the event listener as well. This allows a single listener to handle events from
|
||||
// multiple wallets without needing to worry about being thread safe.
|
||||
synchronized (l) {
|
||||
l.onReorganize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an immutable view of the transactions currently waiting for network confirmations.
|
||||
*/
|
||||
public Collection<Transaction> getPendingTransactions() {
|
||||
return Collections.unmodifiableCollection(pending.values());
|
||||
}
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ public class BlockChainTest {
|
||||
private BlockChain chain;
|
||||
private Address coinbaseTo;
|
||||
private NetworkParameters unitTestParams;
|
||||
private Address someOtherGuy;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
@ -44,8 +45,10 @@ public class BlockChainTest {
|
||||
unitTestParams = NetworkParameters.unitTests();
|
||||
wallet = new Wallet(unitTestParams);
|
||||
wallet.addKey(new ECKey());
|
||||
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
|
||||
chain = new BlockChain(unitTestParams, wallet, new MemoryBlockStore(unitTestParams));
|
||||
|
||||
coinbaseTo = wallet.keychain.get(0).toAddress(unitTestParams);
|
||||
someOtherGuy = new ECKey().toAddress(unitTestParams);
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -86,23 +89,26 @@ public class BlockChainTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testForking() throws Exception {
|
||||
// Check that if the block chain forks, we end up using the right one.
|
||||
// Start by building a couple of blocks on top of the genesis block.
|
||||
final boolean[] flags = new boolean[1];
|
||||
flags[0] = false;
|
||||
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() {
|
||||
flags[0] = true;
|
||||
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(flags[0]);
|
||||
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:
|
||||
@ -114,24 +120,21 @@ public class BlockChainTest {
|
||||
// \-> b3
|
||||
//
|
||||
// Nothing should happen at this point. We saw b2 first so it takes priority.
|
||||
Address someOtherGuy = new ECKey().toAddress(unitTestParams);
|
||||
Block b3 = b1.createNextBlock(someOtherGuy);
|
||||
assertTrue(chain.add(b3));
|
||||
assertFalse(flags[0]); // No re-org took place.
|
||||
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(flags[0]); // Re-org took place.
|
||||
flags[0] = false;
|
||||
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.
|
||||
if (false) {
|
||||
// These tests do not pass currently, as wallet handling of re-orgs isn't implemented.
|
||||
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
// ... and back to the first testNetChain
|
||||
// ... and back to the first chain.
|
||||
Block b5 = b2.createNextBlock(coinbaseTo);
|
||||
Block b6 = b5.createNextBlock(coinbaseTo);
|
||||
assertTrue(chain.add(b5));
|
||||
@ -140,8 +143,83 @@ public class BlockChainTest {
|
||||
// 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
|
||||
|
@ -26,38 +26,66 @@ import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class WalletTest {
|
||||
static final NetworkParameters params = NetworkParameters.testNet();
|
||||
static final NetworkParameters params = NetworkParameters.unitTests();
|
||||
|
||||
private Address myAddress;
|
||||
private Wallet wallet;
|
||||
private BlockStore blockStore;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
public void setUp() throws Exception {
|
||||
ECKey myKey = new ECKey();
|
||||
myAddress = myKey.toAddress(params);
|
||||
wallet = new Wallet(params);
|
||||
wallet.addKey(myKey);
|
||||
blockStore = new MemoryBlockStore(params);
|
||||
}
|
||||
|
||||
private static byte fakeHashCounter = 0;
|
||||
private Transaction createFakeTx(BigInteger nanocoins, Address to) {
|
||||
Transaction t = new Transaction(params);
|
||||
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to);
|
||||
TransactionOutput o1 = new TransactionOutput(params, nanocoins, to, t);
|
||||
t.addOutput(o1);
|
||||
// t1 is not a valid transaction - it has no inputs. Nonetheless, if we set it up with a fake hash it'll be
|
||||
// valid enough for these tests.
|
||||
byte[] hash = new byte[32];
|
||||
for (byte i = 0; i < 32; i++) hash[i] = i;
|
||||
t.setFakeHashForTesting(hash);
|
||||
hash[0] = fakeHashCounter++;
|
||||
t.setFakeHashForTesting(new Sha256Hash(hash));
|
||||
return t;
|
||||
}
|
||||
|
||||
class BlockPair {
|
||||
StoredBlock storedBlock;
|
||||
Block block;
|
||||
}
|
||||
|
||||
// Emulates receiving a valid block that builds on top of the chain.
|
||||
private BlockPair createFakeBlock(Transaction... transactions) {
|
||||
try {
|
||||
Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params));
|
||||
for (Transaction tx : transactions)
|
||||
b.addTransaction(tx);
|
||||
b.solve();
|
||||
BlockPair pair = new BlockPair();
|
||||
pair.block = b;
|
||||
pair.storedBlock = blockStore.getChainHead().build(b);
|
||||
blockStore.put(pair.storedBlock);
|
||||
blockStore.setChainHead(pair.storedBlock);
|
||||
return pair;
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
} catch (BlockStoreException e) {
|
||||
throw new RuntimeException(e); // Cannot happen.
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBasicSpending() throws Exception {
|
||||
// We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change.
|
||||
BigInteger v1 = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(v1, myAddress);
|
||||
|
||||
wallet.receive(t1);
|
||||
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
|
||||
ECKey k2 = new ECKey();
|
||||
@ -66,12 +94,27 @@ public class WalletTest {
|
||||
|
||||
// Do some basic sanity checks.
|
||||
assertEquals(1, t2.inputs.size());
|
||||
LOG(t2.inputs.get(0).getScriptSig().toString());
|
||||
assertEquals(myAddress, t2.inputs.get(0).getScriptSig().getFromAddress());
|
||||
|
||||
// We have NOT proven that the signature is correct!
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSideChain() throws Exception {
|
||||
// The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance.
|
||||
BigInteger v1 = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(v1, myAddress);
|
||||
|
||||
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
|
||||
BigInteger v2 = toNanoCoins(0, 50);
|
||||
Transaction t2 = createFakeTx(v2, myAddress);
|
||||
wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
|
||||
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testListener() throws Exception {
|
||||
final Transaction fakeTx = createFakeTx(Utils.toNanoCoins(1, 0), myAddress);
|
||||
@ -86,7 +129,7 @@ public class WalletTest {
|
||||
}
|
||||
};
|
||||
wallet.addEventListener(listener);
|
||||
wallet.receive(fakeTx);
|
||||
wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertTrue(didRun[0]);
|
||||
}
|
||||
|
||||
@ -97,24 +140,29 @@ public class WalletTest {
|
||||
BigInteger v2 = toNanoCoins(0, 50);
|
||||
Transaction t1 = createFakeTx(v1, myAddress);
|
||||
Transaction t2 = createFakeTx(v2, myAddress);
|
||||
StoredBlock b1 = createFakeBlock(t1).storedBlock;
|
||||
StoredBlock b2 = createFakeBlock(t2).storedBlock;
|
||||
BigInteger expected = toNanoCoins(5, 50);
|
||||
wallet.receive(t1);
|
||||
wallet.receive(t2);
|
||||
wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
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);
|
||||
// We started with 5.50 so we should have 4.50 left.
|
||||
|
||||
// Balance should be 0.50 because the change output is pending confirmation by the network.
|
||||
assertEquals(toNanoCoins(0, 50), wallet.getBalance());
|
||||
|
||||
// Now confirm the transaction by including it into a block.
|
||||
StoredBlock b3 = createFakeBlock(spend).storedBlock;
|
||||
wallet.receive(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);
|
||||
assertEquals(bitcoinValueToFriendlyString(v4),
|
||||
bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
// And spend another coin ...
|
||||
wallet.confirmSend(wallet.createSend(new ECKey().toAddress(params), v3));
|
||||
BigInteger v5 = toNanoCoins(3, 50);
|
||||
assertEquals(bitcoinValueToFriendlyString(v5),
|
||||
bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
}
|
||||
|
||||
// Intuitively you'd expect to be able to create a transaction with identical inputs and outputs and get an
|
||||
@ -125,17 +173,22 @@ public class WalletTest {
|
||||
@Test
|
||||
public void testBlockChainCatchup() throws Exception {
|
||||
Transaction tx1 = createFakeTx(Utils.toNanoCoins(1, 0), myAddress);
|
||||
wallet.receive(tx1);
|
||||
StoredBlock b1 = createFakeBlock(tx1).storedBlock;
|
||||
wallet.receive(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.
|
||||
wallet.receive(send1);
|
||||
// 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(send1).storedBlock;
|
||||
wallet.receive(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);
|
||||
StoredBlock b3 = createFakeBlock(send2).storedBlock;
|
||||
wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80");
|
||||
}
|
||||
|
||||
@ -143,8 +196,8 @@ public class WalletTest {
|
||||
public void testBalances() throws Exception {
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
Transaction tx1 = createFakeTx(nanos, myAddress);
|
||||
wallet.receive(tx1);
|
||||
assertEquals(nanos, tx1.getValueSentToMe(wallet));
|
||||
wallet.receive(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);
|
||||
// Reserialize.
|
||||
|
Loading…
Reference in New Issue
Block a user