mirror of
https://github.com/aljazceru/BTCPayServerPlugins.git
synced 2025-12-17 07:34:24 +01:00
mixing reasons
This commit is contained in:
@@ -9,11 +9,13 @@ using NBitcoin;
|
|||||||
using WalletWasabi.Blockchain.Keys;
|
using WalletWasabi.Blockchain.Keys;
|
||||||
using WalletWasabi.Blockchain.TransactionOutputs;
|
using WalletWasabi.Blockchain.TransactionOutputs;
|
||||||
using WalletWasabi.Crypto.Randomness;
|
using WalletWasabi.Crypto.Randomness;
|
||||||
|
using WalletWasabi.Exceptions;
|
||||||
using WalletWasabi.Extensions;
|
using WalletWasabi.Extensions;
|
||||||
using WalletWasabi.Helpers;
|
using WalletWasabi.Helpers;
|
||||||
using WalletWasabi.WabiSabi;
|
using WalletWasabi.WabiSabi;
|
||||||
using WalletWasabi.WabiSabi.Backend.Rounds;
|
using WalletWasabi.WabiSabi.Backend.Rounds;
|
||||||
using WalletWasabi.WabiSabi.Client;
|
using WalletWasabi.WabiSabi.Client;
|
||||||
|
using WalletWasabi.WabiSabi.Client.StatusChangedEvents;
|
||||||
using WalletWasabi.Wallets;
|
using WalletWasabi.Wallets;
|
||||||
|
|
||||||
namespace BTCPayServer.Plugins.Wabisabi;
|
namespace BTCPayServer.Plugins.Wabisabi;
|
||||||
@@ -83,7 +85,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
var maxPerType = new Dictionary<AnonsetType, int>();
|
var maxPerType = new Dictionary<AnonsetType, int>();
|
||||||
|
|
||||||
var attemptingTobeParanoidWhenDoingPayments = payments.Any() && _wallet.WabisabiStoreSettings.ParanoidPayments;
|
var attemptingTobeParanoidWhenDoingPayments = payments.Any() && _wallet.WabisabiStoreSettings.ParanoidPayments;
|
||||||
var attemptingToMixToOtherWallet = string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet);
|
var attemptingToMixToOtherWallet = !string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet);
|
||||||
selectCoins:
|
selectCoins:
|
||||||
maxPerType.Clear();
|
maxPerType.Clear();
|
||||||
if (attemptingTobeParanoidWhenDoingPayments || attemptingToMixToOtherWallet)
|
if (attemptingTobeParanoidWhenDoingPayments || attemptingToMixToOtherWallet)
|
||||||
@@ -106,6 +108,15 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
ConsolidationModeType.WhenLowFeeAndManyUTXO => isLowFee && candidates.Count() > BTCPayWallet.HighAmountOfCoins,
|
ConsolidationModeType.WhenLowFeeAndManyUTXO => isLowFee && candidates.Count() > BTCPayWallet.HighAmountOfCoins,
|
||||||
_ => throw new ArgumentOutOfRangeException()
|
_ => throw new ArgumentOutOfRangeException()
|
||||||
};
|
};
|
||||||
|
var mixReasons = await _wallet.ShouldMix(utxoSelectionParameters.CoordinatorName, isLowFee, payments.Any());
|
||||||
|
if (!mixReasons.Any())
|
||||||
|
{
|
||||||
|
throw new CoinJoinClientException(CoinjoinError.NoCoinsEligibleToMix, "ShouldMix returned false, so we will not mix");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_wallet.LogDebug($"ShouldMix returned true for {mixReasons.Length} reasons: {string.Join(", ", mixReasons)}");
|
||||||
|
}
|
||||||
Dictionary<AnonsetType, int> idealMinimumPerType = new Dictionary<AnonsetType, int>()
|
Dictionary<AnonsetType, int> idealMinimumPerType = new Dictionary<AnonsetType, int>()
|
||||||
{{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}};
|
{{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}};
|
||||||
|
|
||||||
@@ -113,7 +124,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
Random.Shared.Next(20, 31),
|
Random.Shared.Next(20, 31),
|
||||||
maxPerType,
|
maxPerType,
|
||||||
idealMinimumPerType,
|
idealMinimumPerType,
|
||||||
consolidationMode, liquidityClue, secureRandom);
|
consolidationMode, liquidityClue, secureRandom,mixReasons);
|
||||||
|
|
||||||
if (attemptingTobeParanoidWhenDoingPayments && !solution.HandledPayments.Any())
|
if (attemptingTobeParanoidWhenDoingPayments && !solution.HandledPayments.Any())
|
||||||
{
|
{
|
||||||
@@ -122,30 +133,26 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
goto selectCoins;
|
goto selectCoins;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attemptingToMixToOtherWallet && !solution.Coins.Any())
|
var onlyForPayments = mixReasons.Length == 1 && mixReasons.Contains(IWallet.MixingReason.Payment);
|
||||||
|
if (onlyForPayments && !solution.HandledPayments.Any())
|
||||||
{
|
{
|
||||||
// check that we have enough coins to mix to other wallet
|
throw new CoinJoinClientException(CoinjoinError.NoCoinsEligibleToMix, "ShouldMix returned true only for payments, but no handled payments were found, so we will not mix");
|
||||||
attemptingToMixToOtherWallet = false;
|
|
||||||
goto selectCoins;
|
|
||||||
}
|
}
|
||||||
|
var onlyForConsolidation = mixReasons.Length == 1 && mixReasons.Contains(IWallet.MixingReason.Consolidation);
|
||||||
|
if(onlyForConsolidation && solution.Coins.Count() < 10)
|
||||||
|
{
|
||||||
|
throw new CoinJoinClientException(CoinjoinError.NoCoinsEligibleToMix, "ShouldMix returned true only for consolidation, but less than 10 coins were found, so we will not mix");
|
||||||
|
}
|
||||||
|
|
||||||
_wallet.LogTrace(solution.ToString());
|
_wallet.LogTrace(solution.ToString());
|
||||||
return (solution.Coins.ToImmutableList(), async coins =>
|
return (solution.Coins.ToImmutableList(), async coins =>
|
||||||
{
|
{
|
||||||
|
var onlyForConsolidation = mixReasons.Length == 1 && mixReasons.Contains(IWallet.MixingReason.Consolidation);
|
||||||
|
if(onlyForConsolidation && coins.Count() < 10)
|
||||||
if (consolidationMode && coins.Count() <8)
|
|
||||||
{
|
{
|
||||||
var cv = new CoinsView(candidates);
|
_wallet.LogTrace("ShouldMix returned true only for consolidation, but less than 10 coins were registered successfully, so we will not mix");
|
||||||
var percentage = await _wallet.GetPrivacyPercentageAsync(cv);
|
|
||||||
var fullyPrivate = await _wallet.IsWalletPrivateAsync(cv);
|
|
||||||
|
|
||||||
if(percentage >=1 && fullyPrivate && !solution.HandledPayments.Any() )
|
|
||||||
{
|
|
||||||
_wallet.LogTrace("less than ideal coins were registered, so we will abandon this");
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|
||||||
@@ -153,10 +160,11 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task<SubsetSolution> SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters,
|
private async Task<SubsetSolution> SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters,
|
||||||
IEnumerable<SmartCoin> coins,IEnumerable<SmartCoin> ineligibleCoins, IEnumerable<PendingPayment> pendingPayments,
|
IEnumerable<SmartCoin> coins, IEnumerable<SmartCoin> ineligibleCoins,
|
||||||
|
IEnumerable<PendingPayment> pendingPayments,
|
||||||
int maxCoins,
|
int maxCoins,
|
||||||
Dictionary<AnonsetType, int> maxPerType, Dictionary<AnonsetType, int> idealMinimumPerType,
|
Dictionary<AnonsetType, int> maxPerType, Dictionary<AnonsetType, int> idealMinimumPerType,
|
||||||
bool consolidationMode, Money liquidityClue, SecureRandom random)
|
bool consolidationMode, Money liquidityClue, SecureRandom random, IWallet.MixingReason[] mixReason)
|
||||||
{
|
{
|
||||||
// Sort the coins by their anon score and then by descending order their value, and then slightly randomize in 2 ways:
|
// Sort the coins by their anon score and then by descending order their value, and then slightly randomize in 2 ways:
|
||||||
//attempt to shift coins that comes from the same tx AND also attempt to shift coins based on percentage probability
|
//attempt to shift coins that comes from the same tx AND also attempt to shift coins based on percentage probability
|
||||||
@@ -169,48 +177,39 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
var solution = new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget,
|
var solution = new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget,
|
||||||
utxoSelectionParameters);
|
utxoSelectionParameters);
|
||||||
|
|
||||||
var cv = new CoinsView(remainingCoins);
|
|
||||||
var percentage = await _wallet.GetPrivacyPercentageAsync(cv);
|
|
||||||
var fullyPrivate = await _wallet.IsWalletPrivateAsync(cv);
|
|
||||||
var coinjoiningOnlyForPayments = fullyPrivate && remainingPendingPayments.Any();
|
|
||||||
|
|
||||||
if (!consolidationMode && percentage < 1 && _wallet.ConsolidationMode != ConsolidationModeType.Never)
|
|
||||||
{
|
|
||||||
consolidationMode = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
solution.ConsolidationMode = consolidationMode;
|
solution.ConsolidationMode = consolidationMode;
|
||||||
|
|
||||||
if(consolidationMode && coins.Count() < 8 && !coinjoiningOnlyForPayments && ineligibleCoins.Any())
|
// if (consolidationMode && coins.Count() < 8 && !coinjoiningOnlyForPayments &&
|
||||||
{
|
// ineligibleCoins.Any(coin => !coin.Confirmed))
|
||||||
//if we're in consolidation mode, and there are coins not eligible for a reason that will be ok in the near future, we should try to wait for them to become eligible instead of entering multiple coinjoins, which costs more.
|
// {
|
||||||
// why are they ineligible? banned if not too far in the future is ok, unconfrmed as well
|
// //if we're in consolidation mode, and there are coins not eligible for a reason that will be ok in the near future, we should try to wait for them to become eligible instead of entering multiple coinjoins, which costs more.
|
||||||
if (ineligibleCoins.Any(coin => !coin.Confirmed))
|
// // why are they ineligible? banned if not too far in the future is ok, unconfrmed as well
|
||||||
{
|
//
|
||||||
// if there are unconfirmed coins, we should wait for them to confirm, but since we cant determine if they will be unconfirmed for a long time,. let's play a random chance game: the more coins we have towards the 8 coin goal, the bigger the chance we proceed with the coinjoin
|
// // if there are unconfirmed coins, we should wait for them to confirm, but since we cant determine if they will be unconfirmed for a long time,. let's play a random chance game: the more coins we have towards the 8 coin goal, the bigger the chance we proceed with the coinjoin
|
||||||
var rand = Random.Shared.Next(1, 101);
|
// var rand = Random.Shared.Next(1, 101);
|
||||||
var chance = (coins.Count()/8) * 100;
|
// var chance = (coins.Count() / 8) * 100;
|
||||||
_wallet.LogDebug($"coin selection: consolidation mode, and there are coins not eligible for a reason that will be ok in the near future, we should try to wait for them to become eligible instead of entering multiple coinjoins, which costs more. random chance to proceed: {chance} > {rand} (random 0-100) continue: {chance > rand}");
|
// _wallet.LogDebug(
|
||||||
if (chance > rand)
|
// $"coin selection: consolidation mode, and there are coins not eligible for a reason that will be ok in the near future, we should try to wait for them to become eligible instead of entering multiple coinjoins, which costs more. random chance to proceed: {chance} > {rand} (random 0-100) continue: {chance > rand}");
|
||||||
{
|
// if (chance > rand)
|
||||||
return solution;
|
// {
|
||||||
}
|
// return solution;
|
||||||
}
|
//
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
if (fullyPrivate && !coinjoiningOnlyForPayments )
|
//
|
||||||
{
|
// if (fullyPrivate && !coinjoiningOnlyForPayments && percentage )
|
||||||
var rand = Random.Shared.Next(1, 1001);
|
// {
|
||||||
if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability)
|
// var rand = Random.Shared.Next(1, 1001);
|
||||||
{
|
// if (rand > _wallet.WabisabiStoreSettings.ExtraJoinProbability)
|
||||||
_wallet.LogTrace($"All coins are private and we have no pending payments. Skipping join.");
|
// {
|
||||||
return solution;
|
// _wallet.LogTrace($"All coins are private and we have no pending payments. Skipping join.");
|
||||||
}
|
// return solution;
|
||||||
|
// }
|
||||||
_wallet.LogTrace(
|
//
|
||||||
"All coins are private and we have no pending payments but will join just to reduce timing analysis");
|
// _wallet.LogTrace(
|
||||||
}
|
// "All coins are private and we have no pending payments but will join just to reduce timing analysis");
|
||||||
|
// }
|
||||||
|
|
||||||
while (remainingCoins.Any())
|
while (remainingCoins.Any())
|
||||||
{
|
{
|
||||||
@@ -327,14 +326,6 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector
|
|||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (coinjoiningOnlyForPayments && solution.HandledPayments?.Any() is not true)
|
|
||||||
{
|
|
||||||
_wallet.LogInfo(
|
|
||||||
"Attempted to coinjoin only to fulfill payments but the coin selection results yielded no handled payment.");
|
|
||||||
return new SubsetSolution(remainingPendingPayments.Count, _wallet.AnonScoreTarget,
|
|
||||||
utxoSelectionParameters);
|
|
||||||
}
|
|
||||||
return solution;
|
return solution;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,6 +434,6 @@ public class SubsetSolution
|
|||||||
sc.TryGetValue(AnonsetType.Red, out var rcoins);
|
sc.TryGetValue(AnonsetType.Red, out var rcoins);
|
||||||
|
|
||||||
|
|
||||||
return $"Selected {Coins.Count} ({TotalValue} BTC) ({ocoins?.Length + rcoins?.Length} not private, {gcoins?.Length ?? 0} private) coins to pay {TotalPaymentsGross} payments ({TotalPaymentCost} BTC) with {LeftoverValue} BTC leftover\n Consolidation mode:{ConsolidationMode}";
|
return $"Selected {Coins.Count} ({TotalValue} BTC) ({ocoins?.Length + rcoins?.Length + 0} not private, {gcoins?.Length ?? 0} private) coins to pay {TotalPaymentsGross} payments ({TotalPaymentCost} BTC) with {LeftoverValue} BTC leftover\n Consolidation mode:{ConsolidationMode}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Product>Coinjoin</Product>
|
<Product>Coinjoin</Product>
|
||||||
<Description>Allows you to integrate your btcpayserver store with coinjoins.</Description>
|
<Description>Allows you to integrate your btcpayserver store with coinjoins.</Description>
|
||||||
<Version>1.0.70</Version>
|
<Version>1.0.71</Version>
|
||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -137,23 +137,53 @@ public class BTCPayWallet : IWallet, IDestinationProvider
|
|||||||
public bool BatchPayments => WabisabiStoreSettings.PlebMode || WabisabiStoreSettings.BatchPayments;
|
public bool BatchPayments => WabisabiStoreSettings.PlebMode || WabisabiStoreSettings.BatchPayments;
|
||||||
public long? MinimumDenominationAmount => WabisabiStoreSettings.PlebMode? 10000 : WabisabiStoreSettings.MinimumDenominationAmount;
|
public long? MinimumDenominationAmount => WabisabiStoreSettings.PlebMode? 10000 : WabisabiStoreSettings.MinimumDenominationAmount;
|
||||||
|
|
||||||
public async Task<bool> IsWalletPrivateAsync()
|
|
||||||
|
|
||||||
|
public async Task<IWallet.MixingReason[]> ShouldMix(string coordinatorName, bool? isLowFee = null,
|
||||||
|
bool? anyPayments = false)
|
||||||
{
|
{
|
||||||
return await IsWalletPrivateAsync(await GetAllCoins());
|
var results = new List<IWallet.MixingReason>();
|
||||||
|
|
||||||
|
if(BatchPayments && anyPayments is true)
|
||||||
|
results.Add(IWallet.MixingReason.Payment);
|
||||||
|
else if(BatchPayments && anyPayments is null)
|
||||||
|
return new []{IWallet.MixingReason.PreliminaryMixConclusion};
|
||||||
|
|
||||||
|
var candidates = (await GetCoinjoinCoinCandidatesAsync(coordinatorName)).ToArray();
|
||||||
|
if (!candidates.Any())
|
||||||
|
return Array.Empty<IWallet.MixingReason>();
|
||||||
|
|
||||||
|
var confirmed = candidates.Where(coin => coin.IsAvailable()).ToArray();
|
||||||
|
if (ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && confirmed.Count() >= HighAmountOfCoins )
|
||||||
|
{
|
||||||
|
if (isLowFee is null)
|
||||||
|
{
|
||||||
|
return new []{IWallet.MixingReason.PreliminaryMixConclusion};
|
||||||
|
}
|
||||||
|
else if (isLowFee is true)
|
||||||
|
{
|
||||||
|
results.Add(IWallet.MixingReason.Consolidation);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> IsWalletPrivateAsync(CoinsView coins)
|
var privacy = await GetPrivacyPercentageAsync(new CoinsView(candidates));
|
||||||
|
if (privacy >= 1)
|
||||||
{
|
{
|
||||||
var privacy= await GetPrivacyPercentageAsync(coins);
|
var rand = Random.Shared.Next(1, 1001);
|
||||||
var mixToOtherWallet = !WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings
|
if (rand <= (WabisabiStoreSettings.PlebMode ? 0 : WabisabiStoreSettings.ExtraJoinProbability))
|
||||||
.MixToOtherWallet);
|
results.Add(IWallet.MixingReason.ExtraJoin);
|
||||||
var forceConsolidate = ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && coins.Available().Confirmed().Count() > HighAmountOfCoins;
|
if (!string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet))
|
||||||
return !BatchPayments && privacy >= 1 && !mixToOtherWallet && !forceConsolidate;
|
{
|
||||||
|
results.Add(IWallet.MixingReason.WalletForward);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
results.Add(IWallet.MixingReason.NotPrivate);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ForceConsolidate(CoinsView coins, bool isLowFee)
|
return results.ToArray();
|
||||||
{
|
|
||||||
return ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && isLowFee && coins.Available().Confirmed().Count() > HighAmountOfCoins;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<double> GetPrivacyPercentageAsync(CoinsView coins)
|
public async Task<double> GetPrivacyPercentageAsync(CoinsView coins)
|
||||||
|
|||||||
Submodule submodules/walletwasabi updated: 41b4ef363c...f1feba62af
Reference in New Issue
Block a user