From 735cb0734e881d2884a2e2b3a1939c8b76339eaa Mon Sep 17 00:00:00 2001 From: PeterChrz Date: Tue, 11 Aug 2026 21:58:13 -0400 Subject: [PATCH] fix(buzz): retry NIP-42 auth with backoff to fix race condition The go-nostr RelayConnect returns immediately after the WebSocket handshake, but the relay has not yet sent the AUTH challenge. Calling relay.Auth() right away signs the auth event with an empty challenge tag, which the Buzz relay rejects with 'auth-required: verification failed'. Retry the Auth() call up to 3 times with 1s backoff between attempts, giving the relay time to deliver the challenge. --- pkg/channels/buzz/buzz.go | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) 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)