Added CoinSelector interface and a few simple algos as a sub-package

This commit contains a basic definition for CoinSelector along with some
utility classes and some basic algos to make creating transactions from
a set of available unspent outpoints easier.

Thanks to @dajohi, @davec, @jrick for all the feedback and suggestions
regarding interfaces, organization, optimization, comments and
documentation.
This commit is contained in:
Francis Lam 2014-03-16 17:02:46 -04:00
parent 60d4bed78f
commit 02a1584784
6 changed files with 812 additions and 22 deletions

96
coinset/README.md Normal file
View file

@ -0,0 +1,96 @@
coinset
=======
Package coinset provides bitcoin-specific convenience functions for selecting
from and managing sets of unspent transaction outpoints (UTXOs).
A comprehensive suite of tests is provided to ensure proper functionality. See
`test_coverage.txt` for the gocov coverage report. Alternatively, if you are
running a POSIX OS, you can run the `cov_report.sh` script for a real-time
report. Package coinset is licensed under the liberal ISC license.
## Documentation
Full `go doc` style documentation for the project can be viewed online without
installing this package by using the GoDoc site here:
http://godoc.org/github.com/conformal/btcutil/coinset
You can also view the documentation locally once the package is installed with
the `godoc` tool by running `godoc -http=":6060"` and pointing your browser to
http://localhost:6060/pkg/github.com/conformal/btcutil/coinset
## Installation
```bash
$ go get github.com/conformal/btcutil/coinset
```
## Usage
Each unspent transaction outpoint is represented by the Coin interface. An
example of a concrete type that implements Coin is coinset.SimpleCoin.
The typical use case for this library is for creating raw bitcoin transactions
given a set of Coins that may be spent by the user, for example as below:
```Go
var unspentCoins = []coinset.Coin{ ... }
```
When the user needs to spend a certain amount, they will need to select a
subset of these coins which contain at least that value. CoinSelector is
an interface that represents types that implement coin selection algos,
subject to various criteria. There are a few examples of CoinSelector's:
- MinIndexCoinSelector
- MinNumberCoinSelector
- MaxValueAgeCoinSelector
- MinPriorityCoinSelector
For example, if the user wishes to maximize the probability that their
transaction is mined quickly, they could use the MaxValueAgeCoinSelector to
select high priority coins, then also attach a relatively high fee.
```Go
selector := &coinset.MaxValueAgeCoinSelector{
MaxInputs: 10,
MinAmountChange: 10000,
}
selectedCoins, err := selector.CoinSelect(targetAmount + bigFee, unspentCoins)
if err != nil {
return err
}
msgTx := coinset.NewMsgTxWithInputCoins(selectedCoins)
...
```
The user can then create the msgTx.TxOut's as required, then sign the
transaction and transmit it to the network.
## GPG Verification Key
All official release tags are signed by Conformal so users can ensure the code
has not been tampered with and is coming from Conformal. To verify the
signature perform the following:
- Download the public key from the Conformal website at
https://opensource.conformal.com/GIT-GPG-KEY-conformal.txt
- Import the public key into your GPG keyring:
```bash
gpg --import GIT-GPG-KEY-conformal.txt
```
- Verify the release tag with the following command where `TAG_NAME` is a
placeholder for the specific tag:
```bash
git tag -v TAG_NAME
```
## License
Package coinset is licensed under the liberal ISC License.

394
coinset/coins.go Normal file
View file

