diff --git a/core/src/main/java/com/google/bitcoin/script/Script.java b/core/src/main/java/com/google/bitcoin/script/Script.java index 8be24b817..74d6c64f3 100644 --- a/core/src/main/java/com/google/bitcoin/script/Script.java +++ b/core/src/main/java/com/google/bitcoin/script/Script.java @@ -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 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...] 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...] - // 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: return SIG_SIZE; diff --git a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java index 4d0d1392a..9a7f1793a 100644 --- a/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java +++ b/core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java @@ -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 signatures) { - return createP2SHMultiSigInputScript(signatures, null); + List sigs = new ArrayList(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 signatures, - byte[] multisigProgramBytes) { - List sigs = new ArrayList(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 signatures, + Script multisigProgram) { + List sigs = new ArrayList(); + 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()); } /** diff --git a/core/src/test/java/com/google/bitcoin/script/ScriptTest.java b/core/src/test/java/com/google/bitcoin/script/ScriptTest.java index 7334a1acb..8cb1ab4b1 100644 --- a/core/src/test/java/com/google/bitcoin/script/ScriptTest.java +++ b/core/src/test/java/com/google/bitcoin/script/ScriptTest.java @@ -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 {