diff --git a/core/src/main/java/com/google/bitcoin/utils/TestWithWallet.java b/core/src/main/java/com/google/bitcoin/utils/TestWithWallet.java index d5a883be..fcbd49f1 100644 --- a/core/src/main/java/com/google/bitcoin/utils/TestWithWallet.java +++ b/core/src/main/java/com/google/bitcoin/utils/TestWithWallet.java @@ -21,6 +21,7 @@ import com.google.bitcoin.params.UnitTestParams; import com.google.bitcoin.store.BlockStore; import com.google.bitcoin.store.MemoryBlockStore; +import javax.annotation.Nullable; import java.io.IOException; import java.math.BigInteger; @@ -58,6 +59,7 @@ public class TestWithWallet { Wallet.SendRequest.DEFAULT_FEE_PER_KB = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; } + @Nullable protected Transaction sendMoneyToWallet(Wallet wallet, Transaction tx, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException { if (type == null) { @@ -70,26 +72,26 @@ public class TestWithWallet { if (type == AbstractBlockChain.NewBlockType.BEST_CHAIN) wallet.notifyNewBestBlock(bp.storedBlock); } - return tx; + return wallet.getTransaction(tx.getHash()); // Can be null if tx is a double spend that's otherwise irrelevant. } - protected Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type) throws IOException, - ProtocolException, VerificationException { + @Nullable + protected Transaction sendMoneyToWallet(Transaction tx, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException { return sendMoneyToWallet(this.wallet, tx, type); } - protected Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, Address toAddress, AbstractBlockChain.NewBlockType type) - throws IOException, ProtocolException, VerificationException { + @Nullable + protected Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, Address toAddress, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException { return sendMoneyToWallet(wallet, createFakeTx(params, value, toAddress), type); } - protected Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, ECKey toPubKey, AbstractBlockChain.NewBlockType type) - throws IOException, ProtocolException, VerificationException { + @Nullable + protected Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, ECKey toPubKey, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException { return sendMoneyToWallet(wallet, createFakeTx(params, value, toPubKey), type); } - protected Transaction sendMoneyToWallet(BigInteger value, AbstractBlockChain.NewBlockType type) throws IOException, - ProtocolException, VerificationException { + @Nullable + protected Transaction sendMoneyToWallet(BigInteger value, AbstractBlockChain.NewBlockType type) throws IOException, VerificationException { return sendMoneyToWallet(this.wallet, createFakeTx(params, value, myAddress), type); } } diff --git a/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java b/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java index 40b94a67..43984554 100644 --- a/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java +++ b/core/src/main/java/com/google/bitcoin/wallet/DefaultCoinSelector.java @@ -4,6 +4,7 @@ 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 com.google.common.annotations.VisibleForTesting; import java.math.BigInteger; import java.util.*; @@ -23,31 +24,7 @@ public class DefaultCoinSelector implements CoinSelector { // 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); - } - }); + sortOutputs(sortedOutputs); } // 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). @@ -64,6 +41,34 @@ public class DefaultCoinSelector implements CoinSelector { return new CoinSelection(BigInteger.valueOf(total), selected); } + @VisibleForTesting static void sortOutputs(ArrayList outputs) { + Collections.sort(outputs, 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); + } + }); + } + /** Sub-classes can override this to just customize whether transactions are usable, but keep age sorting. */ protected boolean shouldSelect(Transaction tx) { return isSelectable(tx); @@ -73,7 +78,8 @@ public class DefaultCoinSelector implements CoinSelector { // 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; + 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/wallet/DefaultCoinSelectorTest.java b/core/src/test/java/com/google/bitcoin/wallet/DefaultCoinSelectorTest.java new file mode 100644 index 00000000..c23e1dfa --- /dev/null +++ b/core/src/test/java/com/google/bitcoin/wallet/DefaultCoinSelectorTest.java @@ -0,0 +1,108 @@ +/** + * Copyright 2013 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.bitcoin.wallet; + +import com.google.bitcoin.core.*; +import com.google.bitcoin.params.UnitTestParams; +import com.google.bitcoin.utils.TestUtils; +import com.google.bitcoin.utils.TestWithWallet; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.math.BigInteger; +import java.net.InetAddress; +import java.util.ArrayList; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.junit.Assert.*; + +public class DefaultCoinSelectorTest extends TestWithWallet { + private static final NetworkParameters params = UnitTestParams.get(); + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + } + + @After + @Override + public void tearDown() throws Exception { + super.tearDown(); + } + + @Test + public void selectable() throws Exception { + Transaction t; + t = new Transaction(params); + t.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.PENDING); + assertFalse(DefaultCoinSelector.isSelectable(t)); + t.getConfidence().setSource(TransactionConfidence.Source.SELF); + assertFalse(DefaultCoinSelector.isSelectable(t)); + t.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByName("1.2.3.4"))); + assertFalse(DefaultCoinSelector.isSelectable(t)); + t.getConfidence().markBroadcastBy(new PeerAddress(InetAddress.getByName("5.6.7.8"))); + assertTrue(DefaultCoinSelector.isSelectable(t)); + t = new Transaction(params); + t.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.BUILDING); + assertTrue(DefaultCoinSelector.isSelectable(t)); + } + + @Test + public void depthOrdering() throws Exception { + // Send two transactions in two blocks on top of each other. + Transaction t1 = checkNotNull(sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN)); + Transaction t2 = checkNotNull(sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN)); + + // Check we selected just the oldest one. + DefaultCoinSelector selector = new DefaultCoinSelector(); + CoinSelection selection = selector.select(Utils.COIN, wallet.calculateAllSpendCandidates(true)); + assertTrue(selection.gathered.contains(t1.getOutputs().get(0))); + assertEquals(Utils.COIN, selection.valueGathered); + + // Check we ordered them correctly (by depth). + ArrayList candidates = new ArrayList(); + candidates.add(t2.getOutput(0)); + candidates.add(t1.getOutput(0)); + DefaultCoinSelector.sortOutputs(candidates); + assertEquals(t1.getOutput(0), candidates.get(0)); + assertEquals(t2.getOutput(0), candidates.get(1)); + } + + @Test + public void coinAgeOrdering() throws Exception { + // Send three transactions in four blocks on top of each other. Coin age of t1 is 1*4=4, coin age of t2 = 2*2=4 + // and t3=0.01. + Transaction t1 = checkNotNull(sendMoneyToWallet(Utils.COIN, AbstractBlockChain.NewBlockType.BEST_CHAIN)); + // Padding block. + wallet.notifyNewBestBlock(TestUtils.createFakeBlock(blockStore).storedBlock); + final BigInteger TWO_COINS = Utils.COIN.multiply(BigInteger.valueOf(2)); + Transaction t2 = checkNotNull(sendMoneyToWallet(TWO_COINS, AbstractBlockChain.NewBlockType.BEST_CHAIN)); + Transaction t3 = checkNotNull(sendMoneyToWallet(Utils.CENT, AbstractBlockChain.NewBlockType.BEST_CHAIN)); + + // Should be ordered t2, t1, t3. + ArrayList candidates = new ArrayList(); + candidates.add(t3.getOutput(0)); + candidates.add(t2.getOutput(0)); + candidates.add(t1.getOutput(0)); + DefaultCoinSelector.sortOutputs(candidates); + assertEquals(t2.getOutput(0), candidates.get(0)); + assertEquals(t1.getOutput(0), candidates.get(1)); + assertEquals(t3.getOutput(0), candidates.get(2)); + } +}