Refactor and cocument the algorithm
MODIFY
WalletWasabi.Tests/UnitTests/AllFeeEstimateTests.cs
WalletWasabi.Tests/UnitTests/AllFeeEstimateTests.cs
@@ -239,12 +239,12 @@ namespace WalletWasabi.Tests.UnitTests
[Repeat(1_000)]
public async Task ExhaustiveEstimationsAsync(int dummy)
{
+ var mempoolInfo = MempoolInfoGenerator.GenerateMempoolInfo();
var rpc = CreateAndConfigureRpcClient(
estimator: MempoolInfoGenerator.GenerateFeeRateForTarget,
- mempoolMinFee: 0.00037,
hasPeersInfo: true
);
- rpc.OnGetMempoolInfoAsync = async () => await Task.FromResult(MempoolInfoGenerator.GenerateMempoolInfo());
+ rpc.OnGetMempoolInfoAsync = async () => await Task.FromResult(mempoolInfo);
var feeRates = await rpc.EstimateAllFeeAsync(EstimateSmartFeeMode.Conservative);
var estimations = feeRates.Estimations;
@@ -252,7 +252,7 @@ namespace WalletWasabi.Tests.UnitTests
Assert.Subset(Constants.ConfirmationTargets.ToHashSet(), estimations.Keys.ToHashSet());
Assert.Equal(estimations.Keys, estimations.Keys.OrderBy(x => x));
Assert.Equal(estimations.Values, estimations.Values.OrderByDescending(x => x));
- Assert.All(estimations, (e) => Assert.True(e.Value >= 0.00037));
+ Assert.All(estimations, (e) => Assert.True(e.Value >= mempoolInfo.MemPoolMinFee * 100_000));
}
private static MockRpcClient CreateAndConfigureRpcClient(Func<int, FeeRate> estimator, bool isSynchronized = true, bool hasPeersInfo = false, double mempoolMinFee = 0.00001000)

