package com.mygeopay.core.wallet; import com.mygeopay.core.coins.CoinType; import com.mygeopay.core.coins.FeePolicy; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.InsufficientMoneyException; import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.ScriptException; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionConfidence; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutput; import org.bitcoinj.core.VarInt; import org.bitcoinj.script.Script; import org.bitcoinj.signers.LocalTransactionSigner; import org.bitcoinj.signers.MissingSigResolutionSigner; import org.bitcoinj.signers.TransactionSigner; import org.bitcoinj.wallet.CoinSelection; import org.bitcoinj.wallet.CoinSelector; import org.bitcoinj.wallet.DecryptingKeyBag; import org.bitcoinj.wallet.KeyBag; import org.bitcoinj.wallet.RedeemData; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.locks.ReentrantLock; import static com.mygeopay.core.Preconditions.checkArgument; import static com.mygeopay.core.Preconditions.checkNotNull; import static com.mygeopay.core.Preconditions.checkState; /** * @author John L. Jegutanis */ public class TransactionCreator { private static final Logger log = LoggerFactory.getLogger(TransactionCreator.class); private final WalletAccount account; private final CoinType coinType; private final CoinSelector coinSelector = new WalletCoinSelector(); private final ReentrantLock lock; // TODO remove public TransactionCreator(AbstractWallet account) { this.account = account; lock = account.lock; coinType = account.coinType; } public TransactionCreator(AddressWallet account) { this.account = account; lock = account.lock; coinType = account.coinType; } private static class FeeCalculation { CoinSelection bestCoinSelection; TransactionOutput bestChangeOutput; } /** * Given a spend request containing an incomplete transaction, makes it valid by adding outputs and signed inputs * according to the instructions in the request. The transaction in the request is modified by this method. * * @param req a SendRequest that contains the incomplete transaction and details for how to make it valid. * @throws org.bitcoinj.core.InsufficientMoneyException if the request could not be completed due to not enough balance. * @throws IllegalArgumentException if you try and complete the same SendRequest twice */ void completeTx(SendRequest req) throws InsufficientMoneyException { lock.lock(); try { checkArgument(!req.completed, "Given SendRequest has already been completed."); // Calculate the amount of value we need to import. Coin value = Coin.ZERO; for (TransactionOutput output : req.tx.getOutputs()) { value = value.add(output.getValue()); } log.info("Completing send tx with {} outputs totalling {} (not including fees)", req.tx.getOutputs().size(), value.toFriendlyString()); // If any inputs have already been added, we don't need to get their value from wallet Coin totalInput = Coin.ZERO; for (TransactionInput input : req.tx.getInputs()) if (input.getConnectedOutput() != null) totalInput = totalInput.add(input.getConnectedOutput().getValue()); else log.warn("SendRequest transaction already has inputs but we don't know how much they are worth - they will be added to fee."); value = value.subtract(totalInput); List<TransactionInput> originalInputs = new ArrayList<TransactionInput>(req.tx.getInputs()); // We need to know if we need to add an additional fee because one of our values are smaller than 0.01 BTC int numberOfSoftDustOutputs = 0; if (req.ensureMinRequiredFee && !req.emptyWallet) { // min fee checking is handled later for emptyWallet for (TransactionOutput output : req.tx.getOutputs()) if (output.getValue().compareTo(coinType.getSoftDustLimit()) < 0) { if (output.getValue().compareTo(coinType.getMinNonDust()) < 0) throw new org.bitcoinj.core.Wallet.DustySendRequested(); numberOfSoftDustOutputs++; } } // Calculate a list of ALL potential candidates for spending and then ask a coin selector to provide us // with the actual outputs that'll be used to gather the required amount of value. In this way, users // can customize coin selection policies. // // Note that this code is poorly optimized: the spend candidates only alter when transactions in the wallet // change - it could be pre-calculated and held in RAM, and this is probably an optimization worth doing. LinkedList<TransactionOutput> candidates = calculateAllSpendCandidates(true); CoinSelection bestCoinSelection; TransactionOutput bestChangeOutput = null; if (!req.emptyWallet) { // This can throw InsufficientMoneyException. FeeCalculation feeCalculation; feeCalculation = calculateFee(req, value, originalInputs, numberOfSoftDustOutputs, candidates); bestCoinSelection = feeCalculation.bestCoinSelection; bestChangeOutput = feeCalculation.bestChangeOutput; } else { // We're being asked to empty the wallet. What this means is ensuring "tx" has only a single output // of the total value we can currently spend as determined by the selector, and then subtracting the fee. checkState(req.tx.getOutputs().size() == 1, "Empty wallet TX must have a single output only."); CoinSelector selector = req.coinSelector == null ? coinSelector : req.coinSelector; bestCoinSelection = selector.select(NetworkParameters.MAX_MONEY, candidates); candidates = null; // Selector took ownership and might have changed candidates. Don't access again. req.tx.getOutput(0).setValue(bestCoinSelection.valueGathered); log.info(" emptying {}", bestCoinSelection.valueGathered.toFriendlyString()); } for (TransactionOutput output : bestCoinSelection.gathered) req.tx.addInput(output); if (req.ensureMinRequiredFee && req.emptyWallet) { final Coin baseFee = req.fee == null ? Coin.ZERO : req.fee; final Coin feePerKb = req.feePerKb == null ? Coin.ZERO : req.feePerKb; // If txfee is low (exp LTC 0.0014) it displays an error, A better way is needed to catch low fee value // prompt user LTC fee = 0.001 // TODO a better option to catch (low balance - fee) error Transaction tx = req.tx; if (!adjustOutputDownwardsForFee(tx, bestCoinSelection, baseFee, feePerKb)) throw new org.bitcoinj.core.Wallet.CouldNotAdjustDownwards(); } if (bestChangeOutput != null) { req.tx.addOutput(bestChangeOutput); log.info(" with {} change", bestChangeOutput.getValue().toFriendlyString()); } // Now shuffle the outputs to obfuscate which is the change. if (req.shuffleOutputs) req.tx.shuffleOutputs(); // Now sign the inputs, thus proving that we are entitled to redeem the connected outputs. if (req.signInputs) { signTransaction(req); } // Check size. int size = req.tx.bitcoinSerialize().length; if (size > Transaction.MAX_STANDARD_TX_SIZE) throw new org.bitcoinj.core.Wallet.ExceededMaxTransactionSize(); final Coin calculatedFee = req.tx.getFee(); if (calculatedFee != null) { log.info(" with a fee of {} {}", calculatedFee.toFriendlyString(), coinType.getSymbol()); } // Label the transaction as being self created. We can use this later to spend its change output even before // the transaction is confirmed. We deliberately won't bother notifying listeners here as there's not much // point - the user isn't interested in a confidence transition they made themselves. req.tx.getConfidence().setSource(TransactionConfidence.Source.SELF); // Label the transaction as being a user requested payment. This can be used to render GUI wallet // transaction lists more appropriately, especially when the wallet starts to generate transactions itself // for internal purposes. req.tx.setPurpose(Transaction.Purpose.USER_PAYMENT); req.completed = true; req.fee = calculatedFee; log.info(" completed: {}", req.tx); } finally { lock.unlock(); } } /** * <p>Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction * to have all necessary inputs connected or they will be ignored.</p> * <p>Actual signing is done by pluggable {@link org.bitcoinj.signers.LocalTransactionSigner} * and it's not guaranteed that transaction will be complete in the end.</p> */ void signTransaction(SendRequest req) { lock.lock(); try { Transaction tx = req.tx; List<TransactionInput> inputs = tx.getInputs(); List<TransactionOutput> outputs = tx.getOutputs(); checkState(!inputs.isEmpty()); checkState(!outputs.isEmpty()); KeyBag maybeDecryptingKeyBag = new DecryptingKeyBag(account, req.aesKey); int numInputs = tx.getInputs().size(); for (int i = 0; i < numInputs; i++) { TransactionInput txIn = tx.getInput(i); if (txIn.getConnectedOutput() == null) { log.warn("Missing connected output, assuming input {} is already signed.", i); continue; } try { // We assume if its already signed, its hopefully got a SIGHASH type that will not invalidate when // we sign missing pieces (to check this would require either assuming any signatures are signing // standard output types or a way to get processed signatures out of script execution) txIn.getScriptSig().correctlySpends(tx, i, txIn.getConnectedOutput().getScriptPubKey()); log.warn("Input {} already correctly spends output, assuming SIGHASH type used will be safe and skipping signing.", i); continue; } catch (ScriptException e) { // Expected. } Script scriptPubKey = txIn.getConnectedOutput().getScriptPubKey(); RedeemData redeemData = txIn.getConnectedRedeemData(maybeDecryptingKeyBag); checkNotNull(redeemData, "Transaction exists in wallet that we cannot redeem: %s", txIn.getOutpoint().getHash()); txIn.setScriptSig(scriptPubKey.createEmptyInputScript(redeemData.keys.get(0), redeemData.redeemScript)); } TransactionSigner.ProposedTransaction proposal = new TransactionSigner.ProposedTransaction(tx); TransactionSigner signer = new LocalTransactionSigner(); if (!signer.signInputs(proposal, maybeDecryptingKeyBag)) { log.info("{} returned false for the tx", signer.getClass().getName()); } // resolve missing sigs if any new MissingSigResolutionSigner(req.missingSigsMode).signInputs(proposal, maybeDecryptingKeyBag); } finally { lock.unlock(); } } /** * Returns a list of all possible outputs we could possibly spend, potentially even including immature coinbases * (which the protocol may forbid us from spending). In other words, return all outputs that this wallet holds * keys for and which are not already marked as spent. */ LinkedList<TransactionOutput> calculateAllSpendCandidates(boolean excludeImmatureCoinbases) { lock.lock(); try { LinkedList<TransactionOutput> candidates = Lists.newLinkedList(); for (Transaction tx : Iterables.concat(account.getUnspentTransactions().values(), account.getPendingTransactions().values())) { // Do not try and spend coinbases that were mined too recently, the protocol forbids it. if (excludeImmatureCoinbases && !tx.isMature()) continue; for (TransactionOutput output : tx.getOutputs()) { if (!output.isAvailableForSpending()) continue; if (!output.isMine(account)) continue; candidates.add(output); } } // If we have pending transactions, remove from candidates any future spent outputs for (Transaction pendingTx : account.getPendingTransactions().values()) { for (TransactionInput input : pendingTx.getInputs()) { Transaction tx = account.getTransactions().get(input.getOutpoint().getHash()); if (tx == null) continue; TransactionOutput pendingSpentOutput = tx.getOutput((int) input.getOutpoint().getIndex()); candidates.remove(pendingSpentOutput); } } return candidates; } finally { lock.unlock(); } } private FeeCalculation calculateFee(SendRequest req, Coin value, List<TransactionInput> originalInputs, int numberOfSoftDustOutputs, LinkedList<TransactionOutput> candidates) throws InsufficientMoneyException { checkState(lock.isHeldByCurrentThread(), "Lock is held by another thread"); FeeCalculation result = new FeeCalculation(); // There are 3 possibilities for what adding change might do: // 1) No effect // 2) Causes increase in fee (change < 0.01 COINS) // 3) Causes the transaction to have a dust output or change < fee increase (ie change will be thrown away) // If we get either of the last 2, we keep note of what the inputs looked like at the time and try to // add inputs as we go up the list (keeping track of minimum inputs for each category). At the end, we pick // the best input set as the one which generates the lowest total fee. Coin additionalValueForNextCategory = null; CoinSelection selection3 = null; CoinSelection selection2 = null; TransactionOutput selection2Change = null; CoinSelection selection1 = null; TransactionOutput selection1Change = null; // We keep track of the last size of the transaction we calculated but only if the act of adding inputs and // change resulted in the size crossing a 1000 byte boundary. Otherwise it stays at zero. int lastCalculatedSize = 0; Coin valueNeeded, valueMissing = null; while (true) { resetTxInputs(req, originalInputs); Coin fees = req.fee == null ? Coin.ZERO : req.fee; if (lastCalculatedSize > 0 && coinType.getFeePolicy() == FeePolicy.FEE_PER_KB) { // If the size is exactly 1000 bytes then we'll over-pay, but this should be rare. fees = fees.add(req.feePerKb.multiply((lastCalculatedSize / 1000) + 1)); } else { fees = fees.add(req.feePerKb); // First time around the loop. } if (numberOfSoftDustOutputs > 0) { switch (coinType.getSoftDustPolicy()) { case AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT: if (fees.compareTo(req.feePerKb) < 0) { fees = req.feePerKb; } break; case BASE_FEE_FOR_EACH_SOFT_DUST_TXO: fees = fees.add(req.feePerKb.multiply(numberOfSoftDustOutputs)); break; case NO_POLICY: break; default: throw new RuntimeException("Unknown soft dust policy: " + coinType.getSoftDustPolicy()); } } valueNeeded = value.add(fees); if (additionalValueForNextCategory != null) valueNeeded = valueNeeded.add(additionalValueForNextCategory); Coin additionalValueSelected = additionalValueForNextCategory; // Of the coins we could spend, pick some that we actually will spend. CoinSelector selector = req.coinSelector == null ? coinSelector : req.coinSelector; // selector is allowed to modify candidates list. CoinSelection selection = selector.select(valueNeeded, new LinkedList<TransactionOutput>(candidates)); // Can we afford this? if (selection.valueGathered.compareTo(valueNeeded) < 0) { valueMissing = valueNeeded.subtract(selection.valueGathered); break; } checkState(!selection.gathered.isEmpty() || !originalInputs.isEmpty()); // We keep track of an upper bound on transaction size to calculate fees that need to be added. // Note that the difference between the upper bound and lower bound is usually small enough that it // will be very rare that we pay a fee we do not need to. // // We can't be sure a selection is valid until we check fee per kb at the end, so we just store // them here temporarily. boolean eitherCategory2Or3 = false; boolean isCategory3 = false; Coin change = selection.valueGathered.subtract(valueNeeded); if (additionalValueSelected != null) change = change.add(additionalValueSelected); // If change is a soft dust, we will need to have at least base fee to be accepted by the network if (req.ensureMinRequiredFee && !change.equals(Coin.ZERO) && change.compareTo(coinType.getSoftDustLimit()) < 0) { switch (coinType.getSoftDustPolicy()) { case AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT: if (fees.compareTo(req.feePerKb) < 0) { // This solution may fit into category 2, but it may also be category 3, we'll check that later eitherCategory2Or3 = true; additionalValueForNextCategory = coinType.getSoftDustLimit(); // If the change is smaller than the fee we want to add, this will be negative change = change.subtract(req.feePerKb.subtract(fees)); } break; case BASE_FEE_FOR_EACH_SOFT_DUST_TXO: // This solution may fit into category 2, but it may also be category 3, we'll check that later eitherCategory2Or3 = true; additionalValueForNextCategory = coinType.getSoftDustLimit(); // If the change is smaller than the fee we want to add, this will be negative change = change.subtract(req.feePerKb); break; case NO_POLICY: break; default: throw new RuntimeException("Unknown soft dust policy: " + coinType.getSoftDustPolicy()); } } int size = 0; TransactionOutput changeOutput = null; if (change.signum() > 0) { // The value of the inputs is greater than what we want to send. Just like in real life then, // we need to take back some coins ... this is called "change". Add another output that sends the change // back to us. The address comes either from the request or getChangeAddress() as a default. Address changeAddress = req.changeAddress; if (changeAddress == null) changeAddress = account.getChangeAddress(); changeOutput = new TransactionOutput(coinType, req.tx, change, changeAddress); // If the change output would result in this transaction being rejected as dust, just drop the change and make it a fee if (req.ensureMinRequiredFee && coinType.getMinNonDust().compareTo(change) >= 0) { // This solution definitely fits in category 3 isCategory3 = true; additionalValueForNextCategory = req.feePerKb.add( coinType.getMinNonDust().add(Coin.SATOSHI)); } else { size += changeOutput.bitcoinSerialize().length + VarInt.sizeOf(req.tx.getOutputs().size()) - VarInt.sizeOf(req.tx.getOutputs().size() - 1); // This solution is either category 1 or 2 if (!eitherCategory2Or3) // must be category 1 additionalValueForNextCategory = null; } } else { if (eitherCategory2Or3) { // This solution definitely fits in category 3 (we threw away change because it was smaller than MIN_TX_FEE) isCategory3 = true; additionalValueForNextCategory = req.feePerKb.add(Coin.SATOSHI); } } // Now add unsigned inputs for the selected coins. for (TransactionOutput output : selection.gathered) { TransactionInput input = req.tx.addInput(output); // If the scriptBytes don't default to none, our size calculations will be thrown off. checkState(input.getScriptBytes().length == 0); } // Estimate transaction size and loop again if we need more fee per kb. The serialized tx doesn't // include things we haven't added yet like input signatures/scripts or the change output. size += req.tx.bitcoinSerialize().length; size += estimateBytesForSigning(selection); if (size / 1000 > lastCalculatedSize / 1000 && req.feePerKb.signum() > 0) { lastCalculatedSize = size; // We need more fees anyway, just try again with the same additional value additionalValueForNextCategory = additionalValueSelected; continue; } if (isCategory3) { if (selection3 == null) selection3 = selection; } else if (eitherCategory2Or3) { // If we are in selection2, we will require at least CENT additional. If we do that, there is no way // we can end up back here because CENT additional will always get us to 1 checkState(selection2 == null); checkState(additionalValueForNextCategory.equals(coinType.getSoftDustLimit())); selection2 = selection; selection2Change = checkNotNull(changeOutput); // If we get no change in category 2, we are actually in category 3 } else { // Once we get a category 1 (change kept), we should break out of the loop because we can't do better checkState(selection1 == null); checkState(additionalValueForNextCategory == null); selection1 = selection; selection1Change = changeOutput; } if (additionalValueForNextCategory != null) { if (additionalValueSelected != null) checkState(additionalValueForNextCategory.compareTo(additionalValueSelected) > 0); continue; } break; } resetTxInputs(req, originalInputs); if (selection3 == null && selection2 == null && selection1 == null) { checkNotNull(valueMissing); log.warn("Insufficient value in wallet for send: needed {} more", valueMissing.toFriendlyString()); throw new InsufficientMoneyException(valueMissing); } Coin lowestFee = null; result.bestCoinSelection = null; result.bestChangeOutput = null; if (selection1 != null) { if (selection1Change != null) lowestFee = selection1.valueGathered.subtract(selection1Change.getValue()); else lowestFee = selection1.valueGathered; result.bestCoinSelection = selection1; result.bestChangeOutput = selection1Change; } if (selection2 != null) { Coin fee = selection2.valueGathered.subtract(checkNotNull(selection2Change).getValue()); if (lowestFee == null || fee.compareTo(lowestFee) < 0) { lowestFee = fee; result.bestCoinSelection = selection2; result.bestChangeOutput = selection2Change; } } if (selection3 != null) { if (lowestFee == null || selection3.valueGathered.compareTo(lowestFee) < 0) { result.bestCoinSelection = selection3; result.bestChangeOutput = null; } } return result; } /** * Reduce the value of the first output of a transaction to pay the given feePerKb as appropriate for its size. */ private boolean adjustOutputDownwardsForFee(Transaction tx, CoinSelection coinSelection, Coin baseFee, Coin feePerKb) { TransactionOutput output = tx.getOutput(0); // Check if we need additional fee due to the transaction's size int size = tx.bitcoinSerialize().length; size += estimateBytesForSigning(coinSelection); Coin fee; switch (coinType.getFeePolicy()) { case FEE_PER_KB: fee = baseFee.add(feePerKb.multiply((size / 1000) + 1)); break; case FLAT_FEE: fee = baseFee.add(feePerKb); break; default: throw new RuntimeException("Unknown fee policy: " + coinType.getFeePolicy()); } output.setValue(output.getValue().subtract(fee)); // Check if we need additional fee due to the output's value if (output.getValue().compareTo(coinType.getSoftDustLimit()) < 0) { switch (coinType.getSoftDustPolicy()) { case AT_LEAST_BASE_FEE_IF_SOFT_DUST_TXO_PRESENT: if (fee.compareTo(feePerKb) < 0) { output.setValue(output.getValue().subtract(feePerKb.subtract(fee))); } break; case BASE_FEE_FOR_EACH_SOFT_DUST_TXO: output.setValue(output.getValue().subtract(feePerKb)); break; case NO_POLICY: break; default: throw new RuntimeException("Unknown soft dust policy: " + coinType.getSoftDustPolicy()); } } return coinType.getMinNonDust().compareTo(output.getValue()) <= 0; } private int estimateBytesForSigning(CoinSelection selection) { int size = 0; for (TransactionOutput output : selection.gathered) { try { Script script = output.getScriptPubKey(); ECKey key = null; Script redeemScript = null; if (script.isSentToAddress()) { key = account.findKeyFromPubHash(script.getPubKeyHash()); if (key == null) { log.error("output.getIndex {}", output.getIndex()); log.error("output.getAddressFromP2SH {}", output.getAddressFromP2SH(coinType)); log.error("output.getAddressFromP2PKHScript {}", output.getAddressFromP2PKHScript(coinType)); log.error("output.getParentTransaction().getHash() {}", output.getParentTransaction().getHash()); } checkNotNull(key, "Coin selection includes unspendable outputs"); } else if (script.isPayToScriptHash()) { throw new ScriptException("Wallet does not currently support PayToScriptHash"); // redeemScript = keychain.findRedeemScriptFromPubHash(script.getPubKeyHash()); // checkNotNull(redeemScript, "Coin selection includes unspendable outputs"); } size += script.getNumberOfBytesRequiredToSpend(key, redeemScript); } catch (ScriptException e) { // If this happens it means an output script in a wallet tx could not be understood. That should never // happen, if it does it means the wallet has got into an inconsistent state. throw new IllegalStateException(e); } } return size; } private static void resetTxInputs(SendRequest req, List<TransactionInput> originalInputs) { req.tx.clearInputs(); for (TransactionInput input : originalInputs) req.tx.addInput(input); } }