Skip to content

Auditor nodes call IsMine() on every output identity, causing LocalMembership lock contention under high load #1619

Description

@EvanYan1024

Summary

On an auditor node under c=800 stress (~520 TPS sustained, 19k unique wallets, x509 identities, zkatdlog driver), LocalMembership.refreshAndGet accumulates 82-88% of all mutex wait time (Go mutex profile). Looking at the call chain, the root cause appears to be that tokens.Service.Parse calls auth.IsMine() on every transaction output regardless of node role, and on an auditor the result is always false but the LocalMembership lookup is still performed.

Question for maintainers: is this intentional? Should the auditor path skip IsMine(), or is the Mine flag on TokenToAppend semantically required for auditor records?

Call chain

P2P.handleMessage
  → AuditView.Call
    → AuditApproveView.Call
      → tokens.Service.CacheRequest
        → extractActions                       // tokens.go:364
          → Parse                              // tokens.go:397
            → auth.IsMine()                    // tokens.go:438 (per output)
              → role.Lookup
                → LocalMembership.refreshAndGet

At tokens.go:438 Parse computes mine for every output:

ownerWalletID, ids, mine := auth.IsMine(ctx, &output.Token)

At line 450:

if !mine && !auditorFlag && !issuerFlag {
    continue
}

For an auditor auditorFlag is always true, so mine does not affect keep/discard. The only later use of mine is the TokenToAppend.Flags.Mine field (line 475), which on an auditor is always false.

Evidence

Setup: 1 auditor + 1 institution + 19,996 x509 wallets, c=800, 60s. zkatdlog driver, identity_type=Owner cache fully warmed at start.

mutex profile breakdown (cumulative):

Path Share
LocalMembership.refreshAndGet 88.3%
↳ via GetIdentityInfo 64.2%
↳ via GetIdentifier 24.1%
auditor.Service.Append → auditdb.ReleaseLocks 10.5%
other ~1%

Per-60s-window counters (with negative-caching shim added locally — without the shim every call takes write Lock):

  • ~150,000 IsMine calls per 60s
  • ~6,000 unique owner labels seen → all written to negative cache
  • 0 positive resolutions on the auditor (localIdentitiesByName / localIdentitiesByIdentity never matched a user-wallet label)
  • 100% of slow-path entries either find the label in the negative cache during double-check, or do a PG storedIdentityConfigurations read followed by writing a negative entry

So every IsMine call from the audit hot path is essentially "look up a user-wallet label that this auditor will never have locally → confirm absent → record absence". The lookup result is unused on the auditor (line 450 already kept it via auditorFlag).

audit_view p50 duration is ~1.5s under c=800, with audit_view inflight peaking at 16,943 goroutines (vs audit-pool size 120) — large queue piled up on the LocalMembership write Lock during slow-path entries.

Possible fixes (asking for guidance)

(A) Skip IsMine when auditorFlag is set, in Parse:

var ownerWalletID string
var ids []string
mine := false
if !auditorFlag {
    ownerWalletID, ids, mine = auth.IsMine(ctx, &output.Token)
}

Smallest possible patch. Predicated on Mine: false being acceptable on an auditor's TokenToAppend records. Does anything read Flags.Mine on auditor-owned records?

(B) Auditor short-circuit in Authorization.IsMine:
Return false immediately when the local node is purely an auditor (no owner wallets and not an issuer). Less invasive at the call site but changes IsMine semantics for one node role.

(C) Different audit path that doesn't go through Parse:
More invasive — give the auditor view a dedicated parser that doesn't compute ownership flags.

Environment

  • fabric-token-sdk: v0.10.0
  • Driver: zkatdlog (nogh v1)
  • Identity type: x509
  • Go: 1.25
  • Workload: ring transfers across 19,996 wallets, c=800 concurrent, 60s window

Happy to share full mutex profiles, OTel histogram dumps, or test patches if any of (A)/(B)/(C) sounds promising.

Metadata

Metadata

Labels

enhancementNew feature or request

Type

Fields

No fields configured for Bug.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions