Add test for clustering
MODIFY
WalletWasabi.Tests/UnitTests/TransactionProcessorTests.cs
WalletWasabi.Tests/UnitTests/TransactionProcessorTests.cs
@@ -390,6 +390,87 @@ namespace WalletWasabi.Tests.UnitTests
}
+ [Fact]
+ public async Task SameScriptClusteringAsync()
+ {
+ // --tx0---> (A) --+
+ // |
+ // --tx1---> (B) --+---tx3-+--> (pay to D - myself - cluster (D, A, B, C))
+ // |
+ // --tx2---> (C) --+
+ //
+ // --tx4---> (D - reuse - cluster (D, A, B, C))
+ var transactionProcessor = await CreateTransactionProcessorAsync();
+ var key = transactionProcessor.NewKey("A");
+ var tx0 = CreateCreditingTransaction(key.P2wpkhScript, Money.Coins(1.0m));
+ transactionProcessor.Process(tx0);
+
+ key = transactionProcessor.NewKey("B");
+ var tx1 = CreateCreditingTransaction(key.P2wpkhScript, Money.Coins(1.0m));
+ transactionProcessor.Process(tx1);
+
+ key = transactionProcessor.NewKey("C");
+ var tx2 = CreateCreditingTransaction(key.P2wpkhScript, Money.Coins(1.0m));
+ transactionProcessor.Process(tx2);
+
+ var scoinA = Assert.Single(transactionProcessor.Coins, c => c.Clusters.Labels == "A");
+ var scoinB = Assert.Single(transactionProcessor.Coins, c => c.Clusters.Labels == "B");
+ var scoinC = Assert.Single(transactionProcessor.Coins, c => c.Clusters.Labels == "C");
+
+ var myself = transactionProcessor.NewKey("D").P2wpkhScript;
+ var changeScript = transactionProcessor.NewKey("").P2wpkhScript;
+ var coins = new[] { scoinA.GetCoin(), scoinB.GetCoin(), scoinC.GetCoin() };
+ var tx3 = CreateSpendingTransaction(coins, myself, changeScript);
+ transactionProcessor.Process(tx3);
+
+ var paymentCoin = Assert.Single(transactionProcessor.Coins, c => c.ScriptPubKey == myself);
+ Assert.Equal("D, A, B, C", paymentCoin.Clusters.Labels);
+
+ var tx4 = CreateCreditingTransaction(myself, Money.Coins(7.0m));
+ transactionProcessor.Process(tx4);
+ Assert.Equal(2, transactionProcessor.Coins.Count(c => c.ScriptPubKey == myself));
+ var newPaymentCoin = Assert.Single(transactionProcessor.Coins, c => c.Amount == Money.Coins(7.0m));
+ Assert.Equal("D, A, B, C", newPaymentCoin.Clusters.Labels);
+ }
+
+ [Fact]
+ public async Task EnoughAnonymitySetClusteringAsync()
+ {
+ // --tx0---> (A) --tx1---> (empty)
+ //
+ // Note: tx1 is a coinjoin transaction
+
+ var transactionProcessor = await CreateTransactionProcessorAsync();
+ var key = transactionProcessor.NewKey("A");
+ var tx0 = CreateCreditingTransaction(key.P2wpkhScript, Money.Coins(1.0m));
+ transactionProcessor.Process(tx0);
+
+ var receivedCoin = Assert.Single(transactionProcessor.Coins);
+
+ // build coinjoin transaction
+ var cjtx = Network.RegTest.CreateTransaction();
+
+ for (var i = 0; i < 100; i++)
+ {
+ cjtx.Inputs.Add(GetRandomOutPoint(), Script.Empty, WitScript.Empty);
+ }
+ for (var i = 0; i < 100; i++)
+ {
+ cjtx.Outputs.Add(Money.Coins(0.1m), Script.Empty);
+ }
+ cjtx.Inputs.Add(receivedCoin.GetOutPoint(), Script.Empty, WitScript.Empty);
+ cjtx.Outputs.Add(Money.Coins(0.1m), transactionProcessor.NewKey("").P2wpkhScript);
+ cjtx.Outputs.Add(Money.Coins(0.9m), transactionProcessor.NewKey("").P2wpkhScript);
+ var tx1 = new SmartTransaction(cjtx, Height.Mempool);
+
+ transactionProcessor.Process(tx1);
+ var anonymousCoin = Assert.Single(transactionProcessor.Coins, c => c.Amount == Money.Coins(0.1m));
+ var changeCoin = Assert.Single(transactionProcessor.Coins, c => c.Amount == Money.Coins(0.9m));
+
+ Assert.Empty(anonymousCoin.Clusters.Labels);
+ Assert.NotEmpty(changeCoin.Clusters.Labels);
+ }
+
private async Task<TransactionProcessor> CreateTransactionProcessorAsync([CallerMemberName] string callerName = "")
{
var keyManager = KeyManager.CreateNew(out _, "password");

MODIFY
WalletWasabi/Models/CoinsRegistry.cs
WalletWasabi/Models/CoinsRegistry.cs
@@ -13,15 +13,15 @@ namespace WalletWasabi.Models
private bool InvalidateSnapshot { get; set; }
private object Lock { get; set; }
private HashSet<SmartCoin> SpentCoins { get; }
- // private Dictionary<SmartCoin, List<SmartCoin>> Clusters { get; }
+ private Dictionary<Script, Cluster> ClustersByScriptPubKey { get; }
public CoinsRegistry()
{
Coins = new HashSet<SmartCoin>();
SpentCoins = new HashSet<SmartCoin>();
- // Clusters = new Dictionary<SmartCoin, List<SmartCoin>>();
LatestCoinsSnapshot = new HashSet<SmartCoin>();
InvalidateSnapshot = false;
+ ClustersByScriptPubKey = new Dictionary<Script, Cluster>();
Lock = new object();
}
@@ -53,7 +53,10 @@ namespace WalletWasabi.Models
added = Coins.Add(coin);
if (added)
{
- // Clusters.Add(coin, new List<SmartCoin>() { coin });
+ if (ClustersByScriptPubKey.TryGetValue(coin.ScriptPubKey, out var cluster))
+ {
+ coin.Clusters = cluster;
+ }
InvalidateSnapshot = true;
}
}
@@ -86,9 +89,13 @@ namespace WalletWasabi.Models
SpentCoins.Add(spentCoin);
var createdCoins = Coins.Where(x => x.TransactionId == spentCoin.SpenderTransactionId);
foreach (var newCoin in createdCoins)
+ {
+ if (newCoin.AnonymitySet < 50)
{
spentCoin.Clusters.Merge(newCoin.Clusters);
newCoin.Clusters = spentCoin.Clusters;
+ ClustersByScriptPubKey.AddOrReplace(newCoin.ScriptPubKey, newCoin.Clusters);
+ }
}
}
InvalidateSnapshot = true;

MODIFY
WalletWasabi/Models/TransactionBuilding/Cluster.cs
WalletWasabi/Models/TransactionBuilding/Cluster.cs
@@ -26,15 +26,21 @@ namespace WalletWasabi.Models
private set => RaiseAndSetIfChanged(ref _labels, value);
}
- internal void Merge(Cluster clusters)
+ public void Merge(Cluster clusters)
+ {
+ Merge(clusters.Coins);
+ }
+
+ public void Merge(IEnumerable<SmartCoin> coins)
{
var insertPosition = 0;
- foreach (var coin in clusters.Coins.ToList())
+ foreach (var coin in coins.ToList())
{
if (!Coins.Contains(coin))
{
Coins.Insert(insertPosition++, coin);
}
+ coin.Clusters = this;
}
Labels = string.Join(", ", Coins.SelectMany(x => x.Label.Labels).Distinct(StringComparer.OrdinalIgnoreCase));
}

Add test for clustering