Punish sybling coinjoiners
MODIFY
WalletWasabi.Tests/Helpers/AnalyzedTransaction/AnalyzedTransaction.cs
WalletWasabi.Tests/Helpers/AnalyzedTransaction/AnalyzedTransaction.cs
@@ -7,6 +7,7 @@ using WalletWasabi.Blockchain.Transactions;
using NBitcoin.Crypto;
using NBitcoin.DataEncoders;
using WalletWasabi.Blockchain.Analysis.Clustering;
+using WalletWasabi.Blockchain.Analysis;
namespace WalletWasabi.Tests.Helpers.AnalyzedTransaction;
@@ -70,7 +71,7 @@ public class AnalyzedTransaction : SmartTransaction
public void AddWalletInput(WalletOutput walletOutput)
{
AddForeignInput(walletOutput.ToForeignOutput());
- AddWalletInput(walletOutput.ToSmartCoin());
+ AddWalletInput(walletOutput);
}
public WalletOutput AddWalletInput(decimal amount = 1, string? label = null, int anonymity = 1)
@@ -93,13 +94,13 @@ public class AnalyzedTransaction : SmartTransaction
{
ForeignOutput foreignOutput = AddForeignOutput(amount, label);
SmartCoin smartCoin = new(this, foreignOutput.Index, CreateHdPubKey(label));
- AddWalletOutput(smartCoin);
+ TryAddWalletOutput(smartCoin);
return new WalletOutput(smartCoin);
}
public void AnalyzeRecursively()
{
- var analyser = ServiceFactory.CreateBlockchainAnalyzer();
+ var analyser = new BlockchainAnalyzer();
HashSet<SmartTransaction> analyzedTransactions = new();
// Analyze transactions in topological sorting

MODIFY
WalletWasabi.Tests/Helpers/AnalyzedTransaction/WalletOutput.cs
WalletWasabi.Tests/Helpers/AnalyzedTransaction/WalletOutput.cs
@@ -7,7 +7,7 @@ namespace WalletWasabi.Tests.Helpers.AnalyzedTransaction;
public record WalletOutput(SmartCoin Coin)
{
- public int Anonymity => Coin.HdPubKey.AnonymitySet;
+ public double Anonymity => Coin.HdPubKey.AnonymitySet;
public SmartCoin ToSmartCoin() => Coin;
@@ -21,7 +21,7 @@ public record WalletOutput(SmartCoin Coin)
ForeignOutput output = ForeignOutput.Create(amount, hdPubKey.P2wpkhScript);
SmartTransaction smartTransaction = new(output.Transaction, 0);
SmartCoin smartCoin = new(smartTransaction, output.Index, hdPubKey);
- smartTransaction.AddWalletOutput(smartCoin);
+ smartTransaction.TryAddWalletOutput(smartCoin);
return new WalletOutput(smartCoin);
}
}

MODIFY
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
WalletWasabi/Blockchain/Analysis/BlockchainAnalyzer.cs
@@ -64,12 +64,14 @@ public class BlockchainAnalyzer
private static void AnalyzeCoinjoinWalletInputs(SmartTransaction tx, out double mixedAnonScore, out double nonMixedAnonScore)
{
+ CoinjoinAnalyzer cjAnal = new(tx);
+
// Consolidation in coinjoins is the only type of consolidation that's acceptable,
// because coinjoins are an exception from common input ownership heuristic.
// Calculate weighted average.
- mixedAnonScore = tx.WalletVirtualInputs.Sum(x => x.HdPubKey.AnonymitySet * x.Amount.Satoshi) / tx.WalletVirtualInputs.Sum(x => x.Amount);
+ mixedAnonScore = tx.WalletVirtualInputs.Sum(x => (x.HdPubKey.AnonymitySet - cjAnal.ComputeInputSanction(x)) * x.Amount.Satoshi) / tx.WalletVirtualInputs.Sum(x => x.Amount);
- nonMixedAnonScore = tx.WalletVirtualInputs.Min(x => x.HdPubKey.AnonymitySet);
+ nonMixedAnonScore = tx.WalletVirtualInputs.Min(x => x.HdPubKey.AnonymitySet - cjAnal.ComputeInputSanction(x));
}
private double AnalyzeSelfSpendWalletInputs(SmartTransaction tx)

