From 11a87317a461749b42f5a8d07ba5601cddc7085c Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Tue, 7 Feb 2012 21:47:21 +0100 Subject: [PATCH] Split transaction creation into building a template and then completing it. Completing a tx means adding inputs and possibly a change output to make the transaction valid. Also add a convenience addOutput() overload to Transaction. This makes it easier to create multi-sends. Patch from Chris Rico. --- AUTHORS | 1 + src/com/google/bitcoin/core/Transaction.java | 7 ++ src/com/google/bitcoin/core/Wallet.java | 73 ++++++++++++++----- tests/com/google/bitcoin/core/WalletTest.java | 37 ++++++++++ 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/AUTHORS b/AUTHORS index 63688822..7d387553 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,3 +11,4 @@ Wolfgang Nagele Jonny Heggheim Steve Coughlan Roman Mandeleil +Chris Rico diff --git a/src/com/google/bitcoin/core/Transaction.java b/src/com/google/bitcoin/core/Transaction.java index 32e2324a..804163af 100644 --- a/src/com/google/bitcoin/core/Transaction.java +++ b/src/com/google/bitcoin/core/Transaction.java @@ -558,6 +558,13 @@ public class Transaction extends ChildMessage implements Serializable { adjustLength(to.length); } + /** + * Creates an output based on the given address and value, adds it to this transaction. + */ + public void addOutput(BigInteger value, Address address) { + addOutput(new TransactionOutput(params, this, value, address)); + } + /** * Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The * signature is over the transaction itself, to prove the redeemer actually created that transaction, diff --git a/src/com/google/bitcoin/core/Wallet.java b/src/com/google/bitcoin/core/Wallet.java index 2370850e..9404a9b4 100644 --- a/src/com/google/bitcoin/core/Wallet.java +++ b/src/com/google/bitcoin/core/Wallet.java @@ -679,7 +679,7 @@ public class Wallet implements Serializable { * and we did not create it, and it spends some of our outputs. * */ - synchronized void commitTx(Transaction tx) throws VerificationException { + public synchronized void commitTx(Transaction tx) throws VerificationException { assert !pending.containsKey(tx.getHash()) : "commitTx called on the same transaction twice"; log.info("commitTx of {}", tx.getHashAsString()); tx.updatedAt = Utils.now(); @@ -884,18 +884,14 @@ public class Wallet implements Serializable { /** * Statelessly creates a transaction that sends the given number of nanocoins to address. The change is sent to - * the first address in the wallet, so you must have added at least one key.

+ * {@link Wallet#getChangeAddress()}, so you must have added at least one key.

*

* This method is stateless in the sense that calling it twice with the same inputs will result in two * Transaction objects which are equal. The wallet is not updated to track its pending status or to mark the * coins as spent until commitTx is called on the result. */ synchronized Transaction createSend(Address address, BigInteger nanocoins) { - // For now let's just pick the first key in our keychain. In future we might want to do something else to - // give the user better privacy here, eg in incognito mode. - assert keychain.size() > 0 : "Can't send value without an address to use for receiving change"; - ECKey first = keychain.get(0); - return createSend(address, nanocoins, first.toAddress(params)); + return createSend(address, nanocoins, getChangeAddress()); } /** @@ -920,8 +916,8 @@ public class Wallet implements Serializable { } /** - * Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to the first key in the - * wallet. The transaction will be announced to any connected nodes asynchronously. If you would like to know when + * Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to {@link Wallet#getChangeAddress()}. + * The transaction will be announced to any connected nodes asynchronously. If you would like to know when * the transaction was successfully sent to at least one node, use * {@link Wallet#sendCoinsOffline(Address, java.math.BigInteger)} and then {@link PeerGroup#broadcastTransaction(Transaction)} * on the result to obtain a {@link java.util.concurrent.Future}. @@ -941,8 +937,8 @@ public class Wallet implements Serializable { } /** - * Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to the first key in the - * wallet. The method will block until the transaction has been announced to at least one node. + * Sends coins to the given address, via the given {@link PeerGroup}. Change is returned to {@link Wallet#getChangeAddress()}. + * The method will block until the transaction has been announced to at least one node. * * @param peerGroup a PeerGroup to use for broadcast or null. * @param to Which address to send coins to. @@ -961,7 +957,7 @@ public class Wallet implements Serializable { } /** - * Sends coins to the given address, via the given {@link Peer}. Change is returned to the first key in the wallet. + * 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. * @@ -1004,6 +1000,33 @@ public class Wallet implements Serializable { synchronized Transaction createSend(Address address, BigInteger nanocoins, Address changeAddress) { log.info("Creating send tx to " + address.toString() + " for " + bitcoinValueToFriendlyString(nanocoins)); + + Transaction sendTx = new Transaction(params); + sendTx.addOutput(nanocoins, address); + + if (completeTx(sendTx, changeAddress)) { + return sendTx; + } else { + return null; + } + } + + /** + * Takes a transaction with arbitrary outputs, gathers the necessary inputs for spending, and signs it + * @param sendTx The transaction to complete + * @param changeAddress Which address to send the change to, in case we can't make exactly the right value from + * our coins. This should be an address we own (is in the keychain). + * @return False if we cannot afford this send, true otherwise + */ + public synchronized boolean completeTx(Transaction sendTx, Address changeAddress) { + // Calculate the transaction total + BigInteger nanocoins = BigInteger.ZERO; + for(TransactionOutput output : sendTx.getOutputs()) { + nanocoins = nanocoins.add(output.getValue()); + } + + log.info("Completing send tx with {} outputs totalling {}", sendTx.getOutputs().size(), bitcoinValueToFriendlyString(nanocoins)); + // To send money to somebody else, we need to do gather up transactions with unspent outputs until we have // sufficient value. Many coin selection algorithms are possible, we use a simple but suboptimal one. // TODO: Sort coins so we use the smallest first, to combat wallet fragmentation and reduce fees. @@ -1023,12 +1046,10 @@ public class Wallet implements Serializable { log.info("Insufficient value in wallet for send, missing " + bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered))); // TODO: Should throw an exception here. - return null; + return false; } assert gathered.size() > 0; - Transaction sendTx = new Transaction(params); sendTx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN); - sendTx.addOutput(new TransactionOutput(params, sendTx, nanocoins, address)); BigInteger change = valueGathered.subtract(nanocoins); 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, @@ -1049,8 +1070,26 @@ public class Wallet implements Serializable { // happen, if it does it means the wallet has got into an inconsistent state. throw new RuntimeException(e); } - log.info(" created {}", sendTx.getHashAsString()); - return sendTx; + log.info(" completed {}", sendTx.getHashAsString()); + return true; + } + + /** + * Takes a transaction with arbitrary outputs, gathers the necessary inputs for spending, and signs it. + * Change goes to {@link Wallet#getChangeAddress()} + * @param sendTx The transaction to complete + * @return False if we cannot afford this send, true otherwise + */ + public synchronized boolean completeTx(Transaction sendTx) { + return completeTx(sendTx, getChangeAddress()); + } + + synchronized Address getChangeAddress() { + // For now let's just pick the first key in our keychain. In future we might want to do something else to + // give the user better privacy here, eg in incognito mode. + assert keychain.size() > 0 : "Can't send value without an address to use for receiving change"; + ECKey first = keychain.get(0); + return first.toAddress(params); } /** diff --git a/tests/com/google/bitcoin/core/WalletTest.java b/tests/com/google/bitcoin/core/WalletTest.java index 1f4be961..4b26e5e2 100644 --- a/tests/com/google/bitcoin/core/WalletTest.java +++ b/tests/com/google/bitcoin/core/WalletTest.java @@ -85,6 +85,43 @@ public class WalletTest { assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL)); } + @Test + public void customTransactionSpending() throws Exception { + // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. + BigInteger v1 = Utils.toNanoCoins(3, 0); + Transaction t1 = createFakeTx(params, v1, myAddress); + + wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN); + assertEquals(v1, wallet.getBalance()); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); + + ECKey k2 = new ECKey(); + Address a2 = k2.toAddress(params); + BigInteger v2 = toNanoCoins(0, 50); + BigInteger v3 = toNanoCoins(0, 75); + BigInteger v4 = toNanoCoins(1, 25); + + Transaction t2 = new Transaction(params); + t2.addOutput(v2, a2); + t2.addOutput(v3, a2); + t2.addOutput(v4, a2); + boolean complete = wallet.completeTx(t2); + + // Do some basic sanity checks. + assertTrue(complete); + assertEquals(1, t2.getInputs().size()); + assertEquals(myAddress, t2.getInputs().get(0).getScriptSig().getFromAddress()); + assertEquals(t2.getConfidence().getConfidenceType(), TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN); + + // We have NOT proven that the signature is correct! + + wallet.commitTx(t2); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); + assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.SPENT)); + assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL)); + } + @Test public void sideChain() throws Exception { // The wallet receives a coin on the main chain, then on a side chain. Only main chain counts towards balance.