github.com/bytom/bytom@v1.1.2-0.20221014091027-bbcba3df6075/account/utxo_keeper.go (about)

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