decred.org/dcrwallet/v3@v3.1.0/wallet/txauthor/author.go (about)

     1  // Copyright (c) 2016 The btcsuite developers
     2  // Copyright (c) 2016 The Decred developers
     3  // Use of this source code is governed by an ISC
     4  // license that can be found in the LICENSE file.
     5  
     6  // Package txauthor provides transaction creation code for wallets.
     7  package txauthor
     8  
     9  import (
    10  	"decred.org/dcrwallet/v3/errors"
    11  	"decred.org/dcrwallet/v3/wallet/txrules"
    12  	"decred.org/dcrwallet/v3/wallet/txsizes"
    13  	"github.com/decred/dcrd/chaincfg/v3"
    14  	"github.com/decred/dcrd/dcrutil/v4"
    15  	"github.com/decred/dcrd/txscript/v4"
    16  	"github.com/decred/dcrd/txscript/v4/sign"
    17  	"github.com/decred/dcrd/wire"
    18  )
    19  
    20  const (
    21  	// generatedTxVersion is the version of the transaction being generated.
    22  	// It is defined as a constant here rather than using the wire.TxVersion
    23  	// constant since a change in the transaction version will potentially
    24  	// require changes to the generated transaction.  Thus, using the wire
    25  	// constant for the generated transaction version could allow creation
    26  	// of invalid transactions for the updated version.
    27  	generatedTxVersion = 1
    28  )
    29  
    30  // InputDetail provides a detailed summary of transaction inputs
    31  // referencing spendable outputs. This consists of the total spendable
    32  // amount, the generated inputs, the redeem scripts and the full redeem
    33  // script sizes.
    34  type InputDetail struct {
    35  	Amount            dcrutil.Amount
    36  	Inputs            []*wire.TxIn
    37  	Scripts           [][]byte
    38  	RedeemScriptSizes []int
    39  }
    40  
    41  // InputSource provides transaction inputs referencing spendable outputs to
    42  // construct a transaction outputting some target amount.  If the target amount
    43  // can not be satisified, this can be signaled by returning a total amount less
    44  // than the target or by returning a more detailed error.
    45  type InputSource func(target dcrutil.Amount) (detail *InputDetail, err error)
    46  
    47  // AuthoredTx holds the state of a newly-created transaction and the change
    48  // output (if one was added).
    49  type AuthoredTx struct {
    50  	Tx                           *wire.MsgTx
    51  	PrevScripts                  [][]byte
    52  	TotalInput                   dcrutil.Amount
    53  	ChangeIndex                  int // negative if no change
    54  	EstimatedSignedSerializeSize int
    55  }
    56  
    57  // ChangeSource provides change output scripts and versions for
    58  // transaction creation.
    59  type ChangeSource interface {
    60  	Script() (script []byte, version uint16, err error)
    61  	ScriptSize() int
    62  }
    63  
    64  func sumOutputValues(outputs []*wire.TxOut) (totalOutput dcrutil.Amount) {
    65  	for _, txOut := range outputs {
    66  		totalOutput += dcrutil.Amount(txOut.Value)
    67  	}
    68  	return totalOutput
    69  }
    70  
    71  // NewUnsignedTransaction creates an unsigned transaction paying to one or more
    72  // non-change outputs.  An appropriate transaction fee is included based on the
    73  // transaction size.
    74  //
    75  // Transaction inputs are chosen from repeated calls to fetchInputs with
    76  // increasing targets amounts.
    77  //
    78  // If any remaining output value can be returned to the wallet via a change
    79  // output without violating mempool dust rules, a P2PKH change output is
    80  // appended to the transaction outputs.  Since the change output may not be
    81  // necessary, fetchChange is called zero or one times to generate this script.
    82  // This function must return a P2PKH script or smaller, otherwise fee estimation
    83  // will be incorrect.
    84  //
    85  // If successful, the transaction, total input value spent, and all previous
    86  // output scripts are returned.  If the input source was unable to provide
    87  // enough input value to pay for every output any any necessary fees, an
    88  // InputSourceError is returned.
    89  func NewUnsignedTransaction(outputs []*wire.TxOut, relayFeePerKb dcrutil.Amount,
    90  	fetchInputs InputSource, fetchChange ChangeSource, maxTxSize int) (*AuthoredTx, error) {
    91  
    92  	const op errors.Op = "txauthor.NewUnsignedTransaction"
    93  
    94  	targetAmount := sumOutputValues(outputs)
    95  	scriptSizes := []int{txsizes.RedeemP2PKHSigScriptSize}
    96  	changeScript, changeScriptVersion, err := fetchChange.Script()
    97  	if err != nil {
    98  		return nil, errors.E(op, err)
    99  	}
   100  	changeScriptSize := fetchChange.ScriptSize()
   101  	maxSignedSize := txsizes.EstimateSerializeSize(scriptSizes, outputs, changeScriptSize)
   102  	targetFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize)
   103  
   104  	for {
   105  		inputDetail, err := fetchInputs(targetAmount + targetFee)
   106  		if err != nil {
   107  			return nil, errors.E(op, err)
   108  		}
   109  
   110  		if inputDetail.Amount < targetAmount+targetFee {
   111  			return nil, errors.E(op, errors.InsufficientBalance)
   112  		}
   113  
   114  		scriptSizes := make([]int, 0, len(inputDetail.RedeemScriptSizes))
   115  		scriptSizes = append(scriptSizes, inputDetail.RedeemScriptSizes...)
   116  
   117  		maxSignedSize = txsizes.EstimateSerializeSize(scriptSizes, outputs, changeScriptSize)
   118  		maxRequiredFee := txrules.FeeForSerializeSize(relayFeePerKb, maxSignedSize)
   119  		remainingAmount := inputDetail.Amount - targetAmount
   120  		if remainingAmount < maxRequiredFee {
   121  			targetFee = maxRequiredFee
   122  			continue
   123  		}
   124  
   125  		if maxSignedSize > maxTxSize {
   126  			return nil, errors.E(errors.Invalid, "signed tx size exceeds allowed maximum")
   127  		}
   128  
   129  		unsignedTransaction := &wire.MsgTx{
   130  			SerType:  wire.TxSerializeFull,
   131  			Version:  generatedTxVersion,
   132  			TxIn:     inputDetail.Inputs,
   133  			TxOut:    outputs,
   134  			LockTime: 0,
   135  			Expiry:   0,
   136  		}
   137  		changeIndex := -1
   138  		changeAmount := inputDetail.Amount - targetAmount - maxRequiredFee
   139  		if changeAmount != 0 && !txrules.IsDustAmount(changeAmount,
   140  			changeScriptSize, relayFeePerKb) {
   141  			if len(changeScript) > txscript.MaxScriptElementSize {
   142  				return nil, errors.E(errors.Invalid, "script size exceed maximum bytes "+
   143  					"pushable to the stack")
   144  			}
   145  			change := &wire.TxOut{
   146  				Value:    int64(changeAmount),
   147  				Version:  changeScriptVersion,
   148  				PkScript: changeScript,
   149  			}
   150  			l := len(outputs)
   151  			unsignedTransaction.TxOut = append(outputs[:l:l], change)
   152  			changeIndex = l
   153  		} else {
   154  			maxSignedSize = txsizes.EstimateSerializeSize(scriptSizes,
   155  				unsignedTransaction.TxOut, 0)
   156  		}
   157  		return &AuthoredTx{
   158  			Tx:                           unsignedTransaction,
   159  			PrevScripts:                  inputDetail.Scripts,
   160  			TotalInput:                   inputDetail.Amount,
   161  			ChangeIndex:                  changeIndex,
   162  			EstimatedSignedSerializeSize: maxSignedSize,
   163  		}, nil
   164  	}
   165  }
   166  
   167  // RandomizeOutputPosition randomizes the position of a transaction's output by
   168  // swapping it with a random output.  The new index is returned.  This should be
   169  // done before signing.
   170  func RandomizeOutputPosition(outputs []*wire.TxOut, index int) int {
   171  	r := cprng.Int31n(int32(len(outputs)))
   172  	outputs[r], outputs[index] = outputs[index], outputs[r]
   173  	return int(r)
   174  }
   175  
   176  // RandomizeChangePosition randomizes the position of an authored transaction's
   177  // change output.  This should be done before signing.
   178  func (tx *AuthoredTx) RandomizeChangePosition() {
   179  	tx.ChangeIndex = RandomizeOutputPosition(tx.Tx.TxOut, tx.ChangeIndex)
   180  }
   181  
   182  // SecretsSource provides private keys and redeem scripts necessary for
   183  // constructing transaction input signatures.  Secrets are looked up by the
   184  // corresponding Address for the previous output script.  Addresses for lookup
   185  // are created using the source's blockchain parameters and means a single
   186  // SecretsSource can only manage secrets for a single chain.
   187  //
   188  // TODO: Rewrite this interface to look up private keys and redeem scripts for
   189  // pubkeys, pubkey hashes, script hashes, etc. as separate interface methods.
   190  // This would remove the ChainParams requirement of the interface and could
   191  // avoid unnecessary conversions from previous output scripts to Addresses.
   192  // This can not be done without modifications to the txscript package.
   193  type SecretsSource interface {
   194  	sign.KeyDB
   195  	sign.ScriptDB
   196  	ChainParams() *chaincfg.Params
   197  }
   198  
   199  // AddAllInputScripts modifies transaction a transaction by adding inputs
   200  // scripts for each input.  Previous output scripts being redeemed by each input
   201  // are passed in prevPkScripts and the slice length must match the number of
   202  // inputs.  Private keys and redeem scripts are looked up using a SecretsSource
   203  // based on the previous output script.
   204  func AddAllInputScripts(tx *wire.MsgTx, prevPkScripts [][]byte, secrets SecretsSource) error {
   205  	inputs := tx.TxIn
   206  	chainParams := secrets.ChainParams()
   207  
   208  	if len(inputs) != len(prevPkScripts) {
   209  		return errors.New("tx.TxIn and prevPkScripts slices must " +
   210  			"have equal length")
   211  	}
   212  
   213  	for i := range inputs {
   214  		pkScript := prevPkScripts[i]
   215  		sigScript := inputs[i].SignatureScript
   216  		script, err := sign.SignTxOutput(chainParams, tx, i,
   217  			pkScript, txscript.SigHashAll, secrets, secrets,
   218  			sigScript, true) // Yes treasury
   219  		if err != nil {
   220  			return err
   221  		}
   222  		inputs[i].SignatureScript = script
   223  	}
   224  
   225  	return nil
   226  }
   227  
   228  // AddAllInputScripts modifies an authored transaction by adding inputs scripts
   229  // for each input of an authored transaction.  Private keys and redeem scripts
   230  // are looked up using a SecretsSource based on the previous output script.
   231  func (tx *AuthoredTx) AddAllInputScripts(secrets SecretsSource) error {
   232  	return AddAllInputScripts(tx.Tx, tx.PrevScripts, secrets)
   233  }