MODIFY
WalletWasabi/Blockchain/Analysis/CoinjoinAnalyzer.cs
WalletWasabi/Blockchain/Analysis/CoinjoinAnalyzer.cs
@@ -8,20 +8,23 @@ namespace WalletWasabi.Blockchain.Analysis;
public class CoinjoinAnalyzer
{
- public CoinjoinAnalyzer(SmartTransaction analyzedTransaction)
+ public CoinjoinAnalyzer(SmartTransaction transaction)
{
- AnalyzedTransaction = analyzedTransaction;
+ AnalyzedTransaction = transaction;
AnalyzedTransactionPrevOuts = AnalyzedTransaction.Transaction.Inputs.Select(input => input.PrevOut).ToHashSet();
}
private HashSet<OutPoint> AnalyzedTransactionPrevOuts { get; }
- private Dictionary<SmartCoin, decimal> CachedInputSanctions { get; } = new();
+ private Dictionary<SmartCoin, double> CachedInputSanctions { get; } = new();
public SmartTransaction AnalyzedTransaction { get; }
- public decimal ComputeInputSanction(SmartCoin transactionInput)
+ public double ComputeInputSanction(WalletVirtualInput virtualInput)
+ => virtualInput.Coins.Select(ComputeInputSanction).Max();
+
+ public double ComputeInputSanction(SmartCoin transactionInput)
{
- decimal ComputeInputSanctionHelper(SmartCoin transactionOutput)
+ double ComputeInputSanctionHelper(SmartCoin transactionOutput)
{
// If we already analyzed the sanction for this output, then return the cached result.
if (CachedInputSanctions.ContainsKey(transactionOutput))
@@ -33,7 +36,7 @@ public class CoinjoinAnalyzer
// We are searching for any transaction inputs of analyzedTransaction that might have come from this transaction.
// If we find such remixed outputs, then we determine how much they contributed to our anonymity set.
SmartTransaction transaction = transactionOutput.Transaction;
- decimal sanction = ComputeAnonymityContribution(transactionOutput, AnalyzedTransactionPrevOuts);
+ double sanction = ComputeAnonymityContribution(transactionOutput, AnalyzedTransactionPrevOuts);
// Recursively branch out into all of the transaction inputs' histories and compute the sanction for each branch.
// Add the worst-case branch to the resulting sanction.
@@ -52,7 +55,7 @@ public class CoinjoinAnalyzer
/// Sometimes we are only interested in how much a certain subset of foreign outputs contributed.
/// This subset can be specified in relevantOutpoints, otherwise all outputs are considered relevant.
/// </summary>
- public static decimal ComputeAnonymityContribution(SmartCoin transactionOutput, HashSet<OutPoint>? relevantOutpoints = null)
+ public static double ComputeAnonymityContribution(SmartCoin transactionOutput, HashSet<OutPoint>? relevantOutpoints = null)
{
SmartTransaction transaction = transactionOutput.Transaction;
IEnumerable<WalletVirtualOutput> walletVirtualOutputs = transaction.WalletVirtualOutputs;
@@ -69,6 +72,6 @@ public class CoinjoinAnalyzer
// If we have multiple equal-valued wallet outputs, then we divide the increase evenly between them.
// The rationale behind this is that picking randomly an output would make our anonset:
// total/ours = 1 + foreign/ours, so the increase in anonymity is foreign/ours.
- return (decimal)equalValueForeignRelevantVirtualOutputCount / equalValueWalletVirtualOutputCount;
+ return (double)equalValueForeignRelevantVirtualOutputCount / equalValueWalletVirtualOutputCount;
}
}

Punish sybling coinjoiners