Refactor txToPairs into smaller functions
Also adds tests for those functions, and improve fee estimation.
This commit is contained in:
parent
ccb2b1e16d
commit
ec8a5bc10c
2 changed files with 409 additions and 169 deletions
359
createtx.go
359
createtx.go
|
@ -17,7 +17,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
badrand "math/rand"
|
badrand "math/rand"
|
||||||
|
@ -32,16 +31,54 @@ import (
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
// InsufficientFunds represents an error where there are not enough
|
const (
|
||||||
|
// All transactions have 4 bytes for version, 4 bytes of locktime,
|
||||||
|
// and 2 varints for the number of inputs and outputs.
|
||||||
|
txOverheadEstimate = 4 + 4 + 1 + 1
|
||||||
|
|
||||||
|
// A best case signature script to redeem a P2PKH output for a
|
||||||
|
// compressed pubkey has 70 bytes of the smallest possible DER signature
|
||||||
|
// (with no leading 0 bytes for R and S), 33 bytes of serialized pubkey,
|
||||||
|
// and data push opcodes for both, plus one byte for the hash type flag
|
||||||
|
// appended to the end of the signature.
|
||||||
|
sigScriptEstimate = 1 + 70 + 1 + 33 + 1
|
||||||
|
|
||||||
|
// A best case tx input serialization cost is 32 bytes of sha, 4 bytes
|
||||||
|
// of output index, 4 bytes of sequnce, and the estimated signature
|
||||||
|
// script size.
|
||||||
|
txInEstimate = 32 + 4 + 4 + sigScriptEstimate
|
||||||
|
|
||||||
|
// A P2PKH pkScript contains the following bytes:
|
||||||
|
// - OP_DUP
|
||||||
|
// - OP_HASH160
|
||||||
|
// - OP_DATA_20 + 20 bytes of pubkey hash
|
||||||
|
// - OP_EQUALVERIFY
|
||||||
|
// - OP_CHECKSIG
|
||||||
|
pkScriptEstimate = 1 + 1 + 1 + 20 + 1 + 1
|
||||||
|
|
||||||
|
// A best case tx output serialization cost is 8 bytes of value, one
|
||||||
|
// byte of varint, and the pkScript size.
|
||||||
|
txOutEstimate = 8 + 1 + pkScriptEstimate
|
||||||
|
)
|
||||||
|
|
||||||
|
func estimateTxSize(numInputs, numOutputs int) int {
|
||||||
|
return txOverheadEstimate + txInEstimate*numInputs + txOutEstimate*numOutputs
|
||||||
|
}
|
||||||
|
|
||||||
|
func feeForSize(incr btcutil.Amount, sz int) btcutil.Amount {
|
||||||
|
return btcutil.Amount(1+sz/1000) * incr
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsufficientFundsError represents an error where there are not enough
|
||||||
// funds from unspent tx outputs for a wallet to create a transaction.
|
// funds from unspent tx outputs for a wallet to create a transaction.
|
||||||
// This may be caused by not enough inputs for all of the desired total
|
// This may be caused by not enough inputs for all of the desired total
|
||||||
// transaction output amount, or due to
|
// transaction output amount, or due to
|
||||||
type InsufficientFunds struct {
|
type InsufficientFundsError struct {
|
||||||
in, out, fee btcutil.Amount
|
in, out, fee btcutil.Amount
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error satisifies the builtin error interface.
|
// Error satisifies the builtin error interface.
|
||||||
func (e InsufficientFunds) Error() string {
|
func (e InsufficientFundsError) Error() string {
|
||||||
total := e.out + e.fee
|
total := e.out + e.fee
|
||||||
if e.fee == 0 {
|
if e.fee == 0 {
|
||||||
return fmt.Sprintf("insufficient funds: transaction requires "+
|
return fmt.Sprintf("insufficient funds: transaction requires "+
|
||||||
|
@ -80,44 +117,14 @@ func (u ByAmount) Len() int { return len(u) }
|
||||||
func (u ByAmount) Less(i, j int) bool { return u[i].Amount() < u[j].Amount() }
|
func (u ByAmount) Less(i, j int) bool { return u[i].Amount() < u[j].Amount() }
|
||||||
func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
func (u ByAmount) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||||
|
|
||||||
// selectInputs selects the minimum number possible of unspent
|
|
||||||
// outputs to use to create a new transaction that spends amt satoshis.
|
|
||||||
// btcout is the total number of satoshis which would be spent by the
|
|
||||||
// combination of all selected previous outputs. err will equal
|
|
||||||
// ErrInsufficientFunds if there are not enough unspent outputs to spend amt
|
|
||||||
// amt.
|
|
||||||
func selectInputs(eligible []txstore.Credit, amt, fee btcutil.Amount,
|
|
||||||
minconf int) (selected []txstore.Credit, out btcutil.Amount, err error) {
|
|
||||||
|
|
||||||
// Iterate throguh eligible transactions, appending to outputs and
|
|
||||||
// increasing out. This is finished when out is greater than the
|
|
||||||
// requested amt to spend.
|
|
||||||
selected = make([]txstore.Credit, 0, len(eligible))
|
|
||||||
for _, e := range eligible {
|
|
||||||
selected = append(selected, e)
|
|
||||||
out += e.Amount()
|
|
||||||
if out >= amt+fee {
|
|
||||||
return selected, out, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if out < amt+fee {
|
|
||||||
return nil, 0, InsufficientFunds{out, amt, fee}
|
|
||||||
}
|
|
||||||
|
|
||||||
return selected, out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// txToPairs creates a raw transaction sending the amounts for each
|
// txToPairs creates a raw transaction sending the amounts for each
|
||||||
// address/amount pair and fee to each address and the miner. minconf
|
// address/amount pair and fee to each address and the miner. minconf
|
||||||
// specifies the minimum number of confirmations required before an
|
// specifies the minimum number of confirmations required before an
|
||||||
// unspent output is eligible for spending. Leftover input funds not sent
|
// unspent output is eligible for spending. Leftover input funds not sent
|
||||||
// to addr or as a fee for the miner are sent to a newly generated
|
// to addr or as a fee for the miner are sent to a newly generated
|
||||||
// address. If change is needed to return funds back to an owned
|
// address. InsufficientFundsError is returned if there are not enough
|
||||||
// address, changeUtxo will point to a unconfirmed (height = -1, zeroed
|
// eligible unspent outputs to create the transaction.
|
||||||
// block hash) Utxo. ErrInsufficientFunds is returned if there are not
|
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount, minconf int) (*CreatedTx, error) {
|
||||||
// enough eligible unspent outputs to create the transaction.
|
|
||||||
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
|
|
||||||
minconf int) (*CreatedTx, error) {
|
|
||||||
|
|
||||||
// Key store must be unlocked to compose transaction. Grab the
|
// Key store must be unlocked to compose transaction. Grab the
|
||||||
// unlock if possible (to prevent future unlocks), or return the
|
// unlock if possible (to prevent future unlocks), or return the
|
||||||
|
@ -128,122 +135,135 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
|
||||||
}
|
}
|
||||||
defer heldUnlock.Release()
|
defer heldUnlock.Release()
|
||||||
|
|
||||||
// Create a new transaction which will include all input scripts.
|
|
||||||
msgtx := btcwire.NewMsgTx()
|
|
||||||
|
|
||||||
// Calculate minimum amount needed for inputs.
|
|
||||||
var amt btcutil.Amount
|
|
||||||
for _, v := range pairs {
|
|
||||||
// Error out if any amount is negative.
|
|
||||||
if v <= 0 {
|
|
||||||
return nil, ErrNonPositiveAmount
|
|
||||||
}
|
|
||||||
amt += v
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = addOutputs(msgtx, pairs); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current block's height and hash.
|
// Get current block's height and hash.
|
||||||
bs, err := w.chainSvr.BlockStamp()
|
bs, err := w.chainSvr.BlockStamp()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
eligible, err := w.findEligibleOuptuts(minconf, bs)
|
eligible, err := w.findEligibleOutputs(minconf, bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Sort eligible inputs, as selectInputs expects these to be sorted
|
|
||||||
// by amount in reverse order.
|
return createTx(eligible, pairs, bs, w.FeeIncrement, w.KeyStore, w.changeAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTx selects inputs (from the given slice of eligible utxos)
|
||||||
|
// whose amount are sufficient to fulfil all the desired outputs plus
|
||||||
|
// the mining fee. It then creates and returns a CreatedTx containing
|
||||||
|
// the selected inputs and the given outputs, validating it (using
|
||||||
|
// validateMsgTx) as well.
|
||||||
|
func createTx(
|
||||||
|
eligible []txstore.Credit,
|
||||||
|
outputs map[string]btcutil.Amount,
|
||||||
|
bs *keystore.BlockStamp,
|
||||||
|
feeIncrement btcutil.Amount,
|
||||||
|
keys *keystore.Store,
|
||||||
|
changeAddress func(*keystore.BlockStamp) (btcutil.Address, error)) (
|
||||||
|
*CreatedTx, error) {
|
||||||
|
|
||||||
|
msgtx := btcwire.NewMsgTx()
|
||||||
|
minAmount, err := addOutputs(msgtx, outputs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort eligible inputs so that we first pick the ones with highest
|
||||||
|
// amount, thus reducing number of inputs.
|
||||||
sort.Sort(sort.Reverse(ByAmount(eligible)))
|
sort.Sort(sort.Reverse(ByAmount(eligible)))
|
||||||
|
|
||||||
var selectedInputs []txstore.Credit
|
// Start by adding enough inputs to cover for the total amount of all
|
||||||
// changeAddr is nil/zeroed until a change address is needed, and reused
|
// desired outputs.
|
||||||
// again in case a change utxo has already been chosen.
|
var input txstore.Credit
|
||||||
|
var inputs []txstore.Credit
|
||||||
|
totalAdded := btcutil.Amount(0)
|
||||||
|
for totalAdded < minAmount {
|
||||||
|
if len(eligible) == 0 {
|
||||||
|
return nil, InsufficientFundsError{totalAdded, minAmount, 0}
|
||||||
|
}
|
||||||
|
input, eligible = eligible[0], eligible[1:]
|
||||||
|
inputs = append(inputs, input)
|
||||||
|
msgtx.AddTxIn(btcwire.NewTxIn(input.OutPoint(), nil))
|
||||||
|
totalAdded += input.Amount()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an initial fee estimate based on the number of selected inputs
|
||||||
|
// and added outputs, with no change.
|
||||||
|
szEst := estimateTxSize(len(inputs), len(msgtx.TxOut))
|
||||||
|
feeEst := minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height)
|
||||||
|
|
||||||
|
// Now make sure the sum amount of all our inputs is enough for the
|
||||||
|
// sum amount of all outputs plus the fee. If necessary we add more,
|
||||||
|
// inputs, but in that case we also need to recalculate the fee.
|
||||||
|
for totalAdded < minAmount+feeEst {
|
||||||
|
if len(eligible) == 0 {
|
||||||
|
return nil, InsufficientFundsError{totalAdded, minAmount, feeEst}
|
||||||
|
}
|
||||||
|
input, eligible = eligible[0], eligible[1:]
|
||||||
|
inputs = append(inputs, input)
|
||||||
|
msgtx.AddTxIn(btcwire.NewTxIn(input.OutPoint(), nil))
|
||||||
|
szEst += txInEstimate
|
||||||
|
totalAdded += input.Amount()
|
||||||
|
feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height)
|
||||||
|
}
|
||||||
|
|
||||||
var changeAddr btcutil.Address
|
var changeAddr btcutil.Address
|
||||||
var changeIdx int
|
// changeIdx is -1 unless there's a change output.
|
||||||
|
changeIdx := -1
|
||||||
|
|
||||||
// Make a copy of msgtx before any inputs are added. This will be
|
|
||||||
// used as a starting point when trying a fee and starting over with
|
|
||||||
// a higher fee if not enough was originally chosen.
|
|
||||||
txNoInputs := msgtx.Copy()
|
|
||||||
|
|
||||||
// Get the number of satoshis to increment fee by when searching for
|
|
||||||
// the minimum tx fee needed.
|
|
||||||
fee := btcutil.Amount(0)
|
|
||||||
for {
|
for {
|
||||||
msgtx = txNoInputs.Copy()
|
change := totalAdded - minAmount - feeEst
|
||||||
changeIdx = -1
|
|
||||||
|
|
||||||
// Select eligible outputs to be used in transaction based on the amount
|
|
||||||
// needed to be sent, and the current fee estimation.
|
|
||||||
inputs, btcin, err := selectInputs(eligible, amt, fee, minconf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are leftover unspent outputs, and return coins back to
|
|
||||||
// a new address we own.
|
|
||||||
change := btcin - amt - fee
|
|
||||||
if change > 0 {
|
if change > 0 {
|
||||||
// Get a new change address if one has not already been found.
|
|
||||||
if changeAddr == nil {
|
if changeAddr == nil {
|
||||||
changeAddr, err = w.KeyStore.ChangeAddress(bs)
|
changeAddr, err = changeAddress(bs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get next address: %s", err)
|
return nil, err
|
||||||
}
|
|
||||||
w.KeyStore.MarkDirty()
|
|
||||||
err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("cannot request updates for "+
|
|
||||||
"change address: %v", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spend change.
|
changeIdx, err = addChange(msgtx, change, changeAddr)
|
||||||
pkScript, err := btcscript.PayToAddrScript(changeAddr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("cannot create txout script: %s", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
msgtx.AddTxOut(btcwire.NewTxOut(int64(change), pkScript))
|
|
||||||
|
|
||||||
// Randomize index of the change output.
|
|
||||||
rng := badrand.New(badrand.NewSource(time.Now().UnixNano()))
|
|
||||||
r := rng.Int31n(int32(len(msgtx.TxOut))) // random index
|
|
||||||
c := len(msgtx.TxOut) - 1 // change index
|
|
||||||
msgtx.TxOut[r], msgtx.TxOut[c] = msgtx.TxOut[c], msgtx.TxOut[r]
|
|
||||||
changeIdx = int(r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = w.addInputsToTx(msgtx, inputs); err != nil {
|
if err = signMsgTx(msgtx, inputs, keys); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
noFeeAllowed := false
|
if feeForSize(feeIncrement, msgtx.SerializeSize()) <= feeEst {
|
||||||
if !cfg.DisallowFree {
|
// The required fee for this size is less than or equal to what
|
||||||
noFeeAllowed = allowFree(bs.Height, inputs, msgtx.SerializeSize())
|
// we guessed, so we're done.
|
||||||
}
|
|
||||||
if minFee := minimumFee(w.FeeIncrement, msgtx, noFeeAllowed); fee < minFee {
|
|
||||||
fee = minFee
|
|
||||||
} else {
|
|
||||||
selectedInputs = inputs
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if change > 0 {
|
||||||
|
// Remove the change output since the next iteration will add
|
||||||
|
// it again (with a new amount) if necessary.
|
||||||
|
tmp := msgtx.TxOut[:changeIdx]
|
||||||
|
tmp = append(tmp, msgtx.TxOut[changeIdx+1:]...)
|
||||||
|
msgtx.TxOut = tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateMsgTx(msgtx, selectedInputs); err != nil {
|
feeEst += feeIncrement
|
||||||
|
for totalAdded < minAmount+feeEst {
|
||||||
|
if len(eligible) == 0 {
|
||||||
|
return nil, InsufficientFundsError{totalAdded, minAmount, feeEst}
|
||||||
|
}
|
||||||
|
input, eligible = eligible[0], eligible[1:]
|
||||||
|
inputs = append(inputs, input)
|
||||||
|
msgtx.AddTxIn(btcwire.NewTxIn(input.OutPoint(), nil))
|
||||||
|
szEst += txInEstimate
|
||||||
|
totalAdded += input.Amount()
|
||||||
|
feeEst = minimumFee(feeIncrement, szEst, msgtx.TxOut, inputs, bs.Height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validateMsgTx(msgtx, inputs); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.Buffer{}
|
|
||||||
buf.Grow(msgtx.SerializeSize())
|
|
||||||
if err := msgtx.BtcEncode(&buf, btcwire.ProtocolVersion); err != nil {
|
|
||||||
// Hitting OOM by growing or writing to a bytes.Buffer already
|
|
||||||
// panics, and all returned errors are unexpected.
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
info := &CreatedTx{
|
info := &CreatedTx{
|
||||||
tx: btcutil.NewTx(msgtx),
|
tx: btcutil.NewTx(msgtx),
|
||||||
changeAddr: changeAddr,
|
changeAddr: changeAddr,
|
||||||
|
@ -252,25 +272,66 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addOutputs(msgtx *btcwire.MsgTx, pairs map[string]btcutil.Amount) error {
|
// addChange adds a new output with the given amount and address, and
|
||||||
|
// randomizes the index (and returns it) of the newly added output.
|
||||||
|
func addChange(msgtx *btcwire.MsgTx, change btcutil.Amount, changeAddr btcutil.Address) (int, error) {
|
||||||
|
pkScript, err := btcscript.PayToAddrScript(changeAddr)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot create txout script: %s", err)
|
||||||
|
}
|
||||||
|
msgtx.AddTxOut(btcwire.NewTxOut(int64(change), pkScript))
|
||||||
|
|
||||||
|
// Randomize index of the change output.
|
||||||
|
rng := badrand.New(badrand.NewSource(time.Now().UnixNano()))
|
||||||
|
r := rng.Int31n(int32(len(msgtx.TxOut))) // random index
|
||||||
|
c := len(msgtx.TxOut) - 1 // change index
|
||||||
|
msgtx.TxOut[r], msgtx.TxOut[c] = msgtx.TxOut[c], msgtx.TxOut[r]
|
||||||
|
return int(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// changeAddress obtains a new btcutil.Address to be used as a change
|
||||||
|
// transaction output. It will also mark the KeyStore as dirty and
|
||||||
|
// tells chainSvr to watch that address.
|
||||||
|
func (w *Wallet) changeAddress(bs *keystore.BlockStamp) (btcutil.Address, error) {
|
||||||
|
changeAddr, err := w.KeyStore.ChangeAddress(bs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get change address: %s", err)
|
||||||
|
}
|
||||||
|
w.KeyStore.MarkDirty()
|
||||||
|
err = w.chainSvr.NotifyReceived([]btcutil.Address{changeAddr})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot request updates for "+
|
||||||
|
"change address: %v", err)
|
||||||
|
}
|
||||||
|
return changeAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addOutputs adds the given address/amount pairs as outputs to msgtx,
|
||||||
|
// returning their total amount.
|
||||||
|
func addOutputs(msgtx *btcwire.MsgTx, pairs map[string]btcutil.Amount) (btcutil.Amount, error) {
|
||||||
|
var minAmount btcutil.Amount
|
||||||
for addrStr, amt := range pairs {
|
for addrStr, amt := range pairs {
|
||||||
|
if amt <= 0 {
|
||||||
|
return minAmount, ErrNonPositiveAmount
|
||||||
|
}
|
||||||
|
minAmount += amt
|
||||||
addr, err := btcutil.DecodeAddress(addrStr, activeNet.Params)
|
addr, err := btcutil.DecodeAddress(addrStr, activeNet.Params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot decode address: %s", err)
|
return minAmount, fmt.Errorf("cannot decode address: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add output to spend amt to addr.
|
// Add output to spend amt to addr.
|
||||||
pkScript, err := btcscript.PayToAddrScript(addr)
|
pkScript, err := btcscript.PayToAddrScript(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create txout script: %s", err)
|
return minAmount, fmt.Errorf("cannot create txout script: %s", err)
|
||||||
}
|
}
|
||||||
txout := btcwire.NewTxOut(int64(amt), pkScript)
|
txout := btcwire.NewTxOut(int64(amt), pkScript)
|
||||||
msgtx.AddTxOut(txout)
|
msgtx.AddTxOut(txout)
|
||||||
}
|
}
|
||||||
return nil
|
return minAmount, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (w *Wallet) findEligibleOuptuts(minconf int, bs *keystore.BlockStamp) ([]txstore.Credit, error) {
|
func (w *Wallet) findEligibleOutputs(minconf int, bs *keystore.BlockStamp) ([]txstore.Credit, error) {
|
||||||
unspent, err := w.TxStore.UnspentOutputs()
|
unspent, err := w.TxStore.UnspentOutputs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -306,13 +367,16 @@ func (w *Wallet) findEligibleOuptuts(minconf int, bs *keystore.BlockStamp) ([]tx
|
||||||
return eligible, nil
|
return eligible, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// For every unspent output given, add a new input to the given MsgTx. Only P2PKH outputs are
|
// signMsgTx sets the SignatureScript for every item in msgtx.TxIn.
|
||||||
// supported at this point.
|
// It must be called every time a msgtx is changed.
|
||||||
func (w *Wallet) addInputsToTx(msgtx *btcwire.MsgTx, outputs []txstore.Credit) error {
|
// Only P2PKH outputs are supported at this point.
|
||||||
for _, ip := range outputs {
|
func signMsgTx(msgtx *btcwire.MsgTx, prevOutputs []txstore.Credit, store *keystore.Store) error {
|
||||||
msgtx.AddTxIn(btcwire.NewTxIn(ip.OutPoint(), nil))
|
if len(prevOutputs) != len(msgtx.TxIn) {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"Number of prevOutputs (%d) does not match number of tx inputs (%d)",
|
||||||
|
len(prevOutputs), len(msgtx.TxIn))
|
||||||
}
|
}
|
||||||
for i, output := range outputs {
|
for i, output := range prevOutputs {
|
||||||
// Errors don't matter here, as we only consider the
|
// Errors don't matter here, as we only consider the
|
||||||
// case where len(addrs) == 1.
|
// case where len(addrs) == 1.
|
||||||
_, addrs, _, _ := output.Addresses(activeNet.Params)
|
_, addrs, _, _ := output.Addresses(activeNet.Params)
|
||||||
|
@ -324,13 +388,12 @@ func (w *Wallet) addInputsToTx(msgtx *btcwire.MsgTx, outputs []txstore.Credit) e
|
||||||
return UnsupportedTransactionType
|
return UnsupportedTransactionType
|
||||||
}
|
}
|
||||||
|
|
||||||
ai, err := w.KeyStore.Address(apkh)
|
ai, err := store.Address(apkh)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot get address info: %v", err)
|
return fmt.Errorf("cannot get address info: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pka := ai.(keystore.PubKeyAddress)
|
pka := ai.(keystore.PubKeyAddress)
|
||||||
|
|
||||||
privkey, err := pka.PrivKey()
|
privkey, err := pka.PrivKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot get private key: %v", err)
|
return fmt.Errorf("cannot get private key: %v", err)
|
||||||
|
@ -343,10 +406,11 @@ func (w *Wallet) addInputsToTx(msgtx *btcwire.MsgTx, outputs []txstore.Credit) e
|
||||||
}
|
}
|
||||||
msgtx.TxIn[i].SignatureScript = sigscript
|
msgtx.TxIn[i].SignatureScript = sigscript
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateMsgTx(msgtx *btcwire.MsgTx, inputs []txstore.Credit) error {
|
func validateMsgTx(msgtx *btcwire.MsgTx, prevOutputs []txstore.Credit) error {
|
||||||
flags := btcscript.ScriptCanonicalSignatures | btcscript.ScriptStrictMultiSig
|
flags := btcscript.ScriptCanonicalSignatures | btcscript.ScriptStrictMultiSig
|
||||||
bip16 := time.Now().After(btcscript.Bip16Activation)
|
bip16 := time.Now().After(btcscript.Bip16Activation)
|
||||||
if bip16 {
|
if bip16 {
|
||||||
|
@ -354,7 +418,7 @@ func validateMsgTx(msgtx *btcwire.MsgTx, inputs []txstore.Credit) error {
|
||||||
}
|
}
|
||||||
for i, txin := range msgtx.TxIn {
|
for i, txin := range msgtx.TxIn {
|
||||||
engine, err := btcscript.NewScript(
|
engine, err := btcscript.NewScript(
|
||||||
txin.SignatureScript, inputs[i].TxOut().PkScript, i, msgtx, flags)
|
txin.SignatureScript, prevOutputs[i].TxOut().PkScript, i, msgtx, flags)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot create script engine: %s", err)
|
return fmt.Errorf("cannot create script engine: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -365,28 +429,31 @@ func validateMsgTx(msgtx *btcwire.MsgTx, inputs []txstore.Credit) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// minimumFee calculates the minimum fee required for a transaction.
|
// minimumFee estimates the minimum fee required for a transaction.
|
||||||
// If allowFree is true, a fee may be zero so long as the entire
|
// If cfg.DisallowFree is false, a fee may be zero so long as txLen
|
||||||
// transaction has a serialized length less than 1 kilobyte
|
// s less than 1 kilobyte and none of the outputs contain a value
|
||||||
// and none of the outputs contain a value less than 1 bitcent.
|
// less than 1 bitcent. Otherwise, the fee will be calculated using
|
||||||
// Otherwise, the fee will be calculated using TxFeeIncrement,
|
// incr, incrementing the fee for each kilobyte of transaction.
|
||||||
// incrementing the fee for each kilobyte of transaction.
|
func minimumFee(incr btcutil.Amount, txLen int, outputs []*btcwire.TxOut, prevOutputs []txstore.Credit, height int32) btcutil.Amount {
|
||||||
func minimumFee(incr btcutil.Amount, tx *btcwire.MsgTx, allowFree bool) btcutil.Amount {
|
allowFree := false
|
||||||
txLen := tx.SerializeSize()
|
if !cfg.DisallowFree {
|
||||||
fee := btcutil.Amount(int64(1+txLen/1000) * int64(incr))
|
allowFree = allowNoFeeTx(height, prevOutputs, txLen)
|
||||||
|
}
|
||||||
|
fee := feeForSize(incr, txLen)
|
||||||
|
|
||||||
if allowFree && txLen < 1000 {
|
if allowFree && txLen < 1000 {
|
||||||
fee = 0
|
fee = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if fee < incr {
|
if fee < incr {
|
||||||
for _, txOut := range tx.TxOut {
|
for _, txOut := range outputs {
|
||||||
if txOut.Value < btcutil.SatoshiPerBitcent {
|
if txOut.Value < btcutil.SatoshiPerBitcent {
|
||||||
return incr
|
return incr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// How can fee be smaller than 0 here?
|
||||||
if fee < 0 || fee > btcutil.MaxSatoshi {
|
if fee < 0 || fee > btcutil.MaxSatoshi {
|
||||||
fee = btcutil.MaxSatoshi
|
fee = btcutil.MaxSatoshi
|
||||||
}
|
}
|
||||||
|
@ -394,10 +461,10 @@ func minimumFee(incr btcutil.Amount, tx *btcwire.MsgTx, allowFree bool) btcutil.
|
||||||
return fee
|
return fee
|
||||||
}
|
}
|
||||||
|
|
||||||
// allowFree calculates the transaction priority and checks that the
|
// allowNoFeeTx calculates the transaction priority and checks that the
|
||||||
// priority reaches a certain threshold. If the threshhold is
|
// priority reaches a certain threshold. If the threshhold is
|
||||||
// reached, a free transaction fee is allowed.
|
// reached, a free transaction fee is allowed.
|
||||||
func allowFree(curHeight int32, txouts []txstore.Credit, txSize int) bool {
|
func allowNoFeeTx(curHeight int32, txouts []txstore.Credit, txSize int) bool {
|
||||||
const blocksPerDayEstimate = 144.0
|
const blocksPerDayEstimate = 144.0
|
||||||
const txSizeEstimate = 250.0
|
const txSizeEstimate = 250.0
|
||||||
const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate
|
const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate
|
||||||
|
|
183
createtx_test.go
183
createtx_test.go
|
@ -1,21 +1,49 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/conformal/btcscript"
|
||||||
"github.com/conformal/btcutil"
|
"github.com/conformal/btcutil"
|
||||||
|
"github.com/conformal/btcwallet/keystore"
|
||||||
|
"github.com/conformal/btcwallet/txstore"
|
||||||
"github.com/conformal/btcwire"
|
"github.com/conformal/btcwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// This is a tx that transfers funds (0.371 BTC) to addresses of known privKeys.
|
||||||
|
// It contains 6 outputs, in this order, with the following values/addresses:
|
||||||
|
// {0: 0.2283 (addr: myVT6o4GfR57Cfw7pP3vayfHZzMHh2BxXJ - change),
|
||||||
|
// 1: 0.03 (addr: mjqnv9JoxdYyQK7NMZGCKLxNWHfA6XFVC7),
|
||||||
|
// 2: 0.09 (addr: mqi4izJxVr9wRJmoHe3CUjdb7YDzpJmTwr),
|
||||||
|
// 3: 0.1 (addr: mu7q5vxiGCXYKXEtvspP77bYxjnsEobJGv),
|
||||||
|
// 4: 0.15 (addr: mw66YGmegSNv3yfS4brrtj6ZfAZ4DMmhQN),
|
||||||
|
// 5: 0.001 (addr: mgLBkENLdGXXMfu5RZYPuhJdC88UgvsAxY)}
|
||||||
|
var txInfo = struct {
|
||||||
|
hex string
|
||||||
|
amount btcutil.Amount
|
||||||
|
privKeys []string
|
||||||
|
}{
|
||||||
|
hex: "010000000113918955c6ba3c7a2e8ec02ca3e91a2571cb11ade7d5c3e9c1a73b3ac8309d74000000006b483045022100a6f33d4ad476d126ee45e19e43190971e148a1e940abe4165bc686d22ac847e502200936efa4da4225787d4b7e11e8f3389dba626817d7ece0cab38b4f456b0880d6012103ccb8b1038ad6af10a15f68e8d5e347c08befa6cc2ab1718a37e3ea0e38102b92ffffffff06b05b5c01000000001976a914c5297a660cef8088b8472755f4827df7577c612988acc0c62d00000000001976a9142f7094083d750bdfc1f2fad814779e2dde35ce2088ac40548900000000001976a9146fcb336a187619ca20b84af9eac9fbff68d1061d88ac80969800000000001976a91495322d12e18345f4855cbe863d4a8ebcc0e95e0188acc0e1e400000000001976a914aace7f06f94fa298685f6e58769543993fa5fae888aca0860100000000001976a91408eec7602655fdb2531f71070cca4c363c3a15ab88ac00000000",
|
||||||
|
amount: btcutil.Amount(3e6 + 9e6 + 1e7 + 1.5e7 + 1e5),
|
||||||
|
privKeys: []string{
|
||||||
|
"cSYUVdPL6pkabu7Fxp4PaKqYjJFz2Aopw5ygunFbek9HAimLYxp4",
|
||||||
|
"cVnNzZm3DiwkN1Ghs4W8cwcJC9f6TynCCcqzYt8n1c4hwjN2PfTw",
|
||||||
|
"cUgo8PrKj7NzttKRMKwgF3ahXNrLA253pqjWkPGS7Z9iZcKT8EKG",
|
||||||
|
"cSosEHx1freK7B1B6QicPcrH1h5VqReSHew6ZYhv6ntiUJRhowRc",
|
||||||
|
"cR9ApAZ3FLtRMfqRBEr3niD9Mmmvfh3V8Uh56qfJ5b4bFH8ibDkA"}}
|
||||||
|
|
||||||
|
var (
|
||||||
|
outAddr1 = "1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX"
|
||||||
|
outAddr2 = "12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG"
|
||||||
|
)
|
||||||
|
|
||||||
func Test_addOutputs(t *testing.T) {
|
func Test_addOutputs(t *testing.T) {
|
||||||
msgtx := btcwire.NewMsgTx()
|
msgtx := btcwire.NewMsgTx()
|
||||||
pairs := map[string]btcutil.Amount{
|
pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1}
|
||||||
"1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX": 10,
|
if _, err := addOutputs(msgtx, pairs); err != nil {
|
||||||
"12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG": 1,
|
|
||||||
}
|
|
||||||
if err := addOutputs(msgtx, pairs); err != nil {
|
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(msgtx.TxOut) != 2 {
|
if len(msgtx.TxOut) != 2 {
|
||||||
|
@ -27,3 +55,148 @@ func Test_addOutputs(t *testing.T) {
|
||||||
t.Fatalf("Expected values to be [1, 10], got: %v", values)
|
t.Fatalf("Expected values to be [1, 10], got: %v", values)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateTx(t *testing.T) {
|
||||||
|
cfg = &config{DisallowFree: false}
|
||||||
|
bs := &keystore.BlockStamp{Height: 11111}
|
||||||
|
keys := newKeyStore(t, txInfo.privKeys, bs)
|
||||||
|
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
|
||||||
|
var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) {
|
||||||
|
return changeAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick all utxos from txInfo as eligible input.
|
||||||
|
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1, 2, 3, 4, 5})
|
||||||
|
// Now create a new TX sending 25e6 satoshis to the following addresses:
|
||||||
|
outputs := map[string]btcutil.Amount{outAddr1: 15e6, outAddr2: 10e6}
|
||||||
|
tx, err := createTx(eligible, outputs, bs, defaultFeeIncrement, keys, tstChangeAddress)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tx.changeAddr.String() != changeAddr.String() {
|
||||||
|
t.Fatalf("Unexpected change address; got %v, want %v",
|
||||||
|
tx.changeAddr.String(), changeAddr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
msgTx := tx.tx.MsgTx()
|
||||||
|
if len(msgTx.TxOut) != 3 {
|
||||||
|
t.Fatalf("Unexpected number of outputs; got %d, want 3", len(msgTx.TxOut))
|
||||||
|
}
|
||||||
|
|
||||||
|
// The outputs in our new TX amount to 25e6 satoshis, so to fulfil that
|
||||||
|
// createTx should have picked the utxos with indices 4, 3 and 5, which
|
||||||
|
// total 25.1e6.
|
||||||
|
if len(msgTx.TxIn) != 3 {
|
||||||
|
t.Fatalf("Unexpected number of inputs; got %d, want 3", len(msgTx.TxIn))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given the input (15e6 + 10e6 + 1e7) and requested output (15e6 + 10e6)
|
||||||
|
// amounts in the new TX, we should have a change output with 8.99e6, which
|
||||||
|
// implies a fee of 1e4 satoshis.
|
||||||
|
expectedChange := btcutil.Amount(8.99e6)
|
||||||
|
|
||||||
|
outputs[changeAddr.String()] = expectedChange
|
||||||
|
checkOutputsMatch(t, msgTx, outputs)
|
||||||
|
|
||||||
|
minFee := feeForSize(defaultFeeIncrement, msgTx.SerializeSize())
|
||||||
|
actualFee := btcutil.Amount(1e4)
|
||||||
|
if minFee > actualFee {
|
||||||
|
t.Fatalf("Requested fee (%v) for tx size higher than actual fee (%v)", minFee, actualFee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateTxInsufficientFundsError(t *testing.T) {
|
||||||
|
cfg = &config{DisallowFree: false}
|
||||||
|
outputs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1e9}
|
||||||
|
eligible := eligibleInputsFromTx(t, txInfo.hex, []uint32{1})
|
||||||
|
bs := &keystore.BlockStamp{Height: 11111}
|
||||||
|
changeAddr, _ := btcutil.DecodeAddress("muqW4gcixv58tVbSKRC5q6CRKy8RmyLgZ5", activeNet.Params)
|
||||||
|
var tstChangeAddress = func(bs *keystore.BlockStamp) (btcutil.Address, error) {
|
||||||
|
return changeAddr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := createTx(eligible, outputs, bs, defaultFeeIncrement, nil, tstChangeAddress)
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected InsufficientFundsError, got no error")
|
||||||
|
} else if _, ok := err.(InsufficientFundsError); !ok {
|
||||||
|
t.Errorf("Unexpected error, got %v, want InsufficientFundsError", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkOutputsMatch checks that the outputs in the tx match the expected ones.
|
||||||
|
func checkOutputsMatch(t *testing.T, msgtx *btcwire.MsgTx, expected map[string]btcutil.Amount) {
|
||||||
|
// This is a bit convoluted because the index of the change output is randomized.
|
||||||
|
for addrStr, v := range expected {
|
||||||
|
addr, err := btcutil.DecodeAddress(addrStr, activeNet.Params)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Cannot decode address: %v", err)
|
||||||
|
}
|
||||||
|
pkScript, err := btcscript.PayToAddrScript(addr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Cannot create pkScript: %v", err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
for _, txout := range msgtx.TxOut {
|
||||||
|
if reflect.DeepEqual(txout.PkScript, pkScript) && txout.Value == int64(v) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("PkScript %v not found in msgtx.TxOut: %v", pkScript, msgtx.TxOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newKeyStore creates a new keystore and imports the given privKey into it.
|
||||||
|
func newKeyStore(t *testing.T, privKeys []string, bs *keystore.BlockStamp) *keystore.Store {
|
||||||
|
passphrase := []byte{0, 1}
|
||||||
|
keys, err := keystore.New("/tmp/keys.bin", "Default acccount", passphrase,
|
||||||
|
activeNet.Params, bs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, key := range privKeys {
|
||||||
|
wif, err := btcutil.DecodeWIF(key)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err = keys.Unlock(passphrase); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = keys.ImportPrivateKey(wif, bs)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
|
// eligibleInputsFromTx decodes the given txHex and returns the outputs with
|
||||||
|
// the given indices as eligible inputs.
|
||||||
|
func eligibleInputsFromTx(t *testing.T, txHex string, indices []uint32) []txstore.Credit {
|
||||||
|
serialized, err := hex.DecodeString(txHex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tx, err := btcutil.NewTxFromBytes(serialized)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s := txstore.New("/tmp/tx.bin")
|
||||||
|
r, err := s.InsertTx(tx, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
eligible := make([]txstore.Credit, len(indices))
|
||||||
|
for i, idx := range indices {
|
||||||
|
credit, err := r.AddCredit(idx, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
eligible[i] = credit
|
||||||
|
}
|
||||||
|
return eligible
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue