From 16ea90474525709b93613952568ad4a844888173 Mon Sep 17 00:00:00 2001 From: ziggie Date: Thu, 9 Feb 2023 13:41:27 +0100 Subject: [PATCH] Draft Approach --- cmd/sweepaccount/main.go | 60 +++++- go.mod | 2 + wallet/createtx.go | 14 +- wallet/psbt.go | 14 +- wallet/txauthor/author.go | 122 +++++++++++- wallet/txauthor/author_test.go | 348 +++++++++++++++++++++++++++++++++ wallet/txauthor/txselection.go | 243 +++++++++++++++++++++++ 7 files changed, 786 insertions(+), 17 deletions(-) create mode 100644 wallet/txauthor/txselection.go diff --git a/cmd/sweepaccount/main.go b/cmd/sweepaccount/main.go index 85284931f1..2756d5327b 100644 --- a/cmd/sweepaccount/main.go +++ b/cmd/sweepaccount/main.go @@ -22,6 +22,7 @@ import ( "github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/wallet/txsizes" + "github.com/btcsuite/btcwallet/wtxmgr" "github.com/jessevdk/go-flags" ) @@ -187,6 +188,55 @@ func makeInputSource(outputs []btcjson.ListUnspentResult) txauthor.InputSource { } } +func fetchInputs(outputs []btcjson.ListUnspentResult) ([]wtxmgr.Credit, error) { + var ( + totalInputValue btcutil.Amount + inputs = make([]wtxmgr.Credit, 0, len(outputs)) + sourceErr error + ) + for _, output := range outputs { + output := output + + outputAmount, err := btcutil.NewAmount(output.Amount) + if err != nil { + sourceErr = fmt.Errorf( + "invalid amount `%v` in listunspent result", + output.Amount) + break + } + if outputAmount == 0 { + continue + } + if !saneOutputValue(outputAmount) { + sourceErr = fmt.Errorf( + "impossible output amount `%v` in listunspent result", + outputAmount) + break + } + totalInputValue += outputAmount + + previousOutPoint, err := parseOutPoint(&output) + if err != nil { + sourceErr = fmt.Errorf( + "invalid data in listunspent result: %v", + err) + break + } + + inputs = append(inputs, wtxmgr.Credit{ + OutPoint: previousOutPoint, + Amount: outputAmount, + }) + } + + if sourceErr == nil && totalInputValue == 0 { + sourceErr = noInputValue{} + } + + return inputs, sourceErr + +} + // makeDestinationScriptSource creates a ChangeSource which is used to receive // all correlated previous input value. A non-change address is created by this // function. @@ -277,10 +327,14 @@ func sweep() error { numErrors++ } for _, previousOutputs := range sourceOutputs { - inputSource := makeInputSource(previousOutputs) + // inputSource := makeInputSource(previousOutputs) + inputs, err := fetchInputs(previousOutputs) + if err != nil { + return err + } destinationSource := makeDestinationScriptSource(rpcClient, opts.DestinationAccount) - tx, err := txauthor.NewUnsignedTransaction(nil, opts.FeeRate.Amount, - inputSource, destinationSource) + tx, err := txauthor.NewUnsignedTransaction2(nil, opts.FeeRate.Amount, + inputs, destinationSource) if err != nil { if err != (noInputValue{}) { reportError("Failed to create unsigned transaction: %v", err) diff --git a/go.mod b/go.mod index 5d65207b5d..40e0b37637 100644 --- a/go.mod +++ b/go.mod @@ -32,3 +32,5 @@ require ( ) go 1.16 + +replace github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 => /Users/ziggie-pro/working_freetime/btcwallet/wallet/txauthor diff --git a/wallet/createtx.go b/wallet/createtx.go index 7f0d75847c..cb98a23934 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -139,13 +139,16 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, return err } - var inputSource txauthor.InputSource + // var inputSource txauthor.InputSource + // We need to define the Selection Strategy + var inputSelectionStrategy txauthor.InputSelectionStrategy = txauthor.ConstantSelection switch coinSelectionStrategy { // Pick largest outputs first. case CoinSelectionLargest: sort.Sort(sort.Reverse(byAmount(eligible))) - inputSource = makeInputSource(eligible) + // inputSource = makeInputSource(eligible) + inputSelectionStrategy = txauthor.PositiveYieldingSelection // Select coins at random. This prevents the creation of ever // smaller utxos over time that may never become economical to @@ -170,12 +173,13 @@ func (w *Wallet) txToOutputs(outputs []*wire.TxOut, positivelyYielding[i], positivelyYielding[j] = positivelyYielding[j], positivelyYielding[i] }) + inputSelectionStrategy = txauthor.RandomSelection - inputSource = makeInputSource(positivelyYielding) + // inputSource = makeInputSource(positivelyYielding) } - tx, err = txauthor.NewUnsignedTransaction( - outputs, feeSatPerKb, inputSource, changeSource, + tx, err = txauthor.NewUnsignedTransaction2( + outputs, feeSatPerKb, eligible, inputSelectionStrategy, changeSource, ) if err != nil { return err diff --git a/wallet/psbt.go b/wallet/psbt.go index 8b81bfad28..0278741632 100644 --- a/wallet/psbt.go +++ b/wallet/psbt.go @@ -178,7 +178,7 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope, PkScript: utxo.PkScript, } } - inputSource := constantInputSource(credits) + // inputSource := constantInputSource(credits) // Build the TxCreateOption to retrieve the change scope. opts := defaultTxCreateOptions() @@ -197,25 +197,25 @@ func (w *Wallet) FundPsbt(packet *psbt.Packet, keyScope *waddrmgr.KeyScope, dbtx, opts.changeKeyScope, account, ) if err != nil { - return err + return fmt.Errorf("could not add change address to "+ + "database: %v", err) } // Ask the txauthor to create a transaction with our // selected coins. This will perform fee estimation and // add a change output if necessary. - tx, err = txauthor.NewUnsignedTransaction( - txOut, feeSatPerKB, inputSource, changeSource, + tx, err = txauthor.NewUnsignedTransaction2( + txOut, feeSatPerKB, credits, txauthor.ConstantSelection, changeSource, ) if err != nil { return fmt.Errorf("fee estimation not "+ - "successful: %v", err) + "successful: %w", err) } return nil }) if err != nil { - return 0, fmt.Errorf("could not add change address to "+ - "database: %v", err) + return 0, err } } diff --git a/wallet/txauthor/author.go b/wallet/txauthor/author.go index 2ac83eab40..a10b0a46fc 100644 --- a/wallet/txauthor/author.go +++ b/wallet/txauthor/author.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/wallet/txsizes" + "github.com/btcsuite/btcwallet/wtxmgr" ) // SumOutputValues sums up the list of TxOuts and returns an Amount. @@ -43,9 +44,12 @@ type InputSourceError interface { } // Default implementation of InputSourceError. -type insufficientFundsError struct{} +type insufficientFundsError struct { +} + +func (insufficientFundsError) InputSourceError() { -func (insufficientFundsError) InputSourceError() {} +} func (insufficientFundsError) Error() string { return "insufficient funds available to construct transaction" } @@ -168,6 +172,120 @@ func NewUnsignedTransaction(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount, } } +// NewUnsignedTransaction2 is an experimental function to solve the fee-calculation problem which is appareant +// in NewUnsignedTransaction. +func NewUnsignedTransaction2(outputs []*wire.TxOut, feeRatePerKb btcutil.Amount, + inputs []wtxmgr.Credit, selectionStrategy InputSelectionStrategy, changeSource *ChangeSource) (*AuthoredTx, error) { + + changeScript, err := changeSource.NewScript() + if err != nil { + return nil, err + } + + targetAmount := SumOutputValues(outputs) + + inputSelection := txSelector{ + inputState: &inputState{ + feeRatePerKb: feeRatePerKb, + targetAmount: targetAmount, + outputs: outputs, + selectionStrategy: selectionStrategy, + changeOutpoint: wire.TxOut{ + PkScript: changeScript, + }, + }, + } + + switch selectionStrategy { + + case PositiveYieldingSelection, RandomSelection: + + // We look through all our inputs and add an input until + // the amount is enough to pay the target amount and the + // transaction fees. + for _, input := range inputs { + if !inputSelection.inputState.enoughInput() { + + // We try to add a new input but we only consider + // positive yielding inputs. Therefore when the input + // is not added we check if the inputs where ordered + // according to their size, which is the case when the + // inputselection strategy is positive yielding. Then we + // abort quickly because all follwing outputs will be + // negative yielding as well. + if !inputSelection.add(input) && + selectionStrategy == PositiveYieldingSelection { + + return nil, txSelectionError{ + targetAmount: inputSelection.inputState.targetAmount, + txFee: inputSelection.inputState.txFee, + availableAmt: inputSelection.inputState.inputTotal, + } + } + + continue + } + + // We stop obviously considering inputs when the input amount + // is enough to fund the transaction. + break + } + + case ConstantSelection: + // In case of a constant selection all inputs are added + // although they might be negative yielding so we do not + // check for the return value. + inputSelection.add(inputs...) + } + + // This check is needed to make sure we have enough inputs + // after going trough all eligable inputs. + if !inputSelection.inputState.enoughInput() { + return nil, txSelectionError{ + targetAmount: inputSelection.inputState.targetAmount, + txFee: inputSelection.inputState.txFee, + availableAmt: inputSelection.inputState.inputTotal, + } + } + + // We need the inputs in the right format for the followup functions. + txIn := make([]*wire.TxIn, 0, len(inputSelection.inputState.inputs)) + inputValues := make([]btcutil.Amount, 0, len(inputSelection.inputState.inputs)) + + for _, input := range inputSelection.inputState.inputs { + txIn = append(txIn, wire.NewTxIn(&input.OutPoint, nil, nil)) + inputValues = append(inputValues, input.Amount) + } + + unsignedTransaction := &wire.MsgTx{ + Version: wire.TxVersion, + TxIn: txIn, + TxOut: outputs, + LockTime: 0, + } + + // Default is no change output. We check if changeOutpoint in the + // input state is above dust, and add the changeoutput to the + // transaction. + changeIndex := -1 + if !txrules.IsDustOutput(&inputSelection.inputState.changeOutpoint, + txrules.DefaultRelayFeePerKb) { + + l := len(outputs) + unsignedTransaction.TxOut = append(outputs[:l:l], &inputSelection.inputState.changeOutpoint) + changeIndex = l + } + + return &AuthoredTx{ + Tx: unsignedTransaction, + PrevScripts: inputSelection.inputState.scripts, + PrevInputValues: inputValues, + TotalInput: inputSelection.inputState.inputTotal, + 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. diff --git a/wallet/txauthor/author_test.go b/wallet/txauthor/author_test.go index 1dbb6b68b8..14e2444a62 100644 --- a/wallet/txauthor/author_test.go +++ b/wallet/txauthor/author_test.go @@ -8,9 +8,11 @@ import ( "testing" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet/txrules" "github.com/btcsuite/btcwallet/wallet/txsizes" + "github.com/btcsuite/btcwallet/wtxmgr" ) func p2pkhOutputs(amounts ...btcutil.Amount) []*wire.TxOut { @@ -22,6 +24,98 @@ func p2pkhOutputs(amounts ...btcutil.Amount) []*wire.TxOut { return v } +type inputType uint8 + +const ( + p2pkh inputType = iota + p2wkh + p2npkh + p2tr +) + +type testOutput struct { + amount btcutil.Amount + inputType inputType +} + +// createOutput creates outputs of a transaction depending on their +// output script. +func createOutput(testOutputs ...testOutput) []*wire.TxOut { + + outputs := make([]*wire.TxOut, 0, len(testOutputs)) + var outScript []byte + + for _, output := range testOutputs { + switch output.inputType { + + case p2pkh: + outScript = make([]byte, txsizes.P2PKHPkScriptSize) + + case p2wkh: + outScript = make([]byte, txsizes.P2WPKHPkScriptSize) + + case p2npkh: + outScript = make([]byte, txsizes.NestedP2WPKHPkScriptSize) + + case p2tr: + outScript = make([]byte, txsizes.P2TRPkScriptSize) + + } + outputs = append(outputs, wire.NewTxOut(int64(output.amount), outScript)) + } + return outputs +} + +// createCredit creates the unspent outputs for the transaction in the right format. +func createCredit(txIn ...testOutput) []wtxmgr.Credit { + + credits := make([]wtxmgr.Credit, len(txIn)) + + var ( + zeroV1KeyPush = [34]byte{ + txscript.OP_1, txscript.OP_DATA_32, // 32 byte of zeroes here + } + + zeroV0KeyPush = [22]byte{ + txscript.OP_0, txscript.OP_DATA_20, // 32 byte of zeroes here + } + + zeroScriptPush = [23]byte{txscript.OP_0, txscript.OP_DATA_20, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + txscript.OP_EQUAL} + + zeroLegacyKeyPush = [25]byte{txscript.OP_DUP, txscript.OP_HASH160, + txscript.OP_DATA_20, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + txscript.OP_EQUALVERIFY, txscript.OP_CHECKSIG} + ) + + var pkScript []byte + + for idx, in := range txIn { + switch in.inputType { + + case p2pkh: + pkScript = zeroLegacyKeyPush[:] + + case p2wkh: + pkScript = zeroV0KeyPush[:] + + case p2npkh: + pkScript = zeroScriptPush[:] + case p2tr: + pkScript = zeroV1KeyPush[:] + } + + credits[idx] = wtxmgr.Credit{ + OutPoint: wire.OutPoint{}, + Amount: btcutil.Amount(in.amount), + PkScript: pkScript, + } + } + return credits +} + func makeInputSource(unspents []*wire.TxOut) InputSource { // Return outputs in order. currentTotal := btcutil.Amount(0) @@ -222,3 +316,257 @@ func TestNewUnsignedTransaction(t *testing.T) { } } } + +func TestNewUnsignedTransaction2(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + Credit []testOutput + Outputs []testOutput + RelayFee btcutil.Amount + ChangeAmount btcutil.Amount + InputSourceError bool + InputCount int + SelectionStrategy InputSelectionStrategy + }{ + 0: { + name: "insufficient funds", + Credit: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + Outputs: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + RelayFee: 1e3, + InputSourceError: true, + SelectionStrategy: PositiveYieldingSelection, + }, + 1: { + name: "1 input and 1 output + change", + Credit: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + Outputs: []testOutput{{ + amount: 1e6, inputType: p2wkh}, + }, + RelayFee: 1e3, + ChangeAmount: 1e8 - 1e6 - txrules.FeeForSerializeSize(1e3, + txsizes.EstimateVirtualSize(0, 0, 1, 0, createOutput(testOutput{ + amount: 1e6, inputType: p2wkh}), + txsizes.P2WPKHPkScriptSize)), + InputCount: 1, + SelectionStrategy: PositiveYieldingSelection, + }, + 2: { + name: "1 input and 1 output but no change paying exactly 10 sat/vbyte", + Credit: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + // 110 is the txsize in vbytes of a transaction with 1 P2WKH input and 1 P2WKH output. + Outputs: []testOutput{{ + amount: 1e8 - 1100, inputType: p2wkh}, + }, + RelayFee: 1e4, + ChangeAmount: 0, + InputCount: 1, + }, + 3: { + name: "1 input and 1 output plus change which is exactly the dustlimit(p2wkh)", + Credit: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + // 110 is the txsize in vbytes of a transaction with 1 P2WKH input and 1 P2WKH output. + // 31 is the size of the additional P2WKH change output. + Outputs: []testOutput{{ + amount: 1e8 - 110 - 31 - 294, inputType: p2wkh}, + }, + RelayFee: 1e3, + ChangeAmount: 294, + InputCount: 1, + }, + 4: { + name: "1 input and 1 output aiming for 1 sat/vbyte but Changeoutput is below dustlimit 294" + + "leading to a higher feerate because the change gets purged", + Credit: []testOutput{{ + amount: 1e8, inputType: p2wkh}, + }, + // 122 is the txsize in vbytes of a transaction with 1 P2WKH input and 1 P2TR output. + // 31 is the size of the P2WKH output and 294 is the Dustlimit for a P2WKH. + Outputs: []testOutput{{ + amount: 1e8 - 122 - 31 - 293, inputType: p2tr}, + }, + RelayFee: 1e3, + ChangeAmount: 0, + InputCount: 1, + }, + 5: { + name: "2 inputs with the first input negative yielding and 1 output plus change", + Credit: []testOutput{ + {amount: 1530, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{{ + amount: 1e4, inputType: p2tr}, + }, + RelayFee: 1e4, + // 1530 sat is the fee when spending 1 P2WKH input and 1 P2TR output + P2WKH change + // at the defined fee-level. + ChangeAmount: 1e6 - 1530 - 1e4, + InputCount: 1, + SelectionStrategy: RandomSelection, + }, + 6: { + name: "2 inputs with the first input slightly positive yielding and 1 output plus change", + Credit: []testOutput{ + {amount: 1531, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{{ + amount: 1e4, inputType: p2tr}, + }, + RelayFee: 1e4, + // 2220 sat is the fee when spending 2 P2WKH input and 1 P2TR output + P2WKH change + // at the defined fee-level. + // 1530 sat is the fee when spending 1 P2WKH input and 1 P2TR output + P2WKH change + // at the defined fee-level. + ChangeAmount: 1e6 + 1531 - 2220 - 1e4, + InputCount: 2, + SelectionStrategy: RandomSelection, + }, + 7: { + name: "2 inputs with the first input negative yielding but constant input selection" + + "plus change", + Credit: []testOutput{ + {amount: 330, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{}, + RelayFee: 1e4, + // 1790 sat is the fee when spending 2 P2WKH input and a P2WKH change + // at the defined fee-level. + ChangeAmount: 1e6 - 1790 + 330, + InputCount: 2, + SelectionStrategy: ConstantSelection, + }, + 8: { + name: "2 initial inputs but only 1 input is sufficient (postive yielding)", + Credit: []testOutput{ + {amount: 1e6, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{ + {amount: 1e4, inputType: p2tr}, + }, + RelayFee: 1e3, + // 153 is the tx size in vbytes with 1 P2WKH input and 1 P2TR output + // plus 1 P2WKH change output. + ChangeAmount: 1e6 - 153 - 1e4, + InputCount: 1, + SelectionStrategy: PositiveYieldingSelection, + }, + 9: { + name: "3 inputs with a constant input selection" + + "1 output plus change", + Credit: []testOutput{ + {amount: 100, inputType: p2wkh}, + {amount: 100, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{ + {amount: 1e4, inputType: p2tr}, + }, + RelayFee: 1e3, + // 290 is the tx size in vbytes with 3 P2WKH Inputs and 1 P2TR output + // plus 1 P2WKH change output. + ChangeAmount: 1e6 + 200 - 290 - 1e4, + InputCount: 3, + SelectionStrategy: ConstantSelection, + }, + 10: { + name: "2 initial inputs with a positive yielding selection" + + "failing because first input is negative yielding", + Credit: []testOutput{ + {amount: 100, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{ + {amount: 1e4, inputType: p2tr}, + }, + RelayFee: 1e3, + InputSourceError: true, + SelectionStrategy: PositiveYieldingSelection, + }, + 11: { + name: "2 initial inputs with a positive yielding selection" + + "where both are needed", + Credit: []testOutput{ + {amount: 1e6, inputType: p2wkh}, + {amount: 1e6, inputType: p2wkh}, + }, + Outputs: []testOutput{ + {amount: 1.1e6, inputType: p2tr}, + }, + RelayFee: 1e3, + // 222 is the tx size in vbytes with 2 P2WKH Inputs and 1 P2TR output + // plus 1 P2WKH change output. + ChangeAmount: 2*1e6 - 222 - (1.1e6), + InputCount: 2, + SelectionStrategy: PositiveYieldingSelection, + }, + } + + changeSource := &ChangeSource{ + NewScript: func() ([]byte, error) { + // Only length matters for these tests. + pkScript := make([]byte, txsizes.P2WPKHPkScriptSize) + // We need to make sure that the pkScript looks like + // a common P2WKH script otherwise the dustlimit is + // calculated wrongly. + pkScript[1] = txscript.OP_DATA_20 + return pkScript, nil + }, + ScriptSize: txsizes.P2WPKHPkScriptSize, + } + + for i, test := range tests { + inputSource := createCredit(test.Credit...) + outputs := createOutput(test.Outputs...) + tx, err := NewUnsignedTransaction2(outputs, test.RelayFee, inputSource, test.SelectionStrategy, 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) + } + } +} diff --git a/wallet/txauthor/txselection.go b/wallet/txauthor/txselection.go new file mode 100644 index 0000000000..3a22b35fa2 --- /dev/null +++ b/wallet/txauthor/txselection.go @@ -0,0 +1,243 @@ +package txauthor + +import ( + "fmt" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcwallet/wallet/txrules" + "github.com/btcsuite/btcwallet/wallet/txsizes" + "github.com/btcsuite/btcwallet/wtxmgr" +) + +type txSelectionError struct { + targetAmount btcutil.Amount + txFee btcutil.Amount + availableAmt btcutil.Amount +} + +func (txSelectionError) InputSourceError() { + +} +func (e txSelectionError) Error() string { + return fmt.Sprintf("insufficient funds available to construct transaction: "+ + "amount: %v, minimum fee: %v, available amount: %v", e.targetAmount, + e.txFee, e.availableAmt) +} + +// InputSelectionStrategy defines how funds are selected when +// building and UnsignedTransaction +type InputSelectionStrategy int + +const ( + // PositiveYieldingSelection requires the inputs to be + // ordered by amount so that we can fail early in case + // an input is not positive yielding + PositiveYieldingSelection InputSelectionStrategy = iota + + // RandomSelection means there could still be some inputs + // which are larger than the previous one therefore we + // will loop through all the inputs. + RandomSelection + + // ConstantSelection allows use to consider all inputs + // in one batch withtout checking for positive yield. + ConstantSelection +) + +type inputState struct { + // feeRatePerKb is the feerate which is used for fee calculation. + feeRatePerKb btcutil.Amount + + // txFee of the current transaction state when serialized in satoshis. + txFee btcutil.Amount + + // inputTotal is the total value of all inputs. + inputTotal btcutil.Amount + + // Amount we want to fund + targetAmount btcutil.Amount + + // changeOutpoint is the change output of the transaction. This will + // be what is left over after subtracting the targetAmount and + // the tx fee from the inputTotal. + // + // NOTE: This (value) might be below the dust limit, or even negative since it + // is the change remaining in case we pay the fee for a change output. + changeOutpoint wire.TxOut + + // inputs is the set of tx inputs which will be usedd to create the transaction. + inputs []wtxmgr.Credit + + // outputs are the outputs of the transaction not including the change. + outputs []*wire.TxOut + + // scripts are kept separate to fullfill the api. + scripts [][]byte + + // selectionStrategy decides whether we addd negative yielding inputs and + // fails prematurely in case the inputs are ordered. + selectionStrategy InputSelectionStrategy +} + +// virutalSizeEstimate is the (worst case) tx weight with the current set of +// inputs. It takes a parameter whether to add a change output or not. +func (t *inputState) virutalSizeEstimate(change bool) int { + + // We count the types of inputs, which we'll use to estimate + // the vsize of the transaction. + var nested, p2wpkh, p2tr, p2pkh int + for _, input := range t.inputs { + pkScript := input.PkScript + switch { + // If this is a p2sh output, we assume this is a + // nested P2WKH. + case txscript.IsPayToScriptHash(pkScript): + nested++ + case txscript.IsPayToWitnessPubKeyHash(pkScript): + p2wpkh++ + case txscript.IsPayToTaproot(pkScript): + p2tr++ + default: + p2pkh++ + } + } + + var maxSignedSize int + if change { + maxSignedSize = txsizes.EstimateVirtualSize( + p2pkh, p2tr, p2wpkh, nested, t.outputs, len(t.changeOutpoint.PkScript), + ) + } else { + maxSignedSize = txsizes.EstimateVirtualSize( + p2pkh, p2tr, p2wpkh, nested, t.outputs, 0, + ) + } + return maxSignedSize + +} + +// enoughInput returns true if we've accumulated enough inputs to pay the fees +// and have at least one output that meets the dust limit. +func (t *inputState) enoughInput() bool { + // If we have a change output above dust, then we certainly have enough + // inputs to the transaction. + if !txrules.IsDustOutput(&t.changeOutpoint, + txrules.DefaultRelayFeePerKb) { + return true + } + + // We did not have enough input for a change output. Check if we have + // enough input to pay the fees for a transaction with no change + // output. + fee := txrules.FeeForSerializeSize(t.feeRatePerKb, t.virutalSizeEstimate(false)) + t.txFee = fee + + if t.inputTotal < t.targetAmount+fee { + return false + } + + // We passed all the checks and can pay for the transactonfees. + + // NOTE There is no check performed here whether the transaction has an output. + // This should be done before or downstream. + return true +} + +func (t *inputState) clone() inputState { + s := inputState{ + feeRatePerKb: t.feeRatePerKb, + txFee: t.txFee, + inputTotal: t.inputTotal, + changeOutpoint: t.changeOutpoint, + selectionStrategy: t.selectionStrategy, + targetAmount: t.targetAmount, + scripts: make([][]byte, len(t.scripts)), + outputs: make([]*wire.TxOut, len(t.outputs)), + inputs: make([]wtxmgr.Credit, len(t.inputs)), + } + + copy(s.scripts, t.scripts) + copy(s.outputs, t.outputs) + copy(s.inputs, t.inputs) + + return s +} + +// totalOutput is the total amount left for us after paying fees. +// +// NOTE: This might be dust. +func (t *inputState) totalOutput() btcutil.Amount { + // We return an output amount of 0 so that the first + // input is added successfully. Otherwise adding the + // first amount would fails as long its smaller than + // the target amount. This allows us to successfully + // evaluate the first input to the state according to + // its yielding. + if len(t.inputs) == 0 { + return 0 + } + + return t.targetAmount + btcutil.Amount(t.changeOutpoint.Value) +} + +// add adds a new input to the set. It returns a bool indicating whether the +// input was added to the set. An input is rejected if it decreases the tx +// output value after paying fees. +func (t *inputState) addToState(inputs ...wtxmgr.Credit) *inputState { + + // Clone the current set state. + tempInputState := t.clone() + + for _, input := range inputs { + tempInputState.inputs = append(tempInputState.inputs, input) + tempInputState.scripts = append(tempInputState.scripts, input.PkScript) + tempInputState.inputTotal += input.Amount + } + + // Recalculate the tx fee. + fee := txrules.FeeForSerializeSize(tempInputState.feeRatePerKb, tempInputState.virutalSizeEstimate(true)) + tempInputState.txFee = fee + + tempInputState.changeOutpoint.Value = int64(tempInputState.inputTotal - tempInputState.targetAmount - fee) + + // Calculate the yield of this input from the change in total tx output + // value. + inputYield := tempInputState.totalOutput() - t.totalOutput() + + switch t.selectionStrategy { + // Don't add inputs that cost more for us to use. + case PositiveYieldingSelection, RandomSelection: + // Because our starting value for the change output + // is the negative target amount we make sure we always + // include postive yielding inputs only. + if inputYield <= 0 { + return nil + } + + // ConstantSelection does not include an yield check. All inputs + // are used for the transaction. + case ConstantSelection: + } + + return &tempInputState +} + +// txSelector is needed to swap the input states if the +// addToState function is successful. +type txSelector struct { + inputState *inputState +} + +// add tries to add the input to the current inputState. +func (t *txSelector) add(input ...wtxmgr.Credit) bool { + newState := t.inputState.addToState(input...) + if newState == nil { + return false + } + + t.inputState = newState + + return true +}