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.
This commit is contained in:
Josh Rickmar 2016-02-27 23:30:56 -05:00
parent 82e743754f
commit f084802fec
13 changed files with 873 additions and 794 deletions

View file

@ -26,7 +26,6 @@ const (
defaultLogLevel = "info"
defaultLogDirname = "logs"
defaultLogFilename = "btcwallet.log"
defaultDisallowFree = false
defaultRPCMaxClients = 10
defaultRPCMaxWebsockets = 25
@ -59,8 +58,7 @@ type config struct {
Profile string `long:"profile" description:"Enable HTTP profiling on given port -- NOTE port must be between 1024 and 65536"`
// Wallet options
WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"`
DisallowFree bool `long:"disallowfree" description:"Force transactions to always include a fee"`
WalletPass string `long:"walletpass" default-mask:"-" description:"The public wallet password -- Only required if the wallet was created with one"`
// RPC client options
RPCConnect string `short:"c" long:"rpcconnect" description:"Hostname/IP and port of btcd RPC server to connect to (default localhost:18334, mainnet: localhost:8334, simnet: localhost:18556)"`
@ -97,7 +95,8 @@ type config struct {
ExperimentalRPCListeners []string `long:"experimentalrpclisten" description:"Listen for RPC connections on this interface/port"`
// Deprecated options
KeypoolSize uint `short:"k" long:"keypoolsize" description:"DEPRECATED -- Maximum number of addresses in keypool"`
DisallowFree bool `long:"disallowfree" description:"DEPRECATED -- Force transactions to always include a fee"`
KeypoolSize uint `short:"k" long:"keypoolsize" description:"DEPRECATED -- Maximum number of addresses in keypool"`
}
// cleanAndExpandPath expands environement variables and leading ~ in the
@ -221,7 +220,6 @@ func loadConfig() (*config, []string, error) {
WalletPass: wallet.InsecurePubPassphrase,
RPCKey: defaultRPCKeyFile,
RPCCert: defaultRPCCertFile,
DisallowFree: defaultDisallowFree,
LegacyRPCMaxClients: defaultRPCMaxClients,
LegacyRPCMaxWebsockets: defaultRPCMaxWebsockets,
}

View file

@ -0,0 +1,26 @@
// 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 helpers provides convenience functions to simplify wallet code. This
// package is intended for internal wallet use only.
package helpers
import (
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
func SumOutputValues(outputs []*wire.TxOut) (totalOutput btcutil.Amount) {
for _, txOut := range outputs {
totalOutput += btcutil.Amount(txOut.Value)
}
return totalOutput
}
func SumOutputSerializeSizes(outputs []*wire.TxOut) (serializeSize int) {
for _, txOut := range outputs {
serializeSize += txOut.SerializeSize()
}
return serializeSize
}

View file

@ -24,6 +24,7 @@ import (
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/wtxmgr"
)
@ -511,7 +512,7 @@ func GetInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClient) (
// to using the manager version.
info.WalletVersion = int32(waddrmgr.LatestMgrVersion)
info.Balance = bal.ToBTC()
info.PaytxFee = w.FeeIncrement.ToBTC()
info.PaytxFee = w.RelayFee.ToBTC()
// We don't set the following since they don't make much sense in the
// wallet architecture:
// - unlocked_until
@ -1384,14 +1385,40 @@ func LockUnspent(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
return true, nil
}
// makeOutputs creates a slice of transaction outputs from a pair of address
// strings to amounts. This is used to create the outputs to include in newly
// created transactions from a JSON object describing the output destinations
// and amounts.
func makeOutputs(pairs map[string]btcutil.Amount, chainParams *chaincfg.Params) ([]*wire.TxOut, error) {
outputs := make([]*wire.TxOut, 0, len(pairs))
for addrStr, amt := range pairs {
addr, err := btcutil.DecodeAddress(addrStr, chainParams)
if err != nil {
return nil, fmt.Errorf("cannot decode address: %s", err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return nil, fmt.Errorf("cannot create txout script: %s", err)
}
outputs = append(outputs, wire.NewTxOut(int64(amt), pkScript))
}
return outputs, nil
}
// sendPairs creates and sends payment transactions.
// It returns the transaction hash in string format upon success
// All errors are returned in btcjson.RPCError format
func sendPairs(w *wallet.Wallet, amounts map[string]btcutil.Amount,
account uint32, minconf int32) (string, error) {
txSha, err := w.SendPairs(amounts, account, minconf)
outputs, err := makeOutputs(amounts, w.ChainParams())
if err != nil {
if err == wallet.ErrNonPositiveAmount {
return "", err
}
txSha, err := w.SendOutputs(outputs, account, minconf)
if err != nil {
if err == txrules.ErrAmountNegative {
return "", ErrNeedPositiveAmount
}
if waddrmgr.IsError(err, waddrmgr.ErrLocked) {
@ -1549,7 +1576,7 @@ func SetTxFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) {
if err != nil {
return nil, err
}
w.FeeIncrement = incr
w.RelayFee = incr
// A boolean true result is returned upon success.
return true, nil

View file

@ -1,125 +1,99 @@
// Copyright (c) 2013-2015 The btcsuite developers
// Copyright (c) 2013-2016 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package wallet
import (
"errors"
"fmt"
badrand "math/rand"
"sort"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wtxmgr"
)
const (
// All transactions have 4 bytes for version, 4 bytes of locktime,
// and 2 varints for the number of inputs and outputs.
txOverheadEstimate = 4 + 4 + 1 + 1
// byAmount defines the methods needed to satisify sort.Interface to
// sort credits by their output amount.
type byAmount []wtxmgr.Credit
// A best case signature script to redeem a P2PKH output for a
// compressed pubkey has 70 bytes of the smallest possible DER signature
// (with no leading 0 bytes for R and S), 33 bytes of serialized pubkey,
// and data push opcodes for both, plus one byte for the hash type flag
// appended to the end of the signature.
sigScriptEstimate = 1 + 70 + 1 + 33 + 1
func (s byAmount) Len() int { return len(s) }
func (s byAmount) Less(i, j int) bool { return s[i].Amount < s[j].Amount }
func (s byAmount) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// A best case tx input serialization cost is 32 bytes of sha, 4 bytes
// of output index, 4 bytes of sequnce, and the estimated signature
// script size.
txInEstimate = 32 + 4 + 4 + sigScriptEstimate
func makeInputSource(eligible []wtxmgr.Credit) txauthor.InputSource {
// Pick largest outputs first. This is only done for compatibility with
// previous tx creation code, not because it's a good idea.
sort.Sort(sort.Reverse(byAmount(eligible)))
// A P2PKH pkScript contains the following bytes:
// - OP_DUP
// - OP_HASH160
// - OP_DATA_20 + 20 bytes of pubkey hash
// - OP_EQUALVERIFY
// - OP_CHECKSIG
pkScriptEstimate = 1 + 1 + 1 + 20 + 1 + 1
// Current inputs and their total value. These are closed over by the
// returned input source and reused across multiple calls.
currentTotal := btcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(eligible))
currentScripts := make([][]byte, 0, len(eligible))
// A best case tx output serialization cost is 8 bytes of value, one
// byte of varint, and the pkScript size.
txOutEstimate = 8 + 1 + pkScriptEstimate
)
func estimateTxSize(numInputs, numOutputs int) int {
return txOverheadEstimate + txInEstimate*numInputs + txOutEstimate*numOutputs
}
func feeForSize(incr btcutil.Amount, sz int) btcutil.Amount {
return btcutil.Amount(1+sz/1000) * incr
}
// InsufficientFundsError represents an error where there are not enough
// funds from unspent tx outputs for a wallet to create a transaction.
// This may be caused by not enough inputs for all of the desired total
// transaction output amount, or due to
type InsufficientFundsError struct {
in, out, fee btcutil.Amount
}
// Error satisifies the builtin error interface.
func (e InsufficientFundsError) Error() string {
total := e.out + e.fee
if e.fee == 0 {
return fmt.Sprintf("insufficient funds: transaction requires "+
"%s input but only %v spendable", total, e.in)
return func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn, [][]byte, error) {
for currentTotal < target && len(eligible) != 0 {
nextCredit := &eligible[0]
eligible = eligible[1:]
nextInput := wire.NewTxIn(&nextCredit.OutPoint, nil)
currentTotal += nextCredit.Amount
currentInputs = append(currentInputs, nextInput)
currentScripts = append(currentScripts, nextCredit.PkScript)
}
return currentTotal, currentInputs, currentScripts, nil
}
return fmt.Sprintf("insufficient funds: transaction requires %s input "+
"(%v output + %v fee) but only %v spendable", total, e.out,
e.fee, e.in)
}
// ErrUnsupportedTransactionType represents an error where a transaction
// cannot be signed as the API only supports spending P2PKH outputs.
var ErrUnsupportedTransactionType = errors.New("Only P2PKH transactions are supported")
// ErrNonPositiveAmount represents an error where a bitcoin amount is
// not positive (either negative, or zero).
var ErrNonPositiveAmount = errors.New("amount is not positive")
// ErrNegativeFee represents an error where a fee is erroneously
// negative.
var ErrNegativeFee = errors.New("fee is negative")
// defaultFeeIncrement is the default minimum transation fee (0.00001 BTC,
// measured in satoshis) added to transactions requiring a fee.
const defaultFeeIncrement = 1e3
// CreatedTx holds the state of a newly-created transaction and the change
// output (if one was added).
type CreatedTx struct {
MsgTx *wire.MsgTx
ChangeAddr btcutil.Address
ChangeIndex int // negative if no change
Fee btcutil.Amount
// secretSource is an implementation of txauthor.SecretSource for the wallet's
// address manager.
type secretSource struct {
*waddrmgr.Manager
}
// ByAmount defines the methods needed to satisify sort.Interface to
// sort a slice of Utxos by their amount.
type ByAmount []wtxmgr.Credit
func (s secretSource) GetKey(addr btcutil.Address) (*btcec.PrivateKey, bool, error) {
ma, err := s.Address(addr)
if err != nil {
return nil, false, err
}
mpka, ok := ma.(waddrmgr.ManagedPubKeyAddress)
if !ok {
e := fmt.Errorf("managed address type for %v is `%T` but "+
"want waddrmgr.ManagedPubKeyAddress", addr, ma)
return nil, false, e
}
privKey, err := mpka.PrivKey()
if err != nil {
return nil, false, err
}
return privKey, ma.Compressed(), nil
}
func (u ByAmount) Len() int { return len(u) }
func (u ByAmount) Less(i, j int) bool { return u[i].Amount < u[j].Amount }
func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
// txToPairs creates a raw transaction sending the amounts for each
// address/amount pair and fee to each address and the miner. minconf
// specifies the minimum number of confirmations required before an
// unspent output is eligible for spending. Leftover input funds not sent
// to addr or as a fee for the miner are sent to a newly generated
// address. InsufficientFundsError is returned if there are not enough
// eligible unspent outputs to create the transaction.
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minconf int32) (*CreatedTx, error) {
func (s secretSource) GetScript(addr btcutil.Address) ([]byte, error) {
ma, err := s.Address(addr)
if err != nil {
return nil, err
}
msa, ok := ma.(waddrmgr.ManagedScriptAddress)
if !ok {
e := fmt.Errorf("managed address type for %v is `%T` but "+
"want waddrmgr.ManagedScriptAddress", addr, ma)
return nil, e
}
return msa.Script()
}
// txToOutputs creates a signed transaction which includes each output from
// outputs. Previous outputs to reedeem are chosen from the passed account's
// UTXO set and minconf policy. An additional output may be added to return
// change to the wallet. An appropriate fee is included based on the wallet's
// current relay fee. The wallet must be unlocked to create the transaction.
func (w *Wallet) txToOutputs(outputs []*wire.TxOut, account uint32, minconf int32) (*txauthor.AuthoredTx, error) {
// Address manager must be unlocked to compose transaction. Grab
// the unlock if possible (to prevent future unlocks), or return the
// error if already locked.
@ -145,182 +119,51 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, account uint32, minc
return nil, err
}
return createTx(eligible, pairs, bs, w.FeeIncrement, w.Manager, account, w.NewChangeAddress, w.chainParams, w.DisallowFree)
}
// createTx selects inputs (from the given slice of eligible utxos)
// whose amount are sufficient to fulfil all the desired outputs plus
// the mining fee. It then creates and returns a CreatedTx containing
// the selected inputs and the given outputs, validating it (using
// validateMsgTx) as well.
func createTx(eligible []wtxmgr.Credit,
outputs map[string]btcutil.Amount, bs *waddrmgr.BlockStamp,
feeIncrement btcutil.Amount, mgr *waddrmgr.Manager, account uint32,
changeAddress func(account uint32) (btcutil.Address, error),
chainParams *chaincfg.Params, disallowFree bool) (*CreatedTx, error) {
msgtx := wire.NewMsgTx()
minAmount, err := addOutputs(msgtx, outputs, chainParams)
if err != nil {
return nil, err
}
// Sort eligible inputs so that we first pick the ones with highest
// amount, thus reducing number of inputs.
sort.Sort(sort.Reverse(ByAmount(eligible)))
// Start by adding enough inputs to cover for the total amount of all
// desired outputs.
var input wtxmgr.Credit
var inputs []wtxmgr.Credit
totalAdded := btcutil.Amount(0)
for totalAdded < minAmount {
if len(eligible) == 0 {
return nil, InsufficientFundsError{totalAdded, minAmount, 0}
inputSource := makeInputSource(eligible)
changeSource := func() ([]byte, error) {
// Derive the change output script. As a hack to allow spending from
// the imported account, change addresses are created from account 0.
var changeAddr btcutil.Address
if account == waddrmgr.ImportedAddrAccount {
changeAddr, err = w.NewChangeAddress(0)
} else {
changeAddr, err = w.NewChangeAddress(account)
}
input, eligible = eligible[0], eligible[1:]
inputs = append(inputs, input)
msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil))
totalAdded += input.Amount
}
// Get an initial fee estimate based on the number of selected inputs
// and added outputs, with no change.
szEst := estimateTxSize(len(inputs), len(msgtx.TxOut))
feeEst := minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height, disallowFree)
// Now make sure the sum amount of all our inputs is enough for the
// sum amount of all outputs plus the fee. If necessary we add more,
// inputs, but in that case we also need to recalculate the fee.
for totalAdded < minAmount+feeEst {
if len(eligible) == 0 {
return nil, InsufficientFundsError{totalAdded, minAmount, feeEst}
}
input, eligible = eligible[0], eligible[1:]
inputs = append(inputs, input)
msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil))
szEst += txInEstimate
totalAdded += input.Amount
feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height, disallowFree)
}
// If we're spending the outputs of an imported address, we default
// to generating change addresses from the default account.
prevAccount := account
if account == waddrmgr.ImportedAddrAccount {
account = waddrmgr.DefaultAccountNum
}
var changeAddr btcutil.Address
// changeIdx is -1 unless there's a change output.
changeIdx := -1
for {
change := totalAdded - minAmount - feeEst
if change > 0 {
if changeAddr == nil {
changeAddr, err = changeAddress(account)
if err != nil {
return nil, err
}
}
changeIdx, err = addChange(msgtx, change, changeAddr)
if err != nil {
return nil, err
}
}
if err = signMsgTx(msgtx, inputs, mgr, chainParams); err != nil {
if err != nil {
return nil, err
}
if feeForSize(feeIncrement, msgtx.SerializeSize()) <= feeEst {
if change > 0 && prevAccount == waddrmgr.ImportedAddrAccount {
log.Warnf("Spend from imported account produced change: moving"+
" %v from imported account into default account.", change)
}
// The required fee for this size is less than or equal to what
// we guessed, so we're done.
break
}
if change > 0 {
// Remove the change output since the next iteration will add
// it again (with a new amount) if necessary.
tmp := msgtx.TxOut[:changeIdx]
tmp = append(tmp, msgtx.TxOut[changeIdx+1:]...)
msgtx.TxOut = tmp
}
feeEst += feeIncrement
for totalAdded < minAmount+feeEst {
if len(eligible) == 0 {
return nil, InsufficientFundsError{totalAdded, minAmount, feeEst}
}
input, eligible = eligible[0], eligible[1:]
inputs = append(inputs, input)
msgtx.AddTxIn(wire.NewTxIn(&input.OutPoint, nil))
szEst += txInEstimate
totalAdded += input.Amount
feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height, disallowFree)
}
return txscript.PayToAddrScript(changeAddr)
}
if err := validateMsgTx(msgtx, inputs); err != nil {
tx, err := txauthor.NewUnsignedTransaction(outputs, w.RelayFee,
inputSource, changeSource)
if err != nil {
return nil, err
}
info := &CreatedTx{
MsgTx: msgtx,
ChangeAddr: changeAddr,
ChangeIndex: changeIdx,
Fee: feeEst, // Last estimate is the actual fee
// Randomize change position, if change exists, before signing. This
// doesn't affect the serialize size, so the change amount will still be
// valid.
if tx.ChangeIndex >= 0 {
tx.RandomizeChangePosition()
}
return info, nil
}
// addChange adds a new output with the given amount and address, and
// randomizes the index (and returns it) of the newly added output.
func addChange(msgtx *wire.MsgTx, change btcutil.Amount, changeAddr btcutil.Address) (int, error) {
pkScript, err := txscript.PayToAddrScript(changeAddr)
err = tx.AddAllInputScripts(secretSource{w.Manager})
if err != nil {
return 0, fmt.Errorf("cannot create txout script: %s", err)
return nil, err
}
msgtx.AddTxOut(wire.NewTxOut(int64(change), pkScript))
// Randomize index of the change output.
rng := badrand.New(badrand.NewSource(time.Now().UnixNano()))
r := rng.Int31n(int32(len(msgtx.TxOut))) // random index
c := len(msgtx.TxOut) - 1 // change index
msgtx.TxOut[r], msgtx.TxOut[c] = msgtx.TxOut[c], msgtx.TxOut[r]
return int(r), nil
}
// addOutputs adds the given address/amount pairs as outputs to msgtx,
// returning their total amount.
func addOutputs(msgtx *wire.MsgTx, pairs map[string]btcutil.Amount, chainParams *chaincfg.Params) (btcutil.Amount, error) {
var minAmount btcutil.Amount
for addrStr, amt := range pairs {
if amt <= 0 {
return minAmount, ErrNonPositiveAmount
}
minAmount += amt
addr, err := btcutil.DecodeAddress(addrStr, chainParams)
if err != nil {
return minAmount, fmt.Errorf("cannot decode address: %s", err)
}
// Add output to spend amt to addr.
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
return minAmount, fmt.Errorf("cannot create txout script: %s", err)
}
txout := wire.NewTxOut(int64(amt), pkScript)
msgtx.AddTxOut(txout)
err = validateMsgTx(tx.Tx, tx.PrevScripts)
if err != nil {
return nil, err
}
return minAmount, nil
if tx.ChangeIndex >= 0 && account == waddrmgr.ImportedAddrAccount {
changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value)
log.Warnf("Spend from imported account produced change: moving"+
" %v from imported account into default account.", changeAmount)
}
return tx, nil
}
func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) {
@ -356,20 +199,16 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr
continue
}
// Filter out unspendable outputs, that is, remove those that
// (at this time) are not P2PKH outputs. Other inputs must be
// manually included in transactions and sent (for example,
// using createrawtransaction, signrawtransaction, and
// sendrawtransaction).
class, addrs, _, err := txscript.ExtractPkScriptAddrs(
// Only include the output if it is associated with the passed
// account.
//
// TODO: Handle multisig outputs by determining if enough of the
// addresses are controlled.
_, addrs, _, err := txscript.ExtractPkScriptAddrs(
output.PkScript, w.chainParams)
if err != nil || class != txscript.PubKeyHashTy {
if err != nil || len(addrs) != 1 {
continue
}
// Only include the output if it is associated with the passed
// account. There should only be one address since this is a
// P2PKH script.
addrAcct, err := w.Manager.AddrAccount(addrs[0])
if err != nil || addrAcct != account {
continue
@ -380,122 +219,20 @@ func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr
return eligible, nil
}
// signMsgTx sets the SignatureScript for every item in msgtx.TxIn.
// It must be called every time a msgtx is changed.
// Only P2PKH outputs are supported at this point.
func signMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit, mgr *waddrmgr.Manager, chainParams *chaincfg.Params) error {
if len(prevOutputs) != len(msgtx.TxIn) {
return fmt.Errorf(
"Number of prevOutputs (%d) does not match number of tx inputs (%d)",
len(prevOutputs), len(msgtx.TxIn))
}
for i, output := range prevOutputs {
// Errors don't matter here, as we only consider the
// case where len(addrs) == 1.
_, addrs, _, _ := txscript.ExtractPkScriptAddrs(output.PkScript,
chainParams)
if len(addrs) != 1 {
continue
}
apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash)
if !ok {
return ErrUnsupportedTransactionType
}
ai, err := mgr.Address(apkh)
if err != nil {
return fmt.Errorf("cannot get address info: %v", err)
}
pka := ai.(waddrmgr.ManagedPubKeyAddress)
privkey, err := pka.PrivKey()
if err != nil {
return fmt.Errorf("cannot get private key: %v", err)
}
sigscript, err := txscript.SignatureScript(msgtx, i,
output.PkScript, txscript.SigHashAll, privkey,
ai.Compressed())
if err != nil {
return fmt.Errorf("cannot create sigscript: %s", err)
}
msgtx.TxIn[i].SignatureScript = sigscript
}
return nil
}
func validateMsgTx(msgtx *wire.MsgTx, prevOutputs []wtxmgr.Credit) error {
for i := range msgtx.TxIn {
vm, err := txscript.NewEngine(prevOutputs[i].PkScript,
msgtx, i, txscript.StandardVerifyFlags, nil)
// validateMsgTx verifies transaction input scripts for tx. All previous output
// scripts from outputs redeemed by the transaction, in the same order they are
// spent, must be passed in the prevScripts slice.
func validateMsgTx(tx *wire.MsgTx, prevScripts [][]byte) error {
for i, prevScript := range prevScripts {
vm, err := txscript.NewEngine(prevScript, tx, i,
txscript.StandardVerifyFlags, nil)
if err != nil {
return fmt.Errorf("cannot create script engine: %s", err)
}
if err = vm.Execute(); err != nil {
err = vm.Execute()
if err != nil {
return fmt.Errorf("cannot validate transaction: %s", err)
}
}
return nil
}
// minimumFee estimates the minimum fee required for a transaction.
// If cfg.DisallowFree is false, a fee may be zero so long as txLen
// s less than 1 kilobyte and none of the outputs contain a value
// less than 1 bitcent. Otherwise, the fee will be calculated using
// incr, incrementing the fee for each kilobyte of transaction.
func minimumFee(incr btcutil.Amount, txLen int, outputs []*wire.TxOut, prevOutputs []wtxmgr.Credit, height int32, disallowFree bool) btcutil.Amount {
allowFree := false
if !disallowFree {
allowFree = allowNoFeeTx(height, prevOutputs, txLen)
}
fee := feeForSize(incr, txLen)
if allowFree && txLen < 1000 {
fee = 0
}
if fee < incr {
for _, txOut := range outputs {
if txOut.Value < btcutil.SatoshiPerBitcent {
return incr
}
}
}
// How can fee be smaller than 0 here?
if fee < 0 || fee > btcutil.MaxSatoshi {
fee = btcutil.MaxSatoshi
}
return fee
}
// allowNoFeeTx calculates the transaction priority and checks that the
// priority reaches a certain threshold. If the threshhold is
// reached, a free transaction fee is allowed.
func allowNoFeeTx(curHeight int32, txouts []wtxmgr.Credit, txSize int) bool {
const blocksPerDayEstimate = 144.0
const txSizeEstimate = 250.0
const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate
var weightedSum int64
for _, txout := range txouts {
depth := chainDepth(txout.Height, curHeight)
weightedSum += int64(txout.Amount) * int64(depth)
}
priority := float64(weightedSum) / float64(txSize)
return priority > threshold
}
// chainDepth returns the chaindepth of a target given the current
// blockchain height.
func chainDepth(target, current int32) int32 {
if target == -1 {
// target is not yet in a block.
return 0
}
// target is in a block.
return current - target + 1
}

View file

@ -1,248 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
package wallet
import (
"encoding/hex"
"os"
"path/filepath"
"reflect"
"sort"
"testing"
"time"
"github.com/btcsuite/btcd/blockchain"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/walletdb"
_ "github.com/btcsuite/btcwallet/walletdb/bdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
// This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys.
// It contains 6 outputs, in this order, with the following values/addresses:
// {0: 0.2283 (addr: myVT6o4GfR57Cfw7pP3vayfHZzMHh2BxXJ - change),
// 1: 0.03 (addr: mjqnv9JoxdYyQK7NMZGCKLxNWHfA6XFVC7),
// 2: 0.09 (addr: mqi4izJxVr9wRJmoHe3CUjdb7YDzpJmTwr),
// 3: 0.1 (addr: mu7q5vxiGCXYKXEtvspP77bYxjnsEobJGv),
// 4: 0.15 (addr: mw66YGmegSNv3yfS4brrtj6ZfAZ4DMmhQN),
// 5: 0.001 (addr: mgLBkENLdGXXMfu5RZYPuhJdC88UgvsAxY)}
var txInfo = struct {
hex string
amount btcutil.Amount
privKeys []string
}{
hex: "010000000113918955c6ba3c7a2e8ec02ca3e91a2571cb11ade7d5c3e9c1a73b3ac8309d74000000006b483045022100a6f33d4ad476d126ee45e19e43190971e148a1e940abe4165bc686d22ac847e502200936efa4da4225787d4b7e11e8f3389dba626817d7ece0cab38b4f456b0880d6012103ccb8b1038ad6af10a15f68e8d5e347c08befa6cc2ab1718a37e3ea0e38102b92ffffffff06b05b5c01000000001976a914c5297a660cef8088b8472755f4827df7577c612988acc0c62d00000000001976a9142f7094083d750bdfc1f2fad814779e2dde35ce2088ac40548900000000001976a9146fcb336a187619ca20b84af9eac9fbff68d1061d88ac80969800000000001976a91495322d12e18345f4855cbe863d4a8ebcc0e95e0188acc0e1e400000000001976a914aace7f06f94fa298685f6e58769543993fa5fae888aca0860100000000001976a91408eec7602655fdb2531f71070cca4c363c3a15ab88ac00000000",
amount: btcutil.Amount(3e6 + 9e6 + 1e7 + 1.5e7 + 1e5),
privKeys: []string{
"cSYUVdPL6pkabu7Fxp4PaKqYjJFz2Aopw5ygunFbek9HAimLYxp4",
"cVnNzZm3DiwkN1Ghs4W8cwcJC9f6TynCCcqzYt8n1c4hwjN2PfTw",
"cUgo8PrKj7NzttKRMKwgF3ahXNrLA253pqjWkPGS7Z9iZcKT8EKG",
"cSosEHx1freK7B1B6QicPcrH1h5VqReSHew6ZYhv6ntiUJRhowRc",
"cR9ApAZ3FLtRMfqRBEr3niD9Mmmvfh3V8Uh56qfJ5b4bFH8ibDkA"}}
var (
outAddr1 = "1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX"
outAddr2 = "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG"
)
// fastScrypt are options to passed to the wallet address manager to speed up
// the scrypt derivations.
var fastScrypt = &waddrmgr.ScryptOptions{
N: 16,
R: 8,
P: 1,
}
func Test_addOutputs(t *testing.T) {
msgtx := wire.NewMsgTx()
pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1}
if _, err := addOutputs(msgtx, pairs, &chaincfg.TestNet3Params); err != nil {
t.Fatal(err)
}
if len(msgtx.TxOut) != 2 {
t.Fatalf("Expected 2 outputs, found only %d", len(msgtx.TxOut))
}
values := []int{int(msgtx.TxOut[0].Value), int(msgtx.TxOut[1].Value)}
sort.Ints(values)
if !reflect.DeepEqual(values, []int{1, 10}) {
t.Fatalf("Expected values to be [1, 10], got: %v", values)
}
}
func TestCreateTx(t *testing.T) {
bs := &waddrmgr.BlockStamp{Height: 11111}
mgr := newManager(t, txInfo.privKeys, bs)
account := uint32(0)
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", &chaincfg.TestNet3Params)
var tstChangeAddress = func(account uint32) (btcutil.Address, error) {
return changeAddr, nil
}
// Pick all utxos from txInfo as eligible input.
eligible := mockCredits(t, txInfo.hex, []uint32{1, 2, 3, 4, 5})
// Now create a new TX sending 25e6 satoshis to the following addresses:
outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6}
tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, mgr, account, tstChangeAddress, &chaincfg.TestNet3Params, false)
if err != nil {
t.Fatal(err)
}
if tx.ChangeAddr.String() != changeAddr.String() {
t.Fatalf("Unexpected change address; got %v, want %v",
tx.ChangeAddr.String(), changeAddr.String())
}
msgTx := tx.MsgTx
if len(msgTx.TxOut) != 3 {
t.Fatalf("Unexpected number of outputs; got %d, want 3", len(msgTx.TxOut))
}
// The outputs in our new TX amount to 25e6 satoshis, so to fulfil that
// createTx should have picked the utxos with indices 4, 3 and 5, which
// total 25.1e6.
if len(msgTx.TxIn) != 3 {
t.Fatalf("Unexpected number of inputs; got %d, want 3", len(msgTx.TxIn))
}
// Given the input (15e6 + 10e6 + 1e7) and requested output (15e6 + 10e6)
// amounts in the new TX, we should have a change output with 8.99e6, which
// implies a fee of 1e3 satoshis.
expectedChange := btcutil.Amount(8.999e6)
outputs[changeAddr.String()] = expectedChange
checkOutputsMatch(t, msgTx, outputs)
minFee := feeForSize(defaultFeeIncrement, msgTx.SerializeSize())
actualFee := btcutil.Amount(1e3)
if minFee > actualFee {
t.Fatalf("Requested fee (%v) for tx size higher than actual fee (%v)", minFee, actualFee)
}
}
func TestCreateTxInsufficientFundsError(t *testing.T) {
outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9}
eligible := mockCredits(t, txInfo.hex, []uint32{1})
bs := &waddrmgr.BlockStamp{Height: 11111}
account := uint32(0)
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", &chaincfg.TestNet3Params)
var tstChangeAddress = func(account uint32) (btcutil.Address, error) {
return changeAddr, nil
}
_, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, account, tstChangeAddress, &chaincfg.TestNet3Params, false)
if err == nil {
t.Error("Expected InsufficientFundsError, got no error")
} else if _, ok := err.(InsufficientFundsError); !ok {
t.Errorf("Unexpected error, got %v, want InsufficientFundsError", err)
}
}
// checkOutputsMatch checks that the outputs in the tx match the expected ones.
func checkOutputsMatch(t *testing.T, msgtx *wire.MsgTx, expected map[string]btcutil.Amount) {
// This is a bit convoluted because the index of the change output is randomized.
for addrStr, v := range expected {
addr, err := btcutil.DecodeAddress(addrStr, &chaincfg.TestNet3Params)
if err != nil {
t.Fatalf("Cannot decode address: %v", err)
}
pkScript, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Fatalf("Cannot create pkScript: %v", err)
}
found := false
for _, txout := range msgtx.TxOut {
if reflect.DeepEqual(txout.PkScript, pkScript) && txout.Value == int64(v) {
found = true
break
}
}
if !found {
t.Fatalf("PkScript %v not found in msgtx.TxOut: %v", pkScript, msgtx.TxOut)
}
}
}
// newManager creates a new waddrmgr and imports the given privKey into it.
func newManager(t *testing.T, privKeys []string, bs *waddrmgr.BlockStamp) *waddrmgr.Manager {
dbPath := filepath.Join(os.TempDir(), "wallet.bin")
os.Remove(dbPath)
db, err := walletdb.Create("bdb", dbPath)
if err != nil {
t.Fatal(err)
}
namespace, err := db.Namespace(waddrmgrNamespaceKey)
if err != nil {
t.Fatal(err)
}
seed, err := hdkeychain.GenerateSeed(hdkeychain.RecommendedSeedLen)
if err != nil {
t.Fatal(err)
}
pubPassphrase := []byte("pub")
privPassphrase := []byte("priv")
mgr, err := waddrmgr.Create(namespace, seed, pubPassphrase,
privPassphrase, &chaincfg.TestNet3Params, fastScrypt)
if err != nil {
t.Fatal(err)
}
for _, key := range privKeys {
wif, err := btcutil.DecodeWIF(key)
if err != nil {
t.Fatal(err)
}
if err = mgr.Unlock(privPassphrase); err != nil {
t.Fatal(err)
}
_, err = mgr.ImportPrivateKey(wif, bs)
if err != nil {
t.Fatal(err)
}
}
return mgr
}
// mockCredits decodes the given txHex and returns the outputs with
// the given indices as eligible inputs.
func mockCredits(t *testing.T, txHex string, indices []uint32) []wtxmgr.Credit {
serialized, err := hex.DecodeString(txHex)
if err != nil {
t.Fatal(err)
}
utx, err := btcutil.NewTxFromBytes(serialized)
if err != nil {
t.Fatal(err)
}
tx := utx.MsgTx()
isCB := blockchain.IsCoinBaseTx(tx)
now := time.Now()
eligible := make([]wtxmgr.Credit, len(indices))
c := wtxmgr.Credit{
OutPoint: wire.OutPoint{Hash: *utx.Sha()},
BlockMeta: wtxmgr.BlockMeta{
Block: wtxmgr.Block{Height: -1},
},
}
for i, idx := range indices {
c.OutPoint.Index = idx
c.Amount = btcutil.Amount(tx.TxOut[idx].Value)
c.PkScript = tx.TxOut[idx].PkScript
c.Received = now
c.FromCoinBase = isCB
eligible[i] = c
}
return eligible
}

View file

@ -1,140 +0,0 @@
// Copyright (c) 2015 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
// TODO(jrick) Due to the extra encapsulation added during the switch
// to the new txstore, structures can no longer be mocked due to private
// members. Since all members for RecvTxOut and SignedTx are private, the
// simplist solution would be to make RecvTxOut an interface and create
// our own types satisifying the interface for this test package. Until
// then, disable this test.
//
// +build ignore
package wallet
import (
"testing"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/tx"
)
func init() {
cfg = &Config{
KeypoolSize: 100,
}
}
type allowFreeTest struct {
name string
inputs []*tx.Utxo
curHeight int32
txSize int
free bool
}
var allowFreeTests = []allowFreeTest{
{
name: "priority < 57,600,000",
inputs: []*tx.Utxo{
{
Amt: btcutil.SatoshiPerBitcoin,
Height: 0,
},
},
curHeight: 142, // 143 confirmations
txSize: 250,
free: false,
},
{
name: "priority == 57,600,000",
inputs: []*tx.Utxo{
{
Amt: btcutil.SatoshiPerBitcoin,
Height: 0,
},
},
curHeight: 143, // 144 confirmations
txSize: 250,
free: false,
},
{
name: "priority > 57,600,000",
inputs: []*tx.Utxo{
{
Amt: btcutil.SatoshiPerBitcoin,
Height: 0,
},
},
curHeight: 144, // 145 confirmations
txSize: 250,
free: true,
},
}
func TestAllowFree(t *testing.T) {
for _, test := range allowFreeTests {
calcFree := allowFree(test.curHeight, test.inputs, test.txSize)
if calcFree != test.free {
t.Errorf("Allow free test '%v' failed.", test.name)
}
}
}
func TestFakeTxs(t *testing.T) {
// First we need a wallet.
w, err := keystore.NewStore("banana wallet", "", []byte("banana"),
wire.MainNet, &keystore.BlockStamp{}, 100)
if err != nil {
t.Errorf("Can not create encrypted wallet: %s", err)
return
}
a := &Wallet{
Wallet: w,
lockedOutpoints: map[wire.OutPoint]struct{}{},
}
w.Unlock([]byte("banana"))
// Create and add a fake Utxo so we have some funds to spend.
//
// This will pass validation because txcscript is unaware of invalid
// tx inputs, however, this example would fail in btcd.
utxo := &tx.Utxo{}
addr, err := w.NextChainedAddress(&keystore.BlockStamp{}, 100)
if err != nil {
t.Errorf("Cannot get next address: %s", err)
return
}
copy(utxo.AddrHash[:], addr.ScriptAddress())
ophash := (wire.ShaHash)([...]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
28, 29, 30, 31, 32})
out := wire.NewOutPoint(&ophash, 0)
utxo.Out = tx.OutPoint(*out)
ss, err := txscript.PayToAddrScript(addr)
if err != nil {
t.Errorf("Could not create utxo PkScript: %s", err)
return
}
utxo.Subscript = tx.PkScript(ss)
utxo.Amt = 1000000
utxo.Height = 12345
a.UtxoStore = append(a.UtxoStore, utxo)
// Fake our current block height so btcd doesn't need to be queried.
curBlock.BlockStamp.Height = 12346
// Create the transaction.
pairs := map[string]int64{
"17XhEvq9Nahdj7Xe1nv6oRe1tEmaHUuynH": 5000,
}
_, err = a.txToPairs(pairs, 1)
if err != nil {
t.Errorf("Tx creation failed: %s", err)
return
}
}

View file

@ -0,0 +1,74 @@
// 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 txsizes
import (
"github.com/btcsuite/btcd/wire"
h "github.com/btcsuite/btcwallet/internal/helpers"
)
// Worst case script and input/output size estimates.
const (
// RedeemP2PKHSigScriptSize is the worst case (largest) serialize size
// of a transaction input script that redeems a compressed P2PKH output.
// It is calculated as:
//
// - OP_DATA_73
// - 72 bytes DER signature + 1 byte sighash
// - OP_DATA_33
// - 33 bytes serialized compressed pubkey
RedeemP2PKHSigScriptSize = 1 + 73 + 1 + 33
// P2PKHPkScriptSize is the size of a transaction output script that
// pays to a compressed pubkey hash. It is calculated as:
//
// - OP_DUP
// - OP_HASH160
// - OP_DATA_20
// - 20 bytes pubkey hash
// - OP_EQUALVERIFY
// - OP_CHECKSIG
P2PKHPkScriptSize = 1 + 1 + 1 + 20 + 1 + 1
// RedeemP2PKHInputSize is the worst case (largest) serialize size of a
// transaction input redeeming a compressed P2PKH output. It is
// calculated as:
//
// - 32 bytes previous tx
// - 4 bytes output index
// - 1 byte compact int encoding value 107
// - 107 bytes signature script
// - 4 bytes sequence
RedeemP2PKHInputSize = 32 + 4 + 1 + RedeemP2PKHSigScriptSize + 4
// P2PKHOutputSize is the serialize size of a transaction output with a
// P2PKH output script. It is calculated as:
//
// - 8 bytes output value
// - 1 byte compact int encoding value 25
// - 25 bytes P2PKH output script
P2PKHOutputSize = 8 + 1 + P2PKHPkScriptSize
)
// EstimateSerializeSize returns a worst case serialize size estimate for a
// signed transaction that spends inputCount number of compressed P2PKH outputs
// and contains each transaction output from txOuts. The estimated size is
// incremented for an additional P2PKH change output if addChangeOutput is true.
func EstimateSerializeSize(inputCount int, txOuts []*wire.TxOut, addChangeOutput bool) int {
changeSize := 0
outputCount := len(txOuts)
if addChangeOutput {
changeSize = P2PKHOutputSize
outputCount++
}
// 8 additional bytes are for version and locktime
return 8 + wire.VarIntSerializeSize(uint64(inputCount)) +
wire.VarIntSerializeSize(uint64(outputCount)) +
inputCount*RedeemP2PKHInputSize +
h.SumOutputSerializeSizes(txOuts) +
changeSize
}

View file

@ -0,0 +1,62 @@
package txsizes_test
import (
"testing"
"github.com/btcsuite/btcd/wire"
. "github.com/btcsuite/btcwallet/wallet/internal/txsizes"
)
const (
p2pkhScriptSize = P2PKHPkScriptSize
p2shScriptSize = 23
)
func makeInts(value int, n int) []int {
v := make([]int, n)
for i := range v {
v[i] = value
}
return v
}
func TestEstimateSerializeSize(t *testing.T) {
tests := []struct {
InputCount int
OutputScriptLengths []int
AddChangeOutput bool
ExpectedSizeEstimate int
}{
0: {1, []int{}, false, 159},
1: {1, []int{p2pkhScriptSize}, false, 193},
2: {1, []int{}, true, 193},
3: {1, []int{p2pkhScriptSize}, true, 227},
4: {1, []int{p2shScriptSize}, false, 191},
5: {1, []int{p2shScriptSize}, true, 225},
6: {2, []int{}, false, 308},
7: {2, []int{p2pkhScriptSize}, false, 342},
8: {2, []int{}, true, 342},
9: {2, []int{p2pkhScriptSize}, true, 376},
10: {2, []int{p2shScriptSize}, false, 340},
11: {2, []int{p2shScriptSize}, true, 374},
// 0xfd is discriminant for 16-bit compact ints, compact int
// total size increases from 1 byte to 3.
12: {1, makeInts(p2pkhScriptSize, 0xfc), false, 8727},
13: {1, makeInts(p2pkhScriptSize, 0xfd), false, 8727 + P2PKHOutputSize + 2},
14: {1, makeInts(p2pkhScriptSize, 0xfc), true, 8727 + P2PKHOutputSize + 2},
15: {0xfc, []int{}, false, 37558},
16: {0xfd, []int{}, false, 37558 + RedeemP2PKHInputSize + 2},
}
for i, test := range tests {
outputs := make([]*wire.TxOut, 0, len(test.OutputScriptLengths))
for _, l := range test.OutputScriptLengths {
outputs = append(outputs, &wire.TxOut{PkScript: make([]byte, l)})
}
actualEstimate := EstimateSerializeSize(test.InputCount, outputs, test.AddChangeOutput)
if actualEstimate != test.ExpectedSizeEstimate {
t.Errorf("Test %d: Got %v: Expected %v", i, actualEstimate, test.ExpectedSizeEstimate)
}
}
}

200
wallet/txauthor/author.go Normal file
View file

@ -0,0 +1,200 @@
// 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)
}

