635 lines
26 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|
|
}
|