java-topology/defects/wasabi-0001/test/wasabi-0001-test.cs

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);
}
}