Skip to content

Commit 0ab3c5c

Browse files
Effi-SAkramBitar
authored andcommitted
Added Len Check
Signed-off-by: Effi-S <effi.szt@gmail.com>
1 parent cc52d9e commit 0ab3c5c

3 files changed

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

0 commit comments

Comments
 (0)