3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-07 14:54:15 +00:00

Keep opcode in ScriptChunk representation of scripts. The goal is to know how data was pushed and be able to apply malleability rules. All unit-tests pass.

This commit is contained in:
Andreas Schildbach 2014-03-08 22:15:00 +01:00 committed by Mike Hearn
parent b47995ed97
commit c236ae418f
4 changed files with 67 additions and 48 deletions

View File

@ -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.
@ -108,12 +109,12 @@ public class Script {
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("] ");
}
}
@ -192,13 +193,13 @@ public class Script {
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.
@ -272,12 +273,16 @@ public class Script {
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());
}
@ -381,8 +386,7 @@ public class Script {
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++;
@ -397,19 +401,12 @@ public class Script {
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)
@ -499,13 +496,13 @@ public class Script {
// 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.
}
@ -605,7 +602,7 @@ public class Script {
stack.add(chunk.data);
} else {
int opcode = 0xFF & chunk.data[0];
int opcode = chunk.opcode;
if (opcode > OP_16) {
opCount++;
if (opCount > 201)
@ -1254,7 +1251,7 @@ public class Script {
// 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();

View File

@ -42,20 +42,31 @@ public class 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;
}

View File

@ -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.
@ -22,38 +23,42 @@ import java.io.IOException;
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() {
@ -62,25 +67,33 @@ public class ScriptChunk {
}
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
}
}
@ -91,7 +104,7 @@ public class ScriptChunk {
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;
@ -100,7 +113,7 @@ public class ScriptChunk {
@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;

View File

@ -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);