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 }