Prevent building CJ with fee lower than minMemPoolFee
ADD
WalletWasabi.Tests/UnitTests/CoordinatorRoundTests.cs
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using NBitcoin;
+using WalletWasabi.Blockchain.Blocks;
+using WalletWasabi.CoinJoin.Coordinator.Banning;
+using WalletWasabi.CoinJoin.Coordinator.Rounds;
+using Xunit;
+
+namespace WalletWasabi.Tests.UnitTests
+{
+ public class CoordinatorRoundTests
+ {
+ [Fact]
+ public async Task ssAsync()
+ {
+ var rpc = new MockRpcClient();
+ var roundConfig = new CoordinatorRoundConfig();
+ var utxoReferee = new UtxoReferee(Network.Main, "./", rpc, roundConfig);
+ var confirmationTarget = 12;
+ var round = new CoordinatorRound(rpc, utxoReferee, roundConfig, adjustedConfirmationTarget: confirmationTarget, confirmationTarget, roundConfig.ConfirmationTargetReductionRate);
+
+ static OutPoint GetRandomOutPoint() => new OutPoint(RandomUtils.GetUInt256(), 0);
+ var tx = Network.Main.CreateTransaction();
+ tx.Version = 1;
+ tx.LockTime = LockTime.Zero;
+ tx.Inputs.Add(GetRandomOutPoint(), new Script(OpcodeType.OP_0, OpcodeType.OP_0), sequence: Sequence.Final);
+ tx.Inputs.Add(GetRandomOutPoint(), new Script(OpcodeType.OP_0, OpcodeType.OP_0), sequence: Sequence.Final);
+ tx.Outputs.Add(Money.Coins(1.9995m), new Key().ScriptPubKey);
+
+ // Under normal circunstances
+ {
+ rpc.OnGetMempoolAsync = () => Task.FromResult(new MemPoolInfo
+ {
+ MemPoolMinFee = 0.00001000 // 1 s/b (default value)
+ });
+
+ var tx0 = tx.Clone();
+ var spentCoins = tx0.Inputs.Select(x => new Coin(x.PrevOut, new TxOut(Money.Coins(1), new Key().ScriptPubKey)));
+ var txFeeBeforeOptimization = tx0.GetFee(spentCoins.ToArray());
+ await round.TryOptimizeFeesAsync(tx0, spentCoins );
+ var txFeeAfterOptimization = tx0.GetFee(spentCoins.ToArray());
+ Assert.True(txFeeAfterOptimization < txFeeBeforeOptimization);
+ }
+
+ // Under heavy mempool pressure
+ {
+ rpc.OnGetMempoolAsync = () => Task.FromResult(new MemPoolInfo
+ {
+ MemPoolMinFee = 0.0028 // 280 s/b
+ });
+
+ var tx1 = tx.Clone();
+ var spentCoins = tx1.Inputs.Select(x => new Coin(x.PrevOut, new TxOut(Money.Coins(1), new Key().ScriptPubKey)));
+ var txFeeBeforeOptimization = tx1.GetFee(spentCoins.ToArray());
+ await round.TryOptimizeFeesAsync(tx1, spentCoins );
+ var txFeeAfterOptimization = tx1.GetFee(spentCoins.ToArray());
+
+ Assert.True(txFeeAfterOptimization == txFeeBeforeOptimization);
+ }
+ }
+
+ [Fact]
+ public async Task xxAsync()
+ {
+ const double DefaultMinMemPoolFee = 0.00001000; // 1 s/b (default value)
+ const double HighertMinMemPoolFee = 0.00200000; // 200 s/b
+ const int InputSizeInBytes = 67;
+ const int OutputSizeInBytes = 33;
+
+ var rpc = new MockRpcClient();
+ {
+ rpc.OnGetMempoolAsync = () => Task.FromResult(new MemPoolInfo
+ {
+ MemPoolMinFee = DefaultMinMemPoolFee
+ });
+
+ var (feePerInputs, feePerOutputs) = await CoordinatorRound.CalculateFeesAsync(rpc, 12);
+
+ var defaultMinMemPoolFeeRate = new FeeRate(Money.Coins((decimal)DefaultMinMemPoolFee));
+ Assert.True(feePerInputs > defaultMinMemPoolFeeRate.GetFee(InputSizeInBytes));
+ Assert.True(feePerOutputs > defaultMinMemPoolFeeRate.GetFee(OutputSizeInBytes));
+ }
+
+ {
+ rpc.OnGetMempoolAsync = () => Task.FromResult(new MemPoolInfo
+ {
+ MemPoolMinFee = HighertMinMemPoolFee
+ });
+
+ var (feePerInputs, feePerOutputs) = await CoordinatorRound.CalculateFeesAsync(rpc, 12);
+
+ var higherMinMemPoolFeeRate = new FeeRate(Money.Coins((decimal)HighertMinMemPoolFee));
+ Assert.True(feePerInputs == higherMinMemPoolFeeRate.GetFee(InputSizeInBytes));
+ Assert.True(feePerOutputs == higherMinMemPoolFeeRate.GetFee(OutputSizeInBytes));
+ }
+ }
+ }
+}
\ No newline at end of file

