github.com/Bytom/bytom@v1.1.2-0.20210127130405-ae40204c0b09/account/utxo_keeper.go (about)

     1  package account
     2  
     3  import (
     4  	"container/list"
     5  	"encoding/json"
     6  	"sort"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	log "github.com/sirupsen/logrus"
    12  
    13  	"github.com/bytom/bytom/errors"
    14  	"github.com/bytom/bytom/protocol/bc"
    15  	dbm "github.com/bytom/bytom/database/leveldb"
    16  )
    17  
    18  const desireUtxoCount = 5
    19  
    20  // pre-define error types
    21  var (
    22  	ErrInsufficient = errors.New("reservation found insufficient funds")
    23  	ErrImmature     = errors.New("reservation found immature funds")
    24  	ErrReserved     = errors.New("reservation found outputs already reserved")
    25  	ErrMatchUTXO    = errors.New("can't find utxo with given hash")
    26  	ErrReservation  = errors.New("couldn't find reservation")
    27  )
    28  
    29  // UTXO describes an individual account utxo.
    30  type UTXO struct {
    31  	OutputID            bc.Hash
    32  	SourceID            bc.Hash
    33  	AssetID             bc.AssetID
    34  	Amount              uint64
    35  	SourcePos           uint64
    36  	ControlProgram      []byte
    37  	AccountID           string
    38  	Address             string
    39  	ControlProgramIndex uint64
    40  	ValidHeight         uint64
    41  	Change              bool
    42  }
    43  
    44  // reservation describes a reservation of a set of UTXOs
    45  type reservation struct {
    46  	id     uint64
    47  	utxos  []*UTXO
    48  	change uint64
    49  	expiry time.Time
    50  }
    51  
    52  type utxoKeeper struct {
    53  	// `sync/atomic` expects the first word in an allocated struct to be 64-bit
    54  	// aligned on both ARM and x86-32. See https://goo.gl/zW7dgq for more details.
    55  	nextIndex     uint64
    56  	db            dbm.DB
    57  	mtx           sync.RWMutex
    58  	currentHeight func() uint64
    59  
    60  	unconfirmed  map[bc.Hash]*UTXO
    61  	reserved     map[bc.Hash]uint64
    62  	reservations map[uint64]*reservation
    63  }
    64  
    65  func newUtxoKeeper(f func() uint64, walletdb dbm.DB) *utxoKeeper {
    66  	uk := &utxoKeeper{
    67  		db:            walletdb,
    68  		currentHeight: f,
    69  		unconfirmed:   make(map[bc.Hash]*UTXO),
    70  		reserved:      make(map[bc.Hash]uint64),
    71  		reservations:  make(map[uint64]*reservation),
    72  	}
    73  	go uk.expireWorker()
    74  	return uk
    75  }
    76  
    77  func (uk *utxoKeeper) AddUnconfirmedUtxo(utxos []*UTXO) {
    78  	uk.mtx.Lock()
    79  	defer uk.mtx.Unlock()
    80  
    81  	for _, utxo := range utxos {
    82  		uk.unconfirmed[utxo.OutputID] = utxo
    83  	}
    84  }
    85  
    86  // Cancel canceling the reservation with the provided ID.
    87  func (uk *utxoKeeper) Cancel(rid uint64) {
    88  	uk.mtx.Lock()
    89  	uk.cancel(rid)
    90  	uk.mtx.Unlock()
    91  }
    92  
    93  // ListUnconfirmed return all the unconfirmed utxos
    94  func (uk *utxoKeeper) ListUnconfirmed() []*UTXO {
    95  	uk.mtx.Lock()
    96  	defer uk.mtx.Unlock()
    97  
    98  	utxos := []*UTXO{}
    99  	for _, utxo := range uk.unconfirmed {
   100  		utxos = append(utxos, utxo)
   101  	}
   102  	return utxos
   103  }
   104  
   105  func (uk *utxoKeeper) RemoveUnconfirmedUtxo(hashes []*bc.Hash) {
   106  	uk.mtx.Lock()
   107  	defer uk.mtx.Unlock()
   108  
   109  	for _, hash := range hashes {
   110  		delete(uk.unconfirmed, *hash)
   111  	}
   112  }
   113  
   114  func (uk *utxoKeeper) Reserve(accountID string, assetID *bc.AssetID, amount uint64, useUnconfirmed bool, exp time.Time) (*reservation, error) {
   115  	uk.mtx.Lock()
   116  	defer uk.mtx.Unlock()
   117  
   118  	utxos, immatureAmount := uk.findUtxos(accountID, assetID, useUnconfirmed)
   119  	optUtxos, optAmount, reservedAmount := uk.optUTXOs(utxos, amount)
   120  	if optAmount+reservedAmount+immatureAmount < amount {
   121  		return nil, ErrInsufficient
   122  	}
   123  
   124  	if optAmount+reservedAmount < amount {
   125  		return nil, ErrImmature
   126  	}
   127  
   128  	if optAmount < amount {
   129  		return nil, ErrReserved
   130  	}
   131  
   132  	result := &reservation{
   133  		id:     atomic.AddUint64(&uk.nextIndex, 1),
   134  		utxos:  optUtxos,
   135  		change: optAmount - amount,
   136  		expiry: exp,
   137  	}
   138  
   139  	uk.reservations[result.id] = result
   140  	for _, u := range optUtxos {
   141  		uk.reserved[u.OutputID] = result.id
   142  	}
   143  	return result, nil
   144  }
   145  
   146  func (uk *utxoKeeper) ReserveParticular(outHash bc.Hash, useUnconfirmed bool, exp time.Time) (*reservation, error) {
   147  	uk.mtx.Lock()
   148  	defer uk.mtx.Unlock()
   149  
   150  	if _, ok := uk.reserved[outHash]; ok {
   151  		return nil, ErrReserved
   152  	}
   153  
   154  	u, err := uk.findUtxo(outHash, useUnconfirmed)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	if u.ValidHeight > uk.currentHeight() {
   160  		return nil, ErrImmature
   161  	}
   162  
   163  	result := &reservation{
   164  		id:     atomic.AddUint64(&uk.nextIndex, 1),
   165  		utxos:  []*UTXO{u},
   166  		expiry: exp,
   167  	}
   168  	uk.reservations[result.id] = result
   169  	uk.reserved[u.OutputID] = result.id
   170  	return result, nil
   171  }
   172  
   173  func (uk *utxoKeeper) cancel(rid uint64) {
   174  	res, ok := uk.reservations[rid]
   175  	if !ok {
   176  		return
   177  	}
   178  
   179  	delete(uk.reservations, rid)
   180  	for _, utxo := range res.utxos {
   181  		delete(uk.reserved, utxo.OutputID)
   182  	}
   183  }
   184  
   185  func (uk *utxoKeeper) expireWorker() {
   186  	ticker := time.NewTicker(1000 * time.Millisecond)
   187  	defer ticker.Stop()
   188  
   189  	for now := range ticker.C {
   190  		uk.expireReservation(now)
   191  	}
   192  }
   193  
   194  func (uk *utxoKeeper) expireReservation(t time.Time) {
   195  	uk.mtx.Lock()
   196  	defer uk.mtx.Unlock()
   197  
   198  	for rid, res := range uk.reservations {
   199  		if res.expiry.Before(t) {
   200  			uk.cancel(rid)
   201  		}
   202  	}
   203  }
   204  
   205  func (uk *utxoKeeper) findUtxos(accountID string, assetID *bc.AssetID, useUnconfirmed bool) ([]*UTXO, uint64) {
   206  	immatureAmount := uint64(0)
   207  	currentHeight := uk.currentHeight()
   208  	utxos := []*UTXO{}
   209  	appendUtxo := func(u *UTXO) {
   210  		if u.AccountID != accountID || u.AssetID != *assetID {
   211  			return
   212  		}
   213  		if u.ValidHeight > currentHeight {
   214  			immatureAmount += u.Amount
   215  		} else {
   216  			utxos = append(utxos, u)
   217  		}
   218  	}
   219  
   220  	utxoIter := uk.db.IteratorPrefix([]byte(UTXOPreFix))
   221  	defer utxoIter.Release()
   222  	for utxoIter.Next() {
   223  		u := &UTXO{}
   224  		if err := json.Unmarshal(utxoIter.Value(), u); err != nil {
   225  			log.WithFields(log.Fields{"module": logModule, "err": err}).Error("utxoKeeper findUtxos fail on unmarshal utxo")
   226  			continue
   227  		}
   228  		appendUtxo(u)
   229  	}
   230  	if !useUnconfirmed {
   231  		return utxos, immatureAmount
   232  	}
   233  
   234  	for _, u := range uk.unconfirmed {
   235  		appendUtxo(u)
   236  	}
   237  	return utxos, immatureAmount
   238  }
   239  
   240  func (uk *utxoKeeper) findUtxo(outHash bc.Hash, useUnconfirmed bool) (*UTXO, error) {
   241  	if u, ok := uk.unconfirmed[outHash]; useUnconfirmed && ok {
   242  		return u, nil
   243  	}
   244  
   245  	u := &UTXO{}
   246  	if data := uk.db.Get(StandardUTXOKey(outHash)); data != nil {
   247  		return u, json.Unmarshal(data, u)
   248  	}
   249  	if data := uk.db.Get(ContractUTXOKey(outHash)); data != nil {
   250  		return u, json.Unmarshal(data, u)
   251  	}
   252  	return nil, ErrMatchUTXO
   253  }
   254  
   255  func (uk *utxoKeeper) optUTXOs(utxos []*UTXO, amount uint64) ([]*UTXO, uint64, uint64) {
   256  	//sort the utxo by amount, bigger amount in front
   257  	var optAmount, reservedAmount uint64
   258  	sort.Slice(utxos, func(i, j int) bool {
   259  		return utxos[i].Amount > utxos[j].Amount
   260  	})
   261  
   262  	//push all the available utxos into list
   263  	utxoList := list.New()
   264  	for _, u := range utxos {
   265  		if _, ok := uk.reserved[u.OutputID]; ok {
   266  			reservedAmount += u.Amount
   267  			continue
   268  		}
   269  		utxoList.PushBack(u)
   270  	}
   271  
   272  	optList := list.New()
   273  	for node := utxoList.Front(); node != nil; node = node.Next() {
   274  		//append utxo if we haven't reached the required amount
   275  		if optAmount < amount {
   276  			optList.PushBack(node.Value)
   277  			optAmount += node.Value.(*UTXO).Amount
   278  			continue
   279  		}
   280  
   281  		largestNode := optList.Front()
   282  		replaceList := list.New()
   283  		replaceAmount := optAmount - largestNode.Value.(*UTXO).Amount
   284  
   285  		for ; node != nil && replaceList.Len() <= desireUtxoCount-optList.Len(); node = node.Next() {
   286  			replaceList.PushBack(node.Value)
   287  			if replaceAmount += node.Value.(*UTXO).Amount; replaceAmount >= amount {
   288  				optList.Remove(largestNode)
   289  				optList.PushBackList(replaceList)
   290  				optAmount = replaceAmount
   291  				break
   292  			}
   293  		}
   294  
   295  		//largestNode remaining the same means that there is nothing to be replaced
   296  		if largestNode == optList.Front() {
   297  			break
   298  		}
   299  	}
   300  
   301  	optUtxos := []*UTXO{}
   302  	for e := optList.Front(); e != nil; e = e.Next() {
   303  		optUtxos = append(optUtxos, e.Value.(*UTXO))
   304  	}
   305  	return optUtxos, optAmount, reservedAmount
   306  }