Fix anon set calculation at address reuse
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.SetAnonymitySet(anonymitySet, stx.GetHash());
+ pubKey.SetAnonymitySet(anonymitySet, stx.GetHash(), true);
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.SetAnonymitySet(tx.GetAnonymitySet(bechCoin.Outpoint.N), tx.GetHash());
+ key.SetAnonymitySet(tx.GetAnonymitySet(bechCoin.Outpoint.N), tx.GetHash(), true);
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.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());
+ key1.SetAnonymitySet(tx1.GetAnonymitySet(bech1Coin.Outpoint.N), tx1.GetHash(), true);
+ key2.SetAnonymitySet(tx2.GetAnonymitySet(bech2Coin.Outpoint.N), tx2.GetHash(), true);
+ key3.SetAnonymitySet(tx3.GetAnonymitySet(bech3Coin.Outpoint.N), tx3.GetHash(), true);
+ key4.SetAnonymitySet(tx4.GetAnonymitySet(bech4Coin.Outpoint.N), tx4.GetHash(), true);
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.SetAnonymitySet(1, randomStx.GetHash());
+ randomKey.SetAnonymitySet(1, randomStx.GetHash(), true);
Assert.True(2 == (await chaumianClient1.QueueCoinsToMixAsync(password, smartCoin1, smartCoin2)).Count());
await chaumianClient1.DequeueCoinsFromMixAsync(smartCoin1, DequeueReason.UserRequested);

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.SetAnonymitySet(3, tx.GetHash());
+ key.SetAnonymitySet(3, tx.GetHash(), false);
analyser.Analyze(tx);
@@ -38,7 +38,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash(), false);
}
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.SetAnonymitySet(3, tx.GetHash());
+ key.SetAnonymitySet(3, tx.GetHash(), false);
analyser.Analyze(tx);
@@ -74,7 +74,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash(), false);
}
analyser.Analyze(tx);
@@ -94,7 +94,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash(), false);
}
analyser.Analyze(tx);
@@ -113,7 +113,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash(), false);
}
analyser.Analyze(tx);
@@ -132,7 +132,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
foreach (var coin in tx.WalletInputs)
{
- coin.HdPubKey.SetAnonymitySet(3, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(3, tx.GetHash(), false);
}
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.SetAnonymitySet(30, uint256.One);
+ reuse.SetAnonymitySet(30, uint256.One, true);
analyser.Analyze(tx);
@@ -35,6 +35,45 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
Assert.True(tx.WalletOutputs.First().HdPubKey.AnonymitySet < 30);
}
+ [Fact]
+ public void AddressReusePunishmentProcessTwice()
+ {
+ // If there's reuse in input and output side, then output side didn't gain, nor lose anonymity.
+ var analyser = ServiceFactory.CreateBlockchainAnalyzer();
+ var km = ServiceFactory.CreateKeyManager();
+ var reuse = BitcoinFactory.CreateHdPubKey(km);
+ var tx = BitcoinFactory.CreateSmartTransaction(
+ 9,
+ Enumerable.Repeat(Money.Coins(1m), 9),
+ new[] { (Money.Coins(1.1m), 100, BitcoinFactory.CreateHdPubKey(km)) },
+ 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.SetAnonymitySet(30, uint256.One, true);
+
+ analyser.Analyze(tx);
+ var inputAnonsets = tx.WalletInputs.Select(x => x.HdPubKey.AnonymitySet).ToArray();
+ var outputAnonsets = tx.WalletOutputs.Select(x => x.HdPubKey.AnonymitySet).ToArray();
+
+ analyser.Analyze(tx);
+ var newInputAnonsets = tx.WalletInputs.Select(x => x.HdPubKey.AnonymitySet).ToArray();
+ var newOutputAnonsets = tx.WalletOutputs.Select(x => x.HdPubKey.AnonymitySet).ToArray();
+
+ // Anonsets should not change.
+ for (int i = 0; i < inputAnonsets.Length; i++)
+ {
+ var anonset = inputAnonsets[i];
+ var newAnonset = newInputAnonsets[i];
+ Assert.Equal(anonset, newAnonset);
+ }
+ for (int i = 0; i < outputAnonsets.Length; i++)
+ {
+ var anonset = outputAnonsets[i];
+ var newAnonset = newOutputAnonsets[i];
+ Assert.Equal(anonset, newAnonset);
+ }
+ }
+
[Fact]
public void SelfSpendReuse()
{
@@ -47,7 +86,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
new[] { (Money.Coins(1.1m), 100, BitcoinFactory.CreateHdPubKey(km)) },
new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet, reuse) });
- reuse.SetAnonymitySet(30, uint256.One);
+ reuse.SetAnonymitySet(30, uint256.One, true);
analyser.Analyze(tx);
@@ -144,7 +183,7 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
new[] { (Money.Coins(1.1m), 100, BitcoinFactory.CreateHdPubKey(ServiceFactory.CreateKeyManager())) },
new[] { (Money.Coins(1m), HdPubKey.DefaultHighAnonymitySet, reuse) });
- reuse.SetAnonymitySet(30, uint256.One);
+ reuse.SetAnonymitySet(30, uint256.One, true);
analyser.Analyze(tx);

