Refactor txToPairs into smaller functions

Also adds tests for those functions, and improve fee estimation.
This commit is contained in:
Guilherme Salgado 2014-09-25 14:27:18 -03:00
parent ccb2b1e16d
commit ec8a5bc10c
2 changed files with 409 additions and 169 deletions

View file

@ -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.
sort.Sort(sort.Reverse(ByAmount(eligible)))
var selectedInputs []txstore.Credit return createTx(eligible, pairs, bs, w.FeeIncrement, w.KeyStore, w.changeAddress)
// changeAddr is nil/zeroed until a change address is needed, and reused }
// again in case a change utxo has already been chosen.
var changeAddr btcutil.Address
var changeIdx int
// Make a copy of msgtx before any inputs are added. This will be // createTx selects inputs (from the given slice of eligible utxos)
// used as a starting point when trying a fee and starting over with // whose amount are sufficient to fulfil all the desired outputs plus
// a higher fee if not enough was originally chosen. // the mining fee. It then creates and returns a CreatedTx containing
txNoInputs := msgtx.Copy() // 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) {
// Get the number of satoshis to increment fee by when searching for msgtx := btcwire.NewMsgTx()
// the minimum tx fee needed. minAmount, err := addOutputs(msgtx, outputs)
fee := btcutil.Amount(0) if err != nil {
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
if change > 0 {
// Get a new change address if one has not already been found.
if changeAddr == nil {
changeAddr, err = w.KeyStore.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)
}
}
// Spend change.
pkScript, err := btcscript.PayToAddrScript(changeAddr)
if err != nil {
return nil, 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]
changeIdx = int(r)
}
if err = w.addInputsToTx(msgtx, inputs); 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
break
}
}
if err = validateMsgTx(msgtx, selectedInputs); err != nil {
return nil, err return nil, err
} }
buf := bytes.Buffer{} // Sort eligible inputs so that we first pick the ones with highest
buf.Grow(msgtx.SerializeSize()) // amount, thus reducing number of inputs.
if err := msgtx.BtcEncode(&buf, btcwire.ProtocolVersion); err != nil { sort.Sort(sort.Reverse(ByAmount(eligible)))
// Hitting OOM by growing or writing to a bytes.Buffer already
// panics, and all returned errors are unexpected. // Start by adding enough inputs to cover for the total amount of all
panic(err) // 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
// changeIdx is -1 unless there's a change output.
changeIdx := -1
for {
change := totalAdded - minAmount - feeEst
if change > 0 {
if changeAddr == nil {
changeAddr, err = changeAddress(bs)
if err != nil {
return nil, err
}
}
changeIdx, err = addChange(msgtx, change, changeAddr)
if err != nil {
return nil, err
}
}
if err = signMsgTx(msgtx, inputs, keys); err != nil {
return nil, err
}
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
}
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
}
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

View file

@ -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
}