View file

@ -0,0 +1,204 @@
// 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_test
import (
"testing"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
. "github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/wallet/internal/txsizes"
)
func p2pkhOutputs(amounts ...btcutil.Amount) []*wire.TxOut {
v := make([]*wire.TxOut, 0, len(amounts))
for _, a := range amounts {
outScript := make([]byte, txsizes.P2PKHOutputSize)
v = append(v, wire.NewTxOut(int64(a), outScript))
}
return v
}
func makeInputSource(unspents []*wire.TxOut) InputSource {
// Return outputs in order.
currentTotal := btcutil.Amount(0)
currentInputs := make([]*wire.TxIn, 0, len(unspents))
f := func(target btcutil.Amount) (btcutil.Amount, []*wire.TxIn, [][]byte, error) {
for currentTotal < target && len(unspents) != 0 {
u := unspents[0]
unspents = unspents[1:]
nextInput := wire.NewTxIn(&wire.OutPoint{}, nil)
currentTotal += btcutil.Amount(u.Value)
currentInputs = append(currentInputs, nextInput)
}
return currentTotal, currentInputs, make([][]byte, len(currentInputs)), nil
}
return InputSource(f)
}
func TestNewUnsignedTransaction(t *testing.T) {
tests := []struct {
UnspentOutputs []*wire.TxOut
Outputs []*wire.TxOut
RelayFee btcutil.Amount
ChangeAmount btcutil.Amount
InputSourceError bool
InputCount int
}{
0: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8),
RelayFee: 1e3,
InputSourceError: true,
},
1: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e6),
RelayFee: 1e3,
ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6), true)),
InputCount: 1,
},
2: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e6),
RelayFee: 1e4,
ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e4,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6), true)),
InputCount: 1,
},
3: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
RelayFee: 1e4,
ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(1e4,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6, 1e6, 1e6), true)),
InputCount: 1,
},
4: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e6, 1e6, 1e6),
RelayFee: 2.55e3,
ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6, 1e6, 1e6), true)),
InputCount: 1,
},
// Test dust thresholds (546 for a 1e3 relay fee).
5: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 1e3,
ChangeAmount: 0,
InputCount: 1,
},
6: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 1e3,
ChangeAmount: 546,
InputCount: 1,
},
// Test dust thresholds (1392.3 for a 2.55e3 relay fee).
7: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 1392 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 2.55e3,
ChangeAmount: 0,
InputCount: 1,
},
8: {
UnspentOutputs: p2pkhOutputs(1e8),
Outputs: p2pkhOutputs(1e8 - 1393 - txrules.FeeForSerializeSize(2.55e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 2.55e3,
ChangeAmount: 1393,
InputCount: 1,
},
// Test two unspent outputs available but only one needed
// (tested fee only includes one input rather than using a
// serialize size for each).
9: {
UnspentOutputs: p2pkhOutputs(1e8, 1e8),
Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 1e3,
ChangeAmount: 546,
InputCount: 1,
},
// Test that second output is not included to make the change
// output not dust and be included in the transaction.
//
// It's debatable whether or not this is a good idea, but it's
// how the function was written, so test it anyways.
10: {
UnspentOutputs: p2pkhOutputs(1e8, 1e8),
Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))),
RelayFee: 1e3,
ChangeAmount: 0,
InputCount: 1,
},
// Test two unspent outputs available where both are needed.
11: {
UnspentOutputs: p2pkhOutputs(1e8, 1e8),
Outputs: p2pkhOutputs(1e8),
RelayFee: 1e3,
ChangeAmount: 1e8 - txrules.FeeForSerializeSize(1e3,
txsizes.EstimateSerializeSize(2, p2pkhOutputs(1e8), true)),
InputCount: 2,
},
}
changeSource := func() ([]byte, error) {
// Only length matters for these tests.
return make([]byte, txsizes.P2PKHPkScriptSize), nil
}
for i, test := range tests {
inputSource := makeInputSource(test.UnspentOutputs)
tx, err := NewUnsignedTransaction(test.Outputs, test.RelayFee, inputSource, changeSource)
switch e := err.(type) {
case nil:
case InputSourceError:
if !test.InputSourceError {
t.Errorf("Test %d: Returned InputSourceError but expected "+
"change output with amount %v", i, test.ChangeAmount)
}
continue
default:
t.Errorf("Test %d: Unexpected error: %v", i, e)
continue
}
if tx.ChangeIndex < 0 {
if test.ChangeAmount != 0 {
t.Errorf("Test %d: No change output added but expected output with amount %v",
i, test.ChangeAmount)
continue
}
} else {
changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value)
if changeAmount != test.ChangeAmount {
t.Errorf("Test %d: Got change amount %v, Expected %v",
i, changeAmount, test.ChangeAmount)
continue
}
}
if len(tx.Tx.TxIn) != test.InputCount {
t.Errorf("Test %d: Used %d outputs from input source, Expected %d",
i, len(tx.Tx.TxIn), test.InputCount)
}
}
}

