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.

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;
@ -613,17 +614,10 @@ 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(" / ");
s.append(in.getOutpoint().toString());
@ -634,18 +628,10 @@ public class Transaction extends ChildMessage implements Serializable {
}
for (TransactionOutput out : outputs) {
s.append(" ");
s.append("to ");
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(" ");
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();
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:

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