tangem-app-android-audited/app/src/main/java/com/tangem/wallet/Transaction.java
2019-06-11 12:40:26 +03:00

635 lines
26 KiB
Java

package com.tangem.wallet;
/**
* Created by Ilia on 29.09.2017.
*/
import com.tangem.wallet.btc.BitcoinException;
import com.tangem.wallet.btc.BitcoinInputStream;
import com.tangem.wallet.btc.BitcoinOutputStream;
import com.tangem.util.CryptoUtil;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Stack;
@SuppressWarnings("WeakerAccess")
public final class Transaction {
public final int version;
public final Input[] inputs;
public final Output[] outputs;
public final int lockTime;
public Transaction(byte[] rawBytes) throws BitcoinException {
if (rawBytes == null) {
throw new BitcoinException(BitcoinException.ERR_NO_INPUT, "empty input");
}
BitcoinInputStream bais = null;
try {
bais = new BitcoinInputStream(rawBytes);
version = bais.readInt32();
if (version != 1 && version != 2 && version != 3 && version != -1273714314) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unsupported TX version", version);
}
int inputsCount = 0;
int first = bais.readByte();
if(first == 0)
{
int skip = bais.readByte();
inputsCount = bais.readByte();
}
else
{
inputsCount = first;
}
//int inputsCount = (int) bais.readVarInt(); TODO:
inputs = new Input[inputsCount];
for (int i = 0; i < inputsCount; i++) {
OutPoint outPoint = new OutPoint(BTCUtils.reverse(bais.readChars(32)), bais.readInt32());
byte[] script = bais.readChars((int) bais.readVarInt());
int sequence = bais.readInt32();
inputs[i] = new Input(outPoint, new Script(script), sequence);
}
int outputsCount = (int) bais.readVarInt();
outputs = new Output[outputsCount];
for (int i = 0; i < outputsCount; i++) {
long value = bais.readInt64();
long scriptSize = bais.readVarInt();
if (scriptSize < 0 || scriptSize > 10_000_000) {
throw new BitcoinException(BitcoinException.ERR_BAD_FORMAT, "Script size for output " + i +
" is strange (" + scriptSize + " bytes).");
}
byte[] script = bais.readChars((int) scriptSize);
outputs[i] = new Output(value, new Script(script));
}
lockTime = bais.readInt32();
} catch (EOFException e) {
throw new BitcoinException(BitcoinException.ERR_BAD_FORMAT, "TX incomplete");
} catch (IOException e) {
throw new IllegalArgumentException("Unable to read TX");
} catch (Error e) {
throw new IllegalArgumentException("Unable to read TX: " + e);
} finally {
if (bais != null) {
try {
bais.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
public Transaction(Input[] inputs, Output[] outputs, int lockTime) {
this.version = 1;
this.inputs = inputs;
this.outputs = outputs;
this.lockTime = lockTime;
}
public byte[] getBytes() {
BitcoinOutputStream baos = new BitcoinOutputStream();
try {
baos.writeInt32(version);
baos.writeVarInt(inputs.length);
for (Input input : inputs) {
baos.write(BTCUtils.reverse(input.outPoint.hash));
baos.writeInt32(input.outPoint.index);
int scriptLen = input.script == null ? 0 : input.script.bytes.length;
baos.writeVarInt(scriptLen);
if (scriptLen > 0) {
baos.write(input.script.bytes);
}
baos.writeInt32(input.sequence);
}
baos.writeVarInt(outputs.length);
for (Output output : outputs) {
baos.writeInt64(output.value);
int scriptLen = output.script == null ? 0 : output.script.bytes.length;
baos.writeVarInt(scriptLen);
if (scriptLen > 0) {
baos.write(output.script.bytes);
}
}
baos.writeInt32(lockTime);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return baos.toByteArray();
}
@Override
public String toString() {
return "{" +
"\n\"inputs\":\n" + printAsJsonArray(inputs) +
",\n\"outputs\":\n" + printAsJsonArray(outputs) +
",\n\"lockTime\":\"" + lockTime + "\"}\n";
}
private String printAsJsonArray(Object[] a) {
if (a == null) {
return "null";
}
if (a.length == 0) {
return "[]";
}
int iMax = a.length - 1;
StringBuilder sb = new StringBuilder();
sb.append('[');
for (int i = 0; ; i++) {
sb.append(String.valueOf(a[i]));
if (i == iMax)
return sb.append(']').toString();
sb.append(",\n");
}
}
public static class Input {
public final OutPoint outPoint;
public final Script script;
public final int sequence;
public Input(OutPoint outPoint, Script script, int sequence) {
this.outPoint = outPoint;
this.script = script;
this.sequence = sequence;
}
@Override
public String toString() {
return "{\n\"outPoint\":" + outPoint + ",\n\"script\":\"" + script + "\",\n\"sequence\":\"" + Integer.toHexString(sequence) + "\"\n}\n";
}
}
public static class OutPoint {
public final byte[] hash;//32-byte hash of the transaction from which we want to redeem an output
public final int index;//Four-byte field denoting the output index we want to redeem from the transaction with the above hash (output number 2 = output index 1)
public OutPoint(byte[] hash, int index) {
this.hash = hash;
this.index = index;
}
@Override
public String toString() {
return "{" + "\"hash\":\"" + BTCUtils.toHex(hash) + "\", \"index\":\"" + index + "\"}";
}
}
public static class Output {
public final long value;
public final Script script;
public Output(long value, Script script) {
this.value = value;
this.script = script;
}
@Override
public String toString() {
return "{\n\"value\":\"" + value * 1e-8 + "\",\"script\":\"" + script + "\"\n}";
}
}
public static final class Script {
public static class ScriptInvalidException extends Exception {
public ScriptInvalidException() {
}
public ScriptInvalidException(String s) {
super(s);
}
}
public static final byte OP_FALSE = 0;
public static final byte OP_TRUE = 0x51;
public static final byte OP_PUSHDATA1 = 0x4c;
public static final byte OP_PUSHDATA2 = 0x4d;
public static final byte OP_PUSHDATA4 = 0x4e;
public static final byte OP_DUP = 0x76;//Duplicates the top stack item.
public static final byte OP_DROP = 0x75;
public static final byte OP_HASH160 = (byte) 0xA9;//The input is hashed twice: first with SHA-256 and then with RIPEMD-160.
public static final byte OP_VERIFY = 0x69;//Marks transaction as invalid if top stack value is not true. True is removed, but false is not.
public static final byte OP_EQUAL = (byte) 0x87;//Returns 1 if the inputs are exactly equal, 0 otherwise.
public static final byte OP_EQUALVERIFY = (byte) 0x88;//Same as OP_EQUAL, but runs OP_VERIFY afterward.
public static final byte OP_CHECKSIG = (byte) 0xAC;//The entire transaction's outputs, inputs, and script (from the most recently-executed OP_CODESEPARATOR to the end) are hashed. The signature used by OP_CHECKSIG must be a valid signature for this hash and public key. If it is, 1 is returned, 0 otherwise.
public static final byte OP_CHECKSIGVERIFY = (byte) 0xAD;
public static final byte OP_NOP = 0x61;
public static final byte SIGHASH_ALL = 1;
public final byte[] bytes;
public Script(byte[] rawBytes) {
bytes = rawBytes;
}
public Script(byte[] data1, byte[] data2) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(data1.length + data2.length + 2);
try {
writeBytes(data1, baos);
writeBytes(data2, baos);
baos.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
bytes = baos.toByteArray();
}
private static void writeBytes(byte[] data, ByteArrayOutputStream baos) throws IOException {
if (data.length < OP_PUSHDATA1) {
baos.write(data.length);
} else if (data.length < 0xff) {
baos.write(OP_PUSHDATA1);
baos.write(data.length);
} else if (data.length < 0xffff) {
baos.write(OP_PUSHDATA2);
baos.write(data.length & 0xff);
baos.write((data.length >> 8) & 0xff);
} else {
baos.write(OP_PUSHDATA4);
baos.write(data.length & 0xff);
baos.write((data.length >> 8) & 0xff);
baos.write((data.length >> 16) & 0xff);
baos.write((data.length >>> 24) & 0xff);
}
baos.write(data);
}
public void run(Stack<byte[]> stack) throws ScriptInvalidException {
run(0, null, stack);
}
public void run(int inputIndex, Transaction tx, Stack<byte[]> stack) throws ScriptInvalidException {
for (int pos = 0; pos < bytes.length; pos++) {
switch (bytes[pos]) {
case OP_NOP:
break;
case OP_DROP:
if (stack.isEmpty()) {
throw new IllegalArgumentException("stack empty on OP_DROP");
}
stack.pop();
break;
case OP_DUP:
if (stack.isEmpty()) {
throw new IllegalArgumentException("stack empty on OP_DUP");
}
stack.push(stack.peek());
break;
case OP_HASH160:
if (stack.isEmpty()) {
throw new IllegalArgumentException("stack empty on OP_HASH160");
}
stack.push(CryptoUtil.sha256ripemd160(stack.pop()));
break;
case OP_EQUAL:
case OP_EQUALVERIFY:
if (stack.size() < 2) {
throw new IllegalArgumentException("not enough elements to perform OP_EQUAL");
}
stack.push(new byte[]{(byte) (Arrays.equals(stack.pop(), stack.pop()) ? 1 : 0)});
if (bytes[pos] == OP_EQUALVERIFY) {
if (verifyFails(stack)) {
throw new ScriptInvalidException("wrong address");
}
}
break;
case OP_VERIFY:
if (verifyFails(stack)) {
throw new ScriptInvalidException();
}
break;
case OP_CHECKSIG:
case OP_CHECKSIGVERIFY:
byte[] publicKey = stack.pop();
byte[] signatureAndHashType = stack.pop();
if (signatureAndHashType[signatureAndHashType.length - 1] != SIGHASH_ALL) {
throw new IllegalArgumentException("I cannot check this sig type: " + signatureAndHashType[signatureAndHashType.length - 1]);
}
byte[] signature = new byte[signatureAndHashType.length - 1];
System.arraycopy(signatureAndHashType, 0, signature, 0, signature.length);
byte[] hash = hashTransaction(inputIndex, bytes, tx);
//boolean valid = BTCUtils.verify(publicKey, signature, hash);
if (bytes[pos] == OP_CHECKSIG) {
stack.push(new byte[]{(byte) (1)});
} else {
if (verifyFails(stack)) {
throw new ScriptInvalidException("Bad signature");
}
if (!stack.empty()) {
throw new ScriptInvalidException("Bad signature - superfluous scriptSig operations");
}
}
break;
case OP_FALSE:
stack.push(new byte[]{0});
break;
case OP_TRUE:
stack.push(new byte[]{1});
break;
default:
int op = bytes[pos] & 0xff;
int len;
if (op < OP_PUSHDATA1) {
len = op;
byte[] data = new byte[len];
System.arraycopy(bytes, pos + 1, data, 0, len);
stack.push(data);
pos += data.length;
} else if (op == OP_PUSHDATA1) {
len = bytes[pos + 1] & 0xff;
byte[] data = new byte[len];
System.arraycopy(bytes, pos + 1, data, 0, len);
stack.push(data);
pos += 1 + data.length;
} else {
throw new IllegalArgumentException("I cannot read this data: " + Integer.toHexString(bytes[pos]));
}
break;
}
}
}
public static byte[] hashTransaction(int inputIndex, byte[] subscript, Transaction tx) {
Input[] unsignedInputs = new Input[tx.inputs.length];
for (int i = 0; i < tx.inputs.length; i++) {
Input txInput = tx.inputs[i];
if (i == inputIndex) {
unsignedInputs[i] = new Input(txInput.outPoint, new Script(subscript), txInput.sequence);
} else {
unsignedInputs[i] = new Input(txInput.outPoint, new Script(new byte[0]), txInput.sequence);
}
}
Transaction unsignedTransaction = new Transaction(unsignedInputs, tx.outputs, tx.lockTime);
return hashTransactionForSigning(unsignedTransaction);
}
public static byte[] hashTransactionForSigning(Transaction unsignedTransaction) {
byte[] txUnsignedBytes = unsignedTransaction.getBytes();
BitcoinOutputStream baos = new BitcoinOutputStream();
try {
baos.write(txUnsignedBytes);
baos.writeInt32(Script.SIGHASH_ALL);
baos.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
return CryptoUtil.doubleSha256(baos.toByteArray());
}
public static boolean verifyFails(Stack<byte[]> stack) {
byte[] input;
boolean valid;
input = stack.pop();
if (input.length == 0 || (input.length == 1 && input[0] == OP_FALSE)) {
//false
stack.push(new byte[]{OP_FALSE});
valid = false;
} else {
//true
valid = true;
}
return !valid;
}
@Override
public String toString() {
return convertBytesToReadableString(bytes);
}
//converts something like "OP_DUP OP_HASH160 ba507bae8f1643d2556000ca26b9301b9069dc6b OP_EQUALVERIFY OP_CHECKSIG" into bytes
public static byte[] convertReadableStringToBytes(String readableString) {
String[] tokens = readableString.trim().split("\\s+");
ByteArrayOutputStream os = new ByteArrayOutputStream();
for (String token : tokens) {
switch (token) {
case "OP_NOP":
os.write(OP_NOP);
break;
case "OP_DROP":
os.write(OP_DROP);
break;
case "OP_DUP":
os.write(OP_DUP);
break;
case "OP_HASH160":
os.write(OP_HASH160);
break;
case "OP_EQUAL":
os.write(OP_EQUAL);
break;
case "OP_EQUALVERIFY":
os.write(OP_EQUALVERIFY);
break;
case "OP_VERIFY":
os.write(OP_VERIFY);
break;
case "OP_CHECKSIG":
os.write(OP_CHECKSIG);
break;
case "OP_CHECKSIGVERIFY":
os.write(OP_CHECKSIGVERIFY);
break;
case "OP_FALSE":
os.write(OP_FALSE);
break;
case "OP_TRUE":
os.write(OP_TRUE);
break;
default:
if (token.startsWith("OP_")) {
throw new IllegalArgumentException("I don't know this operation: " + token);
}
byte[] data = BTCUtils.fromHex(token);
if (data == null) {
throw new IllegalArgumentException("I don't know what's this: " + token);
}
if (data.length < OP_PUSHDATA1) {
os.write(data.length);
try {
os.write(data);
} catch (IOException e) {
throw new RuntimeException("ByteArrayOutputStream behaves weird: " + e);
}
} else if (data.length <= 255) {
os.write(OP_PUSHDATA1);
os.write(data.length);
try {
os.write(data);
} catch (IOException e) {
throw new RuntimeException("ByteArrayOutputStream behaves weird: " + e);
}
} else {
throw new IllegalArgumentException("OP_PUSHDATA2 & OP_PUSHDATA4 are not supported");
}
break;
}
}
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
return os.toByteArray();
}
public static String convertBytesToReadableString(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (int pos = 0; pos < bytes.length; pos++) {
if (sb.length() > 0) {
sb.append(' ');
}
switch (bytes[pos]) {
case OP_NOP:
sb.append("OP_NOP");
break;
case OP_DROP:
sb.append("OP_DROP");
break;
case OP_DUP:
sb.append("OP_DUP");
break;
case OP_HASH160:
sb.append("OP_HASH160");
break;
case OP_EQUAL:
sb.append("OP_EQUAL");
break;
case OP_EQUALVERIFY:
sb.append("OP_EQUALVERIFY");
break;
case OP_VERIFY:
sb.append("OP_VERIFY");
break;
case OP_CHECKSIG:
sb.append("OP_CHECKSIG");
break;
case OP_CHECKSIGVERIFY:
sb.append("OP_CHECKSIGVERIFY");
break;
case OP_FALSE:
sb.append("OP_FALSE");
break;
case OP_TRUE:
sb.append("OP_TRUE");
break;
default:
int op = bytes[pos] & 0xff;
int len;
if (op < OP_PUSHDATA1) {
len = op;
byte[] data = new byte[len];
System.arraycopy(bytes, pos + 1, data, 0, len);
sb.append(BTCUtils.toHex(data));
pos += data.length;
} else if (op == OP_PUSHDATA1) {
len = bytes[pos + 1] & 0xff;
byte[] data = new byte[len];
System.arraycopy(bytes, pos + 1, data, 0, len);//FIXME I suspect there is off by one error...
sb.append(BTCUtils.toHex(data));
pos += 1 + data.length;
} else {
throw new IllegalArgumentException("I cannot read this data: " + Integer.toHexString(bytes[pos]) + " at " + pos);
}
break;
}
}
return sb.toString();
}
@Override
public boolean equals(Object o) {
return this == o || !(o == null || getClass() != o.getClass()) && Arrays.equals(bytes, ((Script) o).bytes);
}
@Override
public int hashCode() {
return Arrays.hashCode(bytes);
}
public static Script buildOutput(String address) throws BitcoinException {
//noinspection TryWithIdenticalCatches
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] == 0 || addressWithCheckSumAndNetworkCode[0] == 111 || addressWithCheckSumAndNetworkCode[0] == 48) { //0 for BTC/BCH 1 address | 48 for LTC L address
return buildOutputP2H(address);
}
if(addressWithCheckSumAndNetworkCode[0] == 5 || addressWithCheckSumAndNetworkCode[0] == (byte)0xc4 || addressWithCheckSumAndNetworkCode[0] == 50) { //5 for BTC/BCH/LTC 3 address | 50 for LTC M address
return buildOutputP2SH(address);
}
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}
public static Script buildOutputP2SH(String address) throws BitcoinException {
try {
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] != 5 && addressWithCheckSumAndNetworkCode[0] != (byte)0xc4 && addressWithCheckSumAndNetworkCode[0] != 50) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}
byte[] bareAddress = new byte[20];
System.arraycopy(addressWithCheckSumAndNetworkCode, 1, bareAddress, 0, bareAddress.length);
ByteArrayOutputStream buf = new ByteArrayOutputStream(23);
buf.write(OP_HASH160);
writeBytes(bareAddress, buf);
buf.write(OP_EQUAL);
return new Script(buf.toByteArray());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Script buildOutputP2H(String address) throws BitcoinException {
//noinspection TryWithIdenticalCatches
try {
byte[] addressWithCheckSumAndNetworkCode = Base58.decodeBase58(address);
if (addressWithCheckSumAndNetworkCode[0] != 0 && addressWithCheckSumAndNetworkCode[0] != 111 && addressWithCheckSumAndNetworkCode[0] != 48) {
throw new BitcoinException(BitcoinException.ERR_UNSUPPORTED, "Unknown address type", address);
}
byte[] bareAddress = new byte[20];
System.arraycopy(addressWithCheckSumAndNetworkCode, 1, bareAddress, 0, bareAddress.length);
MessageDigest digestSha = MessageDigest.getInstance("SHA-256");
digestSha.update(addressWithCheckSumAndNetworkCode, 0, addressWithCheckSumAndNetworkCode.length - 4);
byte[] calculatedDigest = digestSha.digest(digestSha.digest());
for (int i = 0; i < 4; i++) {
if (calculatedDigest[i] != addressWithCheckSumAndNetworkCode[addressWithCheckSumAndNetworkCode.length - 4 + i]) {
throw new BitcoinException(BitcoinException.ERR_BAD_FORMAT, "Bad address", address);
}
}
ByteArrayOutputStream buf = new ByteArrayOutputStream(25);
buf.write(OP_DUP);
buf.write(OP_HASH160);
writeBytes(bareAddress, buf);
buf.write(OP_EQUALVERIFY);
buf.write(OP_CHECKSIG);
return new Script(buf.toByteArray());
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}