diff --git a/core/src/main/java/com/google/bitcoin/core/ECKey.java b/core/src/main/java/com/google/bitcoin/core/ECKey.java index 44e4d96be..5c776bb53 100644 --- a/core/src/main/java/com/google/bitcoin/core/ECKey.java +++ b/core/src/main/java/com/google/bitcoin/core/ECKey.java @@ -197,6 +197,14 @@ public class ECKey implements Serializable { this(privKey, pubKey, false); } + public boolean isPubKeyOnly() { + return priv == null; + } + + public boolean hasPrivKey() { + return priv != null; + } + /** * Output this ECKey as an ASN.1 encoded private key, as understood by OpenSSL or used by the BitCoin reference * implementation in its wallet storage format. @@ -725,7 +733,7 @@ public class ECKey implements Serializable { public byte[] getPrivKeyBytes() { return Utils.bigIntegerToBytes(priv, 32); } - + /** * Exports the private key in the form used by the Satoshi client "dumpprivkey" and "importprivkey" commands. Use * the {@link com.google.bitcoin.core.DumpedPrivateKey#toString()} method to get the string. diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java index 3461fc0be..7b6b1928c 100644 --- a/core/src/main/java/com/google/bitcoin/core/Transaction.java +++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java @@ -26,6 +26,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.spongycastle.crypto.params.KeyParameter; +import javax.annotation.Nullable; import java.io.*; import java.math.BigInteger; import java.text.ParseException; @@ -51,7 +52,7 @@ import static com.google.bitcoin.core.Utils.*; public class Transaction extends ChildMessage implements Serializable { private static final Logger log = LoggerFactory.getLogger(Transaction.class); private static final long serialVersionUID = -8567546957352643140L; - + /** Threshold for lockTime: below this value it is interpreted as block number, otherwise as timestamp. **/ public static final int LOCKTIME_THRESHOLD = 500000000; // Tue Nov 5 00:53:20 1985 UTC @@ -86,7 +87,7 @@ public class Transaction extends ChildMessage implements Serializable { // This is an in memory helper only. private transient Sha256Hash hash; - + // Data about how confirmed this tx is. Serialized, may be null. private TransactionConfidence confidence; @@ -97,7 +98,7 @@ public class Transaction extends ChildMessage implements Serializable { // // If this transaction is not stored in the wallet, appearsInHashes is null. private Set appearsInHashes; - + // Transactions can be encoded in a way that will use more bytes than is optimal // (due to VarInts having multiple encodings) // MAX_BLOCK_SIZE must be compared to the optimal encoding, not the actual encoding, so when parsing, we keep track @@ -222,7 +223,7 @@ public class Transaction extends ChildMessage implements Serializable { } return v; } - + /* * If isSpent - check that all my outputs spent, otherwise check that there at least * one unspent. @@ -415,7 +416,7 @@ public class Transaction extends ChildMessage implements Serializable { } return updatedAt; } - + public void setUpdateTime(Date updatedAt) { this.updatedAt = updatedAt; } @@ -534,7 +535,7 @@ public class Transaction extends ChildMessage implements Serializable { optimalEncodingMessageSize += 4; length = cursor - offset; } - + public int getOptimalEncodingMessageSize() { if (optimalEncodingMessageSize != 0) return optimalEncodingMessageSize; @@ -613,18 +614,11 @@ public class Transaction extends ChildMessage implements Serializable { } for (TransactionInput in : inputs) { s.append(" "); - s.append("from "); + s.append("in "); try { Script scriptSig = in.getScriptSig(); - if (scriptSig.getChunks().size() == 2) - s.append(scriptSig.getFromAddress(params).toString()); - else if (scriptSig.getChunks().size() == 1) { - s.append("[sig:"); - s.append(bytesToHexString(scriptSig.getChunks().get(0).data)); - s.append("]"); - } else - s.append(scriptSig); + s.append(scriptSig); s.append(" / "); s.append(in.getOutpoint().toString()); } catch (Exception e) { @@ -633,19 +627,11 @@ public class Transaction extends ChildMessage implements Serializable { s.append(String.format("%n")); } for (TransactionOutput out : outputs) { - s.append(" "); - s.append("to "); + s.append(" "); + s.append("out "); try { Script scriptPubKey = out.getScriptPubKey(); - if (scriptPubKey.isSentToAddress()) { - s.append(scriptPubKey.getToAddress(params).toString()); - } else if (scriptPubKey.isSentToRawPubKey()) { - s.append("[pubkey:"); - s.append(bytesToHexString(scriptPubKey.getPubKey())); - s.append("]"); - } else { - s.append(scriptPubKey); - } + s.append(scriptPubKey); s.append(" "); s.append(bitcoinValueToFriendlyString(out.getValue())); s.append(" BTC"); @@ -770,7 +756,7 @@ public class Transaction extends ChildMessage implements Serializable { * @param wallet A wallet is required to fetch the keys needed for signing. * @param aesKey The AES key to use to decrypt the key before signing. Null if no decryption is required. */ - public synchronized void signInputs(SigHash hashType, Wallet wallet, KeyParameter aesKey) throws ScriptException { + public synchronized void signInputs(SigHash hashType, Wallet wallet, @Nullable KeyParameter aesKey) throws ScriptException { // TODO: This should be a method of the TransactionInput that (possibly?) operates with a copy of this object. Preconditions.checkState(inputs.size() > 0); Preconditions.checkState(outputs.size() > 0); @@ -816,7 +802,14 @@ public class Transaction extends ChildMessage implements Serializable { // The anyoneCanPay feature isn't used at the moment. boolean anyoneCanPay = false; byte[] connectedPubKeyScript = input.getOutpoint().getConnectedPubKeyScript(); - signatures[i] = calculateSignature(i, key, aesKey, connectedPubKeyScript, hashType, anyoneCanPay); + if (key.hasPrivKey() || key.isEncrypted()) { + signatures[i] = calculateSignature(i, key, aesKey, connectedPubKeyScript, hashType, anyoneCanPay); + } else { + // Create a dummy signature to ensure the transaction is of the correct size when we try to ensure + // the right fee-per-kb is attached. If the wallet doesn't have the privkey, the user is assumed to + // be doing something special and that they will replace the dummy signature with a real one later. + signatures[i] = TransactionSignature.dummy(); + } } // Now we have calculated each signature, go through and create the scripts. Reminder: the script consists: @@ -950,13 +943,13 @@ public class Transaction extends ChildMessage implements Serializable { // ever put into scripts. Deleting OP_CODESEPARATOR is a step that should never be required but if we don't // do it, we could split off the main chain. connectedScript = Script.removeAllInstancesOfOp(connectedScript, ScriptOpCodes.OP_CODESEPARATOR); - + // Set the input to the script of its output. Satoshi does this but the step has no obvious purpose as // the signature covers the hash of the prevout transaction which obviously includes the output script // already. Perhaps it felt safer to him in some way, or is another leftover from how the code was written. TransactionInput input = inputs.get(inputIndex); input.setScriptBytes(connectedScript); - + ArrayList outputs = this.outputs; if ((sigHashType & 0x1f) == (SigHash.NONE.ordinal() + 1)) { // SIGHASH_NONE means no outputs are signed at all - the signature is effectively for a "blank cheque". @@ -995,7 +988,7 @@ public class Transaction extends ChildMessage implements Serializable { if (i != inputIndex) inputs.get(i).setSequenceNumber(0); } - + ArrayList inputs = this.inputs; if ((sigHashType & SIGHASH_ANYONECANPAY_VALUE) == SIGHASH_ANYONECANPAY_VALUE) { // SIGHASH_ANYONECANPAY means the signature in the input is not broken by changes/additions/removals @@ -1153,7 +1146,7 @@ public class Transaction extends ChildMessage implements Serializable { throw new VerificationException("Transaction had no inputs or no outputs."); if (this.getMessageSize() > Block.MAX_BLOCK_SIZE) throw new VerificationException("Transaction larger than MAX_BLOCK_SIZE"); - + BigInteger valueOut = BigInteger.ZERO; for (TransactionOutput output : outputs) { if (output.getValue().compareTo(BigInteger.ZERO) < 0) @@ -1162,7 +1155,7 @@ public class Transaction extends ChildMessage implements Serializable { } if (valueOut.compareTo(params.MAX_MONEY) > 0) throw new VerificationException("Total transaction output value greater than possible"); - + if (isCoinBase()) { if (inputs.get(0).getScriptBytes().length < 2 || inputs.get(0).getScriptBytes().length > 100) throw new VerificationException("Coinbase script size out of range"); diff --git a/core/src/main/java/com/google/bitcoin/crypto/TransactionSignature.java b/core/src/main/java/com/google/bitcoin/crypto/TransactionSignature.java index 224d37181..1702df115 100644 --- a/core/src/main/java/com/google/bitcoin/crypto/TransactionSignature.java +++ b/core/src/main/java/com/google/bitcoin/crypto/TransactionSignature.java @@ -48,6 +48,17 @@ public class TransactionSignature extends ECKey.ECDSASignature { setSigHash(mode, anyoneCanPay); } + /** + * Returns a dummy invalid signature whose R/S values are set such that they will take up the same number of + * encoded bytes as a real signature. This can be useful when you want to fill out a transaction to be of the + * right size (e.g. for fee calculations) but don't have the requisite signing key yet and will fill out the + * real signature later. + */ + public static TransactionSignature dummy() { + BigInteger val = BigInteger.ONE.shiftLeft(32 * 8); // 32 byte signatures. + return new TransactionSignature(val, val); + } + /** Calculates the byte used in the protocol to represent the combination of mode and anyoneCanPay. */ public static int calcSigHashValue(Transaction.SigHash mode, boolean anyoneCanPay) { int sighashFlags = mode.ordinal() + 1; diff --git a/core/src/test/java/com/google/bitcoin/core/TestWithWallet.java b/core/src/test/java/com/google/bitcoin/core/TestWithWallet.java index 31bfd4be8..4a2fb6ef4 100644 --- a/core/src/test/java/com/google/bitcoin/core/TestWithWallet.java +++ b/core/src/test/java/com/google/bitcoin/core/TestWithWallet.java @@ -80,6 +80,11 @@ public class TestWithWallet { return sendMoneyToWallet(wallet, createFakeTx(params, value, toAddress), type); } + protected Transaction sendMoneyToWallet(Wallet wallet, BigInteger value, ECKey toPubKey, AbstractBlockChain.NewBlockType type) + throws IOException, ProtocolException, VerificationException { + return sendMoneyToWallet(wallet, createFakeTx(params, value, toPubKey), type); + } + protected Transaction sendMoneyToWallet(BigInteger value, AbstractBlockChain.NewBlockType type) throws IOException, ProtocolException, VerificationException { return sendMoneyToWallet(this.wallet, createFakeTx(params, value, myAddress), type); 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 a7bfd383f..0bdf43b60 100644 --- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java +++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java @@ -22,6 +22,7 @@ import com.google.bitcoin.core.WalletTransaction.Pool; import com.google.bitcoin.crypto.KeyCrypter; import com.google.bitcoin.crypto.KeyCrypterException; import com.google.bitcoin.crypto.KeyCrypterScrypt; +import com.google.bitcoin.crypto.TransactionSignature; import com.google.bitcoin.store.WalletProtobufSerializer; import com.google.bitcoin.utils.Threading; import com.google.bitcoin.wallet.KeyTimeCoinSelector; @@ -380,7 +381,7 @@ public class WalletTest extends TestWithWallet { confTxns.add(tx); } }); - + // Receive some money. BigInteger oneCoin = Utils.toNanoCoins(1, 0); Transaction tx1 = sendMoneyToWallet(oneCoin, AbstractBlockChain.NewBlockType.BEST_CHAIN); @@ -443,9 +444,9 @@ public class WalletTest extends TestWithWallet { TransactionOutput output = new TransactionOutput(params, tx, Utils.toNanoCoins(0, 5), someOtherGuy); tx.addOutput(output); wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN); - + assertTrue("Wallet is not consistent", wallet.isConsistent()); - + Transaction txClone = new Transaction(params, tx.bitcoinSerialize()); try { wallet.receiveFromBlock(txClone, null, BlockChain.NewBlockType.BEST_CHAIN); @@ -463,9 +464,9 @@ public class WalletTest extends TestWithWallet { TransactionOutput output = new TransactionOutput(params, tx, Utils.toNanoCoins(0, 5), someOtherGuy); tx.addOutput(output); wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN); - + assertTrue(wallet.isConsistent()); - + wallet.addWalletTransaction(new WalletTransaction(Pool.PENDING, tx)); assertFalse(wallet.isConsistent()); } @@ -479,7 +480,7 @@ public class WalletTest extends TestWithWallet { TransactionOutput output = new TransactionOutput(params, tx, Utils.toNanoCoins(0, 5), someOtherGuy); tx.addOutput(output); assertTrue(wallet.isConsistent()); - + wallet.addWalletTransaction(new WalletTransaction(Pool.SPENT, tx)); assertFalse(wallet.isConsistent()); } @@ -978,7 +979,7 @@ public class WalletTest extends TestWithWallet { assertNotNull(results[0]); assertEquals(f, results[1]); } - + @Test public void spendOutputFromPendingTransaction() throws Exception { // We'll set up a wallet that receives a coin, then sends a coin of lesser value and keeps the change. @@ -995,13 +996,13 @@ public class WalletTest extends TestWithWallet { req.ensureMinRequiredFee = false; boolean complete = wallet.completeTx(req); assertTrue(complete); - + // Commit t2, so it is placed in the pending pool wallet.commitTx(t2); assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); assertEquals(1, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.ALL)); - + // Now try to the spend the output. ECKey k3 = new ECKey(); BigInteger v3 = toNanoCoins(0, 25); @@ -1009,13 +1010,13 @@ public class WalletTest extends TestWithWallet { t3.addOutput(v3, k3.toAddress(params)); t3.addInput(o2); t3.signInputs(SigHash.ALL, wallet); - + // Commit t3, so the coins from the pending t2 are spent wallet.commitTx(t3); assertEquals(0, wallet.getPoolSize(WalletTransaction.Pool.UNSPENT)); assertEquals(2, wallet.getPoolSize(WalletTransaction.Pool.PENDING)); assertEquals(3, wallet.getPoolSize(WalletTransaction.Pool.ALL)); - + // Now the output of t2 must not be available for spending assertFalse(o2.isAvailableForSpending()); } @@ -1995,4 +1996,38 @@ public class WalletTest extends TestWithWallet { assertNotNull(tx); assertEquals(200, tx.getInputs().size()); } + + @SuppressWarnings("ConstantConditions") + @Test + public void completeTxPartiallySigned() throws Exception { + // Check the wallet will write dummy scriptSigs for inputs that we have only pubkeys for without the privkey. + ECKey priv = new ECKey(); + ECKey pub = new ECKey(null, priv.getPubKey()); + wallet.addKey(pub); + ECKey priv2 = new ECKey(); + wallet.addKey(priv2); + // Send three transactions, with one being an address type and the other being a raw CHECKSIG type pubkey only, + // and the final one being a key we do have. We expect the first two inputs to be dummy values and the last + // to be signed correctly. + Transaction t1 = sendMoneyToWallet(wallet, Utils.CENT, pub.toAddress(params), AbstractBlockChain.NewBlockType.BEST_CHAIN); + Transaction t2 = sendMoneyToWallet(wallet, Utils.CENT, pub, AbstractBlockChain.NewBlockType.BEST_CHAIN); + Transaction t3 = sendMoneyToWallet(wallet, Utils.CENT, priv2, AbstractBlockChain.NewBlockType.BEST_CHAIN); + + ECKey dest = new ECKey(); + Wallet.SendRequest req = Wallet.SendRequest.emptyWallet(dest.toAddress(params)); + assertTrue(wallet.completeTx(req)); + byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin(); + // Selected inputs can be in any order. + for (int i = 0; i < req.tx.getInputs().size(); i++) { + TransactionInput input = req.tx.getInput(i); + if (input.getConnectedOutput().getParentTransaction().equals(t1)) { + assertArrayEquals(dummySig, input.getScriptSig().getChunks().get(0).data); + } else if (input.getConnectedOutput().getParentTransaction().equals(t2)) { + assertArrayEquals(dummySig, input.getScriptSig().getChunks().get(0).data); + } else if (input.getConnectedOutput().getParentTransaction().equals(t3)) { + input.getScriptSig().correctlySpends(req.tx, i, t3.getOutput(0).getScriptPubKey(), true); + } + } + assertTrue(TransactionSignature.isEncodingCanonical(dummySig)); + } }