diff --git a/BTCPayServer.Plugins.Payjoin/Plugin.cs b/BTCPayServer.Plugins.Payjoin/Plugin.cs index 36a0714..0592b6b 100644 --- a/BTCPayServer.Plugins.Payjoin/Plugin.cs +++ b/BTCPayServer.Plugins.Payjoin/Plugin.cs @@ -60,6 +60,7 @@ public override void Execute(IServiceCollection applicationBuilder) provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), + provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), provider.GetRequiredService(), diff --git a/BTCPayServer.Plugins.Payjoin/Services/PayjoinOhttpKeysProvider.cs b/BTCPayServer.Plugins.Payjoin/Services/PayjoinOhttpKeysProvider.cs index b639476..46fc4f4 100644 --- a/BTCPayServer.Plugins.Payjoin/Services/PayjoinOhttpKeysProvider.cs +++ b/BTCPayServer.Plugins.Payjoin/Services/PayjoinOhttpKeysProvider.cs @@ -13,8 +13,10 @@ namespace BTCPayServer.Plugins.Payjoin.Services; public sealed class PayjoinOhttpKeysProvider { - // TODO: Consider making this configurable if 12 hours is not a good duration for caching OHTTP keys. - private static readonly TimeSpan OhttpKeysCacheDuration = TimeSpan.FromHours(12); + // Directory OHTTP keys can rotate; a shorter lifetime bounds how long stale keys are served, and + // Invalidate lets a failed session build drop them immediately instead of waiting out the window. + // TODO: Consider making this configurable if one hour is not a good duration for caching OHTTP keys. + private static readonly TimeSpan OhttpKeysCacheDuration = TimeSpan.FromHours(1); // TODO: Consider making this configurable if 10 seconds is not a good timeout for fetching OHTTP keys. private static readonly TimeSpan OhttpKeysFetchTimeout = TimeSpan.FromSeconds(10); @@ -121,6 +123,13 @@ internal async Task FetchKeysAsync( } } + // Stale directory OHTTP keys are one way session construction fails; dropping the cached + // keys costs at most one refetch and lets the next attempt start from fresh material. + internal void Invalidate(string storeId, SystemUri ohttpRelayUrl, string directoryUrl) + { + _memoryCache.Remove(CreateCacheKey(storeId, ohttpRelayUrl, directoryUrl)); + } + internal static string CreateCacheKey(string storeId, SystemUri ohttpRelayUrl, string directoryUrl) { return $"PayjoinOhttpKeys_{storeId}_{ohttpRelayUrl.AbsoluteUri}_{directoryUrl}"; diff --git a/BTCPayServer.Plugins.Payjoin/Services/PayjoinUriSessionService.cs b/BTCPayServer.Plugins.Payjoin/Services/PayjoinUriSessionService.cs index a1ff1a3..f55164e 100644 --- a/BTCPayServer.Plugins.Payjoin/Services/PayjoinUriSessionService.cs +++ b/BTCPayServer.Plugins.Payjoin/Services/PayjoinUriSessionService.cs @@ -35,6 +35,7 @@ public sealed class PayjoinUriSessionService private readonly BTCPayNetworkProvider _networkProvider; private readonly PayjoinReceiverSessionStore _receiverSessionStore; private readonly PayjoinMailroomManager _mailroomManager; + private readonly PayjoinOhttpKeysProvider _ohttpKeysProvider; private readonly PayjoinAvailabilityService _availabilityService; private readonly PayjoinSessionBuildLock _sessionBuildLock; private readonly IPayjoinAccountingBridgeService _accountingBridgeService; @@ -44,6 +45,7 @@ internal PayjoinUriSessionService( BTCPayNetworkProvider networkProvider, PayjoinReceiverSessionStore receiverSessionStore, PayjoinMailroomManager mailroomManager, + PayjoinOhttpKeysProvider ohttpKeysProvider, PayjoinAvailabilityService availabilityService, PayjoinSessionBuildLock sessionBuildLock, IPayjoinAccountingBridgeService accountingBridgeService, @@ -52,6 +54,7 @@ internal PayjoinUriSessionService( _networkProvider = networkProvider; _receiverSessionStore = receiverSessionStore; _mailroomManager = mailroomManager; + _ohttpKeysProvider = ohttpKeysProvider; _availabilityService = availabilityService; _sessionBuildLock = sessionBuildLock; _accountingBridgeService = accountingBridgeService; @@ -129,19 +132,31 @@ public async Task BuildAsync( if (session is null) { - var selectedRelay = await _mailroomManager.SelectBootstrapRouteAsync( + var bootstrapRoute = await _mailroomManager.SelectBootstrapRouteAsync( storeSettings, storeId, invoiceId, cancellationToken).ConfigureAwait(false); - if (selectedRelay is null) + if (bootstrapRoute is null) { return LogUnexpectedFallbackAndReturnBip21(bip21, invoiceId, "OHTTP keys are unavailable from all configured relays"); } var bootstrapPersister = new CapturingReceiverSessionPersister(); - InitializeSession(destination, due, selectedRelay.DirectoryUrl.AbsoluteUri, selectedRelay.OhttpKeys, monitoringExpiresAt, bootstrapPersister); + try + { + InitializeSession(destination, due, bootstrapRoute.DirectoryUrl.AbsoluteUri, bootstrapRoute.OhttpKeys, monitoringExpiresAt, bootstrapPersister); + } + catch (UniffiException) + { + // Stale directory OHTTP keys are one way session construction fails; dropping the + // cached keys costs at most one refetch and lets the next attempt start from fresh + // material. Replayed sessions never reach here, so their failures keep the cache. + _ohttpKeysProvider.Invalidate(storeId, bootstrapRoute.RelayUrl, bootstrapRoute.DirectoryUrl.AbsoluteUri); + throw; + } + session = _receiverSessionStore.CreateSession( invoiceId, destination,