3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

Add some unit tests for default coin selector that are more fine grained than what the WalletTest code provides.

This commit is contained in:
Mike Hearn 2013-09-20 17:36:01 +02:00
parent 2b4595c4f0
commit 628aba15f8
3 changed files with 151 additions and 35 deletions

View File

@ -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);
}
}

View File

@ -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<TransactionOutput>() {
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<TransactionOutput> outputs) {
Collections.sort(outputs, new Comparator<TransactionOutput>() {
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;

View File

@ -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<TransactionOutput> candidates = new ArrayList<TransactionOutput>();
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<TransactionOutput> candidates = new ArrayList<TransactionOutput>();
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));
}
}