@ -0,0 +1,394 @@
package coinset
import (
"container/list"
"errors"
"github.com/conformal/btcutil"
"github.com/conformal/btcwire"
"sort"
)
// Coin represents a spendable transaction outpoint
type Coin interface {
Hash() *btcwire.ShaHash
Index() uint32
Value() int64
PkScript() []byte
NumConfs() int64
ValueAge() int64
}
// Coins represents a set of Coins
type Coins interface {
Coins() []Coin
}
// CoinSet is a utility struct for the modifications of a set of
// Coins that implements the Coins interface. To create a CoinSet,
// you must call NewCoinSet with nil for an empty set or a slice of
// coins as the initial contents.
//
// It is important to note that the all the Coins being added or removed
// from a CoinSet must have a constant ValueAge() during the use of
// the CoinSet, otherwise the cached values will be incorrect.
type CoinSet struct {
coinList *list.List
totalValue int64
totalValueAge int64
}
// Ensure that CoinSet is a Coins
var _ Coins = NewCoinSet(nil)
// NewCoinSet creates a CoinSet containing the coins provided.
// To create an empty CoinSet, you may pass null as the coins input parameter.
func NewCoinSet(coins []Coin) *CoinSet {
newCoinSet := &CoinSet{
coinList: list.New(),
totalValue: 0,
totalValueAge: 0,
}
for _, coin := range coins {
newCoinSet.PushCoin(coin)
}
return newCoinSet
}
// Coins returns a new slice of the coins contained in the set.
func (cs *CoinSet) Coins() []Coin {
coins := make([]Coin, cs.coinList.Len())
for i, e := 0, cs.coinList.Front(); e != nil; i, e = i+1, e.Next() {
coins[i] = e.Value.(Coin)
}
return coins
}
// TotalValue returns the total value of the coins in the set.
func (cs *CoinSet) TotalValue() (value int64) {
return cs.totalValue
}
// TotalValueAge returns the total value * number of confirmations
// of the coins in the set.
func (cs *CoinSet) TotalValueAge() (valueAge int64) {
return cs.totalValueAge
}
// Num returns the number of coins in the set
func (cs *CoinSet) Num() int {
return cs.coinList.Len()
}
// PushCoin adds a coin to the end of the list and updates
// the cached value amounts.
func (cs *CoinSet) PushCoin(c Coin) {
cs.coinList.PushBack(c)
cs.totalValue += c.Value()
cs.totalValueAge += c.ValueAge()
}
// PopCoin removes the last coin on the list and returns it.
func (cs *CoinSet) PopCoin() Coin {
back := cs.coinList.Back()
if back == nil {
return nil
}
return cs.removeElement(back)
}
// ShiftCoin removes the first coin on the list and returns it.
func (cs *CoinSet) ShiftCoin() Coin {
front := cs.coinList.Front()
if front == nil {
return nil
}
return cs.removeElement(front)
}
// removeElement updates the cached value amounts in the CoinSet,
// removes the element from the list, then returns the Coin that
// was removed to the caller.
func (cs *CoinSet) removeElement(e *list.Element) Coin {
c := e.Value.(Coin)
cs.coinList.Remove(e)
cs.totalValue -= c.Value()
cs.totalValueAge -= c.ValueAge()
return c
}
// NewMsgTxWithInputCoins takes the coins in the CoinSet and makes them
// the inputs to a new btcwire.MsgTx which is returned.
func NewMsgTxWithInputCoins(inputCoins Coins) *btcwire.MsgTx {
msgTx := btcwire.NewMsgTx()
coins := inputCoins.Coins()
msgTx.TxIn = make([]*btcwire.TxIn, len(coins))
for i, coin := range coins {
msgTx.TxIn[i] = &btcwire.TxIn{
PreviousOutpoint: btcwire.OutPoint{
Hash: *coin.Hash(),
Index: coin.Index(),
},
SignatureScript: nil,
Sequence: btcwire.MaxTxInSequenceNum,
}
}
return msgTx
}
var (
// ErrCoinsNoSelectionAvailable is returned when a CoinSelector believes there is no
// possible combination of coins which can meet the requirements provided to the selector.
ErrCoinsNoSelectionAvailable = errors.New("no coin selection possible")
)
// satisfiesTargetValue checks that the totalValue is either exactly the targetValue
// or is greater than the targetValue by at least the minChange amount.
func satisfiesTargetValue(targetValue, minChange, totalValue int64) bool {
return (totalValue == targetValue || totalValue >= targetValue+minChange)
}
// CoinSelector is an interface that wraps the CoinSelect method.
//
// CoinSelect will attempt to select a subset of the coins which has at
// least the targetValue amount. CoinSelect is not guaranteed to return a
// selection of coins even if the total value of coins given is greater
// than the target value.
//
// The exact choice of coins in the subset will be implementation specific.
//
// It is important to note that the Coins being used as inputs need to have
// a constant ValueAge() during the execution of CoinSelect.
type CoinSelector interface {
CoinSelect(targetValue int64, coins []Coin) (Coins, error)
}
// MinIndexCoinSelector is a CoinSelector that attempts to construct a
// selection of coins whose total value is at least targetValue and prefers
// any number of lower indexes (as in the ordered array) over higher ones.
type MinIndexCoinSelector struct {
MaxInputs int
MinChangeAmount int64
}
// CoinSelect will attempt to select coins using the algorithm described
// in the MinIndexCoinSelector struct.
func (s MinIndexCoinSelector) CoinSelect(targetValue int64, coins []Coin) (Coins, error) {
cs := NewCoinSet(nil)
for n := 0; n < len(coins) && n < s.MaxInputs; n++ {
cs.PushCoin(coins[n])
if satisfiesTargetValue(targetValue, s.MinChangeAmount, cs.TotalValue()) {
return cs, nil
}
}
return nil, ErrCoinsNoSelectionAvailable
}
// MinNumberCoinSelector is a CoinSelector that attempts to construct
// a selection of coins whose total value is at least targetValue
// that uses as few of the inputs as possible.
type MinNumberCoinSelector struct {
MaxInputs int
MinChangeAmount int64
}
// CoinSelect will attempt to select coins using the algorithm described
// in the MinNumberCoinSelector struct.
func (s MinNumberCoinSelector) CoinSelect(targetValue int64, coins []Coin) (Coins, error) {
sortedCoins := make([]Coin, 0, len(coins))
sortedCoins = append(sortedCoins, coins...)
sort.Sort(sort.Reverse(byAmount(sortedCoins)))
return (&MinIndexCoinSelector{
MaxInputs: s.MaxInputs,
MinChangeAmount: s.MinChangeAmount,
}).CoinSelect(targetValue, sortedCoins)
}
// MaxValueAgeCoinSelector is a CoinSelector that attempts to construct
// a selection of coins whose total value is at least targetValue
// that has as much input value-age as possible.
//
// This would be useful in the case where you want to maximize
// likelihood of the inclusion of your transaction in the next mined
// block.
type MaxValueAgeCoinSelector struct {
MaxInputs int
MinChangeAmount int64
}
// CoinSelect will attempt to select coins using the algorithm described
// in the MaxValueAgeSelector struct.
func (s MaxValueAgeCoinSelector) CoinSelect(targetValue int64, coins []Coin) (Coins, error) {
sortedCoins := make([]Coin, 0, len(coins))
sortedCoins = append(sortedCoins, coins...)
sort.Sort(sort.Reverse(byValueAge(sortedCoins)))
return (&MinIndexCoinSelector{
MaxInputs: s.MaxInputs,
MinChangeAmount: s.MinChangeAmount,
}).CoinSelect(targetValue, sortedCoins)
}
// MinPriorityCoinSelector is a CoinSelector that attempts to construct
// a selection of coins whose total value is at least targetValue and
// whose average value-age per input is greater than MinAvgValueAgePerInput.
// If there is change, it must exceed MinChangeAmount to be a valid selection.
//
// When possible, MinPriorityCoinSelector will attempt to reduce the average
// input priority over the threshold, but no guarantees will be made as to
// minimality of the selection. The selection below is almost certainly
// suboptimal.
//
type MinPriorityCoinSelector struct {
MaxInputs int
MinChangeAmount int64
MinAvgValueAgePerInput int64
}
// CoinSelect will attempt to select coins using the algorithm described
// in the MinPriorityCoinSelector struct.
func (s MinPriorityCoinSelector) CoinSelect(targetValue int64, coins []Coin) (Coins, error) {
possibleCoins := make([]Coin, 0, len(coins))
possibleCoins = append(possibleCoins, coins...)
sort.Sort(byValueAge(possibleCoins))
// find the first coin with sufficient valueAge
cutoffIndex := -1
for i := 0; i < len(possibleCoins); i++ {
if possibleCoins[i].ValueAge() >= s.MinAvgValueAgePerInput {
cutoffIndex = i
break
}
}
if cutoffIndex < 0 {
return nil, ErrCoinsNoSelectionAvailable
}
// create sets of input coins that will obey minimum average valueAge
for i := cutoffIndex; i < len(possibleCoins); i++ {
possibleHighCoins := possibleCoins[cutoffIndex : i+1]
// choose a set of high-enough valueAge coins
highSelect, err := (&MinNumberCoinSelector{
MaxInputs: s.MaxInputs,
MinChangeAmount: s.MinChangeAmount,
}).CoinSelect(targetValue, possibleHighCoins)
if err != nil {
// attempt to add available low priority to make a solution
for numLow := 1; numLow <= cutoffIndex && numLow+(i-cutoffIndex) <= s.MaxInputs; numLow++ {
allHigh := NewCoinSet(possibleCoins[cutoffIndex : i+1])
newTargetValue := targetValue - allHigh.TotalValue()
newMaxInputs := allHigh.Num() + numLow
if newMaxInputs > numLow {
newMaxInputs = numLow
}
newMinAvgValueAge := ((s.MinAvgValueAgePerInput * int64(allHigh.Num()+numLow)) - allHigh.TotalValueAge()) / int64(numLow)
// find the minimum priority that can be added to set
lowSelect, err := (&MinPriorityCoinSelector{
MaxInputs: newMaxInputs,
MinChangeAmount: s.MinChangeAmount,
MinAvgValueAgePerInput: newMinAvgValueAge,
}).CoinSelect(newTargetValue, possibleCoins[0:cutoffIndex])
if err != nil {
continue
}
for _, coin := range lowSelect.Coins() {
allHigh.PushCoin(coin)
}
return allHigh, nil
}
// oh well, couldn't fix, try to add more high priority to the set.
} else {
extendedCoins := NewCoinSet(highSelect.Coins())
// attempt to lower priority towards target with lowest ones first
for n := 0; n < cutoffIndex; n++ {
if extendedCoins.Num() >= s.MaxInputs {
break
}
if possibleCoins[n].ValueAge() == 0 {
continue
}
extendedCoins.PushCoin(possibleCoins[n])
if extendedCoins.TotalValueAge()/int64(extendedCoins.Num()) < s.MinAvgValueAgePerInput {
extendedCoins.PopCoin()
continue
}
}
return extendedCoins, nil
}
}
return nil, ErrCoinsNoSelectionAvailable
}
type byValueAge []Coin
func (a byValueAge) Len() int { return len(a) }
func (a byValueAge) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byValueAge) Less(i, j int) bool { return a[i].ValueAge() < a[j].ValueAge() }
type byAmount []Coin
func (a byAmount) Len() int { return len(a) }
func (a byAmount) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a byAmount) Less(i, j int) bool { return a[i].Value() < a[j].Value() }
// SimpleCoin defines a concrete instance of Coin that is backed by a
// btcutil.Tx, a specific outpoint index, and the number of confirmations
// that transaction has had.
type SimpleCoin struct {
Tx *btcutil.Tx
TxIndex uint32
TxNumConfs int64
}
// Ensure that SimpleCoin is a Coin
var _ Coin = &SimpleCoin{}
// Hash returns the hash value of the transaction on which the Coin is an output
func (c *SimpleCoin) Hash() *btcwire.ShaHash {
return c.Tx.Sha()
}
// Index returns the index of the output on the transaction which the Coin represents
func (c *SimpleCoin) Index() uint32 {
return c.TxIndex
}
// txOut returns the TxOut of the transaction the Coin represents
func (c *SimpleCoin) txOut() *btcwire.TxOut {
return c.Tx.MsgTx().TxOut[c.TxIndex]
}
// Value returns the value of the Coin
func (c *SimpleCoin) Value() int64 {
return c.txOut().Value
}
// PkScript returns the outpoint script of the Coin.
//
// This can be used to determine what type of script the Coin uses
// and extract standard addresses if possible using
// btcscript.ExtractPkScriptAddrs for example.
func (c *SimpleCoin) PkScript() []byte {
return c.txOut().PkScript
}
// NumConfs returns the number of confirmations that the transaction the Coin references
// has had.
func (c *SimpleCoin) NumConfs() int64 {
return c.TxNumConfs
}
// ValueAge returns the product of the value and the number of confirmations. This is
// used as an input to calculate the priority of the transaction.
func (c *SimpleCoin) ValueAge() int64 {
return c.TxNumConfs * c.Value()
}

