3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-19 13:45:48 +00:00

Wallet: Provide new balance types to calculate balances excluding watching outputs. This is useful for wallets where transactions have been manually added and thus there is a mix of watching and non-watching transactions. The "new in 0.13" behaviour that getBalance(AVAILABLE) includes unspendable outputs is preserved, so the more typical approach of having a watching wallet and calling getBalance() still does what you expect and reports the balance of the watched wallet.

API change: send completion would previously include watched outputs and could therefore throw MissingPrivateKeyException. This has now changed so watched outputs won't be considered and thus the exception may change to be InsufficientMoneyException, unless completing a pre-prepared transaction that is already connected to watched outputs.
This commit is contained in:
Mike Hearn 2015-04-19 15:55:10 +01:00
parent ef9e49d5e7
commit 82a0ddd4de
5 changed files with 130 additions and 67 deletions

View File

@ -2984,12 +2984,16 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
/** /**
* <p>It's possible to calculate a wallets balance from multiple points of view. This enum selects which * <p>It's possible to calculate a wallets balance from multiple points of view. This enum selects which
* getBalance() should use.</p> * {@link #getBalance(BalanceType)} should use.</p>
* *
* <p>Consider a real-world example: you buy a snack costing $5 but you only have a $10 bill. At the start you have * <p>Consider a real-world example: you buy a snack costing $5 but you only have a $10 bill. At the start you have
* $10 viewed from every possible angle. After you order the snack you hand over your $10 bill. From the * $10 viewed from every possible angle. After you order the snack you hand over your $10 bill. From the
* perspective of your wallet you have zero dollars (AVAILABLE). But you know in a few seconds the shopkeeper * perspective of your wallet you have zero dollars (AVAILABLE). But you know in a few seconds the shopkeeper
* will give you back $5 change so most people in practice would say they have $5 (ESTIMATED).</p> * will give you back $5 change so most people in practice would say they have $5 (ESTIMATED).</p>
*
* <p>The fact that the wallet can track transactions which are not spendable by itself ("watching wallets") adds
* another type of balance to the mix. Although the wallet won't do this by default, advanced use cases that
* override the relevancy checks can end up with a mix of spendable and unspendable transactions.</p>
*/ */
public enum BalanceType { public enum BalanceType {
/** /**
@ -2999,11 +3003,17 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
ESTIMATED, ESTIMATED,
/** /**
* Balance that can be safely used to create new spends. This is whatever the default coin selector would * Balance that could be safely used to create new spends, if we had all the needed private keys. This is
* make available, which by default means transaction outputs with at least 1 confirmation and pending * whatever the default coin selector would make available, which by default means transaction outputs with at
* transactions created by our own wallet which have been propagated across the network. * least 1 confirmation and pending transactions created by our own wallet which have been propagated across
* the network. Whether we <i>actually</i> have the private keys or not is irrelevant for this balance type.
*/ */
AVAILABLE AVAILABLE,
/** Same as ESTIMATED but only for outputs we have the private keys for and can sign ourselves. */
ESTIMATED_SPENDABLE,
/** Same as AVAILABLE but only for outputs we have the private keys for and can sign ourselves. */
AVAILABLE_SPENDABLE
} }
/** /**
@ -3020,10 +3030,12 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
public Coin getBalance(BalanceType balanceType) { public Coin getBalance(BalanceType balanceType) {
lock.lock(); lock.lock();
try { try {
if (balanceType == BalanceType.AVAILABLE) { if (balanceType == BalanceType.AVAILABLE || balanceType == BalanceType.AVAILABLE_SPENDABLE) {
return getBalance(coinSelector); List<TransactionOutput> candidates = calculateAllSpendCandidates(true, balanceType == BalanceType.AVAILABLE_SPENDABLE);
} else if (balanceType == BalanceType.ESTIMATED) { CoinSelection selection = coinSelector.select(NetworkParameters.MAX_MONEY, candidates);
List<TransactionOutput> all = calculateAllSpendCandidates(false); return selection.valueGathered;
} else if (balanceType == BalanceType.ESTIMATED || balanceType == BalanceType.ESTIMATED_SPENDABLE) {
List<TransactionOutput> all = calculateAllSpendCandidates(false, balanceType == BalanceType.ESTIMATED_SPENDABLE);
Coin value = Coin.ZERO; Coin value = Coin.ZERO;
for (TransactionOutput out : all) value = value.add(out.getValue()); for (TransactionOutput out : all) value = value.add(out.getValue());
return value; return value;
@ -3036,14 +3048,15 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
/** /**
* Returns the balance that would be considered spendable by the given coin selector. Just asks it to select * Returns the balance that would be considered spendable by the given coin selector, including watched outputs
* as many coins as possible and returns the total. * (i.e. balance includes outputs we don't have the private keys for). Just asks it to select as many coins as
* possible and returns the total.
*/ */
public Coin getBalance(CoinSelector selector) { public Coin getBalance(CoinSelector selector) {
lock.lock(); lock.lock();
try { try {
checkNotNull(selector); checkNotNull(selector);
List<TransactionOutput> candidates = calculateAllSpendCandidates(true); List<TransactionOutput> candidates = calculateAllSpendCandidates(true, false);
CoinSelection selection = selector.select(NetworkParameters.MAX_MONEY, candidates); CoinSelection selection = selector.select(NetworkParameters.MAX_MONEY, candidates);
return selection.valueGathered; return selection.valueGathered;
} finally { } finally {
@ -3610,13 +3623,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us // Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us
// with the actual outputs that'll be used to gather the required amount of value. In this way, users // with the actual outputs that'll be used to gather the required amount of value. In this way, users
// can customize coin selection policies. // can customize coin selection policies. The call below will ignore immature coinbases and outputs
// // we don't have the keys for.
// Note that this code is poorly optimized: the spend candidates only alter when transactions in the wallet List<TransactionOutput> candidates = calculateAllSpendCandidates(true, req.missingSigsMode == MissingSigsMode.THROW);
// change - it could be pre-calculated and held in RAM, and this is probably an optimization worth doing.
List<TransactionOutput> candidates = calculateAllSpendCandidates(true);
eraseCandidatesWithoutKeys(candidates);
CoinSelection bestCoinSelection; CoinSelection bestCoinSelection;
TransactionOutput bestChangeOutput = null; TransactionOutput bestChangeOutput = null;
@ -3689,27 +3698,6 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
private void eraseCandidatesWithoutKeys(List<TransactionOutput> candidates) {
ListIterator<TransactionOutput> it = candidates.listIterator();
while (it.hasNext()) {
TransactionOutput output = it.next();
try {
Script script = output.getScriptPubKey();
if (script.isSentToAddress()) {
if (findKeyFromPubHash(script.getPubKeyHash()) == null)
it.remove();
} else if (script.isPayToScriptHash()) {
if (findRedeemDataFromScriptHash(script.getPubKeyHash()) == null)
it.remove();
}
} catch (ScriptException e) {
// If this happens it means an output script in a wallet tx could not be understood. That should never
// happen, if it does it means the wallet has got into an inconsistent state.
throw new IllegalStateException(e);
}
}
}
/** /**
* <p>Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction * <p>Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction
* to have all necessary inputs connected or they will be ignored.</p> * to have all necessary inputs connected or they will be ignored.</p>
@ -3780,17 +3768,35 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
/** /**
* Returns a list of all possible outputs we could possibly spend, potentially even including immature coinbases * Returns a list of the outputs that can potentially be spent, i.e. that we have the keys for and are unspent
* (which the protocol may forbid us from spending). In other words, return all outputs that this wallet holds * according to our knowledge of the block chain.
* keys for and which are not already marked as spent.
*/ */
public List<TransactionOutput> calculateAllSpendCandidates() {
return calculateAllSpendCandidates(true, true);
}
/** @deprecated Use {@link #calculateAllSpendCandidates(boolean, boolean)} or the zero-parameter form instead. */
@Deprecated
public List<TransactionOutput> calculateAllSpendCandidates(boolean excludeImmatureCoinbases) { public List<TransactionOutput> calculateAllSpendCandidates(boolean excludeImmatureCoinbases) {
return calculateAllSpendCandidates(excludeImmatureCoinbases, true);
}
/**
* Returns a list of all outputs that are being tracked by this wallet either from the {@link UTXOProvider}
* (in this case the existence or not of private keys is ignored), or the wallets internal storage (the default)
* taking into account the flags.
*
* @param excludeImmatureCoinbases Whether to ignore coinbase outputs that we will be able to spend in future once they mature.
* @param excludeUnsignable Whether to ignore outputs that we are tracking but don't have the keys to sign for.
*/
public List<TransactionOutput> calculateAllSpendCandidates(boolean excludeImmatureCoinbases, boolean excludeUnsignable) {
lock.lock(); lock.lock();
try { try {
List<TransactionOutput> candidates; List<TransactionOutput> candidates;
if (vUTXOProvider == null) { if (vUTXOProvider == null) {
candidates = new ArrayList<TransactionOutput>(myUnspents.size()); candidates = new ArrayList<TransactionOutput>(myUnspents.size());
for (TransactionOutput output : myUnspents) { for (TransactionOutput output : myUnspents) {
if (excludeUnsignable && !canSignFor(output.getScriptPubKey())) continue;
Transaction transaction = checkNotNull(output.getParentTransaction()); Transaction transaction = checkNotNull(output.getParentTransaction());
if (excludeImmatureCoinbases && !transaction.isMature()) if (excludeImmatureCoinbases && !transaction.isMature())
continue; continue;
@ -3805,11 +3811,36 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
} }
} }
/**
* Returns true if this wallet has at least one of the private keys needed to sign for this scriptPubKey. Returns
* false if the form of the script is not known or if the script is OP_RETURN.
*/
public boolean canSignFor(Script script) {
if (script.isSentToRawPubKey()) {
byte[] pubkey = script.getPubKey();
ECKey key = findKeyFromPubKey(pubkey);
return key != null && (key.isEncrypted() || key.hasPrivKey());
} if (script.isPayToScriptHash()) {
RedeemData data = findRedeemDataFromScriptHash(script.getPubKeyHash());
return data != null && canSignFor(data.redeemScript);
} else if (script.isSentToAddress()) {
ECKey key = findKeyFromPubHash(script.getPubKeyHash());
return key != null && (key.isEncrypted() || key.hasPrivKey());
} else if (script.isSentToMultiSig()) {
for (ECKey pubkey : script.getPubKeys()) {
ECKey key = findKeyFromPubKey(pubkey.getPubKey());
if (key != null && (key.isEncrypted() || key.hasPrivKey()))
return true;
}
}
return false;
}
/** /**
* Returns the spendable candidates from the {@link UTXOProvider} based on keys that the wallet contains. * Returns the spendable candidates from the {@link UTXOProvider} based on keys that the wallet contains.
* @return The list of candidates. * @return The list of candidates.
*/ */
protected LinkedList<TransactionOutput> calculateAllSpendCandidatesFromUTXOProvider(boolean excludeImmatureCoinbases){ protected LinkedList<TransactionOutput> calculateAllSpendCandidatesFromUTXOProvider(boolean excludeImmatureCoinbases) {
checkState(lock.isHeldByCurrentThread()); checkState(lock.isHeldByCurrentThread());
UTXOProvider utxoProvider = checkNotNull(vUTXOProvider, "No UTXO provider has been set"); UTXOProvider utxoProvider = checkNotNull(vUTXOProvider, "No UTXO provider has been set");
LinkedList<TransactionOutput> candidates = Lists.newLinkedList(); LinkedList<TransactionOutput> candidates = Lists.newLinkedList();
@ -3829,7 +3860,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
// We need to handle the pending transactions that we know about. // We need to handle the pending transactions that we know about.
for (Transaction tx : pending.values()) { for (Transaction tx : pending.values()) {
// Remove the spent outputs. // Remove the spent outputs.
for(TransactionInput input : tx.getInputs()) { for (TransactionInput input : tx.getInputs()) {
if (input.getConnectedOutput().isMine(this)) { if (input.getConnectedOutput().isMine(this)) {
candidates.remove(input.getConnectedOutput()); candidates.remove(input.getConnectedOutput());
} }
@ -4865,7 +4896,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
for (Transaction other : others) for (Transaction other : others)
selector.excludeOutputsSpentBy(other); selector.excludeOutputsSpentBy(other);
// TODO: Make this use the standard SendRequest. // TODO: Make this use the standard SendRequest.
CoinSelection toMove = selector.select(Coin.ZERO, calculateAllSpendCandidates(true)); CoinSelection toMove = selector.select(Coin.ZERO, calculateAllSpendCandidates());
if (toMove.valueGathered.equals(Coin.ZERO)) return null; // Nothing to do. if (toMove.valueGathered.equals(Coin.ZERO)) return null; // Nothing to do.
maybeUpgradeToHD(aesKey); maybeUpgradeToHD(aesKey);
Transaction rekeyTx = new Transaction(params); Transaction rekeyTx = new Transaction(params);

View File

@ -38,6 +38,8 @@ import java.util.*;
import static org.bitcoinj.script.ScriptOpCodes.*; import static org.bitcoinj.script.ScriptOpCodes.*;
import static com.google.common.base.Preconditions.*; import static com.google.common.base.Preconditions.*;
// TODO: Redesign this entire API to be more type safe and organised.
/** /**
* <p>Programs embedded inside transactions that control redemption of payments.</p> * <p>Programs embedded inside transactions that control redemption of payments.</p>
* *
@ -480,6 +482,22 @@ public class Script {
throw new IllegalStateException("Could not find matching key " + key.toString() + " in script " + this); throw new IllegalStateException("Could not find matching key " + key.toString() + " in script " + this);
} }
/**
* Returns a list of the keys required by this script, assuming a multi-sig script.
*
* @throws ScriptException if the script type is not understood or is pay to address or is P2SH (run this method on the "Redeem script" instead).
*/
public List<ECKey> getPubKeys() {
if (!isSentToMultiSig())
throw new ScriptException("Only usable for multisig scripts.");
ArrayList<ECKey> result = Lists.newArrayList();
int numKeys = Script.decodeFromOpN(chunks.get(chunks.size() - 2).opcode);
for (int i = 0 ; i < numKeys ; i++)
result.add(ECKey.fromPublicOnly(chunks.get(1 + i).data));
return result;
}
private int findSigInRedeem(byte[] signatureBytes, Sha256Hash hash) { private int findSigInRedeem(byte[] signatureBytes, Sha256Hash hash) {
checkArgument(chunks.get(0).isOpCode()); // P2SH scriptSig checkArgument(chunks.get(0).isOpCode()); // P2SH scriptSig
int numKeys = Script.decodeFromOpN(chunks.get(chunks.size() - 2).opcode); int numKeys = Script.decodeFromOpN(chunks.get(chunks.size() - 2).opcode);

View File

@ -1161,12 +1161,13 @@ public class WalletTest extends TestWithWallet {
assertFalse(wallet.isWatching()); assertFalse(wallet.isWatching());
} }
@Test(expected = ECKey.MissingPrivateKeyException.class) @Test
public void watchingWallet() throws Exception { public void watchingWallet() throws Exception {
DeterministicKey watchKey = wallet.getWatchingKey(); DeterministicKey watchKey = wallet.getWatchingKey();
String serialized = watchKey.serializePubB58(params); String serialized = watchKey.serializePubB58(params);
watchKey = DeterministicKey.deserializeB58(null, serialized, params);
Wallet watchingWallet = Wallet.fromWatchingKey(params, watchKey); // Construct watching wallet.
Wallet watchingWallet = Wallet.fromWatchingKey(params, DeterministicKey.deserializeB58(null, serialized, params));
DeterministicKey key2 = watchingWallet.freshReceiveKey(); DeterministicKey key2 = watchingWallet.freshReceiveKey();
assertEquals(myKey, key2); assertEquals(myKey, key2);
@ -1174,7 +1175,17 @@ public class WalletTest extends TestWithWallet {
key2 = watchingWallet.freshKey(KeyChain.KeyPurpose.CHANGE); key2 = watchingWallet.freshKey(KeyChain.KeyPurpose.CHANGE);
assertEquals(key, key2); assertEquals(key, key2);
key.sign(Sha256Hash.ZERO_HASH); key.sign(Sha256Hash.ZERO_HASH);
try {
key2.sign(Sha256Hash.ZERO_HASH); key2.sign(Sha256Hash.ZERO_HASH);
fail();
} catch (ECKey.MissingPrivateKeyException e) {
// Expected
}
receiveATransaction(watchingWallet, myKey.toAddress(params));
assertEquals(COIN, watchingWallet.getBalance());
assertEquals(COIN, watchingWallet.getBalance(Wallet.BalanceType.AVAILABLE));
assertEquals(ZERO, watchingWallet.getBalance(Wallet.BalanceType.AVAILABLE_SPENDABLE));
} }
@Test(expected = ECKey.MissingPrivateKeyException.class) @Test(expected = ECKey.MissingPrivateKeyException.class)
@ -2826,8 +2837,14 @@ public class WalletTest extends TestWithWallet {
@Test (expected = ECKey.MissingPrivateKeyException.class) @Test (expected = ECKey.MissingPrivateKeyException.class)
public void completeTxPartiallySignedThrows() throws Exception { public void completeTxPartiallySignedThrows() throws Exception {
byte[] emptySig = new byte[]{}; sendMoneyToWallet(wallet, CENT, wallet.freshReceiveKey(), AbstractBlockChain.NewBlockType.BEST_CHAIN);
completeTxPartiallySigned(Wallet.MissingSigsMode.THROW, emptySig); SendRequest req = SendRequest.emptyWallet(new ECKey().toAddress(params));
wallet.completeTx(req);
// Delete the sigs
for (TransactionInput input : req.tx.getInputs())
input.setScriptBytes(new byte[]{});
Wallet watching = Wallet.fromWatchingKey(params, wallet.getWatchingKey().dropParent().dropPrivateBytes());
watching.completeTx(Wallet.SendRequest.forTx(req.tx));
} }
@Test @Test

View File

@ -82,7 +82,11 @@ public class ScriptTest {
public void testMultiSig() throws Exception { public void testMultiSig() throws Exception {
List<ECKey> keys = Lists.newArrayList(new ECKey(), new ECKey(), new ECKey()); List<ECKey> keys = Lists.newArrayList(new ECKey(), new ECKey(), new ECKey());
assertTrue(ScriptBuilder.createMultiSigOutputScript(2, keys).isSentToMultiSig()); assertTrue(ScriptBuilder.createMultiSigOutputScript(2, keys).isSentToMultiSig());
assertTrue(ScriptBuilder.createMultiSigOutputScript(3, keys).isSentToMultiSig()); Script script = ScriptBuilder.createMultiSigOutputScript(3, keys);
assertTrue(script.isSentToMultiSig());
List<ECKey> pubkeys = new ArrayList<ECKey>(3);
for (ECKey key : keys) pubkeys.add(ECKey.fromPublicOnly(key.getPubKeyPoint()));
assertEquals(script.getPubKeys(), pubkeys);
assertFalse(ScriptBuilder.createOutputScript(new ECKey()).isSentToMultiSig()); assertFalse(ScriptBuilder.createOutputScript(new ECKey()).isSentToMultiSig());
try { try {
// Fail if we ask for more signatures than keys. // Fail if we ask for more signatures than keys.

View File

@ -17,22 +17,15 @@
package org.bitcoinj.wallet; package org.bitcoinj.wallet;
import org.bitcoinj.core.*; import org.bitcoinj.core.*;
import org.bitcoinj.params.RegTestParams; import org.bitcoinj.params.*;
import org.bitcoinj.params.UnitTestParams; import org.bitcoinj.testing.*;
import org.bitcoinj.testing.FakeTxBuilder; import org.junit.*;
import org.bitcoinj.testing.TestWithWallet;
import org.bitcoinj.wallet.CoinSelection;
import org.bitcoinj.wallet.DefaultCoinSelector;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.net.InetAddress; import java.net.*;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import static com.google.common.base.Preconditions.*;
import static org.bitcoinj.core.Coin.*; import static org.bitcoinj.core.Coin.*;
import static com.google.common.base.Preconditions.checkNotNull;
import static org.junit.Assert.*; import static org.junit.Assert.*;
public class DefaultCoinSelectorTest extends TestWithWallet { public class DefaultCoinSelectorTest extends TestWithWallet {
@ -80,7 +73,7 @@ public class DefaultCoinSelectorTest extends TestWithWallet {
// Check we selected just the oldest one. // Check we selected just the oldest one.
DefaultCoinSelector selector = new DefaultCoinSelector(); DefaultCoinSelector selector = new DefaultCoinSelector();
CoinSelection selection = selector.select(COIN, wallet.calculateAllSpendCandidates(true)); CoinSelection selection = selector.select(COIN, wallet.calculateAllSpendCandidates());
assertTrue(selection.gathered.contains(t1.getOutputs().get(0))); assertTrue(selection.gathered.contains(t1.getOutputs().get(0)));
assertEquals(COIN, selection.valueGathered); assertEquals(COIN, selection.valueGathered);