Fix anonymity set bug
MODIFY
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
WalletWasabi.Tests/Helpers/BitcoinFactory.cs
@@ -87,7 +87,7 @@ namespace WalletWasabi.Tests.Helpers
var tx = Transaction.Create(Network.Main);
tx.Outputs.Add(new TxOut(amount, pubKey.P2wpkhScript));
var stx = new SmartTransaction(tx, height);
- pubKey.AnonymitySet = anonymitySet;
+ pubKey.SetAnonymitySet(anonymitySet, stx.GetHash());
return new SmartCoin(stx, index, pubKey);
}

MODIFY
WalletWasabi.Tests/RegressionTests/CoinJoinTests.cs
WalletWasabi.Tests/RegressionTests/CoinJoinTests.cs
@@ -1113,7 +1113,7 @@ namespace WalletWasabi.Tests.RegressionTests
var bechCoin = tx.Outputs.GetCoins(bech.ScriptPubKey).Single();
var smartCoin = new SmartCoin(stx, bechCoin.Outpoint.N, key);
- key.AnonymitySet = tx.GetAnonymitySet(bechCoin.Outpoint.N);
+ key.SetAnonymitySet(tx.GetAnonymitySet(bechCoin.Outpoint.N), tx.GetHash());
var chaumianClient = new CoinJoinClient(synchronizer, rpc.Network, keyManager, new Kitchen());
@@ -1230,10 +1230,10 @@ namespace WalletWasabi.Tests.RegressionTests
var smartCoin2 = new SmartCoin(stx2, bech2Coin.Outpoint.N, key2);
var smartCoin3 = new SmartCoin(stx3, bech3Coin.Outpoint.N, key3);
var smartCoin4 = new SmartCoin(stx4, bech4Coin.Outpoint.N, key4);
- key1.AnonymitySet = tx1.GetAnonymitySet(bech1Coin.Outpoint.N);
- key2.AnonymitySet = tx2.GetAnonymitySet(bech2Coin.Outpoint.N);
- key3.AnonymitySet = tx3.GetAnonymitySet(bech3Coin.Outpoint.N);
- key4.AnonymitySet = tx4.GetAnonymitySet(bech4Coin.Outpoint.N);
+ key1.SetAnonymitySet(tx1.GetAnonymitySet(bech1Coin.Outpoint.N), tx1.GetHash());
+ key2.SetAnonymitySet(tx2.GetAnonymitySet(bech2Coin.Outpoint.N), tx2.GetHash());
+ key3.SetAnonymitySet(tx3.GetAnonymitySet(bech3Coin.Outpoint.N), tx3.GetHash());
+ key4.SetAnonymitySet(tx4.GetAnonymitySet(bech4Coin.Outpoint.N), tx4.GetHash());
var chaumianClient1 = new CoinJoinClient(synchronizer, rpc.Network, keyManager, new Kitchen());
var chaumianClient2 = new CoinJoinClient(synchronizer, rpc.Network, keyManager, new Kitchen());
@@ -1261,7 +1261,7 @@ namespace WalletWasabi.Tests.RegressionTests
randomTx.Outputs.Add(new TxOut(Money.Coins(3m), randomKey.P2wpkhScript));
var randomStx = new SmartTransaction(randomTx, Height.Mempool);
await chaumianClient1.DequeueCoinsFromMixAsync(new SmartCoin(randomStx, 0, randomKey), DequeueReason.UserRequested);
- randomKey.AnonymitySet = 1;
+ randomKey.SetAnonymitySet(1, randomStx.GetHash());
Assert.True(2 == (await chaumianClient1.QueueCoinsToMixAsync(password, smartCoin1, smartCoin2)).Count());
await chaumianClient1.DequeueCoinsFromMixAsync(smartCoin1, DequeueReason.UserRequested);

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/CoinJoinAnonscoreTests.cs
@@ -27,6 +27,34 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
Assert.Equal(10, tx.WalletOutputs.First().HdPubKey.AnonymitySet);
}
+ [Fact]
+ public void DoubleProcessing()
+ {
+ var analyser = ServiceFactory.CreateBlockchainAnalyzer();
+ var tx = BitcoinFactory.CreateSmartTransaction(9, Enumerable.Repeat(Money.Coins(1m), 9), new[] { (Money.Coins(1.1m), 1) }, new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet) });
+ analyser.Analyze(tx);
+ analyser.Analyze(tx);
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ // 10 participants, 1 is you, your anonset is 10.
+ Assert.Equal(10, tx.WalletOutputs.First().HdPubKey.AnonymitySet);
+ }
+
+ [Fact]
+ public void OtherWalletChangesThings()
+ {
+ var analyser = ServiceFactory.CreateBlockchainAnalyzer();
+ var tx = BitcoinFactory.CreateSmartTransaction(9, Enumerable.Repeat(Money.Coins(1m), 8), new[] { (Money.Coins(1.1m), 1) }, new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet), (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet) });
+ var sc = tx.WalletOutputs.First();
+ tx.WalletOutputs.Remove(sc);
+ analyser.Analyze(tx);
+ tx.WalletOutputs.Add(sc);
+ analyser.Analyze(tx);
+ Assert.Equal(1, tx.WalletInputs.First().HdPubKey.AnonymitySet);
+ // 10 participants, 2 is you, your anonset is 10/2 = 5.
+ Assert.Equal(5, tx.WalletOutputs.First().HdPubKey.AnonymitySet);
+ Assert.Equal(5, tx.WalletOutputs.Skip(1).First().HdPubKey.AnonymitySet);
+ }
+
[Fact]
public void Inheritance()
{

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/NormalSpendAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/NormalSpendAnonScoreTests.cs
@@ -20,7 +20,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 1, 1, 0);
var coin = Assert.Single(tx.WalletInputs);
var key = coin.HdPubKey;
- key.AnonymitySet = 3;
+ key.SetAnonymitySet(3, tx.GetHash());
analyser.Analyze(tx);
@@ -38,7 +38,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.AnonymitySet = 3;
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
}
analyser.Analyze(tx);
@@ -56,7 +56,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 3, 1, 0);
var coin = Assert.Single(tx.WalletInputs);
var key = coin.HdPubKey;
- key.AnonymitySet = 3;
+ key.SetAnonymitySet(3, tx.GetHash());
analyser.Analyze(tx);
@@ -74,7 +74,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.AnonymitySet = 3;
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
}
analyser.Analyze(tx);
@@ -94,7 +94,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.AnonymitySet = 3;
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
}
analyser.Analyze(tx);
@@ -113,7 +113,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.AnonymitySet = 3;
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
}
analyser.Analyze(tx);
@@ -132,7 +132,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.AnonymitySet = 3;
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
}
analyser.Analyze(tx);

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/PubKeyReuseAnonScoreTests.cs
@@ -25,7 +25,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet, reuse) });
// Make the reused key anonymity set something smaller than 109 (which should be the final anonymity set)
- reuse.AnonymitySet = 30;
+ reuse.SetAnonymitySet(30, uint256.One);
analyser.Analyze(tx);
@@ -47,7 +47,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
new[] { (Money.Coins(1.1m), 100, BitcoinFactory.CreateHdPubKey(km)) },
new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet, reuse) });
- reuse.AnonymitySet = 30;
+ reuse.SetAnonymitySet(30, uint256.One);
analyser.Analyze(tx);
@@ -144,7 +144,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
new[] { (Money.Coins(1.1m), 100, BitcoinFactory.CreateHdPubKey(ServiceFactory.CreateKeyManager())) },
new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet, reuse) });
- reuse.AnonymitySet = 30;
+ reuse.SetAnonymitySet(30, uint256.One);
analyser.Analyze(tx);

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/SelfSpendAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/SelfSpendAnonScoreTests.cs
+using NBitcoin;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -19,7 +20,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 1, 1);
var coin = Assert.Single(tx.WalletInputs);
var key = coin.HdPubKey;
- key.AnonymitySet = 3;
+ key.SetAnonymitySet(3, tx.GetHash());
analyser.Analyze(tx);
@@ -37,7 +38,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 1, 3);
var coin = Assert.Single(tx.WalletInputs);
var key = coin.HdPubKey;
- key.AnonymitySet = 3;
+ key.SetAnonymitySet(3, tx.GetHash());
analyser.Analyze(tx);
@@ -56,10 +57,10 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 3, 1);
var smallestAnonset = 3;
- tx.WalletInputs.First().HdPubKey.AnonymitySet = smallestAnonset;
+ tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One);
foreach (var coin in tx.WalletInputs.Skip(1))
{
- coin.HdPubKey.AnonymitySet = 100;
+ coin.HdPubKey.SetAnonymitySet(100, tx.GetHash());
}
analyser.Analyze(tx);
@@ -76,10 +77,10 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 3, 3);
var smallestAnonset = 3;
- tx.WalletInputs.First().HdPubKey.AnonymitySet = smallestAnonset;
+ tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One);
foreach (var coin in tx.WalletInputs.Skip(1))
{
- coin.HdPubKey.AnonymitySet = 100;
+ coin.HdPubKey.SetAnonymitySet(100, tx.GetHash());
}
analyser.Analyze(tx);

