diff --git a/config.go b/config.go index 58d46f7..b3695fc 100644 --- a/config.go +++ b/config.go @@ -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, } diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go new file mode 100644 index 0000000..ba58f49 --- /dev/null +++ b/internal/helpers/helpers.go @@ -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 +} diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 0fa54c7..e4b2464 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -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 diff --git a/wallet/createtx.go b/wallet/createtx.go index 3b0ffb1..500f0b3 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -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 -} diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go deleted file mode 100644 index cdff97d..0000000 --- a/wallet/createtx_test.go +++ /dev/null @@ -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 -} diff --git a/wallet/createtx_test_disabled.go b/wallet/createtx_test_disabled.go deleted file mode 100644 index b15afe0..0000000 --- a/wallet/createtx_test_disabled.go +++ /dev/null @@ -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 - } -} diff --git a/wallet/internal/txsizes/size.go b/wallet/internal/txsizes/size.go new file mode 100644 index 0000000..8e69f70 --- /dev/null +++ b/wallet/internal/txsizes/size.go @@ -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 +} diff --git a/wallet/internal/txsizes/size_test.go b/wallet/internal/txsizes/size_test.go new file mode 100644 index 0000000..93620be --- /dev/null +++ b/wallet/internal/txsizes/size_test.go @@ -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) + } + } +} diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go new file mode 100644 index 0000000..8a4dcb4 --- /dev/null +++ b/wallet/txauthor/author.go @@ -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) +} diff --git a/wallet/txauthor/author_test.go b/wallet/txauthor/author_test.go new file mode 100644 index 0000000..94c32df --- /dev/null +++ b/wallet/txauthor/author_test.go @@ -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) + } + } +} diff --git a/wallet/txauthor/cprng.go b/wallet/txauthor/cprng.go new file mode 100644 index 0000000..9369f1c --- /dev/null +++ b/wallet/txauthor/cprng.go @@ -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) +} diff --git a/wallet/txrules/rules.go b/wallet/txrules/rules.go new file mode 100644 index 0000000..f7ebdd6 --- /dev/null +++ b/wallet/txrules/rules.go @@ -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 +} diff --git a/wallet/wallet.go b/wallet/wallet.go index ffb64c0..4bfa72b 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -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{}),