From e83a12d9953721c71c972ebc75a71ace7f663180 Mon Sep 17 00:00:00 2001 From: Nicolas Dorier Date: Thu, 17 Jul 2025 13:48:18 +0900 Subject: [PATCH] Make sure that mempool space fee rate doesn't drop below node's minRelayTxFee (#6853) --- BTCPayServer.Tests/ThirdPartyTests.cs | 1 + .../Services/Fees/FeeProviderFactory.cs | 28 ++++++++-- .../Services/Fees/MempoolSpaceFeeProvider.cs | 54 ++++++++----------- 3 files changed, 47 insertions(+), 36 deletions(-) diff --git a/BTCPayServer.Tests/ThirdPartyTests.cs b/BTCPayServer.Tests/ThirdPartyTests.cs index a8b91ed68..86ae74518 100644 --- a/BTCPayServer.Tests/ThirdPartyTests.cs +++ b/BTCPayServer.Tests/ThirdPartyTests.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using BTCPayServer.Controllers; using BTCPayServer.Data; +using BTCPayServer.HostedServices; using BTCPayServer.Hosting; using BTCPayServer.Models.StoreViewModels; using BTCPayServer.Models.WalletViewModels; diff --git a/BTCPayServer/Services/Fees/FeeProviderFactory.cs b/BTCPayServer/Services/Fees/FeeProviderFactory.cs index a6374db29..c399655cd 100644 --- a/BTCPayServer/Services/Fees/FeeProviderFactory.cs +++ b/BTCPayServer/Services/Fees/FeeProviderFactory.cs @@ -15,10 +15,11 @@ namespace BTCPayServer.Services.Fees; public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask { public FeeProviderFactory( - BTCPayServerEnvironment Environment, - ExplorerClientProvider ExplorerClients, - IHttpClientFactory HttpClientFactory, - IMemoryCache MemoryCache) + BTCPayServerEnvironment Environment, + ExplorerClientProvider ExplorerClients, + IHttpClientFactory HttpClientFactory, + NBXplorerDashboard Dashboard, + IMemoryCache MemoryCache) { _FeeProviders = new(); @@ -41,9 +42,26 @@ public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask providers.Add(new NBXplorerFeeProvider(client)); providers.Add(new StaticFeeProvider(new FeeRate(100L, 1))); var fallback = new FallbackFeeProvider(providers.ToArray()); - _FeeProviders.Add(network, fallback); + + _FeeProviders.Add(network, new ClampFeeProvider(fallback, Dashboard, network.CryptoCode)); } } + + class ClampFeeProvider(IFeeProvider inner, NBXplorerDashboard dashboard, string cryptoCode) : IFeeProvider + { + public async Task GetFeeRateAsync(int blockTarget = 20) + { + var rate = await inner.GetFeeRateAsync(blockTarget); + var d = dashboard.Get(cryptoCode); + var min = d?.MempoolInfo?.MempoolMinfeeRate; + min ??= d?.Status?.BitcoinStatus?.MinRelayTxFee; + if (min is not null && rate < min) + rate = min; + rate = FeeRate.Min(rate, new FeeRate(2000m)); + return rate; + } + } + private readonly Dictionary _FeeProviders; public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network) { diff --git a/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs b/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs index 561fd5caf..bf82fa0da 100644 --- a/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs +++ b/BTCPayServer/Services/Fees/MempoolSpaceFeeProvider.cs @@ -3,14 +3,9 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; using NBitcoin; -using Org.BouncyCastle.Asn1.X509; -using YamlDotNet.Core.Tokens; namespace BTCPayServer.Services.Fees; @@ -20,7 +15,7 @@ public class MempoolSpaceFeeProvider( IHttpClientFactory httpClientFactory, bool testnet) : IFeeProvider { - private string ExplorerLink = testnet switch + private readonly string _explorerLink = testnet switch { true => "https://mempool.space/testnet/api/v1/fees/recommended", false => "https://mempool.space/api/v1/fees/recommended" @@ -31,30 +26,31 @@ public class MempoolSpaceFeeProvider( var result = await GetFeeRatesAsync(); return InterpolateOrBound(result, blockTarget); - + } - + internal static FeeRate InterpolateOrBound(BlockFeeRate[] ordered, int target) { - (BlockFeeRate lb, BlockFeeRate hb) = (ordered[0], ordered[^1]); + var (lb, hb) = (ordered[0], ordered[^1]); target = Math.Clamp(target, lb.Blocks, hb.Blocks); - for (int i = 0; i < ordered.Length; i++) + foreach (var t in ordered) { - if (ordered[i].Blocks > lb.Blocks && ordered[i].Blocks <= target) - lb = ordered[i]; - if (ordered[i].Blocks < hb.Blocks && ordered[i].Blocks >= target) - hb = ordered[i]; + if (t.Blocks > lb.Blocks && t.Blocks <= target) + lb = t; + if (t.Blocks < hb.Blocks && t.Blocks >= target) + hb = t; } if (hb.Blocks == lb.Blocks) return hb.FeeRate; - var a = (decimal)(target - lb.Blocks) / (decimal)(hb.Blocks - lb.Blocks); + var a = (decimal)(target - lb.Blocks) / (hb.Blocks - lb.Blocks); return new FeeRate((1 - a) * lb.FeeRate.SatoshiPerByte + a * hb.FeeRate.SatoshiPerByte); } - readonly TimeSpan Expiration = TimeSpan.FromMinutes(25); + + private readonly TimeSpan _expiration = TimeSpan.FromMinutes(25); public async Task RefreshCache() { var rate = await GetFeeRatesCore(); - memoryCache.Set(cacheKey, rate, Expiration); + memoryCache.Set(cacheKey, rate, _expiration); } public bool CachedOnly { get; set; } @@ -66,7 +62,7 @@ public class MempoolSpaceFeeProvider( { return (await memoryCache.GetOrCreateAsync(cacheKey, async entry => { - entry.AbsoluteExpirationRelativeToNow = Expiration; + entry.AbsoluteExpirationRelativeToNow = _expiration; return await GetFeeRatesCore(); }))!; } @@ -80,11 +76,12 @@ public class MempoolSpaceFeeProvider( async Task GetFeeRatesCore() { var client = httpClientFactory.CreateClient(nameof(MempoolSpaceFeeProvider)); - using var result = await client.GetAsync(ExplorerLink); + using var result = await client.GetAsync(_explorerLink); result.EnsureSuccessStatusCode(); var recommendedFees = await result.Content.ReadAsAsync>(); var r = new List(); - foreach ((var feeId, decimal value) in recommendedFees) + + foreach (var (feeId, value) in recommendedFees) { var target = feeId switch { @@ -98,6 +95,7 @@ public class MempoolSpaceFeeProvider( }; r.Add(new(target, new FeeRate(value))); } + var ordered = r.OrderBy(k => k.Blocks).ToArray(); for (var i = 0; i < ordered.Length; i++) { @@ -108,18 +106,12 @@ public class MempoolSpaceFeeProvider( } return ordered; } - + internal static decimal RandomizeByPercentage(decimal value, decimal percentage) { - if (value is 1) - return 1; - decimal range = (value * percentage) / 100m; - var res = value + (range * 2.0m) * ((decimal)(Random.Shared.NextDouble() - 0.5)); - return res switch - { - < 1m => 1m, - > 2000m => 2000m, - _ => res - }; + if (value == 1.0m) + return 1.0m; + var range = (value * percentage) / 100m; + return value + (range * 2.0m) * ((decimal)(Random.Shared.NextDouble() - 0.5)); } }