156 lines
6.4 KiB
C#
156 lines
6.4 KiB
C#
// Unit test: wasabi-0001 CoinJoinCoinSelector AnonScoreTxSourceBiasedShuffle O(N^3) -> O(N^2)
|
|
// Validates that HashSet-based TransactionId tracking produces identical results
|
|
// while reducing comparison count from O(N^3) to O(N^2).
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
/// <summary>
|
|
/// Simulates the defective and patched AnonScoreTxSourceBiasedShuffle logic.
|
|
/// </summary>
|
|
public class Wasabi0001Test
|
|
{
|
|
record FakeCoin(int Id, int TransactionId, double AnonymitySet);
|
|
|
|
static int ComparisonCount_Defective = 0;
|
|
static int ComparisonCount_Patched = 0;
|
|
|
|
/// <summary>Defective: List.Any for TransactionId dedup, O(N^3)</summary>
|
|
static List<FakeCoin> DefectiveShuffle(FakeCoin[] coins)
|
|
{
|
|
ComparisonCount_Defective = 0;
|
|
var orderedCoins = new List<FakeCoin>();
|
|
for (int i = 0; i < coins.Length; i++)
|
|
{
|
|
var remaining = coins.Except(orderedCoins).OrderBy(x => x.AnonymitySet).ToList();
|
|
var alternating = new List<FakeCoin>();
|
|
var skipped = new List<FakeCoin>();
|
|
foreach (var c in remaining)
|
|
{
|
|
bool inAlternating = alternating.Any(x => { ComparisonCount_Defective++; return x.TransactionId == c.TransactionId; });
|
|
bool inOrdered = orderedCoins.Any(x => { ComparisonCount_Defective++; return x.TransactionId == c.TransactionId; });
|
|
if (inAlternating || inOrdered)
|
|
skipped.Add(c);
|
|
else
|
|
alternating.Add(c);
|
|
}
|
|
alternating.AddRange(skipped);
|
|
// Deterministic pick: take first element instead of random
|
|
var coin = alternating[0];
|
|
orderedCoins.Add(coin);
|
|
}
|
|
return orderedCoins;
|
|
}
|
|
|
|
/// <summary>Patched: HashSet for TransactionId dedup, O(N^2)</summary>
|
|
static List<FakeCoin> PatchedShuffle(FakeCoin[] coins)
|
|
{
|
|
ComparisonCount_Patched = 0;
|
|
var orderedCoins = new List<FakeCoin>();
|
|
var orderedTxIds = new HashSet<int>();
|
|
for (int i = 0; i < coins.Length; i++)
|
|
{
|
|
var remaining = coins.Except(orderedCoins).OrderBy(x => x.AnonymitySet).ToList();
|
|
var alternating = new List<FakeCoin>();
|
|
var alternatingTxIds = new HashSet<int>();
|
|
var skipped = new List<FakeCoin>();
|
|
foreach (var c in remaining)
|
|
{
|
|
ComparisonCount_Patched++; // HashSet.Contains is O(1)
|
|
if (alternatingTxIds.Contains(c.TransactionId) || orderedTxIds.Contains(c.TransactionId))
|
|
skipped.Add(c);
|
|
else
|
|
{
|
|
alternating.Add(c);
|
|
alternatingTxIds.Add(c.TransactionId);
|
|
}
|
|
}
|
|
alternating.AddRange(skipped);
|
|
var coin = alternating[0];
|
|
orderedCoins.Add(coin);
|
|
orderedTxIds.Add(coin.TransactionId);
|
|
}
|
|
return orderedCoins;
|
|
}
|
|
|
|
public static void Main()
|
|
{
|
|
int passed = 0;
|
|
int failed = 0;
|
|
|
|
// Test 1: Correctness, both produce same output order
|
|
{
|
|
var coins = Enumerable.Range(0, 20).Select(i =>
|
|
new FakeCoin(i, i / 3, (double)(20 - i))).ToArray();
|
|
|
|
var defective = DefectiveShuffle(coins);
|
|
var patched = PatchedShuffle(coins);
|
|
|
|
bool same = defective.Select(c => c.Id).SequenceEqual(patched.Select(c => c.Id));
|
|
if (same) { Console.WriteLine("PASS test1_correctness: identical output order"); passed++; }
|
|
else { Console.WriteLine("FAIL test1_correctness: output order differs"); failed++; }
|
|
}
|
|
|
|
// Test 2: Performance at N=100
|
|
{
|
|
var coins = Enumerable.Range(0, 100).Select(i =>
|
|
new FakeCoin(i, i / 5, (double)(100 - i))).ToArray();
|
|
|
|
DefectiveShuffle(coins);
|
|
int defectiveComps = ComparisonCount_Defective;
|
|
|
|
PatchedShuffle(coins);
|
|
int patchedComps = ComparisonCount_Patched;
|
|
|
|
double ratio = (double)defectiveComps / patchedComps;
|
|
Console.WriteLine($" N=100: defective={defectiveComps} patched={patchedComps} ratio={ratio:F1}x");
|
|
|
|
if (ratio > 10.0) { Console.WriteLine("PASS test2_perf_n100: >10x fewer comparisons"); passed++; }
|
|
else { Console.WriteLine($"FAIL test2_perf_n100: ratio {ratio:F1}x not >10x"); failed++; }
|
|
}
|
|
|
|
// Test 3: Performance at N=200
|
|
{
|
|
var coins = Enumerable.Range(0, 200).Select(i =>
|
|
new FakeCoin(i, i / 5, (double)(200 - i))).ToArray();
|
|
|
|
DefectiveShuffle(coins);
|
|
int defectiveComps = ComparisonCount_Defective;
|
|
|
|
PatchedShuffle(coins);
|
|
int patchedComps = ComparisonCount_Patched;
|
|
|
|
double ratio = (double)defectiveComps / patchedComps;
|
|
Console.WriteLine($" N=200: defective={defectiveComps} patched={patchedComps} ratio={ratio:F1}x");
|
|
|
|
if (ratio > 30.0) { Console.WriteLine("PASS test3_perf_n200: >30x fewer comparisons"); passed++; }
|
|
else { Console.WriteLine($"FAIL test3_perf_n200: ratio {ratio:F1}x not >30x"); failed++; }
|
|
}
|
|
|
|
// Test 4: Edge case, single coin
|
|
{
|
|
var coins = new[] { new FakeCoin(0, 0, 1.0) };
|
|
var defective = DefectiveShuffle(coins);
|
|
var patched = PatchedShuffle(coins);
|
|
bool same = defective.Count == 1 && patched.Count == 1 && defective[0].Id == patched[0].Id;
|
|
if (same) { Console.WriteLine("PASS test4_single_coin: correct for N=1"); passed++; }
|
|
else { Console.WriteLine("FAIL test4_single_coin"); failed++; }
|
|
}
|
|
|
|
// Test 5: All same TransactionId
|
|
{
|
|
var coins = Enumerable.Range(0, 50).Select(i =>
|
|
new FakeCoin(i, 42, (double)i)).ToArray();
|
|
var defective = DefectiveShuffle(coins);
|
|
var patched = PatchedShuffle(coins);
|
|
bool same = defective.Select(c => c.Id).SequenceEqual(patched.Select(c => c.Id));
|
|
if (same) { Console.WriteLine("PASS test5_same_txid: identical when all txids equal"); passed++; }
|
|
else { Console.WriteLine("FAIL test5_same_txid"); failed++; }
|
|
}
|
|
|
|
Console.WriteLine($"\n{passed}/{passed + failed} tests passed");
|
|
if (failed > 0) Environment.Exit(1);
|
|
}
|
|
}
|