Support creating spends without all the private keys.

Dummy signatures are inserted instead. Also, simplify Transaction.toString().
This commit is contained in:
Mike Hearn 2013-09-16 15:46:23 +02:00
parent 81d76a76c3
commit 8d839ae5ad
5 changed files with 97 additions and 45 deletions

View File

@ -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.

View File

@ -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<Sha256Hash> 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<TransactionOutput> 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<TransactionInput> 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");

View File

@ -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;

View File

@ -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);

View File

@ -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));
}
}