mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2024-11-20 02:09:29 +01:00
Support creating spends without all the private keys.
Dummy signatures are inserted instead. Also, simplify Transaction.toString().
This commit is contained in:
parent
81d76a76c3
commit
8d839ae5ad
@ -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.
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user