lbcwallet/createtx.go
Josh Rickmar b9fd527d33 Remove account support, fix races on btcd connect.
This commit is the result of several big changes being made to the
wallet.  In particular, the "handshake" (initial sync to the chain
server) was quite racy and required proper synchronization.  To make
fixing this race easier, several other changes were made to the
internal wallet data structures and much of the RPC server ended up
being rewritten.

First, all account support has been removed.  The previous Account
struct has been replaced with a Wallet structure, which includes a
keystore for saving keys, and a txstore for storing relevant
transactions.  This decision has been made since it is the opinion of
myself and other developers that bitcoind accounts are fundamentally
broken (as accounts implemented by bitcoind support both arbitrary
address groupings as well as moving balances between accounts -- these
are fundamentally incompatible features), and since a BIP0032 keystore
is soon planned to be implemented (at which point, "accounts" can
return as HD extended keys).  With the keystore handling the grouping
of related keys, there is no reason have many different Account
structs, and the AccountManager has been removed as well.  All RPC
handlers that take an account option will only work with "" (the
default account) or "*" if the RPC allows specifying all accounts.

Second, much of the RPC server has been cleaned up.  The global
variables for the RPC server and chain server client have been moved
to part of the rpcServer struct, and the handlers for each RPC method
that are looked up change depending on which components have been set.
Passthrough requests are also no longer handled specially, but when
the chain server is set, a handler to perform the passthrough will be
returned if the method is not otherwise a wallet RPC.  The
notification system for websocket clients has also been rewritten so
wallet components can send notifications through channels, rather than
requiring direct access to the RPC server itself, or worse still,
sending directly to a websocket client's send channel.  In the future,
this will enable proper registration of notifications, rather than
unsolicited broadcasts to every connected websocket client (see
issue #84).

Finally, and the main reason why much of this cleanup was necessary,
the races during intial sync with the chain server have been fixed.
Previously, when the 'Handshake' was run, a rescan would occur which
would perform modifications to Account data structures as
notifications were received.  Synchronization was provided with a
single binary semaphore which serialized all access to wallet and
account data.  However, the Handshake itself was not able to run with
this lock (or else notifications would block), and many data races
would occur as both notifications were being handled.  If GOMAXPROCS
was ever increased beyond 1, btcwallet would always immediately crash
due to invalid addresses caused by the data races on startup.  To fix
this, the single lock for all wallet access has been replaced with
mutexes for both the keystore and txstore.  Handling of btcd
notifications and client requests may now occur simultaneously.
GOMAXPROCS has also been set to the number of logical CPUs at the
beginning of main, since with the data races fixed, there's no reason
to prevent the extra parallelism gained by increasing it.

Closes #78.

Closes #101.

Closes #110.
2014-07-25 13:26:14 -05:00

398 lines
12 KiB
Go

/*
* Copyright (c) 2013, 2014 Conformal Systems LLC <info@conformal.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package main
import (
"bytes"
"errors"
"fmt"
badrand "math/rand"
"sort"
"time"
"github.com/conformal/btcchain"
"github.com/conformal/btcscript"
"github.com/conformal/btcutil"
"github.com/conformal/btcwallet/keystore"
"github.com/conformal/btcwallet/txstore"
"github.com/conformal/btcwire"
)
// InsufficientFunds 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 InsufficientFunds struct {
in, out, fee btcutil.Amount
}
// Error satisifies the builtin error interface.
func (e InsufficientFunds) 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 fmt.Sprintf("insufficient funds: transaction requires %s input "+
"(%v output + %v fee) but only %v spendable", total, e.out,
e.fee, e.in)
}
// 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.0001 BTC,
// measured in satoshis) added to transactions requiring a fee.
const defaultFeeIncrement = 10000
type CreatedTx struct {
tx *btcutil.Tx
changeAddr btcutil.Address
changeIndex int // negative if no change
}
// ByAmount defines the methods needed to satisify sort.Interface to
// sort a slice of Utxos by their amount.
type ByAmount []txstore.Credit
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] }
// selectInputs selects the minimum number possible of unspent
// outputs to use to create a new transaction that spends amt satoshis.
// btcout is the total number of satoshis which would be spent by the
// combination of all selected previous outputs. err will equal
// ErrInsufficientFunds if there are not enough unspent outputs to spend amt
// amt.
func selectInputs(eligible []txstore.Credit, amt, fee btcutil.Amount,
minconf int) (selected []txstore.Credit, out btcutil.Amount, err error) {
// Iterate throguh eligible transactions, appending to outputs and
// increasing out. This is finished when out is greater than the
// requested amt to spend.
selected = make([]txstore.Credit, 0, len(eligible))
for _, e := range eligible {
selected = append(selected, e)
out += e.Amount()
if out >= amt+fee {
return selected, out, nil
}
}
if out < amt+fee {
return nil, 0, InsufficientFunds{out, amt, fee}
}
return selected, out, nil
}
// 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. If change is needed to return funds back to an owned
// address, changeUtxo will point to a unconfirmed (height = -1, zeroed
// block hash) Utxo. ErrInsufficientFunds is returned if there are not
// enough eligible unspent outputs to create the transaction.
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
minconf int) (*CreatedTx, error) {
// Key store must be unlocked to compose transaction. Grab the
// unlock if possible (to prevent future unlocks), or return the
// error if the keystore is already locked.
heldUnlock, err := w.HoldUnlock()
if err != nil {
return nil, err
}
defer heldUnlock.Release()
// Create a new transaction which will include all input scripts.
msgtx := btcwire.NewMsgTx()
// Calculate minimum amount needed for inputs.
var amt btcutil.Amount
for _, v := range pairs {
// Error out if any amount is negative.
if v <= 0 {
return nil, ErrNonPositiveAmount
}
amt += v
}
// Add outputs to new tx.
for addrStr, amt := range pairs {
addr, err := btcutil.DecodeAddress(addrStr, activeNet.Params)
if err != nil {
return nil, fmt.Errorf("cannot decode address: %s", err)
}
// Add output to spend amt to addr.
pkScript, err := btcscript.PayToAddrScript(addr)
if err != nil {
return nil, fmt.Errorf("cannot create txout script: %s", err)
}
txout := btcwire.NewTxOut(int64(amt), pkScript)
msgtx.AddTxOut(txout)
}
// Get current block's height and hash.
bs, err := w.chainSvr.BlockStamp()
if err != nil {
return nil, err
}
// Make a copy of msgtx before any inputs are added. This will be
// used as a starting point when trying a fee and starting over with
// a higher fee if not enough was originally chosen.
txNoInputs := msgtx.Copy()
unspent, err := w.TxStore.UnspentOutputs()
if err != nil {
return nil, err
}
// 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).
eligible := make([]txstore.Credit, 0, len(unspent))
for i := range unspent {
switch btcscript.GetScriptClass(unspent[i].TxOut().PkScript) {
case btcscript.PubKeyHashTy:
if !unspent[i].Confirmed(minconf, bs.Height) {
continue
}
// Coinbase transactions must have have reached maturity
// before their outputs may be spent.
if unspent[i].IsCoinbase() {
target := btcchain.CoinbaseMaturity
if !unspent[i].Confirmed(target, bs.Height) {
continue
}
}
// Locked unspent outputs are skipped.
if w.LockedOutpoint(*unspent[i].OutPoint()) {
continue
}
eligible = append(eligible, unspent[i])
}
}
// Sort eligible inputs, as selectInputs expects these to be sorted
// by amount in reverse order.
sort.Sort(sort.Reverse(ByAmount(eligible)))
var selectedInputs []txstore.Credit
// changeAddr is nil/zeroed until a change address is needed, and reused
// again in case a change utxo has already been chosen.
var changeAddr btcutil.Address
var changeIdx int
// Get the number of satoshis to increment fee by when searching for
// the minimum tx fee needed.
fee := btcutil.Amount(0)
for {
msgtx = txNoInputs.Copy()
changeIdx = -1
// Select eligible outputs to be used in transaction based on the amount
// neededing to sent, and the current fee estimation.
inputs, btcin, err := selectInputs(eligible, amt, fee, minconf)
if err != nil {
return nil, err
}
// Check if there are leftover unspent outputs, and return coins back to
// a new address we own.
change := btcin - amt - fee
if change > 0 {
// Get a new change address if one has not already been found.
if changeAddr == nil {
changeAddr, err = w.KeyStore.ChangeAddress(bs)
if err != nil {
return nil, fmt.Errorf("failed to get next address: %s", err)
}
w.KeyStore.MarkDirty()
err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr})
if err != nil {
return nil, fmt.Errorf("cannot request updates for "+
"change address: %v", err)
}
}
// Spend change.
pkScript, err := btcscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, fmt.Errorf("cannot create txout script: %s", err)
}
msgtx.AddTxOut(btcwire.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]
changeIdx = int(r)
}
// Selected unspent outputs become new transaction's inputs.
for _, ip := range inputs {
msgtx.AddTxIn(btcwire.NewTxIn(ip.OutPoint(), nil))
}
for i, input := range inputs {
// Errors don't matter here, as we only consider the
// case where len(addrs) == 1.
_, addrs, _, _ := input.Addresses(activeNet.Params)
if len(addrs) != 1 {
continue
}
apkh, ok := addrs[0].(*btcutil.AddressPubKeyHash)
if !ok {
continue // don't handle inputs to this yet
}
ai, err := w.KeyStore.Address(apkh)
if err != nil {
return nil, fmt.Errorf("cannot get address info: %v", err)
}
pka := ai.(keystore.PubKeyAddress)
privkey, err := pka.PrivKey()
if err != nil {
return nil, fmt.Errorf("cannot get private key: %v", err)
}
sigscript, err := btcscript.SignatureScript(msgtx, i,
input.TxOut().PkScript, btcscript.SigHashAll, privkey,
ai.Compressed())
if err != nil {
return nil, fmt.Errorf("cannot create sigscript: %s", err)
}
msgtx.TxIn[i].SignatureScript = sigscript
}
noFeeAllowed := false
if !cfg.DisallowFree {
noFeeAllowed = allowFree(bs.Height, inputs, msgtx.SerializeSize())
}
if minFee := minimumFee(w.FeeIncrement, msgtx, noFeeAllowed); fee < minFee {
fee = minFee
} else {
selectedInputs = inputs
break
}
}
// Validate msgtx before returning the raw transaction.
flags := btcscript.ScriptCanonicalSignatures
bip16 := time.Now().After(btcscript.Bip16Activation)
if bip16 {
flags |= btcscript.ScriptBip16
}
for i, txin := range msgtx.TxIn {
engine, err := btcscript.NewScript(txin.SignatureScript,
selectedInputs[i].TxOut().PkScript, i, msgtx, flags)
if err != nil {
return nil, fmt.Errorf("cannot create script engine: %s", err)
}
if err = engine.Execute(); err != nil {
return nil, fmt.Errorf("cannot validate transaction: %s", err)
}
}
buf := bytes.Buffer{}
buf.Grow(msgtx.SerializeSize())
if err := msgtx.BtcEncode(&buf, btcwire.ProtocolVersion); err != nil {
// Hitting OOM by growing or writing to a bytes.Buffer already
// panics, and all returned errors are unexpected.
panic(err)
}
info := &CreatedTx{
tx: btcutil.NewTx(msgtx),
changeAddr: changeAddr,
changeIndex: changeIdx,
}
return info, nil
}
// minimumFee calculates the minimum fee required for a transaction.
// If allowFree is true, a fee may be zero so long as the entire
// transaction has a serialized length less than 1 kilobyte
// and none of the outputs contain a value less than 1 bitcent.
// Otherwise, the fee will be calculated using TxFeeIncrement,
// incrementing the fee for each kilobyte of transaction.
func minimumFee(incr btcutil.Amount, tx *btcwire.MsgTx, allowFree bool) btcutil.Amount {
txLen := tx.SerializeSize()
fee := btcutil.Amount(int64(1+txLen/1000) * int64(incr))
if allowFree && txLen < 1000 {
fee = 0
}
if fee < incr {
for _, txOut := range tx.TxOut {
if txOut.Value < btcutil.SatoshiPerBitcent {
return incr
}
}
}
if fee < 0 || fee > btcutil.MaxSatoshi {
fee = btcutil.MaxSatoshi
}
return fee
}
// allowFree 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 allowFree(curHeight int32, txouts []txstore.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.BlockHeight, 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
}