Skip to content

Commit ba27c1c

Browse files
committed
fix(token): restrict positional input dedup to canonically built inputs
Index defaults to 0, so two literal-built inputs with no token ID would silently collapse on a position nobody set. NewInput becomes the canonical way to build an input and stamps an unexported position-known marker; UniquePerInput collapses on position only for marked inputs, while literal-built inputs keep the previous keep-all behavior, locked by a regression test. Signed-off-by: Evan <evanyan@sign.global>
1 parent 27941e9 commit ba27c1c

3 files changed

Lines changed: 65 additions & 31 deletions

File tree

token/request.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -908,11 +908,7 @@ func (r *Request) inputs(ctx context.Context, failOnMissing bool) (*InputStream,
908908
func (r *Request) extractIssueInputs(actionIndex int, metadata *IssueMetadata) ([]*Input, error) {
909909
var inputs []*Input
910910
for j, input := range metadata.Inputs {
911-
inputs = append(inputs, &Input{
912-
ActionIndex: actionIndex,
913-
Index: j,
914-
Id: input.TokenID,
915-
})
911+
inputs = append(inputs, NewInput(actionIndex, j, input.TokenID))
916912
}
917913

918914
return inputs, nil
@@ -939,15 +935,12 @@ func (r *Request) extractTransferInputs(ctx context.Context, actionIndex int, me
939935
return nil, errors.Wrapf(err, "failed getting enrollment id and revocation handle [%d,%d]", actionIndex, j)
940936
}
941937

942-
inputs = append(inputs, &Input{
943-
ActionIndex: actionIndex,
944-
Index: j,
945-
Id: metadata.TokenIDAt(j),
946-
Owner: sender.Identity,
947-
OwnerAuditInfo: sender.AuditInfo,
948-
EnrollmentID: eID,
949-
RevocationHandler: rID,
950-
})
938+
in := NewInput(actionIndex, j, metadata.TokenIDAt(j))
939+
in.Owner = sender.Identity
940+
in.OwnerAuditInfo = sender.AuditInfo
941+
in.EnrollmentID = eID
942+
in.RevocationHandler = rID
943+
inputs = append(inputs, in)
951944
}
952945
}
953946

