// 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 changeAmount != 0 && !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 } }
func TestNewUnsignedTransaction(t *testing.T) { tests := []struct { UnspentOutputs []*wire.TxOut Outputs []*wire.TxOut RelayFee btcutil.Amount ChangeAmount btcutil.Amount InputSourceError bool InputCount int }{ 0: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8), RelayFee: 1e3, InputSourceError: true, }, 1: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e6), RelayFee: 1e3, ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6), true)), InputCount: 1, }, 2: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e6), RelayFee: 1e4, ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e4, txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6), true)), InputCount: 1, }, 3: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e6, 1e6, 1e6), RelayFee: 1e4, ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(1e4, txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6, 1e6, 1e6), true)), InputCount: 1, }, 4: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e6, 1e6, 1e6), RelayFee: 2.55e3, ChangeAmount: 1e8 - 3e6 - txrules.FeeForSerializeSize(2.55e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(1e6, 1e6, 1e6), true)), InputCount: 1, }, // Test dust thresholds (546 for a 1e3 relay fee). 5: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 1e3, ChangeAmount: 0, InputCount: 1, }, 6: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 1e3, ChangeAmount: 546, InputCount: 1, }, // Test dust thresholds (1392.3 for a 2.55e3 relay fee). 7: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8 - 1392 - txrules.FeeForSerializeSize(2.55e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 2.55e3, ChangeAmount: 0, InputCount: 1, }, 8: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8 - 1393 - txrules.FeeForSerializeSize(2.55e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 2.55e3, ChangeAmount: 1393, InputCount: 1, }, // Test two unspent outputs available but only one needed // (tested fee only includes one input rather than using a // serialize size for each). 9: { UnspentOutputs: p2pkhOutputs(1e8, 1e8), Outputs: p2pkhOutputs(1e8 - 546 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 1e3, ChangeAmount: 546, InputCount: 1, }, // Test that second output is not included to make the change // output not dust and be included in the transaction. // // It's debatable whether or not this is a good idea, but it's // how the function was written, so test it anyways. 10: { UnspentOutputs: p2pkhOutputs(1e8, 1e8), Outputs: p2pkhOutputs(1e8 - 545 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(1, p2pkhOutputs(0), true))), RelayFee: 1e3, ChangeAmount: 0, InputCount: 1, }, // Test two unspent outputs available where both are needed. 11: { UnspentOutputs: p2pkhOutputs(1e8, 1e8), Outputs: p2pkhOutputs(1e8), RelayFee: 1e3, ChangeAmount: 1e8 - txrules.FeeForSerializeSize(1e3, txsizes.EstimateSerializeSize(2, p2pkhOutputs(1e8), true)), InputCount: 2, }, // Test that zero change outputs are not included // (ChangeAmount=0 means don't include any change output). 12: { UnspentOutputs: p2pkhOutputs(1e8), Outputs: p2pkhOutputs(1e8), RelayFee: 0, ChangeAmount: 0, InputCount: 1, }, } changeSource := func() ([]byte, error) { // Only length matters for these tests. return make([]byte, txsizes.P2PKHPkScriptSize), nil } for i, test := range tests { inputSource := makeInputSource(test.UnspentOutputs) tx, err := NewUnsignedTransaction(test.Outputs, test.RelayFee, inputSource, changeSource) switch e := err.(type) { case nil: case InputSourceError: if !test.InputSourceError { t.Errorf("Test %d: Returned InputSourceError but expected "+ "change output with amount %v", i, test.ChangeAmount) } continue default: t.Errorf("Test %d: Unexpected error: %v", i, e) continue } if tx.ChangeIndex < 0 { if test.ChangeAmount != 0 { t.Errorf("Test %d: No change output added but expected output with amount %v", i, test.ChangeAmount) continue } } else { changeAmount := btcutil.Amount(tx.Tx.TxOut[tx.ChangeIndex].Value) if test.ChangeAmount == 0 { t.Errorf("Test %d: Included change output with value %v but expected no change", i, changeAmount) continue } if changeAmount != test.ChangeAmount { t.Errorf("Test %d: Got change amount %v, Expected %v", i, changeAmount, test.ChangeAmount) continue } } if len(tx.Tx.TxIn) != test.InputCount { t.Errorf("Test %d: Used %d outputs from input source, Expected %d", i, len(tx.Tx.TxIn), test.InputCount) } } }