252
coinset/coins_test.go Normal file
View file

@ -0,0 +1,252 @@
package coinset_test
import (
"bytes"
"encoding/hex"
"fmt"
"github.com/conformal/btcutil"
"github.com/conformal/btcutil/coinset"
"github.com/conformal/btcwire"
"github.com/conformal/fastsha256"
"testing"
)
type TestCoin struct {
TxHash *btcwire.ShaHash
TxIndex uint32
TxValue int64
TxNumConfs int64
}
func (c *TestCoin) Hash() *btcwire.ShaHash { return c.TxHash }
func (c *TestCoin) Index() uint32 { return c.TxIndex }
func (c *TestCoin) Value() int64 { return c.TxValue }
func (c *TestCoin) PkScript() []byte { return nil }
func (c *TestCoin) NumConfs() int64 { return c.TxNumConfs }
func (c *TestCoin) ValueAge() int64 { return c.TxValue * c.TxNumConfs }
func NewCoin(index, value, numConfs int64) coinset.Coin {
h := fastsha256.New()
h.Write([]byte(fmt.Sprintf("%d", index)))
hash, _ := btcwire.NewShaHash(h.Sum(nil))
c := &TestCoin{
TxHash: hash,
TxIndex: 0,
TxValue: value,
TxNumConfs: numConfs,
}
return coinset.Coin(c)
}
type coinSelectTest struct {
selector coinset.CoinSelector
inputCoins []coinset.Coin
targetValue int64
expectedCoins []coinset.Coin
expectedError error
}
func testCoinSelector(tests []coinSelectTest, t *testing.T) {
for testIndex, test := range tests {
cs, err := test.selector.CoinSelect(test.targetValue, test.inputCoins)
if err != test.expectedError {
t.Errorf("[%d] expected a different error: got=%v, expected=%v", testIndex, err, test.expectedError)
continue
}
if test.expectedCoins != nil {
if cs == nil {
t.Errorf("[%d] expected non-nil coinset", testIndex)
continue
}
coins := cs.Coins()
if len(coins) != len(test.expectedCoins) {
t.Errorf("[%d] expected different number of coins: got=%d, expected=%d", testIndex, len(coins), len(test.expectedCoins))
continue
}
for n := 0; n < len(test.expectedCoins); n++ {
if coins[n] != test.expectedCoins[n] {
t.Errorf("[%d] expected different coins at coin index %d: got=%#v, expected=%#v", testIndex, n, coins[n], test.expectedCoins[n])
continue
}
}
coinSet := coinset.NewCoinSet(coins)
if coinSet.TotalValue() < test.targetValue {
t.Errorf("[%d] targetValue not satistifed", testIndex)
continue
}
}
}
}
var coins = []coinset.Coin{
NewCoin(1, 100000000, 1),
NewCoin(2, 10000000, 20),
NewCoin(3, 50000000, 0),
NewCoin(4, 25000000, 6),
}
func TestCoinSet(t *testing.T) {
cs := coinset.NewCoinSet(nil)
if cs.PopCoin() != nil {
t.Error("Expected popCoin of empty to be nil")
}
if cs.ShiftCoin() != nil {
t.Error("Expected shiftCoin of empty to be nil")
}
cs.PushCoin(coins[0])
cs.PushCoin(coins[1])
cs.PushCoin(coins[2])
if cs.PopCoin() != coins[2] {
t.Error("Expected third coin")
}
if cs.ShiftCoin() != coins[0] {
t.Error("Expected first coin")
}
mtx := coinset.NewMsgTxWithInputCoins(cs)
if len(mtx.TxIn) != 1 {
t.Errorf("Expected only 1 TxIn, got %d", len(mtx.TxIn))
}
op := mtx.TxIn[0].PreviousOutpoint
if !op.Hash.IsEqual(coins[1].Hash()) || op.Index != coins[1].Index() {
t.Errorf("Expected the second coin to be added as input to mtx")
}
}
var minIndexSelectors = []coinset.MinIndexCoinSelector{
coinset.MinIndexCoinSelector{MaxInputs: 10, MinChangeAmount: 10000},
coinset.MinIndexCoinSelector{MaxInputs: 2, MinChangeAmount: 10000},
}
var minIndexTests = []coinSelectTest{
{minIndexSelectors[0], coins, coins[0].Value() - minIndexSelectors[0].MinChangeAmount, []coinset.Coin{coins[0]}, nil},
{minIndexSelectors[0], coins, coins[0].Value() - minIndexSelectors[0].MinChangeAmount + 1, []coinset.Coin{coins[0], coins[1]}, nil},
{minIndexSelectors[0], coins, 100000000, []coinset.Coin{coins[0]}, nil},
{minIndexSelectors[0], coins, 110000000, []coinset.Coin{coins[0], coins[1]}, nil},
{minIndexSelectors[0], coins, 140000000, []coinset.Coin{coins[0], coins[1], coins[2]}, nil},
{minIndexSelectors[0], coins, 200000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{minIndexSelectors[1], coins, 10000000, []coinset.Coin{coins[0]}, nil},
{minIndexSelectors[1], coins, 110000000, []coinset.Coin{coins[0], coins[1]}, nil},
{minIndexSelectors[1], coins, 140000000, nil, coinset.ErrCoinsNoSelectionAvailable},
}
func TestMinIndexSelector(t *testing.T) {
testCoinSelector(minIndexTests, t)
}
var minNumberSelectors = []coinset.MinNumberCoinSelector{
coinset.MinNumberCoinSelector{MaxInputs: 10, MinChangeAmount: 10000},
coinset.MinNumberCoinSelector{MaxInputs: 2, MinChangeAmount: 10000},
}
var minNumberTests = []coinSelectTest{
{minNumberSelectors[0], coins, coins[0].Value() - minNumberSelectors[0].MinChangeAmount, []coinset.Coin{coins[0]}, nil},
{minNumberSelectors[0], coins, coins[0].Value() - minNumberSelectors[0].MinChangeAmount + 1, []coinset.Coin{coins[0], coins[2]}, nil},
{minNumberSelectors[0], coins, 100000000, []coinset.Coin{coins[0]}, nil},
{minNumberSelectors[0], coins, 110000000, []coinset.Coin{coins[0], coins[2]}, nil},
{minNumberSelectors[0], coins, 160000000, []coinset.Coin{coins[0], coins[2], coins[3]}, nil},
{minNumberSelectors[0], coins, 184990000, []coinset.Coin{coins[0], coins[2], coins[3], coins[1]}, nil},
{minNumberSelectors[0], coins, 184990001, nil, coinset.ErrCoinsNoSelectionAvailable},
{minNumberSelectors[0], coins, 200000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{minNumberSelectors[1], coins, 10000000, []coinset.Coin{coins[0]}, nil},
{minNumberSelectors[1], coins, 110000000, []coinset.Coin{coins[0], coins[2]}, nil},
{minNumberSelectors[1], coins, 140000000, []coinset.Coin{coins[0], coins[2]}, nil},
}
func TestMinNumberSelector(t *testing.T) {
testCoinSelector(minNumberTests, t)
}
var maxValueAgeSelectors = []coinset.MaxValueAgeCoinSelector{
coinset.MaxValueAgeCoinSelector{MaxInputs: 10, MinChangeAmount: 10000},
coinset.MaxValueAgeCoinSelector{MaxInputs: 2, MinChangeAmount: 10000},
}
var maxValueAgeTests = []coinSelectTest{
{maxValueAgeSelectors[0], coins, 100000, []coinset.Coin{coins[1]}, nil},
{maxValueAgeSelectors[0], coins, 10000000, []coinset.Coin{coins[1]}, nil},
{maxValueAgeSelectors[0], coins, 10000001, []coinset.Coin{coins[1], coins[3]}, nil},
{maxValueAgeSelectors[0], coins, 35000000, []coinset.Coin{coins[1], coins[3]}, nil},
{maxValueAgeSelectors[0], coins, 135000000, []coinset.Coin{coins[1], coins[3], coins[0]}, nil},
{maxValueAgeSelectors[0], coins, 185000000, []coinset.Coin{coins[1], coins[3], coins[0], coins[2]}, nil},
{maxValueAgeSelectors[0], coins, 200000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{maxValueAgeSelectors[1], coins, 40000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{maxValueAgeSelectors[1], coins, 35000000, []coinset.Coin{coins[1], coins[3]}, nil},
{maxValueAgeSelectors[1], coins, 34990001, nil, coinset.ErrCoinsNoSelectionAvailable},
}
func TestMaxValueAgeSelector(t *testing.T) {
testCoinSelector(maxValueAgeTests, t)
}
var minPrioritySelectors = []coinset.MinPriorityCoinSelector{
coinset.MinPriorityCoinSelector{MaxInputs: 10, MinChangeAmount: 10000, MinAvgValueAgePerInput: 100000000},
coinset.MinPriorityCoinSelector{MaxInputs: 02, MinChangeAmount: 10000, MinAvgValueAgePerInput: 200000000},
coinset.MinPriorityCoinSelector{MaxInputs: 02, MinChangeAmount: 10000, MinAvgValueAgePerInput: 150000000},
coinset.MinPriorityCoinSelector{MaxInputs: 03, MinChangeAmount: 10000, MinAvgValueAgePerInput: 150000000},
coinset.MinPriorityCoinSelector{MaxInputs: 10, MinChangeAmount: 10000, MinAvgValueAgePerInput: 1000000000},
coinset.MinPriorityCoinSelector{MaxInputs: 10, MinChangeAmount: 10000, MinAvgValueAgePerInput: 175000000},
coinset.MinPriorityCoinSelector{MaxInputs: 02, MinChangeAmount: 10000, MinAvgValueAgePerInput: 125000000},
}
var connectedCoins = []coinset.Coin{coins[0], coins[1], coins[3]}
var minPriorityTests = []coinSelectTest{
{minPrioritySelectors[0], connectedCoins, 100000000, []coinset.Coin{coins[0]}, nil},
{minPrioritySelectors[0], connectedCoins, 125000000, []coinset.Coin{coins[0], coins[3]}, nil},
{minPrioritySelectors[0], connectedCoins, 135000000, []coinset.Coin{coins[0], coins[3], coins[1]}, nil},
{minPrioritySelectors[0], connectedCoins, 140000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{minPrioritySelectors[1], connectedCoins, 100000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{minPrioritySelectors[1], connectedCoins, 10000000, []coinset.Coin{coins[1]}, nil},
{minPrioritySelectors[1], connectedCoins, 100000000, nil, coinset.ErrCoinsNoSelectionAvailable},
{minPrioritySelectors[2], connectedCoins, 11000000, []coinset.Coin{coins[3]}, nil},
{minPrioritySelectors[2], connectedCoins, 25000001, []coinset.Coin{coins[3], coins[1]}, nil},
{minPrioritySelectors[3], connectedCoins, 25000001, []coinset.Coin{coins[3], coins[1], coins[0]}, nil},
{minPrioritySelectors[3], connectedCoins, 100000000, []coinset.Coin{coins[3], coins[1], coins[0]}, nil},
{minPrioritySelectors[3], []coinset.Coin{coins[1], coins[2]}, 10000000, []coinset.Coin{coins[1]}, nil},
{minPrioritySelectors[4], connectedCoins, 1, nil, coinset.ErrCoinsNoSelectionAvailable},
{minPrioritySelectors[5], connectedCoins, 20000000, []coinset.Coin{coins[1], coins[3]}, nil},
{minPrioritySelectors[6], connectedCoins, 25000000, []coinset.Coin{coins[3], coins[0]}, nil},
}
func TestMinPrioritySelector(t *testing.T) {
testCoinSelector(minPriorityTests, t)
}
var (
// should be two outpoints, with 1st one having 0.035BTC value.
testSimpleCoinTxHash = "9b5965c86de51d5dc824e179a05cf232db78c80ae86ca9d7cb2a655b5e19c1e2"
testSimpleCoinTxHex = "0100000001a214a110f79e4abe073865ea5b3745c6e82c913bad44be70652804a5e4003b0a010000008c493046022100edd18a69664efa57264be207100c203e6cade1888cbb88a0ad748548256bb2f0022100f1027dc2e6c7f248d78af1dd90027b5b7d8ec563bb62aa85d4e74d6376f3868c0141048f3757b65ed301abd1b0e8942d1ab5b50594d3314cff0299f300c696376a0a9bf72e74710a8af7a5372d4af4bb519e2701a094ef48c8e48e3b65b28502452dceffffffff02e0673500000000001976a914686dd149a79b4a559d561fbc396d3e3c6628b98d88ace86ef102000000001976a914ac3f995655e81b875b38b64351d6f896ddbfc68588ac00000000"
testSimpleCoinTxValue0 = int64(3500000)
testSimpleCoinTxPkScript0Hex = "76a914686dd149a79b4a559d561fbc396d3e3c6628b98d88ac"
testSimpleCoinTxPkScript0Bytes, _ = hex.DecodeString(testSimpleCoinTxPkScript0Hex)
testSimpleCoinTxBytes, _ = hex.DecodeString(testSimpleCoinTxHex)
testSimpleCoinTx, _ = btcutil.NewTxFromBytes(testSimpleCoinTxBytes)
testSimpleCoin = &coinset.SimpleCoin{
Tx: testSimpleCoinTx,
TxIndex: 0,
TxNumConfs: 1,
}
)
func TestSimpleCoin(t *testing.T) {
if testSimpleCoin.Hash().String() != testSimpleCoinTxHash {
t.Error("Different value for tx hash than expected")
}
if testSimpleCoin.Index() != 0 {
t.Error("Different value for index of outpoint than expected")
}
if testSimpleCoin.Value() != testSimpleCoinTxValue0 {
t.Error("Different value of coin value than expected")
}
if !bytes.Equal(testSimpleCoin.PkScript(), testSimpleCoinTxPkScript0Bytes) {
t.Error("Different value of coin pkScript than expected")
}
if testSimpleCoin.NumConfs() != 1 {
t.Error("Differet value of num confs than expected")
}
if testSimpleCoin.ValueAge() != testSimpleCoinTxValue0 {
t.Error("Different value of coin value * age than expected")
}
}

17
coinset/cov_report.sh Normal file
View file

@ -0,0 +1,17 @@
#!/bin/sh
# This script uses gocov to generate a test coverage report.
# The gocov tool my be obtained with the following command:
# go get github.com/axw/gocov/gocov
#
# It will be installed to $GOPATH/bin, so ensure that location is in your $PATH.
# Check for gocov.
type gocov >/dev/null 2>&1
if [ $? -ne 0 ]; then
echo >&2 "This script requires the gocov tool."
echo >&2 "You may obtain it with the following command:"
echo >&2 "go get github.com/axw/gocov/gocov"
exit 1
fi
gocov test | gocov report

31
coinset/test_coverage.txt Normal file
View file

@ -0,0 +1,31 @@
github.com/conformal/btcutil/coinset/coins.go MinPriorityCoinSelector.CoinSelect 100.00% (39/39)
github.com/conformal/btcutil/coinset/coins.go NewMsgTxWithInputCoins 100.00% (6/6)
github.com/conformal/btcutil/coinset/coins.go MinIndexCoinSelector.CoinSelect 100.00% (6/6)
github.com/conformal/btcutil/coinset/coins.go CoinSet.removeElement 100.00% (5/5)
github.com/conformal/btcutil/coinset/coins.go NewCoinSet 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go CoinSet.Coins 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go CoinSet.PopCoin 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go CoinSet.ShiftCoin 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go MinNumberCoinSelector.CoinSelect 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go MaxValueAgeCoinSelector.CoinSelect 100.00% (4/4)
github.com/conformal/btcutil/coinset/coins.go CoinSet.PushCoin 100.00% (3/3)
github.com/conformal/btcutil/coinset/coins.go CoinSet.TotalValueAge 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.NumConfs 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.ValueAge 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go CoinSet.TotalValue 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byValueAge.Len 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byValueAge.Swap 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byValueAge.Less 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byAmount.Len 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byAmount.Swap 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go byAmount.Less 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.Hash 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.Index 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.txOut 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.Value 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go SimpleCoin.PkScript 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go CoinSet.Num 100.00% (1/1)
github.com/conformal/btcutil/coinset/coins.go satisfiesTargetValue 100.00% (1/1)
github.com/conformal/btcutil/coinset ---------------------------------- 100.00% (100/100)

View file

@ -1,59 +1,59 @@
github.com/conformal/btcutil/base58.go Base58Decode 100.00% (20/20)
github.com/conformal/btcutil/address.go DecodeAddr 100.00% (18/18)
github.com/conformal/btcutil/base58.go Base58Encode 100.00% (15/15)
github.com/conformal/btcutil/addrconvs.go DecodeAddress 100.00% (14/14)
github.com/conformal/btcutil/block.go Block.Tx 100.00% (12/12)
github.com/conformal/btcutil/block.go Block.Transactions 100.00% (11/11)
github.com/conformal/btcutil/block.go Block.TxShas 100.00% (10/10)
github.com/conformal/btcutil/address.go encodeAddress 100.00% (9/9)
github.com/conformal/btcutil/addrconvs.go EncodeAddress 100.00% (8/8)
github.com/conformal/btcutil/addrconvs.go EncodeScriptHash 100.00% (8/8)
github.com/conformal/btcutil/addrconvs.go encodeHashWithNetId 100.00% (7/7)
github.com/conformal/btcutil/addrconvs.go EncodeAddress 100.00% (8/8)
github.com/conformal/btcutil/tx.go NewTxFromBytes 100.00% (7/7)
github.com/conformal/btcutil/block.go NewBlockFromBytes 100.00% (7/7)
github.com/conformal/btcutil/addrconvs.go encodeHashWithNetId 100.00% (7/7)
github.com/conformal/btcutil/block.go Block.Sha 100.00% (5/5)
github.com/conformal/btcutil/tx.go Tx.Sha 100.00% (5/5)
github.com/conformal/btcutil/block.go Block.TxSha 100.00% (4/4)
github.com/conformal/btcutil/hash160.go calcHash 100.00% (2/2)
github.com/conformal/btcutil/address.go NewAddressScriptHash 100.00% (2/2)
github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1)
github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressScriptHash.String 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1)
github.com/conformal/btcutil/block.go Block.MsgBlock 100.00% (1/1)
github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressScriptHash.ScriptAddress 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKeyHash.ScriptAddress 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKeyHash.EncodeAddress 100.00% (1/1)
github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1)
github.com/conformal/btcutil/block.go NewBlock 100.00% (1/1)
github.com/conformal/btcutil/block.go Block.SetHeight 100.00% (1/1)
github.com/conformal/btcutil/block.go Block.Height 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKey.EncodeAddress 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKey.ScriptAddress 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1)
github.com/conformal/btcutil/tx.go NewTx 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKey.String 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressScriptHash.EncodeAddress 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.SetIndex 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.Index 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressScriptHash.ScriptAddress 100.00% (1/1)
github.com/conformal/btcutil/hash160.go Hash160 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKeyHash.String 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressPubKeyHash.EncodeAddress 100.00% (1/1)
github.com/conformal/btcutil/address.go AddressScriptHash.EncodeAddress 100.00% (1/1)
github.com/conformal/btcutil/block.go NewBlockFromBlockAndBytes 100.00% (1/1)
github.com/conformal/btcutil/block.go OutOfRangeError.Error 100.00% (1/1)
github.com/conformal/btcutil/address.go DecodeAddr 95.65% (22/23)
github.com/conformal/btcutil/block.go Block.MsgBlock 100.00% (1/1)
github.com/conformal/btcutil/tx.go Tx.MsgTx 100.00% (1/1)
github.com/conformal/btcutil/appdata.go appDataDir 92.00% (23/25)
github.com/conformal/btcutil/address.go NewAddressPubKeyHash 91.67% (11/12)
github.com/conformal/btcutil/address.go NewAddressScriptHashFromHash 91.67% (11/12)
github.com/conformal/btcutil/address.go NewAddressPubKeyHash 91.67% (11/12)
github.com/conformal/btcutil/addrconvs.go EncodePrivateKey 90.91% (20/22)
github.com/conformal/btcutil/block.go Block.Bytes 88.89% (8/9)
github.com/conformal/btcutil/block.go Block.TxLoc 88.89% (8/9)
github.com/conformal/btcutil/block.go Block.Bytes 88.89% (8/9)
github.com/conformal/btcutil/address.go AddressPubKey.serialize 85.71% (6/7)
github.com/conformal/btcutil/addrconvs.go DecodePrivateKey 83.33% (20/24)
github.com/conformal/btcutil/address.go NewAddressPubKey 83.33% (15/18)
github.com/conformal/btcutil/address.go checkBitcoinNet 83.33% (5/6)
github.com/conformal/btcutil/addrconvs.go DecodePrivateKey 82.61% (19/23)
github.com/conformal/btcutil/address.go AddressPubKeyHash.IsForNet 60.00% (3/5)
github.com/conformal/btcutil/address.go AddressPubKey.IsForNet 60.00% (3/5)
github.com/conformal/btcutil/address.go AddressScriptHash.IsForNet 60.00% (3/5)
github.com/conformal/btcutil/certgen.go NewTLSCertPair 0.00% (0/50)
github.com/conformal/btcutil/address.go AddressPubKey.AddressPubKeyHash 0.00% (0/3)
github.com/conformal/btcutil/address.go AddressPubKey.Format 0.00% (0/1)
github.com/conformal/btcutil/address.go AddressPubKey.SetFormat 0.00% (0/1)
github.com/conformal/btcutil/address.go AddressPubKeyHash.String 0.00% (0/1)
github.com/conformal/btcutil/appdata.go AppDataDir 0.00% (0/1)
github.com/conformal/btcutil ------------------------------- 80.15% (323/403)
github.com/conformal/btcutil/address.go AddressPubKey.SetFormat 0.00% (0/1)
github.com/conformal/btcutil/address.go AddressPubKey.Format 0.00% (0/1)
github.com/conformal/btcutil/address.go AddressScriptHash.String 0.00% (0/1)
github.com/conformal/btcutil ------------------------------- 79.70% (318/399)