lbcwallet/votingpool/input_selection.go

272 lines
8.6 KiB
Go
Raw Normal View History

/*
* Copyright (c) 2015-2016 The btcsuite developers
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
package votingpool
import (
"bytes"
"fmt"
"sort"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcutil"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/btcsuite/btcwallet/wtxmgr"
)
const eligibleInputMinConfirmations = 100
2015-04-07 17:40:13 +02:00
// credit is an abstraction over wtxmgr.Credit used in the construction of
// voting pool withdrawal transactions.
type credit struct {
2015-04-07 17:40:13 +02:00
wtxmgr.Credit
addr WithdrawalAddress
}
2015-04-07 17:40:13 +02:00
func newCredit(c wtxmgr.Credit, addr WithdrawalAddress) credit {
return credit{Credit: c, addr: addr}
}
func (c *credit) String() string {
2015-04-07 17:40:13 +02:00
return fmt.Sprintf("credit of %v locked to %v", c.Amount, c.addr)
}
// byAddress defines the methods needed to satisify sort.Interface to sort a
2015-04-07 17:40:13 +02:00
// slice of credits by their address.
type byAddress []credit
func (c byAddress) Len() int { return len(c) }
func (c byAddress) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
// Less returns true if the element at positions i is smaller than the
// element at position j. The 'smaller-than' relation is defined to be
// the lexicographic ordering defined on the tuple (SeriesID, Index,
// Branch, TxSha, OutputIndex).
func (c byAddress) Less(i, j int) bool {
2015-04-07 17:40:13 +02:00
iAddr := c[i].addr
jAddr := c[j].addr
if iAddr.seriesID < jAddr.seriesID {
return true
}
if iAddr.seriesID > jAddr.seriesID {
return false
}
// The seriesID are equal, so compare index.
if iAddr.index < jAddr.index {
return true
}
if iAddr.index > jAddr.index {
return false
}
// The seriesID and index are equal, so compare branch.
if iAddr.branch < jAddr.branch {
return true
}
if iAddr.branch > jAddr.branch {
return false
}
// The seriesID, index, and branch are equal, so compare hash.
txidComparison := bytes.Compare(c[i].OutPoint.Hash[:], c[j].OutPoint.Hash[:])
if txidComparison < 0 {
return true
}
if txidComparison > 0 {
return false
}
// The seriesID, index, branch, and hash are equal, so compare output
// index.
2015-04-07 17:40:13 +02:00
return c[i].OutPoint.Index < c[j].OutPoint.Index
}
// getEligibleInputs returns eligible inputs with addresses between startAddress
// and the last used address of lastSeriesID. They're reverse ordered based on
// their address.
func (p *Pool) getEligibleInputs(ns, addrmgrNs walletdb.ReadBucket, store *wtxmgr.Store, txmgrNs walletdb.ReadBucket, startAddress WithdrawalAddress,
lastSeriesID uint32, dustThreshold btcutil.Amount, chainHeight int32,
2015-04-07 17:40:13 +02:00
minConf int) ([]credit, error) {
if p.Series(lastSeriesID) == nil {
str := fmt.Sprintf("lastSeriesID (%d) does not exist", lastSeriesID)
return nil, newError(ErrSeriesNotExists, str, nil)
}
unspents, err := store.UnspentOutputs(txmgrNs)
if err != nil {
return nil, newError(ErrInputSelection, "failed to get unspent outputs", err)
}
addrMap, err := groupCreditsByAddr(unspents, p.manager.ChainParams())
if err != nil {
return nil, err
}
2015-04-07 17:40:13 +02:00
var inputs []credit
address := startAddress
for {
log.Debugf("Looking for eligible inputs at address %v", address.addrIdentifier())
if candidates, ok := addrMap[address.addr.EncodeAddress()]; ok {
2015-04-07 17:40:13 +02:00
var eligibles []credit
for _, c := range candidates {
2015-04-07 17:40:13 +02:00
candidate := newCredit(c, address)
if p.isCreditEligible(candidate, minConf, chainHeight, dustThreshold) {
eligibles = append(eligibles, candidate)
}
}
inputs = append(inputs, eligibles...)
}
nAddr, err := nextAddr(p, ns, addrmgrNs, address.seriesID, address.branch, address.index, lastSeriesID+1)
if err != nil {
return nil, newError(ErrInputSelection, "failed to get next withdrawal address", err)
} else if nAddr == nil {
log.Debugf("getEligibleInputs: reached last addr, stopping")
break
}
address = *nAddr
}
sort.Sort(sort.Reverse(byAddress(inputs)))
return inputs, nil
}
// nextAddr returns the next WithdrawalAddress according to the input selection
// rules: http://opentransactions.org/wiki/index.php/Input_Selection_Algorithm_(voting_pools)
// It returns nil if the new address' seriesID is >= stopSeriesID.
func nextAddr(p *Pool, ns, addrmgrNs walletdb.ReadBucket, seriesID uint32, branch Branch, index Index, stopSeriesID uint32) (
*WithdrawalAddress, error) {
series := p.Series(seriesID)
if series == nil {
return nil, newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil)
}
branch++
if int(branch) > len(series.publicKeys) {
highestIdx, err := p.highestUsedSeriesIndex(ns, seriesID)
if err != nil {
return nil, err
}
if index > highestIdx {
seriesID++
log.Debugf("nextAddr(): reached last branch (%d) and highest used index (%d), "+
"moving on to next series (%d)", branch, index, seriesID)
index = 0
} else {
index++
}
branch = 0
}
if seriesID >= stopSeriesID {
return nil, nil
}
addr, err := p.WithdrawalAddress(ns, addrmgrNs, seriesID, branch, index)
if err != nil && err.(Error).ErrorCode == ErrWithdrawFromUnusedAddr {
// The used indices will vary between branches so sometimes we'll try to
// get a WithdrawalAddress that hasn't been used before, and in such
// cases we just need to move on to the next one.
log.Debugf("nextAddr(): skipping addr (series #%d, branch #%d, index #%d) as it hasn't "+
"been used before", seriesID, branch, index)
return nextAddr(p, ns, addrmgrNs, seriesID, branch, index, stopSeriesID)
}
return addr, err
}
// highestUsedSeriesIndex returns the highest index among all of this Pool's
// used addresses for the given seriesID. It returns 0 if there are no used
// addresses with the given seriesID.
func (p *Pool) highestUsedSeriesIndex(ns walletdb.ReadBucket, seriesID uint32) (Index, error) {
maxIdx := Index(0)
series := p.Series(seriesID)
if series == nil {
return maxIdx,
newError(ErrSeriesNotExists, fmt.Sprintf("unknown seriesID: %d", seriesID), nil)
}
for i := range series.publicKeys {
idx, err := p.highestUsedIndexFor(ns, seriesID, Branch(i))
if err != nil {
return Index(0), err
}
if idx > maxIdx {
maxIdx = idx
}
}
return maxIdx, nil
}
// groupCreditsByAddr converts a slice of credits to a map from the string
// representation of an encoded address to the unspent outputs associated with
// that address.
2015-04-07 17:40:13 +02:00
func groupCreditsByAddr(credits []wtxmgr.Credit, chainParams *chaincfg.Params) (
map[string][]wtxmgr.Credit, error) {
addrMap := make(map[string][]wtxmgr.Credit)
for _, c := range credits {
2015-04-07 17:40:13 +02:00
_, addrs, _, err := txscript.ExtractPkScriptAddrs(c.PkScript, chainParams)
if err != nil {
return nil, newError(ErrInputSelection, "failed to obtain input address", err)
}
// As our credits are all P2SH we should never have more than one
// address per credit, so let's error out if that assumption is
// violated.
if len(addrs) != 1 {
return nil, newError(ErrInputSelection, "input doesn't have exactly one address", nil)
}
encAddr := addrs[0].EncodeAddress()
if v, ok := addrMap[encAddr]; ok {
addrMap[encAddr] = append(v, c)
} else {
2015-04-07 17:40:13 +02:00
addrMap[encAddr] = []wtxmgr.Credit{c}
}
}
return addrMap, nil
}
// isCreditEligible tests a given credit for eligibilty with respect
// to number of confirmations, the dust threshold and that it is not
// the charter output.
2015-04-07 17:40:13 +02:00
func (p *Pool) isCreditEligible(c credit, minConf int, chainHeight int32,
dustThreshold btcutil.Amount) bool {
2015-04-07 17:40:13 +02:00
if c.Amount < dustThreshold {
return false
}
2015-04-07 17:40:13 +02:00
if confirms(c.BlockMeta.Block.Height, chainHeight) < int32(minConf) {
return false
}
if p.isCharterOutput(c) {
return false
}
return true
}
// isCharterOutput - TODO: In order to determine this, we need the txid
// and the output index of the current charter output, which we don't have yet.
2015-04-07 17:40:13 +02:00
func (p *Pool) isCharterOutput(c credit) bool {
return false
}
2015-04-07 17:40:13 +02:00
// confirms returns the number of confirmations for a transaction in a block at
// height txHeight (or -1 for an unconfirmed tx) given the chain height
// curHeight.
func confirms(txHeight, curHeight int32) int32 {
switch {
case txHeight == -1, txHeight > curHeight:
return 0
default:
return curHeight - txHeight + 1
}
}