diff --git a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/PaymentAwareOutputProviderTests.cs b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/PaymentAwareOutputProviderTests.cs index ff89e3439dc..6ec44041c8f 100644 --- a/WalletWasabi.Tests/UnitTests/WabiSabi/Client/PaymentAwareOutputProviderTests.cs +++ b/WalletWasabi.Tests/UnitTests/WabiSabi/Client/PaymentAwareOutputProviderTests.cs @@ -1,8 +1,14 @@ using NBitcoin; using System.Linq; +using WabiSabi.Crypto; +using WalletWasabi.Blockchain.TransactionBuilding; +using WalletWasabi.Helpers; using WalletWasabi.Tests.Helpers; using WalletWasabi.WabiSabi.Backend; +using WalletWasabi.WabiSabi.Client; using WalletWasabi.WabiSabi.Client.Batching; +using WalletWasabi.WabiSabi.Client.CoinJoin.Client.Decomposer; +using WalletWasabi.WabiSabi.Client.CredentialDependencies; using Xunit; namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client; @@ -10,31 +16,90 @@ namespace WalletWasabi.Tests.UnitTests.WabiSabi.Client; public class PaymentAwareOutputProviderTests { [Fact] - public void CreateOutputsForPaymentsTest() + public void CreateOutputsForImpossiblePaymentTest() { var rpc = new MockRpcClient(); var wallet = new TestWallet("random-wallet", rpc); var paymentBatch = new PaymentBatch(); - var outputProvider = new PaymentAwareOutputProvider(wallet, paymentBatch); - var roundParameters = WabiSabiFactory.CreateRoundParameters(new WabiSabiConfig()); using Key key = new(); paymentBatch.AddPayment( key.PubKey.GetAddress(ScriptPubKeyType.Segwit, rpc.Network), - Money.Coins(0.00005432m)); + Money.Coins(101.0001m)); // Too big, non-standard payment which cannot be done. + + var roundParameters = WabiSabiFactory.CreateRoundParameters(new WabiSabiConfig()); + var registeredCoinsEffectiveValues = new[] { Money.Coins(100m) }; + var theirCoinEffectiveValues = new[] { Money.Coins(0.2m), Money.Coins(0.1m), Money.Coins(0.05m), Money.Coins(0.0025m), Money.Coins(0.0001m) }; + var availableVsize = roundParameters.MaxVsizeAllocationPerAlice - Constants.P2wpkhInputVirtualSize; + + var outputProvider = new PaymentAwareOutputProvider(wallet, paymentBatch); + var outputs = outputProvider.GetOutputs( + roundId: uint256.Zero, + roundParameters, + registeredCoinsEffectiveValues, + theirCoinEffectiveValues, + availableVsize).ToArray(); + + var nonAwaredOutputProvider = new OutputProvider(wallet); + var decomposedOutputs = nonAwaredOutputProvider.GetOutputs( + uint256.Zero, + roundParameters, + registeredCoinsEffectiveValues, + theirCoinEffectiveValues, + availableVsize).ToArray(); + + decimal ToDecimal(TxOut o) => o.Value.ToDecimal(MoneyUnit.BTC); + Assert.Equal(outputs.Sum(ToDecimal), decomposedOutputs.Sum(ToDecimal)); + + // Make sure this doesn't throw + var vsizes = Enumerable.Repeat(0L, int.MaxValue).Prepend(availableVsize); + DependencyGraph.ResolveCredentialDependencies(registeredCoinsEffectiveValues, outputs, roundParameters.MiningFeeRate, vsizes); + } + + [Theory] + [InlineData("0.00484323")] + [InlineData("0.007")] + [InlineData("0.007123")] + [InlineData("0.00555")] + [InlineData("0.001, 0.001, 0.001, 0.001, 0.001, 0.001")] + public void CreateOutputsForPaymentsTest(string testData) + { + var payments = testData.Split(",").Select(decimal.Parse).ToArray(); + var rpc = new MockRpcClient(); + var wallet = new TestWallet("random-wallet", rpc); + var paymentBatch = new PaymentBatch(); + var outputProvider = new PaymentAwareOutputProvider(wallet, paymentBatch); + + var roundParameters = WabiSabiFactory.CreateRoundParameters(new WabiSabiConfig()); + + var scriptPubKeys = payments.Select(payment => + { + using Key key = new(); + var scriptPubKey = key.PubKey.GetAddress(ScriptPubKeyType.Segwit, rpc.Network); + paymentBatch.AddPayment(scriptPubKey, Money.Coins(payment)); + return scriptPubKey; + }).ToArray(); + + var registeredCoinsEffectiveValues = new[] { Money.Coins(0.00484323m), Money.Coins(0.003m), Money.Coins(0.00004323m) }; + var totalRegisteredEffectiveValue = registeredCoinsEffectiveValues.Sum().ToDecimal(MoneyUnit.BTC); var outputs = outputProvider.GetOutputs( roundId: uint256.Zero, roundParameters, - new[] { Money.Coins(0.00484323m), Money.Coins(0.003m), Money.Coins(0.00004323m) }, + registeredCoinsEffectiveValues, new[] { Money.Coins(0.2m), Money.Coins(0.1m), Money.Coins(0.05m), Money.Coins(0.0025m), Money.Coins(0.0001m) }, int.MaxValue).ToArray(); - Assert.Equal(outputs[0].ScriptPubKey, key.PubKey.GetScriptPubKey(ScriptPubKeyType.Segwit)); - Assert.Equal(outputs[0].Value, Money.Coins(0.00005432m)); + Assert.All(payments.Take(4).Zip(scriptPubKeys, outputs, (x, y, z) => (Payment: x, Destination: y, Output: z)), x => + { + Assert.Equal(x.Output.ScriptPubKey, x.Destination.ScriptPubKey); + Assert.Equal(x.Output.Value, Money.Coins(x.Payment)); + }); - Assert.True(outputs.Length > 2, $"There were {outputs.Length} outputs."); // The rest was decomposed - Assert.InRange(outputs.Sum(x => x.Value.ToDecimal(MoneyUnit.BTC)), 0.007500m, 0.007800m); // no money was lost + Assert.True(outputs.Length >= 2, $"There were {outputs.Length} outputs."); // The rest was decomposed + Assert.InRange(outputs.Sum(x => x.Value.ToDecimal(MoneyUnit.BTC)), + totalRegisteredEffectiveValue - 0.00025m, + totalRegisteredEffectiveValue); // no money was lost } [Theory] diff --git a/WalletWasabi/WabiSabi/Client/CoinJoin/Client/CoinJoinClient.cs b/WalletWasabi/WabiSabi/Client/CoinJoin/Client/CoinJoinClient.cs index 7eb04bbc07a..3c3c797ff9e 100644 --- a/WalletWasabi/WabiSabi/Client/CoinJoin/Client/CoinJoinClient.cs +++ b/WalletWasabi/WabiSabi/Client/CoinJoin/Client/CoinJoinClient.cs @@ -772,8 +772,7 @@ private async Task> ProceedWithOutputRegistrationPhaseAsync(u using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, phaseTimeoutCts.Token); var registeredCoins = registeredAliceClients.Select(x => x.SmartCoin.Coin); - var inputEffectiveValuesAndSizes = registeredAliceClients.Select(x => (x.EffectiveValue, x.SmartCoin.ScriptPubKey.EstimateInputVsize())); - var availableVsize = registeredAliceClients.SelectMany(x => x.IssuedVsizeCredentials).Sum(x => x.Value); + var availableVsizes = registeredAliceClients.SelectMany(x => x.IssuedVsizeCredentials.Where(y=>y.Value > 0)).Select(x => x.Value); // Calculate outputs values var constructionState = roundState.Assert(); @@ -782,9 +781,9 @@ private async Task> ProceedWithOutputRegistrationPhaseAsync(u var registeredCoinEffectiveValues = registeredAliceClients.Select(x => x.EffectiveValue); var theirCoinEffectiveValues = theirCoins.Select(x => x.EffectiveValue(roundParameters.MiningFeeRate)); - var outputTxOuts = OutputProvider.GetOutputs(roundId, roundParameters, registeredCoinEffectiveValues, theirCoinEffectiveValues, (int)availableVsize).ToArray(); + var outputTxOuts = OutputProvider.GetOutputs(roundId, roundParameters, registeredCoinEffectiveValues, theirCoinEffectiveValues, (int)availableVsizes.Sum()).ToArray(); - DependencyGraph dependencyGraph = DependencyGraph.ResolveCredentialDependencies(inputEffectiveValuesAndSizes, outputTxOuts, roundParameters.MiningFeeRate, roundParameters.MaxVsizeAllocationPerAlice); + DependencyGraph dependencyGraph = DependencyGraph.ResolveCredentialDependencies(registeredCoinEffectiveValues, outputTxOuts, roundParameters.MiningFeeRate, availableVsizes); DependencyGraphTaskScheduler scheduler = new(dependencyGraph); var combinedToken = linkedCts.Token; diff --git a/WalletWasabi/WabiSabi/Client/CredentialDependencies/DependencyGraph.cs b/WalletWasabi/WabiSabi/Client/CredentialDependencies/DependencyGraph.cs index 6c7a9677049..04d3ac5c97d 100644 --- a/WalletWasabi/WabiSabi/Client/CredentialDependencies/DependencyGraph.cs +++ b/WalletWasabi/WabiSabi/Client/CredentialDependencies/DependencyGraph.cs @@ -31,22 +31,20 @@ public record DependencyGraph /// and may contain additional nodes if reissuance requests are /// required. /// - public static DependencyGraph ResolveCredentialDependencies(IEnumerable<(Money EffectiveValue, int InputSize)> effectiveValuesAndSizes, IEnumerable outputs, FeeRate feeRate, long vsizeAllocationPerInput) + public static DependencyGraph ResolveCredentialDependencies(IEnumerable effectiveValues, IEnumerable outputs, FeeRate feeRate, IEnumerable availableVSizes) { - var effectiveValues = effectiveValuesAndSizes.Select(x => x.EffectiveValue.Satoshi); - var inputSizes = effectiveValuesAndSizes.Select(x => vsizeAllocationPerInput - x.InputSize); + var effectiveValuesInSats = effectiveValues.Select(x => x.Satoshi); if (effectiveValues.Any(x => x <= Money.Zero)) { - throw new InvalidOperationException($"Not enough funds to pay for the fees."); + throw new InvalidOperationException("Not enough funds to pay for the fees."); } var outputSizes = outputs.Select(x => (long)x.ScriptPubKey.EstimateOutputVsize()); - var effectiveCosts = outputs.Select(txout => txout.EffectiveCost(feeRate).Satoshi); - + var effectiveCostsInSats = outputs.Select(txout => txout.EffectiveCost(feeRate).Satoshi); return ResolveCredentialDependencies( - effectiveValues.Zip(inputSizes).ToArray(), - effectiveCosts.Zip(outputSizes).ToArray() + effectiveValuesInSats.Zip(availableVSizes).ToArray(), + effectiveCostsInSats.Zip(outputSizes).ToArray() ); }