From e18b9d363ec846d4dc6ce1f54f4124acf15cf240 Mon Sep 17 00:00:00 2001
From: Mike Hearn
Date: Fri, 19 Apr 2013 13:13:48 +0200
Subject: [PATCH] Script: add support for crafting multisig outputs and hide
program behind Script.getProgram()
---
.../java/com/google/bitcoin/core/Script.java | 93 ++++++++++++++++---
.../com/google/bitcoin/core/Transaction.java | 8 ++
.../bitcoin/core/FullBlockTestGenerator.java | 2 +-
.../com/google/bitcoin/core/ScriptTest.java | 22 +++++
.../com/google/bitcoin/core/WalletTest.java | 2 +-
5 files changed, 114 insertions(+), 13 deletions(-)
diff --git a/core/src/main/java/com/google/bitcoin/core/Script.java b/core/src/main/java/com/google/bitcoin/core/Script.java
index bbdd10db..29ad4050 100644
--- a/core/src/main/java/com/google/bitcoin/core/Script.java
+++ b/core/src/main/java/com/google/bitcoin/core/Script.java
@@ -17,6 +17,8 @@
package com.google.bitcoin.core;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.digests.RIPEMD160Digest;
import java.io.ByteArrayOutputStream;
@@ -28,6 +30,9 @@ import java.security.NoSuchAlgorithmException;
import java.util.*;
import static com.google.bitcoin.core.Utils.bytesToHexString;
+import static com.google.common.base.Preconditions.checkArgument;
+
+// TODO: Make this class a superclass with derived classes giving accessor methods for the various common templates.
/**
* A chunk in a script
@@ -60,6 +65,8 @@ class ScriptChunk {
* static methods for building scripts.
*/
public class Script {
+ private static final Logger log = LoggerFactory.getLogger(Script.class);
+
// Some constants used for decoding the scripts, copied from the reference client
// push value
public static final int OP_0 = 0x00;
@@ -195,7 +202,7 @@ public class Script {
public static final int OP_INVALIDOPCODE = 0xff;
- byte[] program;
+ private byte[] program;
private int cursor;
// The program is a set of byte[]s where each element is either [opcode] or [data, data, data ...]
@@ -231,6 +238,11 @@ public class Script {
}
return buf.toString();
}
+
+ /** Returns the serialized program as a newly created byte array. */
+ public byte[] getProgram() {
+ return Arrays.copyOf(program, program.length);
+ }
/**
* Converts the given OpCode into a string (eg "0", "PUSHDATA", or "NON_OP(10)")
@@ -679,6 +691,28 @@ public class Script {
return createOutputScript(pubkey.getPubKey());
}
+ /** Creates a program that requires at least N of the given keys to sign, using OP_CHECKMULTISIG. */
+ public static byte[] createMultiSigOutputScript(int threshold, List pubkeys) {
+ checkArgument(threshold > 0);
+ checkArgument(threshold <= pubkeys.size());
+ checkArgument(pubkeys.size() <= 16); // That's the max we can represent with a single opcode.
+ if (pubkeys.size() > 3) {
+ log.warn("Creating a multi-signature output that is non-standard: {} pubkeys, should be <= 3", pubkeys.size());
+ }
+ try {
+ ByteArrayOutputStream bits = new ByteArrayOutputStream();
+ bits.write(encodeToOpN(threshold));
+ for (ECKey key : pubkeys) {
+ writeBytes(bits, key.getPubKey());
+ }
+ bits.write(encodeToOpN(pubkeys.size()));
+ bits.write(OP_CHECKMULTISIG);
+ return bits.toByteArray();
+ } catch (IOException e) {
+ throw new RuntimeException(e); // Cannot happen.
+ }
+ }
+
public static byte[] createInputScript(byte[] signature, byte[] pubkey) {
try {
// TODO: Do this by creating a Script *first* then having the script reassemble itself into bytes.
@@ -718,7 +752,7 @@ public class Script {
case OP_CHECKMULTISIG:
case OP_CHECKMULTISIGVERIFY:
if (accurate && lastOpCode >= OP_1 && lastOpCode <= OP_16)
- sigOps += getOpNValue(lastOpCode);
+ sigOps += decodeFromOpN(lastOpCode);
else
sigOps += 20;
break;
@@ -731,15 +765,26 @@ public class Script {
return sigOps;
}
- /**
- * Convince method to get the int value of OP_N
- */
- private static int getOpNValue(int opcode) throws ScriptException {
+ private static int decodeFromOpN(byte opcode) {
+ return decodeFromOpN(0xFF & opcode);
+ }
+ private static int decodeFromOpN(int opcode) {
+ checkArgument(opcode >= 0 && opcode <= OP_16, "decodeFromOpN called on non OP_N opcode");
if (opcode == OP_0)
return 0;
- if (opcode < OP_1 || opcode > OP_16) // This should absolutely never happen
- throw new ScriptException("getOpNValue called on non OP_N opcode");
- return opcode + 1 - OP_1;
+ else
+ return opcode + 1 - OP_1;
+ }
+
+ private static int encodeToOpN(byte value) {
+ return encodeToOpN(0xFF & value);
+ }
+ private static int encodeToOpN(int value) {
+ checkArgument(value >= 0 && value <= 16, "encodeToOpN called for a value we cannot encode in an opcode.");
+ if (value == 0)
+ return OP_0;
+ else
+ return value - 1 + OP_1;
}
/**
@@ -793,7 +838,33 @@ public class Script {
(program[1] & 0xff) == 0x14 &&
(program[22] & 0xff) == OP_EQUAL;
}
-
+
+ /**
+ * Returns whether this script matches the format used for multisig outputs: [n] [keys...] [m] CHECKMULTISIG
+ */
+ public boolean isSentToMultiSig() {
+ if (chunks.size() < 4) return false;
+ ScriptChunk chunk = chunks.get(chunks.size() - 1);
+ // Must end in OP_CHECKMULTISIG[VERIFY].
+ if (!chunk.isOpCode) return false;
+ if (!(chunk.equalsOpCode(OP_CHECKMULTISIG) || chunk.equalsOpCode(OP_CHECKMULTISIGVERIFY))) return false;
+ try {
+ // Second to last chunk must be an OP_N opcode and there should be that many data chunks (keys).
+ ScriptChunk m = chunks.get(chunks.size() - 2);
+ if (!m.isOpCode) return false;
+ int numKeys = decodeFromOpN(m.data[0]);
+ if (chunks.size() != 3 + numKeys) return false;
+ for (int i = 1; i < chunks.size() - 2; i++) {
+ if (chunks.get(i).isOpCode) return false;
+ }
+ // First chunk must be an OP_N opcode too.
+ decodeFromOpN(chunks.get(0).data[0]);
+ } catch (IllegalStateException e) {
+ return false; // Not an OP_N opcode.
+ }
+ return true;
+ }
+
private static boolean equalsRange(byte[] a, int start, byte[] b) {
if (start + b.length > a.length)
return false;
@@ -958,7 +1029,7 @@ public class Script {
case OP_14:
case OP_15:
case OP_16:
- stack.add(Utils.reverseBytes(Utils.encodeMPI(BigInteger.valueOf(getOpNValue(opcode)), false)));
+ stack.add(Utils.reverseBytes(Utils.encodeMPI(BigInteger.valueOf(decodeFromOpN(opcode)), false)));
break;
case OP_NOP:
break;
diff --git a/core/src/main/java/com/google/bitcoin/core/Transaction.java b/core/src/main/java/com/google/bitcoin/core/Transaction.java
index 2742dc6f..1d94bdf7 100644
--- a/core/src/main/java/com/google/bitcoin/core/Transaction.java
+++ b/core/src/main/java/com/google/bitcoin/core/Transaction.java
@@ -689,6 +689,14 @@ public class Transaction extends ChildMessage implements Serializable {
addOutput(new TransactionOutput(params, this, value, pubkey));
}
+ /**
+ * Creates an output that pays to the given script. The address and key forms are specialisations of this method,
+ * you won't normally need to use it unless you're doing unusual things.
+ */
+ public void addOutput(BigInteger value, Script script) {
+ 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,
diff --git a/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java b/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java
index 89dc0312..857d93bd 100644
--- a/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java
+++ b/core/src/test/java/com/google/bitcoin/core/FullBlockTestGenerator.java
@@ -1289,7 +1289,7 @@ public class FullBlockTestGenerator {
input.setSequenceNumber(sequence);
t.addInput(input);
- byte[] connectedPubKeyScript = prevOut.scriptPubKey.program;
+ byte[] connectedPubKeyScript = prevOut.scriptPubKey.getProgram();
Sha256Hash hash = t.hashTransactionForSignature(0, connectedPubKeyScript, SigHash.ALL, false);
// Sign input
diff --git a/core/src/test/java/com/google/bitcoin/core/ScriptTest.java b/core/src/test/java/com/google/bitcoin/core/ScriptTest.java
index 0937361c..7d982cde 100644
--- a/core/src/test/java/com/google/bitcoin/core/ScriptTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/ScriptTest.java
@@ -16,6 +16,7 @@
package com.google.bitcoin.core;
+import com.google.common.collect.Lists;
import org.junit.Test;
import org.spongycastle.util.encoders.Hex;
@@ -59,6 +60,27 @@ public class ScriptTest {
assertEquals("mkFQohBpy2HDXrCwyMrYL5RtfrmeiuuPY2", toAddr.toString());
}
+ @Test
+ public void testMultiSig() throws Exception {
+ List keys = Lists.newArrayList(new ECKey(), new ECKey(), new ECKey());
+ assertTrue(new Script(Script.createMultiSigOutputScript(2, keys)).isSentToMultiSig());
+ assertTrue(new Script(Script.createMultiSigOutputScript(3, keys)).isSentToMultiSig());
+ assertFalse(new Script(Script.createOutputScript(new ECKey())).isSentToMultiSig());
+ try {
+ // Fail if we ask for more signatures than keys.
+ Script.createMultiSigOutputScript(4, keys);
+ fail();
+ } catch (Throwable e) {
+ // Expected.
+ }
+ try {
+ // Must have at least one signature required.
+ Script.createMultiSigOutputScript(0, keys);
+ } catch (Throwable e) {
+ // Expected.
+ }
+ }
+
@Test
public void testIp() throws Exception {
byte[] bytes = Hex.decode("41043e96222332ea7848323c08116dddafbfa917b8e37f0bdf63841628267148588a09a43540942d58d49717ad3fabfe14978cf4f0a8b84d2435dad16e9aa4d7f935ac");
diff --git a/core/src/test/java/com/google/bitcoin/core/WalletTest.java b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
index bc0eca9c..5b1ec0ce 100644
--- a/core/src/test/java/com/google/bitcoin/core/WalletTest.java
+++ b/core/src/test/java/com/google/bitcoin/core/WalletTest.java
@@ -710,7 +710,7 @@ public class WalletTest extends TestWithWallet {
@Override
public void onTransactionConfidenceChanged(Wallet wallet, Transaction tx) {
super.onTransactionConfidenceChanged(wallet, tx);
- if (tx.getConfidence().getConfidenceType() ==
+ if (tx.getConfidence().getConfidenceType() ==
TransactionConfidence.ConfidenceType.DEAD) {
called[0] = tx;
called[1] = tx.getConfidence().getOverridingTransaction();