Add ability to create scriptSigs with empty signatures

For partial scriptSig OP_0 is used as a signature placeholder:
Pay-to-address: OP_0 [pubkey]
Pay-to-pubkey: OP_0
P2SH with 2-of-3 CHECKMULTISIG: OP_0 OP_0 OP_0 [redeem script]
This commit is contained in:
Kosta Korenkov 2014-08-05 19:18:45 +04:00 committed by Mike Hearn
parent bc97cf977f
commit 5cfdca1c98
3 changed files with 119 additions and 32 deletions

View file

@ -367,7 +367,28 @@ public class Script {
throw new RuntimeException(e);
}
}
/**
* Creates an incomplete scriptSig that, once filled with signatures, can redeem output containing this scriptPubKey.
* Instead of the signatures resulting script has OP_0.
* Having incomplete input script allows to pass around partially signed tx.
* It is expected that this program later on will be updated with proper signatures.
*/
public Script createEmptyInputScript(@Nullable ECKey key, @Nullable Script redeemScript) {
if (isSentToAddress()) {
checkArgument(key != null, "Key required to create pay-to-address input script");
return ScriptBuilder.createInputScript(null, key);
} else if (isSentToRawPubKey()) {
return ScriptBuilder.createInputScript(null);
} else if (isPayToScriptHash()) {
checkArgument(redeemScript != null, "Redeem script required to create P2SH input script");
return ScriptBuilder.createP2SHMultiSigInputScript(null, redeemScript);
} else {
throw new ScriptException("Do not understand script type: " + this);
}
}
////////////////////// Interface used during verification of transactions/blocks ////////////////////////////////
private static int getSigOpCount(List<ScriptChunk> chunks, boolean accurate) throws ScriptException {
@ -448,6 +469,24 @@ public class Script {
return 0;
}
/**
* Returns number of signatures required to satisfy this script.
*/
public int getNumberOfSignaturesRequiredToSpend() {
if (isSentToMultiSig()) {
// for N of M CHECKMULTISIG script we will need N signatures to spend
ScriptChunk nChunk = chunks.get(0);
return Script.decodeFromOpN(nChunk.opcode);
} else if (isSentToAddress() || isSentToRawPubKey()) {
// pay-to-address and pay-to-pubkey require single sig
return 1;
} else if (isPayToScriptHash()) {
throw new IllegalStateException("For P2SH number of signatures depends on redeem script");
} else {
throw new IllegalStateException("Unsupported script type");
}
}
/**
* Returns number of bytes required to spend this script. It accepts optional ECKey and redeemScript that may
* be required for certain types of script to estimate target size.
@ -456,16 +495,10 @@ public class Script {
if (isPayToScriptHash()) {
// scriptSig: <sig> [sig] [sig...] <redeemscript>
checkArgument(redeemScript != null, "P2SH script requires redeemScript to be spent");
// for N of M CHECKMULTISIG redeem script we will need N signatures to spend
ScriptChunk nChunk = redeemScript.getChunks().get(0);
int n = Script.decodeFromOpN(nChunk.opcode);
return n * SIG_SIZE + redeemScript.getProgram().length;
return redeemScript.getNumberOfSignaturesRequiredToSpend() * SIG_SIZE + redeemScript.getProgram().length;
} else if (isSentToMultiSig()) {
// scriptSig: OP_0 <sig> [sig] [sig...]
// for N of M CHECKMULTISIG script we will need N signatures to spend
ScriptChunk nChunk = chunks.get(0);
int n = Script.decodeFromOpN(nChunk.opcode);
return n * SIG_SIZE + 1;
return getNumberOfSignaturesRequiredToSpend() * SIG_SIZE + 1;
} else if (isSentToRawPubKey()) {
// scriptSig: <sig>
return SIG_SIZE;

View file

@ -111,15 +111,23 @@ public class ScriptBuilder {
return new ScriptBuilder().data(key.getPubKey()).op(OP_CHECKSIG).build();
}
/** Creates a scriptSig that can redeem a pay-to-address output. */
public static Script createInputScript(TransactionSignature signature, ECKey pubKey) {
/**
* Creates a scriptSig that can redeem a pay-to-address output.
* If given signature is null, incomplete scriptSig will be created with OP_0 instead of signature
*/
public static Script createInputScript(@Nullable TransactionSignature signature, ECKey pubKey) {
byte[] pubkeyBytes = pubKey.getPubKey();
return new ScriptBuilder().data(signature.encodeToBitcoin()).data(pubkeyBytes).build();
byte[] sigBytes = signature != null ? signature.encodeToBitcoin() : new byte[]{};
return new ScriptBuilder().data(sigBytes).data(pubkeyBytes).build();
}
/** Creates a scriptSig that can redeem a pay-to-pubkey output. */
public static Script createInputScript(TransactionSignature signature) {
return new ScriptBuilder().data(signature.encodeToBitcoin()).build();
/**
* Creates a scriptSig that can redeem a pay-to-pubkey output.
* If given signature is null, incomplete scriptSig will be created with OP_0 instead of signature
*/
public static Script createInputScript(@Nullable TransactionSignature signature) {
byte[] sigBytes = signature != null ? signature.encodeToBitcoin() : new byte[]{};
return new ScriptBuilder().data(sigBytes).build();
}
/** Creates a program that requires at least N of the given keys to sign, using OP_CHECKMULTISIG. */
@ -139,7 +147,12 @@ public class ScriptBuilder {
/** Create a program that satisfies an OP_CHECKMULTISIG program. */
public static Script createMultiSigInputScript(List<TransactionSignature> signatures) {
return createP2SHMultiSigInputScript(signatures, null);
List<byte[]> sigs = new ArrayList<byte[]>(signatures.size());
for (TransactionSignature signature : signatures) {
sigs.add(signature.encodeToBitcoin());
}
return createMultiSigInputScriptBytes(sigs, null);
}
/** Create a program that satisfies an OP_CHECKMULTISIG program. */
@ -152,13 +165,24 @@ public class ScriptBuilder {
return createMultiSigInputScriptBytes(signatures, null);
}
/** Create a program that satisfies a pay-to-script hashed OP_CHECKMULTISIG program. */
public static Script createP2SHMultiSigInputScript(List<TransactionSignature> signatures,
byte[] multisigProgramBytes) {
List<byte[]> sigs = new ArrayList<byte[]>(signatures.size());
for (TransactionSignature signature : signatures)
sigs.add(signature.encodeToBitcoin());
return createMultiSigInputScriptBytes(sigs, multisigProgramBytes);
/**
* Create a program that satisfies a pay-to-script hashed OP_CHECKMULTISIG program.
* If given signature list is null, incomplete scriptSig will be created with OP_0 instead of signatures
*/
public static Script createP2SHMultiSigInputScript(@Nullable List<TransactionSignature> signatures,
Script multisigProgram) {
List<byte[]> sigs = new ArrayList<byte[]>();
if (signatures == null) {
// create correct number of empty signatures
int numSigs = multisigProgram.getNumberOfSignaturesRequiredToSpend();
for (int i = 0; i < numSigs; i++)
sigs.add(new byte[]{});
} else {
for (TransactionSignature signature : signatures) {
sigs.add(signature.encodeToBitcoin());
}
}
return createMultiSigInputScriptBytes(sigs, multisigProgram.getProgram());
}
/**

View file

@ -24,8 +24,6 @@ import com.google.bitcoin.params.MainNetParams;
import com.google.bitcoin.params.TestNet3Params;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.Assert;
import org.junit.Test;
@ -34,14 +32,12 @@ import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.*;
import static com.google.bitcoin.core.Utils.HEX;
import static com.google.bitcoin.script.ScriptOpCodes.OP_0;
import static com.google.bitcoin.script.ScriptOpCodes.OP_INVALIDOPCODE;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.junit.Assert.*;
public class ScriptTest {
@ -131,7 +127,7 @@ public class ScriptTest {
TransactionSignature party2TransactionSignature = new TransactionSignature(party2Signature, SigHash.ALL, false);
// Create p2sh multisig input script
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(party1TransactionSignature, party2TransactionSignature), multisigScript.getProgram());
Script inputScript = ScriptBuilder.createP2SHMultiSigInputScript(ImmutableList.of(party1TransactionSignature, party2TransactionSignature), multisigScript);
// Assert that the input script contains 4 chunks
assertTrue(inputScript.getChunks().size() == 4);
@ -150,7 +146,41 @@ public class ScriptTest {
// Assert that the input script created does not end with the original
// multisig script
scriptChunk = inputScript.getChunks().get(inputScript.getChunks().size() - 1);
Assert.assertThat(scriptChunk.data, IsNot.not(IsEqual.equalTo(multisigScript.getProgram())));
Assert.assertThat(scriptChunk.data, IsNot.not(equalTo(multisigScript.getProgram())));
}
@Test
public void testCreateEmptyInputScript() throws Exception {
TransactionSignature dummySig = TransactionSignature.dummy();
ECKey key = new ECKey();
// pay-to-pubkey
Script inputScript = ScriptBuilder.createInputScript(dummySig);
assertThat(inputScript.getChunks().get(0).data, equalTo(dummySig.encodeToBitcoin()));
inputScript = ScriptBuilder.createInputScript(null);
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
// pay-to-address
inputScript = ScriptBuilder.createInputScript(dummySig, key);
assertThat(inputScript.getChunks().get(0).data, equalTo(dummySig.encodeToBitcoin()));
inputScript = ScriptBuilder.createInputScript(null, key);
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
assertThat(inputScript.getChunks().get(1).data, equalTo(key.getPubKey()));
// pay-to-script-hash
ECKey key2 = new ECKey();
Script multisigScript = ScriptBuilder.createMultiSigOutputScript(2, Arrays.asList(key, key2));
inputScript = ScriptBuilder.createP2SHMultiSigInputScript(Arrays.asList(dummySig, dummySig), multisigScript);
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()));
inputScript = ScriptBuilder.createP2SHMultiSigInputScript(null, multisigScript);
assertThat(inputScript.getChunks().get(0).opcode, equalTo(OP_0));
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()));
}
private Script parseScriptString(String string) throws Exception {