token/stream.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@ func (o *OutputStream) String() string {
233233
// Input models an input of a token action
234234
type Input struct {
235235
ActionIndex int
236-
// Index is the position of this input within its action
236+
// Index is the position of this input within its action; it takes part in
237+
// deduplication only on inputs built through NewInput
237238
Index int
238239
Id *token.ID
239240
Owner Identity
@@ -242,6 +243,19 @@ type Input struct {
242243
RevocationHandler string
243244
Type token.Type
244245
Quantity token.Quantity
246+
247+
// positionKnown records that (ActionIndex, Index) was set deliberately —
248+
// NewInput stamps it — so UniquePerInput never collapses literal-built
249+
// inputs on their zero-valued position
250+
positionKnown bool
251+
}
252+
253+
// NewInput returns an Input positioned within its action. The position stands
254+
// in for the token ID during deduplication when the metadata has the token ID
255+
// filtered out (see UniquePerInput), so inputs are built through here; an
256+
// input built as a literal keeps no position and never collapses on it.
257+
func NewInput(actionIndex, index int, id *token.ID) *Input {
258+
return &Input{ActionIndex: actionIndex, Index: index, Id: id, positionKnown: true}
245259
}
246260

247261
// InputStream models a stream over a set of inputs (Input).
@@ -398,7 +412,10 @@ func (is *InputStream) ByType(tokenType token.Type) *InputStream {
398412
// composite owner's members once. The token is identified by its token ID,
399413
// or by (ActionIndex, Index) when the token ID has been filtered out of the
400414
// metadata; the two key spaces never mix, since only transfer inputs lose
401-
// their token ID. Identity consumers use the full stream instead.
415+
// their token ID. The position counts only for inputs built through NewInput:
416+
// a literal-built input without a token ID is kept as it stands, never
417+
// collapsed on a position nobody set. Identity consumers use the full stream
418+
// instead.
402419
func (is *InputStream) UniquePerInput() *InputStream {
403420
type idKey struct {
404421
id token.ID
@@ -422,6 +439,9 @@ func (is *InputStream) UniquePerInput() *InputStream {
422439

423440
return true
424441
}
442+
if !t.positionKnown {
443+
return true
444+
}
425445
k := posKey{actionIndex: t.ActionIndex, index: t.Index, eID: t.EnrollmentID}
426446
if seenByPos[k] {
427447
return false

token/stream_test.go

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,16 @@ func TestOutputStream_UniquePerOutput(t *testing.T) {
285285
}
286286
}
287287

288+
// extractedInput builds an input the way the request's extraction path does,
289+
// so its position takes part in deduplication.
290+
func extractedInput(actionIndex, index int, id *token.ID, eID, rh string) *Input {
291+
in := NewInput(actionIndex, index, id)
292+
in.EnrollmentID = eID
293+
in.RevocationHandler = rh
294+
295+
return in
296+
}
297+
288298
func TestInputStream_UniquePerInput(t *testing.T) {
289299
cases := []struct {
290300
name string
@@ -324,35 +334,46 @@ func TestInputStream_UniquePerInput(t *testing.T) {
324334
{
325335
"member rows with a filtered-out token ID collapse on action and input index",
326336
[]*Input{
327-
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "first"},
328-
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "second"},
337+
extractedInput(0, 0, nil, "enroll1", "first"),
338+
extractedInput(0, 0, nil, "enroll1", "second"),
329339
},
330-
[]*Input{{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1", RevocationHandler: "first"}},
340+
[]*Input{extractedInput(0, 0, nil, "enroll1", "first")},
331341
},
332342
{
333343
"filtered rows with different positions or enrollment IDs all survive",
334344
[]*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"},
345+
extractedInput(0, 0, nil, "enroll1", ""),
346+
extractedInput(0, 1, nil, "enroll1", ""),
347+
extractedInput(1, 0, nil, "enroll1", ""),
348+
extractedInput(0, 0, nil, "enroll2", ""),
339349
},
340350
[]*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"},
351+
extractedInput(0, 0, nil, "enroll1", ""),
352+
extractedInput(0, 1, nil, "enroll1", ""),
353+
extractedInput(1, 0, nil, "enroll1", ""),
354+
extractedInput(0, 0, nil, "enroll2", ""),
345355
},
346356
},
347357
{
348358
"a row with a token ID never collides with a positionally identical row without one",
349359
[]*Input{
350-
{ActionIndex: 0, Index: 0, Id: &token.ID{TxId: "tx0", Index: 0}, EnrollmentID: "enroll1"},
351-
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
360+
extractedInput(0, 0, &token.ID{TxId: "tx0", Index: 0}, "enroll1", ""),
361+
extractedInput(0, 0, nil, "enroll1", ""),
362+
},
363+
[]*Input{
364+
extractedInput(0, 0, &token.ID{TxId: "tx0", Index: 0}, "enroll1", ""),
365+
extractedInput(0, 0, nil, "enroll1", ""),
366+
},
367+
},
368+
{
369+
"literal-built rows with no token ID never collapse on the zero-valued position",
370+
[]*Input{
371+
{EnrollmentID: "enroll1"},
372+
{EnrollmentID: "enroll1"},
352373
},
353374
[]*Input{
354-
{ActionIndex: 0, Index: 0, Id: &token.ID{TxId: "tx0", Index: 0}, EnrollmentID: "enroll1"},
355-
{ActionIndex: 0, Index: 0, EnrollmentID: "enroll1"},
375+
{EnrollmentID: "enroll1"},
376+
{EnrollmentID: "enroll1"},
356377
},
357378
},
358379
}

0 commit comments

Comments
 (0)