Summary
AddToken/DeleteToken events are published synchronously inside the still-open database transaction, before the caller who owns that transaction has committed or rolled it back. Subscribers can therefore observe a token that is later rolled back and never actually persisted.
Where
token/services/tokens/storage.go:220-226 (AppendToken) — t.Notify(ctx, AddToken, ...) runs immediately after t.Tx.StoreToken(...), in the same call.
token/services/tokens/storage.go:159-163 (DeleteToken) — same pattern for DeleteToken.
token/services/tokens/tokens.go:97-161 (AppendValid) — never commits; it calls AppendToken/DeleteTokens on the transaction and returns. The transaction is committed by the caller (the finality listener), after AppendValid has already returned and the events have already gone out.
Impact
The in-repo subscriber is the interactive certifier (token/services/ttx/certifier/interactive/client.go:122 subscribes, :248 casts, OnReceive enqueues the token ID for asynchronous certification). If the surrounding transaction is rolled back after AppendValid returns an error further up the call stack, the certifier has already been notified about a token that will never exist in the store, producing spurious certification attempts and retries. events.Publisher is also a public extension point (any application-level subscriber sees the same premature event).
Reproduction
tx, _ := tokens.NewTransaction(pub, &tokendb.Transaction{TokenStoreTransaction: mockTx}, tmsID)
err := tx.AppendToken(ctx, tta)
require.NoError(t, err)
require.Equal(t, 1, pub.PublishCallCount(), "AddToken should have been published")
// caller now decides to roll back
require.NoError(t, tx.Rollback())
// BUG: publish call count is unchanged — the event already escaped and cannot be retracted
assert.Equal(t, 1, pub.PublishCallCount())
=== RUN TestTransaction_AppendToken_NotifiesBeforeRollback
--- PASS: TestTransaction_AppendToken_NotifiesBeforeRollback (0.00s)
PASS
Severity
High — an event bus observing not-yet-committed, possibly-never-committed state is a correctness hazard for every current and future subscriber.
Part of #2112.
Summary
AddToken/DeleteTokenevents are published synchronously inside the still-open database transaction, before the caller who owns that transaction has committed or rolled it back. Subscribers can therefore observe a token that is later rolled back and never actually persisted.Where
token/services/tokens/storage.go:220-226(AppendToken) —t.Notify(ctx, AddToken, ...)runs immediately aftert.Tx.StoreToken(...), in the same call.token/services/tokens/storage.go:159-163(DeleteToken) — same pattern forDeleteToken.token/services/tokens/tokens.go:97-161(AppendValid) — never commits; it callsAppendToken/DeleteTokenson the transaction and returns. The transaction is committed by the caller (the finality listener), afterAppendValidhas already returned and the events have already gone out.Impact
The in-repo subscriber is the interactive certifier (
token/services/ttx/certifier/interactive/client.go:122subscribes,:248casts,OnReceiveenqueues the token ID for asynchronous certification). If the surrounding transaction is rolled back afterAppendValidreturns an error further up the call stack, the certifier has already been notified about a token that will never exist in the store, producing spurious certification attempts and retries.events.Publisheris also a public extension point (any application-level subscriber sees the same premature event).Reproduction
Severity
High — an event bus observing not-yet-committed, possibly-never-committed state is a correctness hazard for every current and future subscriber.
Part of #2112.