package com.greenaddress.greenapi; import com.google.common.collect.ImmutableList; import com.google.common.primitives.Bytes; import com.greenaddress.greenbits.GaService; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOptions; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionWitness; import org.bitcoinj.core.VarInt; import org.bitcoinj.params.MainNetParams; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; import com.blockstream.libwally.Wally; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import android.util.Log; public class GATx { private static final String TAG = GATx.class.getSimpleName(); private static final int SIG_LEN = 73; // Average signature length private static final List<byte[]> EMPTY_SIGS = ImmutableList.of(new byte[SIG_LEN], new byte[SIG_LEN]); private static final byte[] EMPTY_WITNESS_DATA = new byte[0]; private static final byte[] EMPTY_WITNESS_SIG = new byte[SIG_LEN + 1]; // 1=Sighash flag byte // Script types in end points public static final int P2SH_FORTIFIED_OUT = 10; public static final int P2SH_P2WSH_FORTIFIED_OUT = 14; public static final int REDEEM_P2SH_FORTIFIED = 150; public static final int REDEEM_P2SH_P2WSH_FORTIFIED = 159; public static final int MAX_BLOCK_NUM = 500000000 - 1; // From nTimeLock field definition public static class ChangeOutput { public final TransactionOutput mOutput; public final Integer mPointer; public final Boolean mIsSegwit; public ChangeOutput(final TransactionOutput output, final Integer pointer, final Boolean isSegwit) { mOutput = output; mPointer = pointer; mIsSegwit = isSegwit; } } public static int getOutScriptType(final int scriptType) { switch (scriptType) { case REDEEM_P2SH_FORTIFIED: return P2SH_FORTIFIED_OUT; case REDEEM_P2SH_P2WSH_FORTIFIED: return P2SH_P2WSH_FORTIFIED_OUT; default: return scriptType; } } public static void sortUtxos(final List<JSONMap> utxos, final boolean minimizeInputs) { Collections.sort(utxos, new Comparator<JSONMap>() { @Override public int compare(final JSONMap lhs, final JSONMap rhs) { int cmp = 0; if (!minimizeInputs) { // When not minimizing inputs, prefer earlier block times; // By spending earlier utxos we can avoid re-deposits. cmp = lhs.getInt("block_height", MAX_BLOCK_NUM).compareTo(rhs.getInt("block_height", MAX_BLOCK_NUM)); } if (cmp == 0) cmp = lhs.getBigInteger("value").compareTo(rhs.getBigInteger("value")); return cmp; } }); } public static byte[] createOutScript(final GaService service, final JSONMap ep) { return service.createOutScript(ep.getInt("subaccount", 0), ep.getInt(ep.getKey("pubkey_pointer", "pointer"))); } public static byte[] createInScript(final List<byte[]> sigs, final byte[] outScript, final int scriptType) { if (scriptType == P2SH_FORTIFIED_OUT || scriptType == REDEEM_P2SH_FORTIFIED) // FIXME: investigate P2SH_ vs REDEEM_P2SH_ and ideally make it consistent here return ScriptBuilder.createMultiSigInputScriptBytes(sigs, outScript).getProgram(); // REDEEM_P2SH_P2WSH_FORTIFIED: PUSH(OP_0 PUSH(sha256(outScript))) return Bytes.concat(Wally.hex_to_bytes("220020"), Wally.sha256(outScript)); } public static void addInput(final GaService service, final Transaction tx, final JSONMap ep) { final int scriptType = ep.getInt("script_type"); final byte[] outscript = createOutScript(service, ep); final byte[] inscript = createInScript(EMPTY_SIGS, outscript, scriptType); final TransactionOutPoint op; op = new TransactionOutPoint(service.getNetworkParameters(), ep.getInt("pt_idx"), ep.getHash("txhash")); final TransactionInput in = new TransactionInput(service.getNetworkParameters(), null, inscript, op, ep.getCoin("value")); TransactionWitness witness = null; if (getOutScriptType(scriptType) == P2SH_P2WSH_FORTIFIED_OUT) { // To calculate the tx weight correctly, we must set the witness data // to the correct number of pushes of data of the correct size. // Before sending the transaction, we replace this witness data with // just the users signature, since the server recreates the witness // data itself to replace the server sig and script placeholders. witness = new TransactionWitness(4); witness.setPush(0, EMPTY_WITNESS_DATA); // Dummy for off by 1 in OP_CHECKMULTISIG witness.setPush(1, EMPTY_WITNESS_SIG); // Users signature witness.setPush(2, EMPTY_WITNESS_SIG); // GA Server signature witness.setPush(3, outscript); // Outscript } if (service.isRBFEnabled()) in.setSequenceNumber(0xFFFFFFFD); // Opt-in RBF else in.setSequenceNumber(0xFFFFFFFE); // Ensures nlocktime is recognized tx.addInput(in); if (witness != null) tx.setWitness(tx.getInputs().size() - 1, witness); } public static boolean addTxOutput(final GaService service, final Transaction tx, final Coin amount, final String recipient) { if (service.isElements()) return false; // Only base58 supported for elements currently try { tx.addOutput(amount, Address.fromBase58(service.getNetworkParameters(), recipient)); return true; } catch (final Exception e) { try { final byte[] decoded = GaService.decodeBech32Address(recipient, service.getNetworkParameters()); if (decoded == null || decoded[0] != 0) return false; // Only v0 segwit scripts are supported final byte[] scriptHash = Arrays.copyOfRange(decoded, 2, decoded.length); if (decoded.length == Wally.WALLY_SCRIPTPUBKEY_P2WPKH_LEN) { tx.addOutput(amount, Address.fromP2WPKHHash(service.getNetworkParameters(), scriptHash)); return true; } else if (decoded.length == Wally.WALLY_SCRIPTPUBKEY_P2WSH_LEN) { tx.addOutput(amount, Address.fromP2WSHHash(service.getNetworkParameters(), scriptHash)); return true; } } catch (final Exception e2) { // Fall through } } return false; } private static Address createChangeAddress(final JSONMap addrInfo, final NetworkParameters params) { byte[] script = addrInfo.getBytes("script"); if (addrInfo.getString("addr_type").equals("p2wsh")) script = ScriptBuilder.createP2WSHOutputScript(Wally.sha256(script)).getProgram(); return Address.fromP2SHHash(params, Wally.hash160(script)); } /* Add a new change output to a tx */ public static ChangeOutput addChangeOutput(final GaService service, final Transaction tx, final int subaccount) { final JSONMap addrInfo = service.getNewAddress(subaccount); if (addrInfo == null) return null; return new ChangeOutput(tx.addOutput(Coin.ZERO, createChangeAddress(addrInfo, service.getNetworkParameters())), addrInfo.getInt("pointer"), addrInfo.getString("addr_type").equals("p2wsh")); } /* Identify the change output in a tx */ public static ChangeOutput findChangeOutput(final List<JSONMap> endPoints, final Transaction tx, final int forSubAccount) { int index = -1; int pubkey_pointer = -1; int scriptType = 0; for (final JSONMap ep : endPoints) { if (!ep.getBool("is_credit") || !ep.getBool("is_relevant") || ep.getInt("subaccount", 0) != forSubAccount) continue; if (index != -1) { // Found another output paying to this account. This can // only happend when redepositing to our own acount with // a change output (e.g. by manually sending some amount // of funds to ourself). In this case the change output // will have been created after the amount output by the // tx construction code, so the output with the highest // pubkey pointer is our change, as they are incremented // for each new output. Note that we can't use the order // of the output in the tx due to change randomisation. if (ep.getInt("pubkey_pointer") < pubkey_pointer) continue; // Not our change output } index = ep.getInt("pt_idx"); pubkey_pointer = ep.getInt("pubkey_pointer"); scriptType = ep.getInt("script_type"); } if (index == -1) return null; return new ChangeOutput(tx.getOutput(index), pubkey_pointer, getOutScriptType(scriptType) == P2SH_P2WSH_FORTIFIED_OUT); } /* Swap the change and recipient output in a tx with 50% probability */ public static boolean randomizeChangeOutput(final Transaction tx) { if (CryptoHelper.randomBytes(1)[0] < 0) return false; final TransactionOutput a = tx.getOutput(0); final TransactionOutput b = tx.getOutput(1); tx.clearOutputs(); tx.addOutput(b); tx.addOutput(a); return true; } /* Create previous outputs for tx construction from uxtos */ public static List<Output> createPrevouts(final GaService service, final List<JSONMap> utxos) { final List<Output> prevOuts = new ArrayList<>(); for (final JSONMap utxo : utxos) prevOuts.add(new Output(utxo.getInt("subaccount"), utxo.getInt(utxo.getKey("pubkey_pointer", "pointer")), HDKey.BRANCH_REGULAR, getOutScriptType(utxo.getInt("script_type")), Wally.hex_from_bytes(createOutScript(service, utxo)), utxo.getLong("value"))); return prevOuts; } /* Return the previous transactions for each of a txs inputs */ public static List<Transaction> getPreviousTransactions(final GaService service, final Transaction tx) { final List<Transaction> previousTxs = new ArrayList<>(); try { for (final TransactionInput in : tx.getInputs()) { final String txhex = service.getRawOutputHex(in.getOutpoint().getHash()); previousTxs.add(GaService.buildTransaction(txhex, service.getNetworkParameters())); } } catch (final Exception e) { e.printStackTrace(); return null; } return previousTxs; } // Estimate the size of Elements specific parts of a tx private static int estimateElementsSize(final Transaction tx, final Network network) { if (!network.isElements()) return 0; final int sjSize = Wally.asset_surjectionproof_size(tx.getInputs().size()); final int cmtSize = Wally.EC_PUBLIC_KEY_LEN; // Estimate the rangeproof len as 160 bytes per 2 bits used to express the // output value (currently 32), plus fixed overhead of 128 (this is a slight // over-estimate given that only up to +100 has been seen in the wild). // FIXME: This assumes 32 bit maximum amounts as per current wally impl. final int rpSize = ((32 / 2) * 160) + 128; final int singleOutputSize = sjSize + VarInt.sizeOf(sjSize) + cmtSize + VarInt.sizeOf(cmtSize) + rpSize + VarInt.sizeOf(rpSize); return singleOutputSize * tx.getOutputs().size(); } public static Coin getFeeEstimateForRBF(final GaService service) throws GAException { return getFeeEstimate(service, 1); } // Return the best estimate of the fee rate in satoshi/1000 bytes public static Coin getFeeEstimate(final GaService service, final int forBlock) throws GAException { // Iterate the estimates from shortest to longest confirmation time final SortedSet<Integer> keys = new TreeSet<>(); for (final String block : service.getFeeEstimates().mData.keySet()) keys.add(Integer.parseInt(block)); for (final Integer blockNum : keys) { double feeRate = service.getFeeRate(blockNum); if (feeRate <= 0.0) continue; // No estimate available: Try next confirmation rate final int actualBlock = service.getFeeBlocks(blockNum); if (actualBlock < forBlock) continue; // Use forBlock confirmation rate and later only // Found rate at or later than our target confirmation time return Coin.valueOf((long) (feeRate * 1000 * 1000 * 100)); } // At this point, we don't have a usable fee rate estimate if (service.isElements()) return Coin.valueOf(1); return service.getMinFeeRate(); } public static JSONMap makeLimitsData(final Coin limitDelta, final Coin fee, final int changeIndex) { final JSONMap m = new JSONMap(); m.mData.put("asset", "BTC"); m.mData.put("amount", limitDelta.getValue()); m.mData.put("fee", fee.getValue()); m.mData.put("change_idx", changeIndex); return m; } public static Coin addUtxo(final GaService service, final Transaction tx, final List<JSONMap> utxos, final List<JSONMap> used) { return addUtxo(service, tx, utxos, used, null, null, null, null); } public static Coin addUtxo(final GaService service, final Transaction tx, final List<JSONMap> utxos, final List<JSONMap> used, final List<Long> inValues, final List<byte[]> inAssetIds, final List<byte[]> inAbfs, final List<byte[]> inVbfs) { final JSONMap utxo = utxos.get(0); utxos.remove(0); if (utxo.getBool("confidential")) { inAssetIds.add(utxo.getBytes("assetId")); inAbfs.add(utxo.getBytes("abf")); inVbfs.add(utxo.getBytes("vbf")); } used.add(utxo); addInput(service, tx, utxo); if (inValues != null) inValues.add(utxo.getLong("value")); return utxo.getCoin("value"); } public static int getTxVSize(final Transaction tx, final Network network) { final int vSize; if (!(network.isElements() || tx.hasWitness())) { vSize = tx.unsafeBitcoinSerialize().length; Log.d(TAG, "getTxVSize(non-sw): " + vSize); } else { /* For segwit, the fee is based on the weighted size of the tx */ tx.transactionOptions = TransactionOptions.NONE; final int nonSwSize = tx.unsafeBitcoinSerialize().length; tx.transactionOptions = TransactionOptions.ALL; final int swSize = tx.unsafeBitcoinSerialize().length; final int fullSize = swSize + estimateElementsSize(tx, network); vSize = (int) Math.ceil((nonSwSize * 3 + fullSize) / 4.0); Log.d(TAG, "getTxVSize(sw): " + nonSwSize + '/' + swSize + '/' + vSize); } return vSize; } // Calculate the fee that must be paid for a tx public static Coin getTxFee(final GaService service, final Transaction tx, final Coin feeRate) { final Coin minRate = service.getMinFeeRate(); final Coin rate = feeRate.isLessThan(minRate) ? minRate : feeRate; Log.d(TAG, "getTxFee(rates): " + rate.value + '/' + feeRate.value + '/' + minRate.value); final int vSize = getTxVSize(tx, service.getNetwork()); final double fee = (double) vSize * rate.value / 1000.0; final long roundedFee = (long) Math.ceil(fee); // Round up Log.d(TAG, "getTxFee: fee is " + roundedFee); return Coin.valueOf(roundedFee); } public static PreparedTransaction signTransaction(final GaService service, final Transaction tx, final List<JSONMap> usedUtxos, final int subAccount, final ChangeOutput changeOutput) { // Fetch previous outputs final List<Output> prevOuts = createPrevouts(service, usedUtxos); final PreparedTransaction ptx; ptx = new PreparedTransaction(changeOutput, subAccount, tx, service.findSubaccountByType(subAccount, "2of3")); ptx.mPrevoutRawTxs = new HashMap<>(); for (final Transaction prevTx : getPreviousTransactions(service, tx)) { if (prevTx == null) throw new RuntimeException("Previous transaction not found"); ptx.mPrevoutRawTxs.put(Wally.hex_from_bytes(prevTx.getHash().getBytes()), prevTx); } // Sign the tx final List<byte[]> signatures = service.signTransaction(tx, ptx, prevOuts); for (int i = 0; i < signatures.size(); ++i) { final byte[] sig = signatures.get(i); final JSONMap utxo = usedUtxos.get(i); final int scriptType = utxo.getInt("script_type"); final byte[] outscript = createOutScript(service, utxo); final List<byte[]> userSigs = ImmutableList.of(new byte[]{0}, sig); final byte[] inscript = createInScript(userSigs, outscript, scriptType); tx.getInput(i).setScriptSig(new Script(inscript)); if (getOutScriptType(scriptType) == P2SH_P2WSH_FORTIFIED_OUT) { // Replace the witness data with just the user signature: // the server will recreate the witness data to include the // dummy OP_CHECKMULTISIG push, user + server sigs and script. final TransactionWitness witness = new TransactionWitness(1); witness.setPush(0, sig); tx.setWitness(i, witness); } } return ptx; } }