mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-02-24 14:50:57 +01:00
Make LocalTransactionSigner to sign P2SH inputs.
Transaction inputs are now prepopulated with empty scriptSig. Each signer is expected to update this scriptSig with a signature at a proper place. There is a new method in RedeemData to locate index of the key/signature within scriptSig/program. To generalize an above approach for all supported types of inputs, RedeemData can now represent data for any type of input. For pay-to-address and pay-to-pubkey inputs it is expected to contain single key and CHECKSIG program. Signers now accept ProposedTransaction object that could carry additional metadata shared between signers. For now it shares derivation path of the signing key. To preserve the dummy sig feature, a new flag was introduced in a SendRequest. It specifies whether to fill empty sigs with dummies during tx completion or not. Default value is true (for backward compatibility). There is a CustomTransactionSigner class that may be used as a base for simple third-party signers (or may be not). It is used in unit test which may be treated as a usage example.
This commit is contained in:
parent
b8e84ecdc7
commit
d8944b922f
15 changed files with 511 additions and 101 deletions
|
@ -18,6 +18,8 @@
|
|||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import com.google.bitcoin.wallet.RedeemData;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
@ -303,6 +305,16 @@ public class TransactionInput extends ChildMessage implements Serializable {
|
|||
return tx.getOutputs().get((int) outpoint.getIndex());
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for getOutpoint().getConnectedRedeemData(keyBag)
|
||||
* @see TransactionOutPoint#getConnectedRedeemData(com.google.bitcoin.wallet.KeyBag)
|
||||
*/
|
||||
@Nullable
|
||||
public RedeemData getConnectedRedeemData(KeyBag keyBag) throws ScriptException {
|
||||
return getOutpoint().getConnectedRedeemData(keyBag);
|
||||
}
|
||||
|
||||
|
||||
public enum ConnectMode {
|
||||
DISCONNECT_ON_CONFLICT,
|
||||
ABORT_ON_CONFLICT
|
||||
|
|
|
@ -135,8 +135,10 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the ECKey identified in the connected output, for either pay-to-address scripts, pay-to-key or P2SH scripts.
|
||||
* If the script forms cannot be understood, throws ScriptException.
|
||||
* Returns the ECKey identified in the connected output, for either pay-to-address scripts or pay-to-key scripts.
|
||||
* For P2SH scripts you can use {@link #getConnectedRedeemData(com.google.bitcoin.wallet.KeyBag)} and then get the
|
||||
* key from RedeemData.
|
||||
* If the script form cannot be understood, throws ScriptException.
|
||||
*
|
||||
* @return an ECKey or null if the connected key cannot be found in the wallet.
|
||||
*/
|
||||
|
@ -151,12 +153,32 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
|||
} else if (connectedScript.isSentToRawPubKey()) {
|
||||
byte[] pubkeyBytes = connectedScript.getPubKey();
|
||||
return keyBag.findKeyFromPubKey(pubkeyBytes);
|
||||
} else {
|
||||
throw new ScriptException("Could not understand form of connected output script: " + connectedScript);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the RedeemData identified in the connected output, for either pay-to-address scripts, pay-to-key
|
||||
* or P2SH scripts.
|
||||
* If the script forms cannot be understood, throws ScriptException.
|
||||
*
|
||||
* @return a RedeemData or null if the connected data cannot be found in the wallet.
|
||||
*/
|
||||
@Nullable
|
||||
public RedeemData getConnectedRedeemData(KeyBag keyBag) throws ScriptException {
|
||||
TransactionOutput connectedOutput = getConnectedOutput();
|
||||
checkNotNull(connectedOutput, "Input is not connected so cannot retrieve key");
|
||||
Script connectedScript = connectedOutput.getScriptPubKey();
|
||||
if (connectedScript.isSentToAddress()) {
|
||||
byte[] addressBytes = connectedScript.getPubKeyHash();
|
||||
return RedeemData.of(keyBag.findKeyFromPubHash(addressBytes), connectedScript);
|
||||
} else if (connectedScript.isSentToRawPubKey()) {
|
||||
byte[] pubkeyBytes = connectedScript.getPubKey();
|
||||
return RedeemData.of(keyBag.findKeyFromPubKey(pubkeyBytes), connectedScript);
|
||||
} else if (connectedScript.isPayToScriptHash()) {
|
||||
byte[] scriptHash = connectedScript.getPubKeyHash();
|
||||
RedeemData redeemData = keyBag.findRedeemDataFromScriptHash(scriptHash);
|
||||
if (redeemData == null)
|
||||
return null;
|
||||
return redeemData.getFullKey();
|
||||
return keyBag.findRedeemDataFromScriptHash(scriptHash);
|
||||
} else {
|
||||
throw new ScriptException("Could not understand form of connected output script: " + connectedScript);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ 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.DummySigSigner;
|
||||
import com.google.bitcoin.signers.LocalTransactionSigner;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.store.UnreadableWalletException;
|
||||
|
@ -823,6 +824,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||
* Returns RedeemData object or null if no such data was found.
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public RedeemData findRedeemDataFromScriptHash(byte[] payToScriptHash) {
|
||||
lock.lock();
|
||||
try {
|
||||
|
@ -2989,6 +2991,15 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||
*/
|
||||
public boolean shuffleOutputs = true;
|
||||
|
||||
/**
|
||||
* If this flag is set (the default), any signature this wallet failed to obtain during completion will be
|
||||
* replaced with dummy signature ({@link com.google.bitcoin.crypto.TransactionSignature#dummy() }). This is
|
||||
* useful when you'd like to know the fee for a transaction without knowing the user's password, as fee depends
|
||||
* on size. If flag set to false, missing signatures will appear as empty sigs (OP_0) in transaction
|
||||
* inputs' scriptSigs.
|
||||
*/
|
||||
public boolean useDummySignatures = true;
|
||||
|
||||
// Tracks if this has been passed to wallet.completeTx already: just a safety check.
|
||||
private boolean completed;
|
||||
|
||||
|
@ -3333,7 +3344,7 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||
|
||||
// Now sign the inputs, thus proving that we are entitled to redeem the connected outputs.
|
||||
if (req.signInputs) {
|
||||
signTransaction(req.tx, req.aesKey);
|
||||
signTransaction(req);
|
||||
}
|
||||
|
||||
// Check size.
|
||||
|
@ -3363,21 +3374,55 @@ 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. Optional aesKey should be provided if this wallet is encrypted</p>
|
||||
* <p>Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction
|
||||
* to have all necessary inputs connected or they will be ignored.</p>
|
||||
* <p>Actual signing is done by pluggable {@link #signers} and it's not guaranteed that
|
||||
* transaction will be complete in the end.</p>
|
||||
*/
|
||||
public void signTransaction(Transaction tx, @Nullable KeyParameter aesKey) {
|
||||
public void signTransaction(SendRequest req) {
|
||||
lock.lock();
|
||||
try {
|
||||
Transaction tx = req.tx;
|
||||
List<TransactionInput> inputs = tx.getInputs();
|
||||
List<TransactionOutput> outputs = tx.getOutputs();
|
||||
checkState(inputs.size() > 0);
|
||||
checkState(outputs.size() > 0);
|
||||
KeyBag maybeDecryptingKeyBag = aesKey != null ? new DecryptingKeyBag(this, aesKey) : this;
|
||||
|
||||
KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(this, req.aesKey);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey();
|
||||
RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag);
|
||||
checkNotNull(redeemData, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash());
|
||||
txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript));
|
||||
}
|
||||
|
||||
TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx);
|
||||
for (TransactionSigner signer : signers) {
|
||||
if (!signer.signInputs(tx, maybeDecryptingKeyBag))
|
||||
if (!signer.signInputs(proposal, maybeDecryptingKeyBag))
|
||||
log.info("{} returned false for the tx", signer.getClass().getName());
|
||||
}
|
||||
|
||||
if (req.useDummySignatures)
|
||||
new DummySigSigner().signInputs(proposal, maybeDecryptingKeyBag);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@ -4273,7 +4318,9 @@ public class Wallet extends BaseTaggableObject implements Serializable, BlockCha
|
|||
}
|
||||
rekeyTx.getConfidence().setSource(TransactionConfidence.Source.SELF);
|
||||
rekeyTx.setPurpose(Transaction.Purpose.KEY_ROTATION);
|
||||
signTransaction(rekeyTx, aesKey);
|
||||
SendRequest req = SendRequest.forTx(rekeyTx);
|
||||
req.aesKey = aesKey;
|
||||
signTransaction(req);
|
||||
// KeyTimeCoinSelector should never select enough inputs to push us oversize.
|
||||
checkState(rekeyTx.bitcoinSerialize().length < Transaction.MAX_STANDARD_TX_SIZE);
|
||||
return rekeyTx;
|
||||
|
|
|
@ -388,6 +388,14 @@ public class Script {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the given scriptSig with a signature placeholder on the given position replaced with the given signature.
|
||||
*/
|
||||
public Script getScriptSigWithSignature(Script scriptSig, byte[] sigBytes, int index) {
|
||||
return ScriptBuilder.updateScriptWithSignature(scriptSig, sigBytes, index, isPayToScriptHash());
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////// Interface used during verification of transactions/blocks ////////////////////////////////
|
||||
|
||||
|
|
|
@ -23,10 +23,7 @@ import com.google.bitcoin.crypto.TransactionSignature;
|
|||
import com.google.common.collect.Lists;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.google.bitcoin.script.ScriptOpCodes.*;
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
@ -202,6 +199,32 @@ public class ScriptBuilder {
|
|||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a copy of the given scriptSig with a signature placeholder on the given position replaced with the given signature.
|
||||
*/
|
||||
public static Script updateScriptWithSignature(Script scriptSig, byte[] signature, int index, boolean isMultisig) {
|
||||
ScriptBuilder builder = new ScriptBuilder();
|
||||
Iterator<ScriptChunk> it = scriptSig.getChunks().iterator();
|
||||
int numChunks = 0;
|
||||
// skip first OP_0 for multisig scripts
|
||||
if (isMultisig)
|
||||
builder.addChunk(it.next());
|
||||
for (; it.hasNext(); ) {
|
||||
ScriptChunk chunk = it.next();
|
||||
// replace the first OP_0 with signature data
|
||||
if (chunk.equalsOpCode(OP_0)) {
|
||||
if (numChunks == index)
|
||||
builder.data(signature);
|
||||
else
|
||||
builder.addChunk(chunk);
|
||||
} else {
|
||||
builder.addChunk(chunk);
|
||||
}
|
||||
numChunks++;
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a scriptPubKey that sends to the given script hash. Read
|
||||
* <a href="https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki">BIP 16</a> to learn more about this
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* 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.*;
|
||||
import com.google.bitcoin.crypto.ChildNumber;
|
||||
import com.google.bitcoin.crypto.TransactionSignature;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import com.google.bitcoin.wallet.RedeemData;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
/**
|
||||
* <p>This signer may be used as a template for creating custom multisig transaction signers.</p>
|
||||
* <p>
|
||||
* Concrete implementations have to implement {@link #getSignature(com.google.bitcoin.core.Sha256Hash, java.util.List)}
|
||||
* method returning a signature and a public key of the keypair used to created that signature.
|
||||
* It's up to custom implementation where to locate signatures: it may be a network connection,
|
||||
* some local API or something else.
|
||||
* </p>
|
||||
*/
|
||||
public abstract class CustomTransactionSigner extends StatelessTransactionSigner {
|
||||
private static final Logger log = LoggerFactory.getLogger(CustomTransactionSigner.class);
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
|
||||
Transaction tx = propTx.partialTx;
|
||||
int numInputs = tx.getInputs().size();
|
||||
for (int i = 0; i < numInputs; i++) {
|
||||
TransactionInput txIn = tx.getInput(i);
|
||||
TransactionOutput txOut = txIn.getConnectedOutput();
|
||||
if (txOut == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!txOut.getScriptPubKey().isPayToScriptHash()) {
|
||||
log.warn("CustomTransactionSigner works only with P2SH transactions");
|
||||
return false;
|
||||
}
|
||||
Script inputScript = txIn.getScriptSig();
|
||||
checkNotNull(inputScript);
|
||||
RedeemData redeemData = txIn.getConnectedRedeemData(keyBag);
|
||||
if (redeemData == null) {
|
||||
log.warn("No redeem data found for input {}", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
Sha256Hash sighash = tx.hashForSignature(i, redeemData.redeemScript, Transaction.SigHash.ALL, false);
|
||||
SignatureAndKey sigKey = getSignature(sighash, propTx.keyPaths.get(txIn));
|
||||
TransactionSignature txSig = new TransactionSignature(sigKey.sig, Transaction.SigHash.ALL, false);
|
||||
int sigIndex = redeemData.getKeyIndex(sigKey.pubKey);
|
||||
if (sigIndex < 0)
|
||||
throw new RuntimeException("Redeem script doesn't contain our key"); // This should not happen
|
||||
inputScript = txOut.getScriptPubKey().getScriptSigWithSignature(inputScript, txSig.encodeToBitcoin(), sigIndex);
|
||||
txIn.setScriptSig(inputScript);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected abstract SignatureAndKey getSignature(Sha256Hash sighash, List<ChildNumber> derivationPath);
|
||||
|
||||
public class SignatureAndKey {
|
||||
public final ECKey.ECDSASignature sig;
|
||||
public final ECKey pubKey;
|
||||
|
||||
public SignatureAndKey(ECKey.ECDSASignature sig, ECKey pubKey) {
|
||||
this.sig = sig;
|
||||
this.pubKey = pubKey;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* 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.TransactionInput;
|
||||
import com.google.bitcoin.crypto.TransactionSignature;
|
||||
import com.google.bitcoin.script.Script;
|
||||
import com.google.bitcoin.script.ScriptChunk;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* This transaction signer fills up empty signatures in partial input scripts with a dummy signature.
|
||||
*/
|
||||
public class DummySigSigner extends StatelessTransactionSigner {
|
||||
private static final Logger log = LoggerFactory.getLogger(DummySigSigner.class);
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
|
||||
int numInputs = propTx.partialTx.getInputs().size();
|
||||
byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin();
|
||||
for (int i = 0; i < numInputs; i++) {
|
||||
TransactionInput txIn = propTx.partialTx.getInput(i);
|
||||
if (txIn.getConnectedOutput() == null) {
|
||||
log.warn("Missing connected output, assuming input {} is already signed.", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey();
|
||||
Script inputScript = txIn.getScriptSig();
|
||||
if (scriptPubKey.isPayToScriptHash()) {
|
||||
// all chunks except the first one (OP_0) and the last (redeem script) are signatures
|
||||
for (int j = 1; j < inputScript.getChunks().size() - 1; j++) {
|
||||
ScriptChunk scriptChunk = inputScript.getChunks().get(j);
|
||||
if (scriptChunk.equalsOpCode(0)) {
|
||||
txIn.setScriptSig(scriptPubKey.getScriptSigWithSignature(inputScript, dummySig, j));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (inputScript.getChunks().get(0).equalsOpCode(0))
|
||||
txIn.setScriptSig(scriptPubKey.getScriptSigWithSignature(inputScript, dummySig, 0));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -19,22 +19,27 @@ 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.DeterministicKey;
|
||||
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 com.google.bitcoin.wallet.RedeemData;
|
||||
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>
|
||||
* <p>{@link TransactionSigner} implementation for signing inputs using keys from provided {@link com.google.bitcoin.wallet.KeyBag}.</p>
|
||||
* <p>This signer doesn't create input scripts for tx inputs. Instead it expects inputs to contain scripts with
|
||||
* empty sigs and replaces one of the empty sigs with calculated signature.
|
||||
* </p>
|
||||
* <p>This signer is always implicitly added into every wallet and it is the first signer to be executed during tx
|
||||
* completion. As the first signer to create a signature, it stores derivation path of the signing key in a given
|
||||
* {@link ProposedTransaction} object that will be also passed then to the next signer in chain. This allows other
|
||||
* signers to use correct signing key for P2SH inputs, because all the keys involved in a single P2SH address have
|
||||
* the same derivation path.</p>
|
||||
* <p>This signer always uses {@link com.google.bitcoin.core.Transaction.SigHash#ALL} signing mode.</p>
|
||||
*/
|
||||
public class LocalTransactionSigner implements TransactionSigner {
|
||||
public class LocalTransactionSigner extends StatelessTransactionSigner {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalTransactionSigner.class);
|
||||
|
||||
@Override
|
||||
|
@ -43,16 +48,8 @@ public class LocalTransactionSigner implements TransactionSigner {
|
|||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deserialize(byte[] data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(Transaction tx, KeyBag keyBag) {
|
||||
public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
|
||||
Transaction tx = propTx.partialTx;
|
||||
int numInputs = tx.getInputs().size();
|
||||
for (int i = 0; i < numInputs; i++) {
|
||||
TransactionInput txIn = tx.getInput(i);
|
||||
|
@ -61,14 +58,6 @@ public class LocalTransactionSigner implements TransactionSigner {
|
|||
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
|
||||
|
@ -80,30 +69,44 @@ public class LocalTransactionSigner implements TransactionSigner {
|
|||
// 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;
|
||||
RedeemData redeemData = txIn.getConnectedRedeemData(keyBag);
|
||||
ECKey key;
|
||||
// locate private key in redeem data. For pay-to-address and pay-to-key inputs RedeemData will always contain
|
||||
// only one key (with private bytes). For P2SH inputs RedeemData will contain multiple keys, one of which MAY
|
||||
// have private bytes
|
||||
if (redeemData == null || (key = redeemData.getFullKey()) == null) {
|
||||
log.warn("No local key found for input {}", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey();
|
||||
Script inputScript = txIn.getScriptSig();
|
||||
// script here would be either a standard CHECKSIG program for pay-to-address or pay-to-pubkey inputs or
|
||||
// a CHECKMULTISIG program for P2SH inputs
|
||||
byte[] script = redeemData.redeemScript.getProgram();
|
||||
try {
|
||||
signature = tx.calculateSignature(i, key, connectedPubKeyScript, Transaction.SigHash.ALL, false);
|
||||
TransactionSignature signature = tx.calculateSignature(i, key, script, Transaction.SigHash.ALL, false);
|
||||
|
||||
// at this point we have incomplete inputScript with OP_0 in place of one or more signatures. We already
|
||||
// have calculated the signature using the local key and now need to insert it in the correct place
|
||||
// within inputScript. For pay-to-address and pay-to-key script there is only one signature and it always
|
||||
// goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to get an index of the
|
||||
// signing key within CHECKMULTISIG program as signatures are placed in the same order as public keys
|
||||
// in redeem script
|
||||
int sigIndex = redeemData.getKeyIndex(key);
|
||||
// update input script with the signature at the proper position
|
||||
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), sigIndex);
|
||||
txIn.setScriptSig(inputScript);
|
||||
|
||||
// for P2SH inputs we need to share derivation path of the signing key with other signers, so that they
|
||||
// use correct key to calculate their signatures
|
||||
if (key instanceof DeterministicKey)
|
||||
propTx.keyPaths.put(txIn, (((DeterministicKey) key).getPath()));
|
||||
} 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);
|
||||
log.warn("No private key in keypair for input {}", 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,30 @@
|
|||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* A signer that doesn't have any state to be serialized.
|
||||
*/
|
||||
public abstract class StatelessTransactionSigner implements TransactionSigner {
|
||||
@Override
|
||||
public void deserialize(byte[] data) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] serialize() {
|
||||
return new byte[0];
|
||||
}
|
||||
}
|
|
@ -16,21 +16,46 @@
|
|||
package com.google.bitcoin.signers;
|
||||
|
||||
import com.google.bitcoin.core.Transaction;
|
||||
import com.google.bitcoin.core.TransactionInput;
|
||||
import com.google.bitcoin.crypto.ChildNumber;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
import org.spongycastle.crypto.params.KeyParameter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* <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
|
||||
* along with the wallet data. In order for a wallet to recreate signer after deserialization, each signer
|
||||
* should have no-args constructor</p>
|
||||
*/
|
||||
public interface TransactionSigner {
|
||||
|
||||
/**
|
||||
* This class wraps transaction proposed to complete keeping a metadata that may be updated, used and effectively
|
||||
* shared by transaction signers.
|
||||
*/
|
||||
public class ProposedTransaction {
|
||||
|
||||
public final Transaction partialTx;
|
||||
|
||||
/**
|
||||
* HD key paths used for each input to derive a signing key. It's useful for multisig inputs only.
|
||||
* The keys used to create a single P2SH address have the same derivation path, so to use a correct key each signer
|
||||
* has to know a derivation path of signing keys used by previous signers. For each input signers will use the
|
||||
* same derivation path and we need to store only one key path per input.
|
||||
*/
|
||||
public final Map<TransactionInput, List<ChildNumber>> keyPaths;
|
||||
|
||||
public ProposedTransaction(Transaction partialTx) {
|
||||
this.partialTx = partialTx;
|
||||
this.keyPaths = new HashMap<TransactionInput, List<ChildNumber>>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this signer is ready to be used.
|
||||
*/
|
||||
|
@ -51,6 +76,6 @@ public interface TransactionSigner {
|
|||
* 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);
|
||||
boolean signInputs(ProposedTransaction propTx, KeyBag keyBag);
|
||||
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
*/
|
||||
package com.google.bitcoin.testing;
|
||||
|
||||
import com.google.bitcoin.core.Transaction;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.wallet.KeyBag;
|
||||
|
||||
|
@ -46,7 +45,7 @@ public class NopTransactionSigner implements TransactionSigner {
|
|||
}
|
||||
|
||||
@Override
|
||||
public boolean signInputs(Transaction t, KeyBag keyBag) {
|
||||
public boolean signInputs(ProposedTransaction t, KeyBag keyBag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,20 +28,37 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
|||
|
||||
/**
|
||||
* A DecryptingKeyBag filters a pre-existing key bag, decrypting keys as they are requested using the provided
|
||||
* AES key.
|
||||
* AES key. If the keys are encrypted and no AES key provided, {@link com.google.bitcoin.core.ECKey.KeyIsEncryptedException}
|
||||
* will be thrown.
|
||||
*/
|
||||
public class DecryptingKeyBag implements KeyBag {
|
||||
protected final KeyBag target;
|
||||
protected final KeyParameter aesKey;
|
||||
|
||||
public DecryptingKeyBag(KeyBag target, KeyParameter aesKey) {
|
||||
public DecryptingKeyBag(KeyBag target, @Nullable KeyParameter aesKey) {
|
||||
this.target = checkNotNull(target);
|
||||
this.aesKey = checkNotNull(aesKey);
|
||||
this.aesKey = aesKey;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ECKey maybeDecrypt(ECKey key) {
|
||||
return key == null ? null : key.decrypt(aesKey);
|
||||
if (key == null)
|
||||
return null;
|
||||
else if (key.isEncrypted()) {
|
||||
if (aesKey == null)
|
||||
throw new ECKey.KeyIsEncryptedException();
|
||||
return key.decrypt(aesKey);
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
private RedeemData maybeDecrypt(RedeemData redeemData) {
|
||||
List<ECKey> decryptedKeys = new ArrayList<ECKey>();
|
||||
for (ECKey key : redeemData.keys) {
|
||||
decryptedKeys.add(maybeDecrypt(key));
|
||||
}
|
||||
return RedeemData.of(decryptedKeys, redeemData.redeemScript);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
|
@ -59,11 +76,6 @@ public class DecryptingKeyBag implements KeyBag {
|
|||
@Nullable
|
||||
@Override
|
||||
public RedeemData findRedeemDataFromScriptHash(byte[] scriptHash) {
|
||||
RedeemData redeemData = target.findRedeemDataFromScriptHash(scriptHash);
|
||||
List<ECKey> decryptedKeys = new ArrayList<ECKey>();
|
||||
for (ECKey key : redeemData.keys) {
|
||||
decryptedKeys.add(maybeDecrypt(key));
|
||||
}
|
||||
return RedeemData.of(decryptedKeys, redeemData.redeemScript);
|
||||
return maybeDecrypt(target.findRedeemDataFromScriptHash(scriptHash));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,34 +18,45 @@ package com.google.bitcoin.wallet;
|
|||
import com.google.bitcoin.core.ECKey;
|
||||
import com.google.bitcoin.script.Script;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
/**
|
||||
* This class aggregates portion of data required to spend transaction output.
|
||||
* This class aggregates data required to spend transaction output.
|
||||
*
|
||||
* For pay-to-address and pay-to-pubkey transactions it will have only a single key and no redeem script.
|
||||
* For multisignature transactions there will be multiple keys one of which will be a full key and the rest are watch only.
|
||||
* These keys will be sorted in the same order they appear in a program (lexicographical order).
|
||||
* For P2SH transactions there also will be a redeem script.
|
||||
* For pay-to-address and pay-to-pubkey transactions it will have only a single key and CHECKSIG program as redeemScript.
|
||||
* For multisignature transactions there will be multiple keys one of which will be a full key and the rest are watch only,
|
||||
* redeem script will be a CHECKMULTISIG program. Keys will be sorted in the same order they appear in
|
||||
* a program (lexicographical order).
|
||||
*/
|
||||
public class RedeemData {
|
||||
@Nullable public final Script redeemScript;
|
||||
public final Script redeemScript;
|
||||
public final List<ECKey> keys;
|
||||
|
||||
private RedeemData(List<ECKey> keys, @Nullable Script redeemScript) {
|
||||
private RedeemData(List<ECKey> keys, Script redeemScript) {
|
||||
this.redeemScript = redeemScript;
|
||||
List<ECKey> sortedKeys = new ArrayList<ECKey>(keys);
|
||||
Collections.sort(sortedKeys, ECKey.PUBKEY_COMPARATOR);
|
||||
this.keys = sortedKeys;
|
||||
}
|
||||
|
||||
public static RedeemData of(List<ECKey> keys, @Nullable Script redeemScript) {
|
||||
public static RedeemData of(List<ECKey> keys, Script redeemScript) {
|
||||
return new RedeemData(keys, redeemScript);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates RedeemData for pay-to-address or pay-to-pubkey input. Provided key is a single private key needed
|
||||
* to spend such inputs and provided program should be a proper CHECKSIG program.
|
||||
*/
|
||||
public static RedeemData of(ECKey key, Script program) {
|
||||
checkArgument(program.isSentToAddress() || program.isSentToRawPubKey());
|
||||
return key != null ? new RedeemData(Arrays.asList(key), program) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first key that has private bytes
|
||||
*/
|
||||
|
@ -58,8 +69,30 @@ public class RedeemData {
|
|||
return key;
|
||||
} catch (IllegalStateException e) {
|
||||
// no private bytes. Proceed to the next key
|
||||
} catch (ECKey.MissingPrivateKeyException e) {
|
||||
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns index of the given key in program that this RedeemData satisfies. For CHECKSIG programs this
|
||||
* will always be 0. Returned index may be used to insert corresponding signature into proper place in input script.
|
||||
* If key is not found, -1 is returned.
|
||||
*/
|
||||
public int getKeyIndex(ECKey key) {
|
||||
boolean isMultisig = keys.size() > 0;
|
||||
if (isMultisig) {
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
byte[] pubKey = keys.get(i).getPubKey();
|
||||
if (Arrays.equals(pubKey, key.getPubKey()))
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package com.google.bitcoin.core;
|
|||
|
||||
import com.google.bitcoin.core.Wallet.SendRequest;
|
||||
import com.google.bitcoin.crypto.*;
|
||||
import com.google.bitcoin.signers.CustomTransactionSigner;
|
||||
import com.google.bitcoin.signers.TransactionSigner;
|
||||
import com.google.bitcoin.store.BlockStoreException;
|
||||
import com.google.bitcoin.store.MemoryBlockStore;
|
||||
|
@ -104,6 +105,16 @@ public class WalletTest extends TestWithWallet {
|
|||
final DeterministicKeyChain keyChain = new DeterministicKeyChain(new SecureRandom());
|
||||
DeterministicKey partnerKey = DeterministicKey.deserializeB58(null, keyChain.getWatchingKey().serializePubB58());
|
||||
|
||||
CustomTransactionSigner signer = new CustomTransactionSigner() {
|
||||
@Override
|
||||
protected SignatureAndKey getSignature(Sha256Hash sighash, List<ChildNumber> derivationPath) {
|
||||
ImmutableList<ChildNumber> keyPath = ImmutableList.copyOf(derivationPath);
|
||||
DeterministicKey key = keyChain.getKeyByPath(keyPath, true);
|
||||
return new SignatureAndKey(key.sign(sighash), key.getPubOnly());
|
||||
}
|
||||
};
|
||||
wallet.addTransactionSigner(signer);
|
||||
|
||||
wallet.addFollowingAccountKeys(ImmutableList.of(partnerKey));
|
||||
}
|
||||
|
||||
|
@ -134,6 +145,14 @@ public class WalletTest extends TestWithWallet {
|
|||
basicSpendingCommon(encryptedWallet, myEncryptedAddress, new ECKey().toAddress(params), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void basicSpendingFromP2SH() throws Exception {
|
||||
createMarriedWallet();
|
||||
Address destination = new ECKey().toAddress(params);
|
||||
myAddress = wallet.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS);
|
||||
basicSpendingCommon(wallet, myAddress, destination, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spendingWithIncompatibleSigners() throws Exception {
|
||||
wallet.addTransactionSigner(new NopTransactionSigner(true));
|
||||
|
@ -319,7 +338,7 @@ public class WalletTest extends TestWithWallet {
|
|||
assertEquals(wallet.getChangeAddress(), t2.getOutput(1).getScriptPubKey().getToAddress(params));
|
||||
|
||||
// Do some basic sanity checks.
|
||||
basicSanityChecks(wallet, t2, toAddress, destination);
|
||||
basicSanityChecks(wallet, t2, destination);
|
||||
|
||||
// Broadcast the transaction and commit.
|
||||
broadcastAndCommit(wallet, t2);
|
||||
|
@ -359,11 +378,8 @@ public class WalletTest extends TestWithWallet {
|
|||
assertTrue(depthFuture.isDone());
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
// Having a test for deprecated method getFromAddress() is no evil so we suppress the warning here.
|
||||
private void basicSanityChecks(Wallet wallet, Transaction t, Address fromAddress, Address destination) throws VerificationException {
|
||||
private void basicSanityChecks(Wallet wallet, Transaction t, Address destination) throws VerificationException {
|
||||
assertEquals("Wrong number of tx inputs", 1, t.getInputs().size());
|
||||
assertEquals(fromAddress, t.getInput(0).getScriptSig().getFromAddress(params));
|
||||
assertEquals("Wrong number of tx outputs",2, t.getOutputs().size());
|
||||
assertEquals(destination, t.getOutput(0).getScriptPubKey().getToAddress(params));
|
||||
assertEquals(wallet.getChangeAddress(), t.getOutputs().get(1).getScriptPubKey().getToAddress(params));
|
||||
|
@ -1349,7 +1365,7 @@ public class WalletTest extends TestWithWallet {
|
|||
Transaction t3 = new Transaction(params);
|
||||
t3.addOutput(v3, k3.toAddress(params));
|
||||
t3.addInput(o2);
|
||||
wallet.signTransaction(t3, null);
|
||||
wallet.signTransaction(SendRequest.forTx(t3));
|
||||
|
||||
// Commit t3, so the coins from the pending t2 are spent
|
||||
wallet.commitTx(t3);
|
||||
|
@ -1890,7 +1906,7 @@ public class WalletTest extends TestWithWallet {
|
|||
Transaction spendTx5 = new Transaction(params);
|
||||
spendTx5.addOutput(CENT, notMyAddr);
|
||||
spendTx5.addInput(tx5.getOutput(0));
|
||||
wallet.signTransaction(spendTx5, null);
|
||||
wallet.signTransaction(SendRequest.forTx(spendTx5));
|
||||
|
||||
wallet.receiveFromBlock(spendTx5, block, AbstractBlockChain.NewBlockType.BEST_CHAIN, 4);
|
||||
assertEquals(COIN, wallet.getBalance());
|
||||
|
@ -2141,7 +2157,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
|
||||
wallet.signTransaction(request4.tx, null);
|
||||
wallet.signTransaction(request4);
|
||||
byte[] scriptSig = request4.tx.getInput(0).getScriptBytes();
|
||||
wallet.completeTx(request4);
|
||||
assertEquals(1, request4.tx.getInputs().size());
|
||||
|
@ -2385,9 +2401,20 @@ public class WalletTest extends TestWithWallet {
|
|||
assertEquals(200, tx.getInputs().size());
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Test
|
||||
public void completeTxPartiallySigned() throws Exception {
|
||||
public void completeTxPartiallySignedWithDummySigs() throws Exception {
|
||||
byte[] dummySig = TransactionSignature.dummy().encodeToBitcoin();
|
||||
completeTxPartiallySigned(true, dummySig);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeTxPartiallySignedWithoutDummySigs() throws Exception {
|
||||
byte[] emptySig = new byte[]{};
|
||||
completeTxPartiallySigned(false, emptySig);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
public void completeTxPartiallySigned(boolean useDummySignatures, byte[] expectedSig) 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 = ECKey.fromPublicOnly(priv.getPubKeyPoint());
|
||||
|
@ -2402,15 +2429,16 @@ public class WalletTest extends TestWithWallet {
|
|||
|
||||
ECKey dest = new ECKey();
|
||||
Wallet.SendRequest req = Wallet.SendRequest.emptyWallet(dest.toAddress(params));
|
||||
req.useDummySignatures = useDummySignatures;
|
||||
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);
|
||||
assertArrayEquals(expectedSig, input.getScriptSig().getChunks().get(0).data);
|
||||
} else if (input.getConnectedOutput().getParentTransaction().equals(t2)) {
|
||||
assertArrayEquals(dummySig, input.getScriptSig().getChunks().get(0).data);
|
||||
assertArrayEquals(expectedSig, input.getScriptSig().getChunks().get(0).data);
|
||||
} else if (input.getConnectedOutput().getParentTransaction().equals(t3)) {
|
||||
input.getScriptSig().correctlySpends(req.tx, i, t3.getOutput(0).getScriptPubKey(), true);
|
||||
}
|
||||
|
@ -2512,7 +2540,7 @@ public class WalletTest extends TestWithWallet {
|
|||
|
||||
@Test
|
||||
public void transactionSignersShouldBeSerializedAlongWithWallet() throws Exception {
|
||||
final TransactionSigner signer = new NopTransactionSigner(true);
|
||||
TransactionSigner signer = new NopTransactionSigner(true);
|
||||
wallet.addTransactionSigner(signer);
|
||||
assertEquals(2, wallet.getTransactionSigners().size());
|
||||
Protos.Wallet protos = new WalletProtobufSerializer().walletToProto(wallet);
|
||||
|
|
|
@ -181,8 +181,14 @@ public class ScriptTest {
|
|||
assertThat(inputScript.getChunks().get(1).opcode, equalTo(OP_0));
|
||||
assertThat(inputScript.getChunks().get(2).opcode, equalTo(OP_0));
|
||||
assertThat(inputScript.getChunks().get(3).data, equalTo(multisigScript.getProgram()));
|
||||
|
||||
inputScript = ScriptBuilder.updateScriptWithSignature(inputScript, dummySig.encodeToBitcoin(), 1, true);
|
||||
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
|
||||
assertThat(inputScript.getChunks().get(1).opcode, equalTo(OP_0));
|
||||
assertThat(inputScript.getChunks().get(2).data, equalTo(dummySig.encodeToBitcoin()));
|
||||
assertThat(inputScript.getChunks().get(3).data, equalTo(multisigScript.getProgram()));
|
||||
}
|
||||
|
||||
|
||||
private Script parseScriptString(String string) throws Exception {
|
||||
String[] words = string.split("[ \\t\\n]");
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue