Group by script
MODIFY
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
@@ -24,6 +24,9 @@ namespace WalletWasabi.Tests.Helpers
}
public static SmartTransaction CreateSmartTransaction(int othersInputCount, IEnumerable<Money> othersOutputs, IEnumerable<(Money value, int anonset, HdPubKey hdpk)> ownInputs, IEnumerable<(Money value, int anonset, HdPubKey hdpk)> ownOutputs)
+ => CreateSmartTransaction(othersInputCount, othersOutputs.Select(x => new TxOut(x, new Key())), ownInputs, ownOutputs);
+
+ public static SmartTransaction CreateSmartTransaction(int othersInputCount, IEnumerable<TxOut> othersOutputs, IEnumerable<(Money value, int anonset, HdPubKey hdpk)> ownInputs, IEnumerable<(Money value, int anonset, HdPubKey hdpk)> ownOutputs)
{
var tx = Transaction.Create(Network.Main);
var walletInputs = new HashSet<SmartCoin>();
@@ -34,17 +37,17 @@ namespace WalletWasabi.Tests.Helpers
tx.Inputs.Add(CreateOutPoint());
}
var idx = (uint)othersInputCount - 1;
- foreach (var txo in ownInputs)
+ foreach (var (value, anonset, hdpk) in ownInputs)
{
idx++;
- var sc = CreateSmartCoin(txo.hdpk, txo.value, idx, anonymitySet: txo.anonset);
+ var sc = CreateSmartCoin(hdpk, value, idx, anonymitySet: anonset);
tx.Inputs.Add(sc.OutPoint);
walletInputs.Add(sc);
}
- foreach (var val in othersOutputs)
+ foreach (var output in othersOutputs)
{
- tx.Outputs.Add(new TxOut(val, new Key()));
+ tx.Outputs.Add(output);
}
idx = (uint)othersOutputs.Count() - 1;
foreach (var txo in ownOutputs)

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
@@ -151,5 +151,36 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
// 100 is the input anonset, so outputs shouldn't go lower than that.
Assert.All(tx.WalletOutputs.Select(x => x.HdPubKey.AnonymitySet), x => Assert.True(x >= 100));
}
+
+ [Fact]
+ public void OutputSideAddressReuseBySomeoneElse()
+ {
+ // If there's reuse in output side by another participant, then we should not gain anonsets by them.
+ // https://github.com/zkSNACKs/WalletWasabi/pull/4724/commits/6f5893ca57e35eadb6e20f164bdf0696bb14eea1#r530847724
+ var analyser = ServiceFactory.CreateBlockchainAnalyzer();
+ var km = ServiceFactory.CreateKeyManager();
+ var equalOutputAmount = Money.Coins(1m);
+ var reusedTxOut = new TxOut(equalOutputAmount, new Key());
+ var tx = BitcoinFactory.CreateSmartTransaction(
+ 9,
+ Repeat(() => new TxOut(equalOutputAmount, new Key()), 7).Concat(new[] { reusedTxOut, reusedTxOut }),
+ new[] { (Money.Coins(1.1m), 1, BitcoinFactory.CreateHdPubKey(km)) },
+ new[] { (equalOutputAmount, HdPubKey.DefaultHighAnonymitySet, BitcoinFactory.CreateHdPubKey(km)) });
+
+ analyser.Analyze(tx);
+
+ Assert.All(tx.WalletInputs, x => Assert.Equal(1, x.HdPubKey.AnonymitySet));
+
+ // Normally it'd be 10, but because of reuse it should be only 8.
+ Assert.Equal(8, tx.WalletOutputs.First().HdPubKey.AnonymitySet);
+ }
+
+ public IEnumerable<TResult> Repeat<TResult>(Func<TResult> action, int count)
+ {
+ for (int i = 0; i < count; i++)
+ {
+ yield return action();
+ }
+ }
}
}

MODIFY
WalletWasabi/Extensions/NBitcoinExtensions.cs
WalletWasabi/Extensions/NBitcoinExtensions.cs
@@ -131,6 +131,8 @@ namespace NBitcoin
var inputCount = me.Inputs.Count;
var indistinguishableOutputs = me.Outputs
+ .GroupBy(x => x.ScriptPubKey)
+ .ToDictionary(x => x.Key, y => y.Sum(z => z.Value))
.GroupBy(x => x.Value)
.ToDictionary(x => x.Key, y => y.Count());

Group by script