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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
badrand "math/rand"
|
||||
|
@ -32,16 +31,54 @@ import (
|
|||
"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.
|
||||
// This may be caused by not enough inputs for all of the desired total
|
||||
// transaction output amount, or due to
|
||||
type InsufficientFunds struct {
|
||||
type InsufficientFundsError struct {
|
||||
in, out, fee btcutil.Amount
|
||||
}
|
||||
|
||||
// Error satisifies the builtin error interface.
|
||||
func (e InsufficientFunds) Error() string {
|
||||
func (e InsufficientFundsError) Error() string {
|
||||
total := e.out + e.fee
|
||||
if e.fee == 0 {
|
||||
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) 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
|
||||
// address/amount pair and fee to each address and the miner. minconf
|
||||
// specifies the minimum number of confirmations required before an
|
||||
// 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
|
||||
// address. If change is needed to return funds back to an owned
|
||||
// address, changeUtxo will point to a unconfirmed (height = -1, zeroed
|
||||
// block hash) Utxo. ErrInsufficientFunds is returned if there are not
|
||||
// enough eligible unspent outputs to create the transaction.
|
||||
func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
|
||||
minconf int) (*CreatedTx, error) {
|
||||
// address. InsufficientFundsError is returned if there are not 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
|
||||
// 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()
|
||||
|
||||
// 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.
|
||||
bs, err := w.chainSvr.BlockStamp()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eligible, err := w.findEligibleOuptuts(minconf, bs)
|
||||
eligible, err := w.findEligibleOutputs(minconf, bs)
|
||||
if err != nil {
|
||||
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)))
|
||||
|
||||
var selectedInputs []txstore.Credit
|
||||
// changeAddr is nil/zeroed until a change address is needed, and reused
|
||||
// again in case a change utxo has already been chosen.
|
||||
// Start by adding enough inputs to cover for the total amount of all
|
||||
// desired outputs.
|
||||
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 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 {
|
||||
msgtx = txNoInputs.Copy()
|
||||
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
|
||||
change := totalAdded - minAmount - feeEst
|
||||
if change > 0 {
|
||||
// Get a new change address if one has not already been found.
|
||||
if changeAddr == nil {
|
||||
changeAddr, err = w.KeyStore.ChangeAddress(bs)
|
||||
changeAddr, err = changeAddress(bs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next 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 nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Spend change.
|
||||
pkScript, err := btcscript.PayToAddrScript(changeAddr)
|
||||
changeIdx, err = addChange(msgtx, change, changeAddr)
|
||||
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
|
||||
}
|
||||
|
||||
noFeeAllowed := false
|
||||
if !cfg.DisallowFree {
|
||||
noFeeAllowed = allowFree(bs.Height, inputs, msgtx.SerializeSize())
|
||||
}
|
||||
if minFee := minimumFee(w.FeeIncrement, msgtx, noFeeAllowed); fee < minFee {
|
||||
fee = minFee
|
||||
} else {
|
||||
selectedInputs = inputs
|
||||
if feeForSize(feeIncrement, msgtx.SerializeSize()) <= feeEst {
|
||||
// The required fee for this size is less than or equal to what
|
||||
// we guessed, so we're done.
|
||||
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
|
||||
}
|
||||
|
||||
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{
|
||||
tx: btcutil.NewTx(msgtx),
|
||||
changeAddr: changeAddr,
|
||||
|
@ -252,25 +272,66 @@ func (w *Wallet) txToPairs(pairs map[string]btcutil.Amount,
|
|||
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 {
|
||||
if amt <= 0 {
|
||||
return minAmount, ErrNonPositiveAmount
|
||||
}
|
||||
minAmount += amt
|
||||
addr, err := btcutil.DecodeAddress(addrStr, activeNet.Params)
|
||||
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.
|
||||
pkScript, err := btcscript.PayToAddrScript(addr)
|
||||
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)
|
||||
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()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -306,13 +367,16 @@ func (w *Wallet) findEligibleOuptuts(minconf int, bs *keystore.BlockStamp) ([]tx
|
|||
return eligible, nil
|
||||
}
|
||||
|
||||
// For every unspent output given, add a new input to the given MsgTx. Only P2PKH outputs are
|
||||
// supported at this point.
|
||||
func (w *Wallet) addInputsToTx(msgtx *btcwire.MsgTx, outputs []txstore.Credit) error {
|
||||
for _, ip := range outputs {
|
||||
msgtx.AddTxIn(btcwire.NewTxIn(ip.OutPoint(), nil))
|
||||
// signMsgTx sets the SignatureScript for every item in msgtx.TxIn.
|
||||
// It must be called every time a msgtx is changed.
|
||||
// Only P2PKH outputs are supported at this point.
|
||||
func signMsgTx(msgtx *btcwire.MsgTx, prevOutputs []txstore.Credit, store *keystore.Store) error {
|
||||
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
|
||||
// case where len(addrs) == 1.
|
||||
_, addrs, _, _ := output.Addresses(activeNet.Params)
|
||||
|
@ -324,13 +388,12 @@ func (w *Wallet) addInputsToTx(msgtx *btcwire.MsgTx, outputs []txstore.Credit) e
|
|||
return UnsupportedTransactionType
|
||||
}
|
||||
|
||||
ai, err := w.KeyStore.Address(apkh)
|
||||
ai, err := store.Address(apkh)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get address info: %v", err)
|
||||
}
|
||||
|
||||
pka := ai.(keystore.PubKeyAddress)
|
||||
|
||||
privkey, err := pka.PrivKey()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
bip16 := time.Now().After(btcscript.Bip16Activation)
|
||||
if bip16 {
|
||||
|
@ -354,7 +418,7 @@ func validateMsgTx(msgtx *btcwire.MsgTx, inputs []txstore.Credit) error {
|
|||
}
|
||||
for i, txin := range msgtx.TxIn {
|
||||
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 {
|
||||
return fmt.Errorf("cannot create script engine: %s", err)
|
||||
}
|
||||
|
@ -365,28 +429,31 @@ func validateMsgTx(msgtx *btcwire.MsgTx, inputs []txstore.Credit) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// minimumFee calculates the minimum fee required for a transaction.
|
||||
// If allowFree is true, a fee may be zero so long as the entire
|
||||
// transaction has a serialized length less than 1 kilobyte
|
||||
// and none of the outputs contain a value less than 1 bitcent.
|
||||
// Otherwise, the fee will be calculated using TxFeeIncrement,
|
||||
// incrementing the fee for each kilobyte of transaction.
|
||||
func minimumFee(incr btcutil.Amount, tx *btcwire.MsgTx, allowFree bool) btcutil.Amount {
|
||||
txLen := tx.SerializeSize()
|
||||
fee := btcutil.Amount(int64(1+txLen/1000) * int64(incr))
|
||||
// minimumFee estimates the minimum fee required for a transaction.
|
||||
// If cfg.DisallowFree is false, a fee may be zero so long as txLen
|
||||
// s less than 1 kilobyte and none of the outputs contain a value
|
||||
// less than 1 bitcent. Otherwise, the fee will be calculated using
|
||||
// incr, 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 {
|
||||
allowFree := false
|
||||
if !cfg.DisallowFree {
|
||||
allowFree = allowNoFeeTx(height, prevOutputs, txLen)
|
||||
}
|
||||
fee := feeForSize(incr, txLen)
|
||||
|
||||
if allowFree && txLen < 1000 {
|
||||
fee = 0
|
||||
}
|
||||
|
||||
if fee < incr {
|
||||
for _, txOut := range tx.TxOut {
|
||||
for _, txOut := range outputs {
|
||||
if txOut.Value < btcutil.SatoshiPerBitcent {
|
||||
return incr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// How can fee be smaller than 0 here?
|
||||
if fee < 0 || fee > btcutil.MaxSatoshi {
|
||||
fee = btcutil.MaxSatoshi
|
||||
}
|
||||
|
@ -394,10 +461,10 @@ func minimumFee(incr btcutil.Amount, tx *btcwire.MsgTx, allowFree bool) btcutil.
|
|||
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
|
||||
// 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 txSizeEstimate = 250.0
|
||||
const threshold = btcutil.SatoshiPerBitcoin * blocksPerDayEstimate / txSizeEstimate
|
||||
|
|
183
createtx_test.go
183
createtx_test.go
|
@ -1,21 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/conformal/btcscript"
|
||||
"github.com/conformal/btcutil"
|
||||
"github.com/conformal/btcwallet/keystore"
|
||||
"github.com/conformal/btcwallet/txstore"
|
||||
"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) {
|
||||
msgtx := btcwire.NewMsgTx()
|
||||
pairs := map[string]btcutil.Amount{
|
||||
"1MirQ9bwyQcGVJPwKUgapu5ouK2E2Ey4gX": 10,
|
||||
"12MzCDwodF9G1e7jfwLXfR164RNtx4BRVG": 1,
|
||||
}
|
||||
if err := addOutputs(msgtx, pairs); err != nil {
|
||||
pairs := map[string]btcutil.Amount{outAddr1: 10, outAddr2: 1}
|
||||
if _, err := addOutputs(msgtx, pairs); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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