From 91b1cc47610bc22cdd4ad0454dd8c47f48fa29c9 Mon Sep 17 00:00:00 2001 From: Wes Date: Mon, 19 Oct 2026 15:10:14 -0700 Subject: [PATCH] perf(mobile): reuse the derived pubkey when signing auth events (#8460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Each relay `/query` signs a NIP-98 event, and each relay media image GET signs a Blossom auth event. Both run on the UI isolate. `nostr`'s `Event.from` derives the pubkey from the secret key on every call, which is about a third of the cost of a signature. Both call sites now pass the pubkey that `myPubkeyProvider` already caches for the active key. The provider is derived from the same `relayConfigProvider` state as the key, so the two always match. This is the quick win. Moving signing to native secp256k1 or a worker isolate would remove the rest of the cost, and that would be a separate change. ## Before and after AOT-compiled benchmark on Apple silicon, `nostr` 2.0.0 `Event.from`, three runs of 50 signatures each: | | ms per signature | |---|---| | `main` (pubkey derived) | 8.8 to 9.6 | | this PR (pubkey passed) | 5.9 to 6.4 | A scroll that shows 20 uncached relay-media images now spends about 60 ms less on the UI thread. ## Test There are no new tests. The existing `queryRelay` test runs through the real `relayConfigProvider` and `myPubkeyProvider`, and asserts the signed event's pubkey for one identity. It does not verify the signature or switch identities, and it would still pass with this optimization removed. The media tests build the service directly, so they only cover the fallback where the pubkey is derived. Review traced the pairing instead. Both the key and the pubkey come from `relayConfigProvider`. `_postForEvents` reads both and signs with no await in between, and the media provider watches both. So the key and pubkey change together on community switch and sign-out. ## Validation - `flutter analyze` - Full mobile suite: 3,534 passed. **Human test:** Wes ran #8456, #8460 and #8462 together on the simulator with his real account and confirmed they work. buzz-review-completed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Wes Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Co-authored-by: Claude Opus 5.5 --- mobile/lib/shared/relay/media_auth.dart | 11 ++++++++++- mobile/lib/shared/relay/relay_session.dart | 1 + mobile/lib/shared/relay/relay_session_auth.dart | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/mobile/lib/shared/relay/media_auth.dart b/mobile/lib/shared/relay/media_auth.dart index 8611ce5b56..cc423a99c6 100644 --- a/mobile/lib/shared/relay/media_auth.dart +++ b/mobile/lib/shared/relay/media_auth.dart @@ -31,6 +31,7 @@ const _mediaGetAuthRefreshMarginSeconds = 60; class MediaGetAuthService { final String _baseUrl; final String? _nsec; + final String? _pubkey; final DateTime Function() _now; Map? _cachedHeaders; @@ -39,9 +40,11 @@ class MediaGetAuthService { MediaGetAuthService({ required String baseUrl, required String? nsec, + String? pubkey, DateTime Function()? now, }) : _baseUrl = baseUrl, _nsec = nsec, + _pubkey = pubkey, _now = now ?? DateTime.now; bool isRelayMediaUrl(String url) { @@ -122,6 +125,8 @@ class MediaGetAuthService { content: 'Get buzz-media', tags: tags, secretKey: privkeyHex, + // Deriving the pubkey costs about a third of a signature, per image. + pubkey: _pubkey, verify: false, ); } @@ -129,7 +134,11 @@ class MediaGetAuthService { final mediaGetAuthServiceProvider = Provider((ref) { final config = ref.watch(relayConfigProvider); - return MediaGetAuthService(baseUrl: config.baseUrl, nsec: config.nsec); + return MediaGetAuthService( + baseUrl: config.baseUrl, + nsec: config.nsec, + pubkey: ref.watch(myPubkeyProvider), + ); }); Map mediaGetHeadersFor(WidgetRef ref, String url) { diff --git a/mobile/lib/shared/relay/relay_session.dart b/mobile/lib/shared/relay/relay_session.dart index 18e03bb2b7..3173afc85f 100644 --- a/mobile/lib/shared/relay/relay_session.dart +++ b/mobile/lib/shared/relay/relay_session.dart @@ -245,6 +245,7 @@ class RelaySessionNotifier extends Notifier { url: url, bodyBytes: bodyBytes, nsec: config.nsec, + pubkey: ref.read(myPubkeyProvider), ), 'Content-Type': 'application/json', }, diff --git a/mobile/lib/shared/relay/relay_session_auth.dart b/mobile/lib/shared/relay/relay_session_auth.dart index 9c6c79ae02..a198fc0151 100644 --- a/mobile/lib/shared/relay/relay_session_auth.dart +++ b/mobile/lib/shared/relay/relay_session_auth.dart @@ -1,10 +1,13 @@ part of 'relay_session.dart'; +/// Pass the signer's hex [pubkey] when it is already known: deriving it from +/// the key costs about a third of a signature, on the UI isolate. String buildNip98AuthHeader({ required String method, required String url, required List bodyBytes, required String? nsec, + String? pubkey, }) { if (nsec == null || nsec.isEmpty) { throw Exception('Cannot query relay: no signing key available'); @@ -27,6 +30,7 @@ String buildNip98AuthHeader({ ['nonce', const Uuid().v4()], ], secretKey: privkeyHex, + pubkey: pubkey, verify: false, ); return 'Nostr ${base64.encode(utf8.encode(event.toJson()))}';