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 4f141fe9..248c011a 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -1648,13 +1648,18 @@ public class Wallet implements Serializable, BlockChainListener { */ public static class SendRequest { /** - * A transaction, probably incomplete, that describes the outline of what you want to do. This typically will + *

A transaction, probably incomplete, that describes the outline of what you want to do. This typically will * mean it has some outputs to the intended destinations, but no inputs or change address (and therefore no * fees) - the wallet will calculate all that for you and update tx later.

* *

Be careful when adding outputs that you check the min output value * ({@link TransactionOutput#getMinNonDustValue(BigInteger)}) to avoid the whole transaction being rejected * because one output is dust.

+ * + *

If there are already inputs to the transaction, make sure their out point has a connected output, + * otherwise their value will be added to fee. Also ensure they are either signed or are spendable by a wallet + * key, otherwise the behavior of {@link Wallet#completeTx(Wallet.SendRequest, boolean)} is undefined (likely + * RuntimeException).

*/ public Transaction tx; @@ -1666,15 +1671,37 @@ public class Wallet implements Serializable, BlockChainListener { public Address changeAddress = null; /** - * A transaction can have a fee attached, which is defined as the difference between the input values + *

A transaction can have a fee attached, which is defined as the difference between the input values * and output values. Any value taken in that is not provided to an output can be claimed by a miner. This * is how mining is incentivized in later years of the Bitcoin system when inflation drops. It also provides * a way for people to prioritize their transactions over others and is used as a way to make denial of service - * attacks expensive. Some transactions require a fee due to their structure - currently bitcoinj does not - * correctly calculate this! As of late 2012 most transactions require no fee. + * attacks expensive.

+ * + *

This is a constant fee (in satoshis) which will be added to the transaction. It is recommended that it be + * at least {@link Transaction#REFERENCE_DEFAULT_MIN_TX_FEE} if it is set, as default reference clients will + * otherwise simply treat the transaction as if there were no fee at all.

+ * + *

You might also consider adding a {@link SendRequest#feePerKb} to set the fee per kb of transaction size + * (rounded down to the nearest kb) as that is how transactions are sorted when added to a block by miners.

*/ public BigInteger fee = BigInteger.ZERO; + /** + *

A transaction can have a fee attached, which is defined as the difference between the input values + * and output values. Any value taken in that is not provided to an output can be claimed by a miner. This + * is how mining is incentivized in later years of the Bitcoin system when inflation drops. It also provides + * a way for people to prioritize their transactions over others and is used as a way to make denial of service + * attacks expensive.

+ * + *

This is a dynamic fee (in satoshis) which will be added to the transaction for each kilobyte in size after + * the first. This is useful as as miners usually sort pending transactions by their fee per unit size when + * choosing which transactions to add to a block. Note that, to keep this equivalent to the reference client + * definition, a kilobyte is defined as 1000 bytes, not 1024.

+ * + *

You might also consider using a {@link SendRequest#fee} to set the fee added for the first kb of size.

+ */ + public BigInteger feePerKb = BigInteger.ZERO; + /** * The AES key to use to decrypt the private keys before signing. * If null then no decryption will be performed and if decryption is required an exception will be thrown. @@ -1683,7 +1710,8 @@ public class Wallet implements Serializable, BlockChainListener { public KeyParameter aesKey = null; // Tracks if this has been passed to wallet.completeTx already: just a safety check. - private boolean completed; + // default for testing + boolean completed; private SendRequest() {} @@ -1733,8 +1761,9 @@ public class Wallet implements Serializable, BlockChainListener { * and lets you see the proposed transaction before anything is done with it.

* *

This is a helper method that is equivalent to using {@link Wallet.SendRequest#to(Address, java.math.BigInteger)} - * followed by {@link Wallet#completeTx(com.google.bitcoin.core.Wallet.SendRequest)} and returning the requests - * transaction object. If you want more control over the process, just do those two steps yourself.

+ * followed by {@link Wallet#completeTx(Wallet.SendRequest, true)} and returning the requests transaction object. + * Note that this means a fee may be automatically added if required, if you want more control over the process, + * just do those two steps yourself.

* *

IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions * that spend the same coins. You have to call {@link Wallet#commitTx(Transaction)} on the created transaction to @@ -1751,7 +1780,7 @@ public class Wallet implements Serializable, BlockChainListener { */ public Transaction createSend(Address address, BigInteger nanocoins) { SendRequest req = SendRequest.to(address, nanocoins); - if (completeTx(req)) { + if (completeTx(req, true) != null) { return req.tx; } else { return null; // No money. @@ -1762,14 +1791,19 @@ public class Wallet implements Serializable, BlockChainListener { * Sends coins to the given address but does not broadcast the resulting pending transaction. It is still stored * in the wallet, so when the wallet is added to a {@link PeerGroup} or {@link Peer} the transaction will be * announced to the network. The given {@link SendRequest} is completed first using - * {@link Wallet#completeTx(com.google.bitcoin.core.Wallet.SendRequest)} to make it valid. + * {@link Wallet#completeTx(Wallet.SendRequest, boolean)} to make it valid. * + * @param enforceDefaultReferenceClientFeeRelayRules Requires that there be enough fee for a default reference client to at least relay the transaction. + * (ie ensure the transaction will not be outright rejected by the network). + * Note that this does not enforce certain fee rules that only apply to transactions which are larger than + * 26,000 bytes. If you get a transaction which is that large, you should set a fee and feePerKb of at least + * {@link Transaction#REFERENCE_DEFAULT_MIN_TX_FEE} * @return the Transaction that was created, or null if there are insufficient coins in the wallet. */ - public Transaction sendCoinsOffline(SendRequest request) { + public Transaction sendCoinsOffline(SendRequest request, boolean enforceDefaultReferenceClientFeeRelayRules) { lock.lock(); try { - if (!completeTx(request)) + if (completeTx(request, enforceDefaultReferenceClientFeeRelayRules) == null) return null; // Not enough money! :-( commitTx(request.tx); return request.tx; @@ -1782,7 +1816,8 @@ public class Wallet implements Serializable, BlockChainListener { /** *

Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to - * {@link Wallet#getChangeAddress()}. No fee is attached even if one would be required.

+ * {@link Wallet#getChangeAddress()}. Note that a fee may be automatically added if one may be required for the + * transaction to be confirmed.

* *

The returned object provides both the transaction, and a future that can be used to learn when the broadcast * is complete. Complete means, if the PeerGroup is limited to only one connection, when it was written out to @@ -1802,7 +1837,7 @@ public class Wallet implements Serializable, BlockChainListener { */ public SendResult sendCoins(PeerGroup peerGroup, Address to, BigInteger value) { SendRequest request = SendRequest.to(to, value); - return sendCoins(peerGroup, request); + return sendCoins(peerGroup, request, true); } /** @@ -1818,14 +1853,19 @@ public class Wallet implements Serializable, BlockChainListener { * * @param peerGroup a PeerGroup to use for broadcast or null. * @param request the SendRequest that describes what to do, get one using static methods on SendRequest itself. + * @param enforceDefaultReferenceClientFeeRelayRules Requires that there be enough fee for a default reference client to at least relay the transaction + * (ie ensure the transaction will not be outright rejected by the network). + * Note that this does not enforce certain fee rules that only apply to transactions which are larger than + * 26,000 bytes. If you get a transaction which is that large, you should set a fee and feePerKb of at least + * {@link Transaction#REFERENCE_DEFAULT_MIN_TX_FEE} * @return An object containing the transaction that was created, and a future for the broadcast of it. */ - public SendResult sendCoins(PeerGroup peerGroup, SendRequest request) { + public SendResult sendCoins(PeerGroup peerGroup, SendRequest request, boolean enforceDefaultReferenceClientFeeRelayRules) { // Does not need to be synchronized as sendCoinsOffline is and the rest is all thread-local. // Commit the TX to the wallet immediately so the spent coins won't be reused. // TODO: We should probably allow the request to specify tx commit only after the network has accepted it. - Transaction tx = sendCoinsOffline(request); + Transaction tx = sendCoinsOffline(request, enforceDefaultReferenceClientFeeRelayRules); if (tx == null) return null; // Not enough money. SendResult result = new SendResult(); @@ -1842,13 +1882,14 @@ public class Wallet implements Serializable, BlockChainListener { /** * Sends coins to the given address, via the given {@link Peer}. Change is returned to {@link Wallet#getChangeAddress()}. * If an exception is thrown by {@link Peer#sendMessage(Message)} the transaction is still committed, so the - * pending transaction must be broadcast by you at some other time. + * pending transaction must be broadcast by you at some other time. Note that a fee may be automatically added + * if one may be required for the transaction to be confirmed. * * @return The {@link Transaction} that was created or null if there was insufficient balance to send the coins. * @throws IOException if there was a problem broadcasting the transaction */ public Transaction sendCoins(Peer peer, SendRequest request) throws IOException { - Transaction tx = sendCoinsOffline(request); + Transaction tx = sendCoinsOffline(request, true); if (tx == null) return null; // Not enough money. peer.sendMessage(tx); @@ -1860,10 +1901,15 @@ public class Wallet implements Serializable, BlockChainListener { * to the instructions in the request. The transaction in the request is modified by this method. * * @param req a SendRequest that contains the incomplete transaction and details for how to make it valid. + * @param enforceDefaultReferenceClientFeeRelayRules Requires that there be enough fee for a default reference client to at least relay the transaction + * (ie ensure the transaction will not be outright rejected by the network). + * Note that this does not enforce certain fee rules that only apply to transactions which are larger than + * 26,000 bytes. If you get a transaction which is that large, you should set a fee and feePerKb of at least + * {@link Transaction#REFERENCE_DEFAULT_MIN_TX_FEE} * @throws IllegalArgumentException if you try and complete the same SendRequest twice. - * @return False if we cannot afford this send, true otherwise. + * @return Either the total fee paid (assuming all existing inputs had a connected output) or null if we cannot afford the transaction. */ - public boolean completeTx(SendRequest req) { + public BigInteger completeTx(SendRequest req, boolean enforceDefaultReferenceClientFeeRelayRules) { lock.lock(); try { Preconditions.checkArgument(!req.completed, "Given SendRequest has already been completed."); @@ -1872,11 +1918,32 @@ public class Wallet implements Serializable, BlockChainListener { for (TransactionOutput output : req.tx.getOutputs()) { value = value.add(output.getValue()); } - value = value.add(req.fee); + BigInteger totalOutput = value; - log.info("Completing send tx with {} outputs totalling {}", + log.info("Completing send tx with {} outputs totalling {} (not including fees)", req.tx.getOutputs().size(), bitcoinValueToFriendlyString(value)); + // If any inputs have already been added, we don't need to get their value from wallet + BigInteger totalInput = BigInteger.ZERO; + for (TransactionInput input : req.tx.getInputs()) + if (input.getConnectedOutput() != null) + totalInput = totalInput.add(input.getConnectedOutput().getValue()); + else + log.warn("SendRequest transaction already has inputs but we don't know how much they are worth - they will be added to fee."); + value = value.subtract(totalInput); + + List originalInputs = new ArrayList(req.tx.getInputs()); + + // We need to know if we need to add an additional fee because one of our values are smaller than 0.01 BTC + boolean needAtLeastReferenceFee = false; + if (enforceDefaultReferenceClientFeeRelayRules) { + for (TransactionOutput output : req.tx.getOutputs()) + if (output.getValue().compareTo(Utils.CENT) < 0) { + needAtLeastReferenceFee = true; + break; + } + } + // 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 // can customize coin selection policies. @@ -1886,30 +1953,198 @@ public class Wallet implements Serializable, BlockChainListener { // Note that output.isMine(this) needs to test the keychain which is currently an array, so it's // O(candidate outputs ^ keychain.size())! There's lots of low hanging fruit here. LinkedList candidates = calculateSpendCandidates(true); - // Of the coins we could spend, pick some that we actually will spend. - CoinSelection selection = coinSelector.select(value, candidates); - // Can we afford this? - if (selection.valueGathered.compareTo(value) < 0) { - log.warn("Insufficient value in wallet for send, missing " + - bitcoinValueToFriendlyString(value.subtract(selection.valueGathered))); + Address changeAddress = req.changeAddress; + int minSize = 0; + // There are 3 possibilities for what adding change might do: + // 1) No effect + // 2) Causes increase in fee (change < 0.01 COINS) + // 3) Causes the transaction to have a dust output or change < fee increase (ie change will be thrown away) + // If we get either of the last 2, we keep note of what the inputs looked like at the time and move try to + // add inputs as we go up the list (keeping track of minimum inputs for each category). At the end, we pick + // the best input set as the one which generates the lowest total fee. + BigInteger additionalValueForNextCategory = null; + CoinSelection selection3 = null; + CoinSelection selection2 = null; TransactionOutput selection2Change = null; + CoinSelection selection1 = null; TransactionOutput selection1Change = null; + while (true) { + req.tx.clearInputs(); + for (TransactionInput input : originalInputs) + req.tx.addInput(input); + + BigInteger fees = req.fee.add(BigInteger.valueOf(minSize/1000).multiply(req.feePerKb)); + if (needAtLeastReferenceFee && fees.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) + fees = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; + + BigInteger valueNeeded = value.add(fees); + if (additionalValueForNextCategory != null) + valueNeeded = valueNeeded.add(additionalValueForNextCategory); + BigInteger additionalValueSelected = additionalValueForNextCategory; + + // Of the coins we could spend, pick some that we actually will spend. + CoinSelection selection = coinSelector.select(valueNeeded, candidates); + // Can we afford this? + if (selection.valueGathered.compareTo(valueNeeded) < 0) + break; + checkState(selection.gathered.size() > 0 || originalInputs.size() > 0); + + // We keep track of an upper bound on transaction size to calculate fees that need added + // Note that the difference between the upper bound and lower bound is usually small enough that it + // will be very rare that we pay a fee we do not need to + int size = 0; + + // We can't be sure a selection is valid until we check fee per kb at the end, so we just store them here temporarily + boolean eitherCategory2Or3 = false; + boolean isCategory3 = false; + + BigInteger change = selection.valueGathered.subtract(valueNeeded); + if (additionalValueSelected != null) + change = change.add(additionalValueSelected); + + TransactionOutput changeOutput = null; + // If change is < 0.01 BTC, we will need to have at least minfee to be accepted by the network + if (enforceDefaultReferenceClientFeeRelayRules && !change.equals(BigInteger.ZERO) && + change.compareTo(Utils.CENT) < 0 && fees.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) { + // This solution may fit into category 2, but it may also be category 3, we'll check that later + eitherCategory2Or3 = true; + additionalValueForNextCategory = Utils.CENT; + // If the change is smaller than the fee we want to add, this will be negative + change = change.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fees)); + } + + 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. The address comes either from the request or getChangeAddress() as a default.. + if (changeAddress == null) + changeAddress = getChangeAddress(); + changeOutput = new TransactionOutput(params, req.tx, change, changeAddress); + // If the change output would result in this transaction being rejected as dust, just drop the change and make it a fee + if (enforceDefaultReferenceClientFeeRelayRules && Transaction.MIN_NONDUST_OUTPUT.compareTo(change) >= 0) { + // This solution definitely fits in category 3 + isCategory3 = true; + additionalValueForNextCategory = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add( + Transaction.MIN_NONDUST_OUTPUT.add(BigInteger.ONE)); + } else { + size += changeOutput.bitcoinSerialize().length + VarInt.sizeOf(req.tx.getOutputs().size()) - VarInt.sizeOf(req.tx.getOutputs().size() - 1); + // This solution is either category 1 or 2 + if (!eitherCategory2Or3) // must be category 1 + additionalValueForNextCategory = null; + } + } else { + if (eitherCategory2Or3) { + // This solution definitely fits in category 3 (we threw away change because it was smaller than MIN_TX_FEE) + isCategory3 = true; + additionalValueForNextCategory = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE); + } + } + + for (TransactionOutput output : selection.gathered) { + req.tx.addInput(output); + // If the scriptBytes don't default to none, our size calculations will be thrown off + checkState(req.tx.getInput(req.tx.getInputs().size()-1).getScriptBytes().length == 0); + try { + if (output.getScriptPubKey().isSentToAddress()) { + // Send-to-address spends usually take maximum pubkey.length (as it may be compressed or not) + 75 bytes + size += this.findKeyFromPubHash(output.getScriptPubKey().getPubKeyHash()).getPubKey().length + 75; + } else if (output.getScriptPubKey().isSentToRawPubKey()) + size += 74; // Send-to-pubkey spends usually take maximum 74 bytes to spend + else + throw new RuntimeException("Unknown output type returned in coin selection"); + } 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 RuntimeException(e); + } + } + + // Estimate transaction size and loop again if we need more fee per kb + size += req.tx.bitcoinSerialize().length; + if (size/1000 > minSize/1000 && req.feePerKb.compareTo(BigInteger.ZERO) > 0) { + minSize = size; + // We need more fees anyway, just try again with the same additional value + additionalValueForNextCategory = additionalValueSelected; + continue; + } + + if (isCategory3) { + if (selection3 == null) + selection3 = selection; + } else if (eitherCategory2Or3) { + // If we are in selection2, we will require at least CENT additional. If we do that, there is no way + // we can end up back here because CENT additional will always get us to 1 + checkState(selection2 == null); + checkState(additionalValueForNextCategory.equals(Utils.CENT)); + selection2 = selection; + selection2Change = checkNotNull(changeOutput); // If we get no change in category 2, we are actually in category 3 + } else { + // Once we get a category 1 (change kept), we should break out of the loop because we can't do better + checkState(selection1 == null); + checkState(additionalValueForNextCategory == null); + selection1 = selection; + selection1Change = changeOutput; + } + + if (additionalValueForNextCategory != null) { + if (additionalValueSelected != null) + checkState(additionalValueForNextCategory.compareTo(additionalValueSelected) > 0); + continue; + } + break; + } + + req.tx.clearInputs(); + for (TransactionInput input : originalInputs) + req.tx.addInput(input); + + if (selection3 == null && selection2 == null && selection1 == null) { + log.warn("Insufficient value in wallet for send"); // TODO: Should throw an exception here. - return false; + return null; } - checkState(selection.gathered.size() > 0); - req.tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); - BigInteger change = selection.valueGathered.subtract(value); - 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. The address comes either from the request or getChangeAddress() as a default. - Address changeAddress = req.changeAddress != null ? req.changeAddress : getChangeAddress(); - log.info(" with {} coins change", bitcoinValueToFriendlyString(change)); - req.tx.addOutput(new TransactionOutput(params, req.tx, change, changeAddress)); + + BigInteger lowestFee = null; + CoinSelection bestCoinSelection = null; + TransactionOutput bestChangeOutput = null; + if (selection1 != null) { + if (selection1Change != null) + lowestFee = selection1.valueGathered.subtract(selection1Change.getValue()); + else + lowestFee = selection1.valueGathered; + bestCoinSelection = selection1; + bestChangeOutput = selection1Change; } - for (TransactionOutput output : selection.gathered) { + + if (selection2 != null) { + BigInteger fee = selection2.valueGathered.subtract(checkNotNull(selection2Change).getValue()); + if (lowestFee == null || fee.compareTo(lowestFee) < 0) { + lowestFee = fee; + bestCoinSelection = selection2; + bestChangeOutput = selection2Change; + } + } + + if (selection3 != null) { + if (lowestFee == null || selection3.valueGathered.compareTo(lowestFee) < 0) { + bestCoinSelection = selection3; + bestChangeOutput = null; + } + } + + for (TransactionOutput output : bestCoinSelection.gathered) req.tx.addInput(output); + + totalInput = totalInput.add(bestCoinSelection.valueGathered); + + req.tx.getConfidence().setConfidenceType(ConfidenceType.PENDING); + + if (bestChangeOutput != null) { + req.tx.addOutput(bestChangeOutput); + totalOutput = totalOutput.add(bestChangeOutput.getValue()); + log.info(" with {} coins change", bitcoinValueToFriendlyString(bestChangeOutput.getValue())); } + //TODO: Shuffle inputs for some anonymity + // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs. try { req.tx.signInputs(Transaction.SigHash.ALL, this, req.aesKey); @@ -1925,7 +2160,7 @@ public class Wallet implements Serializable, BlockChainListener { // TODO: Throw an exception here. log.error("Transaction could not be created without exceeding max size: {} vs {}", size, Transaction.MAX_STANDARD_TX_SIZE); - return false; + return null; } // Label the transaction as being self created. We can use this later to spend its change output even before @@ -1934,7 +2169,7 @@ public class Wallet implements Serializable, BlockChainListener { req.completed = true; log.info(" completed {} with {} inputs", req.tx.getHashAsString(), req.tx.getInputs().size()); - return true; + return totalInput.subtract(totalOutput); } finally { lock.unlock(); } diff --git a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java index 0efa8aa1..df202590 100644 --- a/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java +++ b/core/src/test/java/com/google/bitcoin/core/PeerGroupTest.java @@ -306,7 +306,7 @@ public class PeerGroupTest extends TestWithPeerGroup { // Do the same thing with an offline transaction. peerGroup.removeWallet(wallet); - Transaction t3 = wallet.sendCoinsOffline(Wallet.SendRequest.to(dest, Utils.toNanoCoins(2, 0))); + Transaction t3 = wallet.sendCoinsOffline(Wallet.SendRequest.to(dest, Utils.toNanoCoins(2, 0)), false); assertNull(outbound(p1)); // Nothing sent. // Add the wallet to the peer group (simulate initialization). Transactions should be announced. peerGroup.addWallet(wallet); diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java index 89cd708e..a55fe20d 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -17,6 +17,7 @@ package com.google.bitcoin.core; import com.google.bitcoin.core.Transaction.SigHash; +import com.google.bitcoin.core.Wallet.SendRequest; import com.google.bitcoin.core.WalletTransaction.Pool; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; @@ -51,6 +52,7 @@ import static com.google.bitcoin.core.TestUtils.createFakeTx; import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString; import static com.google.bitcoin.core.Utils.toNanoCoins; import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public class WalletTest extends TestWithWallet { public Logger log = LoggerFactory.getLogger(WalletTest.class.getName()); @@ -125,7 +127,7 @@ public class WalletTest extends TestWithWallet { if (testEncryption) { // Try to create a send with a fee but no password (this should fail). try { - wallet.completeTx(req); + wallet.completeTx(req, false); fail("No exception was thrown trying to sign an encrypted key with no password supplied."); } catch (KeyCrypterException kce) { assertEquals("This ECKey is encrypted but no decryption key has been supplied.", kce.getMessage()); @@ -139,7 +141,7 @@ public class WalletTest extends TestWithWallet { req.fee = toNanoCoins(0, 1); try { - wallet.completeTx(req); + wallet.completeTx(req, false); fail("No exception was thrown trying to sign an encrypted key with the wrong password supplied."); } catch (KeyCrypterException kce) { assertEquals("Could not decrypt bytes", kce.getMessage()); @@ -155,7 +157,7 @@ public class WalletTest extends TestWithWallet { } // Complete the transaction successfully. - wallet.completeTx(req); + wallet.completeTx(req, false); Transaction t2 = req.tx; assertEquals("Wrong number of UNSPENT.3", 1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); @@ -236,7 +238,7 @@ public class WalletTest extends TestWithWallet { Wallet.SendRequest req = Wallet.SendRequest.to(new ECKey().toAddress(params), toNanoCoins(0, 48)); req.aesKey = aesKey; Address a = req.changeAddress = new ECKey().toAddress(params); - wallet.completeTx(req); + wallet.completeTx(req, false); Transaction t3 = req.tx; assertEquals(a, t3.getOutput(1).getScriptPubKey().getToAddress(params)); assertNotNull(t3); @@ -269,7 +271,7 @@ public class WalletTest extends TestWithWallet { t2.addOutput(v2, a2); t2.addOutput(v3, a2); t2.addOutput(v4, a2); - boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2)); + boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2), false) != null; // Do some basic sanity checks. assertTrue(complete); @@ -972,7 +974,7 @@ public class WalletTest extends TestWithWallet { Transaction t2 = new Transaction(params); TransactionOutput o2 = new TransactionOutput(params, t2, v2, k2.toAddress(params)); t2.addOutput(o2); - boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2)); + boolean complete = wallet.completeTx(Wallet.SendRequest.forTx(t2), false) != null; assertTrue(complete); // Commit t2, so it is placed in the pending pool @@ -1173,7 +1175,382 @@ public class WalletTest extends TestWithWallet { tx.addOutput(v, new Address(params, bits)); } Wallet.SendRequest req = Wallet.SendRequest.forTx(tx); - assertFalse(wallet.completeTx(req)); + assertNull(wallet.completeTx(req, true)); + } + + @Test + public void feeSolverAndCoinSelectionTest() throws Exception { + // Make sure TestWithWallet isnt doing anything crazy. + assertTrue(wallet.getTransactions(true).size() == 0); + + Address notMyAddr = new ECKey().toAddress(params); + + // Generate a few outputs to us that are far too small to spend reasonably + StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); + Transaction tx1 = createFakeTx(params, BigInteger.ONE, myAddress); + wallet.receiveFromBlock(tx1, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + Transaction tx2 = createFakeTx(params, BigInteger.ONE, myAddress); assertTrue(!tx1.getHash().equals(tx2.getHash())); + wallet.receiveFromBlock(tx2, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + Transaction tx3 = createFakeTx(params, BigInteger.TEN, myAddress); + wallet.receiveFromBlock(tx3, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + + // No way we can add nearly enough fee + assertTrue(wallet.createSend(notMyAddr, BigInteger.ONE) == null); + // Spend it all without fee enforcement + assertTrue(wallet.sendCoinsOffline(Wallet.SendRequest.to(notMyAddr, BigInteger.TEN.add(BigInteger.ONE.add(BigInteger.ONE))), false) != null); + assertTrue(wallet.getBalance().equals(BigInteger.ZERO)); + + // Add some reasonable-sized outputs + block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); + Transaction tx4 = createFakeTx(params, Utils.COIN, myAddress); + wallet.receiveFromBlock(tx4, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + + // Simple test to make sure if we have an ouput < 0.01 we get a fee + Transaction spend1 = wallet.createSend(notMyAddr, Utils.CENT.subtract(BigInteger.ONE)); + assertTrue(spend1.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend1.getOutput(0).getValue().add(spend1.getOutput(1).getValue()) + .equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); // We should have paid the default minfee + + // But not at exactly 0.01 + Transaction spend2 = wallet.createSend(notMyAddr, Utils.CENT); + assertTrue(spend2.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend2.getOutput(0).getValue().add(spend2.getOutput(1).getValue()).equals(Utils.COIN)); + + // ...but not more fee than what we request + SendRequest request3 = SendRequest.to(notMyAddr, Utils.CENT.subtract(BigInteger.ONE)); + request3.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE); + assertEquals(wallet.completeTx(request3, true), request3.fee); + Transaction spend3 = request3.tx; + assertTrue(spend3.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend3.getOutput(0).getValue().add(spend3.getOutput(1).getValue()) + .equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE)))); + + // ...unless we need it + SendRequest request4 = SendRequest.to(notMyAddr, Utils.CENT.subtract(BigInteger.ONE)); + request4.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(BigInteger.ONE); + assertEquals(wallet.completeTx(request4, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + Transaction spend4 = request4.tx; + assertTrue(spend4.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend4.getOutput(0).getValue().add(spend4.getOutput(1).getValue()) + .equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + SendRequest request5 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.subtract(BigInteger.ONE))); + assertEquals(wallet.completeTx(request5, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + Transaction spend5 = request5.tx; + // If we would have a change output < 0.01, it should add the fee + assertTrue(spend5.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend5.getOutput(0).getValue().add(spend5.getOutput(1).getValue()) + .equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + SendRequest request6 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT)); + assertEquals(wallet.completeTx(request6, true), BigInteger.ZERO); + Transaction spend6 = request6.tx; + // ...but not if change output == 0.01 + assertTrue(spend6.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend6.getOutput(0).getValue().add(spend6.getOutput(1).getValue()).equals(Utils.COIN)); + + SendRequest request7 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Utils.CENT.subtract(BigInteger.valueOf(2)).multiply(BigInteger.valueOf(2)))); + request7.tx.addOutput(Utils.CENT.subtract(BigInteger.ONE), notMyAddr); + assertEquals(wallet.completeTx(request7, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + Transaction spend7 = request7.tx; + // If change is 0.1-nanocoin and we already have a 0.1-nanocoin output, fee should be reference fee + assertTrue(spend7.getOutputs().size() == 3); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend7.getOutput(0).getValue().add(spend7.getOutput(1).getValue()).add(spend7.getOutput(2).getValue()) + .equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + SendRequest request8 = SendRequest.to(notMyAddr, Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE)); + assertEquals(wallet.completeTx(request8, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + Transaction spend8 = request8.tx; + // If we would have a change output == REFERENCE_DEFAULT_MIN_TX_FEE that would cause a fee, throw it away and make it fee + assertTrue(spend8.getOutputs().size() == 1); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend8.getOutput(0).getValue().equals(Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + SendRequest request9 = SendRequest.to(notMyAddr, Utils.COIN.subtract( + Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT))); + assertEquals(wallet.completeTx(request9, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)); + Transaction spend9 = request9.tx; + // ...in fact, also add fee if we would get back less than MIN_NONDUST_OUTPUT + assertTrue(spend9.getOutputs().size() == 1); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend9.getOutput(0).getValue().equals( + Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)))); + + SendRequest request10 = SendRequest.to(notMyAddr, Utils.COIN.subtract( + Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).add(BigInteger.ONE))); + assertEquals(wallet.completeTx(request10, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE); + Transaction spend10 = request10.tx; + // ...but if we get back any more than that, we should get a refund (but still pay fee) + assertTrue(spend10.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend10.getOutput(0).getValue().add(spend10.getOutput(1).getValue()).equals( + Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + SendRequest request11 = SendRequest.to(notMyAddr, Utils.COIN.subtract( + Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT).add(BigInteger.valueOf(2)))); + request11.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE); + assertEquals(wallet.completeTx(request11, true), request11.fee); + Transaction spend11 = request11.tx; + // ...of course fee should be min(request.fee, MIN_TX_FEE) so we should get MIN_TX_FEE.add(ONE) here + assertTrue(spend11.getOutputs().size() == 2); + // We optimize for priority, so the output selected should be the largest one + assertTrue(spend11.getOutput(0).getValue().add(spend11.getOutput(1).getValue()).equals( + Utils.COIN.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(BigInteger.ONE)))); + + // Remove the coin from our wallet + wallet.commitTx(spend11); + Transaction tx5 = createFakeTx(params, Utils.CENT, myAddress); + wallet.receiveFromBlock(tx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + assertTrue(wallet.getBalance().equals(Utils.CENT)); + + // Now test coin selection properly selects coin*depth + for (int i = 0; i < 100; i++) { + block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); + wallet.notifyNewBestBlock(block); + } + + block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); + Transaction tx6 = createFakeTx(params, Utils.COIN, myAddress); + wallet.receiveFromBlock(tx6, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + assertTrue(tx5.getOutput(0).isMine(wallet) && tx5.getOutput(0).isAvailableForSpending() && tx5.getConfidence().getDepthInBlocks() == 100); + assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 1); + + // tx5 and tx6 have exactly the same coin*depth, so the larger should be selected... + Transaction spend12 = wallet.createSend(notMyAddr, Utils.CENT); + assertTrue(spend12.getOutputs().size() == 2 && spend12.getOutput(0).getValue().add(spend12.getOutput(1).getValue()).equals(Utils.COIN)); + + wallet.notifyNewBestBlock(block); + assertTrue(tx5.getOutput(0).isMine(wallet) && tx5.getOutput(0).isAvailableForSpending() && tx5.getConfidence().getDepthInBlocks() == 101); + assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 1); + // Now tx5 has slightly higher coin*depth than tx6... + Transaction spend13 = wallet.createSend(notMyAddr, Utils.CENT); + assertTrue(spend13.getOutputs().size() == 1 && spend13.getOutput(0).getValue().equals(Utils.CENT)); + + block = new StoredBlock(makeSolvedTestBlock(blockStore, notMyAddr), BigInteger.ONE, 1); + wallet.notifyNewBestBlock(block); + assertTrue(tx5.getOutput(0).isMine(wallet) && tx5.getOutput(0).isAvailableForSpending() && tx5.getConfidence().getDepthInBlocks() == 102); + assertTrue(tx6.getOutput(0).isMine(wallet) && tx6.getOutput(0).isAvailableForSpending() && tx6.getConfidence().getDepthInBlocks() == 2); + // Now tx6 has higher coin*depth than tx5... + Transaction spend14 = wallet.createSend(notMyAddr, Utils.CENT); + assertTrue(spend14.getOutputs().size() == 2 && spend14.getOutput(0).getValue().add(spend14.getOutput(1).getValue()).equals(Utils.COIN)); + + // Now test feePerKb + SendRequest request15 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 29; i++) + request15.tx.addOutput(Utils.CENT, notMyAddr); + assertTrue(request15.tx.bitcoinSerialize().length > 1000); + request15.feePerKb = BigInteger.ONE; + assertEquals(wallet.completeTx(request15, true), BigInteger.ONE); + Transaction spend15 = request15.tx; + // If a transaction is over 1kb, the set fee should be added + assertTrue(spend15.getOutputs().size() == 31); + // We optimize for priority, so the output selected should be the largest one + BigInteger outValue15 = BigInteger.ZERO; + for (TransactionOutput out : spend15.getOutputs()) + outValue15 = outValue15.add(out.getValue()); + assertTrue(outValue15.equals(Utils.COIN.subtract(BigInteger.ONE))); + + SendRequest request16 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 29; i++) + request16.tx.addOutput(Utils.CENT, notMyAddr); + assertTrue(request16.tx.bitcoinSerialize().length > 1000); + assertEquals(wallet.completeTx(request16, true), BigInteger.ZERO); + Transaction spend16 = request16.tx; + // Of course the fee shouldn't be added if feePerKb == 0 + assertTrue(spend16.getOutputs().size() == 31); + // We optimize for priority, so the output selected should be the largest one + BigInteger outValue16 = BigInteger.ZERO; + for (TransactionOutput out : spend16.getOutputs()) + outValue16 = outValue16.add(out.getValue()); + assertTrue(outValue16.equals(Utils.COIN)); + + // Create a transaction who's max size could be up to 999 (if signatures were maximum size) + SendRequest request17 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 22; i++) + request17.tx.addOutput(Utils.CENT, notMyAddr); + request17.tx.addOutput(new TransactionOutput(params, request17.tx, Utils.CENT, new byte[15])); + request17.feePerKb = BigInteger.ONE; + assertTrue(wallet.completeTx(request17, true).equals(BigInteger.ZERO) && request17.tx.getInputs().size() == 1); + // Calculate its max length to make sure it is indeed 999 + int theoreticalMaxLength17 = request17.tx.bitcoinSerialize().length + myKey.getPubKey().length + 75; + for (TransactionInput in : request17.tx.getInputs()) + theoreticalMaxLength17 -= in.getScriptBytes().length; + assertTrue(theoreticalMaxLength17 == 999); + Transaction spend17 = request17.tx; + // Its actual size must be between 997 and 999 (inclusive) as signatures have a 3-byte size range (almost always) + assertTrue(spend17.bitcoinSerialize().length >= 997 && spend17.bitcoinSerialize().length <= 999); + // Now check that it didn't get a fee since its max size is 999 + assertTrue(spend17.getOutputs().size() == 25); + // We optimize for priority, so the output selected should be the largest one + BigInteger outValue17 = BigInteger.ZERO; + for (TransactionOutput out : spend17.getOutputs()) + outValue17 = outValue17.add(out.getValue()); + assertTrue(outValue17.equals(Utils.COIN)); + + // Create a transaction who's max size could be up to 1000 (if signatures were maximum size) + SendRequest request18 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 22; i++) + request18.tx.addOutput(Utils.CENT, notMyAddr); + request18.tx.addOutput(new TransactionOutput(params, request18.tx, Utils.CENT, new byte[16])); + request18.feePerKb = BigInteger.ONE; + assertTrue(wallet.completeTx(request18, true).equals(BigInteger.ONE) && request18.tx.getInputs().size() == 1); + // Calculate its max length to make sure it is indeed 1000 + Transaction spend18 = request18.tx; + int theoreticalMaxLength18 = spend18.bitcoinSerialize().length + myKey.getPubKey().length + 75; + for (TransactionInput in : spend18.getInputs()) + theoreticalMaxLength18 -= in.getScriptBytes().length; + assertTrue(theoreticalMaxLength18 == 1000); + // Its actual size must be between 998 and 1000 (inclusive) as signatures have a 3-byte size range (almost always) + assertTrue(spend18.bitcoinSerialize().length >= 998 && spend18.bitcoinSerialize().length <= 1000); + // Now check that it did get a fee since its max size is 1000 + assertTrue(spend18.getOutputs().size() == 25); + // We optimize for priority, so the output selected should be the largest one + BigInteger outValue18 = BigInteger.ZERO; + for (TransactionOutput out : spend18.getOutputs()) + outValue18 = outValue18.add(out.getValue()); + assertTrue(outValue18.equals(Utils.COIN.subtract(BigInteger.ONE))); + + // Now create a transaction that will spend COIN + fee, which makes it require both inputs + assertTrue(wallet.getBalance().equals(Utils.CENT.add(Utils.COIN))); + SendRequest request19 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request19.tx.addOutput(Utils.CENT, notMyAddr); + // If we send now, we shouldnt need a fee and should only have to spend our COIN + assertTrue(wallet.completeTx(request19, true).equals(BigInteger.ZERO) + && request19.tx.getInputs().size() == 1 && request19.tx.getOutputs().size() == 100); + // Now reset request19 and give it a fee per kb + request19.completed = false; request19.tx.clearInputs(); + request19.feePerKb = BigInteger.ONE; + assertTrue(wallet.completeTx(request19, true).equals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) && request19.tx.getInputs().size() == 2); + BigInteger outValue19 = BigInteger.ZERO; + for (TransactionOutput out : request19.tx.getOutputs()) + outValue19 = outValue19.add(out.getValue()); + // But now our change output is CENT-minfee, so we have to pay min fee + // Change this assert when we eventually randomize output order + assertTrue(request19.tx.getOutput(request19.tx.getOutputs().size()-1).getValue().equals(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + assertTrue(outValue19.equals(Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + // Create another transaction that will spend COIN + fee, which makes it require both inputs + SendRequest request20 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request20.tx.addOutput(Utils.CENT, notMyAddr); + // If we send now, we shouldnt need a fee and should only have to spend our COIN + assertTrue(wallet.completeTx(request20, true).equals(BigInteger.ZERO) + && request20.tx.getInputs().size() == 1 && request20.tx.getOutputs().size() == 100); + // Now reset request19 and give it a fee per kb + request20.completed = false; request20.tx.clearInputs(); + request20.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE; + assertTrue(wallet.completeTx(request20, true).equals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(3))) + && request20.tx.getInputs().size() == 2); + BigInteger outValue20 = BigInteger.ZERO; + for (TransactionOutput out : request20.tx.getOutputs()) + outValue20 = outValue20.add(out.getValue()); + // This time the fee we wanted to pay was more, so that should be what we paid + assertTrue(outValue20.equals(Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.multiply(BigInteger.valueOf(3))))); + + // Same as request 19, but make the change 0 (so it doesnt force fee) and make us require min fee as a result of an output < CENT + SendRequest request21 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request21.tx.addOutput(Utils.CENT, notMyAddr); + request21.tx.addOutput(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr); + // If we send without a feePerKb, we should still require REFERENCE_DEFAULT_MIN_TX_FEE because we have an output < 0.01 + assertTrue(wallet.completeTx(request21, true).equals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) && request21.tx.getInputs().size() == 2); + BigInteger outValue21 = BigInteger.ZERO; + for (TransactionOutput out : request21.tx.getOutputs()) + outValue21 = outValue21.add(out.getValue()); + assertTrue(outValue21.equals(Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + // Now create an identical request22 and give it a fee per kb slightly less than what we will have to pay + SendRequest request22 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request22.tx.addOutput(Utils.CENT, notMyAddr); + request22.tx.addOutput(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr); + assertTrue(!Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.mod(BigInteger.valueOf(3)).equals(BigInteger.ZERO)); // This test won't work if REFERENCE_DEFAULT_MIN_TX_FEE is divisiable by 3 + request22.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(BigInteger.valueOf(3)); + // Now check that we get the same exact transaction back + assertTrue(wallet.completeTx(request22, true).equals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) && request22.tx.getInputs().size() == 2); + BigInteger outValue22 = BigInteger.ZERO; + for (TransactionOutput out : request22.tx.getOutputs()) + outValue22 = outValue22.add(out.getValue()); + assertTrue(outValue22.equals(Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + // Now create an identical request23 and give it a fee equal to what we will have to pay anyway + SendRequest request23 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request23.tx.addOutput(Utils.CENT, notMyAddr); + request23.tx.addOutput(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr); + request23.feePerKb = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.divide(BigInteger.valueOf(3)); + request23.fee = Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.mod(BigInteger.valueOf(3)); + // Now check that we get the same exact transaction back + assertTrue(wallet.completeTx(request23, true).equals(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) && request23.tx.getInputs().size() == 2); + BigInteger outValue23 = BigInteger.ZERO; + for (TransactionOutput out : request23.tx.getOutputs()) + outValue23 = outValue23.add(out.getValue()); + assertTrue(outValue23.equals(Utils.COIN.add(Utils.CENT).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE))); + + // Now create an identical request24 and add one nanocoin of fee, putting us over our wallet balance + SendRequest request24 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request24.tx.addOutput(Utils.CENT, notMyAddr); + request24.tx.addOutput(Utils.CENT.subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), notMyAddr); + request24.feePerKb = request23.feePerKb; + request24.fee = request23.fee.add(BigInteger.ONE); + // Now check that we dont complete + assertNull(wallet.completeTx(request24, true)); + + // Test feePerKb when we aren't using enforceDefaultReferenceClientFeeRelayRules + // Same as request 19 + SendRequest request25 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 99; i++) + request25.tx.addOutput(Utils.CENT, notMyAddr); + // If we send now, we shouldnt need a fee and should only have to spend our COIN + assertTrue(wallet.completeTx(request25, true).equals(BigInteger.ZERO) && request25.tx.getInputs().size() == 1 && request25.tx.getOutputs().size() == 100); + // Now reset request19 and give it a fee per kb + request25.completed = false; request25.tx.clearInputs(); + request25.feePerKb = Utils.CENT.divide(BigInteger.valueOf(3)); + assertTrue(wallet.completeTx(request25, false).equals(Utils.CENT.subtract(BigInteger.ONE)) && request25.tx.getInputs().size() == 2); + BigInteger outValue25 = BigInteger.ZERO; + for (TransactionOutput out : request25.tx.getOutputs()) + outValue25 = outValue25.add(out.getValue()); + // Our change output should be one nanocoin + // Change this assert when we eventually randomize output order + assertTrue(request25.tx.getOutput(request25.tx.getOutputs().size()-1).getValue().equals(BigInteger.ONE)); + // and our fee should be CENT-1 nanocoin + assertTrue(outValue25.equals(Utils.COIN.add(BigInteger.ONE))); + + //Spend our CENT output + Transaction spendTx5 = new Transaction(params); + spendTx5.addOutput(Utils.CENT, notMyAddr); + spendTx5.addInput(tx5.getOutput(0)); + spendTx5.signInputs(SigHash.ALL, wallet); + wallet.receiveFromBlock(spendTx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + assertEquals(wallet.getBalance(), Utils.COIN); + + // Ensure change is discarded if it results in a fee larger than the chain (same as 8 and 9 but with feePerKb) + SendRequest request26 = SendRequest.to(notMyAddr, Utils.CENT); + for (int i = 0; i < 98; i++) + request26.tx.addOutput(Utils.CENT, notMyAddr); + request26.tx.addOutput(Utils.CENT.subtract( + Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)), notMyAddr); + assertTrue(request26.tx.bitcoinSerialize().length > 1000); + request26.feePerKb = BigInteger.ONE; + assertEquals(wallet.completeTx(request26, true), Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)); + Transaction spend26 = request26.tx; + // If a transaction is over 1kb, the set fee should be added + assertTrue(spend26.getOutputs().size() == 100); + // We optimize for priority, so the output selected should be the largest one + BigInteger outValue26 = BigInteger.ZERO; + for (TransactionOutput out : spend26.getOutputs()) + outValue26 = outValue26.add(out.getValue()); + assertTrue(outValue26.equals(Utils.COIN.subtract( + Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.add(Transaction.MIN_NONDUST_OUTPUT)))); } // There is a test for spending a coinbase transaction as it matures in BlockChainTest#coinbaseTransactionAvailability diff --git a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java index ceb92cc6..79d9bb82 100644 --- a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java +++ b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java @@ -437,7 +437,7 @@ public class WalletTool { } req.aesKey = wallet.getKeyCrypter().deriveKey(password); } - if (!wallet.completeTx(req)) { + if (wallet.completeTx(req, false) == null) { System.err.println("Insufficient funds: have " + Utils.bitcoinValueToFriendlyString(wallet.getBalance())); return; }