Add more coinjoin tests
MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
@@ -60,7 +60,6 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
public void ChangeOutputInheritence()
{
var analyser = BitcoinMock.RandomBlockchainAnalyzer();
- // Also test with inheritence.
var tx = BitcoinMock.RandomSmartTransaction(9, Enumerable.Repeat(Money.Coins(1m), 9), new[] { (Money.Coins(6.2m), 100) }, new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet), (Money.Coins(5m), HdPubKey.DefaultHighAnonymitySet) });
analyser.Analyze(tx);
@@ -72,5 +71,121 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
Assert.Equal(109, active.HdPubKey.AnonymitySet);
Assert.Equal(100, change.HdPubKey.AnonymitySet);
}
+
+ [Fact]
+ public void MultiDenomination()
+ {
+ // Multiple standard denomination outputs results in correct calculation.
+ var analyser = BitcoinMock.RandomBlockchainAnalyzer();
+ var othersOutputs = new[] { 1, 1, 1, 2, 2 };
+ var tx = BitcoinMock.RandomSmartTransaction(
+ 9,
+ othersOutputs.Select(x => Money.Coins(x)),
+ new[] { (Money.Coins(3.2m), 1) },
+ new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet), (Money.Coins(2m), HdPubKey.DefaultHighAnonymitySet) });
+
+ analyser.Analyze(tx);
+
+ var level1 = tx.WalletOutputs.First(x => x.Amount == Money.Coins(1m));
+ var level2 = tx.WalletOutputs.First(x => x.Amount == Money.Coins(2m));
+
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ Assert.Equal(4, level1.HdPubKey.AnonymitySet);
+ Assert.Equal(3, level2.HdPubKey.AnonymitySet);
+ }
+
+ [Fact]
+ public void MultiDenominationInheritence()
+ {
+ // Multiple denominations inherit properly.
+ var analyser = BitcoinMock.RandomBlockchainAnalyzer();
+ var othersOutputs = new[] { 1, 1, 1, 2, 2 };
+ var tx = BitcoinMock.RandomSmartTransaction(
+ 9,
+ othersOutputs.Select(x => Money.Coins(x)),
+ new[] { (Money.Coins(3.2m), 100) },
+ new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet), (Money.Coins(2m), HdPubKey.DefaultHighAnonymitySet) });
+
+ analyser.Analyze(tx);
+
+ var level1 = tx.WalletOutputs.First(x => x.Amount == Money.Coins(1m));
+ var level2 = tx.WalletOutputs.First(x => x.Amount == Money.Coins(2m));
+
+ Assert.Equal(100, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ Assert.Equal(103, level1.HdPubKey.AnonymitySet);
+ Assert.Equal(102, level2.HdPubKey.AnonymitySet);
+ }
+
+ [Fact]
+ public void SelfAnonsetSanityCheck()
+ {
+ // If we have multiple same denomination in the same coinjoin, then don't gain anonset on ourselves.
+ var analyser = BitcoinMock.RandomBlockchainAnalyzer();
+ var othersOutputs = new[] { 1, 1, 1 };
+ var ownOutputs = new[] { 1, 1 };
+ var tx = BitcoinMock.RandomSmartTransaction(
+ 9,
+ othersOutputs.Select(x => Money.Coins(x)),
+ new[] { (Money.Coins(3.2m), 1) },
+ ownOutputs.Select(x => (Money.Coins(x), HdPubKey.DefaultHighAnonymitySet)));
+
+ analyser.Analyze(tx);
+
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ Assert.All(tx.WalletOutputs, x => Assert.Equal(4, x.HdPubKey.AnonymitySet));
+ }
+
+ [Fact]
+ public void InputSanityCheck()
+ {
+ // Anonset can never be larger than the number of inputs.
+ var analyser = BitcoinMock.RandomBlockchainAnalyzer();
+ var tx = BitcoinMock.RandomSmartTransaction(2, Enumerable.Repeat(Money.Coins(1m), 9), new[] { (Money.Coins(1.1m), 1) }, new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet) });
+
+ analyser.Analyze(tx);
+
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ Assert.Equal(3, tx.WalletOutputs.First().HdPubKey.AnonymitySet);
+ }
+
+ [Fact]
+ public void InputSanityBeforeSelfAnonsetSanityCheck()
+ {
+ // Input sanity check is executed before self anonset sanity check.
+
+ var analyser = BitcoinMock.RandomBlockchainAnalyzer();
+ var othersOutputs = new[] { 1, 1, 1 };
+ var ownOutputs = new[] { 1, 1 };
+ var tx = BitcoinMock.RandomSmartTransaction(
+ 2,
+ othersOutputs.Select(x => Money.Coins(x)),
+ new[] { (Money.Coins(3.2m), 1) },
+ ownOutputs.Select(x => (Money.Coins(x), HdPubKey.DefaultHighAnonymitySet)));
+
+ analyser.Analyze(tx);
+
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ // The anonset calculation should be 5, but there's 4 inputs so - 1 = 3.
+ // After than - 1 because 2 out of it is ours.
+ Assert.All(tx.WalletOutputs, x => Assert.Equal(2, x.HdPubKey.AnonymitySet));
+ }
+
+ [Fact]
+ public void InputMergePunishment()
+ {
+ // Input merging results in worse anonset.
+ }
+
+ [Fact]
+ public void InputMergeProportionalPunishment()
+ {
+ // Input merging more coins results in worse anonset.
+ }
+
+ [Fact]
+ public void InputMergePunishmentDependsOnCjSize()
+ {
+ // Input merging in larger coinjoin results in elss punishmnent.
+ }
}
}

MODIFY
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
@@ -50,13 +50,13 @@ namespace WalletWasabi.Blockchain.Analysis
private void AnalyzeCoinjoin(SmartTransaction tx)
{
var ownInputCount = tx.WalletInputs.Count;
+ var inputCount = tx.Transaction.Inputs.Count;
var distinctWalletInputPubKeys = tx.WalletInputs.Select(x => x.HdPubKey).ToHashSet();
var inheritedAnonset = 0;
if (ownInputCount > 0)
{
// If we provided inputs to the transaction.
// Reusing pubkey on the input side is good, the punishment happened already.
- var inputCount = tx.Transaction.Inputs.Count;
var distinctWalletInputPubKeyCount = distinctWalletInputPubKeys.Count;
var pubKeyReuseCount = ownInputCount - distinctWalletInputPubKeyCount;
var privacyBonus = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet), (double)distinctWalletInputPubKeyCount / (inputCount - pubKeyReuseCount));
@@ -73,11 +73,17 @@ namespace WalletWasabi.Blockchain.Analysis
}
}
- foreach (var newCoin in tx.WalletOutputs)
+ foreach (var newCoin in tx.WalletOutputs.ToArray())
{
// Get the anonymity set of i-th output in the transaction.
var anonset = inheritedAnonset;
- anonset += tx.Transaction.GetAnonymitySet(newCoin.Index);
+ // Calculating gained anonimity shall be limited by the number of inputs.
+ // Although one person may create many equal outputs and it would seem like we successfully get lost
+ // within this elephant, it's not the case as the elephant is more likely to ruin its own privacy, so let's not inflate anonsets for this.
+ anonset += Math.Min(inputCount, tx.Transaction.GetAnonymitySet(newCoin.Index));
+
+ // Don't create many anonset if we've provided a lot of equal outputs.
+ anonset -= tx.WalletOutputs.Count(x => x.Amount == newCoin.Amount) - 1;
HdPubKey hdPubKey = newCoin.HdPubKey;
if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)

Add more coinjoin tests