lbcwallet/wallet/txauthor/author.go
Josh Rickmar f084802fec Refactor wallet transaction creation code.
This began as a change to improve the fee calculation code and evolved
into a much larger refactor which improves the readability and
modularity of all of the transaction creation code.

Transaction fee calculations have been switched from full increments
of the relay fee to a proportion based on the transaction size.  This
means that for a relay fee of 1e3 satoshis/kB, a 500 byte transaction
is only required to pay a 5e2 satoshi fee and a 1500 byte transaction
only need pay a 1.5e3 fee.  The previous code would end up estimating
these fees to be 1e3 and 2e3 respectively.

Because the previous code would add more fee than needed in almost
every case, the transaction size estimations were optimistic
(best/smallest case) and signing was done in a loop where the fee was
incremented by the relay fee again each time the actual size of the
signed transaction rendered the fee too low.  This has switched to
using worst case transaction size estimates rather than best case, and
signing is only performed once.

Transaction input signature creation has switched from using
txscript.SignatureScript to txscript.SignTxOutput.  The new API is
able to redeem outputs other than just P2PKH, so the previous
restrictions about P2SH outputs being unspendable (except through the
signrawtransaction RPC) no longer hold.

Several new public packages have been added:

wallet/txauthor - transaction authoring and signing
wallet/txfees - fee estimations and change output inclusion
wallet/txrules - simple consensus and mempool policy rule checks

Along with some internal packages:

wallet/internal/txsizes - transaction size estimation
internal/helpers - context free convenience functions

The txsizes package is internal as the estimations it provides are
specific for the algorithms used by these new packages.
2016-03-08 17:42:27 -05:00

200 lines
7.4 KiB
Go

