Skip to content

Commit

Permalink
Correct vsize calculation. (WalletWasabi#12618)
Browse files Browse the repository at this point in the history
  • Loading branch information
lontivero authored Sep 12, 2024
1 parent ef8a5de commit 9251caa
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,40 +1,105 @@
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;

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]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,8 +772,7 @@ private async Task<IEnumerable<TxOut>> 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<ConstructionState>();
Expand All @@ -782,9 +781,9 @@ private async Task<IEnumerable<TxOut>> 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,20 @@ public record DependencyGraph
/// and may contain additional nodes if reissuance requests are
/// required.</remarks>
///
public static DependencyGraph ResolveCredentialDependencies(IEnumerable<(Money EffectiveValue, int InputSize)> effectiveValuesAndSizes, IEnumerable<TxOut> outputs, FeeRate feeRate, long vsizeAllocationPerInput)
public static DependencyGraph ResolveCredentialDependencies(IEnumerable<Money> effectiveValues, IEnumerable<TxOut> outputs, FeeRate feeRate, IEnumerable<long> 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()
);
}

Expand Down

0 comments on commit 9251caa

Please sign in to comment.