Fix rounding error in anonscore calculation
MODIFY
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinViewModel.cs
WalletWasabi.Fluent/ViewModels/Wallets/Advanced/WalletCoins/WalletCoinViewModel.cs
@@ -29,7 +29,7 @@ public partial class WalletCoinViewModel : ViewModelBase, IDisposable
Coin.WhenAnyValue(c => c.Confirmed).Subscribe(x => Confirmed = x).DisposeWith(_disposables);
Coin.WhenAnyValue(c => c.HdPubKey.Cluster.Labels).Subscribe(x => SmartLabel = x).DisposeWith(_disposables);
- Coin.WhenAnyValue(c => c.HdPubKey.AnonymitySet).Subscribe(x => AnonymitySet = x).DisposeWith(_disposables);
+ Coin.WhenAnyValue(c => c.HdPubKey.AnonymitySet).Subscribe(x => AnonymitySet = (int)x).DisposeWith(_disposables);
Coin.WhenAnyValue(c => c.CoinJoinInProgress).Subscribe(x => CoinJoinInProgress = x).DisposeWith(_disposables);
Coin.WhenAnyValue(c => c.IsBanned).Subscribe(x => IsBanned = x).DisposeWith(_disposables);
Coin.WhenAnyValue(c => c.BannedUntilUtc).Subscribe(x => BannedUntilUtcToolTip = $"Banned until: {x}").DisposeWith(_disposables);

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
@@ -95,7 +95,7 @@ public class CoinJoinAnonScoreTests
var change = tx.WalletOutputs.First(x => x.Amount == Money.Coins(5m));
Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
- Assert.Equal(59, active.HdPubKey.AnonymitySet);
+ Assert.Equal(59.5, active.HdPubKey.AnonymitySet);
Assert.Equal(1, change.HdPubKey.AnonymitySet);
}
@@ -175,7 +175,7 @@ public class CoinJoinAnonScoreTests
analyser.Analyze(tx);
Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
- Assert.All(tx.WalletOutputs, x => Assert.Equal(5 / 2, x.HdPubKey.AnonymitySet));
+ Assert.All(tx.WalletOutputs, x => Assert.Equal(5 / 2d, x.HdPubKey.AnonymitySet));
}
[Fact]
@@ -197,7 +197,7 @@ public class CoinJoinAnonScoreTests
// The increase in the anonymity set would naively be 1 as there is 1 equal non-wallet output.
// Since 4 outputs are ours, we divide the increase in anonymity between them
// and add that to the inherited anonymity of 4.
- Assert.All(tx.WalletOutputs, x => Assert.Equal(4 + 1 / 4, x.HdPubKey.AnonymitySet));
+ Assert.All(tx.WalletOutputs, x => Assert.Equal(4 + 1 / 4d, x.HdPubKey.AnonymitySet));
}
[Fact]

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
@@ -56,8 +56,8 @@ public class PubKeyReuseAnonScoreTests
var newOutputAnonsets = tx.WalletOutputs.Select(x => x.HdPubKey.AnonymitySet).ToArray();
// Anonsets should not change.
- Assert.Equal<int[]>(inputAnonsets, newInputAnonsets);
- Assert.Equal<int[]>(outputAnonsets, newOutputAnonsets);
+ Assert.Equal(inputAnonsets, newInputAnonsets);
+ Assert.Equal(outputAnonsets, newOutputAnonsets);
}
[Fact]