39
wallet/txauthor/cprng.go Normal file
View file

@ -0,0 +1,39 @@
// 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
import (
"crypto/rand"
"encoding/binary"
mrand "math/rand"
"sync"
)
// cprng is a cryptographically random-seeded math/rand prng. It is seeded
// during package init. Any initialization errors result in panics. It is safe
// for concurrent access.
var cprng = cprngType{}
type cprngType struct {
r *mrand.Rand
mu sync.Mutex
}
func init() {
buf := make([]byte, 8)
_, err := rand.Read(buf)
if err != nil {
panic("Failed to seed prng: " + err.Error())
}
seed := int64(binary.LittleEndian.Uint64(buf))
cprng.r = mrand.New(mrand.NewSource(seed))
}
func (c *cprngType) Int31n(n int32) int32 {
defer c.mu.Unlock() // Int31n may panic
c.mu.Lock()
return c.r.Int31n(n)
}

92
wallet/txrules/rules.go Normal file
View file

@ -0,0 +1,92 @@
// 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 txrules provides transaction rules that should be followed by
// transaction authors for wide mempool acceptance and quick mining.
package txrules
import (
"errors"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil"
)
// DefaultRelayFeePerKb is the default minimum relay fee policy for a mempool.
const DefaultRelayFeePerKb btcutil.Amount = 1e3
// IsDustAmount determines whether a transaction output value and script length would
// cause the output to be considered dust. Transactions with dust outputs are
// not standard and are rejected by mempools with default policies.
func IsDustAmount(amount btcutil.Amount, scriptSize int, relayFeePerKb btcutil.Amount) bool {
// Calculate the total (estimated) cost to the network. This is
// calculated using the serialize size of the output plus the serial
// size of a transaction input which redeems it. The output is assumed
// to be compressed P2PKH as this is the most common script type. Use
// the average size of a compressed P2PKH redeem input (148) rather than
// the largest possible (txsizes.RedeemP2PKHInputSize).
totalSize := 8 + wire.VarIntSerializeSize(uint64(scriptSize)) +
scriptSize + 148
// Dust is defined as an output value where the total cost to the network
// (output size + input size) is greater than 1/3 of the relay fee.
return int64(amount)*1000/(3*int64(totalSize)) < int64(relayFeePerKb)
}
// IsDustOutput determines whether a transaction output is considered dust.
// Transactions with dust outputs are not standard and are rejected by mempools
// with default policies.
func IsDustOutput(output *wire.TxOut, relayFeePerKb btcutil.Amount) bool {
// Unspendable outputs which solely carry data are not checked for dust.
if txscript.GetScriptClass(output.PkScript) == txscript.NullDataTy {
return false
}
// All other unspendable outputs are considered dust.
if txscript.IsUnspendable(output.PkScript) {
return true
}
return IsDustAmount(btcutil.Amount(output.Value), len(output.PkScript),
relayFeePerKb)
}
// Transaction rule violations
var (
ErrAmountNegative = errors.New("transaction output amount is negative")
ErrAmountExceedsMax = errors.New("transaction output amount exceeds maximum value")
ErrOutputIsDust = errors.New("transaction output is dust")
)
// CheckOutput performs simple consensus and policy tests on a transaction
// output.
func CheckOutput(output *wire.TxOut, relayFeePerKb btcutil.Amount) error {
if output.Value < 0 {
return ErrAmountNegative
}
if output.Value > btcutil.MaxSatoshi {
return ErrAmountExceedsMax
}
if IsDustOutput(output, relayFeePerKb) {
return ErrOutputIsDust
}
return nil
}
// FeeForSerializeSize calculates the required fee for a transaction of some
// arbitrary size given a mempool's relay fee policy.
func FeeForSerializeSize(relayFeePerKb btcutil.Amount, txSerializeSize int) btcutil.Amount {
fee := relayFeePerKb * btcutil.Amount(txSerializeSize) / 1000
if fee == 0 && relayFeePerKb > 0 {
fee = relayFeePerKb
}
if fee < 0 || fee > btcutil.MaxSatoshi {
fee = btcutil.MaxSatoshi
}
return fee
}