MODIFY
WalletWasabi/Extensions/RPCClientExtensions.cs
WalletWasabi/Extensions/RPCClientExtensions.cs
@@ -69,36 +69,23 @@ namespace NBitcoin.RPC
/// </summary>
public static async Task<AllFeeEstimate> EstimateAllFeeAsync(this IRPCClient rpc, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative, bool simulateIfRegTest = false)
{
- var estimations = (simulateIfRegTest && rpc.Network == Network.RegTest)
+ var smartEstimations = (simulateIfRegTest && rpc.Network == Network.RegTest)
? SimulateRegTestFeeEstimation(estimateMode)
: await GetFeeEstimationsAsync(rpc, estimateMode).ConfigureAwait(false);
- var rpcStatus = await rpc.GetRpcStatusAsync(CancellationToken.None).ConfigureAwait(false);
var mempoolInfo = await rpc.GetMempoolInfoAsync().ConfigureAwait(false);
var sanityFeeRate = mempoolInfo.GetSanityFeeRate();
- const int BlockSize = 1_000_000;
- static IEnumerable<(int Size, FeeRate From, FeeRate To)> SplitFeeGroupInBlocks(FeeRateGroup group)
- => Enumerable
- .Range(0, (int)group.Sizes / BlockSize)
- .Select(b => (Sizes: BlockSize, From: group.From, To: group.To));
+ var minEstimations = GetFeeEstimationsFromMempoolInfo(mempoolInfo);
- var minFeeRatePerTarget = mempoolInfo.Histogram
- .OrderByDescending(x => x.Group)
- .SkipWhile(x => x.Count < mempoolInfo.Size / 100 ) // Filter those groups with very fee transactions (less than 1%)
- .SelectMany(x => SplitFeeGroupInBlocks(x))
- .Scan((Group: default((int Size, FeeRate From, FeeRate To)), Size: 0), (acc, c) => (c, acc.Size + c.Size))
- .Select(x => (From: x.Group.From, To: x.Group.To, MvB: x.Size, Target: 1 + x.Size / BlockSize))
- .GroupBy(x => x.Target,
- (target, feeGroups) => (Target: target, To: feeGroups.First().To, From: (feeGroups.LastOrDefault().From ?? FeeRate.Zero), MvB: feeGroups.Sum(xx => (decimal)xx.MvB)))
- .ToDictionary(x => x.Target, x => (int)Math.Ceiling(x.From.SatoshiPerByte));
+ var rpcStatus = await rpc.GetRpcStatusAsync(CancellationToken.None).ConfigureAwait(false);
- var fixedEstimations = estimations.GroupJoin(minFeeRatePerTarget,
+ var fixedEstimations = smartEstimations.GroupJoin(minEstimations,
outer => outer.Key,
inner => inner.Key,
(outer, inner) => new { Estimation = outer, MinimumFromMemPool = inner})
.SelectMany(x => x.MinimumFromMemPool.DefaultIfEmpty(),
- (a, b) => (Target: a.Estimation.Key, FeeRate: Math.Max(Math.Max(a.Estimation.Value, b.Value), (int)sanityFeeRate.SatoshiPerByte)))
+ (a, b) => (Target: a.Estimation.Key, FeeRate: Math.Max((int)sanityFeeRate.SatoshiPerByte, Math.Max(a.Estimation.Value, b.Value))))
.ToDictionary(x => x.Target, x => x.FeeRate);
return new AllFeeEstimate(
@@ -136,6 +123,57 @@ namespace NBitcoin.RPC
.ToDictionary(x => x.Blocks, x => (int)Math.Ceiling(x.FeeRate.SatoshiPerByte));
}
+ private static Dictionary<int, int> GetFeeEstimationsFromMempoolInfo(MemPoolInfo mempoolInfo)
+ {
+ const int BlockSize = 1_000_000;
+ static IEnumerable<(int Size, FeeRate From, FeeRate To)> SplitFeeGroupInBlocks(FeeRateGroup group)
+ => Enumerable
+ .Range(0, (int)group.Sizes / BlockSize)
+ .Select(b => (Sizes: BlockSize, From: group.From, To: group.To));
+
+ // Filter those groups with very fee transactions (less than 1%).
+ // This is because in case a few transactions pay unreasonablely high fees
+ // then we don't want our estimations to be affected by those rare cases.
+ var relevantFeeGroups = mempoolInfo.Histogram
+ .OrderByDescending(x => x.Group)
+ .SkipWhile(x => x.Count < mempoolInfo.Size / 100);
+
+ // Splits multi-megabyte fee rate groups in 1mb chunck
+ // We need to count blocks (or 1MvB transaction chuncks) so, in case fee
+ // groups are bigger than 1MvB we split those in multiple 1MvB chuncks.
+ var splittedFeeGroups = relevantFeeGroups.SelectMany(x => SplitFeeGroupInBlocks(x));
+
+ // Assigns the corresponding confirmation target to the set of fee groups.
+ // We have multiples fee rate groups which size are in the range [0..1MvB)
+ //
+ // Example: imagine we have only 4 fee rate groups in the form (size, from, to)
+ // [(10kb, 400, 500) (55kb, 300, 400) (310kb, 200, 300) (700kb, 100, 200)]
+ //
+ // In this case the three first fee rate groups fit well in the next block so
+ // they have target=1 while the fourth will need to wait and for that reason it
+ // is target=2
+ var feeGroupsByTarget = splittedFeeGroups
+ .Scan((Group: default((int Size, FeeRate From, FeeRate To)), Size: 0), (acc, c) => (c, acc.Size + c.Size))
+ .Select(x => (From: x.Group.From, To: x.Group.To, Target: 1 + x.Size / BlockSize));
+
+ // Consolidates all the fee rate groups that share the same confirmation target.
+ // Following the previous example we have the fee rate groups with target in the
+ // form of (target, size, from, to)
+ // [(1, 10kb, 400, 500) (1, 55kb, 300, 400) (1, 310kb, 200, 300) (2, 700kb, 100, 200)]
+ //
+ // But what we need is the following:
+ // [(1, 200, 500) (2, 100, 200)]
+ var consolidatedFeeGroupByTarget = feeGroupsByTarget
+ .GroupBy(x => x.Target,
+ (target, feeGroups) => (Target: target, To: feeGroups.First().To, From: (feeGroups.LastOrDefault().From ?? FeeRate.Zero)));
+
+ // Convert the fee range groups in estimations (minimum)
+ var estimations = consolidatedFeeGroupByTarget
+ .ToDictionary(x => x.Target, x => (int)Math.Ceiling(x.From.SatoshiPerByte));
+
+ return estimations;
+ }
+
public static async Task<RpcStatus> GetRpcStatusAsync(this IRPCClient rpc, CancellationToken cancel)
{
try

Refactor and cocument the algorithm