Make sure that mempool space fee rate doesn't drop below node's minRelayTxFee (#6853)

This commit is contained in:
Nicolas Dorier
2025-07-17 13:48:18 +09:00
committed by GitHub
parent 96abeff86e
commit e83a12d995
3 changed files with 47 additions and 36 deletions

View File

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

View File

@@ -18,6 +18,7 @@ public class FeeProviderFactory : IFeeProviderFactory, IPeriodicTask
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<FeeRate> 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<BTCPayNetworkBase, IFeeProvider> _FeeProviders;
public IFeeProvider CreateFeeProvider(BTCPayNetworkBase network)
{

View File

@@ -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"
@@ -36,25 +31,26 @@ public class MempoolSpaceFeeProvider(
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<BlockFeeRate[]> 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<Dictionary<string, decimal>>();
var r = new List<BlockFeeRate>();
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++)
{
@@ -111,15 +109,9 @@ public class MempoolSpaceFeeProvider(
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));
}
}