View file

@ -27,6 +27,8 @@ import (
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/chain"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wallet/txrules"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
@ -73,7 +75,7 @@ type Wallet struct {
chainClientSyncMtx sync.Mutex
lockedOutpoints map[wire.OutPoint]struct{}
FeeIncrement btcutil.Amount
RelayFee btcutil.Amount
DisallowFree bool
// Channels for rescan processing. Requests are added and merged with
@ -552,12 +554,12 @@ func (w *Wallet) syncWithChain() error {
type (
createTxRequest struct {
account uint32
pairs map[string]btcutil.Amount
outputs []*wire.TxOut
minconf int32
resp chan createTxResponse
}
createTxResponse struct {
tx *CreatedTx
tx *txauthor.AuthoredTx
err error
}
)
@ -578,7 +580,7 @@ out:
for {
select {
case txr := <-w.createTxRequests:
tx, err := w.txToPairs(txr.pairs, txr.account, txr.minconf)
tx, err := w.txToOutputs(txr.outputs, txr.account, txr.minconf)
txr.resp <- createTxResponse{tx, err}
case <-quit:
@ -590,16 +592,16 @@ out:
// CreateSimpleTx creates a new signed transaction spending unspent P2PKH
// outputs with at laest minconf confirmations spending to any number of
// address/amount pairs. Change and an appropiate transaction fee are
// automatically included, if necessary. All transaction creation through
// this function is serialized to prevent the creation of many transactions
// which spend the same outputs.
func (w *Wallet) CreateSimpleTx(account uint32, pairs map[string]btcutil.Amount,
minconf int32) (*CreatedTx, error) {
// address/amount pairs. Change and an appropriate transaction fee are
// automatically included, if necessary. All transaction creation through this
// function is serialized to prevent the creation of many transactions which
// spend the same outputs.
func (w *Wallet) CreateSimpleTx(account uint32, outputs []*wire.TxOut,
minconf int32) (*txauthor.AuthoredTx, error) {
req := createTxRequest{
account: account,
pairs: pairs,
outputs: outputs,
minconf: minconf,
resp: make(chan createTxResponse),
}
@ -1996,9 +1998,9 @@ func (w *Wallet) TotalReceivedForAddr(addr btcutil.Address, minConf int32) (btcu
return amount, err
}
// SendPairs creates and sends payment transactions. It returns the transaction
// hash upon success
func (w *Wallet) SendPairs(amounts map[string]btcutil.Amount, account uint32,
// SendOutputs creates and sends payment transactions. It returns the
// transaction hash upon success.
func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32,
minconf int32) (*wire.ShaHash, error) {
chainClient, err := w.requireChainClient()
@ -2006,15 +2008,22 @@ func (w *Wallet) SendPairs(amounts map[string]btcutil.Amount, account uint32,
return nil, err
}
for _, output := range outputs {
err = txrules.CheckOutput(output, w.RelayFee)
if err != nil {
return nil, err
}
}
// Create transaction, replying with an error if the creation
// was not successful.
createdTx, err := w.CreateSimpleTx(account, amounts, minconf)
createdTx, err := w.CreateSimpleTx(account, outputs, minconf)
if err != nil {
return nil, err
}
// Create transaction record and insert into the db.
rec, err := wtxmgr.NewTxRecordFromMsgTx(createdTx.MsgTx, time.Now())
rec, err := wtxmgr.NewTxRecordFromMsgTx(createdTx.Tx, time.Now())
if err != nil {
log.Errorf("Cannot create record for created transaction: %v", err)
return nil, err
@ -2105,7 +2114,6 @@ func (w *Wallet) SignTransaction(tx *wire.MsgTx, hashType txscript.SigHashType,
return key, pka.Compressed(), nil
})
getScript := txscript.ScriptClosure(func(
addr btcutil.Address) ([]byte, error) {
// If keys were provided then we can only use the
@ -2222,7 +2230,7 @@ func Open(pubPass []byte, params *chaincfg.Params, db walletdb.DB, waddrmgrNS, w
Manager: addrMgr,
TxStore: txMgr,
lockedOutpoints: map[wire.OutPoint]struct{}{},
FeeIncrement: defaultFeeIncrement,
RelayFee: txrules.DefaultRelayFeePerKb,
rescanAddJob: make(chan *RescanJob),
rescanBatch: make(chan *rescanBatch),
rescanNotifications: make(chan interface{}),