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 <noreply@anthropic.com>
Signed-off-by: Tom Brow <tomb@block.xyz>
This commit is contained in:
Tom Brow
2026-10-27 11:29:18 -07:00
parent 5efb40b53d
commit e9d7dfe1f5
3 changed files with 91 additions and 40 deletions

View File

@@ -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) {

View File

@@ -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,
);

View File

@@ -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<BuzzPushLeasePublication> _publish(
BuzzPushLeaseDescriptor descriptor,
nostr.Keys keys,
List<BuzzPushSubscription> 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: '',
),
);