497ffc11f0
This is a rather monolithic commit that moves the old RPC server to its own package (rpc/legacyrpc), introduces a new RPC server using gRPC (rpc/rpcserver), and provides the ability to defer wallet loading until request at a later time by an RPC (--noinitialload). The legacy RPC server remains the default for now while the new gRPC server is not enabled by default. Enabling the new server requires setting a listen address (--experimenalrpclisten). This experimental flag is used to effectively feature gate the server until it is ready to use as a default. Both RPC servers can be run at the same time, but require binding to different listen addresses. In theory, with the legacy RPC server now living in its own package it should become much easier to unit test the handlers. This will be useful for any future changes to the package, as compatibility with Core's wallet is still desired. Type safety has also been improved in the legacy RPC server. Multiple handler types are now used for methods that do and do not require the RPC client as a dependency. This can statically help prevent nil pointer dereferences, and was very useful for catching bugs during refactoring. To synchronize the wallet loading process between the main package (the default) and through the gRPC WalletLoader service (with the --noinitialload option), as well as increasing the loose coupling of packages, a new wallet.Loader type has been added. All creating and loading of existing wallets is done through a single Loader instance, and callbacks can be attached to the instance to run after the wallet has been opened. This is how the legacy RPC server is associated with a loaded wallet, even after the wallet is loaded by a gRPC method in a completely unrelated package. Documentation for the new RPC server has been added to the rpc/documentation directory. The documentation includes a specification for the new RPC API, addresses how to make changes to the server implementation, and provides short example clients in several different languages. Some of the new RPC methods are not implementated exactly as described by the specification. These are considered bugs with the implementation, not the spec. Known bugs are commented as such.
501 lines
16 KiB
Go
501 lines
16 KiB
Go
/*
|
|
* Copyright (c) 2013-2015 The btcsuite developers
|
|
*
|
|
* 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 wallet
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
badrand "math/rand"
|
|
"sort"
|
|
"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/btcwallet/waddrmgr"
|
|
"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
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
// 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 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
|
|
}
|
|
|
|
// ByAmount defines the methods needed to satisify sort.Interface to
|
|
// sort a slice of Utxos by their amount.
|
|
type ByAmount []wtxmgr.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] }
|
|
|
|
// 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) {
|
|
|
|
// Address manager must be unlocked to compose transaction. Grab
|
|
// the unlock if possible (to prevent future unlocks), or return the
|
|
// error if already locked.
|
|
heldUnlock, err := w.HoldUnlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer heldUnlock.Release()
|
|
|
|
chainClient, err := w.requireChainClient()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get current block's height and hash.
|
|
bs, err := chainClient.BlockStamp()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
eligible, err := w.findEligibleOutputs(account, minconf, bs)
|
|
if err != nil {
|
|
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}
|
|
}
|
|
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)
|
|
}
|
|
|
|
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 {
|
|
return nil, err
|
|
}
|
|
|
|
if feeForSize(feeIncrement, msgtx.SerializeSize()) <= feeEst {
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
if err := validateMsgTx(msgtx, inputs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info := &CreatedTx{
|
|
MsgTx: msgtx,
|
|
ChangeAddr: changeAddr,
|
|
ChangeIndex: changeIdx,
|
|
Fee: feeEst, // Last estimate is the actual fee
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("cannot create txout script: %s", 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)
|
|
}
|
|
return minAmount, nil
|
|
}
|
|
|
|
func (w *Wallet) findEligibleOutputs(account uint32, minconf int32, bs *waddrmgr.BlockStamp) ([]wtxmgr.Credit, error) {
|
|
unspent, err := w.TxStore.UnspentOutputs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO: Eventually all of these filters (except perhaps output locking)
|
|
// should be handled by the call to UnspentOutputs (or similar).
|
|
// Because one of these filters requires matching the output script to
|
|
// the desired account, this change depends on making wtxmgr a waddrmgr
|
|
// dependancy and requesting unspent outputs for a single account.
|
|
eligible := make([]wtxmgr.Credit, 0, len(unspent))
|
|
for i := range unspent {
|
|
output := &unspent[i]
|
|
|
|
// Only include this output if it meets the required number of
|
|
// confirmations. Coinbase transactions must have have reached
|
|
// maturity before their outputs may be spent.
|
|
if !confirmed(minconf, output.Height, bs.Height) {
|
|
continue
|
|
}
|
|
if output.FromCoinBase {
|
|
const target = blockchain.CoinbaseMaturity
|
|
if !confirmed(target, output.Height, bs.Height) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Locked unspent outputs are skipped.
|
|
if w.LockedOutpoint(output.OutPoint) {
|
|
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(
|
|
output.PkScript, w.chainParams)
|
|
if err != nil || class != txscript.PubKeyHashTy {
|
|
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
|
|
}
|
|
|
|
eligible = append(eligible, *output)
|
|
}
|
|
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)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot create script engine: %s", err)
|
|
}
|
|
if err = vm.Execute(); 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
|
|
}
|