Use weigthed average for anonymity set calculation in coinjoins
MODIFY
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
@@ -36,25 +36,25 @@ public class BlockchainAnalyzer
}
else
{
- AnalyzeWalletInputs(tx, out HashSet<HdPubKey> distinctWalletInputPubKeys, out int newInputAnonset);
+ AnalyzeWalletInputs(tx, out HashSet<HdPubKey> distinctWalletInputPubKeys, out int startingOutputAnonset);
if (inputCount == ownInputCount)
{
- AnalyzeSelfSpend(tx, newInputAnonset);
+ AnalyzeSelfSpend(tx, startingOutputAnonset);
}
else
{
- AnalyzeCoinjoin(tx, newInputAnonset, distinctWalletInputPubKeys);
+ AnalyzeCoinjoin(tx, startingOutputAnonset, distinctWalletInputPubKeys);
}
- AdjustWalletInputs(tx, distinctWalletInputPubKeys, newInputAnonset);
+ AdjustWalletInputs(tx, distinctWalletInputPubKeys, startingOutputAnonset);
}
AnalyzeClusters(tx);
}
- /// <param name="newInputAnonset">The new anonymity set of the inputs.</param>
- private void AnalyzeWalletInputs(SmartTransaction tx, out HashSet<HdPubKey> distinctWalletInputPubKeys, out int newInputAnonset)
+ /// <param name="startingOutputAnonset">The new anonymity set of the inputs.</param>
+ private void AnalyzeWalletInputs(SmartTransaction tx, out HashSet<HdPubKey> distinctWalletInputPubKeys, out int startingOutputAnonset)
{
// We want to weaken the punishment if the input merge happens in coinjoins.
// Our strategy would be is to set the coefficient in proportion to our own inputs compared to the total inputs of the transaction.
@@ -66,19 +66,27 @@ public class BlockchainAnalyzer
var pubKeyReuseCount = tx.WalletInputs.Count - distinctWalletInputPubKeyCount;
double coefficient = (double)distinctWalletInputPubKeyCount / (tx.Transaction.Inputs.Count - pubKeyReuseCount);
- newInputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet), coefficient);
-
+ // Consolidation in coinjoins is the only type of consolidation that's acceptable,
+ // because coinjoins are an exception from common input ownership heuristic.
+ if (coefficient < 1)
+ {
+ // Calculate weighted average.
+ startingOutputAnonset = (int)(tx.WalletInputs.Sum(x => x.HdPubKey.AnonymitySet * x.Amount) / tx.WalletInputs.Sum(x => x.Amount));
+ }
+ else
+ {
+ startingOutputAnonset = Intersect(distinctWalletInputPubKeys.Select(x => x.AnonymitySet));
foreach (var key in distinctWalletInputPubKeys)
{
- key.SetAnonymitySet(newInputAnonset);
+ key.SetAnonymitySet(startingOutputAnonset);
+ }
}
}
/// <summary>
/// Estimate input cluster anonymity set size, penalizing input consolidations to accounting for intersection attacks.
/// </summary>
- /// <param name="coefficient">If larger than 1, then penalty is larger, if smaller than 1 then penalty is smaller.</param>
- private int Intersect(IEnumerable<int> anonsets, double coefficient)
+ private int Intersect(IEnumerable<int> anonsets)
{
// Sanity check.
if (!anonsets.Any())
@@ -89,23 +97,17 @@ public class BlockchainAnalyzer
// Our smallest anonset is the relevant here, because anonsets cannot grow by intersection punishments.
var smallestAnon = anonsets.Min();
- // Consolidation in coinjoins is the only type of consolidation that's acceptable.
- if (coefficient < 1)
- {
- return smallestAnon;
- }
-
// Punish intersection exponentially.
// If there is only a single anonset then the exponent should be zero to divide by 1 thus retain the input coin anonset.
var intersectPenalty = Math.Pow(2, anonsets.Count() - 1);
- var intersectionAnonset = smallestAnon / Math.Max(1, intersectPenalty * coefficient);
+ var intersectionAnonset = smallestAnon / Math.Max(1, intersectPenalty);
// The minimum anonymity set size is 1, enforce it when the punishment is very large.
var normalizedIntersectionAnonset = Math.Max(1, (int)intersectionAnonset);
return normalizedIntersectionAnonset;
}
- private void AnalyzeCoinjoin(SmartTransaction tx, int newInputAnonset, ISet<HdPubKey> distinctWalletInputPubKeys)
+ private void AnalyzeCoinjoin(SmartTransaction tx, int startingOutputAnonset, ISet<HdPubKey> distinctWalletInputPubKeys)
{
var indistinguishableWalletOutputs = tx
.WalletOutputs.GroupBy(x => x.Amount)
@@ -134,7 +136,7 @@ public class BlockchainAnalyzer
// Account for the inherited anonymity set size from the inputs in the
// anonymity set size estimate.
- anonset += newInputAnonset;
+ anonset += startingOutputAnonset;
HdPubKey hdPubKey = newCoin.HdPubKey;
uint256 txid = tx.GetHash();
@@ -149,12 +151,12 @@ public class BlockchainAnalyzer
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(startingOutputAnonset, txid);
}
else if (tx.WalletOutputs.Where(x => x != newCoin).Select(x => x.HdPubKey).Contains(hdPubKey))
{
// If it's a reuse of another output's 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(startingOutputAnonset, Intersect(new[] { anonset, hdPubKey.AnonymitySet })), txid);
}
else if (hdPubKey.OutputAnonSetReasons.Contains(txid))
{
@@ -171,7 +173,7 @@ public class BlockchainAnalyzer
else
{
// It's address reuse.
- hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }, 1), txid);
+ hdPubKey.SetAnonymitySet(Intersect(new[] { anonset, hdPubKey.AnonymitySet }), txid);
}
}
}
@@ -179,7 +181,7 @@ public class BlockchainAnalyzer
/// <summary>
/// Adjusts the anonset of the inputs to the newly calculated output anonsets.
/// </summary>
- private static void AdjustWalletInputs(SmartTransaction tx, HashSet<HdPubKey> distinctWalletInputPubKeys, int newInputAnonset)
+ private static void AdjustWalletInputs(SmartTransaction tx, HashSet<HdPubKey> distinctWalletInputPubKeys, int startingOutputAnonset)
{
// Sanity check.
if (!tx.WalletOutputs.Any())
@@ -188,7 +190,7 @@ public class BlockchainAnalyzer
}
var smallestOutputAnonset = tx.WalletOutputs.Min(x => x.HdPubKey.AnonymitySet);
- if (smallestOutputAnonset < newInputAnonset)
+ if (smallestOutputAnonset < startingOutputAnonset)
{
foreach (var key in distinctWalletInputPubKeys)
{
@@ -197,17 +199,17 @@ public class BlockchainAnalyzer
}
}
- private void AnalyzeSelfSpend(SmartTransaction tx, int newInputAnonset)
+ private void AnalyzeSelfSpend(SmartTransaction tx, int startingOutputAnonset)
{
foreach (var key in tx.WalletOutputs.Select(x => x.HdPubKey))
{
if (key.AnonymitySet == HdPubKey.DefaultHighAnonymitySet)
{
- key.SetAnonymitySet(newInputAnonset, tx.GetHash());
+ key.SetAnonymitySet(startingOutputAnonset, tx.GetHash());
}
else
{
- key.SetAnonymitySet(Intersect(new[] { newInputAnonset, key.AnonymitySet }, 1), tx.GetHash());
+ key.SetAnonymitySet(Intersect(new[] { startingOutputAnonset, key.AnonymitySet }), tx.GetHash());
}
}
}

Use weigthed average for anonymity set calculation in coinjoins