Payjoin: Better UIH1 & UIH2 based selection (#1473)

* Try to make SelectUTXO care about all inputs and outputs

* wip

* wip

* Add test and fix seelctor

* remove space

* review changes

* revert back to index check
This commit is contained in:
Andrew Camilleri
2020-04-27 18:28:21 +02:00
committed by GitHub
parent 94cdd399d5
commit 3801eeec43
2 changed files with 82 additions and 16 deletions

View File

@@ -26,6 +26,7 @@ using NBitcoin;
using NBitcoin.Altcoins; using NBitcoin.Altcoins;
using NBitcoin.Payment; using NBitcoin.Payment;
using NBitpayClient; using NBitpayClient;
using NBXplorer.Models;
using OpenQA.Selenium; using OpenQA.Selenium;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
@@ -90,6 +91,45 @@ namespace BTCPayServer.Tests
} }
} }
[Fact]
[Trait("Integration", "Integration")]
public async Task ChooseBestUTXOsForPayjoin()
{
using (var tester = ServerTester.Create())
{
await tester.StartAsync();
var network = tester.NetworkProvider.GetNetwork<BTCPayNetwork>("BTC");
var controller = tester.PayTester.GetService<PayJoinEndpointController>();
//Only one utxo, so obvious result
var utxos = new[] {FakeUTXO(1.0m)};
var paymentAmount = 0.5m;
var otherOutputs = new[] {0.5m};
var inputs = new[] {1m};
var result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
Assert.Contains( result.selectedUTXO, utxo => utxos.Contains(utxo));
//no matter what here, no good selection, it seems that payment with 1 utxo generally makes payjoin coin selection unperformant
utxos = new[] {FakeUTXO(0.3m),FakeUTXO(0.7m)};
paymentAmount = 0.5m;
otherOutputs = new[] {0.5m};
inputs = new[] {1m};
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.Ordered, result.selectionType);
//when there is no change, anything works
utxos = new[] {FakeUTXO(1),FakeUTXO(0.1m),FakeUTXO(0.001m),FakeUTXO(0.003m)};
paymentAmount = 0.5m;
otherOutputs = new decimal[0];
inputs = new[] {0.03m, 0.07m};
result = await controller.SelectUTXO(network, utxos, inputs, paymentAmount, otherOutputs);
Assert.Equal(PayJoinEndpointController.PayjoinUtxoSelectionType.HeuristicBased, result.selectionType);
}
}
private Transaction RandomTransaction(BTCPayNetwork network) private Transaction RandomTransaction(BTCPayNetwork network)
{ {
var tx = network.NBitcoinNetwork.CreateTransaction(); var tx = network.NBitcoinNetwork.CreateTransaction();
@@ -98,6 +138,15 @@ namespace BTCPayServer.Tests
return tx; return tx;
} }
private UTXO FakeUTXO(decimal amount)
{
return new UTXO()
{
Value = new Money(amount, MoneyUnit.BTC),
Outpoint = RandomOutpoint()
};
}
private OutPoint RandomOutpoint() private OutPoint RandomOutpoint()
{ {
return new OutPoint(RandomUtils.GetUInt256(), 0); return new OutPoint(RandomUtils.GetUInt256(), 0);

View File

@@ -275,8 +275,9 @@ namespace BTCPayServer.Payments.PayJoin
var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet(); var prevOuts = originalTx.Inputs.Select(o => o.PrevOut).ToHashSet();
utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray();
Array.Sort(utxos, UTXODeterministicComparer.Instance); Array.Sort(utxos, UTXODeterministicComparer.Instance);
foreach (var utxo in await SelectUTXO(network, utxos, output.Value,
psbt.Outputs.Where(o => o.Index != output.Index).Select(o => o.Value).ToArray())) foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC),
psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO)
{ {
selectedUTXOs.Add(utxo.Outpoint, utxo); selectedUTXOs.Add(utxo.Outpoint, utxo);
} }
@@ -505,41 +506,57 @@ namespace BTCPayServer.Payments.PayJoin
return o; return o;
} }
private async Task<UTXO[]> SelectUTXO(BTCPayNetwork network, UTXO[] availableUtxos, Money paymentAmount, public enum PayjoinUtxoSelectionType
Money[] otherOutputs) {
Unavailable,
HeuristicBased,
Ordered
}
[NonAction]
public async Task<(UTXO[] selectedUTXO, PayjoinUtxoSelectionType selectionType)> SelectUTXO(BTCPayNetwork network, UTXO[] availableUtxos, IEnumerable<decimal> otherInputs, decimal mainPaymentOutput,
IEnumerable<decimal> otherOutputs)
{ {
if (availableUtxos.Length == 0) if (availableUtxos.Length == 0)
return Array.Empty<UTXO>(); return (Array.Empty<UTXO>(), PayjoinUtxoSelectionType.Unavailable);
// Assume the merchant wants to get rid of the dust // Assume the merchant wants to get rid of the dust
HashSet<OutPoint> locked = new HashSet<OutPoint>(); HashSet<OutPoint> locked = new HashSet<OutPoint>();
// We don't want to make too many db roundtrip which would be inconvenient for the sender // We don't want to make too many db roundtrip which would be inconvenient for the sender
int maxTries = 30; int maxTries = 30;
int currentTry = 0; int currentTry = 0;
List<UTXO> utxosByPriority = new List<UTXO>();
// UIH = "unnecessary input heuristic", basically "a wallet wouldn't choose more utxos to spend in this scenario". // UIH = "unnecessary input heuristic", basically "a wallet wouldn't choose more utxos to spend in this scenario".
// //
// "UIH1" : one output is smaller than any input. This heuristically implies that that output is not a payment, and must therefore be a change output. // "UIH1" : one output is smaller than any input. This heuristically implies that that output is not a payment, and must therefore be a change output.
// //
// "UIH2": one input is larger than any output. This heuristically implies that no output is a payment, or, to say it better, it implies that this is not a normal wallet-created payment, it's something strange/exotic. // "UIH2": one input is larger than any output. This heuristically implies that no output is a payment, or, to say it better, it implies that this is not a normal wallet-created payment, it's something strange/exotic.
//src: https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539 //src: https://gist.github.com/AdamISZ/4551b947789d3216bacfcb7af25e029e#gistcomment-2796539
foreach (var availableUtxo in availableUtxos) foreach (var availableUtxo in availableUtxos)
{ {
if (currentTry >= maxTries) if (currentTry >= maxTries)
break; break;
//we can only check against our input as we dont know the value of the rest.
var input = (Money)availableUtxo.Value; var invalid = false;
var paymentAmountSum = input + paymentAmount; foreach (var input in otherInputs.Concat(new[] {availableUtxo.Value.GetValue(network)}))
if (otherOutputs.Concat(new[] {paymentAmountSum}).Any(output => input > output))
{ {
//UIH 1 & 2 var computedOutputs =
continue; otherOutputs.Concat(new[] {mainPaymentOutput + availableUtxo.Value.GetValue(network)});
if (computedOutputs.Any(output => input > output))
{
//UIH 1 & 2
invalid = true;
break;
}
} }
if (invalid)
{
continue;
}
if (await _payJoinRepository.TryLock(availableUtxo.Outpoint)) if (await _payJoinRepository.TryLock(availableUtxo.Outpoint))
{ {
return new UTXO[] { availableUtxo }; return (new[] {availableUtxo}, PayjoinUtxoSelectionType.HeuristicBased);
} }
locked.Add(availableUtxo.Outpoint); locked.Add(availableUtxo.Outpoint);
currentTry++; currentTry++;
} }
@@ -549,11 +566,11 @@ namespace BTCPayServer.Payments.PayJoin
break; break;
if (await _payJoinRepository.TryLock(utxo.Outpoint)) if (await _payJoinRepository.TryLock(utxo.Outpoint))
{ {
return new UTXO[] { utxo }; return (new[] {utxo}, PayjoinUtxoSelectionType.Ordered);
} }
currentTry++; currentTry++;
} }
return Array.Empty<UTXO>(); return (Array.Empty<UTXO>(), PayjoinUtxoSelectionType.Unavailable);
} }
} }
} }