From 3cda852e9216037a91a1e40a81bbcd0a7518eed5 Mon Sep 17 00:00:00 2001 From: Kukks Date: Fri, 9 Feb 2024 12:05:39 +0100 Subject: [PATCH] mixing reasons --- .../BTCPayCoinjoinCoinSelector.cs | 131 ++++++++---------- .../BTCPayServer.Plugins.Wabisabi.csproj | 2 +- .../BTCPayWallet.cs | 60 ++++++-- submodules/walletwasabi | 2 +- 4 files changed, 108 insertions(+), 87 deletions(-) diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs index 895a4f2..51e15ee 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayCoinjoinCoinSelector.cs @@ -9,11 +9,13 @@ using NBitcoin; using WalletWasabi.Blockchain.Keys; using WalletWasabi.Blockchain.TransactionOutputs; using WalletWasabi.Crypto.Randomness; +using WalletWasabi.Exceptions; using WalletWasabi.Extensions; using WalletWasabi.Helpers; using WalletWasabi.WabiSabi; using WalletWasabi.WabiSabi.Backend.Rounds; using WalletWasabi.WabiSabi.Client; +using WalletWasabi.WabiSabi.Client.StatusChangedEvents; using WalletWasabi.Wallets; namespace BTCPayServer.Plugins.Wabisabi; @@ -83,7 +85,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector var maxPerType = new Dictionary(); var attemptingTobeParanoidWhenDoingPayments = payments.Any() && _wallet.WabisabiStoreSettings.ParanoidPayments; - var attemptingToMixToOtherWallet = string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet); + var attemptingToMixToOtherWallet = !string.IsNullOrEmpty(_wallet.WabisabiStoreSettings.MixToOtherWallet); selectCoins: maxPerType.Clear(); if (attemptingTobeParanoidWhenDoingPayments || attemptingToMixToOtherWallet) @@ -106,6 +108,15 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector ConsolidationModeType.WhenLowFeeAndManyUTXO => isLowFee && candidates.Count() > BTCPayWallet.HighAmountOfCoins, _ => 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 idealMinimumPerType = new Dictionary() {{AnonsetType.Red, 1}, {AnonsetType.Orange, 1}, {AnonsetType.Green, 1}}; @@ -113,7 +124,7 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector Random.Shared.Next(20, 31), maxPerType, idealMinimumPerType, - consolidationMode, liquidityClue, secureRandom); + consolidationMode, liquidityClue, secureRandom,mixReasons); if (attemptingTobeParanoidWhenDoingPayments && !solution.HandledPayments.Any()) { @@ -121,30 +132,26 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector payments = Array.Empty(); 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 - attemptingToMixToOtherWallet = false; - goto selectCoins; + throw new CoinJoinClientException(CoinjoinError.NoCoinsEligibleToMix, "ShouldMix returned true only for payments, but no handled payments were found, so we will not mix"); } + 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()); return (solution.Coins.ToImmutableList(), async coins => { - - - if (consolidationMode && coins.Count() <8) + var onlyForConsolidation = mixReasons.Length == 1 && mixReasons.Contains(IWallet.MixingReason.Consolidation); + if(onlyForConsolidation && coins.Count() < 10) { - var cv = new CoinsView(candidates); - 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; - } - + _wallet.LogTrace("ShouldMix returned true only for consolidation, but less than 10 coins were registered successfully, so we will not mix"); + return false; } return true; @@ -153,10 +160,11 @@ public class BTCPayCoinjoinCoinSelector : IRoundCoinSelector } private async Task SelectCoinsInternal(UtxoSelectionParameters utxoSelectionParameters, - IEnumerable coins,IEnumerable ineligibleCoins, IEnumerable pendingPayments, + IEnumerable coins, IEnumerable ineligibleCoins, + IEnumerable pendingPayments, int maxCoins, Dictionary maxPerType, Dictionary 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: //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, 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; - - if(consolidationMode && coins.Count() < 8 && !coinjoiningOnlyForPayments && ineligibleCoins.Any()) - { - //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 (ineligibleCoins.Any(coin => !coin.Confirmed)) - { - // 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 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}"); - if (chance > rand) - { - return solution; - } - } - } - - if (fullyPrivate && !coinjoiningOnlyForPayments ) - { - 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 but will join just to reduce timing analysis"); - } + // 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 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 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}"); + // if (chance > rand) + // { + // return solution; + // + // } + // } + // + // if (fullyPrivate && !coinjoiningOnlyForPayments && percentage ) + // { + // 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 but will join just to reduce timing analysis"); + // } 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; } @@ -443,6 +434,6 @@ public class SubsetSolution 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}"; } } diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj index d031085..52957ef 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayServer.Plugins.Wabisabi.csproj @@ -13,7 +13,7 @@ Coinjoin Allows you to integrate your btcpayserver store with coinjoins. - 1.0.70 + 1.0.71 true diff --git a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs index e390ed6..a612c7f 100644 --- a/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs +++ b/Plugins/BTCPayServer.Plugins.Wabisabi/BTCPayWallet.cs @@ -137,23 +137,53 @@ public class BTCPayWallet : IWallet, IDestinationProvider public bool BatchPayments => WabisabiStoreSettings.PlebMode || WabisabiStoreSettings.BatchPayments; public long? MinimumDenominationAmount => WabisabiStoreSettings.PlebMode? 10000 : WabisabiStoreSettings.MinimumDenominationAmount; - public async Task IsWalletPrivateAsync() - { - return await IsWalletPrivateAsync(await GetAllCoins()); - } + - public async Task IsWalletPrivateAsync(CoinsView coins) + public async Task ShouldMix(string coordinatorName, bool? isLowFee = null, + bool? anyPayments = false) { - var privacy= await GetPrivacyPercentageAsync(coins); - var mixToOtherWallet = !WabisabiStoreSettings.PlebMode && !string.IsNullOrEmpty(WabisabiStoreSettings - .MixToOtherWallet); - var forceConsolidate = ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && coins.Available().Confirmed().Count() > HighAmountOfCoins; - return !BatchPayments && privacy >= 1 && !mixToOtherWallet && !forceConsolidate; - } - - public bool ForceConsolidate(CoinsView coins, bool isLowFee) - { - return ConsolidationMode == ConsolidationModeType.WhenLowFeeAndManyUTXO && isLowFee && coins.Available().Confirmed().Count() > HighAmountOfCoins; + var results = new List(); + + 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(); + + 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); + } + } + + var privacy = await GetPrivacyPercentageAsync(new CoinsView(candidates)); + if (privacy >= 1) + { + var rand = Random.Shared.Next(1, 1001); + if (rand <= (WabisabiStoreSettings.PlebMode ? 0 : WabisabiStoreSettings.ExtraJoinProbability)) + results.Add(IWallet.MixingReason.ExtraJoin); + if (!string.IsNullOrEmpty(WabisabiStoreSettings.MixToOtherWallet)) + { + results.Add(IWallet.MixingReason.WalletForward); + } + } + else + { + results.Add(IWallet.MixingReason.NotPrivate); + } + + return results.ToArray(); + } public async Task GetPrivacyPercentageAsync(CoinsView coins) diff --git a/submodules/walletwasabi b/submodules/walletwasabi index 41b4ef3..f1feba6 160000 --- a/submodules/walletwasabi +++ b/submodules/walletwasabi @@ -1 +1 @@ -Subproject commit 41b4ef363c004475bc7e860e96bab4ffcc030c2b +Subproject commit f1feba62af39a096f1b0f7c8b73179c029f42bff