From 572f2a4f4e9cf631e6c611faea9ed97cb35267e5 Mon Sep 17 00:00:00 2001 From: Mike Hearn Date: Thu, 6 Sep 2012 17:45:04 +0200 Subject: [PATCH] Implement the ability to explicitly set a fee in the Wallet.SendRequest. Does not yet auto-calculate the correct fees. Resolves issue 45. Resolves issue 245. --- .../java/com/google/bitcoin/core/Wallet.java | 27 +++++++++++++------ .../com/google/bitcoin/core/WalletTest.java | 19 ++++++++++--- .../com/google/bitcoin/tools/WalletTool.java | 12 ++++++--- 3 files changed, 43 insertions(+), 15 deletions(-) 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 39fcd3e0..f11367cd 100644 --- a/core/src/main/java/com/google/bitcoin/core/Wallet.java +++ b/core/src/main/java/com/google/bitcoin/core/Wallet.java @@ -1276,6 +1276,16 @@ public class Wallet implements Serializable { */ public Address changeAddress; + /** + * 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. + */ + public BigInteger fee = BigInteger.ZERO; + // Tracks if this has been passed to wallet.completeTx already: just a safety check. private boolean completed; @@ -1436,14 +1446,15 @@ public class Wallet implements Serializable { */ public synchronized boolean completeTx(SendRequest req) { Preconditions.checkArgument(!req.completed, "Given SendRequest has already been completed."); - // Calculate the transaction total - BigInteger nanocoins = BigInteger.ZERO; + // Calculate the amount of value we need to import. + BigInteger value = BigInteger.ZERO; for (TransactionOutput output : req.tx.getOutputs()) { - nanocoins = nanocoins.add(output.getValue()); + value = value.add(output.getValue()); } + value = value.add(req.fee); log.info("Completing send tx with {} outputs totalling {}", - req.tx.getOutputs().size(), bitcoinValueToFriendlyString(nanocoins)); + req.tx.getOutputs().size(), bitcoinValueToFriendlyString(value)); // 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. @@ -1461,18 +1472,18 @@ public class Wallet implements Serializable { gathered.add(output); valueGathered = valueGathered.add(output.getValue()); } - if (valueGathered.compareTo(nanocoins) >= 0) break; + if (valueGathered.compareTo(value) >= 0) break; } // Can we afford this? - if (valueGathered.compareTo(nanocoins) < 0) { + if (valueGathered.compareTo(value) < 0) { log.info("Insufficient value in wallet for send, missing " + - bitcoinValueToFriendlyString(nanocoins.subtract(valueGathered))); + bitcoinValueToFriendlyString(value.subtract(valueGathered))); // TODO: Should throw an exception here. return false; } checkState(gathered.size() > 0); req.tx.getConfidence().setConfidenceType(TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN); - BigInteger change = valueGathered.subtract(nanocoins); + BigInteger change = 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 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 3aec2e83..7c5de0a0 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -58,18 +58,25 @@ public class WalletTest { @Test public void basicSpending() throws Exception { - // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. + // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. We + // will attach a small fee. Because the Bitcoin protocol makes it difficult to determine the fee of an + // arbitrary transaction in isolation, we'll check that the fee was set by examining the size of the change. + + // Receive some money. BigInteger v1 = Utils.toNanoCoins(1, 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(); + // Create a send with a fee. + Address destination = new ECKey().toAddress(params); BigInteger v2 = toNanoCoins(0, 50); - Transaction t2 = wallet.createSend(k2.toAddress(params), v2); + Wallet.SendRequest req = Wallet.SendRequest.to(destination, v2); + req.fee = toNanoCoins(0, 1); + wallet.completeTx(req); + Transaction t2 = req.tx; assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.ALL)); @@ -77,6 +84,10 @@ public class WalletTest { assertEquals(1, t2.getInputs().size()); assertEquals(myAddress, t2.getInputs().get(0).getScriptSig().getFromAddress()); assertEquals(t2.getConfidence().getConfidenceType(), TransactionConfidence.ConfidenceType.NOT_SEEN_IN_CHAIN); + assertEquals(2, t2.getOutputs().size()); + assertEquals(destination, t2.getOutputs().get(0).getScriptPubKey().getToAddress()); + assertEquals(wallet.getChangeAddress(), t2.getOutputs().get(1).getScriptPubKey().getToAddress()); + assertEquals(toNanoCoins(0, 49), t2.getOutputs().get(1).getValue()); // We have NOT proven that the signature is correct! 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 bca1d0d7..b1abd9ba 100644 --- a/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java +++ b/tools/src/main/java/com/google/bitcoin/tools/WalletTool.java @@ -88,7 +88,7 @@ public class WalletTool { " --output=1GthXFQMktFLWdh5EPNGqbq3H6WdG8zsWj:1.245\n" + " If the output destination starts with 04 and is 65 bytes (130 chars) it will be\n" + " treated as a public key instead of an address and the send will use \n" + - " CHECKSIG as the script.\n" + + " CHECKSIG as the script. You can also specify a --fee=0.01\n" + "\n>>> WAITING\n" + "You can wait for the condition specified by the --waitfor flag to become true. Transactions and new\n" + @@ -229,6 +229,7 @@ public class WalletTool { parser.accepts("peers").withRequiredArg(); OptionSpec outputFlag = parser.accepts("output").withRequiredArg(); parser.accepts("value").withRequiredArg(); + parser.accepts("fee").withRequiredArg(); conditionFlag = parser.accepts("condition").withRequiredArg(); options = parser.parse(args); @@ -321,7 +322,11 @@ public class WalletTool { System.err.println("You must specify at least one --output=addr:value."); return; } - send(outputFlag.values(options)); + BigInteger fee = BigInteger.ZERO; + if (options.has("fee")) { + fee = Utils.toNanoCoins((String)options.valueOf("fee")); + } + send(outputFlag.values(options), fee); break; } @@ -343,7 +348,7 @@ public class WalletTool { shutdown(); } - private static void send(List outputs) { + private static void send(List outputs, BigInteger fee) { try { // Convert the input strings to outputs. Transaction t = new Transaction(params); @@ -377,6 +382,7 @@ public class WalletTool { } } Wallet.SendRequest req = Wallet.SendRequest.forTx(t); + req.fee = fee; if (!wallet.completeTx(req)) { System.err.println("Insufficient funds: have " + wallet.getBalance()); return;