perf(identity): skip IsMine owner lookup on a pure-auditor node#1827
perf(identity): skip IsMine owner lookup on a pure-auditor node#1827EvanYan1024 wants to merge 11 commits into
Conversation
6e4c4bc to
a4cc30e
Compare
A node that holds an auditor wallet and no owner wallets can never satisfy IsMine, yet the audit path calls it on every transaction output, each time performing a LocalMembership owner lookup that always misses. Under load these misses serialise on the LocalMembership write lock and dominate the auditor's mutex profile. Decide at construction whether the node is a pure auditor (auditor wallet present, zero owner wallets) and, if so, wrap its authorization in a small decorator whose IsMine returns false, skipping the owner lookup. This keeps WalletBasedAuthorization.IsMine a plain per-token predicate and isolates the node-role decision to the constructor. A node that also owns wallets is not wrapped, so ownership is still resolved. Refs LFDT-Panurus#1619 Signed-off-by: Evan <evanyan@sign.global>
f69f02f to
457ef6c
Compare
|
Thanks a lot for working on this PR. This PR LGTM except one concern. As you noted in "Note for reviewers". The question now, can we have a scenario where a pure auditor later registers an owner wallet via RegisterOwnerIdentity? Since the check is fixed at construction, wouldn't it keep skipping and mis-report the tokens it now owns? @adecaro can you have a look on that concern? Regards, |
|
Thanks @AkramBitar — the concern is valid, and it's exactly the caveat flagged in the PR description. Two facts to frame it, then a proposal. Why the decision was frozen at construction. Re-checking per call is not free: Scope of the gap. That said, I have an approach to handle the runtime-registration case — downgrade on registration:
If this shape looks good to you and @adecaro, I'll push it to this PR. |
|
Hello @EvanYan1024 Thanks a lot for your response. @adecaro, could you please share your thoughts?” Regards, |
|
Hi @EvanYan1024 , the approach sounds good. Please, go ahead. Many thanks 🙏 |
…ation The pure-auditor decision was frozen at construction: a node that later registered an owner wallet via RegisterOwnerIdentity would keep short-circuiting IsMine and mis-report tokens it now owns. Let the wallet service signal successful owner-identity registrations through an optional notifier interface. The decorator holds an atomic.Bool checked with a single load in IsMine; the first registration permanently downgrades it to plain delegation. The hook is attached before the emptiness check so a registration racing with construction cannot be missed. If the wallet service cannot signal registrations, the decorator is not applied and the node stays on the unoptimized path. Refs LFDT-Panurus#1619 Signed-off-by: Evan <evanyan@sign.global>
|
Pushed the downgrade-on-registration change as ad2155c. As proposed above: the decorator now holds an atomic.Bool checked with a single load in IsMine — the first successful RegisterOwnerIdentity permanently downgrades it to plain delegation. The wallet service signals registrations through an optional notifier interface (driver.WalletService untouched); the hook is attached before the emptiness check so a registration racing with construction cannot be missed, and without a notifier the decorator is simply not applied. Covered by tests on both sides: the decorator downgrade + no-notifier fallback in authorization_test.go, and listener firing (success only, not on failure) in wallet/service_test.go. @AkramBitar @adecaro |
OnOwnerIdentityRegistered now returns an unregister function. When the node turns out not to be a pure auditor, the constructor discards the decorator and detaches its hook instead of leaving a dead callback on the wallet service. The hook is still attached before the emptiness check, so the construction race remains covered. Signed-off-by: Evan <evanyan@sign.global>
An OwnerWalletIDs error in NewTMSAuthorization silently dropped the pure-auditor short-circuit. The fallback is the correct direction; log it at debug level like the auditor-wallet probe above, and cover the error path with a test. Signed-off-by: Evan <evanyan@sign.global>
|
okay, sorry for the late reply. I think this mechanism is not robust under replication. Image the issuer has two replicas. |
|
Hi @adecaro, you are right — thanks for catching this. I dug deeper and I now think the right fix is structural, not another patch on the flag. Let me summarize the options I worked through: 1. This PR as it stands (flag flipped by 2. Sinking the signal into 3. Root cause. The reason the short-circuit needs all this care is that stock correctness comes from the miss path itself: every So the new approach: keep the miss, make it cheap. Instead of skipping the lookup for pure auditors, This restores exactly the stock semantics — the shared store is consulted on every miss, so replicas need no flags, no events, and there is nothing to invalidate — while removing the full-scan-under-lock cost. A pure auditor gets the same benefit this PR aimed for (its misses no longer take the write lock at all), without any role-based special-casing, and every other node profits too. I have this implemented and tested (including the replication-discovery paths). Since it makes the decorator and the notifier wiring here unnecessary, I'll close this PR and submit the new approach as a fresh PR referencing #1619 and this discussion. Thanks @adecaro @AkramBitar for pushing on the robustness — the result is a much better fix. |
Refs #1619.
Background
On a pure auditor node, the audit path (
tokens.Service.Parse) callsAuthorization.IsMineon every transaction output. The result is never used there — keep/discard is decided by the auditor flag — but each call still performs aLocalMembershipowner lookup that, on an auditor, always misses. Under load these misses serialise on theLocalMembershipwrite lock and dominate the auditor's mutex profile (~82–88% of mutex wait time in #1619's measurements).Change
This implements the fix discussed and accepted in #1619: skip
IsMinefor a pure auditor — a node that holds an auditor wallet and no owner wallets.NewTMSAuthorizationdecides at construction (amIAnAuditor && len(OwnerWalletIDs) == 0) and, when true, wraps the authorization in a small decorator whoseIsMineshort-circuits to("", nil, false); all other checks delegate to the wrapped authorization via embedding.The short-circuit is not frozen at construction. The wallet service signals successful owner-identity registrations through an optional notifier (consumed via an interface assertion, so
driver.WalletServiceitself stays untouched); the decorator holds anatomic.Boolchecked with a single load inIsMine, and the first successfulRegisterOwnerIdentitypermanently downgrades it to plain delegation:The hook is attached before the emptiness check, so a registration racing with construction cannot be missed; when the node turns out not to be a pure auditor, the constructor detaches the hook again. If the wallet service cannot signal registrations, the decorator is not applied and the node stays on the unoptimized-but-correct path; a failing owner-wallet probe is debug-logged and falls back the same way.
This keeps
WalletBasedAuthorization.IsMinea plain per-token predicate and isolates the node-role decision to construction, alongside the existingAuthorizationMultiplexercomposition.NewTMSAuthorizationnow returns theAuthorizationinterface; both call sites already pass the result intoNewAuthorizationMultiplexer(...Authorization), so nothing depends on the concrete type.Gating on "auditor and no owner wallets" (rather than the auditor flag alone) keeps a combined auditor+owner node correct: there the node is not wrapped, so ownership is still resolved and
Mineis set on records it genuinely owns. On a pure auditorIsMineis already alwaysfalse, so this only removes the always-missing lookup.Testing
go build ./token/...andgo test -race ./token/core/common/ ./token/services/identity/wallet/pass. Covered cases: a pure auditor skips the owner lookup (asserted via the mock'sOwnerWalletcall count); a runtime owner registration downgrades the short-circuit; a wallet service without the notifier keeps the owner lookup; an owner-wallet probe failure keeps the owner lookup; an auditor that also owns wallets still resolves ownership and leaves no hook registered; on the wallet-service side, callbacks fire on successful registrations only and stop after unregistering.