diff --git a/core/src/main/java/com/google/bitcoin/core/Wallet.java b/core/src/main/java/com/google/bitcoin/core/Wallet.java index 1d4e36ef..00dff175 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -25,10 +25,7 @@ import com.google.bitcoin.store.UnreadableWalletException; import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.utils.ListenerRegistration; import com.google.bitcoin.utils.Threading; -import com.google.bitcoin.wallet.CoinSelection; -import com.google.bitcoin.wallet.CoinSelector; -import com.google.bitcoin.wallet.KeyTimeCoinSelector; -import com.google.bitcoin.wallet.WalletFiles; +import com.google.bitcoin.wallet.*; import com.google.common.base.Preconditions; import com.google.common.collect.*; import com.google.common.util.concurrent.FutureCallback; @@ -156,77 +153,6 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi private volatile long vKeyRotationTimestamp; private volatile boolean vKeyRotationEnabled; - /** - * This class implements a {@link CoinSelector} which attempts to get the highest priority possible. This means that - * the transaction is the most likely to get confirmed - * Note that this means we may end up "spending" more priority than would be required to get the transaction we are - * creating confirmed. - */ - public static class DefaultCoinSelector implements CoinSelector { - public CoinSelection select(BigInteger biTarget, LinkedList candidates) { - long target = biTarget.longValue(); - HashSet selected = new HashSet(); - // Sort the inputs by age*value so we get the highest "coindays" spent. - // TODO: Consider changing the wallets internal format to track just outputs and keep them ordered. - ArrayList sortedOutputs = new ArrayList(candidates); - // When calculating the wallet balance, we may be asked to select all possible coins, if so, avoid sorting - // them in order to improve performance. - if (!biTarget.equals(NetworkParameters.MAX_MONEY)) { - Collections.sort(sortedOutputs, new Comparator() { - public int compare(TransactionOutput a, TransactionOutput b) { - int depth1 = 0; - int depth2 = 0; - TransactionConfidence conf1 = a.parentTransaction.getConfidence(); - TransactionConfidence conf2 = b.parentTransaction.getConfidence(); - if (conf1.getConfidenceType() == ConfidenceType.BUILDING) depth1 = conf1.getDepthInBlocks(); - if (conf2.getConfidenceType() == ConfidenceType.BUILDING) depth2 = conf2.getDepthInBlocks(); - BigInteger aValue = a.getValue(); - BigInteger bValue = b.getValue(); - BigInteger aCoinDepth = aValue.multiply(BigInteger.valueOf(depth1)); - BigInteger bCoinDepth = bValue.multiply(BigInteger.valueOf(depth2)); - int c1 = bCoinDepth.compareTo(aCoinDepth); - if (c1 != 0) return c1; - // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. - int c2 = bValue.compareTo(aValue); - if (c2 != 0) return c2; - // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. - BigInteger aHash = a.parentTransaction.getHash().toBigInteger(); - BigInteger bHash = b.parentTransaction.getHash().toBigInteger(); - return aHash.compareTo(bHash); - } - }); - } - // Now iterate over the sorted outputs until we have got as close to the target as possible or a little - // bit over (excessive value will be change). - long total = 0; - for (TransactionOutput output : sortedOutputs) { - if (total >= target) break; - // Only pick chain-included transactions, or transactions that are ours and pending. - if (!shouldSelect(output.parentTransaction)) continue; - selected.add(output); - total += output.getValue().longValue(); - } - // Total may be lower than target here, if the given candidates were insufficient to create to requested - // transaction. - return new CoinSelection(BigInteger.valueOf(total), selected); - } - - /** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */ - protected boolean shouldSelect(Transaction tx) { - return isSelectable(tx); - } - - public static boolean isSelectable(Transaction tx) { - // Only pick chain-included transactions, or transactions that are ours and pending. - TransactionConfidence confidence = tx.getConfidence(); - ConfidenceType type = confidence.getConfidenceType(); - if (type.equals(ConfidenceType.BUILDING)) return true; - return type.equals(ConfidenceType.PENDING) && - confidence.getSource().equals(TransactionConfidence.Source.SELF) && - confidence.numBroadcastPeers() > 1; - } - } - /** * This coin selector will select any transaction at all, regardless of where it came from or whether it was * confirmed yet. However immature coinbases will not be included (would be a protocol violation). diff --git a/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java b/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java new file mode 100644 index 00000000..40b94a67 --- /dev/null +++ b/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java @@ -0,0 +1,81 @@ +package com.google.bitcoin.wallet; + +import com.google.bitcoin.core.NetworkParameters; +import com.google.bitcoin.core.Transaction; +import com.google.bitcoin.core.TransactionConfidence; +import com.google.bitcoin.core.TransactionOutput; + +import java.math.BigInteger; +import java.util.*; + +/** + * This class implements a {@link com.google.bitcoin.wallet.CoinSelector} which attempts to get the highest priority + * possible. This means that the transaction is the most likely to get confirmed. Note that this means we may end up + * "spending" more priority than would be required to get the transaction we are creating confirmed. + */ +public class DefaultCoinSelector implements CoinSelector { + public CoinSelection select(BigInteger biTarget, LinkedList candidates) { + long target = biTarget.longValue(); + HashSet selected = new HashSet(); + // Sort the inputs by age*value so we get the highest "coindays" spent. + // TODO: Consider changing the wallets internal format to track just outputs and keep them ordered. + ArrayList sortedOutputs = new ArrayList(candidates); + // When calculating the wallet balance, we may be asked to select all possible coins, if so, avoid sorting + // them in order to improve performance. + if (!biTarget.equals(NetworkParameters.MAX_MONEY)) { + Collections.sort(sortedOutputs, new Comparator() { + public int compare(TransactionOutput a, TransactionOutput b) { + int depth1 = 0; + int depth2 = 0; + TransactionConfidence conf1 = a.getParentTransaction().getConfidence(); + TransactionConfidence conf2 = b.getParentTransaction().getConfidence(); + if (conf1.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) + depth1 = conf1.getDepthInBlocks(); + if (conf2.getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) + depth2 = conf2.getDepthInBlocks(); + BigInteger aValue = a.getValue(); + BigInteger bValue = b.getValue(); + BigInteger aCoinDepth = aValue.multiply(BigInteger.valueOf(depth1)); + BigInteger bCoinDepth = bValue.multiply(BigInteger.valueOf(depth2)); + int c1 = bCoinDepth.compareTo(aCoinDepth); + if (c1 != 0) return c1; + // The "coin*days" destroyed are equal, sort by value alone to get the lowest transaction size. + int c2 = bValue.compareTo(aValue); + if (c2 != 0) return c2; + // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. + BigInteger aHash = a.getParentTransaction().getHash().toBigInteger(); + BigInteger bHash = b.getParentTransaction().getHash().toBigInteger(); + return aHash.compareTo(bHash); + } + }); + } + // Now iterate over the sorted outputs until we have got as close to the target as possible or a little + // bit over (excessive value will be change). + long total = 0; + for (TransactionOutput output : sortedOutputs) { + if (total >= target) break; + // Only pick chain-included transactions, or transactions that are ours and pending. + if (!shouldSelect(output.getParentTransaction())) continue; + selected.add(output); + total += output.getValue().longValue(); + } + // Total may be lower than target here, if the given candidates were insufficient to create to requested + // transaction. + return new CoinSelection(BigInteger.valueOf(total), selected); + } + + /** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */ + protected boolean shouldSelect(Transaction tx) { + return isSelectable(tx); + } + + public static boolean isSelectable(Transaction tx) { + // Only pick chain-included transactions, or transactions that are ours and pending. + TransactionConfidence confidence = tx.getConfidence(); + TransactionConfidence.ConfidenceType type = confidence.getConfidenceType(); + if (type.equals(TransactionConfidence.ConfidenceType.BUILDING)) return true; + return type.equals(TransactionConfidence.ConfidenceType.PENDING) && + confidence.getSource().equals(TransactionConfidence.Source.SELF) && + confidence.numBroadcastPeers() > 1; + } +} diff --git a/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java index 46e3cb12..f8fae96f 100644 --- a/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java +++ b/core/src/test/java/com/google/bitcoin/protocols/channels/PaymentChannelStateTest.java @@ -20,6 +20,7 @@ import com.google.bitcoin.core.*; import com.google.bitcoin.script.Script; import com.google.bitcoin.script.ScriptBuilder; import com.google.bitcoin.utils.TestWithWallet; +import com.google.bitcoin.wallet.DefaultCoinSelector; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; @@ -193,7 +194,7 @@ public class PaymentChannelStateTest extends TestWithWallet { // Create a block with the payment transaction in it and give it to both wallets chain.add(makeSolvedTestBlock(blockStore.getChainHead().getHeader(), new Transaction(params, closeTx.bitcoinSerialize()))); - assertEquals(size.multiply(BigInteger.valueOf(5)), serverWallet.getBalance(new Wallet.DefaultCoinSelector() { + assertEquals(size.multiply(BigInteger.valueOf(5)), serverWallet.getBalance(new DefaultCoinSelector() { @Override protected boolean shouldSelect(Transaction tx) { if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING) @@ -203,7 +204,7 @@ public class PaymentChannelStateTest extends TestWithWallet { })); assertEquals(0, serverWallet.getPendingTransactions().size()); - assertEquals(Utils.COIN.subtract(size.multiply(BigInteger.valueOf(5))), wallet.getBalance(new Wallet.DefaultCoinSelector() { + assertEquals(Utils.COIN.subtract(size.multiply(BigInteger.valueOf(5))), wallet.getBalance(new DefaultCoinSelector() { @Override protected boolean shouldSelect(Transaction tx) { if (tx.getConfidence().getConfidenceType() == TransactionConfidence.ConfidenceType.BUILDING)