Always use position 0 for LocalTransactionSigner sig, we don't know in advance what position we'll end up in

Fix p2sh bloom filter construction
Proper signature insertion for P2SH scriptSig
This commit is contained in:
Devrandom
2014-09-01 17:05:15 -07:00
committed by Mike Hearn
parent 7cb795235d
commit 1f39dcdc6f
8 changed files with 132 additions and 53 deletions

View File

@@ -20,6 +20,7 @@ package com.google.bitcoin.script;
import com.google.bitcoin.core.*;
import com.google.bitcoin.crypto.TransactionSignature;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -37,6 +38,7 @@ import java.util.*;
import static com.google.bitcoin.script.ScriptOpCodes.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
// TODO: Make this class a superclass with derived classes giving accessor methods for the various common templates.
@@ -202,7 +204,7 @@ public class Script {
}
/**
* Returns true if this script is of the form <sig> OP_CHECKSIG. This form was originally intended for transactions
* Returns true if this script is of the form <pubkey> OP_CHECKSIG. This form was originally intended for transactions
* where the peers talked to each other directly via TCP/IP, but has fallen out of favor with time due to that mode
* of operation being susceptible to man-in-the-middle attacks. It is still used in coinbase outputs and can be
* useful more exotic types of transaction, but today most payments are to addresses.
@@ -402,10 +404,73 @@ public class Script {
}
/**
* Returns a copy of the given scriptSig with a signature placeholder on the given position replaced with the given signature.
* Returns a copy of the given scriptSig with the signature inserted in the given position.
*/
public Script getScriptSigWithSignature(Script scriptSig, byte[] sigBytes, int index) {
return ScriptBuilder.updateScriptWithSignature(scriptSig, sigBytes, index, isPayToScriptHash());
int sigsPrefixCount = 0;
int sigsSuffixCount = 0;
if (isPayToScriptHash()) {
sigsPrefixCount = 1; // OP_0 <sig>* <redeemScript>
sigsSuffixCount = 1;
} else if (isSentToMultiSig()) {
sigsPrefixCount = 1; // OP_0 <sig>*
} else if (isSentToAddress()) {
sigsSuffixCount = 1; // <sig> <pubkey>
}
return ScriptBuilder.updateScriptWithSignature(scriptSig, sigBytes, index, sigsPrefixCount, sigsSuffixCount);
}
/**
* Returns the index where a signature by the key should be inserted. Only applicable to
* a P2SH scriptSig.
*/
public int getSigInsertionIndex(Sha256Hash hash, ECKey signingKey) {
// Iterate over existing signatures, skipping the initial OP_0, the final redeem script
// and any placeholder OP_0 sigs.
List<ScriptChunk> existingChunks = chunks.subList(1, chunks.size() - 1);
ScriptChunk redeemScriptChunk = chunks.get(chunks.size() - 1);
checkNotNull(redeemScriptChunk.data);
Script redeemScript = new Script(redeemScriptChunk.data);
int sigCount = 0;
int myIndex = redeemScript.findKeyInRedeem(signingKey);
for (ScriptChunk chunk : existingChunks) {
if (chunk.opcode == OP_0) {
// OP_0, skip
} else {
checkNotNull(chunk.data);
if (myIndex < redeemScript.findSigInRedeem(chunk.data, hash))
return sigCount;
sigCount++;
}
}
return sigCount;
}
private int findKeyInRedeem(ECKey key) {
checkArgument(chunks.get(0).isOpCode()); // P2SH scriptSig
int numKeys = Script.decodeFromOpN(chunks.get(chunks.size() - 2).opcode);
for (int i = 0 ; i < numKeys ; i++) {
if (Arrays.equals(chunks.get(1 + i).data, key.getPubKey())) {
return i;
}
}
throw new IllegalStateException("Could not find matching key " + key.toString() + " in script " + this);
}
private int findSigInRedeem(byte[] signatureBytes, Sha256Hash hash) {
checkArgument(chunks.get(0).isOpCode()); // P2SH scriptSig
int numKeys = Script.decodeFromOpN(chunks.get(chunks.size() - 2).opcode);
TransactionSignature signature = TransactionSignature.decodeFromBitcoin(signatureBytes, true);
for (int i = 0 ; i < numKeys ; i++) {
if (ECKey.fromPublicOnly(chunks.get(i + 1).data).verify(hash, signature)) {
return i;
}
}
throw new IllegalStateException("Could not find matching key for signature on " + hash.toString() + " sig " + Utils.HEX.encode(signatureBytes));
}

View File

