Skip to content

Commit fc2948f

Browse files
committed
fix(token): deduplicate composite-owner inputs whose token IDs are filtered out
UniquePerInput deduplicates by (token ID, enrollment ID) and keeps every input with a nil token ID. Metadata.filterTransfer strips TokenID from the input metadata it clones, so consumers of eID-filtered metadata get nil token IDs and composite-owner member rows are summed once per member again. Give Input an Index, its position within the action, and fall back to (action index, input index, enrollment ID) as the deduplication key when the token ID is absent. Positions survive filtering because filterTransfer emits one entry per input. Inputs carrying a token ID keep the previous key, so existing callers are unaffected. Signed-off-by: Evan <evanyan@sign.global>
1 parent 253b284 commit fc2948f

4 files changed

Lines changed: 66 additions & 18 deletions

File tree

docs/services/storage/ttxdb.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,6 @@ pair once (`OutputStream.UniquePerOutput`), so members sharing an enrollment
100100
ID do not multiply the recorded amount. A spent input is expanded the same
101101
way, one row per member carrying the token's full quantity; sent-amount
102102
aggregation counts each `(token ID, enrollment ID)` pair once
103-
(`InputStream.UniquePerInput`).
103+
(`InputStream.UniquePerInput`), falling back to
104+
`(action index, input index, enrollment ID)` when the token ID has been
105+
filtered out of the metadata.

token/request.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,9 +907,10 @@ func (r *Request) inputs(ctx context.Context, failOnMissing bool) (*InputStream,
907907

908908
func (r *Request) extractIssueInputs(actionIndex int, metadata *IssueMetadata) ([]*Input, error) {
909909
var inputs []*Input
910-
for _, input := range metadata.Inputs {
910+
for j, input := range metadata.Inputs {
911911
inputs = append(inputs, &Input{
912912
ActionIndex: actionIndex,
913+
Index: j,
913914
Id: input.TokenID,
914915
})
915916
}
@@ -940,6 +941,7 @@ func (r *Request) extractTransferInputs(ctx context.Context, actionIndex int, me
940941

941942
inputs = append(inputs, &Input{
942943
ActionIndex: actionIndex,
944+
Index: j,
943945
Id: metadata.TokenIDAt(j),
944946
Owner: sender.Identity,
945947
OwnerAuditInfo: sender.AuditInfo,

token/stream.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,9 @@ func (o *OutputStream) String() string {
232232

233233
// Input models an input of a token action
234234
type Input struct {
235-
ActionIndex int
235+
ActionIndex int
236+
// Index is the position of this input within its action
237+
Index int
236238
Id *token.ID
237239
Owner Identity
238240
OwnerAuditInfo []byte
@@ -391,26 +393,40 @@ func (is *InputStream) ByType(tokenType token.Type) *InputStream {
391393
})
392394
}
393395

394-
// UniquePerInput returns a stream keeping, for each (token ID, EnrollmentID)
395-
// pair, only the first input, so amount aggregation counts a composite
396-
// owner's members once. Inputs with no token ID are all kept. Identity
397-
// consumers use the full stream instead.
396+
// UniquePerInput returns a stream keeping, for each spent token and
397+
// EnrollmentID, only the first input, so amount aggregation counts a
398+
// composite owner's members once. The token is identified by its token ID,
399+
// or by (ActionIndex, Index) when the token ID has been filtered out of the
400+
// metadata; the two key spaces never mix, since only transfer inputs lose
401+
// their token ID. Identity consumers use the full stream instead.
398402
func (is *InputStream) UniquePerInput() *InputStream {
399-
type key struct {
403+
type idKey struct {
400404
id token.ID
401405
eID string
402406
}
403-
seen := map[key]bool{}
407+
type posKey struct {
408+
actionIndex int
409+
index int
410+
eID string
411+
}
412+
seenByID := map[idKey]bool{}
413+
seenByPos := map[posKey]bool{}
404414

405415
return is.Filter(func(t *Input) bool {
406-
if t.Id == nil {
416+
if t.Id != nil {
417+
k := idKey{id: *t.Id, eID: t.EnrollmentID}
418+
if seenByID[k] {
419+
return false
420+
}
421+
seenByID[k] = true
422+
407423
return true
408424
}
409-
k := key{id: *t.Id, eID: t.EnrollmentID}
410-
if seen[k] {
425+
k := posKey{actionIndex: t.ActionIndex, index: t.Index, eID: t.EnrollmentID}
426+
if seenByPos[k] {
411427
return false
412428
}
413-
seen[k] = true
429+
seenByPos[k] = true
414430

415431
return true
416432
})

token/stream_test.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,6 @@ func TestOutputStream_UniquePerOutput(t *testing.T) {
286286
}
287287

288288
func TestInputStream_UniquePerInput(t *testing.T) {
289-
// distinct pointers to the same token ID value must collapse
290289
cases := []struct {
291290
name string
292291
inputs []*Input
@@ -312,7 +311,7 @@ func TestInputStream_UniquePerInput(t *testing.T) {
312311
},
313312
},
314313
{
315-
"different token IDs with the same enrollment ID both survive",
314+
"different token IDs with the same enrollment ID both survive despite equal positions",
316315
[]*Input{
317316
{Id: &token.ID{TxId: "tx0", Index: 0}, EnrollmentID: "enroll1"},
318317
{Id: &token.ID{TxId: "tx0", Index: 1}, EnrollmentID: "enroll1"},
@@ -323,9 +322,38 @@ func TestInputStream_UniquePerInput(t *testing.T) {
323322
},
324323
},
325324
{
326-
"inputs with no token ID are all kept",
327-
[]*Input{{EnrollmentID: "enroll1"}, {EnrollmentID: "enroll1"}},
328-
[]*Input{{EnrollmentID: "enroll1"}, {EnrollmentID: "enroll1"}},
325+
"member rows with a filtered-out token ID collapse on action and input index",
326+
[]*Input{
327+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "first"},
328+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "second"},
329+
},
330+
[]*Input{{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "first"}},
331+
},
332+
{
333+
"filtered rows with different positions or enrollment IDs all survive",
334+
[]*Input{
335+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
336+
{ActionIndex: 0, Index: 1, EnrollmentID: "enroll1"},
337+
{ActionIndex: 1, Index: 0, EnrollmentID: "enroll1"},
338+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll2"},
339+
},
340+
[]*Input{
341+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
342+
{ActionIndex: 0, Index: 1, EnrollmentID: "enroll1"},
343+
{ActionIndex: 1, Index: 0, EnrollmentID: "enroll1"},
344+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll2"},
345+
},
346+
},
347+
{
348+
"a row with a token ID never collides with a positionally identical row without one",
349+
[]*Input{
350+
{ActionIndex: 0, Index: 0, Id: &token.ID{TxId: "tx0", Index: 0}, EnrollmentID: "enroll1"},
351+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
352+
},
353+
[]*Input{
354+
{ActionIndex: 0, Index: 0, Id: &token.ID{TxId: "tx0", Index: 0}, EnrollmentID: "enroll1"},
355+
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
356+
},
329357
},
330358
}
331359
for _, tc := range cases {

0 commit comments

Comments
 (0)