Skip to content

Commit 24e7894

Browse files
committed
Added Len Check
Signed-off-by: Effi-S <effi.szt@gmail.com>
1 parent c5fd305 commit 24e7894

3 files changed

Lines changed: 121 additions & 6 deletions

File tree

token/services/network/fabric/tokenfetcher.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ func (f *spentTokenFetcher) QuerySpentTokens(ctx context.Context, namespace stri
127127
if err := json.Unmarshal(payloadBoxed, &spent); err != nil {
128128
return nil, errors.Wrapf(err, "failed to unmarshal esponse")
129129
}
130+
if len(spent) != len(IDs) {
131+
return nil, errors.Errorf("unexpected number of spent flags: got [%d], expected [%d]", len(spent), len(IDs))
132+
}
130133

131134
return spent, nil
132135
}

token/services/network/fabricx/qe/qe.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,16 @@ func (e *Executor) QuerySpentTokens(_ context.Context, namespace driver.Namespac
158158
}
159159

160160
// This operation depends on the driver.
161-
// Let's assume for now that the driver is non-graph hiding
161+
// Let's assume for now that the driver is non-graph hiding.
162+
//
163+
// The returned slice is positionally aligned with ids: spentFlags[i]
164+
// corresponds to ids[i], so its length always equals len(ids). Callers
165+
// (e.g. tokens.Service.deleteTokens, htlc.OwnerWallet.deleteTokens) index
166+
// the result by token position, so a shorter slice would panic. A nil id
167+
// carries no ledger key and is reported as not spent.
162168
keys := make([]driver.PKey, 0, len(ids))
163-
for _, id := range ids {
169+
keyIndex := make([]int, 0, len(ids))
170+
for i, id := range ids {
164171
if id == nil {
165172
continue
166173
}
@@ -169,9 +176,12 @@ func (e *Executor) QuerySpentTokens(_ context.Context, namespace driver.Namespac
169176
return nil, errors.Wrapf(err, "error creating output id key [%s:%d]", id.TxId, id.Index)
170177
}
171178
keys = append(keys, outputID)
179+
keyIndex = append(keyIndex, i)
172180
}
181+
182+
spentFlags := make([]bool, len(ids))
173183
if len(keys) == 0 {
174-
return nil, nil
184+
return spentFlags, nil
175185
}
176186

177187
qs, err := e.qsProvider.Get(e.network, e.channel)
@@ -187,10 +197,9 @@ func (e *Executor) QuerySpentTokens(_ context.Context, namespace driver.Namespac
187197

188198
// map[driver.Namespace]map[driver.PKey]driver.VaultValue
189199
ns := res[namespace]
190-
spentFlags := make([]bool, len(keys))
191-
for i, key := range keys {
200+
for j, key := range keys {
192201
value := ns[key]
193-
spentFlags[i] = len(value.Raw) == 0
202+
spentFlags[keyIndex[j]] = len(value.Raw) == 0
194203
}
195204

196205
return spentFlags, nil

token/services/network/fabricx/qe/qe_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,3 +341,106 @@ func TestQuerySpentTokens_EmptyIDs(t *testing.T) {
341341
require.Nil(t, res)
342342
assert.Equal(t, 0, qsp.GetCallCount())
343343
}
344+
345+
// TestQuerySpentTokens_NilIDPreservesAlignment is a regression guard: a nil id
346+
// must not shift the remaining flags or shorten the result. The returned slice
347+
// stays positionally aligned with ids (length == len(ids)); the nil position is
348+
// reported as not spent, and only the non-nil ids are looked up on the ledger.
349+
func TestQuerySpentTokens_NilIDPreservesAlignment(t *testing.T) {
350+
ids := []*token.ID{nil, {TxId: "tx1", Index: 0}, {TxId: "tx2", Index: 1}}
351+
k1 := outputKey(t, "tx1", 0)
352+
k2 := outputKey(t, "tx2", 1)
353+
354+
qsp := &mock.QueryServiceProvider{}
355+
qs := &mock.QueryService{}
356+
qsp.GetReturns(qs, nil)
357+
// tx1 is present (unspent) -> false; tx2 is missing (spent) -> true.
358+
qs.GetStatesReturns(map[driver.Namespace]map[driver.PKey]driver.VaultValue{
359+
testNamespace: {k1: {Raw: []byte("token1")}},
360+
}, nil)
361+
362+
e := newExecutor(qsp)
363+
res, err := e.QuerySpentTokens(t.Context(), testNamespace, ids, nil)
364+
require.NoError(t, err)
365+
// index 0 (nil) -> false, index 1 (tx1 present) -> false, index 2 (tx2 missing) -> true.
366+
require.Equal(t, []bool{false, false, true}, res)
367+
368+
// Only the non-nil ids are queried on the ledger; the nil id is not.
369+
queried := qs.GetStatesArgsForCall(0)
370+
require.Equal(t, map[driver.Namespace][]driver.PKey{testNamespace: {k1, k2}}, queried)
371+
}
372+
373+
// TestQuerySpentTokens_AllNilIDs is a regression guard: before the fix, an
374+
// all-nil ids slice returned (nil, nil) — length 0 — which made the callers'
375+
// spent[i] loop panic with index-out-of-range. It must now return a slice of
376+
// len(ids) with every position reported as not spent, without touching the
377+
// query service.
378+
func TestQuerySpentTokens_AllNilIDs(t *testing.T) {
379+
ids := []*token.ID{nil, nil}
380+
381+
qsp := &mock.QueryServiceProvider{}
382+
383+
e := newExecutor(qsp)
384+
res, err := e.QuerySpentTokens(t.Context(), testNamespace, ids, nil)
385+
require.NoError(t, err)
386+
require.Equal(t, []bool{false, false}, res)
387+
assert.Equal(t, 0, qsp.GetCallCount())
388+
}
389+
390+
// TestQuerySpentTokens_InteriorNilPreservesAlignment guards the index mapping
391+
// for a nil that sits between two non-nil ids (not just a leading nil): the
392+
// flag for each non-nil id must land at that id's original position, and the
393+
// nil slot stays not spent.
394+
func TestQuerySpentTokens_InteriorNilPreservesAlignment(t *testing.T) {
395+
ids := []*token.ID{{TxId: "tx1", Index: 0}, nil, {TxId: "tx2", Index: 1}}
396+
k1 := outputKey(t, "tx1", 0)
397+
k2 := outputKey(t, "tx2", 1)
398+
399+
qsp := &mock.QueryServiceProvider{}
400+
qs := &mock.QueryService{}
401+
qsp.GetReturns(qs, nil)
402+
// tx1 missing (spent) -> true; tx2 present (unspent) -> false.
403+
qs.GetStatesReturns(map[driver.Namespace]map[driver.PKey]driver.VaultValue{
404+
testNamespace: {k2: {Raw: []byte("token2")}},
405+
}, nil)
406+
407+
e := newExecutor(qsp)
408+
res, err := e.QuerySpentTokens(t.Context(), testNamespace, ids, nil)
409+
require.NoError(t, err)
410+
// index 0 (tx1 missing) -> true, index 1 (nil) -> false, index 2 (tx2 present) -> false.
411+
require.Equal(t, []bool{true, false, false}, res)
412+
413+
// The nil id is not queried; only tx1 and tx2 are.
414+
queried := qs.GetStatesArgsForCall(0)
415+
require.Equal(t, map[driver.Namespace][]driver.PKey{testNamespace: {k1, k2}}, queried)
416+
}
417+
418+
func TestQuerySpentTokens_ProviderError(t *testing.T) {
419+
ids := []*token.ID{{TxId: "tx1", Index: 0}}
420+
421+
qsp := &mock.QueryServiceProvider{}
422+
qsp.GetReturns(nil, errors.New("boom"))
423+
424+
e := newExecutor(qsp)
425+
res, err := e.QuerySpentTokens(t.Context(), testNamespace, ids, nil)
426+
require.Error(t, err)
427+
assert.Nil(t, res)
428+
assert.Contains(t, err.Error(), "failed getting qs")
429+
assert.Contains(t, err.Error(), "boom")
430+
}
431+
432+
func TestQuerySpentTokens_GetStatesError(t *testing.T) {
433+
ids := []*token.ID{{TxId: "tx1", Index: 0}}
434+
435+
qsp := &mock.QueryServiceProvider{}
436+
qs := &mock.QueryService{}
437+
qsp.GetReturns(qs, nil)
438+
qs.GetStatesReturns(nil, errors.New("rpc failed"))
439+
440+
e := newExecutor(qsp)
441+
res, err := e.QuerySpentTokens(t.Context(), testNamespace, ids, nil)
442+
require.Error(t, err)
443+
assert.Nil(t, res)
444+
assert.Contains(t, err.Error(), "failed getting states")
445+
assert.Contains(t, err.Error(), "rpc failed")
446+
}

0 commit comments

Comments
 (0)