Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

token selector: streamline filter interfaces #637

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions token/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ var (
type OwnerFilter interface {
// ID is the wallet identifier of the owner
ID() string
// ContainsToken returns true if the passed token is recognized, false otherwise.
ContainsToken(token *token2.UnspentToken) bool
}

// Selector is the interface of token selectors
Expand Down
186 changes: 3 additions & 183 deletions token/services/selector/simple/selector.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (

"github.com/hashicorp/go-uuid"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/tracing"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/view"
"github.com/hyperledger-labs/fabric-token-sdk/token"
token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token"
"github.com/pkg/errors"
Expand Down Expand Up @@ -50,15 +49,10 @@ type selector struct {

// Select selects tokens to be spent based on ownership, quantity, and type
func (s *selector) Select(ownerFilter token.OwnerFilter, q, tokenType string) ([]*token2.ID, token2.Quantity, error) {
if ownerFilter == nil {
ownerFilter = &allOwners{}
if ownerFilter == nil || len(ownerFilter.ID()) == 0 {
return nil, nil, errors.Errorf("no owner filter specified")
}

if len(ownerFilter.ID()) != 0 {
return s.selectByID(ownerFilter, q, tokenType)
}

return s.selectByOwner(ownerFilter, q, tokenType)
return s.selectByID(ownerFilter, q, tokenType)
}

func (s *selector) concurrencyCheck(ids []*token2.ID) error {
Expand All @@ -77,7 +71,6 @@ func (s *selector) selectByID(ownerFilter token.OwnerFilter, q string, tokenType
var toBeSpent []*token2.ID
var sum token2.Quantity
var potentialSumWithLocked token2.Quantity
var potentialSumWithNonCertified token2.Quantity
target, err := token2.ToQuantity(q, s.precision)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to convert quantity")
Expand Down Expand Up @@ -105,7 +98,6 @@ func (s *selector) selectByID(ownerFilter token.OwnerFilter, q string, tokenType
// First select only certified
sum = token2.NewZeroQuantity(s.precision)
potentialSumWithLocked = token2.NewZeroQuantity(s.precision)
potentialSumWithNonCertified = token2.NewZeroQuantity(s.precision)
toBeSpent = nil
var toBeCertified []*token2.ID

Expand Down Expand Up @@ -143,155 +135,6 @@ func (s *selector) selectByID(ownerFilter token.OwnerFilter, q string, tokenType
toBeSpent = append(toBeSpent, t.Id)
sum = sum.Add(q)
potentialSumWithLocked = potentialSumWithLocked.Add(q)
potentialSumWithNonCertified = potentialSumWithNonCertified.Add(q)

if target.Cmp(sum) <= 0 {
break
}
}

concurrencyIssue := false
if target.Cmp(sum) <= 0 {
err := s.concurrencyCheck(toBeSpent)
if err == nil {
return toBeSpent, sum, nil
}
concurrencyIssue = true
logger.Errorf("concurrency issue, some of the tokens might not exist anymore [%s]", err)
}

// Unlock and check the conditions for a retry
s.locker.UnlockIDs(toBeSpent...)
s.locker.UnlockIDs(toBeCertified...)

if target.Cmp(potentialSumWithLocked) <= 0 && potentialSumWithLocked.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: sufficient funds but partially locked")
}
if target.Cmp(potentialSumWithNonCertified) <= 0 && potentialSumWithNonCertified.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: sufficient funds but partially not certified")
}

i++
if i >= s.numRetry {
// it is time to fail but how?
if concurrencyIssue {
logger.Debugf("concurrency issue, some of the tokens might not exist anymore")
return nil, nil, errors.WithMessagef(
token.SelectorSufficientFundsButConcurrencyIssue,
"token selection failed: sufficient funs but concurrency issue, potential [%s] tokens of type [%s] were available", potentialSumWithLocked, tokenType,
)
}

if target.Cmp(potentialSumWithLocked) <= 0 && potentialSumWithLocked.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: it is time to fail but how, sufficient funds but locked")
return nil, nil, errors.WithMessagef(
token.SelectorSufficientButLockedFunds,
"token selection failed: sufficient but partially locked funds, potential [%s] tokens of type [%s] are available", potentialSumWithLocked.Decimal(), tokenType,
)
}

if target.Cmp(potentialSumWithNonCertified) <= 0 && potentialSumWithNonCertified.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: it is time to fail but how, sufficient funds but locked")
return nil, nil, errors.WithMessagef(
token.SelectorSufficientButNotCertifiedFunds,
"token selection failed: sufficient but partially not certified, potential [%s] tokens of type [%s] are available", potentialSumWithNonCertified, tokenType,
)
}

// funds are insufficient
logger.Debugf("token selection: it is time to fail but how, insufficient funds")
return nil, nil, errors.WithMessagef(
token.SelectorInsufficientFunds,
"token selection failed: insufficient funds, only [%s] tokens of type [%s] are available", sum.Decimal(), tokenType,
)
}

logger.Debugf("token selection: let's wait [%v] before retry...", s.timeout)
time.Sleep(s.timeout)
}
}

func (s *selector) selectByOwner(ownerFilter token.OwnerFilter, q string, tokenType string) ([]*token2.ID, token2.Quantity, error) {
var toBeSpent []*token2.ID
var sum token2.Quantity
var potentialSumWithLocked token2.Quantity
var potentialSumWithNonCertified token2.Quantity
target, err := token2.ToQuantity(q, s.precision)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to convert quantity")
}

i := 0
for {
logger.Debugf("start token selection, iteration [%d/%d]", i, s.numRetry)
unspentTokens, err := s.queryService.UnspentTokensIterator()
if err != nil {
return nil, nil, errors.Wrap(err, "token selection failed")
}
defer unspentTokens.Close()
logger.Debugf("select token for a quantity of [%s] of type [%s]", q, tokenType)

// First select only certified
sum = token2.NewZeroQuantity(s.precision)
potentialSumWithLocked = token2.NewZeroQuantity(s.precision)
potentialSumWithNonCertified = token2.NewZeroQuantity(s.precision)
toBeSpent = nil
var toBeCertified []*token2.ID

reclaim := s.numRetry == 1 || i > 0
for {
t, err := unspentTokens.Next()
if err != nil {
return nil, nil, errors.Wrap(err, "token selection failed")
}
if t == nil {
break
}

q, err := token2.ToQuantity(t.Quantity, s.precision)
if err != nil {
s.locker.UnlockIDs(toBeSpent...)
s.locker.UnlockIDs(toBeCertified...)
return nil, nil, errors.Wrap(err, "failed to convert quantity")
}

// check type and ownership
if t.Type != tokenType {
if logger.IsEnabledFor(zapcore.DebugLevel) {
logger.Debugf("token [%s,%s] type does not match", q, tokenType)
}
continue
}

rightOwner := ownerFilter.ContainsToken(t)

if !rightOwner {
if logger.IsEnabledFor(zapcore.DebugLevel) {
logger.Debugf("token [%s,%s,%s,%v] owner does not belong to the passed wallet", view.Identity(t.Owner.Raw), q, tokenType, rightOwner)
}
continue
}

// lock the token
if _, err := s.locker.Lock(t.Id, s.txID, reclaim); err != nil {
potentialSumWithLocked = potentialSumWithLocked.Add(q)

if logger.IsEnabledFor(zapcore.DebugLevel) {
logger.Debugf("token [%s,%s,%v] cannot be locked [%s]", q, tokenType, rightOwner, err)
}
continue
}

// Append token
logger.Debugf("adding quantity [%s]", q.Decimal())
toBeSpent = append(toBeSpent, t.Id)
sum = sum.Add(q)
potentialSumWithLocked = potentialSumWithLocked.Add(q)
potentialSumWithNonCertified = potentialSumWithNonCertified.Add(q)

if target.Cmp(sum) <= 0 {
break
Expand All @@ -316,10 +159,6 @@ func (s *selector) selectByOwner(ownerFilter token.OwnerFilter, q string, tokenT
// funds are potentially enough but they are locked
logger.Debugf("token selection: sufficient funds but partially locked")
}
if target.Cmp(potentialSumWithNonCertified) <= 0 && potentialSumWithNonCertified.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: sufficient funds but partially not certified")
}

i++
if i >= s.numRetry {
Expand All @@ -341,15 +180,6 @@ func (s *selector) selectByOwner(ownerFilter token.OwnerFilter, q string, tokenT
)
}

if target.Cmp(potentialSumWithNonCertified) <= 0 && potentialSumWithNonCertified.Cmp(sum) != 0 {
// funds are potentially enough but they are locked
logger.Debugf("token selection: it is time to fail but how, sufficient funds but locked")
return nil, nil, errors.WithMessagef(
token.SelectorSufficientButNotCertifiedFunds,
"token selection failed: sufficient but partially not certified, potential [%s] tokens of type [%s] are available", potentialSumWithNonCertified, tokenType,
)
}

// funds are insufficient
logger.Debugf("token selection: it is time to fail but how, insufficient funds")
return nil, nil, errors.WithMessagef(
Expand All @@ -362,13 +192,3 @@ func (s *selector) selectByOwner(ownerFilter token.OwnerFilter, q string, tokenT
time.Sleep(s.timeout)
}
}

type allOwners struct{}

func (a *allOwners) ID() string {
return ""
}

func (a *allOwners) ContainsToken(token *token2.UnspentToken) bool {
return true
}
Loading