MODIFY
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
@@ -72,7 +72,7 @@ namespace WalletWasabi.Blockchain.Analysis
foreach (var key in distinctWalletInputPubKeys)
{
- key.AnonymitySet = newInputAnonset;
+ key.SetAnonymitySet(newInputAnonset, tx.GetHash());
}
}
@@ -128,27 +128,36 @@ namespace WalletWasabi.Blockchain.Analysis
anonset += newInputAnonset - 1;
HdPubKey hdPubKey = newCoin.HdPubKey;
+ uint256 txid = tx.GetHash();
if (hdPubKey.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
{
// If the new coin's HD pubkey haven't been used yet
// then its anonset haven't been set yet.
// In that case the acquired anonset does not have to be intersected with the default anonset,
// so this coin gets the aquired anonset.
- hdPubKey.AnonymitySet = anonset;
+ hdPubKey.SetAnonymitySet(anonset, txid);
}
else if (distinctWalletInputPubKeys.Contains(hdPubKey))
{
// If it's a reuse of an input's pubkey, then intersection punishment is senseless.
- hdPubKey.AnonymitySet = newInputAnonset;
+ hdPubKey.SetAnonymitySet(newInputAnonset, txid);
}
else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey))
{
// If it's a reuse of another output' pubkey, then intersection punishment can only go as low as the inherited anonset.
- hdPubKey.AnonymitySet = Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1));
+ hdPubKey.SetAnonymitySet(Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1)), txid);
+ }
+ else if (hdPubKey.AnonymitySetReasons.Contains(txid))
+ {
+ // If we already processed this transaction for this script
+ // then we'll go with normal processing, it's not an address reuse,
+ // it's just we're processing the transaction twice.
+ hdPubKey.SetAnonymitySet(anonset, txid);
}
else
{
- hdPubKey.AnonymitySet = Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1);
+ // It's address reuse.
+ hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid);
}
}
}
@@ -169,7 +178,7 @@ namespace WalletWasabi.Blockchain.Analysis
{
foreach (var key in distinctWalletInputPubKeys)
{
- key.AnonymitySet = smallestOutputAnonset;
+ key.SetAnonymitySet(smallestOutputAnonset, tx.GetHash());
}
}
}
@@ -180,11 +189,11 @@ namespace WalletWasabi.Blockchain.Analysis
{
if (key.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
{
- key.AnonymitySet = newInputAnonset;
+ key.SetAnonymitySet(newInputAnonset, tx.GetHash());
}
else
{
- key.AnonymitySet = Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1);
+ key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash());
}
}
}
@@ -194,7 +203,7 @@ namespace WalletWasabi.Blockchain.Analysis
// No matter how much anonymity a user would had gained in a tx, if the money comes from outside, then make anonset 1.
foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey))
{
- key.AnonymitySet = 1;
+ key.SetAnonymitySet(1, tx.GetHash());
}
}
@@ -209,7 +218,7 @@ namespace WalletWasabi.Blockchain.Analysis
// probably would just assume it's ours and go on with its life.
foreach (var key in tx.WalletInputs.Concat(tx.WalletOutputs).Select(x => x.HdPubKey))
{
- key.AnonymitySet = 1;
+ key.SetAnonymitySet(1, tx.GetHash());
}
}

MODIFY
WalletWasabi/Blockchain/Keys/HdPubKey.cs
WalletWasabi/Blockchain/Keys/HdPubKey.cs
@@ -59,10 +59,12 @@ namespace WalletWasabi.Blockchain.Keys
set => RaiseAndSetIfChanged(ref _cluster, value);
}
+ public HashSet<uint256> AnonymitySetReasons { get; } = new();
+
public int AnonymitySet
{
get => _anonymitySet;
- set => RaiseAndSetIfChanged(ref _anonymitySet, value);
+ private set => RaiseAndSetIfChanged(ref _anonymitySet, value);
}
public HashSet<SmartCoin> Coins { get; } = new HashSet<SmartCoin>();
@@ -95,6 +97,12 @@ namespace WalletWasabi.Blockchain.Keys
private int HashCode { get; }
+ public void SetAnonymitySet(int anonset, uint256 reason)
+ {
+ AnonymitySet = anonset;
+ AnonymitySetReasons.Add(reason);
+ }
+
public void SetLabel(SmartLabel label, KeyManager? kmToFile = null)
{
label ??= SmartLabel.Empty;

Fix anonymity set bug