// Copyright (c) 2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// Package txauthor provides transaction creation code for wallets.
package txauthor
import (
"errors"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/wallet/txrules"
h "github.com/btcsuite/btcwallet/internal/helpers"
"github.com/btcsuite/btcwallet/wallet/internal/txsizes"
)
// InputSource provides transaction inputs referencing spendable outputs to
// construct a transaction outputting some target amount. If the target amount
// can not be satisified, this can be signaled by returning a total amount less
// than the target or by returning a more detailed error implementing
// InputSourceError.
type InputSource func(target btcutil.Amount) (total btcutil.Amount, inputs []*wire.TxIn, scripts [][]byte, err error)
// InputSourceError describes the failure to provide enough input value from
// unspent transaction outputs to meet a target amount. A typed error is used
// so input sources can provide their own implementations describing the reason
// for the error, for example, due to spendable policies or locked coins rather
// than the wallet not having enough available input value.
type InputSourceError interface {
error
InputSourceError()
}
// Default implementation of InputSourceError.
type insufficientFundsError struct{}
func (insufficientFundsError) InputSourceError() {}
func (insufficientFundsError) Error() string {
return "insufficient funds available to construct transaction"
}
// AuthoredTx holds the state of a newly-created transaction and the change
// output (if one was added).
type AuthoredTx struct {
Tx *wire.MsgTx
PrevScripts [][]byte
TotalInput btcutil.Amount
ChangeIndex int // negative if no change
}
// ChangeSource provides P2PKH change output scripts for transaction creation.
type ChangeSource func() ([]byte, error)
// NewUnsignedTransaction creates an unsigned transaction paying to one or more
// non-change outputs. An appropriate transaction fee is included based on the
// transaction size.
//
// Transaction inputs are chosen from repeated calls to fetchInputs with
// increasing targets amounts.
//
// If any remaining output value can be returned to the wallet via a change
// output without violating mempool dust rules, a P2PKH change output is
// appended to the transaction outputs. Since the change output may not be
// necessary, fetchChange is called zero or one times to generate this script.
// This function must return a P2PKH script or smaller, otherwise fee estimation
// will be incorrect.
//
// If successful, the transaction, total input value spent, and all previous
// output scripts are returned. If the input source was unable to provide
// enough input value to pay for every output any any necessary fees, an
// InputSourceError is returned.
//
// BUGS: Fee estimation may be off when redeeming non-compressed P2PKH outputs.
func NewUnsignedTransaction(outputs []*wire.TxOut, relayFeePerKb btcutil.Amount,
fetchInputs InputSource, fetchChange ChangeSource) (*AuthoredTx, error) {
targetAmount := h.SumOutputValues(outputs)
estimatedSize := txsizes.EstimateSerializeSize(1, outputs, true)
targetFee := txrules.FeeForSerializeSize(relayFeePerKb, estimatedSize)
for {
inputAmount, inputs, scripts, err := fetchInputs(targetAmount + targetFee)
if err != nil {
return nil, err
}
if inputAmount < targetAmount+targetFee {
return nil, insufficientFundsError{}
}
maxSignedSize := txsizes.EstimateSerializeSize(len(inputs), outputs, true)
maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize)
remainingAmount := inputAmount - targetAmount
if remainingAmount < maxRequiredFee {
targetFee = maxRequiredFee
continue
}
unsignedTransaction := &wire.MsgTx{
Version: wire.TxVersion,
TxIn: inputs,
TxOut: outputs,
LockTime: 0,
}
changeIndex := -1
changeAmount := inputAmount - targetAmount - maxRequiredFee
if !txrules.IsDustAmount(changeAmount, txsizes.P2PKHPkScriptSize, relayFeePerKb) {
changeScript, err := fetchChange()
if err != nil {
return nil, err
}
if len(changeScript) > txsizes.P2PKHPkScriptSize {
return nil, errors.New("fee estimation requires change " +
"scripts no larger than P2PKH output scripts")
}
change := wire.NewTxOut(int64(changeAmount), changeScript)
l := len(outputs)
unsignedTransaction.TxOut = append(outputs[:l:l], change)
changeIndex = l
}
return &AuthoredTx{
Tx: unsignedTransaction,
PrevScripts: scripts,
TotalInput: inputAmount,
ChangeIndex: changeIndex,
}, nil
}
}
// RandomizeOutputPosition randomizes the position of a transaction's output by
// swapping it with a random output. The new index is returned. This should be
// done before signing.
func RandomizeOutputPosition(outputs []*wire.TxOut, index int) int {
r := cprng.Int31n(int32(len(outputs)))
outputs[r], outputs[index] = outputs[index], outputs[r]
return int(r)
}
// RandomizeChangePosition randomizes the position of an authored transaction's
// change output. This should be done before signing.
func (tx *AuthoredTx) RandomizeChangePosition() {
tx.ChangeIndex = RandomizeOutputPosition(tx.Tx.TxOut, tx.ChangeIndex)
}
// SecretsSource provides private keys and redeem scripts necessary for
// constructing transaction input signatures. Secrets are looked up by the
// corresponding Address for the previous output script. Addresses for lookup
// are created using the source's blockchain parameters and means a single
// SecretsSource can only manage secrets for a single chain.
//
// TODO: Rewrite this interface to look up private keys and redeem scripts for
// pubkeys, pubkey hashes, script hashes, etc. as separate interface methods.
// This would remove the ChainParams requirement of the interface and could
// avoid unnecessary conversions from previous output scripts to Addresses.
// This can not be done without modifications to the txscript package.
type SecretsSource interface {
txscript.KeyDB
txscript.ScriptDB
ChainParams() *chaincfg.Params
}
// AddAllInputScripts modifies transaction a transaction by adding inputs
// scripts for each input. Previous output scripts being redeemed by each input
// are passed in prevPkScripts and the slice length must match the number of
// inputs. Private keys and redeem scripts are looked up using a SecretsSource
// based on the previous output script.
func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, secrets SecretsSource) error {
inputs := tx.TxIn
chainParams := secrets.ChainParams()
if len(inputs) != len(prevPkScripts) {
return errors.New("tx.TxIn and prevPkScripts slices must " +
"have equal length")
}
for i := range inputs {
pkScript := prevPkScripts[i]
sigScript := inputs[i].SignatureScript
script, err := txscript.SignTxOutput(chainParams, tx, i,
pkScript, txscript.SigHashAll, secrets, secrets,
sigScript)
if err != nil {
return err
}
inputs[i].SignatureScript = script
}
return nil
}
// AddAllInputScripts modifies an authored transaction by adding inputs scripts
// for each input of an authored transaction. Private keys and redeem scripts
// are looked up using a SecretsSource based on the previous output script.
func (tx *AuthoredTx) AddAllInputScripts(secrets SecretsSource) error {
return AddAllInputScripts(tx.Tx, tx.PrevScripts, secrets)
}