@@ -27,6 +27,7 @@ import java.util.*;
import static com.google.bitcoin.script.ScriptOpCodes.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
/**
* <p>Tools for the construction of commonly used script types. You don't normally need this as it's hidden behind
@@ -200,28 +201,56 @@ public class ScriptBuilder {
}
/**
* Returns a copy of the given scriptSig with a signature placeholder on the given position replaced with the given signature.
* Returns a copy of the given scriptSig with the signature inserted in the given position.
*
* This function assumes that any missing sigs have OP_0 placeholders.
*
* @param targetIndex where to insert the signature
* @param sigsPrefixCount how many items to copy verbatim (e.g. initial OP_0 for multisig)
* @param sigsSuffixCount how many items to copy verbatim at end (e.g. redeemScript for P2SH)
*/
public static Script updateScriptWithSignature(Script scriptSig, byte[] signature, int index, boolean isMultisig) {
public static Script updateScriptWithSignature(Script scriptSig, byte[] signature, int targetIndex,
int sigsPrefixCount, int sigsSuffixCount) {
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)
List<ScriptChunk> inputChunks = scriptSig.getChunks();
int totalChunks = inputChunks.size();
// copy the prefix
for (ScriptChunk chunk: inputChunks.subList(0, sigsPrefixCount))
builder.addChunk(chunk);
// copy the sigs
int pos = 0;
boolean inserted = false;
for (ScriptChunk chunk: inputChunks.subList(sigsPrefixCount, totalChunks - sigsSuffixCount)) {
if (pos == targetIndex) {
inserted = true;
builder.data(signature);
else
builder.addChunk(chunk);
} else {
builder.addChunk(chunk);
pos++;
}
numChunks++;
if (!chunk.equalsOpCode(OP_0)) {
builder.addChunk(chunk);
pos++;
}
}
// add OP_0's if needed, since we skipped them in the previous loop
while (pos < totalChunks - sigsPrefixCount - sigsSuffixCount) {
if (pos == targetIndex) {
inserted = true;
builder.data(signature);
}
else {
builder.addChunk(new ScriptChunk(OP_0, null));
}
pos++;
}
// copy the suffix
for (ScriptChunk chunk: inputChunks.subList(totalChunks - sigsSuffixCount, totalChunks))
builder.addChunk(chunk);
checkState(inserted);
return builder.build();
}

View File

@@ -71,9 +71,7 @@ public abstract class CustomTransactionSigner extends StatelessTransactionSigner
Sha256Hash sighash = tx.hashForSignature(i, redeemData.redeemScript, Transaction.SigHash.ALL, false);
SignatureAndKey sigKey = getSignature(sighash, propTx.keyPaths.get(scriptPubKey));
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
int sigIndex = inputScript.getSigInsertionIndex(sighash, sigKey.pubKey);
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, txSig.encodeToBitcoin(), sigIndex);
txIn.setScriptSig(inputScript);
}

View File

@@ -90,11 +90,11 @@ public class LocalTransactionSigner extends StatelessTransactionSigner {
// 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
// goes first in an inputScript (sigIndex = 0). In P2SH input scripts we need to figure out our relative
// position relative to other signers. Since we don't have that information at this point, and since
// we always run first, we have to depend on the other signers rearranging the signatures as needed.
// Therefore, always place as first signature.
int sigIndex = 0;
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(), sigIndex);
txIn.setScriptSig(inputScript);

View File

@@ -86,6 +86,7 @@ public class MissingSigResolutionSigner extends StatelessTransactionSigner {
}
}
}
// TODO handle non-P2SH multisig
}
return true;
}

View File

@@ -629,7 +629,7 @@ public class KeyChainGroup implements KeyBag {
for (Map.Entry<ByteString, RedeemData> entry : marriedKeysRedeemData.entrySet()) {
filter.insert(entry.getKey().toByteArray());
filter.insert(ScriptBuilder.createP2SHOutputScript(entry.getValue().redeemScript).getProgram());
filter.insert(entry.getValue().redeemScript.getProgram());
}
for (DeterministicKeyChain chain : chains) {

View File

@@ -75,24 +75,4 @@ public class RedeemData {
}
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;
}
}
}

View File

@@ -182,9 +182,15 @@ public class ScriptTest {
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);
inputScript = ScriptBuilder.updateScriptWithSignature(inputScript, dummySig.encodeToBitcoin(), 0, 1, 1);
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
assertThat(inputScript.getChunks().get(1).opcode, equalTo(OP_0));
assertThat(inputScript.getChunks().get(1).data, equalTo(dummySig.encodeToBitcoin()));
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, 1, 1);
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
assertThat(inputScript.getChunks().get(1).data, equalTo(dummySig.encodeToBitcoin()));
assertThat(inputScript.getChunks().get(2).data, equalTo(dummySig.encodeToBitcoin()));
assertThat(inputScript.getChunks().get(3).data, equalTo(multisigScript.getProgram()));
}