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 e0facf80..c8c0d508 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -142,8 +142,8 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi /** Represents the results of a {@link CoinSelector#select(java.math.BigInteger, java.util.LinkedList)} operation */ public static class CoinSelection { public BigInteger valueGathered; - public Set gathered; - public CoinSelection(BigInteger valueGathered, Set gathered) { + public Collection gathered; + public CoinSelection(BigInteger valueGathered, Collection gathered) { this.valueGathered = valueGathered; this.gathered = gathered; } @@ -1523,6 +1523,13 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi */ public Transaction tx; + /** + * When emptyWallet is set, all available coins are sent to the first output in tx (its value is ignored and set + * to {@link com.google.bitcoin.core.Wallet#getBalance()} - the fees required for the transaction). Any + * additional outputs are removed. + */ + public boolean emptyWallet = false; + /** * "Change" means the difference between the value gathered by a transactions inputs (the size of which you * don't really control as it depends on who sent you money), and the value being sent somewhere else. The @@ -1556,10 +1563,10 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi * 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.

+ *

This is a dynamic fee (in satoshis) which will be added to the transaction for each kilobyte in size + * including 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.

*/ @@ -1628,6 +1635,14 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi req.tx = tx; return req; } + + public static SendRequest emptyWallet(Address destination) { + SendRequest req = new SendRequest(); + req.tx = new Transaction(destination.getParameters()); + req.tx.addOutput(BigInteger.ZERO, destination); + req.emptyWallet = true; + return req; + } } /** @@ -1649,7 +1664,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi * prevent this, but that should only occur once the transaction has been accepted by the network. This implies * you cannot have more than one outstanding sending tx at once.

* - *

You MUST ensure that nanocoins is smaller than {@link Transaction#MIN_NONDUST_OUTPUT} or the transaction will + *

You MUST ensure that nanocoins is larger than {@link Transaction#MIN_NONDUST_OUTPUT} or the transaction will * almost certainly be rejected by the network as dust.

* * @param address The Bitcoin address to send the money to. @@ -1801,7 +1816,7 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi // 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 (req.ensureMinRequiredFee) { + if (req.ensureMinRequiredFee && !req.emptyWallet) { // min fee checking is handled later for emptyWallet for (TransactionOutput output : req.tx.getOutputs()) if (output.getValue().compareTo(Utils.CENT) < 0) { if (output.getValue().compareTo(output.getMinNonDustValue()) < 0) { @@ -1822,20 +1837,45 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi // 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); - // This can throw InsufficientMoneyException. - FeeCalculation feeCalculation; - try { - feeCalculation = new FeeCalculation(req, value, originalInputs, needAtLeastReferenceFee, candidates); - } catch (InsufficientMoneyException e) { - // TODO: Propagate this after 0.9 is released and stop returning a boolean. - return false; + CoinSelection bestCoinSelection; + TransactionOutput bestChangeOutput = null; + if (!req.emptyWallet) { + // This can throw InsufficientMoneyException. + FeeCalculation feeCalculation; + try { + feeCalculation = new FeeCalculation(req, value, originalInputs, needAtLeastReferenceFee, candidates); + } catch (InsufficientMoneyException e) { + // TODO: Propagate this after 0.9 is released and stop returning a boolean. + return false; + } + bestCoinSelection = feeCalculation.bestCoinSelection; + bestChangeOutput = feeCalculation.bestChangeOutput; + } else { + BigInteger valueGathered = BigInteger.ZERO; + for (TransactionOutput output : candidates) + valueGathered = valueGathered.add(output.getValue()); + bestCoinSelection = new CoinSelection(valueGathered, candidates); + req.tx.getOutput(0).setValue(valueGathered); } - CoinSelection bestCoinSelection = feeCalculation.bestCoinSelection; - TransactionOutput bestChangeOutput = feeCalculation.bestChangeOutput; for (TransactionOutput output : bestCoinSelection.gathered) req.tx.addInput(output); + if (req.ensureMinRequiredFee && req.emptyWallet) { + TransactionOutput output = req.tx.getOutput(0); + // Check if we need additional fee due to the transaction's size + int size = req.tx.bitcoinSerialize().length; + size += estimateBytesForSigning(bestCoinSelection); + BigInteger fee = (req.fee == null ? BigInteger.ZERO : req.fee) + .add(BigInteger.valueOf((size / 1000) + 1).multiply(req.feePerKb == null ? BigInteger.ZERO : req.feePerKb)); + output.setValue(output.getValue().subtract(fee)); + // Check if we need additional fee due to the output's value + if (output.getValue().compareTo(Utils.CENT) < 0 && fee.compareTo(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE) < 0) + output.setValue(output.getValue().subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE.subtract(fee))); + if (output.getMinNonDustValue().compareTo(output.getValue()) > 0) + return false; + } + totalInput = totalInput.add(bestCoinSelection.valueGathered); if (bestChangeOutput != null) { @@ -3194,30 +3234,30 @@ public class Wallet implements Serializable, BlockChainListener, PeerFilterProvi } } - private int estimateBytesForSigning(CoinSelection selection) { - int size = 0; - for (TransactionOutput output : selection.gathered) { - try { - if (output.getScriptPubKey().isSentToAddress()) { - // Send-to-address spends usually take maximum pubkey.length (as it may be compressed or not) + 75 bytes - size += 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); - } - } - return size; - } - private void resetTxInputs(SendRequest req, List originalInputs) { req.tx.clearInputs(); for (TransactionInput input : originalInputs) req.tx.addInput(input); } } + + private int estimateBytesForSigning(CoinSelection selection) { + int size = 0; + for (TransactionOutput output : selection.gathered) { + try { + if (output.getScriptPubKey().isSentToAddress()) { + // Send-to-address spends usually take maximum pubkey.length (as it may be compressed or not) + 75 bytes + size += 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); + } + } + return size; + } } 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 5aaae972..846e09a3 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -46,6 +46,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static com.google.bitcoin.core.TestUtils.*; +import static com.google.bitcoin.core.TestUtils.makeSolvedTestBlock; import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString; import static com.google.bitcoin.core.Utils.toNanoCoins; import static org.junit.Assert.*; @@ -1825,4 +1826,56 @@ public class WalletTest extends TestWithWallet { Threading.waitForUserCode(); assertEquals(1, flag.get()); } + + @Test + public void testEmptyRandomWallet() throws Exception { + // Add a random set of outputs + StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, new ECKey().toAddress(params)), BigInteger.ONE, 1); + Random rng = new Random(); + for (int i = 0; i < rng.nextInt(100) + 1; i++) { + Transaction tx = createFakeTx(params, BigInteger.valueOf(rng.nextInt((int) Utils.COIN.longValue())), myAddress); + wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + } + SendRequest request = SendRequest.emptyWallet(new ECKey().toAddress(params)); + assertTrue(wallet.completeTx(request)); + wallet.commitTx(request.tx); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + } + + @Test + public void testEmptyWallet() throws Exception { + Address outputKey = new ECKey().toAddress(params); + // Add exactly 0.01 + StoredBlock block = new StoredBlock(makeSolvedTestBlock(blockStore, outputKey), BigInteger.ONE, 1); + Transaction tx = createFakeTx(params, Utils.CENT, myAddress); + wallet.receiveFromBlock(tx, block, AbstractBlockChain.NewBlockType.BEST_CHAIN); + SendRequest request = SendRequest.emptyWallet(outputKey); + assertTrue(wallet.completeTx(request)); + wallet.commitTx(request.tx); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + assertEquals(Utils.CENT, request.tx.getOutput(0).getValue()); + + // Add just under 0.01 + StoredBlock block2 = new StoredBlock(block.getHeader().createNextBlock(outputKey), BigInteger.ONE, 2); + tx = createFakeTx(params, Utils.CENT.subtract(BigInteger.ONE), myAddress); + wallet.receiveFromBlock(tx, block2, AbstractBlockChain.NewBlockType.BEST_CHAIN); + request = SendRequest.emptyWallet(outputKey); + assertTrue(wallet.completeTx(request)); + wallet.commitTx(request.tx); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + assertEquals(Utils.CENT.subtract(BigInteger.ONE).subtract(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE), request.tx.getOutput(0).getValue()); + + // Add an unsendable value + StoredBlock block3 = new StoredBlock(block2.getHeader().createNextBlock(outputKey), BigInteger.ONE, 3); + BigInteger outputValue = Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(BigInteger.ONE); + tx = createFakeTx(params, outputValue, myAddress); + wallet.receiveFromBlock(tx, block3, AbstractBlockChain.NewBlockType.BEST_CHAIN); + request = SendRequest.emptyWallet(outputKey); + assertFalse(wallet.completeTx(request)); + request.ensureMinRequiredFee = false; + assertTrue(wallet.completeTx(request)); + wallet.commitTx(request.tx); + assertEquals(BigInteger.ZERO, wallet.getBalance()); + assertEquals(outputValue, request.tx.getOutput(0).getValue()); + } }