MODIFY
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/SelfSpendAnonScoreTests.cs
WalletWasabi.Tests/UnitTests/BlockchainAnalysis/SelfSpendAnonScoreTests.cs
@@ -20,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.SetAnonymitySet(3, tx.GetHash());
+ key.SetAnonymitySet(3, tx.GetHash(), false);
analyser.Analyze(tx);
@@ -38,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.SetAnonymitySet(3, tx.GetHash());
+ key.SetAnonymitySet(3, tx.GetHash(), false);
analyser.Analyze(tx);
@@ -57,10 +57,10 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 3, 1);
var smallestAnonset = 3;
- tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One);
+ tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One, true);
foreach (var coin in tx.WalletInputs.Skip(1))
{
- coin.HdPubKey.SetAnonymitySet(100, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(100, tx.GetHash(), false);
}
analyser.Analyze(tx);
@@ -77,10 +77,10 @@ namespace WalletWasabi.Tests.UnitTests.BlockchainAnalysis
var tx = BitcoinFactory.CreateSmartTransaction(0, 0, 3, 3);
var smallestAnonset = 3;
- tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One);
+ tx.WalletInputs.First().HdPubKey.SetAnonymitySet(smallestAnonset, uint256.One, true);
foreach (var coin in tx.WalletInputs.Skip(1))
{
- coin.HdPubKey.SetAnonymitySet(100, tx.GetHash());
+ coin.HdPubKey.SetAnonymitySet(100, tx.GetHash(), false);
}
analyser.Analyze(tx);

