Add RedCoinIsolation
MODIFY
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/CoinJoinProfileViewModelBase.cs
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/CoinJoinProfileViewModelBase.cs
@@ -10,6 +10,8 @@ public abstract class CoinJoinProfileViewModelBase : ViewModelBase
public virtual int AnonScoreTarget { get; } = 5;
+ public virtual bool RedCoinIsolation { get; } = false;
+
public virtual int FeeRateMedianTimeFrameHours { get; }
public static bool operator ==(CoinJoinProfileViewModelBase x, CoinJoinProfileViewModelBase y)

MODIFY
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/CoinJoinProfilesViewModel.cs
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/CoinJoinProfilesViewModel.cs
@@ -78,6 +78,7 @@ public partial class CoinJoinProfilesViewModel : DialogViewModelBase<bool>
var selected = SelectedProfile ?? SelectedManualProfile ?? Profiles.First();
keyManager.AutoCoinJoin = AutoCoinJoin;
+ keyManager.RedCoinIsolation = selected.RedCoinIsolation;
keyManager.SetAnonScoreTarget(selected.AnonScoreTarget, toFile: false);
keyManager.SetFeeRateMedianTimeFrame(selected.FeeRateMedianTimeFrameHours, toFile: false);
keyManager.IsCoinjoinProfileSelected = true;

MODIFY
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/PrivateCoinJoinProfileViewModel.cs
WalletWasabi.Fluent/ViewModels/CoinJoinProfiles/PrivateCoinJoinProfileViewModel.cs
@@ -22,6 +22,7 @@ internal class PrivateCoinJoinProfileViewModel : CoinJoinProfileViewModelBase
public override string Description => "Choice of the paranoid. Optimizes for privacy at all costs.";
public override int AnonScoreTarget { get; }
+ public override bool RedCoinIsolation { get; } = true;
public override int FeeRateMedianTimeFrameHours => 0;

MODIFY
WalletWasabi.Fluent/ViewModels/Dialogs/ManualCoinJoinProfileDialogViewModel.cs
WalletWasabi.Fluent/ViewModels/Dialogs/ManualCoinJoinProfileDialogViewModel.cs
@@ -12,6 +12,7 @@ namespace WalletWasabi.Fluent.ViewModels.Dialogs;
public partial class ManualCoinJoinProfileDialogViewModel : DialogViewModelBase<ManualCoinJoinProfileDialogViewModel.ManualCoinJoinProfileDialogViewModelResult?>
{
[AutoNotify] private bool _autoCoinjoin;
+ [AutoNotify] private bool _redCoinIsolation;
[AutoNotify] private int _anonScoreTarget;
[AutoNotify] private TimeFrameItem[] _timeFrames;
[AutoNotify] private TimeFrameItem _selectedTimeFrame;
@@ -19,6 +20,7 @@ public partial class ManualCoinJoinProfileDialogViewModel : DialogViewModelBase<
public ManualCoinJoinProfileDialogViewModel(CoinJoinProfileViewModelBase current, bool autoCoinJoin)
{
_autoCoinjoin = autoCoinJoin;
+ _redCoinIsolation = current.RedCoinIsolation;
_anonScoreTarget = current.AnonScoreTarget;
@@ -39,10 +41,11 @@ public partial class ManualCoinJoinProfileDialogViewModel : DialogViewModelBase<
NextCommand = ReactiveCommand.Create(() =>
{
var auto = AutoCoinjoin;
+ var isolateRed = RedCoinIsolation;
var target = AnonScoreTarget;
var hours = (int)Math.Floor(SelectedTimeFrame.TimeFrame.TotalHours);
- Close(DialogResultKind.Normal, new ManualCoinJoinProfileDialogViewModelResult(AutoCoinjoin, new ManualCoinJoinProfileViewModel(target, hours)));
+ Close(DialogResultKind.Normal, new ManualCoinJoinProfileDialogViewModelResult(auto, isolateRed, new ManualCoinJoinProfileViewModel(target, hours)));
});
}
@@ -54,7 +57,7 @@ public partial class ManualCoinJoinProfileDialogViewModel : DialogViewModelBase<
}
}
- public record ManualCoinJoinProfileDialogViewModelResult(bool AutoCoinJoin, ManualCoinJoinProfileViewModel Profile)
+ public record ManualCoinJoinProfileDialogViewModelResult(bool AutoCoinJoin, bool RedCoinIsolation, ManualCoinJoinProfileViewModel Profile)
{
}
}

