From e9d7dfe1f5345820f39f0ca13b85f764876043df Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 27 Oct 2026 11:29:18 -0700 Subject: [PATCH] fix(mobile): fail push setup when the relay cannot fit a minimal lease A relay whose advertised limits cannot hold the self-directed and DM subscriptions now fails subscription sync instead of saving a lease that publication would always reject, so Settings shows the setup error. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../push/push_subscription_provider.dart | 8 ++ .../shared/push/push_root_isolation_test.dart | 5 +- .../push/push_subscription_provider_test.dart | 118 ++++++++++++------ 3 files changed, 91 insertions(+), 40 deletions(-) diff --git a/mobile/lib/shared/push/push_subscription_provider.dart b/mobile/lib/shared/push/push_subscription_provider.dart index 80c31c1885..c94953e218 100644 --- a/mobile/lib/shared/push/push_subscription_provider.dart +++ b/mobile/lib/shared/push/push_subscription_provider.dart @@ -92,6 +92,14 @@ desiredBuzzPushSubscriptions({ // Each Solo chat only adds bytes, so keep the most recent ones that fit. var fitting = candidates.solos.length; if (!buzzPushSubscriptionsFitLease(descriptor, lease(fitting))) { + // Without Solo chats only DMs and mentions remain. A relay too small for + // those cannot carry push, so fail setup rather than save a lease that + // publication would reject. + if (!buzzPushSubscriptionsFitLease(descriptor, lease(0))) { + throw const FormatException( + 'The relay push limits are too small for a lease.', + ); + } var low = 0; var high = fitting - 1; while (low < high) { diff --git a/mobile/test/shared/push/push_root_isolation_test.dart b/mobile/test/shared/push/push_root_isolation_test.dart index f945f20660..e8bcb78e8f 100644 --- a/mobile/test/shared/push/push_root_isolation_test.dart +++ b/mobile/test/shared/push/push_root_isolation_test.dart @@ -423,8 +423,9 @@ const _descriptor = BuzzPushLeaseDescriptor( executorPubkey: 'pubkey', transport: 'apns', maxLeaseTtlSeconds: 3600, - maxContentLength: 4096, - maxPlaintextLength: 4096, + // The relay's NIP-11 limits, which fit a DM lease at the endpoint limit. + maxContentLength: 65536, + maxPlaintextLength: 32768, maxEndpointLength: 2048, maxStringLength: 512, ); diff --git a/mobile/test/shared/push/push_subscription_provider_test.dart b/mobile/test/shared/push/push_subscription_provider_test.dart index 17602afa6e..7990509d33 100644 --- a/mobile/test/shared/push/push_subscription_provider_test.dart +++ b/mobile/test/shared/push/push_subscription_provider_test.dart @@ -121,13 +121,15 @@ void main() { expect(desired.omittedSoloChats, dmCount); // A relay may advertise any endpoint limit; sizing must not allocate it. - final unbounded = desiredBuzzPushSubscriptions( - descriptor: _descriptor(endpointLength: 1 << 50), - community: _community(keys), - channels: channels, - mutedChannelIds: const [], - )!; - expect(unbounded.omittedSoloChats, soloCount); + expect( + () => desiredBuzzPushSubscriptions( + descriptor: _descriptor(endpointLength: 1 << 50), + community: _community(keys), + channels: channels, + mutedChannelIds: const [], + ), + throwsFormatException, + ); }); for (final (limit, descriptor, soloCount, mutedSolos) in [ @@ -181,42 +183,50 @@ void main() { _id(n), }); // The relay accepts any grant up to its endpoint limit. - final publication = await publishBuzzDevPushLease( - grant: BuzzPushEndpointGrant( - relayOrigin: descriptor.origin, - relayPubkey: descriptor.executorPubkey, - installationId: 'c' * 32, - endpointGrant: 'x' * descriptor.maxEndpointLength, - endpointHash: 'd' * 64, - endpointEpoch: 1, - generation: 9007199254740991, - expiresAt: 1756212000, - ), - descriptor: descriptor, - nsec: keys.nsec, - memberPubkey: keys.public, - subscriptions: desired.subscriptions, - now: () => DateTime.fromMillisecondsSinceEpoch(1752620000 * 1000), - submit: - ({ - required kind, - required content, - required tags, - createdAt, - }) async => const NostrEvent( - id: 'accepted-id', - pubkey: '', - createdAt: 0, - kind: 0, - tags: [], - content: '', - sig: '', - ), + final publication = await _publish( + descriptor, + keys, + desired.subscriptions, ); expect(publication.eventId, 'accepted-id'); }); } + for (final (limit, descriptor) in [ + ('content', _descriptor(contentLength: 100)), + ('plaintext', _descriptor(plaintextLength: 100)), + ]) { + test( + 'fails setup when the $limit limit cannot fit DMs and mentions', + () async { + final keys = nostr.Keys.generate(); + final channels = [channel(_id(0)), _solo(1, keys.public)]; + + // Publication rejects even the lease without Solo chats. + await expectLater( + _publish( + descriptor, + keys, + buildDesiredBuzzPushSubscriptions( + myPubkey: keys.public, + channelIds: [_id(0)], + ), + ), + throwsFormatException, + ); + expect( + () => desiredBuzzPushSubscriptions( + community: _community(keys), + descriptor: descriptor, + channels: channels, + mutedChannelIds: const [], + ), + throwsFormatException, + ); + }, + ); + } + test('waits for the Solo record but not past its failure', () async { const legacySoloID = '123e4567-e89b-42d3-a456-426614174009'; final community = Community.create( @@ -381,3 +391,35 @@ BuzzPushLeaseDescriptor _descriptor({ maxEndpointLength: endpointLength, maxStringLength: 512, ); + +Future _publish( + BuzzPushLeaseDescriptor descriptor, + nostr.Keys keys, + List subscriptions, +) => publishBuzzDevPushLease( + grant: BuzzPushEndpointGrant( + relayOrigin: descriptor.origin, + relayPubkey: descriptor.executorPubkey, + installationId: 'c' * 32, + endpointGrant: 'x' * descriptor.maxEndpointLength, + endpointHash: 'd' * 64, + endpointEpoch: 1, + generation: 9007199254740991, + expiresAt: 1756212000, + ), + descriptor: descriptor, + nsec: keys.nsec, + memberPubkey: keys.public, + subscriptions: subscriptions, + now: () => DateTime.fromMillisecondsSinceEpoch(1752620000 * 1000), + submit: ({required kind, required content, required tags, createdAt}) async => + const NostrEvent( + id: 'accepted-id', + pubkey: '', + createdAt: 0, + kind: 0, + tags: [], + content: '', + sig: '', + ), +);