Skip to content

Commit

Permalink
Keep opcode in ScriptChunk representation of scripts. The goal is to …
Browse files Browse the repository at this point in the history
…know how data was pushed and be able to apply malleability rules. All unit-tests pass.
  • Loading branch information
schildbach authored and mikehearn committed May 22, 2014
1 parent b47995e commit c236ae4
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 48 deletions.
41 changes: 19 additions & 22 deletions core/src/main/java/com/google/bitcoin/script/Script.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/**
* Copyright 2011 Google Inc.
* Copyright 2012 Matt Corallo.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -108,12 +109,12 @@ public String toString() {
StringBuilder buf = new StringBuilder();
for (ScriptChunk chunk : chunks) {
if (chunk.isOpCode()) {
buf.append(getOpCodeName(chunk.data[0]));
buf.append(getOpCodeName(chunk.opcode));
buf.append(" ");
} else {
// Data chunk
buf.append("[");
buf.append(bytesToHexString(chunk.data));
buf.append(chunk.data != null ? bytesToHexString(chunk.data) : "null");
buf.append("] ");
}
}
Expand Down Expand Up @@ -192,13 +193,13 @@ private void parse(byte[] program) throws ScriptException {

ScriptChunk chunk;
if (dataToRead == -1) {
chunk = new ScriptChunk(true, new byte[]{(byte) opcode}, startLocationInProgram);
chunk = new ScriptChunk(opcode, null, startLocationInProgram);
} else {
if (dataToRead > bis.available())
throw new ScriptException("Push of data element that is larger than remaining data");
byte[] data = new byte[(int)dataToRead];
checkState(dataToRead == 0 || bis.read(data, 0, (int)dataToRead) == dataToRead);
chunk = new ScriptChunk(false, data, startLocationInProgram);
chunk = new ScriptChunk(opcode, data, startLocationInProgram);
}
// Save some memory by eliminating redundant copies of the same chunk objects. INTERN_TABLE can be null
// here because this method is called whilst setting it up.
Expand Down Expand Up @@ -272,12 +273,16 @@ public byte[] getPubKey() throws ScriptException {
if (chunks.size() != 2) {
throw new ScriptException("Script not of right size, expecting 2 but got " + chunks.size());
}
if (chunks.get(0).data.length > 2 && chunks.get(1).data.length > 2) {
final ScriptChunk chunk0 = chunks.get(0);
final byte[] chunk0data = chunk0.data;
final ScriptChunk chunk1 = chunks.get(1);
final byte[] chunk1data = chunk1.data;
if (chunk0data != null && chunk0data.length > 2 && chunk1data != null && chunk1data.length > 2) {
// If we have two large constants assume the input to a pay-to-address output.
return chunks.get(1).data;
} else if (chunks.get(1).data.length == 1 && chunks.get(1).equalsOpCode(OP_CHECKSIG) && chunks.get(0).data.length > 2) {
return chunk1data;
} else if (chunk1.equalsOpCode(OP_CHECKSIG) && chunk0data != null && chunk0data.length > 2) {
// A large constant followed by an OP_CHECKSIG is the key.
return chunks.get(0).data;
return chunk0data;
} else {
throw new ScriptException("Script did not match expected form: " + toString());
}
Expand Down Expand Up @@ -381,8 +386,7 @@ private static int getSigOpCount(List<ScriptChunk> chunks, boolean accurate) thr
int lastOpCode = OP_INVALIDOPCODE;
for (ScriptChunk chunk : chunks) {
if (chunk.isOpCode()) {
int opcode = 0xFF & chunk.data[0];
switch (opcode) {
switch (chunk.opcode) {
case OP_CHECKSIG:
case OP_CHECKSIGVERIFY:
sigOps++;
Expand All @@ -397,19 +401,12 @@ private static int getSigOpCount(List<ScriptChunk> chunks, boolean accurate) thr
default:
break;
}
lastOpCode = opcode;
lastOpCode = chunk.opcode;
}
}
return sigOps;
}

/**
* Converts an opcode to its int representation (including OP_1NEGATE and OP_0/OP_FALSE)
* @throws IllegalArgumentException If the opcode is not an OP_N opcode
*/
public static int decodeFromOpN(byte opcode) throws IllegalArgumentException {
return decodeFromOpN((int)opcode);
}
static int decodeFromOpN(int opcode) {
checkArgument((opcode == OP_0 || opcode == OP_1NEGATE) || (opcode >= OP_1 && opcode <= OP_16), "decodeFromOpN called on non OP_N opcode");
if (opcode == OP_0)
Expand Down Expand Up @@ -499,13 +496,13 @@ public boolean isSentToMultiSig() {
// 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]);
int numKeys = decodeFromOpN(m.opcode);
if (numKeys < 1 || 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.
if (decodeFromOpN(chunks.get(0).data[0]) < 1) return false;
if (decodeFromOpN(chunks.get(0).opcode) < 1) return false;
} catch (IllegalStateException e) {
return false; // Not an OP_N opcode.
}
Expand Down Expand Up @@ -605,7 +602,7 @@ private static void executeScript(Transaction txContainingThis, long index,

stack.add(chunk.data);
} else {
int opcode = 0xFF & chunk.data[0];
int opcode = chunk.opcode;
if (opcode > OP_16) {
opCount++;
if (opCount > 201)
Expand Down Expand Up @@ -1254,7 +1251,7 @@ public void correctlySpends(Transaction txContainingThis, long scriptSigIndex, S
// TODO: Check if we can take out enforceP2SH if there's a checkpoint at the enforcement block.
if (enforceP2SH && scriptPubKey.isPayToScriptHash()) {
for (ScriptChunk chunk : chunks)
if (chunk.isOpCode() && (chunk.data[0] & 0xff) > OP_16)
if (chunk.isOpCode() && chunk.opcode > OP_16)
throw new ScriptException("Attempted to spend a P2SH scriptPubKey with a script that contained script ops");

byte[] scriptPubKeyBytes = p2shStack.pollLast();
Expand Down
17 changes: 14 additions & 3 deletions core/src/main/java/com/google/bitcoin/script/ScriptBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,31 @@ public ScriptBuilder() {
}

public ScriptBuilder op(int opcode) {
chunks.add(new ScriptChunk(true, new byte[]{(byte)opcode}));
checkArgument(opcode > OP_PUSHDATA4);
chunks.add(new ScriptChunk(opcode, null));
return this;
}

public ScriptBuilder data(byte[] data) {
byte[] copy = Arrays.copyOf(data, data.length);
chunks.add(new ScriptChunk(false, copy));
int opcode;
if (data.length < OP_PUSHDATA1) {
opcode = data.length; // OP_0 in case of empty vector
} else if (data.length < 256) {
opcode = OP_PUSHDATA1;
} else if (data.length < 65536) {
opcode = OP_PUSHDATA2;
} else {
throw new RuntimeException("Unimplemented");
}
chunks.add(new ScriptChunk(opcode, copy));
return this;
}

public ScriptBuilder smallNum(int num) {
checkArgument(num >= 0, "Cannot encode negative numbers with smallNum");
checkArgument(num <= 16, "Cannot encode numbers larger than 16 with smallNum");
chunks.add(new ScriptChunk(true, new byte[]{(byte)Script.encodeToOpN(num)}));
chunks.add(new ScriptChunk(Script.encodeToOpN(num), null));
return this;
}

Expand Down
53 changes: 33 additions & 20 deletions core/src/main/java/com/google/bitcoin/script/ScriptChunk.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/*
* Copyright 2013 Google Inc.
* Copyright 2014 Andreas Schildbach
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -22,38 +23,42 @@
import java.io.OutputStream;
import java.util.Arrays;

import javax.annotation.Nullable;

import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA1;
import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA2;
import static com.google.bitcoin.script.ScriptOpCodes.OP_PUSHDATA4;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

/**
* An element that is either an opcode or a raw byte array (signature, pubkey, etc).
*/
public class ScriptChunk {
private boolean isOpCode;
public final int opcode;
@Nullable
public final byte[] data;
private int startLocationInProgram;

public ScriptChunk(boolean isOpCode, byte[] data) {
this(isOpCode, data, -1);
public ScriptChunk(int opcode, byte[] data) {
this(opcode, data, -1);
}

public ScriptChunk(boolean isOpCode, byte[] data, int startLocationInProgram) {
this.isOpCode = isOpCode;
public ScriptChunk(int opcode, byte[] data, int startLocationInProgram) {
this.opcode = opcode;
this.data = data;
this.startLocationInProgram = startLocationInProgram;
}

public boolean equalsOpCode(int opCode) {
return isOpCode && data.length == 1 && (0xFF & data[0]) == opCode;
public boolean equalsOpCode(int opcode) {
return opcode == this.opcode;
}

/**
* If this chunk is a single byte of non-pushdata content (could be OP_RESERVED or some invalid Opcode)
*/
public boolean isOpCode() {
return isOpCode;
return opcode > OP_PUSHDATA4;
}

public int getStartLocationInProgram() {
Expand All @@ -62,25 +67,33 @@ public int getStartLocationInProgram() {
}

public void write(OutputStream stream) throws IOException {
if (isOpCode) {
checkState(data.length == 1);
stream.write(data);
} else {
checkState(data.length <= Script.MAX_SCRIPT_ELEMENT_SIZE);
if (data.length < OP_PUSHDATA1) {
stream.write(data.length);
} else if (data.length <= 0xFF) {
if (isOpCode()) {
checkState(data == null);
stream.write(opcode);
} else if (data != null) {
checkNotNull(data);
if (opcode < OP_PUSHDATA1) {
checkState(data.length == opcode);
stream.write(opcode);
} else if (opcode == OP_PUSHDATA1) {
checkState(data.length <= 0xFF);
stream.write(OP_PUSHDATA1);
stream.write(data.length);
} else if (data.length <= 0xFFFF) {
} else if (opcode == OP_PUSHDATA2) {
checkState(data.length <= 0xFFFF);
stream.write(OP_PUSHDATA2);
stream.write(0xFF & data.length);
stream.write(0xFF & (data.length >> 8));
} else {
} else if (opcode == OP_PUSHDATA4) {
checkState(data.length <= Script.MAX_SCRIPT_ELEMENT_SIZE);
stream.write(OP_PUSHDATA4);
Utils.uint32ToByteStreamLE(data.length, stream);
} else {
throw new RuntimeException("Unimplemented");
}
stream.write(data);
} else {
stream.write(opcode); // smallNum
}
}

Expand All @@ -91,7 +104,7 @@ public boolean equals(Object o) {

ScriptChunk chunk = (ScriptChunk) o;

if (isOpCode != chunk.isOpCode) return false;
if (opcode != chunk.opcode) return false;
if (startLocationInProgram != chunk.startLocationInProgram) return false;
if (!Arrays.equals(data, chunk.data)) return false;

Expand All @@ -100,7 +113,7 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
int result = (isOpCode ? 1 : 0);
int result = opcode;
result = 31 * result + (data != null ? Arrays.hashCode(data) : 0);
result = 31 * result + startLocationInProgram;
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,9 +385,7 @@ public class ScriptOpCodes {
/**
* Converts the given OpCode into a string (eg "0", "PUSHDATA", or "NON_OP(10)")
*/
public static String getOpCodeName(byte opCode) {
int opcode = opCode & 0xff;

public static String getOpCodeName(int opcode) {
if (opCodeMap.containsKey(opcode))
return opCodeMap.get(opcode);

Expand Down

0 comments on commit c236ae4

Please sign in to comment.