MODIFY
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
WalletWasabi.Tests/UnitTests/Transactions/TransactionProcessorTests.cs
@@ -1273,15 +1273,15 @@ namespace WalletWasabi.Tests.UnitTests.Transactions
var notYetPrivateCoin = transactionProcessor.NewKey("");
transactionProcessor.Process(CreateCreditingTransaction(notYetPrivateCoin.P2wpkhScript, Money.Coins(1.0m)));
- notYetPrivateCoin.SetAnonymitySet(targetAnonSet - 1, 0);
+ notYetPrivateCoin.SetAnonymitySet(targetAnonSet - 1, 0, true);
var privateCoin1 = transactionProcessor.NewKey("");
transactionProcessor.Process(CreateCreditingTransaction(privateCoin1.P2wpkhScript, Money.Coins(1.0m)));
- privateCoin1.SetAnonymitySet(targetAnonSet, 0);
+ privateCoin1.SetAnonymitySet(targetAnonSet, 0, true);
var privateCoin2 = transactionProcessor.NewKey("");
transactionProcessor.Process(CreateCreditingTransaction(privateCoin2.P2wpkhScript, Money.Coins(1.0m)));
- privateCoin2.SetAnonymitySet(targetAnonSet, 0);
+ privateCoin2.SetAnonymitySet(targetAnonSet, 0, true);
var pockets = CoinPocketHelper.GetPockets(transactionProcessor.Coins, targetAnonSet);
var aPocket = pockets.Single(x => x.SmartLabel == "A");

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.SetAnonymitySet(newInputAnonset, tx.GetHash());
+ key.SetAnonymitySet(newInputAnonset, tx.GetHash(), false);
}
}
@@ -135,29 +135,33 @@ namespace WalletWasabi.Blockchain.Analysis
// 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.SetAnonymitySet(anonset, txid);
+ hdPubKey.SetAnonymitySet(anonset, txid, true);
}
else if (distinctWalletInputPubKeys.Contains(hdPubKey))
{
// If it's a reuse of an input's pubkey, then intersection punishment is senseless.
- hdPubKey.SetAnonymitySet(newInputAnonset, txid);
+ hdPubKey.SetAnonymitySet(newInputAnonset, txid, true);
}
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.SetAnonymitySet(Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1)), txid);
+ hdPubKey.SetAnonymitySet(Math.Max(newInputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1)), txid, true);
}
- else if (hdPubKey.AnonymitySetReasons.Contains(txid))
+ else if (hdPubKey.OutputAnonSetReasons.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);
+ // then we'll go with normal processing.
+ // It may be a duplicated processing or new information arrived (like other wallet loaded)
+ if (hdPubKey.OutputAnonSetReasons.Count == 1)
+ {
+ // If there are more anonsets already then it's address reuse so leave it alone.
+ hdPubKey.SetAnonymitySet(anonset, txid, true);
+ }
}
else
{
// It's address reuse.
- hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid);
+ hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid, true);
}
}
}
@@ -178,7 +182,7 @@ namespace WalletWasabi.Blockchain.Analysis
{
foreach (var key in distinctWalletInputPubKeys)
{
- key.SetAnonymitySet(smallestOutputAnonset, tx.GetHash());
+ key.SetAnonymitySet(smallestOutputAnonset, tx.GetHash(), false);
}
}
}
@@ -189,11 +193,11 @@ namespace WalletWasabi.Blockchain.Analysis
{
if (key.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
{
- key.SetAnonymitySet(newInputAnonset, tx.GetHash());
+ key.SetAnonymitySet(newInputAnonset, tx.GetHash(), true);
}
else
{
- key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash());
+ key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash(), true);
}
}
}
@@ -203,7 +207,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.SetAnonymitySet(1, tx.GetHash());
+ key.SetAnonymitySet(1, tx.GetHash(), true);
}
}
@@ -218,7 +222,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.SetAnonymitySet(1, tx.GetHash());
+ key.SetAnonymitySet(1, tx.GetHash(), false);
}
}

MODIFY
WalletWasabi/Blockchain/Keys/HdPubKey.cs
WalletWasabi/Blockchain/Keys/HdPubKey.cs
@@ -59,7 +59,7 @@ namespace WalletWasabi.Blockchain.Keys
set => RaiseAndSetIfChanged(ref _cluster, value);
}
- public HashSet<uint256> AnonymitySetReasons { get; } = new();
+ public HashSet<uint256> OutputAnonSetReasons { get; set; } = new();
public int AnonymitySet
{
@@ -97,10 +97,14 @@ namespace WalletWasabi.Blockchain.Keys
private int HashCode { get; }
- public void SetAnonymitySet(int anonset, uint256 reason)
+ public void SetAnonymitySet(int anonset, uint256 reason, bool isOutput)
{
+ if (isOutput)
+ {
+ OutputAnonSetReasons.Add(reason);
+ }
+
AnonymitySet = anonset;
- AnonymitySetReasons.Add(reason);
}
public void SetLabel(SmartLabel label, KeyManager? kmToFile = null)

Fix anon set calculation at address reuse