MODIFY
WalletWasabi.Fluent/Views/Dialogs/ManualCoinJoinProfileDialogView.axaml
WalletWasabi.Fluent/Views/Dialogs/ManualCoinJoinProfileDialogView.axaml
<ToggleSwitch IsChecked="{Binding AutoCoinjoin}" />
</DockPanel>
+ <DockPanel ToolTip.Tip="Only allow a single anonymity score 1 coin into a coinjoin registration.">
+ <TextBlock Text="Red coin isolation" />
+ <ToggleSwitch IsChecked="{Binding RedCoinIsolation}" />
+ </DockPanel>
+
<StackPanel Spacing="10" ToolTip.Tip="Minimum anonymity score for a coin to be considered private.">
<TextBlock Text="Anonymity score target" />
<DockPanel>

MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Client/CoinJoinCoinSelectionTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Client/CoinJoinCoinSelectionTests.cs
@@ -22,6 +22,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: false,
anonScoreTarget: 10,
+ redCoinIsolation: false,
ConfigureRng(5));
Assert.Empty(coins);
@@ -43,6 +44,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: false,
anonScoreTarget: AnonymitySet,
+ redCoinIsolation: false,
ConfigureRng(5));
Assert.Empty(coins);
@@ -66,6 +68,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: true,
anonScoreTarget: AnonymitySet,
+ redCoinIsolation: false,
ConfigureRng(5));
Assert.Contains(smallerAnonCoin, coins);
@@ -88,6 +91,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: false,
anonScoreTarget: AnonymitySet,
+ redCoinIsolation: false,
ConfigureRng(1));
Assert.Single(coins);
@@ -110,6 +114,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: false,
anonScoreTarget: AnonymitySet,
+ redCoinIsolation: false,
ConfigureRng(1));
Assert.Single(coins);
@@ -132,6 +137,7 @@ public class CoinJoinCoinSelectionTests
CreateMultipartyTransactionParameters(),
consolidationMode: true,
anonScoreTarget: AnonymitySet,
+ redCoinIsolation: false,
ConfigureRng(1));
Assert.Equal(2, coins.Count);