MODIFY
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
@@ -47,7 +47,7 @@ public class BlockchainAnalyzer
}
else
{
- int startingOutputAnonset;
+ double startingOutputAnonset;
var distinctWalletInputPubKeys = tx.WalletInputs.Select(x => x.HdPubKey).ToHashSet();
if (foreignInputCount == 0)
@@ -58,7 +58,7 @@ public class BlockchainAnalyzer
}
else
{
- AnalyzeCoinjoinWalletInputs(tx, out startingOutputAnonset, out int nonMixedAnonScore);
+ AnalyzeCoinjoinWalletInputs(tx, out startingOutputAnonset, out double nonMixedAnonScore);
AnalyzeCoinjoinWalletOutputs(tx, startingOutputAnonset, nonMixedAnonScore, distinctWalletInputPubKeys);
}
@@ -69,19 +69,19 @@ public class BlockchainAnalyzer
AnalyzeClusters(tx);
}
- private static void AnalyzeCoinjoinWalletInputs(SmartTransaction tx, out int mixedAnonScore, out int nonMixedAnonScore)
+ private static void AnalyzeCoinjoinWalletInputs(SmartTransaction tx, out double mixedAnonScore, out double nonMixedAnonScore)
{
// Consolidation in coinjoins is the only type of consolidation that's acceptable,
// because coinjoins are an exception from common input ownership heuristic.
// Calculate weighted average.
- mixedAnonScore = (int)(tx.WalletInputs.Sum(x => x.HdPubKey.AnonymitySet * x.Amount) / tx.WalletInputs.Sum(x => x.Amount));
+ mixedAnonScore = tx.WalletInputs.Sum(x => x.HdPubKey.AnonymitySet * x.Amount.Satoshi) / tx.WalletInputs.Sum(x => x.Amount);
nonMixedAnonScore = tx.WalletInputs.Min(x => x.HdPubKey.AnonymitySet);
}
- private int AnalyzeSelfSpendWalletInputs(HashSet<HdPubKey> distinctWalletInputPubKeys)
+ private double AnalyzeSelfSpendWalletInputs(HashSet<HdPubKey> distinctWalletInputPubKeys)
{
- int startingOutputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet));
+ double startingOutputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet));
foreach (var key in distinctWalletInputPubKeys)
{
key.SetAnonymitySet(startingOutputAnonset);
@@ -93,7 +93,7 @@ public class BlockchainAnalyzer
/// <summary>
/// Estimate input cluster anonymity set size, penalizing input consolidations to accounting for intersection attacks.
/// </summary>
- private int Intersect(IEnumerable<int> anonsets)
+ private double Intersect(IEnumerable<double> anonsets)
{
// Sanity check.
if (!anonsets.Any())
@@ -110,11 +110,11 @@ public class BlockchainAnalyzer
var intersectionAnonset = smallestAnon / Math.Max(1, intersectPenalty);
// The minimum anonymity set size is 1, enforce it when the punishment is very large.
- var normalizedIntersectionAnonset = Math.Max(1, (int)intersectionAnonset);
+ var normalizedIntersectionAnonset = Math.Max(1d, intersectionAnonset);
return normalizedIntersectionAnonset;
}
- private void AnalyzeCoinjoinWalletOutputs(SmartTransaction tx, int startingMixedOutputAnonset, int startingNonMixedOutputAnonset, ISet<HdPubKey> distinctWalletInputPubKeys)
+ private void AnalyzeCoinjoinWalletOutputs(SmartTransaction tx, double startingMixedOutputAnonset, double startingNonMixedOutputAnonset, ISet<HdPubKey> distinctWalletInputPubKeys)
{
var indistinguishableWalletOutputs = tx.WalletOutputs
.GroupBy(x => x.Amount)
@@ -139,14 +139,14 @@ public class BlockchainAnalyzer
var ownEqualOutputCount = indistinguishableWalletOutputs[output.Value];
// Anonset gain cannot be larger than others' input count.
- var anonset = Math.Min(equalOutputCount - ownEqualOutputCount, foreignInputCount);
+ double anonset = Math.Min(equalOutputCount - ownEqualOutputCount, foreignInputCount);
// Picking randomly an output would make our anonset: total/ours.
anonset /= ownEqualOutputCount;
// If no anonset gain achieved on the output, then it's best to assume it's change.
- int startingOutputAnonset;
- if (anonset == 0)
+ double startingOutputAnonset;
+ if (anonset < 1)
{
isWasabi2Cj ??=
tx.Transaction.Inputs.Count >= 50 // 50 was the minimum input count at the beginning of Wasabi 2.
@@ -215,7 +215,7 @@ public class BlockchainAnalyzer
/// <summary>
/// Adjusts the anonset of the inputs to the newly calculated output anonsets.
/// </summary>
- private static void AdjustWalletInputs(SmartTransaction tx, HashSet<HdPubKey> distinctWalletInputPubKeys, int startingOutputAnonset)
+ private static void AdjustWalletInputs(SmartTransaction tx, HashSet<HdPubKey> distinctWalletInputPubKeys, double startingOutputAnonset)
{
// Sanity check.
if (!tx.WalletOutputs.Any())
@@ -233,7 +233,7 @@ public class BlockchainAnalyzer
}
}
- private void AnalyzeSelfSpendWalletOutputs(SmartTransaction tx, int startingOutputAnonset)
+ private void AnalyzeSelfSpendWalletOutputs(SmartTransaction tx, double startingOutputAnonset)
{
foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey))
{

MODIFY
WalletWasabi/Blockchain/Keys/HdPubKey.cs
WalletWasabi/Blockchain/Keys/HdPubKey.cs
@@ -15,7 +15,7 @@ public class HdPubKey : NotifyPropertyChangedBase, IEquatable<HdPubKey>
{
public const int DefaultHighAnonymitySet = int.MaxValue;
- private int _anonymitySet = DefaultHighAnonymitySet;
+ private double _anonymitySet = DefaultHighAnonymitySet;
private Cluster _cluster;
public HdPubKey(PubKey pubKey, KeyPath fullKeyPath, SmartLabel label, KeyState keyState)
@@ -61,7 +61,7 @@ public class HdPubKey : NotifyPropertyChangedBase, IEquatable<HdPubKey>
public HashSet<uint256> OutputAnonSetReasons { get; } = new();
- public int AnonymitySet
+ public double AnonymitySet
{
get => _anonymitySet;
private set => RaiseAndSetIfChanged(ref _anonymitySet, value);
@@ -97,7 +97,7 @@ public class HdPubKey : NotifyPropertyChangedBase, IEquatable<HdPubKey>
private int HashCode { get; }
- public void SetAnonymitySet(int anonset, uint256? outputAnonSetReason = null)
+ public void SetAnonymitySet(double anonset, uint256? outputAnonSetReason = null)
{
if (outputAnonSetReason is not null)
{

MODIFY
WalletWasabi/CoinJoin/Client/Rounds/ClientState.cs
WalletWasabi/CoinJoin/Client/Rounds/ClientState.cs
@@ -242,7 +242,7 @@ public class ClientState
{
// Generating toxic change leads to mass merging so it's better to merge sooner in coinjoin than the user do it himself in a non-CJ.
// The best selection's anonset should not be lowered by this merge.
- int bestAnonset = bestSet.Min(x => x.HdPubKey.AnonymitySet);
+ var bestAnonset = bestSet.Min(x => x.HdPubKey.AnonymitySet);
var bestSum = Money.Satoshis(bestSet.Sum(x => x.Amount));
if (!bestSum.Almost(amountNeeded, Money.Coins(0.0001m)) // Otherwise it wouldn't generate change so consolidation would make no sense.

Fix rounding error in anonscore calculation