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