MODIFY
WalletWasabi/Blockchain/Keys/KeyManager.cs
WalletWasabi/Blockchain/Keys/KeyManager.cs
@@ -25,6 +25,7 @@ public class KeyManager
{
public const int DefaultAnonScoreTarget = 5;
public const bool DefaultAutoCoinjoin = false;
+ public const bool DefaultRedCoinIsolation = false;
public const int DefaultFeeRateMedianTimeFrameHours = 0;
public const int AbsoluteMinGapLimit = 21;
@@ -176,6 +177,9 @@ public class KeyManager
[JsonProperty(Order = 15, PropertyName = "IsCoinjoinProfileSelected")]
public bool IsCoinjoinProfileSelected { get; set; } = false;
+ [JsonProperty(Order = 16, PropertyName = "RedCoinIsolation")]
+ public bool RedCoinIsolation { get; set; } = DefaultRedCoinIsolation;
+
[JsonProperty(Order = 999)]
private List<HdPubKey> HdPubKeys { get; }

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
WalletWasabi/WabiSabi/Client/CoinJoinClient.cs
@@ -43,6 +43,7 @@ public class CoinJoinClient
RoundStateUpdater roundStatusUpdater,
int anonScoreTarget = int.MaxValue,
bool consolidationMode = false,
+ bool redCoinIsolation = false,
TimeSpan feeRateMedianTimeFrame = default,
TimeSpan doNotRegisterInLastMinuteTimeLimit = default)
{
@@ -52,6 +53,7 @@ public class CoinJoinClient
RoundStatusUpdater = roundStatusUpdater;
AnonScoreTarget = anonScoreTarget;
ConsolidationMode = consolidationMode;
+ RedCoinIsolation = redCoinIsolation;
FeeRateMedianTimeFrame = feeRateMedianTimeFrame;
SecureRandom = new SecureRandom();
DoNotRegisterInLastMinuteTimeLimit = doNotRegisterInLastMinuteTimeLimit;
@@ -68,6 +70,7 @@ public class CoinJoinClient
private TimeSpan DoNotRegisterInLastMinuteTimeLimit { get; }
public bool ConsolidationMode { get; private set; }
+ public bool RedCoinIsolation { get; }
private TimeSpan FeeRateMedianTimeFrame { get; }
private async Task<RoundState> WaitForRoundAsync(uint256 excludeRound, CancellationToken token)
@@ -130,7 +133,7 @@ public class CoinJoinClient
{
currentRoundState = await WaitForRoundAsync(excludeRound, cancellationToken).ConfigureAwait(false);
RoundParameters roundParameteers = currentRoundState.CoinjoinState.Parameters;
- coins = SelectCoinsForRound(coinCandidates, roundParameteers, ConsolidationMode, AnonScoreTarget, SecureRandom);
+ coins = SelectCoinsForRound(coinCandidates, roundParameteers, ConsolidationMode, AnonScoreTarget, RedCoinIsolation, SecureRandom);
if (roundParameteers.MaxSuggestedAmount != default && coins.Any(c => c.Amount > roundParameteers.MaxSuggestedAmount))
{
@@ -435,7 +438,13 @@ public class CoinJoinClient
roundState.LogDebug(string.Join(Environment.NewLine, summary));
}
- internal static ImmutableList<SmartCoin> SelectCoinsForRound(IEnumerable<SmartCoin> coins, RoundParameters parameters, bool consolidationMode, int anonScoreTarget, WasabiRandom rnd)
+ internal static ImmutableList<SmartCoin> SelectCoinsForRound(
+ IEnumerable<SmartCoin> coins,
+ RoundParameters parameters,
+ bool consolidationMode,
+ int anonScoreTarget,
+ bool redCoinIsolation,
+ WasabiRandom rnd)
{
var filteredCoins = coins
.Where(x => parameters.AllowedInputAmounts.Contains(x.Amount))
@@ -447,9 +456,31 @@ public class CoinJoinClient
var privateCoins = filteredCoins
.Where(x => x.HdPubKey.AnonymitySet >= anonScoreTarget)
.ToArray();
- var nonPrivateCoins = filteredCoins
+
+ // If we want to isolate red coins from each other, then only let a single red coin get into our selection candidates.
+ SmartCoin[]? nonPrivateCoins = null;
+ if (redCoinIsolation)
+ {
+ var nonPrivateCandidates = filteredCoins
+ .Where(x => x.HdPubKey.AnonymitySet < anonScoreTarget && x.HdPubKey.AnonymitySet != 1)
+ .ToList();
+
+ var randomRed = filteredCoins
+ .Where(x => x.HdPubKey.AnonymitySet == anonScoreTarget)
+ .RandomElement();
+ if (randomRed is not null)
+ {
+ nonPrivateCandidates.Add(randomRed);
+ }
+
+ nonPrivateCoins = nonPrivateCandidates.ToShuffled().ToArray();
+ }
+ else
+ {
+ nonPrivateCoins = filteredCoins
.Where(x => x.HdPubKey.AnonymitySet < anonScoreTarget)
.ToArray();
+ }
// Make sure it's ordered by 1 private and 1 non-private coins.
// Otherwise we'd keep mixing private coins too much during the end of our mixing sessions.

MODIFY
WalletWasabi/WabiSabi/Client/CoinJoinTrackerFactory.cs
WalletWasabi/WabiSabi/Client/CoinJoinTrackerFactory.cs
@@ -31,6 +31,8 @@ public class CoinJoinTrackerFactory
new InternalDestinationProvider(wallet.KeyManager),
RoundStatusUpdater,
wallet.KeyManager.AnonScoreTarget,
+ consolidationMode: false,
+ redCoinIsolation: wallet.KeyManager.RedCoinIsolation,
feeRateMedianTimeFrame: TimeSpan.FromHours(wallet.KeyManager.FeeRateMedianTimeFrameHours),
doNotRegisterInLastMinuteTimeLimit: TimeSpan.FromMinutes(1));

Add RedCoinIsolation