Пример #1
0
    public IEnumerable <Money> Decompose(IEnumerable <Money> myInputCoinEffectiveValues, IEnumerable <Money> othersInputCoinEffectiveValues)
    {
        var histogram = GetDenominationFrequencies(othersInputCoinEffectiveValues.Concat(myInputCoinEffectiveValues));

        // Filter out and order denominations those have occurred in the frequency table at least twice.
        var preFilteredDenoms = histogram
                                .Where(x => x.Value > 1)
                                .OrderByDescending(x => x.Key)
                                .Select(x => x.Key)
                                .ToArray();

        // Filter out denominations very close to each other.
        // Heavy filtering on the top, little to no filtering on the bottom,
        // because in smaller denom levels larger users are expected to participate,
        // but on larger denom levels there's little chance of finding each other.
        var          increment     = 0.5 / preFilteredDenoms.Length;
        List <ulong> denoms        = new();
        var          currentLength = preFilteredDenoms.Length;

        foreach (var denom in preFilteredDenoms)
        {
            var filterSeverity = 1 + currentLength * increment;
            if (!denoms.Any() || denom <= (denoms.Last() / filterSeverity))
            {
                denoms.Add(denom);
            }
            currentLength--;
        }

        var myInputs       = myInputCoinEffectiveValues.ToArray();
        var myInputSum     = myInputs.Sum();
        var remaining      = myInputSum;
        var remainingVsize = AvailableVsize;

        var setCandidates = new Dictionary <int, (IEnumerable <Money> Decomp, Money Cost)>();
        var random        = new Random();

        // How many times can we participate with the same denomination.
        var maxDenomUsage = random.Next(2, 8);

        // Create the most naive decomposition for starter.
        List <Money> naiveSet = new();
        bool         end      = false;

        foreach (var denomPlusFee in preFilteredDenoms.Where(x => x <= remaining))
        {
            var denomUsage = 0;
            while (denomPlusFee <= remaining)
            {
                // We can only let this go forward if at least 2 output can be added (denom + potential change)
                if (remaining < MinAllowedOutputAmountPlusFee || remainingVsize < 2 * OutputSize)
                {
                    end = true;
                    break;
                }

                naiveSet.Add(denomPlusFee);
                remaining      -= denomPlusFee;
                remainingVsize -= OutputSize;
                denomUsage++;

                // If we reached the limit, the rest will be change.
                if (denomUsage >= maxDenomUsage)
                {
                    end = true;
                    break;
                }
            }

            if (end)
            {
                break;
            }
        }

        var loss = 0UL;

        if (remaining >= MinAllowedOutputAmountPlusFee)
        {
            naiveSet.Add(remaining);
        }
        else
        {
            // This goes to miners.
            loss = remaining;
        }

        // This can happen when smallest denom is larger than the input sum.
        if (naiveSet.Count == 0)
        {
            naiveSet.Add(remaining);
        }

        HashCode hash = new();

        foreach (var item in naiveSet.OrderBy(x => x))
        {
            hash.Add(item);
        }

        setCandidates.Add(
            hash.ToHashCode(),                                     // Create hash to ensure uniqueness.
            (naiveSet, loss + (ulong)naiveSet.Count * OutputFee)); // The cost is the remaining + output cost.

        // Create many decompositions for optimization.
        Decomposer.StdDenoms = denoms.Where(x => x <= myInputSum).Select(x => (long)x).ToArray();
        foreach (var(sum, count, decomp) in Decomposer.Decompose((long)myInputSum, (long)Math.Max(loss, 0.5 * (ulong)MinAllowedOutputAmountPlusFee), Math.Min(8, Math.Max(5, naiveSet.Count))))
        {
            var currentSet = Decomposer.ToRealValuesArray(
                decomp,
                count,
                Decomposer.StdDenoms).Select(Money.Satoshis).ToList();

            hash = new();
            foreach (var item in currentSet.OrderBy(x => x))
            {
                hash.Add(item);
            }
            setCandidates.TryAdd(hash.ToHashCode(), (currentSet, myInputSum - (ulong)currentSet.Sum() + (ulong)count * OutputFee));             // The cost is the remaining + output cost.
        }

        var denomHashSet    = preFilteredDenoms.ToHashSet();
        var finalCandidates = setCandidates.Select(x => x.Value).ToList();

        finalCandidates.Shuffle();

        var orderedCandidates = finalCandidates
                                .OrderBy(x => x.Cost)                                             // Less cost is better.
                                .ThenBy(x => x.Decomp.All(x => denomHashSet.Contains(x)) ? 0 : 1) // Prefer no change.
                                .Select(x => x).ToList();

        var finalCandidate = orderedCandidates.First().Decomp;

        foreach (var candidate in orderedCandidates)
        {
            if (random.NextDouble() < 0.5)
            {
                finalCandidate = candidate.Decomp;
                break;
            }
        }

        finalCandidate = finalCandidate.Select(x => x - OutputFee);

        var totalOutputAmount = finalCandidate.Sum(x => x + OutputFee);

        if (totalOutputAmount > myInputSum)
        {
            throw new InvalidOperationException("The decomposer is creating money. Aborting.");
        }
        if (totalOutputAmount + MinAllowedOutputAmountPlusFee < myInputSum)
        {
            throw new InvalidOperationException("The decomposer is losing money. Aborting.");
        }

        var totalOutputVsize = finalCandidate.Count() * OutputSize;

        if (totalOutputVsize > AvailableVsize)
        {
            throw new InvalidOperationException("The decomposer created more outputs than it can. Aborting.");
        }
        return(finalCandidate);
    }