com.mrd.bitlib.StandardTransactionBuilder.java Source code

Java tutorial

Introduction

Here is the source code for com.mrd.bitlib.StandardTransactionBuilder.java

Source

/*
 * Copyright 2013, 2014 Megion Research & Development GmbH
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mrd.bitlib;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.mrd.bitlib.crypto.BitcoinSigner;
import com.mrd.bitlib.crypto.IPrivateKeyRing;
import com.mrd.bitlib.crypto.IPublicKeyRing;
import com.mrd.bitlib.crypto.PublicKey;
import com.mrd.bitlib.model.*;
import com.mrd.bitlib.util.ByteWriter;
import com.mrd.bitlib.util.CoinUtil;
import com.mrd.bitlib.util.HashUtils;
import com.mrd.bitlib.util.Sha256Hash;

import java.io.Serializable;
import java.util.*;

public class StandardTransactionBuilder {

    // 1000sat per 1000Bytes, from https://github.com/bitcoin/bitcoin/blob/849a7e645323062878604589df97a1cd75517eb1/src/main.cpp#L78
    private static final long MIN_RELAY_FEE = 1000;
    // hash size 32 + output index size 4 + max. script size 140 + ? 1 + ? 4
    private static final int MAX_INPUT_SIZE = 32 + 4 + 140 + 1 + 4;
    // output value 8B + script length 1B + script 25B (always)
    private static final int OUTPUT_SIZE = 8 + 1 + 25;

    public static class InsufficientFundsException extends Exception {
        //todo consider refactoring this into a composite return value instead of an exception. it is not really "exceptional"
        private static final long serialVersionUID = 1L;

        public long sending;
        public long fee;

        public InsufficientFundsException(long sending, long fee) {
            super("Insufficient funds to send " + sending + " satoshis with fee " + fee);
            this.sending = sending;
            this.fee = fee;
        }

    }

    public static class OutputTooSmallException extends Exception {
        //todo consider refactoring this into a composite return value instead of an exception. it is not really "exceptional"
        private static final long serialVersionUID = 1L;

        public long value;

        public OutputTooSmallException(long value) {
            super("An output was added with a value of " + value
                    + " satoshis, which is smaller than the minimum accepted by the Bitcoin network");
        }

    }

    public static class UnableToBuildTransactionException extends Exception {
        private static final long serialVersionUID = 1L;

        public UnableToBuildTransactionException(String msg) {
            super(msg);
        }

    }

    public static class SigningRequest implements Serializable {
        private static final long serialVersionUID = 1L;

        // The public part of the key we will sign with
        public PublicKey publicKey;

        // The data to make a signature on. For transactions this is the
        // transaction hash
        public Sha256Hash toSign;

        public SigningRequest(PublicKey publicKey, Sha256Hash toSign) {
            this.publicKey = publicKey;
            this.toSign = toSign;
        }

    }

    public static class UnsignedTransaction implements Serializable {
        private static final long serialVersionUID = 1L;
        public static final int NO_SEQUENCE = -1;

        private TransactionOutput[] _outputs;
        private UnspentTransactionOutput[] _funding;
        private SigningRequest[] _signingRequests;
        private NetworkParameters _network;

        public TransactionOutput[] getOutputs() {
            return _outputs;
        }

        public UnspentTransactionOutput[] getFundingOutputs() {
            return _funding;
        }

        public UnsignedTransaction(List<TransactionOutput> outputs, List<UnspentTransactionOutput> funding,
                IPublicKeyRing keyRing, NetworkParameters network) {
            _network = network;
            _outputs = outputs.toArray(new TransactionOutput[outputs.size()]);
            _funding = funding.toArray(new UnspentTransactionOutput[funding.size()]);
            _signingRequests = new SigningRequest[_funding.length];

            // Create empty input scripts pointing at the right out points
            TransactionInput[] inputs = new TransactionInput[_funding.length];
            for (int i = 0; i < _funding.length; i++) {
                inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY,
                        getDefaultSequenceNumber());
            }

            // Create transaction with valid outputs and empty inputs
            Transaction transaction = new Transaction(1, inputs, _outputs, getLockTime());

            for (int i = 0; i < _funding.length; i++) {
                UnspentTransactionOutput f = _funding[i];

                // Make sure that we only work on standard output scripts
                if (!(f.script instanceof ScriptOutputStandard)) {
                    throw new RuntimeException("Unsupported script");
                }
                // Find the address of the funding
                byte[] addressBytes = ((ScriptOutputStandard) f.script).getAddressBytes();
                Address address = Address.fromStandardBytes(addressBytes, _network);

                // Find the key to sign with
                PublicKey publicKey = keyRing.findPublicKeyByAddress(address);
                if (publicKey == null) {
                    // This should not happen as we only work on outputs that we have
                    // keys for
                    throw new RuntimeException("Public key not found");
                }

                // Set the input script to the funding output script
                inputs[i].script = ScriptInput.fromOutputScript(_funding[i].script);

                // Calculate the transaction hash that has to be signed
                Sha256Hash hash = hashTransaction(transaction);

                // Set the input to the empty script again
                inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY);

                _signingRequests[i] = new SigningRequest(publicKey, hash);

            }
        }

        public SigningRequest[] getSignatureInfo() {
            return _signingRequests;
        }

        public long calculateFee() {
            long in = 0, out = 0;
            for (UnspentTransactionOutput funding : _funding) {
                in += funding.value;
            }
            for (TransactionOutput output : _outputs) {
                out += output.value;
            }
            return in - out;
        }

        public int getLockTime() {
            return 0;
        }

        public int getDefaultSequenceNumber() {
            return NO_SEQUENCE;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            String fee = CoinUtil.valueString(calculateFee(), false);
            sb.append(String.format("Fee: %s", fee)).append('\n');
            int max = Math.max(_funding.length, _outputs.length);
            for (int i = 0; i < max; i++) {
                UnspentTransactionOutput in = i < _funding.length ? _funding[i] : null;
                TransactionOutput out = i < _outputs.length ? _outputs[i] : null;
                String line;
                if (in != null && out != null) {
                    line = String.format("%36s %13s -> %36s %13s", getAddress(in.script, _network),
                            getValue(in.value), getAddress(out.script, _network), getValue(out.value));
                } else if (in != null) {
                    line = String.format("%36s %13s    %36s %13s", getAddress(in.script, _network),
                            getValue(in.value), "", "");
                } else if (out != null) {
                    line = String.format("%36s %13s    %36s %13s", "", "", getAddress(out.script, _network),
                            getValue(out.value));
                } else {
                    line = "";
                }
                sb.append(line).append('\n');
            }
            return sb.toString();
        }

        private String getAddress(ScriptOutput script, NetworkParameters network) {
            Address address = script.getAddress(network);
            if (address == null) {
                return "Unknown";
            }
            return address.toString();
        }

        private String getValue(Long value) {
            return String.format("(%s)", CoinUtil.valueString(value, false));
        }

    }

    private NetworkParameters _network;
    private List<TransactionOutput> _outputs;

    public StandardTransactionBuilder(NetworkParameters network) {
        _network = network;
        _outputs = new LinkedList<TransactionOutput>();
    }

    public void addOutput(Address sendTo, long value) throws OutputTooSmallException {
        addOutput(createOutput(sendTo, value, _network));
    }

    public void addOutput(TransactionOutput output) throws OutputTooSmallException {
        if (output.value < TransactionUtils.MINIMUM_OUTPUT_VALUE) {
            throw new OutputTooSmallException(output.value);
        }
        _outputs.add(output);
    }

    public void addOutputs(OutputList outputs) throws OutputTooSmallException {
        for (TransactionOutput output : outputs) {
            if (output.value > 0) {
                this.addOutput(output);
            }
        }
    }

    public static TransactionOutput createOutput(Address sendTo, long value, NetworkParameters network) {
        ScriptOutput script;
        if (sendTo.isMultisig(network)) {
            script = new ScriptOutputP2SH(sendTo.getTypeSpecificBytes());
        } else {
            script = new ScriptOutputStandard(sendTo.getTypeSpecificBytes());
        }
        return new TransactionOutput(value, script);
    }

    public static List<byte[]> generateSignatures(SigningRequest[] requests, IPrivateKeyRing keyRing) {
        List<byte[]> signatures = new LinkedList<byte[]>();
        for (SigningRequest request : requests) {
            BitcoinSigner signer = keyRing.findSignerByPublicKey(request.publicKey);
            if (signer == null) {
                // This should not happen as we only work on outputs that we have
                // keys for
                throw new RuntimeException("Private key not found");
            }
            byte[] signature = signer.makeStandardBitcoinSignature(request.toSign);
            signatures.add(signature);
        }
        return signatures;
    }

    /**
     * Create an unsigned transaction and automatically calculate the miner fee.
     * <p>
     * If null is specified as the change address the 'richest' address that is part of the funding is selected as the
     * change address. This way the change always goes to the address contributing most, and the change wil lbe less
     * than the contribution.
     *
     * @param inventory     The list of unspent transaction outputs that can be used as
     *                      funding
     * @param changeAddress The address to send any change to, can be null
     * @param keyRing       The public key ring matching the unspent outputs
     * @param network       The network we are working on
     * @param minerFeeToUse The miner fee to pay for every 1000 bytes of transaction size
     * @return An unsigned transaction or null if not enough funds were available
     * @throws InsufficientFundsException
     */
    public UnsignedTransaction createUnsignedTransaction(Collection<UnspentTransactionOutput> inventory,
            Address changeAddress, IPublicKeyRing keyRing, NetworkParameters network, long minerFeeToUse)
            throws InsufficientFundsException, UnableToBuildTransactionException {
        // Make a copy so we can mutate the list
        List<UnspentTransactionOutput> unspent = new LinkedList<UnspentTransactionOutput>(inventory);
        OldestOutputsFirst oldestOutputsFirst = new OldestOutputsFirst(minerFeeToUse, unspent);
        long fee = oldestOutputsFirst.getFee();
        long outputSum = oldestOutputsFirst.getOutputSum();
        List<UnspentTransactionOutput> funding = pruneRedundantOutputs(oldestOutputsFirst.getAllNeededFundings(),
                fee + outputSum);
        // the number of inputs might have changed - recalculate the fee
        fee = estimateFee(funding.size(), _outputs.size() + 1, minerFeeToUse);

        long found = 0;
        for (UnspentTransactionOutput output : funding) {
            found += output.value;
        }
        // We have fund all the funds we need
        long toSend = fee + outputSum;

        if (changeAddress == null) {
            // If no change address s specified, get the richest address from the
            // funding set
            changeAddress = extractRichest(funding, network);
        }

        // We have our funding, calculate change
        long change = found - toSend;

        // Get a copy of all outputs
        List<TransactionOutput> outputs = new LinkedList<TransactionOutput>(_outputs);

        if (change > 0) {
            // We have more funds than needed, add an output to our change address
            if (change >= TransactionUtils.MINIMUM_OUTPUT_VALUE) {
                // But only if the change is larger than the minimum output accepted
                // by the network
                TransactionOutput changeOutput = createOutput(changeAddress, change, _network);
                // Select a random position for our change so it is harder to analyze our addresses in the block chain.
                // It is OK to use the weak java Random class for this purpose.
                int position = new Random().nextInt(outputs.size() + 1);
                outputs.add(position, changeOutput);
                //} else {
                // The change output would be smaller than what the network would
                // accept. In this case we leave it be as a small increased miner
                // fee.
            }
        }

        UnsignedTransaction unsignedTransaction = new UnsignedTransaction(outputs, funding, keyRing, network);

        // check if we have a reasonable Fee or throw an error otherwise
        int estimateTransactionSize = estimateTransactionSize(unsignedTransaction.getFundingOutputs().length,
                unsignedTransaction.getOutputs().length);
        long calculatedFee = unsignedTransaction.calculateFee();
        float estimatedFeePerKb = (long) ((float) calculatedFee / ((float) estimateTransactionSize / 1000));

        // set a limit of 20mBtc/1000Bytes as absolute limit - it is very likely a bug in the fee estimator or transaction composer
        if (estimatedFeePerKb > Transaction.MAX_MINER_FEE_PER_KB) {
            throw new UnableToBuildTransactionException(String.format(Locale.getDefault(),
                    "Unreasonable high transaction fee of %s sat/1000Byte on a %d Bytes tx. Fee: %d sat, Suggested fee: %d sat",
                    estimatedFeePerKb, estimateTransactionSize, calculatedFee, minerFeeToUse));
        }

        return unsignedTransaction;
    }

    private List<UnspentTransactionOutput> pruneRedundantOutputs(List<UnspentTransactionOutput> funding,
            long outputSum) {
        List<UnspentTransactionOutput> largestToSmallest = Ordering.natural().reverse()
                .onResultOf(new Function<UnspentTransactionOutput, Comparable>() {
                    @Override
                    public Comparable apply(UnspentTransactionOutput input) {
                        return input.value;
                    }
                }).sortedCopy(funding);

        long target = 0;
        for (int i = 0; i < largestToSmallest.size(); i++) {
            UnspentTransactionOutput output = largestToSmallest.get(i);
            target += output.value;
            if (target >= outputSum) {

                List<UnspentTransactionOutput> ret = largestToSmallest.subList(0, i + 1);
                Collections.shuffle(ret);
                return ret;
            }
        }
        return largestToSmallest;
    }

    @VisibleForTesting
    Address extractRichest(Collection<UnspentTransactionOutput> unspent, final NetworkParameters network) {
        Preconditions.checkArgument(!unspent.isEmpty());
        Function<UnspentTransactionOutput, Address> txout2Address = new Function<UnspentTransactionOutput, Address>() {
            @Override
            public Address apply(UnspentTransactionOutput input) {
                return input.script.getAddress(network);
            }
        };
        Multimap<Address, UnspentTransactionOutput> index = Multimaps.index(unspent, txout2Address);
        Address ret = extractRichest(index);
        return Preconditions.checkNotNull(ret);
    }

    private Address extractRichest(Multimap<Address, UnspentTransactionOutput> index) {
        Address ret = null;
        long maxSum = 0;
        for (Address address : index.keys()) {
            Collection<UnspentTransactionOutput> unspentTransactionOutputs = index.get(address);
            long newSum = sum(unspentTransactionOutputs);
            if (newSum > maxSum) {
                ret = address;
            }
            maxSum = newSum;
        }
        return ret;
    }

    private long sum(Iterable<UnspentTransactionOutput> outputs) {
        long sum = 0;
        for (UnspentTransactionOutput output : outputs) {
            sum += output.value;
        }
        return sum;
    }

    public static Transaction finalizeTransaction(UnsignedTransaction unsigned, List<byte[]> signatures) {
        // Create finalized transaction inputs
        TransactionInput[] inputs = new TransactionInput[unsigned._funding.length];
        for (int i = 0; i < unsigned._funding.length; i++) {
            // Create script from signature and public key
            ScriptInputStandard script = new ScriptInputStandard(signatures.get(i),
                    unsigned._signingRequests[i].publicKey.getPublicKeyBytes());
            inputs[i] = new TransactionInput(unsigned._funding[i].outPoint, script,
                    unsigned.getDefaultSequenceNumber());
        }

        // Create transaction with valid outputs and empty inputs
        return new Transaction(1, inputs, unsigned._outputs, unsigned.getLockTime());
    }

    private UnspentTransactionOutput extractOldest(Collection<UnspentTransactionOutput> unspent) {
        // find the "oldest" output
        int minHeight = Integer.MAX_VALUE;
        UnspentTransactionOutput oldest = null;
        for (UnspentTransactionOutput output : unspent) {
            if (!(output.script instanceof ScriptOutputStandard)) {
                // only look for standard scripts
                continue;
            }

            // Unconfirmed outputs have height = -1 -> change this to Int.MAX-1, so that we
            // choose them as the last possible option
            int height = output.height > 0 ? output.height : Integer.MAX_VALUE - 1;

            if (height < minHeight) {
                minHeight = height;
                oldest = output;
            }
        }
        if (oldest == null) {
            // There were no outputs
            return null;
        }
        unspent.remove(oldest);
        return oldest;
    }

    private long outputSum() {
        long sum = 0;
        for (TransactionOutput output : _outputs) {
            sum += output.value;
        }
        return sum;
    }

    private static Sha256Hash hashTransaction(Transaction t) {
        ByteWriter writer = new ByteWriter(1024);
        t.toByteWriter(writer);
        // We also have to write a hash type.
        int hashType = 1;
        writer.putIntLE(hashType);
        // Note that this is NOT reversed to ensure it will be signed
        // correctly. If it were to be printed out
        // however then we would expect that it is IS reversed.
        return HashUtils.doubleSha256(writer.toBytes());
    }

    /**
     * Estimate the size of a transaction by taking the number of inputs and outputs into account. This allows us to
     * give a good estimate of the final transaction size, and determine whether out fee size is large enough.
     *
     * @param inputs  the number of inputs of the transaction
     * @param outputs the number of outputs of a transaction
     * @return The estimated transaction size
     */
    private static int estimateTransactionSize(int inputs, int outputs) {
        int estimate = 0;
        estimate += 4; // Version info
        estimate += CompactInt.toBytes(inputs).length; // num input encoding. Usually 1. >253 inputs -> 3
        estimate += MAX_INPUT_SIZE * inputs;
        estimate += CompactInt.toBytes(outputs).length; // num output encoding. Usually 1. >253 outputs -> 3
        estimate += OUTPUT_SIZE * outputs;
        estimate += 4; // nLockTime
        return estimate;
    }

    /**
     * Returns the estimate needed fee in satoshis for a default P2PKH transaction with a certain number
     * of inputs and outputs and the specified per-kB-fee
     *
     * @param inputs  number of inputs
     * @param outputs number of outputs
     * @param minerFeePerKb miner fee in satoshis per kB
     **/
    public static long estimateFee(int inputs, int outputs, long minerFeePerKb) {
        // fee is based on the size of the transaction, we have to pay for
        // every 1000 bytes
        int txSize = estimateTransactionSize(inputs, outputs);
        long requiredFee = (long) (((float) txSize / 1000.0) * minerFeePerKb);

        // check if our estimation leads to a small fee that's below the default bitcoind-MIN_RELAY_FEE
        // if so, use the MIN_RELAY_FEE
        if (requiredFee < MIN_RELAY_FEE) {
            requiredFee = MIN_RELAY_FEE;
        }

        return requiredFee;
    }

    // TODO: generalize this into an interface and provide different coin-selectors
    private class OldestOutputsFirst {
        private List<UnspentTransactionOutput> allFunding;
        private long fee;
        private long outputSum;

        public OldestOutputsFirst(long minerFeeToUse, List<UnspentTransactionOutput> unspent)
                throws InsufficientFundsException {
            // Find the funding for this transaction
            allFunding = new LinkedList<UnspentTransactionOutput>();
            fee = minerFeeToUse;
            outputSum = outputSum();
            long found = 0;
            while (found < fee + outputSum) {
                UnspentTransactionOutput output = extractOldest(unspent);
                if (output == null) {
                    // We do not have enough funds
                    throw new InsufficientFundsException(outputSum, fee);
                }
                found += output.value;
                allFunding.add(output);
                // When we estimate the fee we automatically add an extra output for an eventual change output.
                // This slightly increases the change for paying a little extra, but adding change is the norm
                fee = estimateFee(allFunding.size(), _outputs.size() + 1, minerFeeToUse);
            }
        }

        public List<UnspentTransactionOutput> getAllNeededFundings() {
            return allFunding;
        }

        public long getFee() {
            return fee;
        }

        public long getOutputSum() {
            return outputSum;
        }

    }
}