MODIFY
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
WalletWasabi.Tests/UnitTests/MockRpcClient.cs
@@ -15,6 +15,7 @@ namespace WalletWasabi.Tests.UnitTests
public Func<uint256, Task<BlockHeader>> OnGetBlockHeaderAsync { get; set; }
public Func<Task<BlockchainInfo>> OnGetBlockchainInfoAsync { get; set; }
public Func<uint256, Task<VerboseBlockInfo>> OnGetVerboseBlockAsync { get; set; }
+ public Func<Task<MemPoolInfo>> OnGetMempoolAsync { get; set; }
public Network Network { get; set; } = Network.RegTest;
public RPCCredentialString CredentialString => new RPCCredentialString();
@@ -59,6 +60,11 @@ namespace WalletWasabi.Tests.UnitTests
throw new NotImplementedException();
}
+ public Task<MemPoolInfo> GetMempoolAsync()
+ {
+ return OnGetMempoolAsync();
+ }
+
public Task<BitcoinAddress> GetNewAddressAsync()
{
throw new NotImplementedException();

MODIFY
WalletWasabi/BitcoinCore/Rpc/CachedRpcClient.cs
WalletWasabi/BitcoinCore/Rpc/CachedRpcClient.cs
@@ -145,6 +145,21 @@ namespace WalletWasabi.BitcoinCore.Rpc
() => base.GetMempoolEntryAsync(txid, throwIfNotFound)).ConfigureAwait(false);
}
+ public override async Task<MemPoolInfo> GetMempoolAsync()
+ {
+ string cacheKey = nameof(GetMempoolAsync);
+ var cacheOptions = new MemoryCacheEntryOptions
+ {
+ Size = 1,
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10)
+ };
+
+ return await Cache.AtomicGetOrCreateAsync(
+ cacheKey,
+ cacheOptions,
+ () => base.GetMempoolAsync()).ConfigureAwait(false);
+ }
+
public override async Task<EstimateSmartFeeResponse> EstimateSmartFeeAsync(int confirmationTarget, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative)
{
string cacheKey = $"{nameof(EstimateSmartFeeAsync)}:{confirmationTarget}:{estimateMode}";

MODIFY
WalletWasabi/BitcoinCore/Rpc/IRPCClient.cs
WalletWasabi/BitcoinCore/Rpc/IRPCClient.cs
@@ -35,6 +35,8 @@ namespace WalletWasabi.BitcoinCore.Rpc
Task<uint256[]> GetRawMempoolAsync();
+ Task<MemPoolInfo> GetMempoolAsync();
+
Task<MempoolAcceptResult> TestMempoolAcceptAsync(Transaction transaction, bool allowHighFees = false);
Task<EstimateSmartFeeResponse> EstimateSmartFeeAsync(int confirmationTarget, EstimateSmartFeeMode estimateMode = EstimateSmartFeeMode.Conservative);

MODIFY
WalletWasabi/BitcoinCore/Rpc/RpcClientBase.cs
WalletWasabi/BitcoinCore/Rpc/RpcClientBase.cs
@@ -55,6 +55,11 @@ namespace WalletWasabi.BitcoinCore.Rpc
return await Rpc.GetMempoolEntryAsync(txid, throwIfNotFound).ConfigureAwait(false);
}
+ public virtual async Task<MemPoolInfo> GetMempoolAsync()
+ {
+ return await Rpc.GetMemPoolAsync();
+ }
+
public virtual async Task<uint256[]> GetRawMempoolAsync()
{
return await Rpc.GetRawMempoolAsync().ConfigureAwait(false);

MODIFY
WalletWasabi/CoinJoin/Coordinator/Rounds/CoordinatorRound.cs
WalletWasabi/CoinJoin/Coordinator/Rounds/CoordinatorRound.cs
@@ -784,7 +784,7 @@ namespace WalletWasabi.CoinJoin.Coordinator.Rounds
return acceptedBlindedOutputScriptsCount;
}
- private async Task TryOptimizeFeesAsync(Transaction transaction, IEnumerable<Coin> spentCoins)
+ internal async Task TryOptimizeFeesAsync(Transaction transaction, IEnumerable<Coin> spentCoins)
{
try
{
@@ -812,6 +812,7 @@ namespace WalletWasabi.CoinJoin.Coordinator.Rounds
// 7.2. Get the most optimal FeeRate.
EstimateSmartFeeResponse estimateSmartFeeResponse = await RpcClient.EstimateSmartFeeAsync(AdjustedConfirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true).ConfigureAwait(false);
+
if (estimateSmartFeeResponse is null)
{
throw new InvalidOperationException($"{nameof(FeeRate)} is not yet initialized.");
@@ -821,7 +822,10 @@ namespace WalletWasabi.CoinJoin.Coordinator.Rounds
if (optimalFeeRate is { } && optimalFeeRate != FeeRate.Zero && currentFeeRate is { } && currentFeeRate != FeeRate.Zero) // This would be really strange if it'd happen.
{
- var sanityFeeRate = new FeeRate(2m); // 2 s/b
+ MemPoolInfo mempoolInfo = await RpcClient.GetMempoolAsync().ConfigureAwait(false);
+ FeeRate minMempoolFee = new FeeRate(Money.Coins((decimal)mempoolInfo.MemPoolMinFee));
+
+ var sanityFeeRate = FeeRate.Max(minMempoolFee, new FeeRate(2m)); // 2 s/b
optimalFeeRate = optimalFeeRate < sanityFeeRate ? sanityFeeRate : optimalFeeRate;
if (optimalFeeRate < currentFeeRate)
{
@@ -831,9 +835,10 @@ namespace WalletWasabi.CoinJoin.Coordinator.Rounds
Money toSave = fee - feeShouldBePaid;
// 7.2.2. Get the outputs to divide the savings between.
- int maxMixCount = transaction.GetIndistinguishableOutputs(includeSingle: true).Max(x => x.count);
- Money bestMixAmount = transaction.GetIndistinguishableOutputs(includeSingle: true).Where(x => x.count == maxMixCount).Max(x => x.value);
- int bestMixCount = transaction.GetIndistinguishableOutputs(includeSingle: true).First(x => x.value == bestMixAmount).count;
+ var indistinguishableOutputs = transaction.GetIndistinguishableOutputs(includeSingle: true).ToArray();
+ int maxMixCount = indistinguishableOutputs.Max(x => x.count);
+ Money bestMixAmount = indistinguishableOutputs.Where(x => x.count == maxMixCount).Max(x => x.value);
+ int bestMixCount = indistinguishableOutputs.First(x => x.value == bestMixAmount).count;
// 7.2.3. Get the savings per best mix outputs.
long toSavePerBestMixOutputs = toSave.Satoshi / bestMixCount;
@@ -894,14 +899,16 @@ namespace WalletWasabi.CoinJoin.Coordinator.Rounds
var outputSizeInBytes = Constants.OutputSizeInBytes;
try
{
+ var mempoolInfo = await rpc.GetMempoolAsync().ConfigureAwait(false);
+ var minMempoolFee = new FeeRate(Money.Coins((decimal)mempoolInfo.MemPoolMinFee));
+ var sanityFeeRate = FeeRate.Max(minMempoolFee, new FeeRate(2m));
+
var estimateSmartFeeResponse = await rpc.EstimateSmartFeeAsync(confirmationTarget, EstimateSmartFeeMode.Conservative, simulateIfRegTest: true).ConfigureAwait(false);
- var feeRate = estimateSmartFeeResponse.FeeRate;
- Money feePerBytes = feeRate.FeePerK / 1000;
+ var feeRate = FeeRate.Max(estimateSmartFeeResponse.FeeRate, sanityFeeRate);
- // Make sure min relay fee (1000 sat) is hit.
- feePerInputs = Math.Max(feePerBytes * inputSizeInBytes, Money.Satoshis(500));
- feePerOutputs = Math.Max(feePerBytes * outputSizeInBytes, Money.Satoshis(250));
+ feePerInputs = feeRate.GetFee(inputSizeInBytes);
+ feePerOutputs = feeRate.GetFee(outputSizeInBytes);
}
catch (Exception ex)
{

Prevent building CJ with fee lower than minMemPoolFee