package io.emax.cosigner.bitcoin.bitcoindrpc;
import io.emax.cosigner.bitcoin.BitcoinResource;
import io.emax.cosigner.bitcoin.common.BitcoinTools;
import io.emax.cosigner.common.ByteUtilities;
import org.slf4j.LoggerFactory;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility class to convert between a raw transaction and the data structure represented here.
*
* @author dorgky
*/
public final class RawTransaction {
private static final org.slf4j.Logger LOGGER = LoggerFactory.getLogger(RawTransaction.class);
private int version;
private long inputCount = 0;
private List<RawInput> inputs = new LinkedList<>();
private long outputCount = 0;
private List<RawOutput> outputs = new LinkedList<>();
private long lockTime;
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public long getInputCount() {
return inputCount;
}
public void setInputCount(long inputCount) {
this.inputCount = inputCount;
}
public List<RawInput> getInputs() {
return inputs;
}
public void setInputs(List<RawInput> inputs) {
this.inputs = new LinkedList<>();
this.inputs.addAll(inputs);
}
public long getOutputCount() {
return outputCount;
}
public void setOutputCount(long outputCount) {
this.outputCount = outputCount;
}
public List<RawOutput> getOutputs() {
return outputs;
}
public void setOutputs(List<RawOutput> outputs) {
this.outputs = new LinkedList<>();
this.outputs.addAll(outputs);
}
public long getLockTime() {
return lockTime;
}
public void setLockTime(long lockTime) {
this.lockTime = lockTime;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (inputCount ^ (inputCount >>> 32));
result = prime * result + ((inputs == null) ? 0 : inputs.hashCode());
result = prime * result + (int) (lockTime ^ (lockTime >>> 32));
result = prime * result + (int) (outputCount ^ (outputCount >>> 32));
result = prime * result + ((outputs == null) ? 0 : outputs.hashCode());
result = prime * result + version;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RawTransaction other = (RawTransaction) obj;
if (inputCount != other.inputCount) {
return false;
}
if (inputs == null) {
if (other.inputs != null) {
return false;
}
} else if (!inputs.equals(other.inputs)) {
return false;
}
if (lockTime != other.lockTime) {
return false;
}
if (outputCount != other.outputCount) {
return false;
}
if (outputs == null) {
if (other.outputs != null) {
return false;
}
} else if (!outputs.equals(other.outputs)) {
return false;
}
return version == other.version;
}
@Override
public String toString() {
return "RawTransaction [version=" + version + ", inputCount=" + inputCount + ", inputs="
+ inputs + ", outputCount=" + outputCount + ", outputs=" + outputs + ", lockTime="
+ lockTime + "]";
}
/**
* Returns a String representing the raw transaction.
*
* @return Hex string representing the raw transaction.
*/
public String encode() {
StringBuilder tx = new StringBuilder();
// Version
byte[] versionBytes =
ByteUtilities.stripLeadingNullBytes(BigInteger.valueOf(getVersion()).toByteArray());
versionBytes = ByteUtilities.leftPad(versionBytes, 4, (byte) 0x00);
versionBytes = ByteUtilities.flipEndian(versionBytes);
tx.append(ByteUtilities.toHexString(versionBytes));
// Number of inputs
setInputCount(getInputs().size());
byte[] inputSizeBytes = writeVariableInt(getInputCount());
tx.append(ByteUtilities.toHexString(inputSizeBytes));
// Inputs
for (int i = 0; i < getInputCount(); i++) {
tx.append(getInputs().get(i).encode());
}
// Number of outputs
setOutputCount(getOutputs().size());
byte[] outputSizeBytes = writeVariableInt(getOutputCount());
tx.append(ByteUtilities.toHexString(outputSizeBytes));
// Outputs
for (int i = 0; i < getOutputCount(); i++) {
tx.append(getOutputs().get(i).encode());
}
// Lock Time
byte[] lockBytes =
ByteUtilities.stripLeadingNullBytes(BigInteger.valueOf(getLockTime()).toByteArray());
lockBytes = ByteUtilities.leftPad(lockBytes, 4, (byte) 0x00);
lockBytes = ByteUtilities.flipEndian(lockBytes);
tx.append(ByteUtilities.toHexString(lockBytes));
return tx.toString();
}
/**
* Decode a raw trasaction.
*
* @param txData Hex string representing the transaction.
* @return Corresponding RawTransaction object.
*/
public static RawTransaction parse(String txData) {
RawTransaction tx = new RawTransaction();
byte[] rawTx = ByteUtilities.toByteArray(txData);
int buffPointer = 0;
// Version
byte[] version = ByteUtilities.readBytes(rawTx, buffPointer, 4);
buffPointer += 4;
version = ByteUtilities.flipEndian(version);
tx.setVersion(new BigInteger(1, version).intValue());
// Number of inputs
VariableInt varInputCount = readVariableInt(rawTx, buffPointer);
buffPointer += varInputCount != null ? varInputCount.getSize() : 0;
tx.setInputCount(varInputCount != null ? varInputCount.getValue() : 0);
// Parse inputs
for (long i = 0; i < tx.getInputCount(); i++) {
byte[] inputData = Arrays.copyOfRange(rawTx, buffPointer, rawTx.length);
RawInput input = RawInput.parse(ByteUtilities.toHexString(inputData));
buffPointer += input.getDataSize();
tx.getInputs().add(input);
}
// Get the number of outputs
VariableInt varOutputCount = readVariableInt(rawTx, buffPointer);
buffPointer += varOutputCount != null ? varOutputCount.getSize() : 0;
tx.setOutputCount(varOutputCount != null ? varOutputCount.getValue() : 0);
// Parse outputs
for (long i = 0; i < tx.getOutputCount(); i++) {
byte[] outputData = Arrays.copyOfRange(rawTx, buffPointer, rawTx.length);
RawOutput output = RawOutput.parse(ByteUtilities.toHexString(outputData));
buffPointer += output.getDataSize();
tx.getOutputs().add(output);
}
// Parse lock time
byte[] lockBytes = ByteUtilities.readBytes(rawTx, buffPointer, 4);
//buffPointer += 4;
lockBytes = ByteUtilities.flipEndian(lockBytes);
tx.setLockTime(new BigInteger(1, lockBytes).longValue());
return tx;
}
public static class VariableInt {
int size;
long value;
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public long getValue() {
return value;
}
public void setValue(long value) {
this.value = value;
}
}
/**
* Parse a byte array of data to extract a variable length integer.
*
* @param data Byte array that contains the integer.
* @param start Position of integer in the byte array.
* @return Information about the integer
*/
public static VariableInt readVariableInt(byte[] data, int start) {
int checkSize = 0xFF & data[start];
VariableInt varInt = new VariableInt();
varInt.setSize(0);
if (checkSize < 0xFD) {
varInt.setSize(1);
varInt.setValue(checkSize);
return varInt;
}
if (checkSize == 0xFD) {
varInt.setSize(3);
} else if (checkSize == 0xFE) {
varInt.setSize(5);
} else if (checkSize == 0xFF) {
varInt.setSize(9);
}
if (varInt.getSize() == 0) {
return null;
}
byte[] newData = ByteUtilities.readBytes(data, start + 1, varInt.getSize() - 1);
newData = ByteUtilities.flipEndian(newData);
varInt.setValue(new BigInteger(1, newData).longValue());
return varInt;
}
/**
* Similar to variable integers, this reads a variable integer that is being pushed on the stack
* in a signature script.
*
* @param data Byte array containing the data to be pushed.
* @param start Position of the integer.
* @return Information about the value and size of the integer.
*/
public static VariableInt readVariableStackInt(byte[] data, int start) {
int checkSize = 0xFF & data[start];
VariableInt varInt = new VariableInt();
varInt.setSize(0);
if (checkSize < 0x4C) {
varInt.setSize(1);
varInt.setValue(checkSize);
return varInt;
}
if (checkSize == 0x4C) {
varInt.setSize(2);
} else if (checkSize == 0x4D) {
varInt.setSize(3);
} else if (checkSize == 0x4E) {
varInt.setSize(5);
} else {
// Just process the byte and advance
varInt.setSize(1);
varInt.setValue(0);
return varInt;
}
if (varInt.getSize() == 0) {
return null;
}
byte[] newData = ByteUtilities.readBytes(data, start + 1, varInt.getSize() - 1);
newData = ByteUtilities.flipEndian(newData);
varInt.setValue(new BigInteger(1, newData).longValue());
return varInt;
}
/**
* Similar to variable integers, this reads a variable integer that is being pushed on the stack
* in a redeem script.
*
* @param data Byte array containing the data to be pushed.
* @param start Position of the integer.
* @return Information about the value and size of the integer, -1 if not a push OP.
*/
public static VariableInt readOpCodeInt(byte[] data, int start) {
int checkSize = 0xFF & data[start];
VariableInt varInt = new VariableInt();
varInt.setSize(0);
if (checkSize == 0x00) {
varInt.setSize(1);
varInt.setValue(checkSize);
return varInt;
}
if (checkSize >= 0x51 && checkSize <= 0x60) {
varInt.setSize(1);
varInt.setValue(checkSize - 0x50);
return varInt;
}
if (checkSize == 0x4C) {
varInt.setSize(2);
} else if (checkSize == 0x4D) {
varInt.setSize(3);
} else if (checkSize == 0x4E) {
varInt.setSize(5);
} else {
// Just process the byte and advance
varInt.setSize(1);
varInt.setValue(checkSize);
return varInt;
}
if (varInt.getSize() == 0) {
return null;
}
byte[] newData = ByteUtilities.readBytes(data, start + 1, varInt.getSize() - 1);
newData = ByteUtilities.flipEndian(newData);
varInt.setValue(new BigInteger(1, newData).longValue());
return varInt;
}
/**
* Encode an integer into a byte array with variable length encoding.
*
* @param data Integer data to encode.
* @return Byte array representation.
*/
public static byte[] writeVariableInt(long data) {
byte[] newData;
if (data < 0x00FD) {
newData = new byte[1];
newData[0] = (byte) (data & 0xFF);
} else if (data <= 0xFFFF) {
newData = new byte[3];
newData[0] = (byte) 0xFD;
} else if (data <= 4294967295L /* 0xFFFFFFFF */) {
newData = new byte[5];
newData[0] = (byte) 0xFE;
} else {
newData = new byte[9];
newData[0] = (byte) 0xFF;
}
byte[] intData = BigInteger.valueOf(data).toByteArray();
intData = ByteUtilities.stripLeadingNullBytes(intData);
intData = ByteUtilities.leftPad(intData, newData.length - 1, (byte) 0x00);
intData = ByteUtilities.flipEndian(intData);
System.arraycopy(intData, 0, newData, 1, newData.length - 1);
return newData;
}
/**
* Encode a stack push integer into a byte array with variable length encoding.
*
* @param data Integer data to encode.
* @return Byte array representation.
*/
public static byte[] writeVariableStackInt(long data) {
byte[] newData;
if (data < 0x4C) {
newData = new byte[1];
newData[0] = (byte) (data & 0xFF);
} else if (data <= 0xFF) {
newData = new byte[2];
newData[0] = (byte) 0x4C;
} else if (data <= 0xFFFF) {
newData = new byte[3];
newData[0] = (byte) 0x4D;
} else {
newData = new byte[5];
newData[0] = (byte) 0x4E;
}
byte[] intData = BigInteger.valueOf(data).toByteArray();
intData = ByteUtilities.stripLeadingNullBytes(intData);
intData = ByteUtilities.leftPad(intData, newData.length - 1, (byte) 0x00);
intData = ByteUtilities.flipEndian(intData);
System.arraycopy(intData, 0, newData, 1, newData.length - 1);
return newData;
}
/**
* Creates a copy of the current raw transaction.
*
* @return A copy of the current object.
*/
public RawTransaction copy() {
RawTransaction rawTx = new RawTransaction();
rawTx.setVersion(getVersion());
rawTx.setInputCount(getInputCount());
for (RawInput input : getInputs()) {
rawTx.getInputs().add(input.copy());
}
rawTx.setOutputCount(getOutputCount());
for (RawOutput output : getOutputs()) {
rawTx.getOutputs().add(output.copy());
}
rawTx.setLockTime(getLockTime());
return rawTx;
}
/**
* Strips the inputs of their scripts, preparing them for signing.
*
* @param tx Transaction to prepare for signing.
* @return Transaction with no script attached to its inputs.
*/
public static RawTransaction stripInputScripts(RawTransaction tx) {
RawTransaction rawTx = tx.copy();
for (RawInput input : rawTx.getInputs()) {
input.setScript("");
input.setScriptSize(0);
}
return rawTx;
}
/**
* Parses non-standard signature scripts for signing.
*
* @param originalScript The script provided in the original output.
* @return The altered script which will be used in signing.
*/
public static String prepareSigScript(String originalScript) {
String modifiedScript = "";
int scriptPosition;
int scriptSectionStart = 0;
boolean foundCheckSig = false;
byte[] scriptBytes = ByteUtilities.toByteArray(originalScript);
for (scriptPosition = 0; scriptPosition < scriptBytes.length; scriptPosition++) {
// Look for CHECKSIGs
if ((scriptBytes[scriptPosition] & 0xFF) >= 0xAC
&& (scriptBytes[scriptPosition] & 0xFF) <= 0xAF && !foundCheckSig) {
// Found one, backtrack to find the 0xAB, set it as the start position.
foundCheckSig = true;
for (int i = (scriptPosition - 1); i >= 0; i--) {
if ((scriptBytes[scriptPosition] & 0xFF) == 0xAB) {
scriptSectionStart = i + 1; // Get the one after the CODESEP, 0 if we don't find one.
break;
}
}
} else {
// Check if the script contains stack arguments, skip them.
if ((scriptBytes[scriptPosition] & 0xFF) >= 0x01
&& (scriptBytes[scriptPosition] & 0xFF) <= 0x4B) {
// This byte is the size
scriptPosition += scriptBytes[scriptPosition] & 0xFF;
} else if ((scriptBytes[scriptPosition] & 0xFF) == 0x4C) {
// Next byte is the size
scriptPosition++;
scriptPosition += scriptBytes[scriptPosition] & 0xFF;
} else if ((scriptBytes[scriptPosition] & 0xFF) == 0x4D) {
// Next 2 bytes are the size
scriptPosition++;
byte[] sizeBytes = ByteUtilities.readBytes(scriptBytes, scriptPosition, 2);
sizeBytes = ByteUtilities.flipEndian(sizeBytes);
int size = new BigInteger(1, sizeBytes).intValue();
scriptPosition++;
scriptPosition += size;
} else if ((scriptBytes[scriptPosition] & 0xFF) == 0x4E) {
// Next 4 bytes are the size
scriptPosition++;
byte[] sizeBytes = ByteUtilities.readBytes(scriptBytes, scriptPosition, 4);
sizeBytes = ByteUtilities.flipEndian(sizeBytes);
int size = new BigInteger(1, sizeBytes).intValue();
scriptPosition += 3;
scriptPosition += size;
} else if ((scriptBytes[scriptPosition] & 0xFF) == 0xAB) {
// If the CHECKSIG was found and we find any 0xAB's, remove them.
if (scriptSectionStart <= scriptPosition) {
// If start > position then we got two 0xAB's
// in a row, skip the copy
byte[] copyArray =
Arrays.copyOfRange(scriptBytes, scriptSectionStart, scriptPosition - 1);
modifiedScript += ByteUtilities.toHexString(copyArray);
}
scriptSectionStart = scriptPosition + 1;
}
}
}
return modifiedScript;
}
/**
* Assuming standard scripts, return the address.
*
* @param script Standard pubkey script to be decoded.
* @return The address that the redeem script corresponds to.
*/
public static String decodePubKeyScript(String script) {
// Regular address
Pattern pattern = Pattern.compile("^76a914(.{40})88ac$");
Matcher matcher = pattern.matcher(script);
if (matcher.matches()) {
String addressBytes = matcher.group(1);
String networkBytes =
BitcoinResource.getResource().getBitcoindRpc().getblockchaininfo().getChain()
== BlockChainName.main ? NetworkBytes.P2PKH.toString() :
NetworkBytes.P2PKH_TEST.toString();
return BitcoinTools.encodeAddress(addressBytes, networkBytes);
}
pattern = Pattern.compile("^a914(.{40})87$");
matcher = pattern.matcher(script);
if (matcher.matches()) {
String addressBytes = matcher.group(1);
String networkBytes =
BitcoinResource.getResource().getBitcoindRpc().getblockchaininfo().getChain()
== BlockChainName.main ? NetworkBytes.P2SH.toString() :
NetworkBytes.P2SH_TEST.toString();
return BitcoinTools.encodeAddress(addressBytes, networkBytes);
}
return null;
}
/**
* Assuming standard scripts, decodes a multi-sig redeem script into its public keys.
*/
public static Iterable<String> decodeRedeemScript(String script) {
byte[] scriptBytes = ByteUtilities.toByteArray(script);
int buffPointer = 0;
LinkedList<String> stack = new LinkedList<>();
LinkedList<String> publicKeys = new LinkedList<>();
VariableInt varInt;
try {
while ((varInt = readOpCodeInt(scriptBytes, buffPointer)) != null) {
LOGGER.debug("VAL: " + varInt.getValue());
if (readVariableStackInt(scriptBytes, buffPointer).getValue() == 0 && varInt.getSize() == 1
&& varInt.getValue() > 16) {
LOGGER.debug("OPCODE: " + varInt.getValue());
// We got an opcode.
if (varInt.getValue() == 0xae) {
LOGGER.debug("OP_CHECKMULTISIG");
// OP_CHECKMULTISIG, process the stack.
long numberKeys = Long.parseLong(stack.getLast());
LOGGER.debug("NUMBER OF KEYS: " + numberKeys);
stack.removeLast();
if (numberKeys > stack.size()) {
// Data's garabage, we won't even try to read it. Bail out.
LOGGER.debug("Error reading script: " + stack.size());
return new LinkedList<>();
}
for (int i = 0; i < numberKeys; i++) {
String pubKey = stack.getLast();
stack.removeLast();
byte[] keyBytes = ByteUtilities.toByteArray(pubKey);
keyBytes = ByteUtilities.stripLeadingNullBytes(keyBytes);
publicKeys.add(ByteUtilities.toHexString(keyBytes));
}
return publicKeys;
} else {
// Non-standard script. Bail out.
LOGGER.debug("Non-standard script, cannot process");
return new LinkedList<>();
}
} else {
// Push it.
LOGGER.debug("Pushing stack variable");
if (readVariableStackInt(scriptBytes, buffPointer).getValue() == 0 && varInt.getSize() == 1
&& varInt.getValue() <= 16) {
LOGGER.debug("Pushing: " + ((Long) varInt.getValue()).toString());
stack.add(((Long) varInt.getValue()).toString());
buffPointer += varInt.getSize();
} else {
buffPointer += varInt.getSize();
LOGGER.debug("Pushing: " + ByteUtilities.toHexString(
ByteUtilities.readBytes(scriptBytes, buffPointer, (int) varInt.getValue())));
stack.add(ByteUtilities.toHexString(ByteUtilities.readBytes(scriptBytes, buffPointer, (int) varInt.getValue())));
buffPointer += varInt.getValue();
}
}
}
} catch (Exception e) {
LOGGER.debug("Bad script caused exception, cannot process", e);
return new LinkedList<>();
}
// We ran out of script without seeing what we expected. Bail out.
return new LinkedList<>();
}
}