mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-03 05:57:21 +00:00
Pluggable signers: simple local KeyBag signer
Introduced pluggable signers notion. Instances of TransactionSigner could be added into the wallet, so that they subsequently applied to transaction to complete it. Existing signing code (Transaction.signInputs) was refactored into LocalTransactionSigner, which is always implicitly added to any wallet. Related pull request: #157
This commit is contained in:
committed by
Mike Hearn
parent
4a9295ccde
commit
f6b2fa5a2b
@@ -972,6 +972,13 @@ public class ECKey implements EncryptableItem, Serializable {
|
||||
return decrypt(crypter, aesKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates decrypted private key if needed.
|
||||
*/
|
||||
public ECKey maybeDecrypt(@Nullable KeyParameter aesKey) throws KeyCrypterException {
|
||||
return isEncrypted() && aesKey != null ? decrypt(aesKey) : this;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Check that it is possible to decrypt the key with the keyCrypter and that the original key is returned.</p>
|
||||
*
|
||||
|
||||
@@ -22,14 +22,11 @@ import com.google.bitcoin.crypto.TransactionSignature;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.script.ScriptBuilder;
|
||||
import com.google.bitcoin.script.ScriptOpCodes;
|
||||
import com.google.bitcoin.wallet.DecryptingKeyBag;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.primitives.Longs;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
@@ -38,7 +35,6 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.bitcoin.core.Utils.*;
|
||||
import static com.google.common.base.Preconditions.*;
|
||||
|
||||
/**
|
||||
* <p>A transaction represents the movement of coins from some addresses to some other addresses. It can also represent
|
||||
@@ -832,125 +828,6 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
return addOutput(new TransactionOutput(params, this, value, script.getProgram()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The
|
||||
* signature is over the transaction itself, to prove the redeemer actually created that transaction,
|
||||
* so we have to do this step last.<p>
|
||||
* <p/>
|
||||
* This method is similar to SignatureHash in script.cpp
|
||||
*
|
||||
* @param hashType This should always be set to SigHash.ALL currently. Other types are unused.
|
||||
* @param wallet A wallet is required to fetch the keys needed for signing.
|
||||
*/
|
||||
public synchronized void signInputs(SigHash hashType, Wallet wallet) throws ScriptException {
|
||||
signInputs(hashType, wallet, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Once a transaction has some inputs and outputs added, the signatures in the inputs can be calculated. The
|
||||
* signature is over the transaction itself, to prove the redeemer actually created that transaction,
|
||||
* so we have to do this step last.</p>
|
||||
*
|
||||
* @param hashType This should always be set to SigHash.ALL currently. Other types are unused.
|
||||
* @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 void signInputs(SigHash hashType, Wallet wallet, @Nullable KeyParameter aesKey) throws ScriptException {
|
||||
if (aesKey == null) {
|
||||
signInputs(hashType, false, wallet);
|
||||
} else {
|
||||
signInputs(hashType, false, new DecryptingKeyBag(wallet, aesKey));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs as many inputs as possible using keys from the given key bag, which are expected to be usable for
|
||||
* signing, i.e. not encrypted and not missing the private key part.
|
||||
*
|
||||
* @param hashType This should always be set to SigHash.ALL currently. Other types are unused.
|
||||
* @param keyBag a provider of keys that are usable as-is for signing.
|
||||
*/
|
||||
public synchronized void signInputs(SigHash hashType, boolean anyoneCanPay, KeyBag keyBag) throws ScriptException {
|
||||
checkState(inputs.size() > 0);
|
||||
checkState(outputs.size() > 0);
|
||||
|
||||
// I don't currently have an easy way to test other modes work, as the official client does not use them.
|
||||
checkArgument(hashType == SigHash.ALL, "Only SIGHASH_ALL is currently supported");
|
||||
|
||||
// The transaction is signed with the input scripts empty except for the input we are signing. In the case
|
||||
// where addInput has been used to set up a new transaction, they are already all empty. The input being signed
|
||||
// has to have the connected OUTPUT program in it when the hash is calculated!
|
||||
//
|
||||
// Note that each input may be claiming an output sent to a different key. So we have to look at the outputs
|
||||
// to figure out which key to sign with.
|
||||
|
||||
TransactionSignature[] signatures = new TransactionSignature[inputs.size()];
|
||||
ECKey[] signingKeys = new ECKey[inputs.size()];
|
||||
for (int i = 0; i < inputs.size(); i++) {
|
||||
TransactionInput input = inputs.get(i);
|
||||
// We don't have the connected output, we assume it was signed already and move on
|
||||
if (input.getOutpoint().getConnectedOutput() == null) {
|
||||
log.warn("Missing connected output, assuming input {} is already signed.", i);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
// We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when
|
||||
// we sign missing pieces (to check this would require either assuming any signatures are signing
|
||||
// standard output types or a way to get processed signatures out of script execution)
|
||||
input.getScriptSig().correctlySpends(this, i, input.getOutpoint().getConnectedOutput().getScriptPubKey(), true);
|
||||
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i);
|
||||
continue;
|
||||
} catch (ScriptException e) {
|
||||
// Expected.
|
||||
}
|
||||
if (input.getScriptBytes().length != 0)
|
||||
log.warn("Re-signing an already signed transaction! Be sure this is what you want.");
|
||||
// Find the signing key we'll need to use.
|
||||
ECKey key = input.getOutpoint().getConnectedKey(keyBag);
|
||||
// This assert should never fire. If it does, it means the wallet is inconsistent.
|
||||
checkNotNull(key, "Transaction exists in wallet that we cannot redeem: %s", input.getOutpoint().getHash());
|
||||
// Keep the key around for the script creation step below.
|
||||
signingKeys[i] = key;
|
||||
// The anyoneCanPay feature isn't used at the moment.
|
||||
byte[] connectedPubKeyScript = input.getOutpoint().getConnectedPubKeyScript();
|
||||
try {
|
||||
signatures[i] = calculateSignature(i, key, connectedPubKeyScript, hashType, anyoneCanPay);
|
||||
} catch (ECKey.KeyIsEncryptedException e) {
|
||||
throw e;
|
||||
} catch (ECKey.MissingPrivateKeyException e) {
|
||||
// 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();
|
||||
log.info("Used dummy signature for input {} due to failure during signing (most likely missing privkey)", i);
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have calculated each signature, go through and create the scripts. Reminder: the script consists:
|
||||
// 1) For pay-to-address outputs: a signature (over a hash of the simplified transaction) and the complete
|
||||
// public key needed to sign for the connected output. The output script checks the provided pubkey hashes
|
||||
// to the address and then checks the signature.
|
||||
// 2) For pay-to-key outputs: just a signature.
|
||||
for (int i = 0; i < inputs.size(); i++) {
|
||||
if (signatures[i] == null)
|
||||
continue;
|
||||
TransactionInput input = inputs.get(i);
|
||||
final TransactionOutput connectedOutput = input.getOutpoint().getConnectedOutput();
|
||||
checkNotNull(connectedOutput); // Quiet static analysis: is never null here but cannot be statically proven
|
||||
Script scriptPubKey = connectedOutput.getScriptPubKey();
|
||||
if (scriptPubKey.isSentToAddress()) {
|
||||
input.setScriptSig(ScriptBuilder.createInputScript(signatures[i], signingKeys[i]));
|
||||
} else if (scriptPubKey.isSentToRawPubKey()) {
|
||||
input.setScriptSig(ScriptBuilder.createInputScript(signatures[i]));
|
||||
} else {
|
||||
// Should be unreachable - if we don't recognize the type of script we're trying to sign for, we should
|
||||
// have failed above when fetching the key to sign with.
|
||||
throw new RuntimeException("Do not understand script type: " + scriptPubKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Every input is now complete.
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates a signature that is valid for being inserted into the input at the given position. This is simply
|
||||
|
||||
@@ -128,7 +128,7 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
* Returns the pubkey script from the connected output.
|
||||
* @throws java.lang.NullPointerException if there is no connected output.
|
||||
*/
|
||||
byte[] getConnectedPubKeyScript() {
|
||||
public byte[] getConnectedPubKeyScript() {
|
||||
byte[] result = checkNotNull(getConnectedOutput()).getScriptBytes();
|
||||
checkState(result.length > 0);
|
||||
return result;
|
||||
|
||||
@@ -26,6 +26,8 @@ import com.google.bitcoin.params.UnitTestParams;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.script.ScriptBuilder;
|
||||
import com.google.bitcoin.script.ScriptChunk;
|
||||
import com.google.bitcoin.signers.LocalTransactionSigner;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.store.UnreadableWalletException;
|
||||
import com.google.bitcoin.store.WalletProtobufSerializer;
|
||||
import com.google.bitcoin.utils.BaseTaggableObject;
|
||||
@@ -203,6 +205,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
// a high risk of being a double spending attack.
|
||||
private RiskAnalysis.Analyzer riskAnalyzer = DefaultRiskAnalysis.FACTORY;
|
||||
|
||||
// Objects that perform transaction signing. Applied subsequently one after another
|
||||
@GuardedBy("lock") private List<TransactionSigner> signers;
|
||||
|
||||
/**
|
||||
* Creates a new, empty wallet with no keys and no transactions. If you want to restore a wallet from disk instead,
|
||||
* see loadFromFile.
|
||||
@@ -248,6 +253,8 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
extensions = new HashMap<String, WalletExtension>();
|
||||
// Use a linked hash map to ensure ordering of event listeners is correct.
|
||||
confidenceChanged = new LinkedHashMap<Transaction, TransactionConfidence.Listener.ChangeReason>();
|
||||
signers = new ArrayList<TransactionSigner>();
|
||||
addTransactionSigner(new LocalTransactionSigner());
|
||||
createTransientState();
|
||||
}
|
||||
|
||||
@@ -281,6 +288,33 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Adds given transaction signer to the list of signers. It will be added to the end of the signers list, so if
|
||||
* this wallet already has some signers added, given signer will be executed after all of them.</p>
|
||||
* <p>Transaction signer should be fully initialized before adding to the wallet, otherwise {@link IllegalStateException}
|
||||
* will be thrown</p>
|
||||
*/
|
||||
public void addTransactionSigner(TransactionSigner signer) {
|
||||
lock.lock();
|
||||
try {
|
||||
if (signer.isReady())
|
||||
signers.add(signer);
|
||||
else
|
||||
throw new IllegalStateException("Signer instance is not ready to be added into Wallet: " + signer.getClass());
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public List<TransactionSigner> getTransactionSigners() {
|
||||
lock.lock();
|
||||
try {
|
||||
return ImmutableList.copyOf(signers);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/******************************************************************************************************************/
|
||||
|
||||
//region Key Management
|
||||
@@ -3283,8 +3317,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
req.tx.shuffleOutputs();
|
||||
|
||||
// Now sign the inputs, thus proving that we are entitled to redeem the connected outputs.
|
||||
if (req.signInputs)
|
||||
req.tx.signInputs(Transaction.SigHash.ALL, this, req.aesKey);
|
||||
if (req.signInputs) {
|
||||
signTransaction(req.tx, Transaction.SigHash.ALL, req.aesKey);
|
||||
}
|
||||
|
||||
// Check size.
|
||||
int size = req.tx.bitcoinSerialize().length;
|
||||
@@ -3312,6 +3347,33 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Signs given transaction. Actual signing is done by pluggable {@link #signers} and it's not guaranteed that
|
||||
* transaction will be complete in the end.</p>
|
||||
* <p>Only {@link com.google.bitcoin.core.Transaction.SigHash#ALL} signing mode is currently supported</p>
|
||||
* <p>Optional aesKey should be provided if this wallet is encrypted</p>
|
||||
*/
|
||||
public void signTransaction(Transaction tx, Transaction.SigHash hashType, @Nullable KeyParameter aesKey) {
|
||||
lock.lock();
|
||||
try {
|
||||
List<TransactionInput> inputs = tx.getInputs();
|
||||
List<TransactionOutput> outputs = tx.getOutputs();
|
||||
checkState(inputs.size() > 0);
|
||||
checkState(outputs.size() > 0);
|
||||
|
||||
// I don't currently have an easy way to test other modes work, as the official client does not use them.
|
||||
checkArgument(hashType == Transaction.SigHash.ALL, "Only SIGHASH_ALL is currently supported");
|
||||
|
||||
KeyBag maybeDecryptingKeyBag = aesKey != null ? new DecryptingKeyBag(this, aesKey) : this;
|
||||
for (TransactionSigner signer : signers) {
|
||||
if (!signer.signInputs(tx, maybeDecryptingKeyBag))
|
||||
log.info("{} returned false for the tx", signer.getClass().getName());
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/** Reduce the value of the first output of a transaction to pay the given feePerKb as appropriate for its size. */
|
||||
private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, Coin baseFee, Coin feePerKb) {
|
||||
TransactionOutput output = tx.getOutput(0);
|
||||
@@ -4193,7 +4255,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
||||
}
|
||||
rekeyTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
|
||||
rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION);
|
||||
rekeyTx.signInputs(Transaction.SigHash.ALL, this, aesKey);
|
||||
signTransaction(rekeyTx, Transaction.SigHash.ALL, aesKey);
|
||||
// KeyTimeCoinSelector should never select enough inputs to push us oversize.
|
||||
checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE);
|
||||
return rekeyTx;
|
||||
|
||||
@@ -185,7 +185,7 @@ public class ScriptBuilder {
|
||||
return createMultiSigInputScriptBytes(sigs, multisigProgram.getProgram());
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Create a program that satisfies an OP_CHECKMULTISIG program, using pre-encoded signatures.
|
||||
* Optionally, appends the script program bytes if spending a P2SH output.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Copyright 2014 Kosta Korenkov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.bitcoin.signers;
|
||||
|
||||
import com.google.bitcoin.core.ECKey;
|
||||
import com.google.bitcoin.core.ScriptException;
|
||||
import com.google.bitcoin.core.Transaction;
|
||||
import com.google.bitcoin.core.TransactionInput;
|
||||
import com.google.bitcoin.crypto.TransactionSignature;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.script.ScriptBuilder;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* <p>{@link TransactionSigner} implementation for signing inputs using keys from provided {@link com.google.bitcoin.wallet.KeyBag}.
|
||||
* It always uses {@link com.google.bitcoin.core.Transaction.SigHash#ALL} signing mode.</p>
|
||||
* <p>At the moment it works for pay-to-address and pay-to-pubkey outputs only and will throw {@link RuntimeException} for
|
||||
* other script types</p>
|
||||
*/
|
||||
public class LocalTransactionSigner implements TransactionSigner {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalTransactionSigner.class);
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserialize(byte[] data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(Transaction tx, KeyBag keyBag) {
|
||||
int numInputs = tx.getInputs().size();
|
||||
for (int i = 0; i < numInputs; i++) {
|
||||
TransactionInput txIn = tx.getInput(i);
|
||||
if (txIn.getConnectedOutput() == null) {
|
||||
log.warn("Missing connected output, assuming input {} is already signed.", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey();
|
||||
|
||||
// skip input if it spends not pay-to-address or pay-to-pubkey tx
|
||||
// we're not returning false here as this signer theoretically could still sign
|
||||
// some of the inputs (if someday it would be possible to have inputs mixed with multisig)
|
||||
if (!scriptPubKey.isSentToAddress() && !scriptPubKey.isSentToRawPubKey())
|
||||
continue;
|
||||
|
||||
try {
|
||||
// We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when
|
||||
// we sign missing pieces (to check this would require either assuming any signatures are signing
|
||||
// standard output types or a way to get processed signatures out of script execution)
|
||||
txIn.getScriptSig().correctlySpends(tx, i, txIn.getConnectedOutput().getScriptPubKey(), true);
|
||||
log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i);
|
||||
continue;
|
||||
} catch (ScriptException e) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
ECKey key = txIn.getOutpoint().getConnectedKey(keyBag);
|
||||
// This assert should never fire. If it does, it means the wallet is inconsistent.
|
||||
checkNotNull(key, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash());
|
||||
byte[] connectedPubKeyScript = txIn.getOutpoint().getConnectedPubKeyScript();
|
||||
TransactionSignature signature;
|
||||
try {
|
||||
signature = tx.calculateSignature(i, key, connectedPubKeyScript, Transaction.SigHash.ALL, false);
|
||||
} catch (ECKey.KeyIsEncryptedException e) {
|
||||
throw e;
|
||||
} catch (ECKey.MissingPrivateKeyException e) {
|
||||
// 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.
|
||||
signature = TransactionSignature.dummy();
|
||||
log.info("Used dummy signature for input {} due to failure during signing (most likely missing privkey)", i);
|
||||
}
|
||||
if (scriptPubKey.isSentToAddress()) {
|
||||
txIn.setScriptSig(ScriptBuilder.createInputScript(signature, key));
|
||||
} else if (scriptPubKey.isSentToRawPubKey()) {
|
||||
txIn.setScriptSig(ScriptBuilder.createInputScript(signature));
|
||||
}
|
||||
// if input spends not pay-to-address or pay-to-pubkey tx
|
||||
// we're not returning false here as this signer theoretically could still sign
|
||||
// some of the inputs (if someday it would be possible to have inputs mixed with multisig)
|
||||
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright 2014 Kosta Korenkov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.bitcoin.signers;
|
||||
|
||||
import com.google.bitcoin.core.Transaction;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* <p>Implementations of this interface are intended to sign inputs of the given transaction. Given transaction may already
|
||||
* be partially signed or somehow altered by other signers.</p>
|
||||
* <p>To make use of the signer, you need to add it into the wallet by
|
||||
* calling {@link com.google.bitcoin.core.Wallet#addTransactionSigner(TransactionSigner)}. Signer will be serialized
|
||||
* along with the wallet data. In order for wallet to recreate signer after deserialization, each signer
|
||||
* should have no-args constructor</p>
|
||||
*/
|
||||
public interface TransactionSigner {
|
||||
|
||||
/**
|
||||
* Returns true if this signer is ready to be used.
|
||||
*/
|
||||
boolean isReady();
|
||||
|
||||
/**
|
||||
* Returns byte array of data representing state of this signer. It's used to serialize/deserialize this signer
|
||||
*/
|
||||
byte[] serialize();
|
||||
|
||||
/**
|
||||
* Uses given byte array of data to reconstruct internal state of this signer
|
||||
*/
|
||||
void deserialize(byte[] data);
|
||||
|
||||
/**
|
||||
* Signs given transaction's inputs.
|
||||
* Returns true if signer is compatible with given transaction (can do something meaningful with it).
|
||||
* Otherwise this method returns false
|
||||
*/
|
||||
boolean signInputs(Transaction tx, KeyBag keyBag);
|
||||
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
|
||||
import com.google.bitcoin.crypto.KeyCrypter;
|
||||
import com.google.bitcoin.crypto.KeyCrypterScrypt;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.signers.LocalTransactionSigner;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.wallet.KeyChainGroup;
|
||||
import com.google.bitcoin.wallet.WalletTransaction;
|
||||
import com.google.common.collect.Lists;
|
||||
@@ -191,6 +193,16 @@ public class WalletProtobufSerializer {
|
||||
walletBuilder.addTags(tag);
|
||||
}
|
||||
|
||||
for (TransactionSigner signer : wallet.getTransactionSigners()) {
|
||||
// do not serialize LocalTransactionSigner as it's being added implicitly
|
||||
if (signer instanceof LocalTransactionSigner)
|
||||
continue;
|
||||
Protos.TransactionSigner.Builder protoSigner = Protos.TransactionSigner.newBuilder();
|
||||
protoSigner.setClassName(signer.getClass().getName());
|
||||
protoSigner.setData(ByteString.copyFrom(signer.serialize()));
|
||||
walletBuilder.addTransactionSigners(protoSigner);
|
||||
}
|
||||
|
||||
// Populate the wallet version.
|
||||
walletBuilder.setVersion(wallet.getVersion());
|
||||
|
||||
@@ -450,6 +462,18 @@ public class WalletProtobufSerializer {
|
||||
wallet.setTag(tag.getTag(), tag.getData());
|
||||
}
|
||||
|
||||
for (Protos.TransactionSigner signerProto : walletProto.getTransactionSignersList()) {
|
||||
try {
|
||||
Class signerClass = Class.forName(signerProto.getClassName());
|
||||
TransactionSigner signer = (TransactionSigner)signerClass.newInstance();
|
||||
signer.deserialize(signerProto.getData().toByteArray());
|
||||
wallet.addTransactionSigner(signer);
|
||||
} catch (Exception e) {
|
||||
throw new UnreadableWalletException("Unable to deserialize TransactionSigner instance: " +
|
||||
signerProto.getClassName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (walletProto.hasVersion()) {
|
||||
wallet.setVersion(walletProto.getVersion());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright 2014 Kosta Korenkov
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.google.bitcoin.testing;
|
||||
|
||||
import com.google.bitcoin.core.Transaction;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
|
||||
public class NopTransactionSigner implements TransactionSigner {
|
||||
private boolean isReady;
|
||||
|
||||
public NopTransactionSigner() {
|
||||
}
|
||||
|
||||
public NopTransactionSigner(boolean ready) {
|
||||
this.isReady = ready;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return isReady;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return isReady ? new byte[]{1} : new byte[]{0};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserialize(byte[] data) {
|
||||
if (data.length > 0)
|
||||
isReady = data[0] == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(Transaction t, KeyBag keyBag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ public interface KeyBag {
|
||||
*
|
||||
* @return ECKey object or null if no such key was found.
|
||||
*/
|
||||
@Nullable
|
||||
public ECKey findKeyFromPubHash(byte[] pubkeyHash);
|
||||
|
||||
/**
|
||||
@@ -38,6 +39,7 @@ public interface KeyBag {
|
||||
*
|
||||
* @return ECKey or null if no such key was found.
|
||||
*/
|
||||
@Nullable
|
||||
public ECKey findKeyFromPubKey(byte[] pubkey);
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,13 +17,15 @@
|
||||
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.core.Transaction.SigHash;
|
||||
import com.google.bitcoin.core.Wallet.SendRequest;
|
||||
import com.google.bitcoin.crypto.*;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.store.BlockStoreException;
|
||||
import com.google.bitcoin.store.MemoryBlockStore;
|
||||
import com.google.bitcoin.store.WalletProtobufSerializer;
|
||||
import com.google.bitcoin.testing.FakeTxBuilder;
|
||||
import com.google.bitcoin.testing.MockTransactionBroadcaster;
|
||||
import com.google.bitcoin.testing.NopTransactionSigner;
|
||||
import com.google.bitcoin.testing.TestWithWallet;
|
||||
import com.google.bitcoin.utils.Threading;
|
||||
import com.google.bitcoin.wallet.*;
|
||||
@@ -94,6 +96,17 @@ public class WalletTest extends TestWithWallet {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private void createMarriedWallet() throws BlockStoreException {
|
||||
wallet = new Wallet(params);
|
||||
blockStore = new MemoryBlockStore(params);
|
||||
chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
|
||||
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58());
|
||||
|
||||
wallet.addFollowingAccountKeys(ImmutableList.of(partnerKey));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getSeedAsWords1() {
|
||||
// Can't verify much here as the wallet is random each time. We could fix the RNG for the unit tests and solve.
|
||||
@@ -121,6 +134,12 @@ public class WalletTest extends TestWithWallet {
|
||||
basicSpendingCommon(encryptedWallet, myEncryptedAddress, new ECKey().toAddress(params), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spendingWithIncompatibleSigners() throws Exception {
|
||||
wallet.addTransactionSigner(new NopTransactionSigner(true));
|
||||
basicSpendingCommon(wallet, myAddress, new ECKey().toAddress(params), false);
|
||||
}
|
||||
|
||||
static class TestRiskAnalysis implements RiskAnalysis {
|
||||
private final boolean risky;
|
||||
|
||||
@@ -1203,12 +1222,7 @@ public class WalletTest extends TestWithWallet {
|
||||
|
||||
@Test
|
||||
public void marriedKeychainBloomFilter() throws Exception {
|
||||
wallet = new Wallet(params);
|
||||
blockStore = new MemoryBlockStore(params);
|
||||
chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
String XPUB = "xpub68KFnj3bqUx1s7mHejLDBPywCAKdJEu1b49uniEEn2WSbHmZ7xbLqFTjJbtx1LUcAt1DwhoqWHmo2s5WMJp6wi38CiF2hYD49qVViKVvAoi";
|
||||
wallet.addFollowingAccountKeys(ImmutableList.of(DeterministicKey.deserializeB58(null, XPUB)));
|
||||
createMarriedWallet();
|
||||
Address address = wallet.currentReceiveAddress();
|
||||
|
||||
assertTrue(wallet.getBloomFilter(0.001).contains(address.getHash160()));
|
||||
@@ -1342,7 +1356,7 @@ public class WalletTest extends TestWithWallet {
|
||||
Transaction t3 = new Transaction(params);
|
||||
t3.addOutput(v3, k3.toAddress(params));
|
||||
t3.addInput(o2);
|
||||
t3.signInputs(SigHash.ALL, wallet);
|
||||
wallet.signTransaction(t3, Transaction.SigHash.ALL, null);
|
||||
|
||||
// Commit t3, so the coins from the pending t2 are spent
|
||||
wallet.commitTx(t3);
|
||||
@@ -1883,7 +1897,8 @@ public class WalletTest extends TestWithWallet {
|
||||
Transaction spendTx5 = new Transaction(params);
|
||||
spendTx5.addOutput(CENT, notMyAddr);
|
||||
spendTx5.addInput(tx5.getOutput(0));
|
||||
spendTx5.signInputs(SigHash.ALL, wallet);
|
||||
wallet.signTransaction(spendTx5, Transaction.SigHash.ALL, null);
|
||||
|
||||
wallet.receiveFromBlock(spendTx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 4);
|
||||
assertEquals(COIN, wallet.getBalance());
|
||||
|
||||
@@ -2133,7 +2148,7 @@ public class WalletTest extends TestWithWallet {
|
||||
SendRequest request4 = SendRequest.to(notMyAddr, CENT);
|
||||
request4.tx.addInput(tx3.getOutput(0));
|
||||
// Now if we manually sign it, completeTx will not replace our signature
|
||||
request4.tx.signInputs(SigHash.ALL, wallet);
|
||||
wallet.signTransaction(request4.tx, Transaction.SigHash.ALL, null);
|
||||
byte[] scriptSig = request4.tx.getInput(0).getScriptBytes();
|
||||
wallet.completeTx(request4);
|
||||
assertEquals(1, request4.tx.getInputs().size());
|
||||
@@ -2496,4 +2511,21 @@ public class WalletTest extends TestWithWallet {
|
||||
assertFalse(wallet.isDeterministicUpgradeRequired());
|
||||
wallet.freshReceiveKey(); // works.
|
||||
}
|
||||
|
||||
@Test(expected = IllegalStateException.class)
|
||||
public void shouldNotAddTransactionSignerThatIsNotReady() throws Exception {
|
||||
wallet.addTransactionSigner(new NopTransactionSigner(false));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionSignersShouldBeSerializedAlongWithWallet() throws Exception {
|
||||
final TransactionSigner signer = new NopTransactionSigner(true);
|
||||
wallet.addTransactionSigner(signer);
|
||||
assertEquals(2, wallet.getTransactionSigners().size());
|
||||
Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet);
|
||||
wallet = new WalletProtobufSerializer().readWallet(params, null, protos);
|
||||
assertEquals(2, wallet.getTransactionSigners().size());
|
||||
assertTrue(wallet.getTransactionSigners().get(1).isReady());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -300,6 +300,16 @@ message Tag {
|
||||
required bytes data = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data required to reconstruct TransactionSigner.
|
||||
*/
|
||||
message TransactionSigner {
|
||||
// fully qualified class name of TransactionSigner implementation
|
||||
required string class_name = 1;
|
||||
// arbitrary data required for signer to function
|
||||
optional bytes data = 2;
|
||||
}
|
||||
|
||||
/** A bitcoin wallet */
|
||||
message Wallet {
|
||||
/**
|
||||
@@ -352,5 +362,8 @@ message Wallet {
|
||||
|
||||
repeated Tag tags = 16;
|
||||
|
||||
// Next tag: 17
|
||||
// transaction signers added to the wallet
|
||||
repeated TransactionSigner transaction_signers = 17;
|
||||
|
||||
// Next tag: 18
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user