package org.ripple.power.txns.btc; import java.io.ByteArrayOutputStream; import java.io.EOFException; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Arrays; /** * A script is a small program contained in the transaction which determines whether or not * an output can be spent. The first half of the script is provided by the transaction input * and the second half of the script is provided by the transaction output. */ public class Script { /** * Checks that the script consists of only canonical push-data operations. * * For canonical scripts, each push-data operation must use the shortest opcode possible. * Numeric values between 0 and 16 must use OP_n opcodes. * * @param scriptBytes Script bytes * @return TRUE if only canonical push-data operations were found * @throws EOFException Script is too short */ public static boolean checkInputScript(byte[] scriptBytes) throws EOFException { boolean scriptValid = true; int offset = 0; int length = scriptBytes.length; while (scriptValid && offset < length) { int opcode = ((int)scriptBytes[offset++])&0xff; if (opcode <= ScriptOpCodes.OP_PUSHDATA4) { int[] result = getDataLength(opcode, scriptBytes, offset); int dataLength = result[0]; offset = result[1]; if (dataLength == 1) { if (opcode != 1 || ((int)scriptBytes[offset]&0xff) <= 16) scriptValid = false; } else if (dataLength < 76) { if (opcode >= ScriptOpCodes.OP_PUSHDATA1) scriptValid = false; } else if (dataLength < 256) { if (opcode != ScriptOpCodes.OP_PUSHDATA1) scriptValid = false; } else if (dataLength < 65536) { if (opcode != ScriptOpCodes.OP_PUSHDATA2) scriptValid = false; } offset += dataLength; if (offset > length) throw new EOFException("End-of-data while processing script"); } else if (opcode > ScriptOpCodes.OP_16) { scriptValid = false; } } return scriptValid; } /** * Get the input data elements * * @param scriptBytes Script bytes * @return Data element list * @throws EOFException Script is too short */ public static List<byte[]> getData(byte[] scriptBytes) throws EOFException { List<byte[]> dataList = new ArrayList<>(); int offset = 0; int length = scriptBytes.length; while (offset<length) { int dataLength; int opcode = ((int)scriptBytes[offset++])&0xff; if (opcode <= ScriptOpCodes.OP_PUSHDATA4) { int[] result = getDataLength(opcode, scriptBytes, offset); dataLength = result[0]; offset = result[1]; if (dataLength > 0) { if (offset+dataLength > length) throw new EOFException("End-of-data while processing script"); dataList.add(Arrays.copyOfRange(scriptBytes, offset, offset+dataLength)); offset += dataLength; } } } return dataList; } /** * Checks script data elements against a Bloom filter * * @param filter Bloom filter * @param scriptBytes Script to check * @return TRUE if a data element in the script matched the filter */ public static boolean checkFilter(BloomFilter filter, byte[] scriptBytes) { boolean foundMatch = false; int offset = 0; int length = scriptBytes.length; // // Check each data element in the script // try { while (offset<length && !foundMatch) { int dataLength; int opcode = ((int)scriptBytes[offset++])&0xff; if (opcode <= ScriptOpCodes.OP_PUSHDATA4) { // // Get the data element // int[] result = getDataLength(opcode, scriptBytes, offset); dataLength = result[0]; offset = result[1]; if (dataLength > 0) { if (offset+dataLength > length) throw new EOFException("End-of-data while processing script"); foundMatch = filter.contains(scriptBytes, offset, dataLength); offset += dataLength; } } } } catch (EOFException exc) { // Invalid script - stop checking } return foundMatch; } /** * Returns the payment type for an output script * * @param scriptBytes Script to check * @return Payment type or 0 if not a standard payment type */ public static int getPaymentType(byte[] scriptBytes) { int paymentType = 0; if (scriptBytes.length > 0) { if (scriptBytes[0] == (byte)ScriptOpCodes.OP_RETURN) { // // Scripts starting with OP_RETURN are unspendable // paymentType = ScriptOpCodes.PAY_TO_NOBODY; } else if (scriptBytes[0] == (byte)ScriptOpCodes.OP_DUP) { // // Check PAY_TO_PUBKEY_HASH // OP_DUP OP_HASH160 <20-byte hash> OP_EQUALVERIFY OP_CHECKSIG // if (scriptBytes.length == 25 && scriptBytes[1] == (byte)ScriptOpCodes.OP_HASH160 && scriptBytes[2] == 20 && scriptBytes[23] == (byte)ScriptOpCodes.OP_EQUALVERIFY && scriptBytes[24] == (byte)ScriptOpCodes.OP_CHECKSIG) paymentType = ScriptOpCodes.PAY_TO_PUBKEY_HASH; } else if (((int)scriptBytes[0]&0xff) <= 65) { // // Check PAY_TO_PUBKEY // <pubkey> OP_CHECKSIG // int length = (int)scriptBytes[0]; if (scriptBytes.length == length+2 && scriptBytes[length+1] == (byte)ScriptOpCodes.OP_CHECKSIG) paymentType = ScriptOpCodes.PAY_TO_PUBKEY; } else if (scriptBytes[0] == (byte)ScriptOpCodes.OP_HASH160) { // // Check PAY_TO_SCRIPT_HASH // OP_HASH160 <20-byte hash> OP_EQUAL // if (scriptBytes.length == 23 && scriptBytes[1] == 20 && scriptBytes[22] == (byte)ScriptOpCodes.OP_EQUAL) paymentType = ScriptOpCodes.PAY_TO_SCRIPT_HASH; } else if (((int)scriptBytes[0]&0xff) >= 81 && ((int)scriptBytes[0]&0xff) <= 96) { // // Check PAY_TO_MULTISIG // <m> <pubkey> <pubkey> ... <n> OP_CHECKMULTISIG // int offset = 1; while (offset < scriptBytes.length) { int opcode = (int)scriptBytes[offset]&0xff; if (opcode <= 65) { // // We have another pubkey - step over it // offset += opcode+1; continue; } if (opcode >= 81 && opcode <= 96) { // // We have found <n> // if (scriptBytes.length == offset+2 && scriptBytes[offset+1] == (byte)ScriptOpCodes.OP_CHECKMULTISIG) paymentType = ScriptOpCodes.PAY_TO_MULTISIG; } break; } } } return paymentType; } /** * Get the length of the next data element * * @param opcode Current opcode * @param scriptBytes Script program * @param startOffset Offset to byte following the opcode * @return Array containing the data length and the offset to the data * @throws EOFException Script is too short */ public static int[] getDataLength(int opcode, byte[] scriptBytes, int startOffset) throws EOFException { int[] result = new int[2]; int offset = startOffset; int dataToRead; if (opcode < ScriptOpCodes.OP_PUSHDATA1) { // These opcodes push data with a length equal to the opcode dataToRead = opcode; } else if (opcode == ScriptOpCodes.OP_PUSHDATA1) { // The data length is in the next byte if (offset > scriptBytes.length-1) throw new EOFException("End-of-data while processing script"); dataToRead = (int)scriptBytes[offset]&0xff; offset++; } else if (opcode == ScriptOpCodes.OP_PUSHDATA2) { // The data length is in the next two bytes if (offset > scriptBytes.length-2) throw new EOFException("End-of-data while processing script"); dataToRead = ((int)scriptBytes[offset]&0xff) | (((int)scriptBytes[offset+1]&0xff)<<8); offset += 2; } else if (opcode == ScriptOpCodes.OP_PUSHDATA4) { // The data length is in the next four bytes if (offset > scriptBytes.length-4) throw new EOFException("End-of-data while processing script"); dataToRead = ((int)scriptBytes[offset]&0xff) | (((int)scriptBytes[offset+1]&0xff)<<8) | (((int)scriptBytes[offset+2]&0xff)<<16) | (((int)scriptBytes[offset+3]&0xff)<<24); offset += 4; } else { dataToRead = 0; } result[0] = dataToRead; result[1] = offset; return result; } /** * Remove all instances of a data element from a script * * @param dataBytes The bytes to be removed * @param scriptBytes The script bytes * @param lastSeparator Last code separator * @return Script subprogram with data element removed */ public static byte[] removeDataElement(byte[] dataBytes, byte[] scriptBytes, int lastSeparator) { byte[] subProgram; try { try (ByteArrayOutputStream outStream = new ByteArrayOutputStream(scriptBytes.length)) { int index = lastSeparator; int count = scriptBytes.length; while (index < count) { int startPos = index; int dataLength = 0; int opcode = ((int)scriptBytes[index++])&0x00ff; if (opcode <= ScriptOpCodes.OP_PUSHDATA4) { int result[] = Script.getDataLength(opcode, scriptBytes, index); dataLength = result[0]; index = result[1]; } boolean copyElement = true; if (dataLength == dataBytes.length) { copyElement = false; for (int i=0; i<dataLength; i++) { if (dataBytes[i] != scriptBytes[index+i]) { copyElement = true; break; } } } if (copyElement) outStream.write(scriptBytes, startPos, index-startPos+dataLength); index += dataLength; } subProgram = outStream.toByteArray(); } } catch (IOException exc) { throw new RuntimeException("Unexpected I/O error from ByteArrayOutputStream", exc); } return subProgram; } }