Skip to content

Commit

Permalink
Simplify Decomposer (WalletWasabi#13287)
Browse files Browse the repository at this point in the history
  • Loading branch information
lontivero authored Jul 25, 2024
1 parent 5101541 commit f5eabcf
Show file tree
Hide file tree
Showing 3 changed files with 43 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,9 @@ private static Coin CreateCoin(Script scriptPubKey, long amount)
[InlineData(39, 728551029, 4999, 8, new long[] { 6973569112, 4294967606, 2324523244, 1162261777, 774841288, 536871222, 268435766, 134218038, 86093752, 50000310, 33554742, 20000310, 14349217, 10000310, 5000310, 3188956, 2097462, 1594633, 1063192, 531751, 354604, 262454, 200310, 131382, 100310, 65846, 50310, 39676, 33078, 20310, 16694, 13432, 10310 })]
public void DecomposeTests(int expectedResultCount, long target, long tolerance, int maxCount, long[] stdDenoms)
{
var denoms = stdDenoms.SkipWhile(x => x > target).ToArray();
var res = Decomposer.Decompose(target, tolerance, maxCount, denoms);
var res = Decomposer.Decompose(target, tolerance, maxCount, stdDenoms);

Assert.True(res.Count() == res.ToHashSet().Count);
Assert.True(expectedResultCount < 0 || res.Count() == expectedResultCount);
Assert.All(res, x => Assert.True(x.Sum == Decomposer.ToRealValuesArray(x.Decomposition, x.Count, denoms).Sum()));
Assert.All(res, x => Assert.True(target - x.Sum < tolerance));
Assert.Equal(expectedResultCount, res.Count());
Assert.All(res, x => Assert.True(x.Sum <= target && x.Sum >= target - tolerance));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -193,16 +193,13 @@ private static Money FindChange(IEnumerable<Output> decomposition, HashSet<Outpu

if (maxNumberOfOutputsAllowed > 1)
{
foreach (var (sum, count, decomp) in Decomposer.Decompose(
foreach (var decomposition in Decomposer.Decompose(
target: (long)myInputSum,
tolerance: MinAllowedOutputAmount + ChangeFee,
maxCount: Math.Min(maxNumberOfOutputsAllowed, 8), // Decomposer doesn't do more than 8.
stdDenoms: stdDenoms))
denoms: stdDenoms))
{
var currentSet = Decomposer.ToRealValuesArray(
decomp,
count,
stdDenoms).Select(Money.Satoshis).ToList();
var currentSet = decomposition.AsEnumerable().Select(Money.Satoshis).ToList();

// Translate back to denominations.
List<Output> finalDenoms = new();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,64 @@ namespace WalletWasabi.WabiSabi.Client.CoinJoin.Client.Decomposer;
/// </summary>
public static class Decomposer
{
public static IEnumerable<(long Sum, int Count, ulong Decomposition)> Decompose(long target, long tolerance, int maxCount, long[] stdDenoms)
public static IEnumerable<IDecomposition> Decompose(long target, long tolerance, int maxCount, long[] denoms)
{
if (maxCount is <= 1 or > 8)
{
throw new ArgumentOutOfRangeException(nameof(maxCount), "The maximum decomposition length cannot be greater than 8 or smaller than 2.");
}
if (target <= 0)
{
throw new ArgumentException("Only positive numbers can be decomposed.", nameof(target));
}

var denoms = stdDenoms.SkipWhile(x => x > target).ToArray();
if (tolerance <= 0 || tolerance >= target)
{
throw new ArgumentException("Tolerance must be greater than zero and less than the target.",
nameof(tolerance));
}

if (denoms.Length > 255)
if (maxCount < 0)
{
throw new ArgumentException("Too many denominations. Maximum number is 255.", nameof(target));
throw new ArgumentException("MaxCount must be greater than or equal to zero.", nameof(maxCount));
}
return InternalCombinations(target, tolerance: tolerance, maxCount, denoms).Take(10_000).ToList();

return InternalCombinations(new NullDecomposition(), 0, target, tolerance: tolerance, maxCount, denoms).Take(10_000).ToList();
}

private static IEnumerable<(long Sum, int Count, ulong Decomposition)> InternalCombinations(long target, long tolerance, int maxLength, long[] denoms)
private static IEnumerable<IDecomposition> InternalCombinations(IDecomposition head, long sum, long target, long tolerance, int k, long[] denoms)
{
IEnumerable<(long Sum, int Count, ulong Decomposition)> Combinations(
int currentDenominationIdx,
ulong accumulator,
long sum,
int k)
var remaining = target - sum;
if (k == 0 || remaining < tolerance)
{
accumulator = accumulator << 8 | (ulong)currentDenominationIdx & 0xff;
var currentDenomination = denoms[currentDenominationIdx];
sum += currentDenomination;
var remaining = target - sum;
if (k == 0 || remaining < tolerance)
{
return new[] { (sum, maxLength - k, accumulator) };
}

currentDenominationIdx = Search(remaining, denoms, currentDenominationIdx);

return Enumerable.Range(0, denoms.Length - currentDenominationIdx)
.TakeWhile(i => k * denoms[currentDenominationIdx + i] >= remaining - tolerance)
.SelectMany((_, i) =>
Combinations(currentDenominationIdx + i, accumulator, sum, k - 1)
.TakeUntil(x => x.Sum == target));
return [head];
}

return denoms.SelectMany((_, i) => Combinations(i, 0ul, 0, maxLength - 1)).Take(5_000).ToList();
var newDenoms = denoms[Search(remaining, denoms)..];
return newDenoms
.TakeWhile(d => k * d >= remaining - tolerance)
.SelectMany((d, i) => InternalCombinations(new Decomposition(d, head), sum + d, target, tolerance, k - 1, newDenoms[i..])
.TakeUntil(x => x.Sum == target));
}

private static int Search(long value, long[] denoms, int offset)
static int Search(long value, long[] denoms)
{
var startingIndex = Array.BinarySearch(denoms, offset, denoms.Length - offset, value, ReverseComparer.Default);
var startingIndex = Array.BinarySearch(denoms, 0, denoms.Length, value, ReverseComparer.Default);
return startingIndex < 0 ? ~startingIndex : startingIndex;
}

public static IEnumerable<long> ToRealValuesArray(ulong decomposition, int count, long[] denoms)
public interface IDecomposition
{
var list = new long[count];
for (var i = 0; i < count; i++)
{
var index = (decomposition >> (i * 8)) & 0xff;
list[count - i - 1] = denoms[index];
}
return list;
long Sum { get; }
IEnumerable<long> AsEnumerable();
}

private record Decomposition(long V, IDecomposition Next) : IDecomposition
{
public long Sum => V + Next.Sum;
public IEnumerable<long> AsEnumerable() => Next.AsEnumerable().Append(V);
}

private record NullDecomposition : IDecomposition
{
public long Sum => 0;
public IEnumerable<long> AsEnumerable() => Array.Empty<long>();
}
}

0 comments on commit f5eabcf

Please sign in to comment.