diff --git a/pkg/channels/buzz/buzz.go b/pkg/channels/buzz/buzz.go index 2510495c..99d27947 100644 --- a/pkg/channels/buzz/buzz.go +++ b/pkg/channels/buzz/buzz.go @@ -10,6 +10,7 @@ import ( "fmt" "strings" "sync" + "time" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip19" @@ -117,15 +118,36 @@ func (c *BuzzChannel) Start(ctx context.Context) error { } c.relay = relay - // NIP-42: the relay challenges, we sign the auth event with the bot identity. - // Buzz relays reject subscriptions from unauthenticated clients, so a failure - // here is fatal rather than advisory. - if err := relay.Auth(c.ctx, func(evt *nostr.Event) error { - return evt.Sign(c.secretKey) - }); err != nil { + // NIP-42: the relay sends an AUTH challenge asynchronously after connect. + // The go-nostr library stores the challenge in an unexported field that is + // populated by the read loop. If we call Auth() too quickly the challenge + // is empty and the relay rejects the event with "verification failed". + // Retry with backoff to give the relay time to deliver the challenge. + const maxAuthAttempts = 3 + var authErr error + for attempt := 0; attempt < maxAuthAttempts; attempt++ { + if attempt > 0 { + logger.WarnCF("buzz", "Retrying NIP-42 auth", map[string]any{ + "attempt": attempt + 1, + "delay": "1s", + }) + time.Sleep(1 * time.Second) + } + authErr = relay.Auth(c.ctx, func(evt *nostr.Event) error { + return evt.Sign(c.secretKey) + }) + if authErr == nil { + break + } + logger.WarnCF("buzz", "NIP-42 auth attempt failed", map[string]any{ + "attempt": attempt + 1, + "error": authErr.Error(), + }) + } + if authErr != nil { _ = relay.Close() c.cancel() - return fmt.Errorf("buzz NIP-42 auth failed: %w", err) + return fmt.Errorf("buzz NIP-42 auth failed: %w", authErr) } channelIDs := []string(c.config.Channels)