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

double spending bugfix #482

Merged
merged 3 commits into from
Jul 11, 2023
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
3 changes: 1 addition & 2 deletions integration/token/fungible/dlog/dlog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ package dlog
import (
"os"

"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash"

"github.com/hyperledger-labs/fabric-smart-client/integration"
"github.com/hyperledger-labs/fabric-smart-client/platform/view/services/hash"
"github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token"
"github.com/hyperledger-labs/fabric-token-sdk/integration/nwo/token/topology"
"github.com/hyperledger-labs/fabric-token-sdk/integration/token/fungible"
Expand Down
23 changes: 17 additions & 6 deletions integration/token/fungible/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ func TransferCash(network *integration.Infrastructure, id string, wallet string,
return ""
}

func TransferCashMultiActions(network *integration.Infrastructure, id string, wallet string, typ string, amounts []uint64, receivers []string, auditor string, expectedErrorMsgs ...string) string {
func TransferCashMultiActions(network *integration.Infrastructure, id string, wallet string, typ string, amounts []uint64, receivers []string, auditor string, tokenID *token.ID, expectedErrorMsgs ...string) string {
Expect(len(amounts) > 1).To(BeTrue())
Expect(len(receivers)).To(BeEquivalentTo(len(amounts)))
transfer := &views.Transfer{
Expand All @@ -257,6 +257,7 @@ func TransferCashMultiActions(network *integration.Infrastructure, id string, wa
Amount: amounts[0],
Recipient: network.Identity(receivers[0]),
RecipientEID: receivers[0],
TokenIDs: []*token.ID{tokenID},
}
for i := 1; i < len(amounts); i++ {
transfer.TransferAction = append(transfer.TransferAction, views.TransferAction{
Expand Down Expand Up @@ -295,7 +296,11 @@ func TransferCashMultiActions(network *integration.Infrastructure, id string, wa
Expect(err.Error()).To(ContainSubstring(msg), "err [%s] should contain [%s]", err.Error(), msg)
}
time.Sleep(5 * time.Second)
return ""
// extract txID from err
strErr := err.Error()
s := strings.LastIndex(strErr, "[<<<")
e := strings.LastIndex(strErr, ">>>]")
return strErr[s+4 : e]
}

func PrepareTransferCash(network *integration.Infrastructure, id string, wallet string, typ string, amount uint64, receiver string, auditor string, tokenID *token.ID, expectedErrorMsgs ...string) (string, []byte) {
Expand Down Expand Up @@ -528,15 +533,21 @@ func CheckOwnerDB(network *integration.Infrastructure, expectedErrors []string,
}
}

func CheckAuditorDB(network *integration.Infrastructure, auditorID string, walletID string) {
func CheckAuditorDB(network *integration.Infrastructure, auditorID string, walletID string, errorCheck func([]string) error) {
errorMessagesBoxed, err := network.Client(auditorID).CallView("CheckTTXDB", common.JSONMarshall(&views.CheckTTXDB{
Auditor: true,
AuditorWalletID: walletID,
}))
Expect(err).NotTo(HaveOccurred())
var errorMessages []string
common.JSONUnmarshal(errorMessagesBoxed.([]byte), &errorMessages)
Expect(len(errorMessages)).To(Equal(0), "expected 0 error messages, got [% v]", errorMessages)
if errorCheck != nil {
var errorMessages []string
common.JSONUnmarshal(errorMessagesBoxed.([]byte), &errorMessages)
Expect(errorCheck(errorMessages)).NotTo(HaveOccurred(), "failed to check errors")
} else {
var errorMessages []string
common.JSONUnmarshal(errorMessagesBoxed.([]byte), &errorMessages)
Expect(len(errorMessages)).To(Equal(0), "expected 0 error messages, got [% v]", errorMessages)
}
}

func PruneInvalidUnspentTokens(network *integration.Infrastructure, ids ...string) {
Expand Down
58 changes: 46 additions & 12 deletions integration/token/fungible/tests.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package fungible

import (
"crypto/rand"
"fmt"
"math/big"
"strings"
"time"
Expand All @@ -22,6 +23,7 @@ import (
"github.com/hyperledger-labs/fabric-token-sdk/token/services/ttxdb"
token2 "github.com/hyperledger-labs/fabric-token-sdk/token/token"
. "github.com/onsi/gomega"
"github.com/pkg/errors"
)

var AuditedTransactions = []*ttxdb.TransactionRecord{
Expand Down Expand Up @@ -386,7 +388,8 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
IssueCash(network, "", "LIRA", 3, "alice", auditor, true, "issuer")
t14 := time.Now()
CheckAuditedTransactions(network, auditor, AuditedTransactions[10:12], &t13, &t14)
txLiraTransfer := TransferCashMultiActions(network, "alice", "", "LIRA", []uint64{2, 3}, []string{"bob", "charlie"}, auditor)
// perform the normal transaction
txLiraTransfer := TransferCashMultiActions(network, "alice", "", "LIRA", []uint64{2, 3}, []string{"bob", "charlie"}, auditor, nil)
t16 := time.Now()
AuditedTransactions[12].TxID = txLiraTransfer
AuditedTransactions[13].TxID = txLiraTransfer
Expand Down Expand Up @@ -447,7 +450,7 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
CheckBalanceAndHolding(network, "issuer", "issuer.owner", "EUR", 10, auditor)

CheckOwnerDB(network, nil, "issuer", "alice", "bob", "charlie", "manager")
CheckAuditorDB(network, auditor, "")
CheckAuditorDB(network, auditor, "", nil)

// Check double spending
txIDPine := IssueCash(network, "", "PINE", 55, "alice", auditor, true, "issuer")
Expand Down Expand Up @@ -488,7 +491,7 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
CheckBalanceAndHolding(network, "alice", "", "PINE", 0, auditor)
CheckBalanceAndHolding(network, "bob", "", "PINE", 55, auditor)
CheckOwnerDB(network, nil, "issuer", "alice", "bob", "charlie", "manager")
CheckAuditorDB(network, auditor, "")
CheckAuditorDB(network, auditor, "", nil)

// Test Auditor ability to override transaction state
txID3, tx3 := PrepareTransferCash(network, "bob", "", "PINE", 10, "alice", auditor, nil)
Expand All @@ -506,11 +509,11 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
// Restart
CheckOwnerDB(network, nil, "bob", "alice")
CheckOwnerDB(network, nil, "issuer", "charlie", "manager")
CheckAuditorDB(network, auditor, "")
CheckAuditorDB(network, auditor, "", nil)
Restart(network, false, "alice", "bob", "charlie", "manager")
CheckOwnerDB(network, nil, "bob", "alice")
CheckOwnerDB(network, nil, "issuer", "charlie", "manager")
CheckAuditorDB(network, auditor, "")
CheckAuditorDB(network, auditor, "", nil)

// Addition transfers
TransferCash(network, "issuer", "", "USD", 50, "issuer", auditor)
Expand Down Expand Up @@ -678,16 +681,16 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
}()
}
// collect the errors, and check that they are all nil, and one of them is the error we expect.
var errors []error
var errs []error
for _, transfer := range transferErrors {
errors = append(errors, <-transfer)
errs = append(errs, <-transfer)
}
Expect((errors[0] == nil && errors[1] != nil) || (errors[0] != nil && errors[1] == nil)).To(BeTrue())
Expect((errs[0] == nil && errs[1] != nil) || (errs[0] != nil && errs[1] == nil)).To(BeTrue())
var errStr string
if errors[0] == nil {
errStr = errors[1].Error()
if errs[0] == nil {
errStr = errs[1].Error()
} else {
errStr = errors[0].Error()
errStr = errs[0].Error()
}
v := strings.Contains(errStr, "pineapple") || strings.Contains(errStr, "lemonade")
Expect(v).To(BeEquivalentTo(true))
Expand Down Expand Up @@ -719,14 +722,45 @@ func TestAll(network *integration.Infrastructure, auditor string, onAuditorResta
// Check consistency
CheckPublicParams(network, "issuer", auditor, "alice", "bob", "charlie", "manager")
CheckOwnerDB(network, nil, "bob", "alice", "issuer", "charlie", "manager")
CheckAuditorDB(network, auditor, "")
CheckAuditorDB(network, auditor, "", nil)
PruneInvalidUnspentTokens(network, "issuer", auditor, "alice", "bob", "charlie", "manager")

for _, name := range []string{"alice", "bob", "charlie", "manager"} {
IDs := ListVaultUnspentTokens(network, name)
CheckIfExistsInVault(network, auditor, IDs)
}

// Check double spending by multiple action in the same transaction

// use the same token for both actions, this must fail
txIssuedPineapples1 := IssueCash(network, "", "Pineapples", 3, "alice", auditor, true, "issuer")
IssueCash(network, "", "Pineapples", 3, "alice", auditor, true, "issuer")
failedTransferTxID := TransferCashMultiActions(network, "alice", "", "Pineapples", []uint64{2, 3}, []string{"bob", "charlie"}, auditor, &token2.ID{TxId: txIssuedPineapples1}, "failed to append spent id", txIssuedPineapples1)
// the above transfer must fail at execution phase, therefore the auditor should be explicitly informed about this transaction
CheckBalance(network, "alice", "", "Pineapples", 6)
CheckHolding(network, "alice", "", "Pineapples", 1, auditor)
CheckBalance(network, "bob", "", "Pineapples", 0)
CheckHolding(network, "bob", "", "Pineapples", 2, auditor)
CheckBalance(network, "charlie", "", "Pineapples", 0)
CheckHolding(network, "charlie", "", "Pineapples", 3, auditor)
fmt.Printf("failed transaction [%s]\n", failedTransferTxID)
SetTransactionAuditStatus(network, auditor, failedTransferTxID, ttx.Deleted)
CheckBalanceAndHolding(network, "alice", "", "Pineapples", 6, auditor)
CheckBalanceAndHolding(network, "bob", "", "Pineapples", 0, auditor)
CheckBalanceAndHolding(network, "charlie", "", "Pineapples", 0, auditor)
CheckAuditorDB(network, auditor, "", func(errs []string) error {
// We should expect 6 errors, 3 records (Alice->Bob, Alice->Charlie, Alice-Alice (the rest) * 2 (envelope non found, no match in vault)
// each error should contain failedTransferTxID
if len(errs) != 6 {
return errors.Errorf("expected only 1 error, got [%d]", len(errs))
}
for _, err := range errs {
if !strings.Contains(err, failedTransferTxID) {
return errors.Errorf("expected error to contain [%s], got [%s]", failedTransferTxID, err)
}
}
return nil
})
}

func TestPublicParamsUpdate(network *integration.Infrastructure, auditor string, ppBytes []byte, tms *topology.TMS, issuerAsAuditor bool) {
Expand Down
5 changes: 3 additions & 2 deletions integration/token/fungible/views/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ type TransferView struct {
*Transfer
}

func (t *TransferView) Call(context view.Context) (interface{}, error) {
func (t *TransferView) Call(context view.Context) (txID interface{}, err error) {
// As a first step operation, the sender contacts the recipient's FSC node
// to ask for the identity to use to assign ownership of the freshly created token.
// Notice that, this step would not be required if the sender knew already which
Expand Down Expand Up @@ -122,6 +122,7 @@ func (t *TransferView) Call(context view.Context) (interface{}, error) {
t.Type,
[]uint64{action.Amount},
[]view.Identity{additionalRecipients[i]},
token2.WithTokenIDs(t.TokenIDs...),
)
assert.NoError(err, "failed adding transfer action [%d:%d]", action.Amount, action.Recipient)
}
Expand All @@ -140,7 +141,7 @@ func (t *TransferView) Call(context view.Context) (interface{}, error) {
// Depending on the token driver implementation, the recipient's signature might or might not be needed to make
// the token transaction valid.
_, err = context.RunView(ttx.NewCollectEndorsementsView(tx))
assert.NoError(err, "failed to sign transaction")
assert.NoError(err, "failed to sign transaction [<<<%s>>>]", tx.ID())

// Sanity checks:
// - the transaction is in busy state in the vault
Expand Down
13 changes: 13 additions & 0 deletions token/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -1093,6 +1093,9 @@ func (r *Request) prepareTransfer(redeem bool, wallet *OwnerWallet, tokenType st
var tokenIDs []*token.ID
var inputSum token.Quantity
var err error

transferOpts.TokenIDs = r.cleanupInputIDs(transferOpts.TokenIDs)

// if inputs have been passed, parse and certify them, if needed
if len(transferOpts.TokenIDs) != 0 {
tokenIDs, inputSum, tokenType, err = r.parseInputIDs(transferOpts.TokenIDs)
Expand Down Expand Up @@ -1193,6 +1196,16 @@ func (r *Request) genOutputs(values []uint64, owners []view.Identity, tokenType
return outputTokens, outputSum, nil
}

func (r *Request) cleanupInputIDs(ds []*token.ID) []*token.ID {
newSlice := make([]*token.ID, 0, len(ds))
for _, item := range ds {
if item != nil {
newSlice = append(newSlice, item)
}
}
return newSlice
}

type requestSer struct {
TxID string
Actions []byte
Expand Down
21 changes: 18 additions & 3 deletions token/services/vault/translator/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ type Translator struct {
RWSet RWSet
TxID string
// SpentIDs the spent IDs added so far
SpentIDs [][]byte
SpentIDs []string

counter uint64
namespace string
Expand Down Expand Up @@ -385,7 +385,9 @@ func (w *Translator) spendTokens(ids []string, graphHiding bool) error {
if err != nil {
return errors.Wrapf(err, "failed to delete output %s", id)
}
w.SpentIDs = append(w.SpentIDs, []byte(id))
if err := w.appendSpentID(id); err != nil {
return errors.Wrapf(err, "failed to append spent id [%s]", id)
}
}
} else {
for _, id := range ids {
Expand All @@ -394,7 +396,9 @@ func (w *Translator) spendTokens(ids []string, graphHiding bool) error {
if err != nil {
return errors.Wrapf(err, "failed to add serial number %s", id)
}
w.SpentIDs = append(w.SpentIDs, []byte(id))
if err := w.appendSpentID(id); err != nil {
return errors.Wrapf(err, "failed to append spent id [%s]", id)
}
}
}

Expand Down Expand Up @@ -425,3 +429,14 @@ func (w *Translator) areTokensSpent(ids []string, graphHiding bool) ([]bool, err

return res, nil
}

func (w *Translator) appendSpentID(id string) error {
// check first it is already in the list
for _, d := range w.SpentIDs {
if d == id {
return errors.Errorf("[%s] already spent", id)
}
}
w.SpentIDs = append(w.SpentIDs, id)
return nil
}